diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index b2e144f8..a9e37cb7 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -1044,6 +1044,33 @@ describe('MessageList nested tool calls', () => { expect(container.querySelectorAll('[data-message-shell="assistant"]')).toHaveLength(0) }) + it('renders stopped tool calls as terminal instead of still generating content', () => { + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + chatState: 'idle', + messages: [ + { + id: 'tool-write', + type: 'tool_use', + toolName: 'Write', + toolUseId: 'write-1', + input: { file_path: '/tmp/story.md' }, + timestamp: 1, + isPending: false, + status: 'stopped', + } as UIMessage, + ], + }), + }, + }) + + render() + + expect(screen.getByText('Stopped')).toBeTruthy() + expect(screen.queryByText('Generating content')).toBeNull() + }) + it('renders saved memory events with an entrypoint to memory settings', () => { useChatStore.setState({ sessions: { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 14282d53..99b578ad 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1151,7 +1151,7 @@ function getMessageMetricSignature(message: UIMessage): string { case 'system': return `${message.type}:${message.content.length}` case 'tool_use': - return `${message.type}:${message.toolName}:${message.toolUseId}:${message.partialInput?.length ?? 0}:${message.isPending ? 1 : 0}` + return `${message.type}:${message.toolName}:${message.toolUseId}:${message.partialInput?.length ?? 0}:${message.isPending ? 1 : 0}:${message.status ?? ''}` case 'tool_result': return `${message.type}:${message.toolUseId}:${message.isError ? 1 : 0}` case 'compact_summary': @@ -2141,6 +2141,7 @@ export const MessageBlock = memo(function MessageBlock({ input={message.input} result={toolResult} isPending={message.isPending} + status={message.status} partialInput={message.partialInput} agentTaskNotification={ message.toolName === 'Agent' diff --git a/desktop/src/components/chat/ToolCallBlock.tsx b/desktop/src/components/chat/ToolCallBlock.tsx index 81b2d7d7..568e33e8 100644 --- a/desktop/src/components/chat/ToolCallBlock.tsx +++ b/desktop/src/components/chat/ToolCallBlock.tsx @@ -1,5 +1,5 @@ import { memo, useMemo, useState } from 'react' -import { LoaderCircle } from 'lucide-react' +import { CircleStop, LoaderCircle } from 'lucide-react' import { CodeViewer } from './CodeViewer' import { DiffViewer } from './DiffViewer' import { TerminalChrome } from './TerminalChrome' @@ -17,6 +17,7 @@ type Props = { agentTaskNotification?: AgentTaskNotification compact?: boolean isPending?: boolean + status?: 'stopped' partialInput?: string } @@ -37,7 +38,7 @@ const TOOL_ICONS: Record = { const WRITER_PREVIEW_MAX_LINES = 120 const WRITER_PREVIEW_MAX_CHARS = 30000 -export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, result, compact = false, isPending = false, partialInput }: Props) { +export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, result, compact = false, isPending = false, status, partialInput }: Props) { const isPlanTool = isExitPlanModeTool(toolName) const [expanded, setExpanded] = useState(isPlanTool) const t = useTranslation() @@ -54,6 +55,9 @@ export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, resu const pendingSummary = isPending && !result ? getPendingSummary(toolName, t) : '' + const stoppedSummary = status === 'stopped' && !result + ? t('tool.stopped') + : '' const preview = useMemo(() => renderPreview(toolName, obj, result, t), [obj, result, toolName, t]) const details = useMemo(() => renderDetails(toolName, obj, t, isPending ? partialInput : undefined), [isPending, obj, partialInput, toolName, t]) @@ -108,6 +112,11 @@ export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, resu