diff --git a/cli/src/components/blocks/agent-branch-wrapper.tsx b/cli/src/components/blocks/agent-branch-wrapper.tsx index dbded04ac5..46da9ea921 100644 --- a/cli/src/components/blocks/agent-branch-wrapper.tsx +++ b/cli/src/components/blocks/agent-branch-wrapper.tsx @@ -17,7 +17,10 @@ import { ToolBlockGroup } from './tool-block-group' import { useTheme } from '../../hooks/use-theme' import { useChatStore } from '../../state/chat-store' import { isTextBlock } from '../../types/chat' -import { getAgentDisplayPrompt } from '../../utils/agent-display' +import { + getAgentDisplayPrompt, + getBasherFinishedOutputPreview, +} from '../../utils/agent-display' import { getAgentStatusInfo } from '../../utils/agent-helpers' import { processBlocks, @@ -52,12 +55,23 @@ function getCollapsedPreview( agentBlock: AgentContentBlock, isStreaming: boolean, isCollapsed: boolean, + availableWidth: number, ): string { // No preview needed if expanded and not streaming if (!isStreaming && !isCollapsed) { return '' } + if (!isStreaming) { + const outputPreview = getBasherFinishedOutputPreview( + agentBlock, + Math.max(24, Math.min(120, availableWidth - 4)), + ) + if (outputPreview) { + return outputPreview + } + } + // For multi-prompt editors, try progress-focused preview first if (isMultiPromptEditor(agentBlock.agentType)) { const multiPromptPreview = getMultiPromptPreview( @@ -427,7 +441,12 @@ export const AgentBranchWrapper = memo( const isStreaming = agentBlock.status === 'running' || agentIsStreaming // Compute collapsed preview text - const preview = getCollapsedPreview(agentBlock, isStreaming, isCollapsed) + const preview = getCollapsedPreview( + agentBlock, + isStreaming, + isCollapsed, + availableWidth, + ) const displayPrompt = getAgentDisplayPrompt(agentBlock) const effectiveStatus = isStreaming ? 'running' : agentBlock.status diff --git a/cli/src/utils/__tests__/agent-display.test.ts b/cli/src/utils/__tests__/agent-display.test.ts index 82e410dcfc..248a7a074a 100644 --- a/cli/src/utils/__tests__/agent-display.test.ts +++ b/cli/src/utils/__tests__/agent-display.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from 'bun:test' -import { getAgentDisplayPrompt } from '../agent-display' +import { + getAgentDisplayPrompt, + getBasherFinishedOutputPreview, + truncateToSingleLinePreview, +} from '../agent-display' import type { AgentContentBlock } from '../../types/chat' @@ -64,3 +68,72 @@ describe('getAgentDisplayPrompt', () => { expect(getAgentDisplayPrompt(block)).toBeUndefined() }) }) + +describe('getBasherFinishedOutputPreview', () => { + test('returns undefined while basher is still running', () => { + const block = createAgentBlock({ + status: 'running', + params: { + what_to_summarize: 'Report the test result', + }, + blocks: [{ type: 'text', content: 'Tests passed' }], + }) + + expect(getBasherFinishedOutputPreview(block)).toBeUndefined() + }) + + test('uses finished basher text output before what_to_summarize', () => { + const block = createAgentBlock({ + status: 'complete', + params: { + what_to_summarize: 'Report the test result', + }, + blocks: [ + { + type: 'text', + content: 'Tests passed\n42 assertions completed', + textType: 'text', + }, + ], + }) + + expect(getBasherFinishedOutputPreview(block)).toBe( + 'Tests passed 42 assertions completed', + ) + }) + + test('falls back to command output when no text block exists', () => { + const block = createAgentBlock({ + status: 'complete', + blocks: [ + { + type: 'tool', + toolCallId: 'tool-1', + toolName: 'run_terminal_command', + input: { command: 'git status --short' }, + output: ' M cli/src/app.tsx\n', + }, + ], + }) + + expect(getBasherFinishedOutputPreview(block)).toBe('M cli/src/app.tsx') + }) + + test('ignores non-basher output', () => { + const block = createAgentBlock({ + agentType: 'code-searcher', + status: 'complete', + blocks: [{ type: 'text', content: 'Search results' }], + }) + + expect(getBasherFinishedOutputPreview(block)).toBeUndefined() + }) +}) + +describe('truncateToSingleLinePreview', () => { + test('collapses whitespace and truncates to the requested length', () => { + expect(truncateToSingleLinePreview('one\ntwo three four', 13)).toBe( + 'one two th...', + ) + }) +}) diff --git a/cli/src/utils/agent-display.ts b/cli/src/utils/agent-display.ts index 18c3668fd4..b91545cea3 100644 --- a/cli/src/utils/agent-display.ts +++ b/cli/src/utils/agent-display.ts @@ -1,6 +1,30 @@ import { getAgentBaseName } from './message-block-helpers' -import type { AgentContentBlock } from '../types/chat' +import type { + AgentContentBlock, + TextContentBlock, + ToolContentBlock, +} from '../types/chat' + +const DEFAULT_BASHER_OUTPUT_PREVIEW_MAX_LENGTH = 120 +const PREVIEW_ELLIPSIS = '...' + +export function truncateToSingleLinePreview( + text: string, + maxLength = DEFAULT_BASHER_OUTPUT_PREVIEW_MAX_LENGTH, +): string | undefined { + const singleLine = text.replace(/\s+/g, ' ').trim() + if (!singleLine) { + return undefined + } + + if (singleLine.length <= maxLength) { + return singleLine + } + + const previewLength = Math.max(0, maxLength - PREVIEW_ELLIPSIS.length) + return `${singleLine.slice(0, previewLength).trimEnd()}${PREVIEW_ELLIPSIS}` +} export function getAgentDisplayPrompt( agentBlock: AgentContentBlock, @@ -19,3 +43,45 @@ export function getAgentDisplayPrompt( ? whatToSummarize.trim() : undefined } + +export function getBasherFinishedOutputPreview( + agentBlock: AgentContentBlock, + maxLength = DEFAULT_BASHER_OUTPUT_PREVIEW_MAX_LENGTH, +): string | undefined { + if ( + getAgentBaseName(agentBlock.agentType) !== 'basher' || + agentBlock.status === 'running' + ) { + return undefined + } + + const blocks = agentBlock.blocks ?? [] + return ( + truncateToSingleLinePreview(getTextOutput(blocks), maxLength) ?? + truncateToSingleLinePreview(getCommandOutput(blocks), maxLength) + ) +} + +function getTextOutput( + blocks: NonNullable, +): string { + return blocks + .filter( + (block): block is TextContentBlock => + block.type === 'text' && block.textType !== 'reasoning', + ) + .map((block) => block.content) + .join('\n') +} + +function getCommandOutput( + blocks: NonNullable, +): string { + return blocks + .filter( + (block): block is ToolContentBlock => + block.type === 'tool' && block.toolName === 'run_terminal_command', + ) + .map((block) => block.output ?? '') + .join('\n') +}