From 27fe90a6f26684776aa80b9fd3ed6b71f4cda515 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:36:16 -0700 Subject: [PATCH] fix(mobile): don't offer an expander whose detail repeats the row (STA-3333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty tool input formats back to the row label verbatim, so `{}` and `[]` advertised an expander and then re-showed the label — the same repeat-the-JSON problem this change set out to remove. Gate `isStructuredToolInput` on the collection actually having contents; the lazy detail path is untouched. Also pins the overlong-path test to the path itself: asserting only length<=80 plus a `…` passed just as well with path labelling deleted. --- src/shared/native-chat-tool-summary.test.ts | 20 ++++++++++++++++++-- src/shared/native-chat-tool-summary.ts | 7 ++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/shared/native-chat-tool-summary.test.ts b/src/shared/native-chat-tool-summary.test.ts index 9ae5d1a99be..ae8efb8f194 100644 --- a/src/shared/native-chat-tool-summary.test.ts +++ b/src/shared/native-chat-tool-summary.test.ts @@ -41,8 +41,13 @@ describe('describeToolInput', () => { it('truncates an overlong path like any other preview', () => { const path = `/very/${'long/'.repeat(30)}file.ts` - expect(describeToolInput({ file_path: path }).length).toBeLessThanOrEqual(80) - expect(describeToolInput({ file_path: path })).toContain('…') + const label = describeToolInput({ file_path: path }) + expect(label.length).toBeLessThanOrEqual(80) + expect(label).toContain('…') + // A bounded JSON preview also satisfies the two assertions above, so pin the + // label to the path itself or this passes with path-labelling deleted. + expect(label).not.toContain('file_path') + expect(label.endsWith('/file.ts')).toBe(true) }) it('keeps the filename when trimming an overlong path', () => { @@ -115,6 +120,17 @@ describe('Codex JSON-string tool arguments', () => { }) }) +describe('isStructuredToolInput', () => { + it('does not offer an expander whose detail would repeat the row label', () => { + // `{}` formats back to `{}` — the label itself. + expect(isStructuredToolInput({})).toBe(false) + expect(isStructuredToolInput([])).toBe(false) + expect(isStructuredToolInput('{}')).toBe(false) + expect(isStructuredToolInput({ command: 'ls' })).toBe(true) + expect(isStructuredToolInput([1])).toBe(true) + }) +}) + describe('summarizeToolInput bounded preview', () => { it('collapses depth beyond the bound instead of serializing the whole tree', () => { const deep = { a: { b: { c: { d: 'buried' } } } } diff --git a/src/shared/native-chat-tool-summary.ts b/src/shared/native-chat-tool-summary.ts index b625373a242..4418fd767f3 100644 --- a/src/shared/native-chat-tool-summary.ts +++ b/src/shared/native-chat-tool-summary.ts @@ -60,7 +60,12 @@ export function formatToolInput(input: unknown): string { * the row label — i.e. whether expanding the row is worth offering. */ export function isStructuredToolInput(input: unknown): boolean { const normalized = normalizeToolInput(input) - return normalized !== null && typeof normalized === 'object' + if (normalized === null || typeof normalized !== 'object') { + return false + } + // An empty object formats back to the row label verbatim, so offering the + // expander would promise detail and then repeat the row. + return Array.isArray(normalized) ? normalized.length > 0 : Object.keys(normalized).length > 0 } export function toolFilePath(input: unknown): string | null {