diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index d911342b..8083f5fd 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -1878,7 +1878,11 @@ export class CodexAcpServer { const disposePromptRequestCancellation = this.observePromptRequestCancellation(signal, sessionState, activePrompt); try { - const eventHandler = new CodexEventHandler(this.connection, sessionState); + const eventHandler = new CodexEventHandler( + this.connection, + sessionState, + () => this.availableCommands.publish(sessionState), + ); const approvalHandler = new CodexApprovalHandler(this.connection, sessionState, activePrompt.signal); const elicitationHandler = new CodexElicitationHandler( this.connection, diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 5739e44e..366ca346 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -71,6 +71,7 @@ export class CodexEventHandler { private readonly connection: AcpClientConnection; private readonly sessionState: SessionState; + private readonly onSkillsChanged: () => void | Promise; private failure: RequestError | null = null; private readonly activeFuzzyFileSearchSessions = new Set(); private readonly activeGuardianApprovalReviews = new Set(); @@ -82,9 +83,14 @@ export class CodexEventHandler { private readonly agentMessagePhases = new Map(); private readonly activeSubAgentActivities = new Set(); - constructor(connection: AcpClientConnection, sessionState: SessionState) { + constructor( + connection: AcpClientConnection, + sessionState: SessionState, + onSkillsChanged: () => void | Promise, + ) { this.connection = connection; this.sessionState = sessionState; + this.onSkillsChanged = onSkillsChanged; } getFailure(): RequestError | null { @@ -188,6 +194,9 @@ export class CodexEventHandler { return this.createThreadGoalClearedEvent(notification.params); case "item/commandExecution/terminalInteraction": return this.createTerminalInteractionEvent(notification.params); + case "skills/changed": + await this.onSkillsChanged(); + return null; // ignored events case "thread/deleted": case "thread/environment/connected": @@ -216,7 +225,6 @@ export class CodexEventHandler { case "thread/realtime/closed": case "windowsSandbox/setupCompleted": case "account/login/completed": - case "skills/changed": case "deprecationNotice": case "mcpServer/oauthLogin/completed": case "externalAgentConfig/import/completed": diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index d58bd594..405dc891 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1351,6 +1351,42 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/available-commands-skills.json"); }); + it('should refresh available commands when skills change', async () => { + const { mockFixture } = setupPromptFixture({ + sessionId: "session-id", + cwd: "/workspace", + additionalDirectories: ["/workspace/extra"], + }); + const listSkillsSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "listSkills").mockResolvedValue({ + data: [{ + cwd: "/workspace", + skills: [{ + name: "build", + description: "Build the project", + shortDescription: "Build", + path: "/workspace", + scope: "user", + enabled: true + }], + errors: [] + }] + }); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "Hello" }], + }); + mockFixture.clearAcpConnectionDump(); + + mockFixture.sendServerNotification({ method: "skills/changed", params: {} }); + await mockFixture.getCodexAcpClient().waitForSessionNotifications("session-id"); + + expect(listSkillsSpy).toHaveBeenCalledWith({ + cwds: ["/workspace", "/workspace/extra"], + }); + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/available-commands-skills.json"); + }); + it('handles builtin slash command locally', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent();