From 16f40c0bd1232d94c22943a599851ad3802f077b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 09:12:03 +0000 Subject: [PATCH 1/4] test(web): reproduce #23497 optionless thread inherits default options An existing thread persisted without options shows and sends the New threads default effort, because deriveEffectiveComposerModelState falls back to the project/settings default selection's options. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/modelSelection.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/web/src/modelSelection.test.ts b/apps/web/src/modelSelection.test.ts index 05a7e76b6b25b..301323af3bbbd 100644 --- a/apps/web/src/modelSelection.test.ts +++ b/apps/web/src/modelSelection.test.ts @@ -361,6 +361,25 @@ describe("instance-scoped model selection", () => { expect(dispatch.modelOptionsForDispatch).toBeUndefined(); }); + it("does not apply default options to an existing thread saved without options", () => { + const instanceId = ProviderInstanceId.make("claudeAgent"); + const driver = ProviderDriverKind.make("claudeAgent"); + const providers = [provider({ provider: driver, instanceId, models: ["claude-opus-5-5"] })]; + const state = deriveEffectiveComposerModelState({ + draft: null, + providers, + selectedProvider: driver, + selectedInstanceId: instanceId, + threadModelSelection: createModelSelection(instanceId, "claude-opus-5-5"), + projectModelSelection: createModelSelection(instanceId, "claude-opus-5-5", [ + { id: "effort", value: "high" }, + ]), + settings: settingsWithProviderInstances(), + }); + + expect(state.modelOptions?.[instanceId]).toBeUndefined(); + }); + it("preserves an explicit draft OpenCode selection while the catalog is empty", () => { const instanceId = ProviderInstanceId.make("opencode_work"); const driver = ProviderDriverKind.make("opencode"); From 4c7f479ac95b503f906c26eb949e8812c615c136 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 09:14:05 +0000 Subject: [PATCH 2/4] fix(web): threads saved without model options no longer inherit default options Co-Authored-By: Claude Opus 5.5 --- apps/web/src/composerDraftStore.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 57ffde65949ff..c269d1557378b 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -1158,9 +1158,11 @@ export function deriveEffectiveComposerModelState(input: { : baseModel; const modelOptions = modelSelectionByProviderToOptions(input.draft?.modelSelectionByProvider) ?? - providerSelectionsFromModelSelection(input.threadModelSelection) ?? - providerSelectionsFromModelSelection(input.projectModelSelection) ?? - null; + // A thread saved without options keeps provider defaults; only threads with + // no selection of their own inherit the project/settings default options. + (input.threadModelSelection + ? providerSelectionsFromModelSelection(input.threadModelSelection) + : providerSelectionsFromModelSelection(input.projectModelSelection)); return { selectedModel, From be2c1110767f535e2430c766e4013aa51b45bd4d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 09:41:43 +0000 Subject: [PATCH 3/4] test(web): add synthetic composer option fallback fixture --- scripts/fixtures/composer-option-fallback.mjs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 scripts/fixtures/composer-option-fallback.mjs diff --git a/scripts/fixtures/composer-option-fallback.mjs b/scripts/fixtures/composer-option-fallback.mjs new file mode 100644 index 0000000000000..79038e46a9486 --- /dev/null +++ b/scripts/fixtures/composer-option-fallback.mjs @@ -0,0 +1,42 @@ +export const selection = { instanceId: "claudeAgent", model: "claude-opus-5-5" }; + +export async function seedComposerOptionFallback(rpc, workspaceRoot) { + const call = async (tag, payload) => { + const exit = await rpc(tag, payload); + if (exit._tag !== "Success") throw new Error(JSON.stringify(exit)); + return exit.value; + }; + await call("server.updateSettings", { + patch: { defaultModelSelection: { ...selection, options: [{ id: "effort", value: "high" }] } }, + }); + await call("projects.mutate", { + type: "project.create", + commandId: "proof-project-create", + projectId: "proof-project", + title: "Synthetic proof", + workspaceRoot, + }); + for (const row of ["V1", "V2", "V3", "V4", "A2", "B"]) { + await call("orchestration.dispatchCommand", { + type: "thread.create", + commandId: `proof-create-${row}`, + createdBy: "agent", + creationSource: "mcp", + threadId: `proof-${row}`, + projectId: "proof-project", + title: "Option fallback proof", + modelSelection: { + ...selection, + ...(row === "A2" + ? { options: [] } + : row === "B" + ? { options: [{ id: "effort", value: "low" }] } + : {}), + }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + } +} From 7f004b1622a7cdc44b3bed768c23b5e84ecfcef2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 10:04:54 +0000 Subject: [PATCH 4/4] chore: register composer option fallback fixture as a knip entry Co-Authored-By: Claude Opus 5.5 --- knip.jsonc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/knip.jsonc b/knip.jsonc index 7fbfe38259951..bd7e3604a19ee 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -20,8 +20,10 @@ // Knip loads its preprocessor through a CLI option; native verification and // worktree setup (from t3.json) run directly. release-desktop.yml bundles the // Windows verify script with esbuild through a path knip does not trace, and - // electron-builder loads the macOS signer by path. + // electron-builder loads the macOS signer by path. Manual UI proofs import + // fixtures by path. "entry": [ + "fixtures/*.mjs", "knip-schemas.ts", "mobile-native-client.ts", "setup-worktree.ts",