From 136badb86ebe6706248c968e149bb002fa2bd0a9 Mon Sep 17 00:00:00 2001 From: Junmo Kim Date: Tue, 5 May 2026 19:20:54 +0900 Subject: [PATCH] fix(gemini): surface tool_call input on Gemini ACP cards (#562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(acp): derive tool_call input from kind+title fallback Gemini 2.5 Flash and 3 Flash Preview omit rawInput entirely on tool_call events while emitting prose (non-JSON) thoughts. Neither the existing rawInput path nor JSON-thought hoisting fires, so the UI shows "Input: null" alongside a perfectly readable title like "README.md" or "ls -la /tmp". Add a conservative fallback that maps known kinds to a minimal input shape: read -> { file_path: title } execute -> { command: title } search -> { pattern: title } think -> null (topic-update prose has no clean arg mapping) unknown -> null (no guessing on shapes we have not verified) Priority: rawInput > hoisted JSON thought > kind+title derive. Lock the new behaviour with synthetic unit tests (8 cases) and a real-Gemini fixture suite captured from gemini-3-flash-preview and gemini-2.5-flash via ACP stdio (4 fixtures, 33/27/13/4 raw sessionUpdate events). The fixtures double as regression guards against future ACP handler changes. * fix(web): suppress duplicate subtitle when equal to tool title Gemini ACP emits a tool_call whose title field is a human-readable summary (often the verbatim shell command or file path). Combined with the kind+title input fallback, an unknown-tool card ends up with the same string in both the title and subtitle slots — e.g. title "cat /tmp/hello.txt" over subtitle "cat /tmp/hello.txt". Add a guard in getToolPresentation's unknown-tool branch: emit subtitle only when it differs from toolName. The known-tool and mcp__* branches are unaffected. * test(acp): align Gemini fixtures to current model set - Drop gemini-2.5-flash fixtures: the captures came from a model that is not part of the PR's evidence model set, and re-running the capture is gated on quota that is not currently available. - Refresh gemini-3-flash-preview read_file / run_shell fixtures with a fresh live capture so they reflect the latest ACP shape (e.g. a `kind: think` tool_call expressing reasoning when the model emits no agent_thought_chunk). - Update fixture-replay expectations: read_file no longer requires reasoning chunks (zero are emitted on this path) and now requires >= 2 tool_calls (think + read). * feat(web): promote semantic title for Gemini ACP tool cards When the unknown-tool ToolCard would render the same string as both the title and the subtitle, promote a semantic label to the title slot so the card reads like a sentence: cat /tmp/hello.txt → Run shell / cat /tmp/hello.txt README.md → Read file / README.md *.ts → Search / *.ts This is a web-only ergonomic change; the underlying ACP message shape (tool_name = title, input = derived from kind+title) is unchanged. Builds on the dedup guard so the title-equals-subtitle case is now handled by promotion rather than by hiding the subtitle. * fix(acp): derive tool_call.input for kind=edit from locations[0].path Gemini's write_file and replace tools both surface as ACP tool_call with kind="edit" and rawInput omitted. The path lives on locations[0] from the very first event; the title is prose like "Writing to foo.txt" or "foo.txt: old => new", which is not safely usable as a file_path. Extend the kind+title fallback to read locations[0].path when kind is "edit", and synthesize { file_path } from it. Title fallback is intentionally not used here so we never feed prose into file_path. Lock the behaviour in with two new fixtures captured live from gemini-3-flash-preview (write_file and replace) plus two synthetic unit tests covering the locations-present and locations-empty paths. * test(acp): add gemini-3.1-pro-preview fixtures for regression coverage Captured 4 raw ACP `sessionUpdate` sequences from a live `gemini-3.1-pro-preview` session via the same isolated hub + runner + spawn pattern used for the existing flash captures (read_file 31 events / run_shell 83 events / write_file 4 events / edit_file 11 events). The pro tier reuses the same kind/title shape as flash: `rawInput` is omitted on every tool_call across read / execute / edit kinds, so the kind+title (and locations[0].path for edit) fallback is exactly what derives the modal Input. Locking these fixtures in guards against future regressions on a second model. The fixture-based regression test gains 4 entries (read / shell / write / edit) mirroring the flash matrix; assertions are unchanged. ACP handler suite: 53 -> 57 pass. --- .../backends/acp/AcpMessageHandler.test.ts | 378 +++++++++++ .../agent/backends/acp/AcpMessageHandler.ts | 81 ++- .../gemini-3-flash-preview-edit-file.json | 84 +++ .../gemini-3-flash-preview-read-file.json | 172 +++++ .../gemini-3-flash-preview-run-shell.json | 61 ++ .../gemini-3-flash-preview-write-file.json | 84 +++ .../gemini-3.1-pro-preview-edit-file.json | 143 +++++ .../gemini-3.1-pro-preview-read-file.json | 249 ++++++++ .../gemini-3.1-pro-preview-run-shell.json | 593 ++++++++++++++++++ .../gemini-3.1-pro-preview-write-file.json | 58 ++ web/src/components/ToolCard/ToolCard.tsx | 15 +- web/src/components/ToolCard/helpers.tsx | 3 +- .../components/ToolCard/knownTools.test.tsx | 74 +++ web/src/components/ToolCard/knownTools.tsx | 26 +- web/src/components/ToolCard/trace.tsx | 2 +- web/src/lib/locales/en.ts | 5 + web/src/lib/locales/zh-CN.ts | 5 + 17 files changed, 2014 insertions(+), 19 deletions(-) create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-edit-file.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-read-file.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-run-shell.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-write-file.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-edit-file.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-read-file.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-run-shell.json create mode 100644 cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-write-file.json create mode 100644 web/src/components/ToolCard/knownTools.test.tsx diff --git a/cli/src/agent/backends/acp/AcpMessageHandler.test.ts b/cli/src/agent/backends/acp/AcpMessageHandler.test.ts index af28ace0..4bd3a219 100644 --- a/cli/src/agent/backends/acp/AcpMessageHandler.test.ts +++ b/cli/src/agent/backends/acp/AcpMessageHandler.test.ts @@ -1022,4 +1022,382 @@ describe('AcpMessageHandler', () => { expect(result.output).toEqual(unknownContent); }); }); + + describe('tool_call input fallback from kind+title (Gemini sends neither rawInput nor JSON thought)', () => { + it('derives { file_path } from read kind + title', () => { + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-read', + title: 'README.md', + kind: 'read', + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toEqual({ file_path: 'README.md' }); + }); + + it('derives { command } from execute kind + title', () => { + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-exec', + title: 'ls -la /tmp', + kind: 'execute', + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toEqual({ command: 'ls -la /tmp' }); + }); + + it('derives { pattern } from search kind + title', () => { + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-search', + title: "'**/AGENTS.md'", + kind: 'search', + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toEqual({ pattern: "'**/AGENTS.md'" }); + }); + + it('keeps input null for think kind (no semantic args mapping)', () => { + // think tool_calls carry topic-update text in title that has no clean + // mapping to a tool argument shape. Better to leave input null than to + // fabricate a misleading derived object. + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-think', + title: 'Update topic to: "Researching Project Overview"', + kind: 'think', + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toBeNull(); + }); + + it('keeps input null for unknown kind (conservative — only known kinds derive)', () => { + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-unknown', + title: 'something exotic', + kind: 'futuristic_kind', + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toBeNull(); + }); + + it('keeps input null when title is missing even for known kind', () => { + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-no-title', + kind: 'read', + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toBeNull(); + }); + + it('derives { file_path } from edit kind + locations[0].path (write/edit case)', () => { + // Gemini emits write_file / replace under kind="edit" with rawInput + // absent. The path lives on `locations[0].path` from the very first + // tool_call event (title is prose like "Writing to foo.txt", which + // is not a file_path candidate). + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-edit', + title: 'Writing to foo.txt', + kind: 'edit', + locations: [{ path: '/abs/path/foo.txt' }], + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toEqual({ file_path: '/abs/path/foo.txt' }); + }); + + it('keeps input null for edit kind when locations is empty (no path to derive)', () => { + // Title like "Writing to foo.txt" is prose, not a file path — + // synthesizing a file_path from it would be misleading. + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-edit-no-loc', + title: 'Writing to foo.txt', + kind: 'edit', + locations: [], + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toBeNull(); + }); + + it('rawInput wins over kind+title fallback (regression guard)', () => { + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-raw-wins', + title: 'README.md', + kind: 'read', + rawInput: { file_path: 'EXPLICIT.md', extra: 'flag' }, + status: 'in_progress' + }); + + const toolCall = messages.find( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCall!.input).toEqual({ file_path: 'EXPLICIT.md', extra: 'flag' }); + }); + + it('applies the same fallback on tool_call_update (when rawInput stays absent)', () => { + // tool_call_update may be the first place we learn kind/title for a + // call that started as a placeholder. The fallback must still derive. + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((message) => messages.push(message)); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCall, + toolCallId: 'fb-update', + kind: 'execute', + title: 'ls -la /tmp', + status: 'in_progress' + }); + + handler.handleUpdate({ + sessionUpdate: ACP_SESSION_UPDATE_TYPES.toolCallUpdate, + toolCallId: 'fb-update', + kind: 'execute', + title: 'ls -la /tmp', + status: 'completed', + content: [{ type: 'content', content: { type: 'text', text: 'demo\n' } }] + }); + + const calls = messages.filter( + (m): m is Extract => m.type === 'tool_call' + ); + // Both initial and update emit a tool_call with derived input. + expect(calls.length).toBeGreaterThanOrEqual(1); + for (const tc of calls) { + expect(tc.input).toEqual({ command: 'ls -la /tmp' }); + } + }); + }); + + describe('real Gemini ACP fixtures (PR evidence)', () => { + // Each fixture was captured from a live Gemini CLI session via ACP stdio. + // These tests lock in the current behaviour under SHA a6f9379 so that + // future changes to AcpMessageHandler cannot silently regress the + // Gemini-specific handling. + // + // Observation from captured gemini-3-flash-preview: Gemini does NOT + // include rawInput in tool_call events and emits prose (non-JSON) + // thoughts. There is therefore no JSON-thought-hoisting trigger — + // tool_call input is null and the thought text surfaces as reasoning. + const fixtureDir = new URL('./__fixtures__', import.meta.url).pathname; + + const fixtures = [ + { + // read_file capture has zero agent_thought_chunk events: this + // model expresses reasoning as a `kind: think` tool_call rather + // than as a thought chunk, so the reasoning channel is empty. + name: 'gemini-3-flash-preview / read_file', + file: `${fixtureDir}/gemini-3-flash-preview-read-file.json`, + expectedMinToolCalls: 2, + expectedMinReasoning: 0, + hasMessageChunks: true, + }, + { + name: 'gemini-3-flash-preview / run_shell', + file: `${fixtureDir}/gemini-3-flash-preview-run-shell.json`, + expectedMinToolCalls: 1, + expectedMinReasoning: 1, + hasMessageChunks: true, + }, + { + // write_file: kind=edit, locations carries the file path. + // Same shape (and zero thought chunks) as read_file. + name: 'gemini-3-flash-preview / write_file', + file: `${fixtureDir}/gemini-3-flash-preview-write-file.json`, + expectedMinToolCalls: 2, + expectedMinReasoning: 0, + hasMessageChunks: true, + }, + { + // replace (in-place edit): same kind=edit + locations pattern. + name: 'gemini-3-flash-preview / edit_file', + file: `${fixtureDir}/gemini-3-flash-preview-edit-file.json`, + expectedMinToolCalls: 2, + expectedMinReasoning: 0, + hasMessageChunks: true, + }, + // ── gemini-3.1-pro-preview captures (live ACP, 2026-05-04) ── + // Same handler shape (rawInput omitted, kind+title fallback drives + // input derivation). The pro tier reuses the same think/read/ + // execute/edit kinds and emits prose thoughts (not JSON), so the + // assertions below match the flash captures. + { + name: 'gemini-3.1-pro-preview / read_file', + file: `${fixtureDir}/gemini-3.1-pro-preview-read-file.json`, + expectedMinToolCalls: 2, + expectedMinReasoning: 0, + hasMessageChunks: true, + }, + { + // run_shell: pro emits a single agent_thought_chunk in addition + // to the execute tool_call. + name: 'gemini-3.1-pro-preview / run_shell', + file: `${fixtureDir}/gemini-3.1-pro-preview-run-shell.json`, + expectedMinToolCalls: 1, + expectedMinReasoning: 1, + hasMessageChunks: true, + }, + { + // write_file: kind=edit, locations carries the file path. + name: 'gemini-3.1-pro-preview / write_file', + file: `${fixtureDir}/gemini-3.1-pro-preview-write-file.json`, + expectedMinToolCalls: 1, + expectedMinReasoning: 0, + hasMessageChunks: true, + }, + { + // replace (in-place edit): pro version interleaves think + read + // + edit kinds before the final agent_message_chunk burst. + name: 'gemini-3.1-pro-preview / edit_file', + file: `${fixtureDir}/gemini-3.1-pro-preview-edit-file.json`, + expectedMinToolCalls: 2, + expectedMinReasoning: 0, + hasMessageChunks: true, + }, + ] as const; + + for (const fx of fixtures) { + it(`replays ${fx.name} and produces sane AgentMessage stream`, () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const data = require(fx.file) as { + model: string; + scenario: string; + updates: unknown[]; + }; + + const messages: AgentMessage[] = []; + const handler = new AcpMessageHandler((m) => messages.push(m)); + for (const update of data.updates) { + handler.handleUpdate(update); + } + handler.flushText(); + + // ── tool_call: at least one must have been emitted ──────────────── + const toolCalls = messages.filter( + (m): m is Extract => m.type === 'tool_call' + ); + expect(toolCalls.length).toBeGreaterThanOrEqual(fx.expectedMinToolCalls); + + // ── tool_call.input: derived from kind+title fallback when rawInput + // and JSON thought are both absent. think kind has no semantic + // args mapping → input stays null; read/execute/search derive + // a typed object from the human-readable title. ─────────────── + // Identify think tool_calls by their original kind in the fixture + // (deriveToolNameWithSource uses title first, so tc.name is the + // title string for these — kind isn't on AgentMessage.tool_call). + const thinkIds = new Set(); + for (const update of data.updates) { + if (typeof update === 'object' && update !== null) { + const u = update as Record; + if (u.sessionUpdate === 'tool_call' && u.kind === 'think') { + const id = typeof u.toolCallId === 'string' ? u.toolCallId : null; + if (id) thinkIds.add(id); + } + } + } + for (const tc of toolCalls) { + if (thinkIds.has(tc.id)) { + expect(tc.input).toBeNull(); + } else { + // After fallback: read → {file_path}, execute → {command}, + // search → {pattern}. tc.input must be a truthy object. + expect(tc.input).not.toBeNull(); + expect(typeof tc.input).toBe('object'); + } + } + + // ── reasoning: at least one prose thought must have surfaced ─────── + const reasoningMsgs = messages.filter( + (m): m is Extract => m.type === 'reasoning' + ); + expect(reasoningMsgs.length).toBeGreaterThanOrEqual(fx.expectedMinReasoning); + + // ── no JSON reasoning leak: no reasoning message should be a bare + // JSON object that was accidentally not hoisted into a tool_call ── + for (const r of reasoningMsgs) { + const trimmed = r.text.trim(); + const isLeakedJson = trimmed.startsWith('{') && trimmed.endsWith('}'); + expect(isLeakedJson).toBe(false); + } + + // ── text messages: none should be a raw JSON blob ───────────────── + const textMsgs = messages.filter( + (m): m is Extract => m.type === 'text' + ); + for (const t of textMsgs) { + const trimmed = t.text.trim(); + // A text message should never be a bare JSON object + const looksLikeJson = trimmed.startsWith('{') && trimmed.endsWith('}'); + expect(looksLikeJson).toBe(false); + } + + // ── optional: assert text messages exist for complete captures ───── + if (fx.hasMessageChunks) { + expect(textMsgs.length).toBeGreaterThan(0); + } + }); + } + }); }); diff --git a/cli/src/agent/backends/acp/AcpMessageHandler.ts b/cli/src/agent/backends/acp/AcpMessageHandler.ts index 4feec31b..81fded50 100644 --- a/cli/src/agent/backends/acp/AcpMessageHandler.ts +++ b/cli/src/agent/backends/acp/AcpMessageHandler.ts @@ -22,6 +22,48 @@ function deriveToolNameFromUpdate(update: Record): DerivedToolN }); } +/** + * Fallback for ACP agents that omit `rawInput` and emit prose thoughts + * (no JSON-form to hoist). The `tool_call` event still carries a + * human-readable `title`, a structural `kind`, and (for file-touching tools) + * a `locations` array. For known kinds we synthesize a minimal input object + * so the UI does not display "Input: null" while the title shows + * "README.md" / "ls -la /tmp". + * + * Conservative on purpose: + * - `read` / `execute` / `search` derive from `title`, which in those kinds + * is the verbatim path / command / pattern. + * - `edit` (file-write / file-replace) derives from `locations[0].path`; + * its title is prose ("Writing to foo.txt"), so the path must come from + * the structured locations field, not the title. + * - `think` stays null — its title carries topic-update prose with no clean + * argument mapping; fabricating one would mislead. + * - Unknown kinds fall through to null rather than guessing a shape. + */ +function deriveInputFromKindAndTitle( + kind: string | null, + title: string | null, + locations: unknown +): Record | null { + if (kind === 'edit') { + const arr = Array.isArray(locations) ? locations : []; + const first = arr[0]; + const path = isObject(first) ? asString(first.path) : null; + return path ? { file_path: path } : null; + } + if (!title) return null; + switch (kind) { + case 'read': + return { file_path: title }; + case 'execute': + return { command: title }; + case 'search': + return { pattern: title }; + default: + return null; + } +} + function extractTextContent(block: unknown): string | null { if (!isObject(block)) return null; if (block.type !== 'text') return null; @@ -324,7 +366,11 @@ export class AcpMessageHandler { const derivedName = deriveToolNameFromUpdate(update); const name = derivedName.name; - const input = update.rawInput ?? null; + // Priority: rawInput > kind+title fallback. + // Use `in` to distinguish "rawInput key absent" from "rawInput is {}". + const input = 'rawInput' in update + ? update.rawInput + : deriveInputFromKindAndTitle(asString(update.kind), asString(update.title), update.locations); const status = normalizeStatus(update.status); this.toolCalls.set(toolCallId, { name, input }); @@ -357,14 +403,31 @@ export class AcpMessageHandler { input, status }); - } else if (existing && (status === 'in_progress' || status === 'pending')) { - this.onMessage({ - type: 'tool_call', - id: toolCallId, - name: existing.name, - input: existing.input, - status - }); + } else if (existing) { + // Enrich existing.input from update's kind+title when initial tool_call + // had neither rawInput nor a hoistable thought. Re-emit when we just + // enriched the input or when the call is still active. + let input = existing.input; + let name = existing.name; + if (input == null) { + const fallback = deriveInputFromKindAndTitle(asString(update.kind), asString(update.title), update.locations); + if (fallback) { + input = fallback; + const derivedName = deriveToolNameFromUpdate(update); + name = this.selectToolNameForUpdate(existing.name ?? null, derivedName); + this.toolCalls.set(toolCallId, { name, input }); + } + } + const justEnriched = existing.input == null && input != null; + if (status === 'in_progress' || status === 'pending' || justEnriched) { + this.onMessage({ + type: 'tool_call', + id: toolCallId, + name, + input, + status + }); + } } if (status === 'completed' || status === 'failed') { diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-edit-file.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-edit-file.json new file mode 100644 index 00000000..abf932c1 --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-edit-file.json @@ -0,0 +1,84 @@ +{ + "model": "gemini-3-flash-preview", + "scenario": "edit_file", + "prompt": "Please edit the file /home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt and replace the first line 'hello write demo line one' with 'UPDATED first line'.", + "captured_at": "2026-04-30T09:14:45.000Z", + "updates": [ + { + "sessionUpdate": "tool_call", + "toolCallId": "update_topic-1777540485086-5", + "status": "in_progress", + "title": "Update topic to: \"Updating write-demo.txt\"", + "content": [], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "update_topic-1777540485086-5", + "status": "completed", + "title": "Update topic to: \"Updating write-demo.txt\"", + "content": [ + { + "type": "content", + "content": { + "type": "text", + "text": "## 📂 Topic: **Updating write-demo.txt**\n\n**Summary:**\nThe user requested to replace the first line of the previously created file. I am using the replace tool to change 'hello write demo line one' to 'UPDATED first line' in /home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt.\n\n> [!STRATEGY]\n> **Intent:** Update the first line of write-demo.txt as requested." + } + } + ], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "replace-1777540485090-6", + "status": "in_progress", + "title": "write-demo.txt: hello write demo line one => UPDATED first line", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "replace-1777540485090-6", + "status": "completed", + "title": "write-demo.txt: hello write demo line one => UPDATED first line", + "content": [ + { + "type": "diff", + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt", + "oldText": "hello write demo line one\nhello line two", + "newText": "UPDATED first line\nhello line two", + "_meta": { + "kind": "modify" + } + } + ], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`write-demo.txt` 파일의 첫 번째 줄을 `UPDATED first line" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`으로 수정했습니다." + } + } + ] +} diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-read-file.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-read-file.json new file mode 100644 index 00000000..5446a93f --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-read-file.json @@ -0,0 +1,172 @@ +{ + "model": "gemini-3-flash-preview", + "scenario": "read_file", + "prompt": "Please read README.md in the current directory and tell me what it contains.", + "captured_at": "2026-04-30T03:18:23.000Z", + "updates": [ + { + "sessionUpdate": "tool_call", + "toolCallId": "update_topic-1777518787583-1", + "status": "in_progress", + "title": "Update topic to: \"Initial Project Overview\"", + "content": [], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "update_topic-1777518787583-1", + "status": "completed", + "title": "Update topic to: \"Initial Project Overview\"", + "content": [ + { + "type": "content", + "content": { + "type": "text", + "text": "## 📂 Topic: **Initial Project Overview**\n\n**Summary:**\nI will start by reading the README.md file in the project root to understand the project's purpose and contents as requested by the user.\n\n> [!STRATEGY]\n> **Intent:** Reading README.md to provide a summary of its contents." + } + } + ], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "read_file-1777518787614-2", + "status": "in_progress", + "title": "README.md", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/README.md" + } + ], + "kind": "read" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "read_file-1777518787614-2", + "status": "completed", + "title": "README.md", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/README.md" + } + ], + "kind": "read" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "The `README.md` file for the **hapi CLI** project describes it as a" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " tool to run and remotely control various AI agent sessions (Claude Code, Codex, Cursor Agent, Gemini, and OpenCode)" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " from the terminal via a \"hapi hub.\"\n\n### Key Features\n* **Multi-Agent Support:** Integrates with Claude CLI" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ", Cursor Agent, Codex, Gemini (via ACP), and OpenCode.\n* **Remote Control:** Sessions can be monitored" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " and controlled through a web app or Telegram Mini App.\n* **Runner Service:** Manages long-running background sessions" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " and provides diagnostics.\n* **MCP Bridge:** Includes an MCP stdio bridge for external tools.\n\n### Core Commands\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "* **Session Management:** `hapi` (Claude), `hapi codex`, `hapi cursor`, `hapi gemini`, " + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`hapi opencode`.\n* **Runner Control:** `hapi runner start/stop/status/list/logs`." + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "\n* **Authentication:** `hapi auth login/logout/status`.\n* **Diagnostics:** `hapi doctor` and" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " `hapi doctor clean`.\n\n### Configuration & Requirements\n* **Environment Variables:** Requires `CLI_API_TOKEN` and" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " `HAPI_API_URL`.\n* **Dependencies:** Needs the respective agent CLIs (Claude, Cursor," + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " OpenCode) installed and Bun for building from source.\n* **Storage:** Defaults to `~/.hapi/` for" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " settings and logs.\n\n### Project Structure\n* `src/api/`: Communication logic (Socket.IO + REST).\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "* `src/agent/`, `src/claude/`, `src/codex/`, etc.: Specific agent integrations" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ".\n* `src/runner/`: Background service implementation.\n* `src/modules/`: Tool" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "ing like ripgrep and difftastic." + } + } + ] +} \ No newline at end of file diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-run-shell.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-run-shell.json new file mode 100644 index 00000000..4ccd636b --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-run-shell.json @@ -0,0 +1,61 @@ +{ + "model": "gemini-3-flash-preview", + "scenario": "run_shell", + "prompt": "Please list files in /tmp using ls -la /tmp", + "captured_at": "2026-04-30T03:18:23.000Z", + "updates": [ + { + "sessionUpdate": "agent_thought_chunk", + "content": { + "type": "text", + "text": "[current working directory /home/lupin/workspace/hapi-fix-gemini-display/cli] (Listing files in /tmp to see the current directory contents.)" + } + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "run_shell_command-1777518846676-1", + "status": "in_progress", + "title": "ls -la /tmp", + "content": [], + "locations": [], + "kind": "execute" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "run_shell_command-1777518846676-1", + "status": "completed", + "title": "ls -la /tmp", + "content": [], + "locations": [], + "kind": "execute" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "The `/tmp` directory contains numerous files and subdirectories, many of which appear to be" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " related to `hapi`, `gemini`, and various E2E testing artifacts (e.g., `hapi-cli" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ".log`, `gemini-client-error-...json`, `hapi-e2e-...` directories). " + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "\n\nIs there a specific file or pattern you're looking for within these results?" + } + } + ] +} \ No newline at end of file diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-write-file.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-write-file.json new file mode 100644 index 00000000..9c916aa0 --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3-flash-preview-write-file.json @@ -0,0 +1,84 @@ +{ + "model": "gemini-3-flash-preview", + "scenario": "write_file", + "prompt": "Please create a file at /home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt with exactly this content: 'hello write demo line one\\nhello line two'.", + "captured_at": "2026-04-30T09:14:15.000Z", + "updates": [ + { + "sessionUpdate": "tool_call", + "toolCallId": "update_topic-1777540455288-3", + "status": "in_progress", + "title": "Update topic to: \"Creating file in workspace\"", + "content": [], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "update_topic-1777540455288-3", + "status": "completed", + "title": "Update topic to: \"Creating file in workspace\"", + "content": [ + { + "type": "content", + "content": { + "type": "text", + "text": "## 📂 Topic: **Creating file in workspace**\n\n**Summary:**\nThe user requested creating write-demo.txt within the allowed workspace directory. I am now creating the file /home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt with the specified two lines of content.\n\n> [!STRATEGY]\n> **Intent:** Create write-demo.txt in the workspace root." + } + } + ], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "write_file-1777540455292-4", + "status": "in_progress", + "title": "Writing to write-demo.txt", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "write_file-1777540455292-4", + "status": "completed", + "title": "Writing to write-demo.txt", + "content": [ + { + "type": "diff", + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt", + "oldText": "", + "newText": "hello write demo line one\nhello line two", + "_meta": { + "kind": "add" + } + } + ], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/write-demo.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`/home/lupin/workspace/hapi-fix-gemini-display" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "/cli/write-demo.txt` 파일을 요청하신 내용으로 생성했습니다." + } + } + ] +} diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-edit-file.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-edit-file.json new file mode 100644 index 00000000..c2c7a248 --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-edit-file.json @@ -0,0 +1,143 @@ +{ + "model": "gemini-3.1-pro-preview", + "scenario": "edit_file", + "prompt": "Please replace 'pro fixture line' with 'pro fixture updated' in cli/pr-fixture-write.txt", + "captured_at": "2026-05-04T03:08:46.000Z", + "updates": [ + { + "sessionUpdate": "tool_call", + "toolCallId": "update_topic-1777864104458-1", + "status": "in_progress", + "title": "Update topic to: \"Updating PR Fixture Write\"", + "content": [], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "update_topic-1777864104458-1", + "status": "completed", + "title": "Update topic to: \"Updating PR Fixture Write\"", + "content": [ + { + "type": "content", + "content": { + "type": "text", + "text": "## 📂 Topic: **Updating PR Fixture Write**\n\n**Summary:**\nI am going to read the contents of cli/pr-fixture-write.txt so I can precisely target the replacement of 'pro fixture line' with 'pro fixture updated'.\n\n> [!STRATEGY]\n> **Intent:** Read cli/pr-fixture-write.txt to get context for replacement" + } + } + ], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "read_file-1777864104493-2", + "status": "in_progress", + "title": "cli/pr-fixture-write.txt", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt" + } + ], + "kind": "read" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "read_file-1777864104493-2", + "status": "completed", + "title": "cli/pr-fixture-write.txt", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt" + } + ], + "kind": "read" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "replace-1777864108456-3", + "status": "in_progress", + "title": "cli/pr-fixture-write.txt: pro fixture line => pro fixture updated", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "replace-1777864108456-3", + "status": "completed", + "title": "cli/pr-fixture-write.txt: pro fixture line => pro fixture updated", + "content": [ + { + "type": "diff", + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt", + "oldText": "pro fixture line\n", + "newText": "pro fixture updated\n", + "_meta": { + "kind": "modify" + } + } + ], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "update_topic-1777864111933-4", + "status": "in_progress", + "title": "Update topic to: \"Finished PR Fixture Update\"", + "content": [], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "update_topic-1777864111933-4", + "status": "completed", + "title": "Update topic to: \"Finished PR Fixture Update\"", + "content": [ + { + "type": "content", + "content": { + "type": "text", + "text": "## 📂 Topic: **Finished PR Fixture Update**\n\n**Summary:**\nI successfully replaced 'pro fixture line' with 'pro fixture updated' in cli/pr-fixture-write.txt.\n\n> [!STRATEGY]\n> **Intent:** Finished replacing text in the PR fixture file." + } + } + ], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`cli/pr-fixture" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-write.txt` 파일의 내용을 성공적으로 업데이트했습니다. 다른 필요하신 작업이 있다면 말씀" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "해 주세요." + } + } + ] +} diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-read-file.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-read-file.json new file mode 100644 index 00000000..c81940f6 --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-read-file.json @@ -0,0 +1,249 @@ +{ + "model": "gemini-3.1-pro-preview", + "scenario": "read_file", + "prompt": "Please read README.md and tell me its contents.", + "captured_at": "2026-05-04T03:08:46.000Z", + "updates": [ + { + "sessionUpdate": "tool_call", + "toolCallId": "update_topic-1777863876330-1", + "status": "in_progress", + "title": "Update tactical intent: \"Read the contents of the README.md file and present it to the user.\"", + "content": [], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "update_topic-1777863876330-1", + "status": "completed", + "title": "Update tactical intent: \"Read the contents of the README.md file and present it to the user.\"", + "content": [ + { + "type": "content", + "content": { + "type": "text", + "text": "> [!STRATEGY]\n> **Intent:** Read the contents of the README.md file and present it to the user." + } + } + ], + "locations": [], + "kind": "think" + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "read_file-1777863879509-2", + "status": "in_progress", + "title": "README.md", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/README.md" + } + ], + "kind": "read" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "read_file-1777863879509-2", + "status": "completed", + "title": "README.md", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/README.md" + } + ], + "kind": "read" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "Here are the contents of the `README.md` file" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ":\n\n```markdown\n# HAPI\n\nRun official Claude Code / Codex / Gemini / OpenCode sessions locally and control" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " them remotely through a Web / PWA / Telegram Mini App.\n\n> **Why HAPI?** HAPI is" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " a local-first alternative to Happy. See [Why Not Happy?](docs/guide/why-hapi.md" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ") for the key differences.\n\n## Features\n\n- **Seamless Handoff** - Work locally, switch to remote" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " when needed, switch back anytime. No context loss, no session restart.\n- **Native First** - HAPI" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " wraps your AI agent instead of replacing it. Same terminal, same experience, same muscle memory.\n- **AFK" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " Without Stopping** - Step away from your desk? Approve AI requests from your phone with one tap.\n- **Your" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " AI, Your Choice** - Claude Code, Codex, Cursor Agent, Gemini, OpenCode—different models, one unified" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " workflow.\n- **Terminal Anywhere** - Run commands from your phone or browser, directly connected to the working machine." + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "\n- **Voice Control** - Talk to your AI agent hands-free using the built-in voice assistant.\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "- **Workspace Browser** - Opt-in via `hapi runner start --workspace-root `: browse a" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " scoped file tree from the web and start sessions in any subdirectory.\n\n## Demo\n\nhttps://github.com/user" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-attachments/assets/38230353-94c6-4dbe-9" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "c29-b2a2cc457546\n\n## Getting Started\n\n```bash\nn" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "px @twsxtd/hapi hub --relay # start hub with E2E encrypted relay\nnpx" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " @twsxtd/hapi # run claude code\n```\n\n`hapi server` remains supported as an" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " alias.\n\nThe terminal will display a URL and QR code. Scan the QR code with your phone or open the URL" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " to access.\n\n> The relay uses WireGuard + TLS for end-to-end encryption. Your data is encrypted" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " from your device to your machine.\n\nFor self-hosted options (Cloudflare Tunnel, Tailscale), see [Installation" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "](docs/guide/installation.md)\n\n## Docs\n\n- [App](docs/guide/pwa." + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "md)\n- [How it Works](docs/guide/how-it-works.md)\n- [" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "Cursor Agent](docs/guide/cursor.md)\n- [Voice Assistant](docs/guide/voice-assistant" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ".md)\n- [Why HAPI](docs/guide/why-hapi.md)\n- [FAQ](" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "docs/guide/faq.md)\n\n## Build from source\n\n```bash\nbun install\nbun run build:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "single-exe\n```\n\n## Credits\n\nHAPI means \"哈皮\" a Chinese transliteration of [Happy](" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "https://github.com/slopus/happy). Great credit to the original project.\n```" + } + } + ] +} diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-run-shell.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-run-shell.json new file mode 100644 index 00000000..7d75b9ed --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-run-shell.json @@ -0,0 +1,593 @@ +{ + "model": "gemini-3.1-pro-preview", + "scenario": "run_shell", + "prompt": "Please run ls -la /tmp", + "captured_at": "2026-05-04T03:08:46.000Z", + "updates": [ + { + "sessionUpdate": "agent_thought_chunk", + "content": { + "type": "text", + "text": "[current working directory /home/lupin/workspace/hapi-fix-gemini-display/cli]" + } + }, + { + "sessionUpdate": "tool_call", + "toolCallId": "run_shell_command-1777863968255-1", + "status": "in_progress", + "title": "ls -la /tmp", + "content": [], + "locations": [], + "kind": "execute" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "run_shell_command-1777863968255-1", + "status": "completed", + "title": "ls -la /tmp", + "content": [], + "locations": [], + "kind": "execute" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`/tmp` 디렉토리의 전체 내용을 확인했습니다.\n\n```text\n합계 64" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "\ndrwxrwxrwt 28 root root 820 5월 4 12:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "06 .\ndrwxr-xr-x 24 root root 4096 3월" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 23 12:41 ..\n-rw-rw-r-- 1 lupin lupin" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 0 5월 4 09:23 .1dee76fdf275f" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "3fb-00000000.hm\n-rw-rw-r-- 1 lup" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "in lupin 0 5월 4 11:33 .58be3d37" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "f7f9f78f-00000000.hm\ndrwxrwxrwt" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 2 root root 80 5월 3 14:53 .ICE-unix\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "drwxrwxrwt 2 root root 80 5월 3 14:53 ." + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "X11-unix\n-r--r--r-- 1 lupin lupin 11 5" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "월 3 14:53 .X2-lock\ndrwxrwxrwt 2 root root" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 40 5월 3 14:53 .XIM-unix\n-rw-rw" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-r-- 1 lupin lupin 0 5월 4 06:01 .f" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "9ee69f7fe56ffe2-00000000.hm\ndr" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "wxrwxrwt 2 root root 40 5월 3 14:53 .font" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-unix\n-rw-rw-r-- 1 lupin lupin 44 5월 4" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 09:26 cap-token\n-rw-rw-r-- 1 lupin lupin " + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "16482 5월 4 09:26 capture-after-prod.log\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-rw-rw-r-- 1 lupin lupin 14581 5월 4" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 10:26 capture-upstream-shell.log\n-rw-rw-r-- 1 lup" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "in lupin 292 5월 4 09:48 cera-fallback-fix" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ".json\n-rw-rw-r-- 1 lupin lupin 1512 5월" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 4 09:27 cera_oauth_copy.py\ndrwxrwxr-x 3" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " lupin lupin 60 5월 3 15:31 claude-100" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "0\nsrwxrwxrwx 1 gdm gdm 0 5월 3 14:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "53 dbus-A6icOtKae1\nsrwxrwxrwx 1 lupin lupin " + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "0 5월 3 14:53 dbus-Me8LOgQTKo\ndrwx" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "------ 2 lupin lupin 40 5월 4 12:06 gemini-" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "shell-4tEX6p\n-rw-r--r-- 1 root root 129" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "5 5월 3 14:54 glances-root.log\ndrwxrwxr-x " + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "3 lupin lupin 180 5월 4 12:03 hapi-blo" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "bs\ndrwx------ 4 lupin lupin 240 5월 4 09:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "25 hapi-e2e-3QZ3Gx\ndrwx------ 4 lupin lupin" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 240 5월 4 12:05 hapi-e2e-pro-" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "HW0qZ3\ndrwxrwxr-x 2 lupin lupin 80 5월 " + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "4 12:06 hapi-pro-dumps\n-rw-rw-r-- 1 lup" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "in lupin 703 5월 4 11:24 ollama-tool-test" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ".json\ndrwx------ 2 lupin lupin 40 5월 3 14:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "53 pulse-PKdhtXMmr18n\ndrwx------ 4 root root 80" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " 5월 3 15:00 snap-private-tmp\ndrwx------ 2 lupin" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": " lupin 60 5월 3 14:53 ssh-SvNZWI2B9" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "Tej\ndrwx------ 3 root root 60 5월 3 14:5" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "3 systemd-private-d874888c787746e688" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "10133c52700431-ModemManager.service-xQh" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "IT9\ndrwx------ 3 root root 60 5월 3 15:0" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "0 systemd-private-d874888c787746e688" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "10133c52700431-bluetooth.service-KFbHeY\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "drwx------ 3 root root 60 5월 3 14:53 systemd" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-private-d874888c787746e688101" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "33c52700431-colord.service-V4OfyG\ndr" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "wx------ 3 root root 60 5월 3 15:00 systemd-" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "private-d874888c787746e6881013" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "3c52700431-fwupd.service-gYJmq8\ndr" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "wx------ 3 root root 60 5월 3 14:53 systemd-" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "private-d874888c787746e6881013" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "3c52700431-polkit.service-OQ18zt\ndrwx" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "------ 3 root root 60 5월 3 14:53 systemd-private" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-d874888c787746e68810133" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "c52700431-power-profiles-daemon.service-pFstLv\ndrwx" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "------ 3 root root 60 5월 3 14:53 systemd-private" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-d874888c787746e68810133" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "c52700431-switcheroo-control.service-QGDDCx\ndr" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "wx------ 3 root root 60 5월 3 14:53 systemd-" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "private-d874888c787746e6881013" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "3c52700431-systemd-logind.service-pxhf1v\n" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "drwx------ 3 root root 60 5월 3 14:53 systemd" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "-private-d874888c787746e688101" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "33c52700431-systemd-oomd.service-BSBK5N" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "\ndrwx------ 3 root root 60 5월 3 14:53 system" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "d-private-d874888c787746e68810" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "133c52700431-systemd-resolved.service-NQjZbe" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "\ndrwx------ 3 root root 60 5월 3 14:53 system" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "d-private-d874888c787746e68810" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "133c52700431-systemd-timesyncd.service-LiY" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "91S\ndrwx------ 3 root root 60 5월 3 14:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "53 systemd-private-d874888c787746e68" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "810133c52700431-upower.service-Ltkc" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "Md\ndrwx------ 2 lupin lupin 100 5월 3 14:" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "53 tigervnc.KPzKth\n```\n\n추가로 확인이 필요한 사항이 있으면 말씀해 주십시오." + } + } + ] +} diff --git a/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-write-file.json b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-write-file.json new file mode 100644 index 00000000..fdb810f4 --- /dev/null +++ b/cli/src/agent/backends/acp/__fixtures__/gemini-3.1-pro-preview-write-file.json @@ -0,0 +1,58 @@ +{ + "model": "gemini-3.1-pro-preview", + "scenario": "write_file", + "prompt": "Please create a new file at cli/pr-fixture-write.txt with the content 'pro fixture line'", + "captured_at": "2026-05-04T03:08:46.000Z", + "updates": [ + { + "sessionUpdate": "tool_call", + "toolCallId": "write_file-1777864040205-1", + "status": "in_progress", + "title": "Writing to cli/pr-fixture-write.txt", + "content": [], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "tool_call_update", + "toolCallId": "write_file-1777864040205-1", + "status": "completed", + "title": "Writing to cli/pr-fixture-write.txt", + "content": [ + { + "type": "diff", + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt", + "oldText": "", + "newText": "pro fixture line", + "_meta": { + "kind": "add" + } + } + ], + "locations": [ + { + "path": "/home/lupin/workspace/hapi-fix-gemini-display/cli/pr-fixture-write.txt" + } + ], + "kind": "edit" + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "`cli/pr-fixture-write" + } + }, + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": ".txt` 파일 생성을 완료했습니다. 요청하신 내용이 성공적으로 작성되었습니다." + } + } + ] +} diff --git a/web/src/components/ToolCard/ToolCard.tsx b/web/src/components/ToolCard/ToolCard.tsx index 112c8c6c..18738347 100644 --- a/web/src/components/ToolCard/ToolCard.tsx +++ b/web/src/components/ToolCard/ToolCard.tsx @@ -59,7 +59,11 @@ function getTaskSummaryChildren(block: ToolCallBlock): { visible: ToolCallBlock[ return { visible, remaining: children.length - visible.length } } -function renderTaskSummary(block: ToolCallBlock, metadata: SessionMetadataSummary | null): ReactNode | null { +function renderTaskSummary( + block: ToolCallBlock, + metadata: SessionMetadataSummary | null, + t: (key: string, params?: Record) => string, +): ReactNode | null { const summary = getTaskSummaryChildren(block) if (!summary) return null @@ -76,7 +80,7 @@ function renderTaskSummary(block: ToolCallBlock, metadata: SessionMetadataSummar - {formatTaskChildLabel(child, metadata)} + {formatTaskChildLabel(child, metadata, t)} @@ -265,19 +269,20 @@ function ToolCardInner(props: ToolCardProps) { childrenCount: props.block.children.length, description: props.block.tool.description, metadata: props.metadata - }), [ + }, t), [ props.block.tool.name, props.block.tool.input, props.block.tool.result, props.block.children.length, props.block.tool.description, - props.metadata + props.metadata, + t ]) const toolName = props.block.tool.name const toolTitle = presentation.title const subtitle = presentation.subtitle ?? props.block.tool.description - const taskSummary = renderTaskSummary(props.block, props.metadata) + const taskSummary = renderTaskSummary(props.block, props.metadata, t) const runningFrom = props.block.tool.startedAt ?? props.block.tool.createdAt const showInline = !presentation.minimal && toolName !== 'Task' const CompactToolView = showInline ? getToolViewComponent(toolName) : null diff --git a/web/src/components/ToolCard/helpers.tsx b/web/src/components/ToolCard/helpers.tsx index 56a57b30..3ad8e893 100644 --- a/web/src/components/ToolCard/helpers.tsx +++ b/web/src/components/ToolCard/helpers.tsx @@ -11,6 +11,7 @@ import { truncate } from '@/lib/toolInputUtils' export function formatTaskChildLabel( child: ToolCallBlock, metadata: SessionMetadataSummary | null, + t?: (key: string, params?: Record) => string, ): string { const presentation = getToolPresentation({ toolName: child.tool.name, @@ -19,7 +20,7 @@ export function formatTaskChildLabel( childrenCount: child.children.length, description: child.tool.description, metadata, - }) + }, t) if (presentation.subtitle) { return truncate(`${presentation.title}: ${presentation.subtitle}`, 140) diff --git a/web/src/components/ToolCard/knownTools.test.tsx b/web/src/components/ToolCard/knownTools.test.tsx new file mode 100644 index 00000000..a7d22f84 --- /dev/null +++ b/web/src/components/ToolCard/knownTools.test.tsx @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest' +import { getToolPresentation } from '@/components/ToolCard/knownTools' + +describe('getToolPresentation — unknown tool semantic title + subtitle dedup', () => { + it('promotes semantic title "Run shell" when toolName equals input.command (Gemini ACP case)', () => { + const presentation = getToolPresentation({ + toolName: 'cat /tmp/hello.txt', + input: { command: 'cat /tmp/hello.txt' }, + result: null, + childrenCount: 0, + description: null, + metadata: null, + }) + + expect(presentation.title).toBe('Run shell') + expect(presentation.subtitle).toBe('cat /tmp/hello.txt') + }) + + it('promotes semantic title "Read file" when toolName equals input.file_path', () => { + const presentation = getToolPresentation({ + toolName: 'README.md', + input: { file_path: 'README.md' }, + result: null, + childrenCount: 0, + description: null, + metadata: null, + }) + + expect(presentation.title).toBe('Read file') + expect(presentation.subtitle).toBe('README.md') + }) + + it('promotes semantic title "Search" when toolName equals input.pattern', () => { + const presentation = getToolPresentation({ + toolName: '*.ts', + input: { pattern: '*.ts' }, + result: null, + childrenCount: 0, + description: null, + metadata: null, + }) + + expect(presentation.title).toBe('Search') + expect(presentation.subtitle).toBe('*.ts') + }) + + it('keeps the original toolName when subtitle differs (no promotion needed)', () => { + const presentation = getToolPresentation({ + toolName: 'run_shell_command', + input: { command: 'ls -la /tmp' }, + result: null, + childrenCount: 0, + description: null, + metadata: null, + }) + + expect(presentation.title).toBe('run_shell_command') + expect(presentation.subtitle).toBe('ls -la /tmp') + }) + + it('returns null subtitle when no recognized input field is present', () => { + const presentation = getToolPresentation({ + toolName: 'mystery_tool', + input: { foo: 'bar' }, + result: null, + childrenCount: 0, + description: null, + metadata: null, + }) + + expect(presentation.title).toBe('mystery_tool') + expect(presentation.subtitle).toBeNull() + }) +}) diff --git a/web/src/components/ToolCard/knownTools.tsx b/web/src/components/ToolCard/knownTools.tsx index 4b6ac59d..22e25f0e 100644 --- a/web/src/components/ToolCard/knownTools.tsx +++ b/web/src/components/ToolCard/knownTools.tsx @@ -446,7 +446,12 @@ export const knownTools: Record & { metadata: SessionMetadataSummary | null }): ToolPresentation { +type Translator = (key: string, params?: Record) => string + +export function getToolPresentation( + opts: Omit & { metadata: SessionMetadataSummary | null }, + t?: Translator +): ToolPresentation { if (opts.toolName.startsWith('mcp__')) { return { icon: , @@ -475,10 +480,25 @@ export function getToolPresentation(opts: Omit & { metadat const subtitle = filePath ?? command ?? pattern ?? url ?? query + // Some ACP agents emit `tool_call.title` as a verbatim argument (the shell + // command or the file path itself). When it equals the input field, + // promote a semantic label to the title slot and let the verbatim arg + // become the subtitle, so the card reads like a sentence instead of + // showing the same string twice. Labels are translated when a Translator + // is supplied; tests and call sites without i18n fall back to English. + let title = opts.toolName + if (subtitle && subtitle === title) { + if (filePath) title = t ? t('tool.semanticTitle.readFile') : 'Read file' + else if (command) title = t ? t('tool.semanticTitle.runShell') : 'Run shell' + else if (pattern) title = t ? t('tool.semanticTitle.search') : 'Search' + else if (url) title = t ? t('tool.semanticTitle.openUrl') : 'Open URL' + else if (query) title = t ? t('tool.semanticTitle.query') : 'Query' + } + return { icon: , - title: opts.toolName, - subtitle: subtitle ? truncate(subtitle, 80) : null, + title, + subtitle: subtitle && subtitle !== title ? truncate(subtitle, 80) : null, minimal: true } } diff --git a/web/src/components/ToolCard/trace.tsx b/web/src/components/ToolCard/trace.tsx index 6ed978fc..ad433861 100644 --- a/web/src/components/ToolCard/trace.tsx +++ b/web/src/components/ToolCard/trace.tsx @@ -197,7 +197,7 @@ type TraceChildRowProps = { function TraceChildRow({ child, metadata, expanded, onToggle }: TraceChildRowProps) { const { t } = useTranslation() - const label = formatTaskChildLabel(child, metadata) + const label = formatTaskChildLabel(child, metadata, t) const FullInputView = getToolFullViewComponent(child.tool.name) const ResultView = getToolResultViewComponent(child.tool.name) diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index 9b9fdbee..148a1dcc 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -201,6 +201,11 @@ export default { 'tool.trace': 'Trace', 'tool.trace.callsSuffix': 'calls', 'tool.result': 'Result', + 'tool.semanticTitle.readFile': 'Read file', + 'tool.semanticTitle.runShell': 'Run shell', + 'tool.semanticTitle.search': 'Search', + 'tool.semanticTitle.openUrl': 'Open URL', + 'tool.semanticTitle.query': 'Query', 'tool.questionsAnswers': 'Questions & Answers', 'tool.submit': 'Submit', 'tool.submitting': 'Submitting…', diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index 02759693..c01f97b9 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -203,6 +203,11 @@ export default { 'tool.trace': '追踪', 'tool.trace.callsSuffix': '次调用', 'tool.result': '结果', + 'tool.semanticTitle.readFile': '读取文件', + 'tool.semanticTitle.runShell': '运行命令', + 'tool.semanticTitle.search': '搜索', + 'tool.semanticTitle.openUrl': '打开链接', + 'tool.semanticTitle.query': '查询', 'tool.questionsAnswers': '问答', 'tool.submit': '提交', 'tool.submitting': '提交中…',