diff --git a/agents/file-explorer/code-searcher.ts b/agents/file-explorer/code-searcher.ts index 43fee7795..68f91659b 100644 --- a/agents/file-explorer/code-searcher.ts +++ b/agents/file-explorer/code-searcher.ts @@ -85,6 +85,7 @@ const codeSearcher: SecretAgentDefinition = { yield { toolName: 'set_output', input: { + message: '', results: toolResults, }, includeToolCall: false, diff --git a/cli/src/components/tools/__tests__/code-search.test.tsx b/cli/src/components/tools/__tests__/code-search.test.tsx new file mode 100644 index 000000000..590e43517 --- /dev/null +++ b/cli/src/components/tools/__tests__/code-search.test.tsx @@ -0,0 +1,45 @@ +import { describe, expect, test } from 'bun:test' +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' + +import { initializeThemeStore } from '../../../hooks/use-theme' +import { CodeSearchComponent } from '../code-search' + +import type { ChatTheme } from '../../../types/theme-system' +import type { ToolBlock } from '../types' + +initializeThemeStore() + +const createToolBlock = ( + output?: string, +): ToolBlock & { toolName: 'code_search' } => ({ + type: 'tool', + toolName: 'code_search', + toolCallId: 'code-search-test', + input: { + pattern: 'getAgentBaseName', + cwd: 'cli/src/utils', + }, + output, +}) + +describe('CodeSearchComponent', () => { + test('uses formatted match count from current code search output', () => { + const result = CodeSearchComponent.render( + createToolBlock(`Found 2 matches +./message-block-helpers.ts: +Line 13: export const getAgentBaseName = (type: string): string => { +Line 196: getAgentBaseName(options.agentType ?? '') === 'code-searcher'`), + {} as ChatTheme, + { + availableWidth: 80, + indentationOffset: 0, + labelWidth: 10, + }, + ) + + const markup = renderToStaticMarkup(<>{result.content}) + + expect(markup).toContain('getAgentBaseName in cli/src/utils (2 results)') + }) +}) diff --git a/cli/src/components/tools/code-search.tsx b/cli/src/components/tools/code-search.tsx index aff023ca2..47d007fee 100644 --- a/cli/src/components/tools/code-search.tsx +++ b/cli/src/components/tools/code-search.tsx @@ -23,13 +23,22 @@ export const CodeSearchComponent = defineToolComponent({ if (toolBlock.output && typeof toolBlock.output === 'string') { const lines = toolBlock.output.split('\n') + const matchCountLine = lines.find((line) => + /^Found \d+ matches?$/.test(line.trim()), + ) + const parsedTotalResults = matchCountLine + ?.trim() + .match(/^Found (\d+) matches?$/)?.[1] - for (const line of lines) { - const trimmed = line.trim() + if (parsedTotalResults !== undefined) { + totalResults = Number(parsedTotalResults) + } else { + for (const line of lines) { + const trimmed = line.trim() - // Result lines start with a number followed by a colon - if (/^\d+:/.test(trimmed)) { - totalResults++ + if (/^(?:Line\s+)?\d+:/.test(trimmed)) { + totalResults++ + } } } } @@ -52,12 +61,7 @@ export const CodeSearchComponent = defineToolComponent({ // Return as content using SimpleToolCallItem return { - content: ( - - ), + content: , } }, }) diff --git a/cli/src/utils/__tests__/message-block-helpers.test.ts b/cli/src/utils/__tests__/message-block-helpers.test.ts index d813de400..55d66522b 100644 --- a/cli/src/utils/__tests__/message-block-helpers.test.ts +++ b/cli/src/utils/__tests__/message-block-helpers.test.ts @@ -376,6 +376,23 @@ describe('extractSpawnAgentResultContent', () => { hasError: false, }) }) + + test('uses an empty structuredOutput message as no display content', () => { + const result = extractSpawnAgentResultContent({ + type: 'structuredOutput', + value: { + message: '', + results: [ + { + stdout: 'Found 1 match\n./file.ts:\nLine 1: needle', + message: 'Exit code: 0', + }, + ], + }, + }) + + expect(result).toEqual({ content: '', hasError: false }) + }) }) describe('appendInterruptionNotice', () => { diff --git a/docs/testing.md b/docs/testing.md index dcc8ee4e7..3862f66ad 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -9,3 +9,37 @@ CLI hook testing note: React 19 + Bun + RTL `renderHook()` is unreliable; prefer ## CLI tmux Testing For testing CLI behavior via tmux, use the helper scripts in `scripts/tmux/`. These handle bracketed paste mode and session logging automatically. Session data is saved to `debug/tmux-sessions/` in YAML format and can be viewed with `bun scripts/tmux/tmux-viewer/index.tsx`. See `scripts/tmux/README.md` for details. + +Useful workflow for agents: + +```bash +# Start the dev CLI in a detached tmux session. +SESSION=$(./scripts/tmux/tmux-cli.sh start --name cli-check -w 160 -h 40 --wait 6) + +# Capture the initial screen. Captures are written to debug/tmux-sessions/$SESSION/. +./scripts/tmux/tmux-cli.sh capture "$SESSION" --label initial + +# Send a prompt. The helper uses bracketed paste so text is not dropped. +./scripts/tmux/tmux-cli.sh send "$SESSION" "Search for getAgentBaseName and report what you find" --wait-idle 4 + +# Capture after the run, then inspect the saved capture text. +./scripts/tmux/tmux-cli.sh capture "$SESSION" --label after-search --wait 2 + +# Clean up when finished. +./scripts/tmux/tmux-cli.sh stop "$SESSION" +``` + +If a change can be verified with a small local harness instead of a live model-backed CLI run, run that harness inside tmux too. This still checks terminal rendering and produces a capture: + +```bash +SESSION=$(./scripts/tmux/tmux-cli.sh start \ + --name render-check \ + -w 160 -h 20 \ + --wait 1 \ + --command "bun .context/my-render-check.tsx") + +./scripts/tmux/tmux-cli.sh capture "$SESSION" --label rendered +./scripts/tmux/tmux-cli.sh stop "$SESSION" +``` + +When verifying UI output, prefer checking the saved capture file for concrete strings that should and should not appear. For example, after expanding a code-searcher agent, check that the capture shows the search summary but not raw structured payload keys like `results:` or `stdout:`.