From f0031abc3969c8f45d88a71321c2e59fb1d6848f Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 26 Jul 2026 16:03:49 -0500 Subject: [PATCH 1/2] fix(gateway): send a raw arg preview on tool.start, not a phrased label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _tool_ctx switched to build_tool_label in #55166, so every tool.start carried an already-phrased string ("Running sleep 70 + 2 commands"). Both clients then apply their own verb on top: the TUI renders Terminal("Running sleep 70 + 2 commands") and the desktop row reads "Ran Running sleep 70 + 2 commands". The friendly labels stay where they belong — the CLI spinner and the gateway progress line, which compose verb + preview at their own call sites. --- tests/test_tui_gateway_server.py | 14 +++++++++++++- tui_gateway/server.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index dbdd2ec5ffc2..d51afc1af13a 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -1456,12 +1456,24 @@ def test_history_to_messages_preserves_tool_calls_for_resume_display(): assert server._history_to_messages(history) == [ {"role": "user", "text": "first prompt"}, - {"context": "Searching files for resume", "name": "search_files", "role": "tool"}, + {"context": "resume", "name": "search_files", "role": "tool"}, {"role": "assistant", "text": "first answer"}, {"role": "user", "text": "second prompt"}, ] +def test_tool_ctx_sends_an_arg_preview_not_a_phrased_label(): + # Clients phrase their own verb around this string: the TUI renders + # `Terminal("")` and the desktop prepends "Running"/"Ran". Sending a + # pre-phrased label made both stutter ("Ran Running sleep 70 + 2 commands") + # and stood in for the real command in the desktop's `$` transcript. + assert server._tool_ctx("terminal", {"command": 'sleep 70; echo "a"; echo "b"'}) == ( + "sleep 70 + 2 commands" + ) + assert server._tool_ctx("read_file", {"path": "/tmp/demo/package.json"}) == "package.json" + assert server._tool_ctx("web_search", {"query": "weather in NYC"}) == "weather in NYC" + + def test_history_to_messages_keeps_reasoning_only_assistant_turn(): # A thinking-only assistant turn (reasoning present, no visible text) is # persisted and recallable, but was dropped from the resumed session view diff --git a/tui_gateway/server.py b/tui_gateway/server.py index ec43de410a5a..4b422e7f3f78 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -4497,10 +4497,20 @@ def _session_info(agent, session: dict | None = None) -> dict: def _tool_ctx(name: str, args: dict) -> str: - try: - from agent.display import build_tool_label + """Argument preview for a tool row — never a phrased label. - return build_tool_label(name, args, max_len=80) or "" + Clients own their own phrasing: the TUI wraps this as ``Terminal("...")`` + and the desktop prepends its own localized verb ("Running"/"Ran"). Sending + ``build_tool_label`` here instead of the raw preview stutters the verb on + both surfaces ("Running Running sleep 70 + 2 commands") and leaks a display + label into the desktop's ``args.context``, where it stands in for the real + command. The friendly labels belong on the CLI spinner, which builds them + from ``build_tool_label`` at its own call sites. + """ + try: + from agent.display import build_tool_preview + + return build_tool_preview(name, args, max_len=80) or "" except Exception: return "" From ec6719b04e5e088e9ce4333c6ea2de02889d10c7 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 26 Jul 2026 16:03:49 -0500 Subject: [PATCH 2/2] fix(desktop): stop the tool row repeating its command three times The expanded terminal row printed the same string as the title, as the `$` transcript, and again as detail. shellCommand preferred the backend's display preview over the real `command` arg, so the transcript showed a summary of what ran rather than what ran; and a terminal call with no output fell through to the generic fallback, which echoes args.context under a transcript already showing it. --- .../assistant-ui/tool/fallback-model.test.ts | 22 +++++++++++++++++++ .../assistant-ui/tool/fallback-model/index.ts | 14 +++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts b/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts index 275cc13fb4f6..19a8cd61447c 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts @@ -340,6 +340,28 @@ describe('buildToolView title actions', () => { expect(view.titleAction).toEqual({ prefix: '', text: 'Running', suffix: ' pnpm run lint' }) }) + it('never stutters the verb or echoes the command when the backend context is a phrased label', () => { + // Older backends stamped tool.start with a *phrased* label + // ("Running sleep 70 + 2 commands") rather than a raw arg preview, and the + // desktop merges that into args.context. The row must still prepend its own + // verb exactly once, show the real command in the `$` transcript, and not + // repeat either string as detail. + const command = 'sleep 70; echo "a"; echo "b"' + + const view = buildToolView( + part({ + args: { command, context: 'Running sleep 70 + 2 commands' }, + result: { exit_code: 0 }, + toolName: 'terminal' + }), + '' + ) + + expect(view.title).toBe('Ran sleep 70 + 2 commands') + expect(view.terminalCommand).toBe(command) + expect(view.detail).toBe('') + }) + it('uses the runtime locale for title text and action placement', () => { setRuntimeI18nLocale('ja') diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts b/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts index 5c6fd8c9b034..6bcd06a63a93 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts @@ -128,9 +128,14 @@ function readFileDisplayTarget(args: Record, result: Record): string { return ( - firstStringField(args, ['context', 'preview']) || firstStringField(args, ['command', 'code']) || contextValue(args) + firstStringField(args, ['command', 'code']) || firstStringField(args, ['context', 'preview']) || contextValue(args) ) } @@ -1079,6 +1084,13 @@ function toolDetailText( if (output || lines) { return [output, lines].filter(Boolean).join('\n') } + + // A terminal row with no output already shows its command in the `$` + // transcript above; the generic fallback would print the same string a + // second time. `execute_code` has no transcript, so it keeps the fallback. + if (part.toolName === 'terminal') { + return '' + } } if (part.toolName === 'web_extract') {