diff --git a/packages/app/src/agent-controls/draft-options-model.ts b/packages/app/src/agent-controls/draft-options-model.ts index aed92ded91..791b001b55 100644 --- a/packages/app/src/agent-controls/draft-options-model.ts +++ b/packages/app/src/agent-controls/draft-options-model.ts @@ -118,6 +118,15 @@ export type DraftControlsRequest = Pick< "provider" | "cwd" | "modeId" | "model" | "thinkingOptionId" | "providerOptions" >; +/** What the draft's form has chosen; the options come from the model. */ +export interface DraftControlsForm { + provider: string | null; + cwd: string; + modeId?: string | null; + modelId?: string | null; + thinkingOptionId?: string | null; +} + /** * The listing request for the draft's form, built from one snapshot of the model. The form * reaches a new provider before the model has observed it, and the model still holds the @@ -126,13 +135,7 @@ export type DraftControlsRequest = Pick< */ export function draftControlsRequest( state: DraftOptionsState, - form: { - provider: string | null; - cwd: string; - modeId?: string | null; - modelId?: string | null; - thinkingOptionId?: string | null; - }, + form: DraftControlsForm, ): DraftControlsRequest | null { if (!form.provider || !form.cwd || form.provider !== state.provider) return null; const providerOptions = ownDraftOptions(state); diff --git a/packages/client/src/compat/legacy-provider-options.ts b/packages/client/src/compat/legacy-provider-options.ts index 9dcda7b1a2..7085d26bf7 100644 --- a/packages/client/src/compat/legacy-provider-options.ts +++ b/packages/client/src/compat/legacy-provider-options.ts @@ -203,11 +203,15 @@ export class LegacyProviderOptions { }; } -/** A legacy daemon stores a profile's options under `featureValues`. */ +/** + * A legacy daemon stores a profile's options under `featureValues`, and keeps an `options` field + * a newer daemon left on it. Both are read, and an explicit `options` key wins, as it does on the + * daemon (`foldFeatureValues`). + */ function profileWithOptions(profile: AgentProfile): AgentProfile { - if (profile.options || !profile.featureValues) return profile; + if (!profile.featureValues) return profile; const { featureValues, ...rest } = profile; - return { ...rest, options: featureValues }; + return { ...rest, options: { ...featureValues, ...profile.options } }; } function profileWithFeatureValues(profile: AgentProfile): AgentProfile { diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index e82c0dd7f9..d2659171d2 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -9165,6 +9165,47 @@ describe("provider options across daemon versions", () => { expect(written).not.toMatchObject({ config: { agentProfiles: [{ options: {} }] } }); }); + test("a legacy daemon's profile with both featureValues and options keeps both when saved", async () => { + // `options` survives on a legacy daemon's passthrough profile after a downgrade, while its + // own app keeps editing `featureValues`. + const { client, sentMessages, mock } = await connect({}); + const reading = client.getDaemonConfig(); + const [read] = sentMessages(); + mock.triggerMessage( + wrapSessionMessage({ + type: "get_daemon_config_response", + payload: { + requestId: read.requestId, + config: { + mcp: { injectIntoAgents: true }, + agentProfiles: [ + { + id: "p1", + name: "Mixed", + provider: "codex", + featureValues: { service_tier: "fast", sandbox_mode: "workspace-write" }, + options: { sandbox_mode: "read-only" }, + }, + ], + }, + }, + }), + ); + const { config } = await reading; + expect(config.agentProfiles?.[0]).toMatchObject({ + options: { service_tier: "fast", sandbox_mode: "read-only" }, + }); + expect(config.agentProfiles?.[0]).not.toHaveProperty("featureValues"); + + void client.patchDaemonConfig({ agentProfiles: config.agentProfiles }).catch(() => {}); + const written = sentMessages().find((message) => message.type === "set_daemon_config_request"); + expect(written).toMatchObject({ + config: { + agentProfiles: [{ featureValues: { service_tier: "fast", sandbox_mode: "read-only" } }], + }, + }); + }); + test("a legacy daemon applies a profile's options as feature values", async () => { const { client, sentMessages } = await connect({}); void client.applyAgentConfig("agent-1", { options: { service_tier: "fast" } }).catch(() => {});