mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-06 14:48:00 -05:00
- 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:
@@ -265,9 +265,10 @@ When connected to Cursor/Windsurf:
|
|||||||
|
|
||||||
- `archon:perform_rag_query` - Search knowledge base
|
- `archon:perform_rag_query` - Search knowledge base
|
||||||
- `archon:search_code_examples` - Find code snippets
|
- `archon:search_code_examples` - Find code snippets
|
||||||
- `archon:manage_project` - Project operations
|
- `archon:create_project`, `archon:list_projects`, `archon:get_project`, `archon:update_project`, `archon:delete_project` - Project operations
|
||||||
- `archon:manage_task` - Task management
|
- `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_available_sources` - List knowledge sources
|
||||||
|
- `archon:get_project_features` - Get project features
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from '
|
|||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
import { ArchonLoadingSpinner } from '../animations/Animations';
|
import { ArchonLoadingSpinner } from '../animations/Animations';
|
||||||
import { DebouncedInput, FeatureInput } from './TaskInputComponents';
|
import { DebouncedInput, FeatureInput, AssigneeTypeaheadInput } from './TaskInputComponents';
|
||||||
import type { Task } from './TaskTableView';
|
import type { Task } from './TaskTableView';
|
||||||
|
|
||||||
interface EditTaskModalProps {
|
interface EditTaskModalProps {
|
||||||
@@ -16,7 +16,15 @@ interface EditTaskModalProps {
|
|||||||
getTasksForPrioritySelection: (status: Task['status']) => Array<{value: number, label: string}>;
|
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
|
// 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);
|
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 ? {
|
setLocalTask(prev => prev ? {
|
||||||
...prev,
|
...prev,
|
||||||
assignee: { name: e.target.value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' }
|
assignee: { name: value, avatar: '' }
|
||||||
} : null);
|
} : null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -167,15 +175,12 @@ export const EditTaskModal = memo(({
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-700 dark:text-gray-300 mb-1">Assignee</label>
|
<label className="block text-gray-700 dark:text-gray-300 mb-1">Assignee</label>
|
||||||
<select
|
<AssigneeTypeaheadInput
|
||||||
value={localTask?.assignee?.name || 'User'}
|
value={localTask?.assignee?.name || 'User'}
|
||||||
onChange={handleAssigneeChange}
|
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"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -169,4 +169,7 @@ export const FeatureInput = memo(({
|
|||||||
prevProps.projectFeatures === nextProps.projectFeatures;
|
prevProps.projectFeatures === nextProps.projectFeatures;
|
||||||
});
|
});
|
||||||
|
|
||||||
FeatureInput.displayName = 'FeatureInput';
|
FeatureInput.displayName = 'FeatureInput';
|
||||||
|
|
||||||
|
// Re-export AssigneeTypeaheadInput for convenience
|
||||||
|
export { AssigneeTypeaheadInput } from './AssigneeTypeaheadInput';
|
||||||
@@ -7,6 +7,7 @@ import { projectService } from '../../services/projectService';
|
|||||||
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
|
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
|
||||||
import { DraggableTaskCard } from './DraggableTaskCard';
|
import { DraggableTaskCard } from './DraggableTaskCard';
|
||||||
import { copyToClipboard } from '../../utils/clipboard';
|
import { copyToClipboard } from '../../utils/clipboard';
|
||||||
|
import { AssigneeTypeaheadInput } from './TaskInputComponents';
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -79,7 +80,7 @@ const reorderTasks = (tasks: Task[], fromIndex: number, toIndex: number): Task[]
|
|||||||
interface EditableCellProps {
|
interface EditableCellProps {
|
||||||
value: string;
|
value: string;
|
||||||
onSave: (value: string) => void;
|
onSave: (value: string) => void;
|
||||||
type?: 'text' | 'textarea' | 'select';
|
type?: 'text' | 'textarea' | 'select' | 'typeahead';
|
||||||
options?: string[];
|
options?: string[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
@@ -140,7 +141,37 @@ const EditableCell = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center w-full">
|
<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
|
<select
|
||||||
value={editValue}
|
value={editValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -341,6 +372,7 @@ const DraggableTaskRow = ({
|
|||||||
<EditableCell
|
<EditableCell
|
||||||
value={task.assignee?.name || 'AI IDE Agent'}
|
value={task.assignee?.name || 'AI IDE Agent'}
|
||||||
onSave={(value) => handleUpdateField('assignee', value || 'AI IDE Agent')}
|
onSave={(value) => handleUpdateField('assignee', value || 'AI IDE Agent')}
|
||||||
|
type="typeahead"
|
||||||
isEditing={editingField === 'assignee'}
|
isEditing={editingField === 'assignee'}
|
||||||
onEdit={() => setEditingField('assignee')}
|
onEdit={() => setEditingField('assignee')}
|
||||||
onCancel={() => setEditingField(null)}
|
onCancel={() => setEditingField(null)}
|
||||||
@@ -513,12 +545,11 @@ const AddTaskRow = ({ onTaskCreate, tasks, statusFilter }: AddTaskRowProps) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
<input
|
<AssigneeTypeaheadInput
|
||||||
type="text"
|
|
||||||
value={newTask.assignee.name}
|
value={newTask.assignee.name}
|
||||||
onChange={(e) => setNewTask(prev => ({
|
onChange={(value) => setNewTask(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
assignee: { name: e.target.value || 'AI IDE Agent', avatar: '' }
|
assignee: { name: value || 'AI IDE Agent', avatar: '' }
|
||||||
}))}
|
}))}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="AI IDE Agent"
|
placeholder="AI IDE Agent"
|
||||||
|
|||||||
@@ -14,8 +14,15 @@ import { TaskTableView, Task } from './TaskTableView';
|
|||||||
import { TaskBoardView } from './TaskBoardView';
|
import { TaskBoardView } from './TaskBoardView';
|
||||||
import { EditTaskModal } from './EditTaskModal';
|
import { EditTaskModal } from './EditTaskModal';
|
||||||
|
|
||||||
// Assignee utilities
|
// Assignee utilities - expanded to include all agent types
|
||||||
const ASSIGNEE_OPTIONS = ['User', 'Archon', 'AI IDE Agent'] as const;
|
const ASSIGNEE_OPTIONS = [
|
||||||
|
'User',
|
||||||
|
'Archon',
|
||||||
|
'AI IDE Agent',
|
||||||
|
'IDE Agent',
|
||||||
|
'prp-executor',
|
||||||
|
'prp-validator'
|
||||||
|
] as const;
|
||||||
|
|
||||||
// Delete confirmation modal component
|
// Delete confirmation modal component
|
||||||
interface DeleteConfirmModalProps {
|
interface DeleteConfirmModalProps {
|
||||||
@@ -140,6 +147,9 @@ export const TasksTab = ({
|
|||||||
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
|
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
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
|
// Track recently deleted tasks to prevent race conditions
|
||||||
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set());
|
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
@@ -164,6 +174,21 @@ export const TasksTab = ({
|
|||||||
return;
|
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
|
// Skip updates while modal is open for the same task to prevent conflicts
|
||||||
if (isModalOpen && editingTask?.id === updatedTask.id) {
|
if (isModalOpen && editingTask?.id === updatedTask.id) {
|
||||||
console.log('[Socket] Skipping update for task being edited:', updatedTask.id);
|
console.log('[Socket] Skipping update for task being edited:', updatedTask.id);
|
||||||
@@ -189,7 +214,7 @@ export const TasksTab = ({
|
|||||||
setTimeout(() => onTasksChange(updated), 0);
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds]);
|
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds, localUpdates]);
|
||||||
|
|
||||||
const handleTaskCreated = useCallback((message: any) => {
|
const handleTaskCreated = useCallback((message: any) => {
|
||||||
const newTask = message.data || message;
|
const newTask = message.data || message;
|
||||||
@@ -534,6 +559,12 @@ export const TasksTab = ({
|
|||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
|
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 {
|
try {
|
||||||
// Then update the backend
|
// Then update the backend
|
||||||
@@ -555,6 +586,13 @@ export const TasksTab = ({
|
|||||||
return updated;
|
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'}`);
|
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -655,6 +693,25 @@ export const TasksTab = ({
|
|||||||
// Inline task update function
|
// Inline task update function
|
||||||
const updateTaskInline = async (taskId: string, updates: Partial<Task>) => {
|
const updateTaskInline = async (taskId: string, updates: Partial<Task>) => {
|
||||||
console.log(`[TasksTab] Inline update for task ${taskId} with updates:`, updates);
|
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 {
|
try {
|
||||||
const updateData: Partial<UpdateTaskRequest> = {};
|
const updateData: Partial<UpdateTaskRequest> = {};
|
||||||
|
|
||||||
@@ -674,11 +731,25 @@ export const TasksTab = ({
|
|||||||
await projectService.updateTask(taskId, updateData);
|
await projectService.updateTask(taskId, updateData);
|
||||||
console.log(`[TasksTab] projectService.updateTask successful for ${taskId}.`);
|
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) {
|
} catch (error) {
|
||||||
console.error(`[TasksTab] Failed to update task ${taskId} inline:`, 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'}`);
|
alert(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ export const IDEGlobalRules = () => {
|
|||||||
|
|
||||||
**MANDATORY: Always complete the full Archon specific task cycle before any coding:**
|
**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()\`
|
2. **Research for Task** → \`archon:search_code_examples()\` + \`archon:perform_rag_query()\`
|
||||||
3. **Implement the Task** → Write code based on research
|
3. **Implement the Task** → Write code based on research
|
||||||
4. **Update Task Status** → \`archon:manage_task(action="update", task_id="...", update_fields={"status": "review"})\`
|
4. **Update Task Status** → \`archon:update_task(task_id="...", status="review")\`
|
||||||
5. **Get Next Task** → \`archon:manage_task(action="list", filter_by="status", filter_value="todo")\`
|
5. **Get Next Task** → \`archon:list_tasks(filter_by="status", filter_value="todo")\`
|
||||||
6. **Repeat Cycle**
|
6. **Repeat Cycle**
|
||||||
|
|
||||||
**NEVER skip task updates with the Archon MCP server. NEVER code without checking current tasks first.**
|
**NEVER skip task updates with the Archon MCP server. NEVER code without checking current tasks first.**
|
||||||
@@ -45,8 +45,7 @@ export const IDEGlobalRules = () => {
|
|||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Create project container
|
# Create project container
|
||||||
archon:manage_project(
|
archon:create_project(
|
||||||
action="create",
|
|
||||||
title="Descriptive Project Name",
|
title="Descriptive Project Name",
|
||||||
github_repo="github.com/user/repo-name"
|
github_repo="github.com/user/repo-name"
|
||||||
)
|
)
|
||||||
@@ -60,7 +59,7 @@ archon:manage_project(
|
|||||||
# First, analyze existing codebase thoroughly
|
# First, analyze existing codebase thoroughly
|
||||||
# Read all major files, understand architecture, identify current state
|
# Read all major files, understand architecture, identify current state
|
||||||
# Then create project container
|
# 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
|
# Research current tech stack and create tasks for remaining work
|
||||||
# Focus on what needs to be built, not what already exists
|
# 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
|
\`\`\`bash
|
||||||
# Check existing project status
|
# 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
|
# Pick up where you left off - no new project creation needed
|
||||||
# Continue with standard development iteration workflow
|
# Continue with standard development iteration workflow
|
||||||
@@ -101,16 +100,14 @@ archon:search_code_examples(query="[specific feature] implementation", match_cou
|
|||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Get current project status
|
# Get current project status
|
||||||
archon:manage_task(
|
archon:list_tasks(
|
||||||
action="list",
|
|
||||||
filter_by="project",
|
filter_by="project",
|
||||||
filter_value="[project_id]",
|
filter_value="[project_id]",
|
||||||
include_closed=false
|
include_closed=false
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get next priority task
|
# Get next priority task
|
||||||
archon:manage_task(
|
archon:list_tasks(
|
||||||
action="list",
|
|
||||||
filter_by="status",
|
filter_by="status",
|
||||||
filter_value="todo",
|
filter_value="todo",
|
||||||
project_id="[project_id]"
|
project_id="[project_id]"
|
||||||
@@ -150,15 +147,14 @@ archon:search_code_examples(
|
|||||||
|
|
||||||
**1. Get Task Details:**
|
**1. Get Task Details:**
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
archon:manage_task(action="get", task_id="[current_task_id]")
|
archon:get_task(task_id="[current_task_id]")
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**2. Update to In-Progress:**
|
**2. Update to In-Progress:**
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
archon:manage_task(
|
archon:update_task(
|
||||||
action="update",
|
|
||||||
task_id="[current_task_id]",
|
task_id="[current_task_id]",
|
||||||
update_fields={"status": "doing"}
|
status="doing"
|
||||||
)
|
)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
@@ -170,10 +166,9 @@ archon:manage_task(
|
|||||||
**4. Complete Task:**
|
**4. Complete Task:**
|
||||||
- When you complete a task mark it under review so that the user can confirm and test.
|
- When you complete a task mark it under review so that the user can confirm and test.
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
archon:manage_task(
|
archon:update_task(
|
||||||
action="update",
|
|
||||||
task_id="[current_task_id]",
|
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:**
|
**Start of each coding session:**
|
||||||
|
|
||||||
1. Check available sources: \`archon:get_available_sources()\`
|
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
|
3. Identify next priority task: Find highest \`task_order\` in "todo" status
|
||||||
4. Conduct task-specific research
|
4. Conduct task-specific research
|
||||||
5. Begin implementation
|
5. Begin implementation
|
||||||
@@ -247,17 +242,15 @@ archon:search_code_examples(query="PostgreSQL connection pooling Node.js", match
|
|||||||
**Status Update Examples:**
|
**Status Update Examples:**
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Move to review when implementation complete but needs testing
|
# Move to review when implementation complete but needs testing
|
||||||
archon:manage_task(
|
archon:update_task(
|
||||||
action="update",
|
|
||||||
task_id="...",
|
task_id="...",
|
||||||
update_fields={"status": "review"}
|
status="review"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Complete task after review passes
|
# Complete task after review passes
|
||||||
archon:manage_task(
|
archon:update_task(
|
||||||
action="update",
|
|
||||||
task_id="...",
|
task_id="...",
|
||||||
update_fields={"status": "done"}
|
status="done"
|
||||||
)
|
)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
@@ -291,8 +284,7 @@ archon:manage_task(
|
|||||||
archon:get_project_features(project_id="...")
|
archon:get_project_features(project_id="...")
|
||||||
|
|
||||||
# Create tasks aligned with features
|
# Create tasks aligned with features
|
||||||
archon:manage_task(
|
archon:create_task(
|
||||||
action="create",
|
|
||||||
project_id="...",
|
project_id="...",
|
||||||
title="...",
|
title="...",
|
||||||
feature="Authentication", # Align with project features
|
feature="Authentication", # Align with project features
|
||||||
|
|||||||
@@ -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 TaskPrioritySchema = z.enum(['low', 'medium', 'high', 'critical']);
|
||||||
export const ProjectColorSchema = z.enum(['cyan', 'purple', 'pink', 'blue', 'orange', 'green']);
|
export const ProjectColorSchema = z.enum(['cyan', 'purple', 'pink', 'blue', 'orange', 'green']);
|
||||||
|
|
||||||
// Assignee schema - simplified to predefined options
|
// Assignee schema - allow any string value (backend no longer restricts this)
|
||||||
export const AssigneeSchema = z.enum(['User', 'Archon', 'AI IDE Agent']);
|
export const AssigneeSchema = z.string();
|
||||||
|
|
||||||
// Project schemas
|
// Project schemas
|
||||||
export const CreateProjectSchema = z.object({
|
export const CreateProjectSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user