diff --git a/apps/backend/agents/session.py b/apps/backend/agents/session.py index 263bf17efb..c821143d50 100644 --- a/apps/backend/agents/session.py +++ b/apps/backend/agents/session.py @@ -395,20 +395,18 @@ async def run_agent_session( # Extract meaningful tool input for display if inp: - if "pattern" in inp: - tool_input_display = f"pattern: {inp['pattern']}" - elif "file_path" in inp: - fp = inp["file_path"] - if len(fp) > 50: - fp = "..." + fp[-47:] - tool_input_display = fp + if "file_path" in inp: + tool_input_display = inp["file_path"] elif "command" in inp: - cmd = inp["command"] - if len(cmd) > 50: - cmd = cmd[:47] + "..." - tool_input_display = cmd + tool_input_display = inp["command"] + elif "pattern" in inp: + tool_input_display = f"/{inp['pattern']}/" elif "path" in inp: tool_input_display = inp["path"] + elif "url" in inp: + tool_input_display = inp["url"] + elif "query" in inp: + tool_input_display = f'"{inp["query"]}"' debug( "session", diff --git a/apps/backend/qa/fixer.py b/apps/backend/qa/fixer.py index 163d27a46b..25e6d2ad57 100644 --- a/apps/backend/qa/fixer.py +++ b/apps/backend/qa/fixer.py @@ -165,14 +165,34 @@ async def run_qa_fixer_session( if inp: if "file_path" in inp: fp = inp["file_path"] - if len(fp) > 50: - fp = "..." + fp[-47:] + if len(fp) > 80: + fp = "..." + fp[-77:] tool_input_display = fp elif "command" in inp: cmd = inp["command"] - if len(cmd) > 50: - cmd = cmd[:47] + "..." + if len(cmd) > 80: + cmd = cmd[:77] + "..." tool_input_display = cmd + elif "pattern" in inp: + pat = inp["pattern"] + if len(pat) > 60: + pat = pat[:57] + "..." + tool_input_display = f"/{pat}/" + elif "path" in inp: + p = inp["path"] + if len(p) > 80: + p = "..." + p[-77:] + tool_input_display = p + elif "url" in inp: + url = inp["url"] + if len(url) > 80: + url = url[:77] + "..." + tool_input_display = url + elif "query" in inp: + q = inp["query"] + if len(q) > 60: + q = q[:57] + "..." + tool_input_display = f'"{q}"' debug( "qa_fixer", diff --git a/apps/backend/qa/reviewer.py b/apps/backend/qa/reviewer.py index a73e3e71af..1969ff593b 100644 --- a/apps/backend/qa/reviewer.py +++ b/apps/backend/qa/reviewer.py @@ -225,12 +225,17 @@ async def run_qa_agent_session( # Extract tool input for display if inp: if "file_path" in inp: - fp = inp["file_path"] - if len(fp) > 50: - fp = "..." + fp[-47:] - tool_input_display = fp + tool_input_display = inp["file_path"] + elif "command" in inp: + tool_input_display = inp["command"] elif "pattern" in inp: - tool_input_display = f"pattern: {inp['pattern']}" + tool_input_display = f"/{inp['pattern']}/" + elif "path" in inp: + tool_input_display = inp["path"] + elif "url" in inp: + tool_input_display = inp["url"] + elif "query" in inp: + tool_input_display = f'"{inp["query"]}"' debug( "qa_reviewer", diff --git a/apps/backend/task_logger/logger.py b/apps/backend/task_logger/logger.py index 954814464c..c1bff59552 100644 --- a/apps/backend/task_logger/logger.py +++ b/apps/backend/task_logger/logger.py @@ -438,7 +438,11 @@ def tool_start( ) if print_to_console: - print(f"\n[Tool: {tool_name}]", flush=True) + # Include input for live status display + if display_input: + print(f"\n[Tool: {tool_name}] {display_input}", flush=True) + else: + print(f"\n[Tool: {tool_name}]", flush=True) def tool_end( self, diff --git a/apps/frontend/src/main/agent/agent-events.ts b/apps/frontend/src/main/agent/agent-events.ts index 99dd9d6b9f..3c1b6e5868 100644 --- a/apps/frontend/src/main/agent/agent-events.ts +++ b/apps/frontend/src/main/agent/agent-events.ts @@ -117,6 +117,34 @@ export class AgentEvents { return { phase: 'failed', message: log.trim().substring(0, 200) }; } + + // Live tool action detection - update message without changing phase + // Works in all phases to show real-time tool activity + const toolMatch = log.match(/\[(?:Tool|Fixer Tool|QA Tool):\s*(\w+)\]\s*(.*)?/i); + if (toolMatch) { + const toolName = toolMatch[1]; + const details = toolMatch[2]?.trim().replace(/\r$/, '') || ''; + + // Build message with action verb and optional details + const toolVerbs: Record = { + 'Read': 'Reading', + 'Edit': 'Editing', + 'Write': 'Writing', + 'Glob': 'Searching files', + 'Grep': 'Searching', + 'Bash': 'Running', + 'Task': 'Running subagent', + 'WebFetch': 'Fetching', + 'WebSearch': 'Searching web' + }; + const verb = toolVerbs[toolName] || ('Using ' + toolName); + + + + const message = details ? (verb + ' ' + details) : (verb + '...'); + return { phase: currentPhase, message }; + } + return null; } diff --git a/apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts b/apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts index 3cb23d30d7..c87e895d2d 100644 --- a/apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts @@ -282,6 +282,7 @@ export function registerAgenteventsHandlers( taskProjectId ); + const phaseToStatus: Record = { idle: null, planning: "in_progress", diff --git a/apps/frontend/src/renderer/components/TaskCard.tsx b/apps/frontend/src/renderer/components/TaskCard.tsx index 5413b4530b..ec290e86c0 100644 --- a/apps/frontend/src/renderer/components/TaskCard.tsx +++ b/apps/frontend/src/renderer/components/TaskCard.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, GitPullRequest, MoreVertical } from 'lucide-react'; +import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, GitPullRequest, MoreVertical, FileText, Search, FolderSearch, Terminal, Pencil, Globe } from 'lucide-react'; import { Card, CardContent } from './ui/card'; import { Badge } from './ui/badge'; import { Button } from './ui/button'; @@ -44,6 +44,50 @@ const CategoryIcon: Record = { testing: FileCode }; +// Data-driven tool styles for live action status +// Order matters: more specific patterns must come before general ones (first match wins) +const TOOL_STYLES: Array<{ + patterns: string[]; + icon: typeof FileText; + color: string; + translationKey: string; +}> = [ + { patterns: ['reading'], icon: FileText, color: 'text-blue-500 bg-blue-500/10', translationKey: 'toolActions.reading' }, + { patterns: ['searching files', 'globbing'], icon: FolderSearch, color: 'text-amber-500 bg-amber-500/10', translationKey: 'toolActions.searchingFiles' }, + { patterns: ['searching web'], icon: Globe, color: 'text-indigo-500 bg-indigo-500/10', translationKey: 'toolActions.searchingWeb' }, + { patterns: ['searching'], icon: Search, color: 'text-green-500 bg-green-500/10', translationKey: 'toolActions.searching' }, + { patterns: ['fetching'], icon: Globe, color: 'text-indigo-500 bg-indigo-500/10', translationKey: 'toolActions.fetching' }, + { patterns: ['editing'], icon: Pencil, color: 'text-purple-500 bg-purple-500/10', translationKey: 'toolActions.editing' }, + { patterns: ['writing'], icon: FileCode, color: 'text-cyan-500 bg-cyan-500/10', translationKey: 'toolActions.writing' }, + { patterns: ['running', 'executing'], icon: Terminal, color: 'text-orange-500 bg-orange-500/10', translationKey: 'toolActions.running' }, + { patterns: ['using'], icon: Wrench, color: 'text-slate-500 bg-slate-500/10', translationKey: 'toolActions.using' }, +]; + +// Helper to detect tool type from execution message and return styling with translation key +function getToolStyleFromMessage(message: string): { + icon: typeof FileText; + color: string; + translationKey: string; + details: string; +} | null { + const lowerMessage = message.toLowerCase(); + const match = TOOL_STYLES.find(style => + style.patterns.some(pattern => lowerMessage.startsWith(pattern)) + ); + if (!match) return null; + + // Extract details by finding which pattern matched and removing it + const matchedPattern = match.patterns.find(p => lowerMessage.startsWith(p)) || ''; + const details = message.slice(matchedPattern.length).trim() || '...'; + + return { + icon: match.icon, + color: match.color, + translationKey: match.translationKey, + details + }; +} + interface TaskCardProps { task: Task; onClick: () => void; @@ -70,6 +114,7 @@ function taskCardPropsAreEqual(prevProps: TaskCardProps, nextProps: TaskCardProp prevTask.reviewReason === nextTask.reviewReason && prevTask.executionProgress?.phase === nextTask.executionProgress?.phase && prevTask.executionProgress?.phaseProgress === nextTask.executionProgress?.phaseProgress && + prevTask.executionProgress?.message === nextTask.executionProgress?.message && prevTask.subtasks.length === nextTask.subtasks.length && prevTask.metadata?.category === nextTask.metadata?.category && prevTask.metadata?.complexity === nextTask.metadata?.complexity && @@ -104,7 +149,7 @@ export const TaskCard = memo(function TaskCard({ task, onClick, onStatusChange } interval: null }); - const isRunning = task.status === 'in_progress'; + const isRunning = task.status === 'in_progress' || task.status === 'ai_review'; const executionPhase = task.executionProgress?.phase; const hasActiveExecution = executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed'; @@ -137,6 +182,22 @@ export const TaskCard = memo(function TaskCard({ task, onClick, onStatusChange } )); }, [task.status, onStatusChange, t]); + // Memoize live action status to avoid recreating on every render + const liveActionStatus = useMemo(() => { + const message = task.executionProgress?.message; + const phase = task.executionProgress?.phase; + // Don't show live status for terminal phases (complete/failed) + if (!message || phase === 'complete' || phase === 'failed') return null; + const toolStyle = getToolStyleFromMessage(message); + return { + icon: toolStyle?.icon ?? Loader2, + colorClass: toolStyle?.color ?? 'text-muted-foreground bg-muted/50', + translationKey: toolStyle?.translationKey ?? 'toolActions.default', + details: toolStyle?.details ?? message, + message, // Keep original for tooltip + }; + }, [task.executionProgress?.message, task.executionProgress?.phase]); + // Memoized stuck check function to avoid recreating on every render const performStuckCheck = useCallback(() => { // IMPORTANT: If the execution phase is 'complete' or 'failed', the task is NOT stuck. @@ -480,6 +541,19 @@ export const TaskCard = memo(function TaskCard({ task, onClick, onStatusChange } )} + {/* Live action status - shows current tool activity */} + {isRunning && !isStuck && liveActionStatus && ( +
+ + + {t(liveActionStatus.translationKey, { details: liveActionStatus.details })} + +
+ )} + {/* Footer */}
diff --git a/apps/frontend/src/renderer/hooks/useIpc.ts b/apps/frontend/src/renderer/hooks/useIpc.ts index daba76573f..99f1bf1e37 100644 --- a/apps/frontend/src/renderer/hooks/useIpc.ts +++ b/apps/frontend/src/renderer/hooks/useIpc.ts @@ -118,6 +118,16 @@ function queueUpdate(taskId: string, update: BatchedUpdate): void { } } + // Live tool action messages bypass batching - apply immediately for real-time feedback + // These are short-lived status updates (Reading file..., Editing..., etc.) + if (update.progress?.message && storeActionsRef) { + const isToolActionMessage = /^(Reading|Editing|Writing|Searching|Running|Using)/.test(update.progress.message); + if (isToolActionMessage) { + storeActionsRef.updateExecutionProgress(taskId, update.progress); + return; + } + } + // For logs, accumulate rather than replace let mergedLogs = existing.logs; if (update.logs) { diff --git a/apps/frontend/src/renderer/stores/task-store.ts b/apps/frontend/src/renderer/stores/task-store.ts index 8d44ad639f..7d6369d8fb 100644 --- a/apps/frontend/src/renderer/stores/task-store.ts +++ b/apps/frontend/src/renderer/stores/task-store.ts @@ -365,6 +365,10 @@ export const useTaskStore = create((set, get) => ({ // This prevents unnecessary re-renders from the memo comparator const phaseChanged = progress.phase && progress.phase !== existingProgress.phase; + // DEBUG: Log message updates + if (progress.message) { + console.log('[Store] Updating executionProgress.message:', { taskId, message: progress.message, phase: progress.phase }); + } return { ...t, executionProgress: { diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json index 6eaf1b5f9b..6931a5cc32 100644 --- a/apps/frontend/src/shared/i18n/locales/en/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json @@ -240,5 +240,17 @@ }, "subtasks": { "untitled": "Untitled subtask" + }, + "toolActions": { + "reading": "Reading {{details}}", + "searchingFiles": "Searching files {{details}}", + "searchingWeb": "Searching web {{details}}", + "searching": "Searching {{details}}", + "fetching": "Fetching {{details}}", + "editing": "Editing {{details}}", + "writing": "Writing {{details}}", + "running": "Running {{details}}", + "using": "Using {{details}}", + "default": "{{details}}" } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json index 7adfb64302..8e886b647a 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json @@ -240,5 +240,17 @@ }, "subtasks": { "untitled": "Sous-tâche sans titre" + }, + "toolActions": { + "reading": "Lecture {{details}}", + "searchingFiles": "Recherche fichiers {{details}}", + "searchingWeb": "Recherche web {{details}}", + "searching": "Recherche {{details}}", + "fetching": "Téléchargement {{details}}", + "editing": "Édition {{details}}", + "writing": "Écriture {{details}}", + "running": "Exécution {{details}}", + "using": "Utilisation {{details}}", + "default": "{{details}}" } }