diff --git a/frontend/src/app/pages/AgentChat/tool-bubbles/ToolGroupBubble.tsx b/frontend/src/app/pages/AgentChat/tool-bubbles/ToolGroupBubble.tsx index 9e855aa5..2f2fe2e4 100644 --- a/frontend/src/app/pages/AgentChat/tool-bubbles/ToolGroupBubble.tsx +++ b/frontend/src/app/pages/AgentChat/tool-bubbles/ToolGroupBubble.tsx @@ -6,6 +6,9 @@ import IconButton from '@mui/material/IconButton'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import TerminalIcon from '@mui/icons-material/Terminal'; +import CheckIcon from '@mui/icons-material/Check'; +import CircularProgress from '@mui/material/CircularProgress'; +import { summarizeToolGroup } from './summarizeToolGroup'; import { AgentMessage, ToolGroupMeta } from '@/shared/state/agentsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useMountReveal } from './useMountReveal'; @@ -124,6 +127,7 @@ const ToolGroupBubble: React.FC = React.memo(({ group, isSessionRunning = return domains; }, [group]); const webGroupLabel = webDomains ? 'Searched the web' : null; + const restingLabel = webGroupLabel ?? workflowGroupLabel; const displayName = workflowGroupLabel || webGroupLabel || meta?.name || group.label; const hasSvg = !!meta?.svg && !workflowGroupLabel && !webGroupLabel; @@ -165,13 +169,14 @@ const ToolGroupBubble: React.FC = React.memo(({ group, isSessionRunning = '&:hover': { color: c.text.secondary }, }} > + {!allDone && } {webDomains && webDomains.length > 0 && } - {webGroupLabel ?? `${group.callCount} tool call${group.callCount === 1 ? '' : 's'}`} + {allDone ? (restingLabel ?? summarizeToolGroup(toolNames) ?? `Ran ${group.callCount} step${group.callCount === 1 ? '' : 's'}`) : (restingLabel ?? 'Working')} - {!allDone && ( - - {completedCount}/{group.callCount} + {!allDone && group.callCount > 1 && ( + + {completedCount} of {group.callCount} )} {allDone && webDomains && webDomains.length > 0 && ( @@ -199,14 +204,16 @@ const ToolGroupBubble: React.FC = React.memo(({ group, isSessionRunning = '&:hover': { bgcolor: 'rgba(0,0,0,0.02)' }, }} > - {webDomains && webDomains.length > 0 ? ( + {!allDone ? ( + + ) : webDomains && webDomains.length > 0 ? ( ) : !meta ? ( ) : hasSvg ? ( - + ) : ( - + )} {!meta && !webGroupLabel && !workflowGroupLabel ? ( @@ -216,10 +223,11 @@ const ToolGroupBubble: React.FC = React.memo(({ group, isSessionRunning = ) : ( {displayName} @@ -231,18 +239,10 @@ const ToolGroupBubble: React.FC = React.memo(({ group, isSessionRunning = {deniedCount} denied )} - {/* Single fraction renders progress AND total; the green color - alone signals completion, and the fraction's denominator - makes the separate ×N chip redundant. tabular-nums + - minWidth keep the position stable as digits change. */} - {allDone && completedCount > 0 && ( - - {completedCount}/{group.callCount} - - )} + {allDone && completedCount > 0 && } {!allDone && pendingCount > 0 && ( - - {completedCount}/{group.callCount} + + {completedCount} of {group.callCount} )} diff --git a/frontend/src/app/pages/AgentChat/tool-bubbles/summarizeToolGroup.ts b/frontend/src/app/pages/AgentChat/tool-bubbles/summarizeToolGroup.ts new file mode 100644 index 00000000..23091a88 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/tool-bubbles/summarizeToolGroup.ts @@ -0,0 +1,28 @@ +const PHRASES: Record string> = { + Read: (n) => `read ${n} file${n === 1 ? '' : 's'}`, + Write: (n) => `wrote ${n} file${n === 1 ? '' : 's'}`, + Edit: (n) => `edited ${n} file${n === 1 ? '' : 's'}`, + Bash: (n) => `ran ${n} command${n === 1 ? '' : 's'}`, + Grep: (n) => `searched the code${n === 1 ? '' : ` ${n} times`}`, + Glob: (n) => `scanned the files${n === 1 ? '' : ` ${n} times`}`, + WebSearch: (n) => `searched the web${n === 1 ? '' : ` ${n} times`}`, + WebFetch: (n) => `read ${n} page${n === 1 ? '' : 's'}`, + TodoWrite: () => `updated the plan`, + Task: (n) => `delegated ${n} task${n === 1 ? '' : 's'}`, +}; + +/** Done-state group header in outcome language ("Read 3 files · ran 2 commands") instead of a raw call count; null when no tool name is recognized. */ +export function summarizeToolGroup(toolNames: string[]): string | null { + const counts = new Map(); + let unknown = 0; + for (const name of toolNames) { + const key = name === 'MultiEdit' ? 'Edit' : name; + if (PHRASES[key]) counts.set(key, (counts.get(key) ?? 0) + 1); + else unknown += 1; + } + if (counts.size === 0) return null; + const parts = Array.from(counts.entries()).slice(0, 3).map(([key, n]) => PHRASES[key](n)); + if (unknown > 0) parts.push(`${unknown} more step${unknown === 1 ? '' : 's'}`); + const joined = parts.join(' · '); + return joined.charAt(0).toUpperCase() + joined.slice(1); +}