Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,9 +1161,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,
Expand Down
19 changes: 19 additions & 0 deletions apps/web/src/modelSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"entry": [
// These fixture scripts are injected into a browser page by path.
"fixtures/*/*.js",
// Manual UI proofs import these fixtures by path.
"fixtures/*.mjs",
"knip-schemas.ts",
"mobile-behavior/run.ts",
"mobile-native-client.ts",
Expand Down
42 changes: 42 additions & 0 deletions scripts/fixtures/composer-option-fallback.mjs
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
Loading