From c8bbe9c2ce55116891a37938db1f32941bf73df8 Mon Sep 17 00:00:00 2001 From: Nikita Ashikhmin Date: Sat, 5 Sep 2026 17:41:34 +0400 Subject: [PATCH] fix: refresh Codex skill catalog in place --- src/AirExtension.ts | 2 + src/CodexAcpServer.ts | 42 +++++- src/CodexAppServerClient.ts | 10 ++ src/CodexCommands.ts | 60 +++++++-- src/CodexEventHandler.ts | 5 +- src/CodexToolCallMapper.ts | 31 ++++- .../CodexACPAgent/CodexAcpClient.test.ts | 60 +++++++++ .../command-action-events.test.ts | 43 ++++++ .../data/available-commands-presentation.json | 124 ++++++++++++++++++ .../CodexACPAgent/data/command-skills.json | 98 +++++++++++++- .../data/mcp-app-tool-presentation.json | 35 +++++ .../CodexACPAgent/initialize.test.ts | 9 +- 12 files changed, 494 insertions(+), 25 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/data/available-commands-presentation.json create mode 100644 src/__tests__/CodexACPAgent/data/mcp-app-tool-presentation.json diff --git a/src/AirExtension.ts b/src/AirExtension.ts index 03b12749..1a442e2d 100644 --- a/src/AirExtension.ts +++ b/src/AirExtension.ts @@ -16,6 +16,8 @@ export const AIR_SESSION_FAILURE_KEY = "sessionFailure"; export const AIR_AGENT_FILE_CHANGE_REPORT_KEY = "agentFileChangeReport"; export const AIR_NATIVE_SUBAGENT_SESSIONS_KEY = "nativeSubagentSessions"; export const AIR_ASYNC_TASKS_KEY = "asyncTasks"; +export const AIR_COMMAND_PRESENTATION_KEY = "commandPresentation"; +export const AIR_TOOL_PRESENTATION_KEY = "toolPresentation"; export const AIR_ASYNC_TASKS_BACKGROUNDED_KEY = "backgrounded"; export const AIR_AGENT_FILE_CHANGE_REPORT_REQUEST_KEY = "agentFileChangeReportRequest"; export const AIR_EXTENSION_VERSION = 1; diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 4dc15e01..9c1aa2a0 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -131,12 +131,14 @@ import {once} from "node:events"; import { AIR_AGENT_FILE_CHANGE_REPORT_KEY, AIR_ASYNC_TASKS_KEY, + AIR_COMMAND_PRESENTATION_KEY, AIR_NATIVE_SUBAGENT_SESSIONS_KEY, AIR_EXTENSION_CAPABILITIES_KEY, AIR_EXTENSION_VERSION, AIR_EXTENSION_VERSION_KEY, AIR_META_KEY, AIR_SESSION_FAILURE_KEY, + AIR_TOOL_PRESENTATION_KEY, clientSupportsAirCapability, JETBRAINS_META_KEY, } from "./AirExtension"; @@ -215,6 +217,14 @@ function clientSupportsAgentFileChangeReports(capabilities: acp.ClientCapabiliti return clientSupportsAirCapability(capabilities, AIR_AGENT_FILE_CHANGE_REPORT_KEY); } +function clientSupportsCommandPresentation(capabilities: acp.ClientCapabilities | null): boolean { + return clientSupportsAirCapability(capabilities, AIR_COMMAND_PRESENTATION_KEY); +} + +function clientSupportsToolPresentation(capabilities: acp.ClientCapabilities | null): boolean { + return clientSupportsAirCapability(capabilities, AIR_TOOL_PRESENTATION_KEY); +} + interface ActiveAuthState { account: Account | null; authConfigured: boolean; @@ -285,6 +295,7 @@ export class CodexAcpServer { private codexProcessGeneration = 0; private initializeRequest: acp.InitializeRequest | null = null; private providerUpdate: Promise | null = null; + private catalogNotificationSubscription: {dispose(): void}; constructor( connection: AcpClientConnection, @@ -318,6 +329,7 @@ export class CodexAcpServer { this.booleanConfigOptionsSupported = false; this.currentAuthStatus = null; this.availableCommands = this.createAvailableCommands(codexAcpClient); + this.catalogNotificationSubscription = this.observeCatalogChanges(codexAcpClient); this.observeCodexProcess(); } @@ -326,10 +338,27 @@ export class CodexAcpServer { this.connection, client, (operation) => this.runWithProcessCheck(operation), - () => this.refreshAuthState(null) + () => this.refreshAuthState(null), + () => clientSupportsCommandPresentation(this.clientCapabilities), ); } + private observeCatalogChanges(client: CodexAcpClient): {dispose(): void} { + return client.appServerClient.onGlobalServerNotification((notification) => { + if (notification.method !== "skills/changed" + && notification.method !== "app/list/updated" + && notification.method !== "externalAgentConfig/import/completed") { + return; + } + for (const sessionState of this.sessions.values()) { + this.publishAvailableCommandsAsync( + sessionState, + this.getSessionGeneration(sessionState.sessionId), + ); + } + }); + } + async initialize( _params: acp.InitializeRequest, ): Promise { @@ -397,6 +426,8 @@ export class CodexAcpServer { AIR_AGENT_FILE_CHANGE_REPORT_KEY, AIR_NATIVE_SUBAGENT_SESSIONS_KEY, AIR_ASYNC_TASKS_KEY, + AIR_COMMAND_PRESENTATION_KEY, + AIR_TOOL_PRESENTATION_KEY, ], }, }, @@ -889,6 +920,7 @@ export class CodexAcpServer { this.activePrompts.delete(params.sessionId); this.steeringQueues.delete(params.sessionId); this.goalControlGenerations.delete(params.sessionId); + this.availableCommands.clear(params.sessionId); } this.endSessionCloseFence(params.sessionId); } @@ -1047,6 +1079,8 @@ export class CodexAcpServer { await replacement.initialize(this.initializeRequest); this.codexAcpClient = replacement; this.availableCommands = this.createAvailableCommands(replacement); + this.catalogNotificationSubscription.dispose(); + this.catalogNotificationSubscription = this.observeCatalogChanges(replacement); const resumeErrors: unknown[] = []; for (const session of this.sessions.values()) { @@ -2258,7 +2292,10 @@ export class CodexAcpServer { return updates; } case "mcpToolCall": - return [await createMcpToolCallUpdate(item)]; + return [await createMcpToolCallUpdate( + item, + clientSupportsToolPresentation(this.clientCapabilities), + )]; case "dynamicToolCall": return [await createDynamicToolCallUpdate(item)]; case "collabAgentToolCall": @@ -2790,6 +2827,7 @@ export class CodexAcpServer { this.sessionFailureEpoch, sessionState.subagents, (accountUpdated) => this.handleAccountUpdated(accountUpdated), + clientSupportsToolPresentation(this.clientCapabilities), ); eventHandler = promptEventHandler; const permissionLifecycle = this.permissionLifecycleContext(sessionState); diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 250c76ef..9be474c7 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -165,6 +165,7 @@ export class CodexAppServerClient { private readonly threadGoalClearedCaptures = new Map void>>(); private readonly threadSettings = new Map(); private readonly staleTurnIds = new Map>(); + private readonly globalNotificationHandlers = new Set<(event: ServerNotification) => void>(); constructor(connection: MessageConnection) { this.connection = connection; @@ -735,6 +736,12 @@ export class CodexAppServerClient { this.notificationHandlers.set(sessionId, callback); } + /** Observes process-wide notifications exactly once, before they are fanned out to sessions. */ + onGlobalServerNotification(callback: (event: ServerNotification) => void): {dispose(): void} { + this.globalNotificationHandlers.add(callback); + return {dispose: () => this.globalNotificationHandlers.delete(callback)}; + } + private codexEventHandlers: Array<(event: CodexConnectionEvent) => void> = []; onClientTransportEvent(callback: (event: CodexConnectionEvent) => void){ this.codexEventHandlers.push(callback); @@ -742,6 +749,9 @@ export class CodexAppServerClient { private notificationHandlers = new Map void>(); private notify(notification: ServerNotification) { + for (const handler of this.globalNotificationHandlers) { + handler(notification); + } const threadId = extractThreadId(notification); if (threadId !== null) { const handler = this.notificationHandlers.get(threadId); diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index 321ac5da..d1cdb210 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -13,6 +13,13 @@ import { DEFAULT_COLLABORATION_MODE, PLAN_COLLABORATION_MODE, } from "./CollaborationModeConfig"; +import { + AIR_COMMAND_PRESENTATION_KEY, + AIR_EXTENSION_VERSION, + AIR_EXTENSION_VERSION_KEY, + AIR_META_KEY, + JETBRAINS_META_KEY, +} from "./AirExtension"; type ParsedSlashCommand = { name: string; @@ -41,27 +48,42 @@ export class CodexCommands { private readonly codexAcpClient: CodexAcpClient; private readonly runWithProcessCheck: (operation: () => Promise) => Promise; private readonly onLogout: LogoutHandler; + private readonly supportsCommandPresentation: () => boolean; + private readonly publishVersions = new Map(); + private readonly lastPublishedCommands = new Map(); constructor( connection: AcpClientConnection, codexAcpClient: CodexAcpClient, runWithProcessCheck: (operation: () => Promise) => Promise, - onLogout: LogoutHandler = () => {} + onLogout: LogoutHandler = () => {}, + supportsCommandPresentation: () => boolean = () => false, ) { this.connection = connection; this.codexAcpClient = codexAcpClient; this.runWithProcessCheck = runWithProcessCheck; this.onLogout = onLogout; + this.supportsCommandPresentation = supportsCommandPresentation; } - async publish(sessionState: SessionState, shouldPublish: () => boolean = () => true): Promise { + async publish( + sessionState: SessionState, + shouldPublish: () => boolean = () => true, + force = false, + ): Promise { + const publishVersion = (this.publishVersions.get(sessionState.sessionId) ?? 0) + 1; + this.publishVersions.set(sessionState.sessionId, publishVersion); try { if (!shouldPublish()) { return; } const skillsResponse = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills(this.createSkillsListParams(sessionState))); const availableCommands = this.buildAvailableCommands(skillsResponse?.data ?? []); - if (availableCommands.length === 0 || !shouldPublish()) { + const serializedCommands = JSON.stringify(availableCommands); + if (availableCommands.length === 0 + || !shouldPublish() + || this.publishVersions.get(sessionState.sessionId) !== publishVersion + || !force && this.lastPublishedCommands.get(sessionState.sessionId) === serializedCommands) { return; } @@ -70,6 +92,9 @@ export class CodexCommands { sessionUpdate: "available_commands_update", availableCommands }); + if (this.publishVersions.get(sessionState.sessionId) === publishVersion) { + this.lastPublishedCommands.set(sessionState.sessionId, serializedCommands); + } } catch (err) { if (shouldPublish()) { logger.error(`Failed to publish available commands for session ${sessionState.sessionId}`, err); @@ -77,6 +102,11 @@ export class CodexCommands { } } + clear(sessionId: string): void { + this.publishVersions.delete(sessionId); + this.lastPublishedCommands.delete(sessionId); + } + private createSkillsListParams(sessionState: SessionState): SkillsListParams { return { cwds: [sessionState.cwd, ...sessionState.additionalDirectories], @@ -99,6 +129,18 @@ export class CodexCommands { name, description, input: null, + ...(this.supportsCommandPresentation() ? { + _meta: { + [JETBRAINS_META_KEY]: { + [AIR_META_KEY]: { + [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION, + [AIR_COMMAND_PRESENTATION_KEY]: skill.pluginId === null + ? {source: "skill"} + : {source: "plugin", pluginId: skill.pluginId}, + }, + }, + }, + } : {}), }); } } @@ -283,17 +325,7 @@ export class CodexCommands { return { handled: true }; } case "skills": { - const response = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills(this.createSkillsListParams(sessionState))); - const skills = (response?.data ?? []).flatMap(entry => entry.skills); - const lines = skills.map(skill => { - const description = skill.shortDescription ?? skill.description ?? ""; - return description ? `- ${skill.name}: ${description}` : `- ${skill.name}`; - }); - const text = lines.length > 0 - ? ["Available skills:", ...lines].join("\n") - : "No skills configured."; - const session = new ACPSessionConnection(this.connection, sessionId); - await session.update(createAgentTextMessageChunk(text)); + await this.publish(sessionState, () => true, true); return { handled: true }; } case "mcp": { diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 7b567541..c6ed7b57 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -229,6 +229,7 @@ export class CodexEventHandler { private readonly subagents: CodexSubagentEventRouter; /** Connection-level `authStatus` sink; the app-server account push feeds it. */ private readonly onAccountUpdated: ((notification: AccountUpdatedNotification) => void) | undefined; + private readonly supportsToolPresentation: boolean; constructor( connection: AcpClientConnection, @@ -242,6 +243,7 @@ export class CodexEventHandler { new ACPSessionConnection(connection, sessionState.sessionId), ), onAccountUpdated?: (notification: AccountUpdatedNotification) => void, + supportsToolPresentation = false, ) { this.onAccountUpdated = onAccountUpdated; this.sessionState = sessionState; @@ -250,6 +252,7 @@ export class CodexEventHandler { this.sessionFailureEpoch = sessionFailureEpoch; this.session = new ACPSessionConnection(connection, sessionState.sessionId); this.subagents = subagents; + this.supportsToolPresentation = supportsToolPresentation; if (sessionState.sessionFailure !== undefined) { this.failuresById.set(sessionState.sessionFailure.id, sessionState.sessionFailure); } @@ -730,7 +733,7 @@ export class CodexEventHandler { return await createCommandExecutionUpdate(event.item); } case "mcpToolCall": - return await createMcpToolCallUpdate(event.item); + return await createMcpToolCallUpdate(event.item, this.supportsToolPresentation); case "dynamicToolCall": return await createDynamicToolCallUpdate(event.item); case "webSearch": diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index d1b4cbb9..3309ed9d 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -33,6 +33,13 @@ import { type TerminalOutputMode, } from "./TerminalOutputMode"; import {createContextCompactionMeta} from "./ContextCompactionMeta"; +import { + AIR_EXTENSION_VERSION, + AIR_EXTENSION_VERSION_KEY, + AIR_META_KEY, + AIR_TOOL_PRESENTATION_KEY, + JETBRAINS_META_KEY, +} from "./AirExtension"; type CodexItemStatus = CommandExecutionStatus | PatchApplyStatus | McpToolCallStatus | DynamicToolCallStatus | CollabAgentToolCallStatus; type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed"; @@ -141,8 +148,18 @@ export function createCommandExecutionCompleteUpdate( } export async function createMcpToolCallUpdate( - item: ThreadItem & { type: "mcpToolCall" } + item: ThreadItem & { type: "mcpToolCall" }, + supportsToolPresentation = false, ): Promise { + const presentation = item.appContext !== null + ? { + source: "app", + appId: item.appContext.connectorId, + ...(item.appContext.appName === null ? {} : {appName: item.appContext.appName}), + } + : item.pluginId === null + ? null + : {source: "plugin", pluginId: item.pluginId}; return { ...await createExecuteToolCallUpdate( item, @@ -150,7 +167,17 @@ export async function createMcpToolCallUpdate( createMcpRawInput(item.server, item.tool, item.arguments), createMcpRawOutput(item.result, item.error), ), - _meta: { is_mcp_tool_call: true }, + _meta: { + is_mcp_tool_call: true, + ...(supportsToolPresentation && presentation !== null ? { + [JETBRAINS_META_KEY]: { + [AIR_META_KEY]: { + [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION, + [AIR_TOOL_PRESENTATION_KEY]: presentation, + }, + }, + } : {}), + }, }; } diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index bb5daf6c..a89ff569 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1605,6 +1605,66 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/available-commands-skills.json"); }); + it('decorates skill and plugin commands when command presentation is negotiated', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + await codexAcpAgent.initialize({ + protocolVersion: 1, + clientCapabilities: { + _meta: {jetbrains: {air: {version: 1, capabilities: ["commandPresentation"]}}}, + }, + }); + + vi.spyOn(mockFixture.getCodexAcpClient(), "listSkills").mockResolvedValue({ + data: [{ + cwd: "/workspace", + skills: [ + {name: "build", description: "Build", path: "/skills/build", scope: "user", enabled: true, pluginId: null}, + {name: "deploy", description: "Deploy", path: "/plugins/quality/deploy", scope: "user", enabled: true, pluginId: "quality"}, + ], + errors: [], + }], + }); + + // @ts-expect-error - exercising private helper + await codexAcpAgent.availableCommands.publish(createTestSessionState({sessionId: "session-id"})); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/available-commands-presentation.json"); + }); + + it('refreshes the catalog once for a burst of process-wide skill changes', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + await codexAcpAgent.initialize({protocolVersion: 1}); + const listSkills = vi.spyOn(mockFixture.getCodexAcpClient(), "listSkills") + .mockResolvedValue({data: []}); + const sessionState = createTestSessionState({sessionId: "session-id", cwd: "/workspace"}); + // @ts-expect-error - install an active session without exercising unrelated session startup + codexAcpAgent.installSessionState(sessionState); + // @ts-expect-error - seed the last published snapshot + await codexAcpAgent.availableCommands.publish(sessionState); + mockFixture.clearAcpConnectionDump(); + + listSkills.mockResolvedValue({ + data: [{ + cwd: "/workspace", + skills: [{name: "new-skill", description: "New", path: "/skills/new", scope: "user", enabled: true, pluginId: null}], + errors: [], + }], + }); + mockFixture.sendServerNotification({method: "skills/changed", params: {}}); + mockFixture.sendServerNotification({method: "skills/changed", params: {}}); + + await vi.waitFor(() => { + const updates = mockFixture.getAcpConnectionEvents([]) + .filter(event => event.method === "sessionUpdate" + && event.args[0].sessionId === sessionState.sessionId + && event.args[0].update.sessionUpdate === "available_commands_update"); + expect(updates).toHaveLength(1); + expect(updates[0]!.args[0].update.availableCommands).toContainEqual(expect.objectContaining({name: "$new-skill"})); + }); + }); + it('handles builtin slash command locally', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent(); diff --git a/src/__tests__/CodexACPAgent/command-action-events.test.ts b/src/__tests__/CodexACPAgent/command-action-events.test.ts index 658a9976..b697783f 100644 --- a/src/__tests__/CodexACPAgent/command-action-events.test.ts +++ b/src/__tests__/CodexACPAgent/command-action-events.test.ts @@ -321,6 +321,49 @@ describe('CodexEventHandler - command action events', () => { ); }); + it('decorates app-backed mcp tools when tool presentation is negotiated', async () => { + await mockFixture.getCodexAcpAgent().initialize({ + protocolVersion: 1, + clientCapabilities: { + _meta: {jetbrains: {air: {version: 1, capabilities: ["toolPresentation"]}}}, + }, + }); + const notification: ServerNotification = { + method: 'item/started', + params: { + threadId: sessionId, + turnId: 'turn-1', + startedAtMs: 0, + item: { + type: "mcpToolCall", + id: "app-call-id", + server: "github-app", + tool: "search", + status: "inProgress", + arguments: {query: "codex"}, + appContext: { + connectorId: "github", + linkId: null, + resourceUri: null, + appName: "GitHub", + actionName: "Search", + }, + readOnlyHint: true, + pluginId: null, + result: null, + error: null, + durationMs: null, + }, + }, + }; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [notification]); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + 'data/mcp-app-tool-presentation.json' + ); + }); + it('should include mcp progress and final logs', async () => { const notifications: ServerNotification[] = [ { diff --git a/src/__tests__/CodexACPAgent/data/available-commands-presentation.json b/src/__tests__/CodexACPAgent/data/available-commands-presentation.json new file mode 100644 index 00000000..559127ad --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/available-commands-presentation.json @@ -0,0 +1,124 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "session-id", + "update": { + "sessionUpdate": "available_commands_update", + "availableCommands": [ + { + "name": "plan", + "description": "Turn plan mode on.", + "input": null, + "_meta": { + "commandAction": { + "kind": "setConfigOption", + "configId": "collaboration_mode", + "value": "plan", + "resetValue": "default", + "presentation": "state" + } + } + }, + { + "name": "mcp", + "description": "List configured Model Context Protocol (MCP) tools.", + "input": null + }, + { + "name": "skills", + "description": "List available skills.", + "input": null + }, + { + "name": "status", + "description": "Display session configuration and token usage.", + "input": null + }, + { + "name": "review", + "description": "Review uncommitted changes, or review with custom instructions.", + "input": { + "hint": "optional review instructions" + } + }, + { + "name": "review-branch", + "description": "Review changes relative to a base branch.", + "input": { + "hint": "branch name" + } + }, + { + "name": "review-commit", + "description": "Review a specific commit.", + "input": { + "hint": "commit sha" + } + }, + { + "name": "compact", + "description": "Summarize conversation to avoid hitting the context limit.", + "input": null + }, + { + "name": "goal", + "description": "Set a goal to keep pursuing.", + "input": { + "hint": "[|clear|pause|resume]" + }, + "_meta": { + "commandAction": { + "kind": "prefixPrompt", + "presentation": "state" + } + } + }, + { + "name": "rename", + "description": "Rename the current session.", + "input": { + "hint": "new name" + } + }, + { + "name": "logout", + "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", + "input": null + }, + { + "name": "$build", + "description": "Build", + "input": null, + "_meta": { + "jetbrains": { + "air": { + "version": 1, + "commandPresentation": { + "source": "skill" + } + } + } + } + }, + { + "name": "$deploy", + "description": "Deploy", + "input": null, + "_meta": { + "jetbrains": { + "air": { + "version": 1, + "commandPresentation": { + "source": "plugin", + "pluginId": "quality" + } + } + } + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/command-skills.json b/src/__tests__/CodexACPAgent/data/command-skills.json index 54b95916..e5a1d2af 100644 --- a/src/__tests__/CodexACPAgent/data/command-skills.json +++ b/src/__tests__/CodexACPAgent/data/command-skills.json @@ -4,11 +4,99 @@ { "sessionId": "sessionId", "update": { - "sessionUpdate": "agent_message_chunk", - "content": { - "type": "text", - "text": "Available skills:\n- build: Build\n- deploy: Deploy the service" - } + "sessionUpdate": "available_commands_update", + "availableCommands": [ + { + "name": "plan", + "description": "Turn plan mode on.", + "input": null, + "_meta": { + "commandAction": { + "kind": "setConfigOption", + "configId": "collaboration_mode", + "value": "plan", + "resetValue": "default", + "presentation": "state" + } + } + }, + { + "name": "mcp", + "description": "List configured Model Context Protocol (MCP) tools.", + "input": null + }, + { + "name": "skills", + "description": "List available skills.", + "input": null + }, + { + "name": "status", + "description": "Display session configuration and token usage.", + "input": null + }, + { + "name": "review", + "description": "Review uncommitted changes, or review with custom instructions.", + "input": { + "hint": "optional review instructions" + } + }, + { + "name": "review-branch", + "description": "Review changes relative to a base branch.", + "input": { + "hint": "branch name" + } + }, + { + "name": "review-commit", + "description": "Review a specific commit.", + "input": { + "hint": "commit sha" + } + }, + { + "name": "compact", + "description": "Summarize conversation to avoid hitting the context limit.", + "input": null + }, + { + "name": "goal", + "description": "Set a goal to keep pursuing.", + "input": { + "hint": "[|clear|pause|resume]" + }, + "_meta": { + "commandAction": { + "kind": "prefixPrompt", + "presentation": "state" + } + } + }, + { + "name": "rename", + "description": "Rename the current session.", + "input": { + "hint": "new name" + } + }, + { + "name": "logout", + "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", + "input": null + }, + { + "name": "$build", + "description": "Build", + "input": null + }, + { + "name": "$deploy", + "description": "Deploy the service", + "input": null + } + ] } } ] diff --git a/src/__tests__/CodexACPAgent/data/mcp-app-tool-presentation.json b/src/__tests__/CodexACPAgent/data/mcp-app-tool-presentation.json new file mode 100644 index 00000000..4fdf76ea --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/mcp-app-tool-presentation.json @@ -0,0 +1,35 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "toolCallId": "app-call-id", + "kind": "execute", + "title": "mcp.github-app.search", + "status": "in_progress", + "rawInput": { + "server": "github-app", + "tool": "search", + "arguments": { + "query": "codex" + } + }, + "_meta": { + "is_mcp_tool_call": true, + "jetbrains": { + "air": { + "version": 1, + "toolPresentation": { + "source": "app", + "appId": "github", + "appName": "GitHub" + } + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/initialize.test.ts b/src/__tests__/CodexACPAgent/initialize.test.ts index 7fff7721..f4e2bcc9 100644 --- a/src/__tests__/CodexACPAgent/initialize.test.ts +++ b/src/__tests__/CodexACPAgent/initialize.test.ts @@ -78,7 +78,14 @@ describe('CodexACPAgent - initialize', () => { jetbrains: { air: { version: 1, - capabilities: ["sessionFailure", "agentFileChangeReport", "nativeSubagentSessions", "asyncTasks"], + capabilities: [ + "sessionFailure", + "agentFileChangeReport", + "nativeSubagentSessions", + "asyncTasks", + "commandPresentation", + "toolPresentation", + ], }, }, },