diff --git a/src/shared/native-chat-tool-summary.test.ts b/src/shared/native-chat-tool-summary.test.ts index ae8efb8f194..dfedc94e089 100644 --- a/src/shared/native-chat-tool-summary.test.ts +++ b/src/shared/native-chat-tool-summary.test.ts @@ -162,6 +162,16 @@ describe('briefToolArg', () => { it('falls back to the command preview when no path is present', () => { expect(briefToolArg({ command: 'git status --short' })).toBe('git status --short') }) + + it('keeps a blank primary argument out of the run header', () => { + // Skipping a blank key must not fall through to raw JSON — the row would + // read `Bash {"command":""}` where it used to read `Bash`. + expect(briefToolArg({ command: '' })).toBe('') + expect(briefToolArg({ cmd: ' ' })).toBe('') + expect(briefToolArg({ cmd: '', file_path: '' })).toBe('') + const blocks: NativeChatBlock[] = [{ type: 'tool-call', name: 'Bash', input: { command: '' } }] + expect(summarizeToolRun(blocks)).toBe('Bash') + }) }) describe('summarizeToolRun', () => { diff --git a/src/shared/native-chat-tool-summary.ts b/src/shared/native-chat-tool-summary.ts index 4418fd767f3..8931234a98f 100644 --- a/src/shared/native-chat-tool-summary.ts +++ b/src/shared/native-chat-tool-summary.ts @@ -74,8 +74,9 @@ export function toolFilePath(input: unknown): string | null { return null } const value = normalized as Record - // A search call's `path` is the directory it scanned, not a file it opened — - // taking it would drop the pattern from the label and offer a link to a folder. + // A search call's `path` is usually the directory it scanned, so taking it as a + // target would label the row with the scan root and link to a folder. Costs the + // link on a file-scoped search; a dead link on every other search is worse. const directory = isSearchToolInput(value) ? undefined : value.path const path = value.file_path ?? value.filePath ?? directory ?? value.notebook_path return typeof path === 'string' && path.length > 0 ? path : null @@ -89,10 +90,16 @@ export function briefToolArg(input: unknown): string { const parts = path.split(/[\\/]/).filter(Boolean) return parts.at(-1) ?? path } - const command = firstPrimaryToolArg(normalized as Record, BRIEF_ARG_KEYS) + const value = normalized as Record + const command = firstPrimaryToolArg(value, BRIEF_ARG_KEYS) if (command) { return command.slice(0, 28) } + // A blank primary key means the call has no brief argument; falling through + // would stand its raw JSON in for one in the run header. + if (BRIEF_ARG_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key))) { + return '' + } } return summarizeToolInput(normalized).slice(0, 28) }