diff --git a/packages/app/src/agent-controls/draft-options-model.test.ts b/packages/app/src/agent-controls/draft-options-model.test.ts index 9588bab568..12ea6979d9 100644 --- a/packages/app/src/agent-controls/draft-options-model.test.ts +++ b/packages/app/src/agent-controls/draft-options-model.test.ts @@ -72,7 +72,11 @@ describe("draft options model", () => { it("never pairs a manually chosen provider with the previous provider's options", () => { const model = openDraftOptions({ provider: "claude" }); - model.applyResolved(answer("claude", "k1", { fast_mode: true })); + model.applyResolved( + answer("claude", "k1", { fast_mode: true }, [ + { type: "toggle", key: "fast_mode", label: "Fast" }, + ]), + ); model.setOption("fast_mode", true); const form = { provider: "claude", cwd: "/repo" }; expect(draftControlsRequest(model.getState(), form)).toMatchObject({ @@ -88,6 +92,29 @@ describe("draft options model", () => { expect(request).toEqual({ provider: "opencode", cwd: "/repo" }); }); + it("stops sending a toggle once the model in use draws no control for it", () => { + const FAST_MODE: AgentOptionControl = { type: "toggle", key: "fast_mode", label: "Fast" }; + const model = openDraftOptions({ provider: "claude", seed: { sandbox_mode: "read-only" } }); + model.applyResolved(answer("claude", "opus", { fast_mode: false }, [FAST_MODE])); + model.setOption("fast_mode", true); + expect(ownDraftOptions(model.getState())).toEqual({ + sandbox_mode: "read-only", + fast_mode: true, + }); + + // The form moves to a model without Fast; its answer lists no such control. + model.applyResolved(answer("claude", "haiku", {}, [])); + + expect(ownDraftOptions(model.getState())).toEqual({ sandbox_mode: "read-only" }); + + // Back on the first model, the toggle is still on. + model.applyResolved(answer("claude", "opus-again", { fast_mode: true }, [FAST_MODE])); + expect(ownDraftOptions(model.getState())).toEqual({ + sandbox_mode: "read-only", + fast_mode: true, + }); + }); + it("keeps a draft's seed when the first provider is chosen", () => { const model = openDraftOptions({ provider: null, seed: { web_search: true } }); diff --git a/packages/app/src/agent-controls/draft-options-model.ts b/packages/app/src/agent-controls/draft-options-model.ts index 791b001b55..a2da5583a9 100644 --- a/packages/app/src/agent-controls/draft-options-model.ts +++ b/packages/app/src/agent-controls/draft-options-model.ts @@ -101,6 +101,8 @@ export function openDraftOptions(input: { /** * Everything the draft would create with, for the daemon to resolve. A last-used value applies * once the provider has told us it still draws a control for that key, and never over the seed. + * A toggle made in this draft stops applying when the latest answer draws no control for it (the + * model changed to one without that control) and applies again when a control for it returns. */ export function ownDraftOptions(state: DraftOptionsState): ProviderOptions | undefined { const controlKeys = new Set((state.resolved?.controls ?? []).map((control) => control.key)); @@ -109,7 +111,10 @@ export function ownDraftOptions(state: DraftOptionsState): ProviderOptions | und ([key]) => controlKeys.has(key) && !(key in state.seed), ), ); - const own = { ...state.seed, ...remembered, ...state.edits }; + const edits = Object.fromEntries( + Object.entries(state.edits).filter(([key]) => !state.resolved || controlKeys.has(key)), + ); + const own = { ...state.seed, ...remembered, ...edits }; return Object.keys(own).length > 0 ? own : undefined; }