From fc8cad0c8a29240a3c903cf8089b3437550462c4 Mon Sep 17 00:00:00 2001 From: weixiaoing <1537476031@qq.com> Date: Sat, 12 Sep 2026 14:02:13 +0800 Subject: [PATCH 1/2] fix(tool-calls): preserve query-bearing MCP names --- src/lib/tool-call-normalization.test.ts | 40 +++++++++++++++++++++++++ src/lib/tool-call-normalization.ts | 14 ++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/lib/tool-call-normalization.test.ts b/src/lib/tool-call-normalization.test.ts index 43aeca74b0..0677e91890 100644 --- a/src/lib/tool-call-normalization.test.ts +++ b/src/lib/tool-call-normalization.test.ts @@ -378,6 +378,46 @@ describe("inferLiveToolName meta.claudeCode.toolName override", () => { }) }) +describe("inferLiveToolName query-bearing MCP calls", () => { + it("keeps an explicit OpenCode MCP tool title", () => { + expect( + inferLiveToolName({ + title: "codegraph_explore", + kind: "other", + rawInput: JSON.stringify({ query: "find the auth flow" }), + }) + ).toBe("codegraph_explore") + }) + + it("still classifies a query as websearch when the wire names websearch", () => { + expect( + inferLiveToolName({ + title: "web_search", + kind: "other", + rawInput: JSON.stringify({ query: "Codeg" }), + }) + ).toBe("websearch") + + expect( + inferLiveToolName({ + title: "Search", + kind: "websearch", + rawInput: JSON.stringify({ query: "Codeg" }), + }) + ).toBe("websearch") + }) + + it("does not infer websearch from a query field alone", () => { + expect( + inferLiveToolName({ + title: "MCP: tool", + kind: "other", + rawInput: JSON.stringify({ query: "find usages" }), + }) + ).not.toBe("websearch") + }) +}) + describe("normalizeToolName collapses delegate_to_agent across hosts", () => { // The codeg multi-agent delegation MCP tool is named the same across hosts // (`delegate_to_agent`) but each host serializes the server prefix diff --git a/src/lib/tool-call-normalization.ts b/src/lib/tool-call-normalization.ts index a3873211b4..bb4310f4bf 100644 --- a/src/lib/tool-call-normalization.ts +++ b/src/lib/tool-call-normalization.ts @@ -389,7 +389,19 @@ function inferFromInput( return "edit" if (hasAnyKey(parsed, ["changes"])) return "edit" if (hasAnyKey(parsed, ["todos"])) return "todowrite" - if (hasAnyKey(parsed, ["query"])) return "websearch" + // `query` is a common MCP argument (for example CodeGraph's + // `codegraph_explore` and Context7's query tools), not a web-search + // discriminator. Only classify it as websearch when the wire also names a + // web-search tool; otherwise `inferLiveToolName` can preserve the explicit + // tool title instead of showing every query-bearing MCP call as "WebSearch". + if ( + hasAnyKey(parsed, ["query"]) && + (normalizedTitle === "websearch" || + normalizedTitle === "web_search" || + normalizedKind === "websearch" || + normalizedKind === "web_search") + ) + return "websearch" if (hasAnyKey(parsed, ["url"])) return "webfetch" const hasPattern = hasAnyKey(parsed, ["pattern"]) From a47c68a971de98e6a2ec2f3b3fc663e78474741f Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 12 Sep 2026 22:31:14 +0800 Subject: [PATCH 2/2] fix(tool-calls): preserve Codex web search actions --- src/lib/tool-call-normalization.test.ts | 49 +++++++++++++++++++++++++ src/lib/tool-call-normalization.ts | 30 +++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/lib/tool-call-normalization.test.ts b/src/lib/tool-call-normalization.test.ts index 0677e91890..3e9111f5ff 100644 --- a/src/lib/tool-call-normalization.test.ts +++ b/src/lib/tool-call-normalization.test.ts @@ -407,6 +407,47 @@ describe("inferLiveToolName query-bearing MCP calls", () => { ).toBe("websearch") }) + it("recognizes Codex web-search action frames", () => { + expect( + inferLiveToolName({ + title: "Open page: https://example.com", + kind: "search", + rawInput: JSON.stringify({ + query: "Codeg", + action: { type: "openPage", url: "https://example.com" }, + }), + }) + ).toBe("websearch") + + // session/load replay omits the `type` marker, but keeps the action title + // and payload. `kind: "search"` must not be enough on its own because + // local fuzzy-file searches use the same ACP kind. + expect( + inferLiveToolName({ + title: "Find in page for 'ACP' in https://example.com", + kind: "search", + rawInput: JSON.stringify({ + query: "Codeg", + action: { + type: "findInPage", + pattern: "ACP", + url: "https://example.com", + }, + }), + }) + ).toBe("websearch") + + // The marker is also sufficient when a title is too generic to identify + // the action on its own. + expect( + inferLiveToolName({ + title: "Search", + kind: "other", + rawInput: JSON.stringify({ type: "webSearch", query: "Codeg" }), + }) + ).toBe("websearch") + }) + it("does not infer websearch from a query field alone", () => { expect( inferLiveToolName({ @@ -415,6 +456,14 @@ describe("inferLiveToolName query-bearing MCP calls", () => { rawInput: JSON.stringify({ query: "find usages" }), }) ).not.toBe("websearch") + + expect( + inferLiveToolName({ + title: "Search for 'find usages'", + kind: "search", + rawInput: JSON.stringify({ query: "find usages" }), + }) + ).toBe("grep") }) }) diff --git a/src/lib/tool-call-normalization.ts b/src/lib/tool-call-normalization.ts index bb4310f4bf..18cdecec2e 100644 --- a/src/lib/tool-call-normalization.ts +++ b/src/lib/tool-call-normalization.ts @@ -257,6 +257,26 @@ function hasAnyKey(obj: Record, keys: string[]): boolean { ) } +/** + * Codex ACP uses human titles for web-search follow-up actions. Those frames + * keep `kind: "search"` (the same kind used by local fuzzy-file search), so + * the title is the only identity signal when the raw-input type is omitted on + * session replay. + */ +function isCodexWebSearchTitle(input: string | null | undefined): boolean { + const title = input?.trim() + if (!title) return false + return /^(?:web\s+search|open\s+page|find\s+in\s+page)(?:\s*:|\s|$)/i.test( + title + ) +} + +/** Codex's raw-input marker is camelCase on the ACP wire (`webSearch`). */ +function isCodexWebSearchType(input: unknown): boolean { + if (typeof input !== "string") return false + return canonicalizeToolName(input).replace(/_/g, "") === "websearch" +} + /** * Wire spellings that mean the same argument as one of the canonical * (snake_case) keys every tool card reads. OpenCode names its tool arguments in @@ -392,14 +412,18 @@ function inferFromInput( // `query` is a common MCP argument (for example CodeGraph's // `codegraph_explore` and Context7's query tools), not a web-search // discriminator. Only classify it as websearch when the wire also names a - // web-search tool; otherwise `inferLiveToolName` can preserve the explicit - // tool title instead of showing every query-bearing MCP call as "WebSearch". + // web-search tool, or when Codex's action title/type identifies the call; + // otherwise `inferLiveToolName` can preserve the explicit tool title instead + // of showing every query-bearing MCP call as "WebSearch". Codex's generic + // `kind: "search"` intentionally remains a local file search (`grep`). if ( hasAnyKey(parsed, ["query"]) && (normalizedTitle === "websearch" || normalizedTitle === "web_search" || normalizedKind === "websearch" || - normalizedKind === "web_search") + normalizedKind === "web_search" || + isCodexWebSearchTitle(title) || + isCodexWebSearchType(parsed.type)) ) return "websearch" if (hasAnyKey(parsed, ["url"])) return "webfetch"