From c17be9ef3b586487acf43b44bc98d560c3c9fc09 Mon Sep 17 00:00:00 2001 From: chenchen Date: Tue, 12 May 2026 16:08:47 +0800 Subject: [PATCH] feat(desktop): StreamingIndicator surfaces active tool + target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the random spinner verb with a friendly description of the tool currently running — "Reading foo.ts", "Editing bar.tsx", "Running npm", "Searching ". Falls back to the verb / chatState label when no tool is active. Why: users were seeing "Percolating... 51s" with no idea whether the agent was stuck or genuinely working. The chatStore already tracks activeToolName + streamingToolInput, the indicator just wasn't using either. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/chat/StreamingIndicator.tsx | 87 +++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/cc-haha/desktop/src/components/chat/StreamingIndicator.tsx b/cc-haha/desktop/src/components/chat/StreamingIndicator.tsx index a6a405a..4318e67 100644 --- a/cc-haha/desktop/src/components/chat/StreamingIndicator.tsx +++ b/cc-haha/desktop/src/components/chat/StreamingIndicator.tsx @@ -8,6 +8,78 @@ function formatElapsed(seconds: number): string { return `${m}m ${s}s` } +function basename(p: string): string { + const parts = p.split(/[/\\]/) + return parts[parts.length - 1] || p +} + +function truncate(s: string, n: number): string { + return s.length > n ? `${s.slice(0, n - 1)}…` : s +} + +/** + * Build a short, user-facing description of the tool that is currently + * executing. Tries to parse the partially-streamed JSON input so the + * user sees e.g. "Reading foo.ts" instead of "Read". + */ +function describeActiveTool(toolName: string, rawInput: string): string { + let parsed: Record | null = null + if (rawInput) { + try { + parsed = JSON.parse(rawInput) as Record + } catch { + // Partial JSON during streaming — try regex extraction of common keys. + const grab = (key: string): string | null => { + const m = rawInput.match(new RegExp(`"${key}"\\s*:\\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)`)) + return m && m[1] ? m[1] : null + } + parsed = { + file_path: grab('file_path'), + command: grab('command'), + pattern: grab('pattern'), + path: grab('path'), + url: grab('url'), + description: grab('description'), + } + } + } + const filePath = (parsed?.file_path as string | undefined) ?? null + const command = (parsed?.command as string | undefined) ?? null + const pattern = (parsed?.pattern as string | undefined) ?? null + const path = (parsed?.path as string | undefined) ?? null + const url = (parsed?.url as string | undefined) ?? null + const description = (parsed?.description as string | undefined) ?? null + + switch (toolName) { + case 'Read': + return filePath ? `Reading ${basename(filePath)}` : 'Reading file' + case 'Edit': + case 'Write': + return filePath ? `Editing ${basename(filePath)}` : 'Editing file' + case 'Bash': + case 'PowerShell': + if (description) return truncate(description, 48) + return command ? `Running ${truncate(command.split(/\s+/)[0] || command, 32)}` : 'Running command' + case 'Grep': + return pattern ? `Searching ${truncate(pattern, 40)}` : 'Searching code' + case 'Glob': + return pattern ? `Finding ${truncate(pattern, 40)}` : 'Finding files' + case 'WebFetch': + case 'WebSearch': + return url ? `Fetching ${truncate(url, 40)}` : toolName === 'WebSearch' ? 'Searching web' : 'Fetching URL' + case 'TodoWrite': + return 'Updating plan' + case 'Agent': + return description ? `Delegating: ${truncate(description, 36)}` : 'Delegating to subagent' + case 'Task': + return description ? `Subtask: ${truncate(description, 40)}` : 'Running subtask' + default: + if (filePath) return `${toolName} ${basename(filePath)}` + if (path) return `${toolName} ${basename(path)}` + return `Running ${toolName}` + } +} + export function StreamingIndicator() { const activeTabId = useTabStore((s) => s.activeTabId) const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined) @@ -15,17 +87,22 @@ export function StreamingIndicator() { const statusVerb = sessionState?.statusVerb ?? '' const elapsedSeconds = sessionState?.elapsedSeconds ?? 0 const tokenUsage = sessionState?.tokenUsage ?? { input_tokens: 0, output_tokens: 0 } - let verb: string - if (statusVerb) { - verb = statusVerb + const activeToolName = sessionState?.activeToolName ?? null + const streamingToolInput = sessionState?.streamingToolInput ?? '' + + let label: string + if (activeToolName) { + label = describeActiveTool(activeToolName, streamingToolInput) + } else if (statusVerb) { + label = statusVerb } else { - verb = chatState === 'thinking' ? 'Thinking' : chatState === 'tool_executing' ? 'Running' : 'Working' + label = chatState === 'thinking' ? 'Thinking' : chatState === 'tool_executing' ? 'Running' : 'Working' } return (
✦ - {verb}... + {label}... {elapsedSeconds > 0 && ( {formatElapsed(elapsedSeconds)}