diff --git a/docs/sprint-7.md b/docs/sprint-7.md index 28836fb6..ceec6180 100644 --- a/docs/sprint-7.md +++ b/docs/sprint-7.md @@ -11,7 +11,7 @@ | ID | Title | Status | Dependencies | Notes | |----|-------|--------|--------------|-------| -| US-701 | Archive sidebar | ⏳ Todo | None | Slide-out viewer with search/filter for archived tasks | +| US-701 | Archive sidebar | ✅ Done | None | Slide-out viewer with search/filter for archived tasks | | US-702 | Restore from archive | ⏳ Todo | US-701 | Unarchive tasks back to board | | US-703 | Subtasks | ⏳ Todo | None | Nested tasks with parent completion logic | | US-704 | Task dependencies | ⏳ Todo | US-703 | Block tasks until dependencies complete | @@ -21,6 +21,7 @@ | US-708 | Preview mode | ⏳ Todo | None | Embedded browser for dev server preview | | US-709 | Merge conflict resolution | ⏳ Todo | None | Visual conflict resolver UI | | US-710 | Time tracking | ⏳ Todo | None | Start/stop timer, manual entry, reports | +| US-711 | Running indicator on cards | ⏳ Todo | None | Spinner/pulse animation when agent running | --- @@ -90,6 +91,11 @@ - Time summary per project - Export time report +### US-711: Running indicator on task cards +- Visual indicator (spinner or pulse) on task card +- Shows when agent is actively running +- Easy to spot active work at a glance from board view + --- ## Progress Log diff --git a/server/.veritas-kanban/activity.json b/server/.veritas-kanban/activity.json index 80a75cf6..b032fe54 100644 --- a/server/.veritas-kanban/activity.json +++ b/server/.veritas-kanban/activity.json @@ -1,4 +1,34 @@ [ + { + "id": "activity_1769427062484_c9s4q6kiq", + "type": "task_created", + "taskId": "task_20260126_PcoH5E", + "taskTitle": "US-711: Running indicator on task cards", + "details": { + "type": "code", + "priority": "medium", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-26T11:31:02.484Z" + }, + { + "id": "activity_1769426843932_va7q38kfq", + "type": "task_updated", + "taskId": "task_20260126_wzj1d0", + "taskTitle": "US-701: Archive sidebar", + "timestamp": "2026-01-26T11:27:23.932Z" + }, + { + "id": "activity_1769426832160_ckd9ynr0s", + "type": "status_changed", + "taskId": "task_20260126_wzj1d0", + "taskTitle": "US-701: Archive sidebar", + "details": { + "from": "todo", + "status": "in-progress" + }, + "timestamp": "2026-01-26T11:27:12.160Z" + }, { "id": "activity_1769426654428_vcko3ka0i", "type": "task_created", diff --git a/server/src/routes/tasks.ts b/server/src/routes/tasks.ts index b2003c6e..1270474f 100644 --- a/server/src/routes/tasks.ts +++ b/server/src/routes/tasks.ts @@ -81,6 +81,17 @@ router.get('/', async (_req, res) => { } }); +// GET /api/tasks/archived - List archived tasks +router.get('/archived', async (_req, res) => { + try { + const tasks = await taskService.listArchivedTasks(); + res.json(tasks); + } catch (error) { + console.error('Error listing archived tasks:', error); + res.status(500).json({ error: 'Failed to list archived tasks' }); + } +}); + // GET /api/tasks/:id - Get single task router.get('/:id', async (req, res) => { try { diff --git a/server/src/services/agent-service.ts b/server/src/services/agent-service.ts index 080e1fff..c65620c7 100644 --- a/server/src/services/agent-service.ts +++ b/server/src/services/agent-service.ts @@ -103,15 +103,10 @@ export class AgentService { // Spawn agent process const worktreePath = this.expandPath(task.git.worktreePath); const args = [...agentConfig.args]; - - // Add prompt for certain agents - if (agent === 'claude-code' || agent === 'amp') { - args.push(prompt); - } const childProcess = spawn(agentConfig.command, args, { cwd: worktreePath, - shell: true, + shell: false, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, @@ -119,6 +114,17 @@ export class AgentService { TERM: 'xterm-256color', }, }); + + // Debug: check if streams exist + console.log(`[Agent ${attemptId}] stdout: ${!!childProcess.stdout}, stderr: ${!!childProcess.stderr}, stdin: ${!!childProcess.stdin}`); + + // Send prompt via stdin for agents that expect it (claude-code, amp) + if (agent === 'claude-code' || agent === 'amp') { + console.log(`[Agent ${attemptId}] Sending prompt via stdin (${prompt.length} chars)`); + childProcess.stdin?.write(prompt); + childProcess.stdin?.end(); + console.log(`[Agent ${attemptId}] stdin closed`); + } // Store in running agents runningAgents.set(taskId, { @@ -136,6 +142,7 @@ export class AgentService { // Handle stdout childProcess.stdout?.on('data', async (data: Buffer) => { const content = data.toString(); + console.log(`[Agent ${attemptId}] stdout:`, content.slice(0, 100)); const output: AgentOutput = { type: 'stdout', content, @@ -148,6 +155,7 @@ export class AgentService { // Handle stderr childProcess.stderr?.on('data', async (data: Buffer) => { const content = data.toString(); + console.log(`[Agent ${attemptId}] stderr:`, content.slice(0, 100)); const output: AgentOutput = { type: 'stderr', content, diff --git a/server/src/services/task-service.ts b/server/src/services/task-service.ts index fb6fd75a..6667d9ed 100644 --- a/server/src/services/task-service.ts +++ b/server/src/services/task-service.ts @@ -203,4 +203,29 @@ export class TaskService { return true; } + + async listArchivedTasks(): Promise { + await this.ensureDirectories(); + + const files = await fs.readdir(this.archiveDir); + const mdFiles = files.filter(f => f.endsWith('.md')); + + const tasks = await Promise.all( + mdFiles.map(async (filename) => { + const filepath = path.join(this.archiveDir, filename); + const content = await fs.readFile(filepath, 'utf-8'); + return this.parseTaskFile(content, filename); + }) + ); + + // Sort by updated date, newest first + return tasks.sort((a: Task, b: Task) => + new Date(b.updated).getTime() - new Date(a.updated).getTime() + ); + } + + async getArchivedTask(id: string): Promise { + const tasks = await this.listArchivedTasks(); + return tasks.find(t => t.id === id) || null; + } } diff --git a/web/src/components/layout/ArchiveSidebar.tsx b/web/src/components/layout/ArchiveSidebar.tsx new file mode 100644 index 00000000..509a5b79 --- /dev/null +++ b/web/src/components/layout/ArchiveSidebar.tsx @@ -0,0 +1,236 @@ +import { useState, useMemo } from 'react'; +import { Archive, RefreshCw, Search, Calendar, FolderOpen } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Badge } from '@/components/ui/badge'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useArchivedTasks } from '@/hooks/useTasks'; +import { cn } from '@/lib/utils'; +import type { Task, TaskType } from '@veritas-kanban/shared'; + +interface ArchiveSidebarProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onTaskClick?: (task: Task) => void; +} + +const typeIcons: Record = { + code: '💻', + research: '🔬', + content: '📝', + automation: '🤖', +}; + +const typeLabels: Record = { + code: 'Code', + research: 'Research', + content: 'Content', + automation: 'Automation', +}; + +function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined, + }); +} + +function ArchivedTaskItem({ task, onClick }: { task: Task; onClick?: () => void }) { + return ( +
+ + {typeIcons[task.type]} + +
+
+ {task.title} +
+
+ {task.project && ( + + + {task.project} + + )} + + + {formatDate(task.updated)} + +
+ {task.tags && task.tags.length > 0 && ( +
+ {task.tags.slice(0, 3).map(tag => ( + + {tag} + + ))} + {task.tags.length > 3 && ( + + +{task.tags.length - 3} + + )} +
+ )} +
+
+ ); +} + +export function ArchiveSidebar({ open, onOpenChange, onTaskClick }: ArchiveSidebarProps) { + const [search, setSearch] = useState(''); + const [typeFilter, setTypeFilter] = useState('all'); + const [projectFilter, setProjectFilter] = useState('all'); + + const { data: archivedTasks, isLoading, refetch, isRefetching } = useArchivedTasks(); + + // Get unique projects for filter dropdown + const projects = useMemo(() => { + if (!archivedTasks) return []; + const projectSet = new Set(archivedTasks.map(t => t.project).filter(Boolean) as string[]); + return Array.from(projectSet).sort(); + }, [archivedTasks]); + + // Filter tasks + const filteredTasks = useMemo(() => { + if (!archivedTasks) return []; + + return archivedTasks.filter(task => { + // Search filter + if (search) { + const searchLower = search.toLowerCase(); + const matchesTitle = task.title.toLowerCase().includes(searchLower); + const matchesDescription = task.description?.toLowerCase().includes(searchLower); + const matchesTags = task.tags?.some(t => t.toLowerCase().includes(searchLower)); + if (!matchesTitle && !matchesDescription && !matchesTags) return false; + } + + // Type filter + if (typeFilter !== 'all' && task.type !== typeFilter) return false; + + // Project filter + if (projectFilter !== 'all' && task.project !== projectFilter) return false; + + return true; + }); + }, [archivedTasks, search, typeFilter, projectFilter]); + + return ( + + + +
+ + + Archive + {archivedTasks && archivedTasks.length > 0 && ( + + {archivedTasks.length} + + )} + + +
+ + {/* Search */} +
+ + setSearch(e.target.value)} + className="pl-8" + /> +
+ + {/* Filters */} +
+ + + {projects.length > 0 && ( + + )} +
+
+ + +
+ {isLoading ? ( +
+ Loading archived tasks... +
+ ) : filteredTasks.length === 0 ? ( +
+ {archivedTasks?.length === 0 + ? 'No archived tasks yet' + : 'No tasks match your filters'} +
+ ) : ( +
+ {filteredTasks.map(task => ( + onTaskClick(task) : undefined} + /> + ))} +
+ )} +
+
+
+
+ ); +} diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 937749f1..520b9092 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -1,8 +1,9 @@ -import { Plus, Settings, Keyboard, Activity } from 'lucide-react'; +import { Plus, Settings, Keyboard, Activity, Archive } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { CreateTaskDialog } from '@/components/task/CreateTaskDialog'; import { SettingsDialog } from '@/components/settings/SettingsDialog'; import { ActivitySidebar } from './ActivitySidebar'; +import { ArchiveSidebar } from './ArchiveSidebar'; import { useState } from 'react'; import { useKeyboard } from '@/hooks/useKeyboard'; @@ -10,6 +11,7 @@ export function Header() { const [createOpen, setCreateOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [activityOpen, setActivityOpen] = useState(false); + const [archiveOpen, setArchiveOpen] = useState(false); const { setOpenCreateDialog, openHelpDialog } = useKeyboard(); // Register the create dialog opener with keyboard context (ref, no useEffect needed) @@ -51,6 +53,14 @@ export function Header() { > +