Preserve Claude model and thinking preferences (#2912)

* fix(agents): preserve Claude thinking selections

Model switches now resolve thinking against the target model's saved preference instead of resetting to the catalog default. Claude owns its configured-model defaults and legacy Fable alias handling, while persisted permission modes remain unchanged.

* chore(claude): tag Fable compatibility shim

* fix(app): resolve persisted model aliases

* fix(models): preserve aliases for older clients

* fix(models): hide compatibility aliases from listings

* fix(settings): hide compatibility model aliases

* test(claude): cover legacy catalog entry

* test(app): expect mock thinking controls in schedules

* fix(app): preserve queued thinking preferences

* fix(app): reconcile schedule provider snapshots

* fix(app): preserve schedule model selections

* fix(claude): preserve exact configured model ids

* fix(app): preserve committed thinking preferences

* fix(app): preserve aliased schedule thinking
This commit is contained in:
Mohamed Boudra
2026-08-06 11:33:18 +02:00
committed by GitHub
parent 73ba37a07a
commit f2054cc701
43 changed files with 1369 additions and 220 deletions
@@ -32,6 +32,7 @@ const HOST_B_MODELS: AgentModelDefinition[] = [
provider: "mock",
id: "model-b",
label: "Model B",
aliases: ["model-b-legacy"],
isDefault: true,
defaultThinkingOptionId: "high",
thinkingOptions: [
@@ -41,6 +42,20 @@ const HOST_B_MODELS: AgentModelDefinition[] = [
},
];
const THINKING_MODELS: AgentModelDefinition[] = [
HOST_B_MODELS[0]!,
{
provider: "mock",
id: "model-c",
label: "Model C",
defaultThinkingOptionId: "low",
thinkingOptions: [
{ id: "low", label: "Low", isDefault: true },
{ id: "high", label: "High" },
],
},
];
const ALL_MODELS = [...HOST_A_MODELS, ...HOST_B_MODELS];
function target(input: {
@@ -81,6 +96,7 @@ function scheduleOnHost(input: {
serverName: string;
cwd: string;
model: string;
thinkingOptionId?: string | null;
cadence?: ScheduleSummary["cadence"];
}): TestSchedule {
return {
@@ -97,7 +113,8 @@ function scheduleOnHost(input: {
cwd: input.cwd,
model: input.model,
modeId: "load-test",
thinkingOptionId: "high",
thinkingOptionId:
input.thinkingOptionId === undefined ? "high" : (input.thinkingOptionId ?? undefined),
archiveOnFinish: false,
isolation: "worktree",
},
@@ -133,13 +150,16 @@ function heartbeatOnHost(cadence: ScheduleSummary["cadence"]): TestSchedule {
};
}
function providerSnapshot(models: AgentModelDefinition[]): { entries: ProviderSnapshotEntry[] } {
function providerSnapshot(
models: AgentModelDefinition[],
status: ProviderSnapshotEntry["status"] = "ready",
): { entries: ProviderSnapshotEntry[] } {
return {
entries: [
{
provider: "mock",
label: "Mock",
status: "ready",
status,
enabled: true,
fetchedAt: "2026-07-01T00:00:00.000Z",
models,
@@ -223,6 +243,69 @@ describe("schedule form model", () => {
});
});
it("reconciles untouched thinking when a loading provider snapshot becomes ready", () => {
const form = open({
mode: "edit",
schedule: scheduleOnHost({
serverId: "host-b",
serverName: "Host B",
cwd: "/repo/b",
model: "model-b",
thinkingOptionId: null,
}),
defaults: { serverId: null, projectTargets: PROJECT_TARGETS, preferences: {} },
});
const loadingModel = { ...HOST_B_MODELS[0] };
delete loadingModel.thinkingOptions;
delete loadingModel.defaultThinkingOptionId;
form.applyProviderSnapshot("host-b", providerSnapshot([loadingModel], "loading"));
expect(form.getState().selectedThinkingOptionId).toBe("");
form.applyProviderSnapshot("host-b", providerSnapshot(HOST_B_MODELS));
expect(form.getState()).toMatchObject({
selectedModel: "model-b",
selectedThinkingOptionId: "high",
selectedThinkingDisplay: { label: "High" },
});
form.setThinking("low");
form.applyProviderSnapshot("host-b", providerSnapshot(HOST_B_MODELS));
expect(form.getState().selectedThinkingOptionId).toBe("low");
});
it("preserves per-model thinking across model switches and snapshot updates", () => {
const form = open({
mode: "edit",
schedule: scheduleOnHost({
serverId: "host-b",
serverName: "Host B",
cwd: "/repo/b",
model: "model-b-legacy",
thinkingOptionId: "low",
}),
defaults: { serverId: null, projectTargets: PROJECT_TARGETS, preferences: {} },
});
form.applyProviderSnapshot("host-b", providerSnapshot(THINKING_MODELS));
form.setModel("mock", "model-b");
expect(form.getState().selectedThinkingOptionId).toBe("low");
form.setThinking("high");
form.setModel("mock", "model-c");
expect(form.getState().selectedThinkingOptionId).toBe("low");
form.setModel("mock", "model-b");
expect(form.getState().selectedThinkingOptionId).toBe("high");
form.setModel("mock", "model-b");
expect(form.getState().selectedThinkingOptionId).toBe("high");
form.applyProviderSnapshot("host-b", providerSnapshot(THINKING_MODELS));
expect(form.getState().selectedThinkingOptionId).toBe("high");
});
it("opens create pristine after an edit instance closes", () => {
const edit = open({
mode: "edit",