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:`.