diff --git a/mobile/src/session/MobileNativeChatMessage.test.ts b/mobile/src/session/MobileNativeChatMessage.test.ts index 8a0a2582bea..e01aa2b23da 100644 --- a/mobile/src/session/MobileNativeChatMessage.test.ts +++ b/mobile/src/session/MobileNativeChatMessage.test.ts @@ -1,5 +1,5 @@ import { createElement } from 'react' -import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import { MAX_TOOL_RESULT_CHARS } from './mobile-native-chat-message-styles' @@ -110,4 +110,32 @@ describe('MobileNativeChatMessage', () => { expect(detail).toHaveLength(MAX_TOOL_RESULT_CHARS + 1) expect(detail?.endsWith('…')).toBe(true) }) + + it('expands formatted detail for a pending JSON-string tool input', () => { + const tree = render({ + id: 'a1', + role: 'assistant', + blocks: [ + { + type: 'tool-call', + name: 'CustomTool', + input: '{"cmd":"git status","description":"Inspect changes"}' + } + ], + timestamp: null, + source: 'transcript' + }) + const textIn = (node: ReactTestInstance): string[] => + node.findAllByType('Text' as never).map((text) => String(text.children.join(''))) + const pressableWith = (label: string) => + tree.root.findAllByType('Pressable' as never).find((node) => textIn(node).includes(label))! + + act(() => pressableWith('1×').props.onPress()) + expect(textIn(tree.root).some((text) => text.startsWith('{\n'))).toBe(false) + + act(() => pressableWith('CustomTool').props.onPress()) + expect(textIn(tree.root)).toContain( + '{\n "cmd": "git status",\n "description": "Inspect changes"\n}' + ) + }) }) diff --git a/mobile/src/session/MobileNativeChatMessage.tsx b/mobile/src/session/MobileNativeChatMessage.tsx index f2a47e6a56c..395f5aca01d 100644 --- a/mobile/src/session/MobileNativeChatMessage.tsx +++ b/mobile/src/session/MobileNativeChatMessage.tsx @@ -19,6 +19,7 @@ import { nativeChatMessageText } from './mobile-native-chat-message-text' import { describeToolInput, formatToolInput, + isStructuredToolInput, summarizeToolRun, toolFilePath } from './mobile-native-chat-tool-summary' @@ -109,7 +110,7 @@ function ToolLine({ callDiff !== null || result !== undefined || preview.length > 40 || - (call !== undefined && call.input !== null && typeof call.input === 'object') + (call !== undefined && isStructuredToolInput(call.input)) // A tool that targets a file (Read/Edit/Write…) renders its preview as a // tappable link that opens the file, independent of the line's expand tap. const filePath = call ? toolFilePath(call.input) : null diff --git a/mobile/src/session/mobile-native-chat-tool-summary.ts b/mobile/src/session/mobile-native-chat-tool-summary.ts index 8f30e41fd97..375b6312bcb 100644 --- a/mobile/src/session/mobile-native-chat-tool-summary.ts +++ b/mobile/src/session/mobile-native-chat-tool-summary.ts @@ -3,6 +3,7 @@ export { countToolCalls, describeToolInput, formatToolInput, + isStructuredToolInput, summarizeToolInput, summarizeToolRun, toolFilePath diff --git a/src/shared/native-chat-tool-summary.ts b/src/shared/native-chat-tool-summary.ts index e4f34152941..6e2fbb04bc1 100644 --- a/src/shared/native-chat-tool-summary.ts +++ b/src/shared/native-chat-tool-summary.ts @@ -57,6 +57,11 @@ export function formatToolInput(input: unknown): string { } } +export function isStructuredToolInput(input: unknown): boolean { + const normalized = normalizeToolInput(input) + return normalized !== null && typeof normalized === 'object' +} + export function toolFilePath(input: unknown): string | null { const normalized = normalizeToolInput(input) if (!normalized || typeof normalized !== 'object') {