diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e51f69928..450eecab88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ file is the human-readable summary of what changed and why. ## Unreleased +### TUI + +- **The model you pick stays picked** — setup no longer pins its "Choose default + model" answer as a top-level `model` in the global config. That pin outranked + everything else on startup (config `model` beats the saved recent models), so + every `/models` switch silently reverted on the next launch and the only fix + was hand-editing `codev.jsonc`. The chosen model is now seeded as the most + recent selection instead, so it still starts as the default and a later switch + simply replaces it. Existing configs keep their pin — delete the `"model"` + line from `~/.config/codev/codev.jsonc` to pick up the new behavior. + ## 1.18.4-3 ### TUI diff --git a/packages/codev-gateway/src/config-block.ts b/packages/codev-gateway/src/config-block.ts index a744696cc2..c1df582484 100644 --- a/packages/codev-gateway/src/config-block.ts +++ b/packages/codev-gateway/src/config-block.ts @@ -27,10 +27,18 @@ const MODEL_MODALITIES: { attachment: boolean; modalities: { input: Modality[]; // mandatory: without it opencode sizes the context window to 0 and disables // auto-compaction; the modality block is mandatory for the same reason (hub PR // #219) — see MODEL_MODALITIES. +// +// Deliberately no top-level `model`, and the hub fragment dropped it in the +// same round (hub PR #221): a pinned default outranks the TUI's persisted +// model selection every startup (Provider defaultModel checks config first, +// then the state dir's model.json recents), so the wizard's one-time choice +// would silently undo every later /models switch. The configure flow seeds +// that choice into the recents instead, which the next switch displaces; the +// hub leads its models map with the chosen id for the same effect. A config +// written by an older hub still carries a pin — nothing here removes one. export function buildGatewayConfig(input: { apiKey: string; baseUrl: string; model: string; models?: string[] }) { const ids = [...new Set([input.model, ...(input.models ?? [])])] return { - model: `${GATEWAY_PROVIDER.id}/${input.model}`, compaction: { auto: true, reserved: GATEWAY_COMPACT_RESERVED, diff --git a/packages/codev-gateway/test/config-block.test.ts b/packages/codev-gateway/test/config-block.test.ts index ee4447bd0a..7335e78f11 100644 --- a/packages/codev-gateway/test/config-block.test.ts +++ b/packages/codev-gateway/test/config-block.test.ts @@ -22,7 +22,6 @@ describe("buildGatewayConfig", () => { models: ["MiniMax/MiniMax-M2.7", "other/model"], }) expect(config).toEqual({ - model: "netgate/MiniMax/MiniMax-M2.7", compaction: { auto: true, reserved: 29491 }, provider: { netgate: { @@ -65,6 +64,13 @@ describe("buildGatewayConfig", () => { const withDupe = buildGatewayConfig({ apiKey: "k", baseUrl: "https://gw.test", model: "m", models: ["m", "n"] }) expect(Object.keys(withDupe.provider.netgate.models)).toEqual(["m", "n"]) }) + + // A pinned default outranks the TUI's persisted selection on every startup, + // so the chosen model is seeded into the recents by the configure flow + // instead of written here. + test("pins no top-level default model", () => { + expect(buildGatewayConfig({ apiKey: "k", baseUrl: "https://gw.test", model: "m" })).not.toHaveProperty("model") + }) }) describe("buildCustomProviderConfig", () => { diff --git a/packages/tui/src/component/dialog-configure.tsx b/packages/tui/src/component/dialog-configure.tsx index d18ef544b0..feb1837fd2 100644 --- a/packages/tui/src/component/dialog-configure.tsx +++ b/packages/tui/src/component/dialog-configure.tsx @@ -3,6 +3,7 @@ import { fetchApiKey, fetchCodevConfig, fetchModels, + GATEWAY_PROVIDER, login, saveApiKey, saveCodevConfig, @@ -13,6 +14,7 @@ import open from "open" import { createSignal, onMount, Show, type Accessor } from "solid-js" import { useClipboard } from "../context/clipboard" import { useEpilogue } from "../context/epilogue" +import { useLocal } from "../context/local" import { runCustomProviderFlow, RETRIES, StatusBox, withRetries } from "./dialog-custom-provider" import { DialogProvider } from "./dialog-provider" import { useExit } from "../context/exit" @@ -46,6 +48,7 @@ interface FlowContext { toast: ReturnType exit: ReturnType epilogue: ReturnType + local: ReturnType } export function DialogConfigure() { @@ -56,6 +59,7 @@ export function DialogConfigure() { toast: useToast(), exit: useExit(), epilogue: useEpilogue(), + local: useLocal(), } // The entry chooser is a non-dismissable setup gate: no escape, no @@ -321,7 +325,17 @@ async function runModelFlow(ctx: FlowContext, creds: GatewayCreds) { }) if (result.error) throw new Error(JSON.stringify(result.error)) }) + // The config block deliberately pins no top-level `model` (it would outrank + // every later /models switch on restart), so persist the choice as the most + // recent selection instead — the highest-priority source a switch can still + // replace. Dispose first like the custom-provider flow does: config.update + // only forks the instance reload, so a bootstrap can otherwise race it and + // come back without the new provider. Skip the seed rather than let + // model.set warn about an unknown provider mid-onboarding if it does. + await ctx.sdk.client.instance.dispose() await ctx.sync.bootstrap() + if (ctx.sync.data.provider.some((item) => item.id === GATEWAY_PROVIDER.id && !!item.models[model])) + ctx.local.model.set({ providerID: GATEWAY_PROVIDER.id, modelID: model }, { recent: true }) ctx.toast.show({ variant: "info", message: `CoDev Code configured — using ${model}` }) ctx.dialog.clear() } catch (err) {