diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 99da1dd7..3f8440f4 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -39,6 +39,7 @@ import { createDynamicToolCallUpdate, createFileChangeUpdate, createMcpToolCallUpdate, + formatWebSearchTitle, } from "./CodexToolCallMapper"; import { createFastModeConfigOption, @@ -898,7 +899,7 @@ export class CodexAcpServer implements acp.Agent { sessionUpdate: "tool_call", toolCallId: item.id, kind: "search", - title: this.formatWebSearchTitle(item), + title: formatWebSearchTitle(item), status: "completed", rawInput: { query: item.query, @@ -958,29 +959,6 @@ export class CodexAcpServer implements acp.Agent { }; } - private formatWebSearchTitle(item: ThreadItem & { type: "webSearch" }): string { - const action = item.action; - if (!action) { - return item.query ? `Web search: ${item.query}` : "Web search"; - } - switch (action.type) { - case "search": { - const queries = action.queries?.filter((query) => query && query.length > 0) ?? []; - const query = action.query ?? (queries.length > 0 ? queries.join(", ") : null) ?? item.query; - return query ? `Web search: ${query}` : "Web search"; - } - case "openPage": - return action.url ? `Open page: ${action.url}` : "Open page"; - case "findInPage": { - const pattern = action.pattern ? ` for '${action.pattern}'` : ""; - const url = action.url ? ` in ${action.url}` : ""; - return `Find in page${pattern}${url}`.trim(); - } - case "other": - return "Web search"; - } - } - private toAcpToolCallStatus(status: CollabAgentToolCallStatus): "in_progress" | "completed" | "failed" { switch (status) { case "inProgress": diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 642ec40a..8cadd0bb 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -40,6 +40,8 @@ import { createFuzzyFileSearchComplete, createFuzzyFileSearchStartOrUpdate, createMcpToolCallUpdate, + createWebSearchCompleteUpdate, + createWebSearchStartUpdate, fuzzyFileSearchToolCallId, } from "./CodexToolCallMapper"; import { stripShellPrefix } from "./CommandUtils"; @@ -303,12 +305,13 @@ export class CodexEventHandler { return await createMcpToolCallUpdate(event.item); case "dynamicToolCall": return await createDynamicToolCallUpdate(event.item); + case "webSearch": + return createWebSearchStartUpdate(event.item); case "collabAgentToolCall": case "userMessage": case "hookPrompt": case "agentMessage": case "reasoning": - case "webSearch": case "imageView": case "imageGeneration": case "enteredReviewMode": @@ -348,11 +351,12 @@ export class CodexEventHandler { text: summary } } + case "webSearch": + return createWebSearchCompleteUpdate(event.item); case "collabAgentToolCall": case "userMessage": case "hookPrompt": case "agentMessage": - case "webSearch": case "imageView": case "imageGeneration": case "enteredReviewMode": diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index 06454aab..a2aeb548 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -33,6 +33,7 @@ type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed"; type GuardianApprovalReviewNotification = | ItemGuardianApprovalReviewStartedNotification | ItemGuardianApprovalReviewCompletedNotification; +type WebSearchItem = ThreadItem & { type: "webSearch" }; function toAcpStatus(status: CodexItemStatus): AcpToolCallStatus { switch (status) { @@ -229,6 +230,54 @@ export function createFuzzyFileSearchComplete( }; } +export function createWebSearchStartUpdate( + item: WebSearchItem +): UpdateSessionEvent { + return { + sessionUpdate: "tool_call", + toolCallId: item.id, + kind: "search", + title: formatWebSearchTitle(item), + status: "in_progress", + rawInput: item, + }; +} + +export function createWebSearchCompleteUpdate( + item: WebSearchItem +): UpdateSessionEvent { + return { + sessionUpdate: "tool_call_update", + toolCallId: item.id, + title: formatWebSearchTitle(item), + status: "completed", + rawInput: item, + }; +} + +export function formatWebSearchTitle(item: WebSearchItem): string { + const action = item.action; + if (!action) { + return item.query ? `Web search: ${item.query}` : "Web search"; + } + switch (action.type) { + case "search": { + const queries = action.queries?.filter((query) => query && query.length > 0) ?? []; + const query = action.query ?? (queries.length > 0 ? queries.join(", ") : null) ?? item.query; + return query ? `Web search: ${query}` : "Web search"; + } + case "openPage": + return action.url ? `Open page: ${action.url}` : "Open page"; + case "findInPage": { + const pattern = action.pattern ? ` for '${action.pattern}'` : ""; + const url = action.url ? ` in ${action.url}` : ""; + return `Find in page${pattern}${url}`.trim(); + } + case "other": + return "Web search"; + } +} + function createCommandActionEvent( id: string, status: CommandExecutionStatus, diff --git a/src/__tests__/CodexACPAgent/data/web-search-action-titles.json b/src/__tests__/CodexACPAgent/data/web-search-action-titles.json new file mode 100644 index 00000000..09bc7e92 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/web-search-action-titles.json @@ -0,0 +1,49 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "toolCallId": "web-open-1", + "kind": "search", + "title": "Open page: https://agentclientprotocol.com", + "status": "in_progress", + "rawInput": { + "type": "webSearch", + "id": "web-open-1", + "query": "https://agentclientprotocol.com", + "action": { + "type": "openPage", + "url": "https://agentclientprotocol.com" + } + } + } + } + ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "toolCallId": "web-find-1", + "kind": "search", + "title": "Find in page for 'tool calls' in https://agentclientprotocol.com/protocol", + "status": "in_progress", + "rawInput": { + "type": "webSearch", + "id": "web-find-1", + "query": "protocol", + "action": { + "type": "findInPage", + "url": "https://agentclientprotocol.com/protocol", + "pattern": "tool calls" + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/web-search-start-and-complete.json b/src/__tests__/CodexACPAgent/data/web-search-start-and-complete.json new file mode 100644 index 00000000..4b93beba --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/web-search-start-and-complete.json @@ -0,0 +1,49 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "toolCallId": "web-search-1", + "kind": "search", + "title": "Web search: agent client protocol", + "status": "in_progress", + "rawInput": { + "type": "webSearch", + "id": "web-search-1", + "query": "agent client protocol", + "action": { + "type": "search", + "query": "agent client protocol", + "queries": null + } + } + } + } + ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call_update", + "toolCallId": "web-search-1", + "title": "Web search: agent client protocol", + "status": "completed", + "rawInput": { + "type": "webSearch", + "id": "web-search-1", + "query": "agent client protocol", + "action": { + "type": "search", + "query": "agent client protocol", + "queries": null + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/web-search-events.test.ts b/src/__tests__/CodexACPAgent/web-search-events.test.ts new file mode 100644 index 00000000..da1074b1 --- /dev/null +++ b/src/__tests__/CodexACPAgent/web-search-events.test.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ServerNotification } from "../../app-server"; +import type { SessionState } from "../../CodexAcpServer"; +import { AgentMode } from "../../AgentMode"; +import { + createCodexMockTestFixture, + createTestSessionState, + setupPromptAndSendNotifications, + type CodexMockTestFixture +} from "../acp-test-utils"; + +describe("CodexEventHandler - web search events", () => { + let mockFixture: CodexMockTestFixture; + const sessionId = "test-session-id"; + + beforeEach(() => { + mockFixture = createCodexMockTestFixture(); + vi.clearAllMocks(); + }); + + const sessionState: SessionState = createTestSessionState({ + sessionId, + currentModelId: "model-id[effort]", + agentMode: AgentMode.DEFAULT_AGENT_MODE + }); + + it("maps web search start and completion to a search tool call", async () => { + const notifications: ServerNotification[] = [ + { + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-1", + startedAtMs: 0, + item: { + type: "webSearch", + id: "web-search-1", + query: "agent client protocol", + action: { + type: "search", + query: "agent client protocol", + queries: null, + }, + }, + }, + }, + { + method: "item/completed", + params: { + threadId: sessionId, + turnId: "turn-1", + completedAtMs: 0, + item: { + type: "webSearch", + id: "web-search-1", + query: "agent client protocol", + action: { + type: "search", + query: "agent client protocol", + queries: null, + }, + }, + }, + }, + ]; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/web-search-start-and-complete.json" + ); + }); + + it("formats open-page and find-in-page web search actions", async () => { + const notifications: ServerNotification[] = [ + { + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-1", + startedAtMs: 0, + item: { + type: "webSearch", + id: "web-open-1", + query: "https://agentclientprotocol.com", + action: { + type: "openPage", + url: "https://agentclientprotocol.com", + }, + }, + }, + }, + { + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-1", + startedAtMs: 0, + item: { + type: "webSearch", + id: "web-find-1", + query: "protocol", + action: { + type: "findInPage", + url: "https://agentclientprotocol.com/protocol", + pattern: "tool calls", + }, + }, + }, + }, + ]; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/web-search-action-titles.json" + ); + }); +});