- Update the Claude and other files based on the changes to the MCP Server.

- Implement Assignment type ahead, allow freeform assignee for flexibility.
This commit is contained in:
sean-eskerium
2025-08-21 00:29:36 -04:00
parent 703f2bca7c
commit 97a280461a
8 changed files with 371 additions and 55 deletions

View File

@@ -0,0 +1,213 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { User, Bot, Code, Shield, CheckCircle } from 'lucide-react';
interface AssigneeTypeaheadInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
onKeyPress?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
autoFocus?: boolean;
}
// Default assignee options with icons
const DEFAULT_ASSIGNEES = [
{ value: 'User', icon: User, color: 'text-blue-500' },
{ value: 'Archon', icon: Bot, color: 'text-pink-500' },
{ value: 'AI IDE Agent', icon: Code, color: 'text-emerald-500' },
{ value: 'IDE Agent', icon: Code, color: 'text-emerald-500' },
{ value: 'prp-executor', icon: Shield, color: 'text-purple-500' },
{ value: 'prp-validator', icon: CheckCircle, color: 'text-cyan-500' }
];
export const AssigneeTypeaheadInput: React.FC<AssigneeTypeaheadInputProps> = ({
value,
onChange,
placeholder = 'Type or select assignee...',
className = '',
onKeyPress,
autoFocus = false
}) => {
const [inputValue, setInputValue] = useState(value);
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(0);
const [filteredOptions, setFilteredOptions] = useState(DEFAULT_ASSIGNEES);
const inputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
// Update input value when prop changes
useEffect(() => {
setInputValue(value);
}, [value]);
// Filter options based on input
useEffect(() => {
const filtered = inputValue.trim() === ''
? DEFAULT_ASSIGNEES
: DEFAULT_ASSIGNEES.filter(option =>
option.value.toLowerCase().includes(inputValue.toLowerCase())
);
// Add current input as an option if it's not in the default list and not empty
if (inputValue.trim() && !DEFAULT_ASSIGNEES.find(opt => opt.value.toLowerCase() === inputValue.toLowerCase())) {
filtered.push({
value: inputValue,
icon: User,
color: 'text-gray-500'
});
}
setFilteredOptions(filtered);
setHighlightedIndex(0);
}, [inputValue]);
// Handle clicking outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
inputRef.current &&
!inputRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInputValue(newValue);
setIsOpen(true);
};
const handleInputFocus = () => {
setIsOpen(true);
};
const handleInputBlur = () => {
// Delay to allow click on dropdown item
setTimeout(() => {
// Only trigger onChange if the value actually changed
if (inputValue !== value) {
onChange(inputValue);
}
setIsOpen(false);
}, 200);
};
const selectOption = useCallback((optionValue: string) => {
setInputValue(optionValue);
onChange(optionValue);
setIsOpen(false);
inputRef.current?.focus();
}, [onChange]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!isOpen && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
setIsOpen(true);
e.preventDefault();
return;
}
if (!isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setHighlightedIndex(prev =>
prev < filteredOptions.length - 1 ? prev + 1 : 0
);
break;
case 'ArrowUp':
e.preventDefault();
setHighlightedIndex(prev =>
prev > 0 ? prev - 1 : filteredOptions.length - 1
);
break;
case 'Enter':
e.preventDefault();
if (filteredOptions[highlightedIndex]) {
selectOption(filteredOptions[highlightedIndex].value);
}
break;
case 'Escape':
e.preventDefault();
setIsOpen(false);
break;
case 'Tab':
if (filteredOptions[highlightedIndex]) {
selectOption(filteredOptions[highlightedIndex].value);
}
break;
}
};
const handleKeyPressWrapper = (e: React.KeyboardEvent<HTMLInputElement>) => {
// Don't trigger the parent's Enter handler if dropdown is open
if (e.key === 'Enter' && isOpen && filteredOptions.length > 0) {
e.preventDefault();
e.stopPropagation();
return;
}
onKeyPress?.(e);
};
return (
<div className="relative">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPressWrapper}
placeholder={placeholder}
className={className}
autoFocus={autoFocus}
/>
{isOpen && filteredOptions.length > 0 && (
<div
ref={dropdownRef}
className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg max-h-60 overflow-auto"
>
{filteredOptions.map((option, index) => {
const Icon = option.icon;
const isHighlighted = index === highlightedIndex;
return (
<div
key={option.value}
onClick={() => selectOption(option.value)}
className={`
flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors
${isHighlighted
? 'bg-cyan-100 dark:bg-cyan-900/30'
: 'hover:bg-gray-100 dark:hover:bg-gray-800'
}
`}
onMouseEnter={() => setHighlightedIndex(index)}
>
<Icon className={`w-4 h-4 ${option.color}`} />
<span className="text-sm text-gray-700 dark:text-gray-300">
{option.value}
</span>
{option.value === inputValue && (
<span className="ml-auto text-xs text-gray-500 dark:text-gray-400">
current
</span>
)}
</div>
);
})}
</div>
)}
</div>
);
};

View File

@@ -2,7 +2,7 @@ import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from '
import { X } from 'lucide-react';
import { Button } from '../ui/Button';
import { ArchonLoadingSpinner } from '../animations/Animations';
import { DebouncedInput, FeatureInput } from './TaskInputComponents';
import { DebouncedInput, FeatureInput, AssigneeTypeaheadInput } from './TaskInputComponents';
import type { Task } from './TaskTableView';
interface EditTaskModalProps {
@@ -16,7 +16,15 @@ interface EditTaskModalProps {
getTasksForPrioritySelection: (status: Task['status']) => Array<{value: number, label: string}>;
}
const ASSIGNEE_OPTIONS = ['User', 'Archon', 'AI IDE Agent'] as const;
// Assignee options - expanded to include all agent types
const ASSIGNEE_OPTIONS = [
'User',
'Archon',
'AI IDE Agent',
'IDE Agent',
'prp-executor',
'prp-validator'
] as const;
// Removed debounce utility - now using DebouncedInput component
@@ -82,10 +90,10 @@ export const EditTaskModal = memo(({
setLocalTask(prev => prev ? { ...prev, task_order: parseInt(e.target.value) } : null);
}, []);
const handleAssigneeChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
const handleAssigneeChange = useCallback((value: string) => {
setLocalTask(prev => prev ? {
...prev,
assignee: { name: e.target.value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' }
assignee: { name: value, avatar: '' }
} : null);
}, []);
@@ -167,15 +175,12 @@ export const EditTaskModal = memo(({
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-gray-700 dark:text-gray-300 mb-1">Assignee</label>
<select
value={localTask?.assignee?.name || 'User'}
<AssigneeTypeaheadInput
value={localTask?.assignee?.name || 'User'}
onChange={handleAssigneeChange}
placeholder="Type or select assignee..."
className="w-full bg-white/50 dark:bg-black/70 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-white rounded-md py-2 px-3 focus:outline-none focus:border-cyan-400 focus:shadow-[0_0_10px_rgba(34,211,238,0.2)] transition-all duration-300"
>
{ASSIGNEE_OPTIONS.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
/>
</div>
<div>

View File

@@ -169,4 +169,7 @@ export const FeatureInput = memo(({
prevProps.projectFeatures === nextProps.projectFeatures;
});
FeatureInput.displayName = 'FeatureInput';
FeatureInput.displayName = 'FeatureInput';
// Re-export AssigneeTypeaheadInput for convenience
export { AssigneeTypeaheadInput } from './AssigneeTypeaheadInput';

View File

@@ -7,6 +7,7 @@ import { projectService } from '../../services/projectService';
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
import { DraggableTaskCard } from './DraggableTaskCard';
import { copyToClipboard } from '../../utils/clipboard';
import { AssigneeTypeaheadInput } from './TaskInputComponents';
export interface Task {
id: string;
@@ -79,7 +80,7 @@ const reorderTasks = (tasks: Task[], fromIndex: number, toIndex: number): Task[]
interface EditableCellProps {
value: string;
onSave: (value: string) => void;
type?: 'text' | 'textarea' | 'select';
type?: 'text' | 'textarea' | 'select' | 'typeahead';
options?: string[];
placeholder?: string;
isEditing: boolean;
@@ -140,7 +141,37 @@ const EditableCell = ({
return (
<div className="flex items-center w-full">
{type === 'select' ? (
{type === 'typeahead' ? (
<div className="relative">
<AssigneeTypeaheadInput
value={editValue}
onChange={(value) => {
setEditValue(value);
// Update the value but don't auto-save yet
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSave();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancel();
}
}}
placeholder={placeholder}
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
autoFocus
/>
{/* Save button for explicit save */}
<button
onClick={handleSave}
className="absolute right-0 top-0 h-full px-2 text-cyan-600 hover:text-cyan-700 dark:text-cyan-400 dark:hover:text-cyan-300"
title="Save (Enter)"
>
</button>
</div>
) : type === 'select' ? (
<select
value={editValue}
onChange={(e) => {
@@ -341,6 +372,7 @@ const DraggableTaskRow = ({
<EditableCell
value={task.assignee?.name || 'AI IDE Agent'}
onSave={(value) => handleUpdateField('assignee', value || 'AI IDE Agent')}
type="typeahead"
isEditing={editingField === 'assignee'}
onEdit={() => setEditingField('assignee')}
onCancel={() => setEditingField(null)}
@@ -513,12 +545,11 @@ const AddTaskRow = ({ onTaskCreate, tasks, statusFilter }: AddTaskRowProps) => {
/>
</td>
<td className="p-3">
<input
type="text"
<AssigneeTypeaheadInput
value={newTask.assignee.name}
onChange={(e) => setNewTask(prev => ({
onChange={(value) => setNewTask(prev => ({
...prev,
assignee: { name: e.target.value || 'AI IDE Agent', avatar: '' }
assignee: { name: value || 'AI IDE Agent', avatar: '' }
}))}
onKeyPress={handleKeyPress}
placeholder="AI IDE Agent"

View File

@@ -14,8 +14,15 @@ import { TaskTableView, Task } from './TaskTableView';
import { TaskBoardView } from './TaskBoardView';
import { EditTaskModal } from './EditTaskModal';
// Assignee utilities
const ASSIGNEE_OPTIONS = ['User', 'Archon', 'AI IDE Agent'] as const;
// Assignee utilities - expanded to include all agent types
const ASSIGNEE_OPTIONS = [
'User',
'Archon',
'AI IDE Agent',
'IDE Agent',
'prp-executor',
'prp-validator'
] as const;
// Delete confirmation modal component
interface DeleteConfirmModalProps {
@@ -140,6 +147,9 @@ export const TasksTab = ({
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// Track local updates to prevent echo from WebSocket
const [localUpdates, setLocalUpdates] = useState<Record<string, number>>({});
// Track recently deleted tasks to prevent race conditions
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set());
@@ -164,6 +174,21 @@ export const TasksTab = ({
return;
}
// Check if this is an echo of a local update
const localUpdateTime = localUpdates[updatedTask.id];
if (localUpdateTime && Date.now() - localUpdateTime < 2000) {
console.log('[Socket] Skipping echo update for locally updated task:', updatedTask.id);
// Clean up the local update marker after the echo protection window
setTimeout(() => {
setLocalUpdates(prev => {
const newUpdates = { ...prev };
delete newUpdates[updatedTask.id];
return newUpdates;
});
}, 2000);
return;
}
// Skip updates while modal is open for the same task to prevent conflicts
if (isModalOpen && editingTask?.id === updatedTask.id) {
console.log('[Socket] Skipping update for task being edited:', updatedTask.id);
@@ -189,7 +214,7 @@ export const TasksTab = ({
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds]);
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds, localUpdates]);
const handleTaskCreated = useCallback((message: any) => {
const newTask = message.data || message;
@@ -534,6 +559,12 @@ export const TasksTab = ({
return updated;
});
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
// Mark this update as local to prevent echo when socket update arrives
setLocalUpdates(prev => ({
...prev,
[taskId]: Date.now()
}));
try {
// Then update the backend
@@ -555,6 +586,13 @@ export const TasksTab = ({
return updated;
});
// Clear the local update marker
setLocalUpdates(prev => {
const newUpdates = { ...prev };
delete newUpdates[taskId];
return newUpdates;
});
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
@@ -655,6 +693,25 @@ export const TasksTab = ({
// Inline task update function
const updateTaskInline = async (taskId: string, updates: Partial<Task>) => {
console.log(`[TasksTab] Inline update for task ${taskId} with updates:`, updates);
// Store the original task for potential rollback
const originalTask = tasks.find(t => t.id === taskId);
// Optimistically update the UI immediately
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === taskId
? { ...task, ...updates }
: task
)
);
// Mark this update as local to prevent echo when socket update arrives
setLocalUpdates(prev => ({
...prev,
[taskId]: Date.now()
}));
try {
const updateData: Partial<UpdateTaskRequest> = {};
@@ -674,11 +731,25 @@ export const TasksTab = ({
await projectService.updateTask(taskId, updateData);
console.log(`[TasksTab] projectService.updateTask successful for ${taskId}.`);
// Don't update local state optimistically - let socket handle it
console.log(`[TasksTab] Waiting for socket update for task ${taskId}.`);
} catch (error) {
console.error(`[TasksTab] Failed to update task ${taskId} inline:`, error);
// Revert the optimistic update on error
if (originalTask) {
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === taskId ? originalTask : task
)
);
}
// Clear the local update marker
setLocalUpdates(prev => {
const newUpdates = { ...prev };
delete newUpdates[taskId];
return newUpdates;
});
alert(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error;
}

View File

@@ -30,11 +30,11 @@ export const IDEGlobalRules = () => {
**MANDATORY: Always complete the full Archon specific task cycle before any coding:**
1. **Check Current Task** → \`archon:manage_task(action="get", task_id="...")\`
1. **Check Current Task** → \`archon:get_task(task_id="...")\`
2. **Research for Task** → \`archon:search_code_examples()\` + \`archon:perform_rag_query()\`
3. **Implement the Task** → Write code based on research
4. **Update Task Status** → \`archon:manage_task(action="update", task_id="...", update_fields={"status": "review"})\`
5. **Get Next Task** → \`archon:manage_task(action="list", filter_by="status", filter_value="todo")\`
4. **Update Task Status** → \`archon:update_task(task_id="...", status="review")\`
5. **Get Next Task** → \`archon:list_tasks(filter_by="status", filter_value="todo")\`
6. **Repeat Cycle**
**NEVER skip task updates with the Archon MCP server. NEVER code without checking current tasks first.**
@@ -45,8 +45,7 @@ export const IDEGlobalRules = () => {
\`\`\`bash
# Create project container
archon:manage_project(
action="create",
archon:create_project(
title="Descriptive Project Name",
github_repo="github.com/user/repo-name"
)
@@ -60,7 +59,7 @@ archon:manage_project(
# First, analyze existing codebase thoroughly
# Read all major files, understand architecture, identify current state
# Then create project container
archon:manage_project(action="create", title="Existing Project Name")
archon:create_project(title="Existing Project Name")
# Research current tech stack and create tasks for remaining work
# Focus on what needs to be built, not what already exists
@@ -70,7 +69,7 @@ archon:manage_project(action="create", title="Existing Project Name")
\`\`\`bash
# Check existing project status
archon:manage_task(action="list", filter_by="project", filter_value="[project_id]")
archon:list_tasks(filter_by="project", filter_value="[project_id]")
# Pick up where you left off - no new project creation needed
# Continue with standard development iteration workflow
@@ -101,16 +100,14 @@ archon:search_code_examples(query="[specific feature] implementation", match_cou
\`\`\`bash
# Get current project status
archon:manage_task(
action="list",
archon:list_tasks(
filter_by="project",
filter_value="[project_id]",
include_closed=false
)
# Get next priority task
archon:manage_task(
action="list",
archon:list_tasks(
filter_by="status",
filter_value="todo",
project_id="[project_id]"
@@ -150,15 +147,14 @@ archon:search_code_examples(
**1. Get Task Details:**
\`\`\`bash
archon:manage_task(action="get", task_id="[current_task_id]")
archon:get_task(task_id="[current_task_id]")
\`\`\`
**2. Update to In-Progress:**
\`\`\`bash
archon:manage_task(
action="update",
archon:update_task(
task_id="[current_task_id]",
update_fields={"status": "doing"}
status="doing"
)
\`\`\`
@@ -170,10 +166,9 @@ archon:manage_task(
**4. Complete Task:**
- When you complete a task mark it under review so that the user can confirm and test.
\`\`\`bash
archon:manage_task(
action="update",
archon:update_task(
task_id="[current_task_id]",
update_fields={"status": "review"}
status="review"
)
\`\`\`
@@ -225,7 +220,7 @@ archon:search_code_examples(query="PostgreSQL connection pooling Node.js", match
**Start of each coding session:**
1. Check available sources: \`archon:get_available_sources()\`
2. Review project status: \`archon:manage_task(action="list", filter_by="project", filter_value="...")\`
2. Review project status: \`archon:list_tasks(filter_by="project", filter_value="...")\`
3. Identify next priority task: Find highest \`task_order\` in "todo" status
4. Conduct task-specific research
5. Begin implementation
@@ -247,17 +242,15 @@ archon:search_code_examples(query="PostgreSQL connection pooling Node.js", match
**Status Update Examples:**
\`\`\`bash
# Move to review when implementation complete but needs testing
archon:manage_task(
action="update",
archon:update_task(
task_id="...",
update_fields={"status": "review"}
status="review"
)
# Complete task after review passes
archon:manage_task(
action="update",
archon:update_task(
task_id="...",
update_fields={"status": "done"}
status="done"
)
\`\`\`
@@ -291,8 +284,7 @@ archon:manage_task(
archon:get_project_features(project_id="...")
# Create tasks aligned with features
archon:manage_task(
action="create",
archon:create_task(
project_id="...",
title="...",
feature="Authentication", # Align with project features

View File

@@ -6,8 +6,8 @@ export const UITaskStatusSchema = z.enum(['backlog', 'in-progress', 'review', 'c
export const TaskPrioritySchema = z.enum(['low', 'medium', 'high', 'critical']);
export const ProjectColorSchema = z.enum(['cyan', 'purple', 'pink', 'blue', 'orange', 'green']);
// Assignee schema - simplified to predefined options
export const AssigneeSchema = z.enum(['User', 'Archon', 'AI IDE Agent']);
// Assignee schema - allow any string value (backend no longer restricts this)
export const AssigneeSchema = z.string();
// Project schemas
export const CreateProjectSchema = z.object({