diff --git a/CLAUDE.md b/CLAUDE.md index 46688916..0bb3b794 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -265,9 +265,10 @@ When connected to Cursor/Windsurf: - `archon:perform_rag_query` - Search knowledge base - `archon:search_code_examples` - Find code snippets -- `archon:manage_project` - Project operations -- `archon:manage_task` - Task management +- `archon:create_project`, `archon:list_projects`, `archon:get_project`, `archon:update_project`, `archon:delete_project` - Project operations +- `archon:create_task`, `archon:list_tasks`, `archon:get_task`, `archon:update_task`, `archon:delete_task` - Task management - `archon:get_available_sources` - List knowledge sources +- `archon:get_project_features` - Get project features ## Important Notes diff --git a/archon-ui-main/src/components/project-tasks/AssigneeTypeaheadInput.tsx b/archon-ui-main/src/components/project-tasks/AssigneeTypeaheadInput.tsx new file mode 100644 index 00000000..06a2915b --- /dev/null +++ b/archon-ui-main/src/components/project-tasks/AssigneeTypeaheadInput.tsx @@ -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) => 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 = ({ + 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(null); + const dropdownRef = useRef(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) => { + 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) => { + 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) => { + // 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 ( +
+ + + {isOpen && filteredOptions.length > 0 && ( +
+ {filteredOptions.map((option, index) => { + const Icon = option.icon; + const isHighlighted = index === highlightedIndex; + + return ( +
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)} + > + + + {option.value} + + {option.value === inputValue && ( + + current + + )} +
+ ); + })} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx b/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx index 6bb5f718..3883a0e5 100644 --- a/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx +++ b/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx @@ -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) => { + 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(({
- + />
diff --git a/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx b/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx index e1a136e3..095502d4 100644 --- a/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx +++ b/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx @@ -169,4 +169,7 @@ export const FeatureInput = memo(({ prevProps.projectFeatures === nextProps.projectFeatures; }); -FeatureInput.displayName = 'FeatureInput'; \ No newline at end of file +FeatureInput.displayName = 'FeatureInput'; + +// Re-export AssigneeTypeaheadInput for convenience +export { AssigneeTypeaheadInput } from './AssigneeTypeaheadInput'; \ No newline at end of file diff --git a/archon-ui-main/src/components/project-tasks/TaskTableView.tsx b/archon-ui-main/src/components/project-tasks/TaskTableView.tsx index ace50d66..e6afe965 100644 --- a/archon-ui-main/src/components/project-tasks/TaskTableView.tsx +++ b/archon-ui-main/src/components/project-tasks/TaskTableView.tsx @@ -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 (
- {type === 'select' ? ( + {type === 'typeahead' ? ( +
+ { + 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 */} + +
+ ) : type === 'select' ? ( 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" diff --git a/archon-ui-main/src/components/project-tasks/TasksTab.tsx b/archon-ui-main/src/components/project-tasks/TasksTab.tsx index 288ff265..a2918d13 100644 --- a/archon-ui-main/src/components/project-tasks/TasksTab.tsx +++ b/archon-ui-main/src/components/project-tasks/TasksTab.tsx @@ -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(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + // Track local updates to prevent echo from WebSocket + const [localUpdates, setLocalUpdates] = useState>({}); + // Track recently deleted tasks to prevent race conditions const [recentlyDeletedIds, setRecentlyDeletedIds] = useState>(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) => { 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 = {}; @@ -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; } diff --git a/archon-ui-main/src/components/settings/IDEGlobalRules.tsx b/archon-ui-main/src/components/settings/IDEGlobalRules.tsx index 0221e78c..9f7cb3cd 100644 --- a/archon-ui-main/src/components/settings/IDEGlobalRules.tsx +++ b/archon-ui-main/src/components/settings/IDEGlobalRules.tsx @@ -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 diff --git a/archon-ui-main/src/lib/projectSchemas.ts b/archon-ui-main/src/lib/projectSchemas.ts index 85192c8b..7e7fe82e 100644 --- a/archon-ui-main/src/lib/projectSchemas.ts +++ b/archon-ui-main/src/lib/projectSchemas.ts @@ -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({