diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 80486fe..82fbf38 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -56,6 +56,7 @@ import { formatWebSearchTitle, } from "./CodexToolCallMapper"; import { + clientSupportsBooleanConfigOptions, createFastModeConfigOption, FAST_MODE_CONFIG_ID, FAST_MODE_OFF, @@ -138,6 +139,7 @@ export class CodexAcpServer { private readonly availableCommands: CodexCommands; private clientInfo: acp.Implementation | null; private terminalOutputMode: TerminalOutputMode; + private booleanConfigOptionsSupported: boolean; private readonly sessions: Map; private readonly pendingMcpStartupSessions: Map; @@ -168,6 +170,7 @@ export class CodexAcpServer { this.getRecentStderr = getRecentStderr ?? (() => ""); this.clientInfo = null; this.terminalOutputMode = "terminal_output_delta"; + this.booleanConfigOptionsSupported = false; this.availableCommands = new CodexCommands( connection, codexAcpClient, @@ -182,6 +185,7 @@ export class CodexAcpServer { logger.log("Initialize request received"); this.clientInfo = _params.clientInfo ?? null; this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities); + this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities); await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params)); return { protocolVersion: acp.PROTOCOL_VERSION, @@ -657,23 +661,18 @@ export class CodexAcpServer { const sessionState = this.sessions.get(params.sessionId); if (!sessionState) throw new Error(`Session ${params.sessionId} not found`); - if (typeof params.value !== "string") { - throw RequestError.invalidParams(); - } - const value = params.value; - switch (params.configId) { case FAST_MODE_CONFIG_ID: - this.applyFastModeChange(sessionState, value); + this.applyFastModeChange(sessionState, params); break; case MODE_CONFIG_ID: - this.applyModeChange(sessionState, value); + this.applyModeChange(sessionState, this.stringConfigValue(params)); break; case MODEL_CONFIG_ID: - this.applyModelChange(sessionState, value); + this.applyModelChange(sessionState, this.stringConfigValue(params)); break; case REASONING_EFFORT_CONFIG_ID: - this.applyReasoningEffortChange(sessionState, value); + this.applyReasoningEffortChange(sessionState, this.stringConfigValue(params)); break; default: throw RequestError.invalidParams(); @@ -684,13 +683,25 @@ export class CodexAcpServer { }; } - private applyFastModeChange(sessionState: SessionState, value: string): void { + private applyFastModeChange(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): void { + const value = params.value; + if (typeof value === "boolean") { + sessionState.fastModeEnabled = value; + return; + } if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) { throw RequestError.invalidParams(); } sessionState.fastModeEnabled = value === FAST_MODE_ON; } + private stringConfigValue(params: acp.SetSessionConfigOptionRequest): string { + if (typeof params.value !== "string") { + throw RequestError.invalidParams(); + } + return params.value; + } + private applyModeChange(sessionState: SessionState, value: string): void { const newMode = AgentMode.find(value); if (!newMode) { @@ -784,9 +795,12 @@ export class CodexAcpServer { createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort), ); } - if (sessionState.currentModelSupportsFast) { - configOptions.push(createFastModeConfigOption(sessionState.fastModeEnabled)); - } + if (sessionState.currentModelSupportsFast) { + configOptions.push(createFastModeConfigOption( + sessionState.fastModeEnabled, + this.booleanConfigOptionsSupported, + )); + } return configOptions; } diff --git a/src/FastModeConfig.ts b/src/FastModeConfig.ts index 4187aaa..8e681c2 100644 --- a/src/FastModeConfig.ts +++ b/src/FastModeConfig.ts @@ -1,8 +1,10 @@ import type {SessionConfigOption} from "@agentclientprotocol/sdk"; +import type * as acp from "@agentclientprotocol/sdk"; import type {ServiceTier} from "./app-server"; import type {Model} from "./app-server/v2"; export const FAST_MODE_CONFIG_ID = "fast-mode"; +export const FAST_MODE_CATEGORY = "model_config"; export const FAST_MODE_ON = "on"; export const FAST_MODE_OFF = "off"; @@ -16,12 +18,27 @@ export function resolveFastServiceTier(fastModeEnabled: boolean, currentModelSup return fastModeEnabled && currentModelSupportsFast ? "fast" : null; } -export function createFastModeConfigOption(fastModeEnabled: boolean): SessionConfigOption { +export function clientSupportsBooleanConfigOptions(clientCapabilities?: acp.ClientCapabilities | null): boolean { + return clientCapabilities?.session?.configOptions?.boolean != null; +} + +export function createFastModeConfigOption(fastModeEnabled: boolean, useBooleanConfigOption = false): SessionConfigOption { + if (useBooleanConfigOption) { + return { + id: FAST_MODE_CONFIG_ID, + name: "Fast mode", + description: FAST_MODE_DESCRIPTION, + category: FAST_MODE_CATEGORY, + type: "boolean", + currentValue: fastModeEnabled, + }; + } + return { id: FAST_MODE_CONFIG_ID, name: "Fast mode", description: FAST_MODE_DESCRIPTION, - category: FAST_MODE_CONFIG_ID, + category: FAST_MODE_CATEGORY, type: "select", currentValue: fastModeEnabled ? FAST_MODE_ON : FAST_MODE_OFF, options: [ diff --git a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts index ba8a71d..07d4244 100644 --- a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts +++ b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts @@ -15,9 +15,18 @@ import { import {MODEL_CONFIG_ID} from "../../ModelConfigOption"; describe("Fast mode session config", () => { + const booleanConfigCapabilities: acp.ClientCapabilities = { + session: { + configOptions: { + boolean: {}, + }, + }, + }; + async function createSession( currentServiceTier: "fast" | "flex" | null = null, - clientInfo: acp.Implementation | null = null + clientInfo: acp.Implementation | null = null, + clientCapabilities?: acp.ClientCapabilities, ) { const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent(); @@ -41,6 +50,7 @@ describe("Fast mode session config", () => { await codexAcpAgent.initialize({ protocolVersion: acp.PROTOCOL_VERSION, clientInfo, + ...(clientCapabilities ? {clientCapabilities} : {}), }); const response = await codexAcpAgent.newSession({cwd: "/test/cwd", mcpServers: []}); @@ -63,6 +73,31 @@ describe("Fast mode session config", () => { expect(response.configOptions).toContainEqual(createFastModeConfigOption(false)); }); + it("returns the Fast mode config option as a boolean when the client supports it", async () => { + const {response} = await createSession(null, null, booleanConfigCapabilities); + + expect(response.configOptions).toContainEqual(createFastModeConfigOption(false, true)); + const option = response.configOptions?.find(option => option.id === FAST_MODE_CONFIG_ID); + expect(option).toMatchObject({ + id: FAST_MODE_CONFIG_ID, + type: "boolean", + currentValue: false, + }); + expect(option).not.toHaveProperty("options"); + }); + + it("keeps the Fast mode select option when boolean support is explicitly absent", async () => { + const {response} = await createSession(null, null, { + session: { + configOptions: { + boolean: null, + }, + }, + }); + + expect(response.configOptions).toContainEqual(createFastModeConfigOption(false)); + }); + it("initializes Fast mode as On when the app-server session tier is fast", async () => { const {response, codexAcpAgent} = await createSession("fast"); @@ -139,6 +174,28 @@ describe("Fast mode session config", () => { expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(false); }); + it("toggles Fast mode through boolean session config options", async () => { + const {codexAcpAgent} = await createSession(null, null, booleanConfigCapabilities); + + const onResponse = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: FAST_MODE_CONFIG_ID, + type: "boolean", + value: true, + }); + expect(onResponse.configOptions).toContainEqual(createFastModeConfigOption(true, true)); + expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(true); + + const offResponse = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: FAST_MODE_CONFIG_ID, + type: "boolean", + value: false, + }); + expect(offResponse.configOptions).toContainEqual(createFastModeConfigOption(false, true)); + expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(false); + }); + it("rejects unknown Fast mode config ids and values", async () => { const {codexAcpAgent} = await createSession();