Work type
Feature slice
Change class
User-facing feature
Why this matters
The TUI forgets which model you chose. Pick a model with /model, quit, restart, and you are back on the server's default — so the choice has to be re-made every session. Owner request: "enable the cli to remember the last used model so when we restart we're using that one."
This is not a missing store. harnesscli already persists user choices to ~/.config/harnesscli/config.json — starred models, gateway, API keys, command history, and the selected theme all survive a restart. The model is the conspicuous omission, and it is the one users change most often.
Root of it: newTUIConfig (cmd/harnesscli/main.go:562) never sets Model, so TUIConfig.Model is always "" and m.selectedModel starts empty. The model then comes from whatever the daemon defaults to.
Protected path
Entry point: go-code / harnesscli --tui.
Current path that must keep working: newTUIConfig builds TUIConfig -> tui.New(cfg) sets selectedModel: cfg.Model (model.go:501) and modelSwitcher = modelswitcher.New(cfg.Model) -> the constructor then loads harnessconfig.Load() and applies starred models, gateway, API keys, and history (model.go:512-518) -> /model opens the switcher -> ModelSelectedMsg (model.go:5186) sets selectedModel, selectedProvider, selectedReasoningEffort, rebuilds the switcher, reapplies the context window, and refreshes the status bar -> runs are submitted with Model: m.selectedModel (model.go:2103, :4241).
Must not change: an empty selectedModel still means "let the daemon choose", which is what happens on a first run with nothing remembered. The context-window recalculation on model change (issue #1306) must still run. Starred models, gateway, API keys, history, and theme must keep loading exactly as they do.
Acceptance contract
- Selecting a model in the TUI persists it, along with its provider and reasoning effort, which are chosen at the same moment and are meaningless apart from it.
- On the next start, the TUI opens on that model without any user action.
- A remembered model is applied to the same fields a live selection sets, so the status bar, the switcher's current-selection highlight, the context window, and submitted runs all agree.
- An explicitly requested model takes precedence over the remembered one. Remembering must never override an instruction.
- Nothing remembered, or a corrupt/unreadable config, degrades to today's behavior — daemon default, no error shown. Losing this preference is never worth a broken start.
- No new storage location, no new file format, no new env var. The existing
harnessconfig store gains fields.
Current architecture and search evidence
cmd/harnesscli/config/config.go:10 — Config with StarredModels, Gateway, APIKeys, HistoryEntries, Theme; Load/Save against ~/.config/harnesscli/config.json, returning an empty Config when the file is absent.
cmd/harnesscli/tui/config_persist.go:15 — persistConfigField(mutate func(*harnessconfig.Config)) error, load-mutate-save. Five existing call sites (model.go:3949, :4013, :5225, :5264, :5385) — the established idiom for exactly this.
cmd/harnesscli/tui/model.go:512-518 — where persisted values are already applied during construction. The natural place to apply a remembered model.
cmd/harnesscli/tui/model.go:5186 — ModelSelectedMsg, the single point where a model choice is made. The natural place to persist.
cmd/harnesscli/main.go:562 — newTUIConfig, which omits Model.
cmd/harnesscli/main.go:149 — flags.String("model", "", "model override for this run"). Default "", so "empty means no override" is already the codebase's convention and gives the precedence rule for free.
Searched: grep -rn "selectedModel" cmd/harnesscli/tui/model.go, grep -rn "persistConfigField", grep -rn "StarredModels", grep -n "TUIConfig{" -A 16 cmd/harnesscli/main.go. No existing model persistence anywhere — grep -rln "lastUsed|last_used|persistState|SaveState" returns nothing.
Note found while searching, worth stating: runTUI never receives the -model flag, so harnesscli --tui -model X silently ignores -model today. That is a separate defect. This slice must not make it worse, and its precedence guard should be written so the flag wins as soon as it is wired.
Cross-surface impact map
Callers and data flow: harnessconfig.Config gains fields; two touch points in tui/model.go (apply at construction, persist on selection). No server, protocol, or provider change.
Config/env/defaults: ~/.config/harnesscli/config.json gains model, provider, reasoning_effort, all omitempty. Older files without them load fine; the file stays readable by an older binary, which ignores unknown fields.
API/CLI/wire formats/tools: none. Runs already carry Model; they will simply carry a remembered value rather than an empty one.
Persistence/schema/cache: additive JSON fields only. No migration. A hand-edited or corrupt file already degrades to an empty Config via the existing Load error path.
Concurrency/lifecycle: persistConfigField does load-mutate-save on the Bubble Tea update goroutine, as the five existing call sites do. There is an atomic-write test (cmd/harnesscli/config/atomic_test.go), so the write path is already covered.
Security/auth/permissions/privacy: a model ID and provider name are not secrets, and this file already stores API keys, so its permissions are already the sensitive question — unchanged here. Nothing new is written that was not already user-visible in the UI.
TUI/web/macOS/other clients: TUI only. macapp and the streaming (non-TUI) path are untouched — notably, a remembered model must NOT leak into non-TUI harnesscli runs, which have their own -model handling.
Provider/model/tool catalog: read-only. A remembered model may no longer exist or may belong to a provider whose key has been removed; the UI already handles an unavailable model with a redirect (#1404), and that path must be exercised rather than assumed.
Deployment/observability/runbooks: none.
Compatibility: additive.
Existing tests/fixtures: cmd/harnesscli/config/config_test.go and atomic_test.go; TUI tests that construct a Model and may now pick up a developer's real ~/.config/harnesscli/config.json if the constructor reads it unconditionally — the existing harnessconfig.Load() call already has this property, so the tests either tolerate it or must be checked.
Documentation: website/docs/cli/ if it documents the config file; docs/logs/engineering-log.md.
Product and UX contract
Entry and exit: unchanged commands. The difference is which model is active on start.
States:
Copy: none needed for the happy path.
Accessibility/motion/responsive: no new UI.
Real interaction QA path: pick a non-default model with /model, confirm the status bar shows it, quit, restart, confirm the status bar shows the same model without touching anything, and submit a run to confirm the request actually carries it — the status bar agreeing is not proof the run does.
In scope
Model, Provider, ReasoningEffort fields on harnessconfig.Config.
- Persist all three on
ModelSelectedMsg via persistConfigField.
- Apply them at construction when no model was explicitly requested, setting the same fields a live selection sets so the status bar, switcher, and submitted runs agree.
- Tests, docs, engineering-log entry.
Out of scope
- Wiring
-model through runTUI into TUIConfig. It is a real defect and deserves its own issue; this slice only ensures the precedence guard is correct so that fix drops in cleanly.
- Per-project (as opposed to per-user) model memory. Not asked for, and it needs a decision about which layer wins.
- Remembering any other run setting (permissions, plan mode, profile).
- The non-TUI streaming path.
Coordination and dependencies
None blocking. Sits on top of #1415/#1420, which touched the same file but not these lines.
Test-first plan
TestConfigRoundTripsModelSelection in cmd/harnesscli/config — a Config with model, provider, and reasoning effort survives Save then Load. Red today: the fields do not exist.
TestModelSelectionIsPersisted in tui — dispatch ModelSelectedMsg and assert the stored config now holds all three values. Must use a redirected config path (or the existing test seam) so it never writes to a developer's real ~/.config. Red today: nothing is written.
TestRememberedModelAppliedAtStartup in tui — with a stored model and an empty TUIConfig.Model, the constructed model has selectedModel, selectedProvider, and selectedReasoningEffort set from the store. Red today: selectedModel is "".
TestExplicitModelBeatsRememberedModel — with a stored model AND a non-empty TUIConfig.Model, the explicit value wins and the stored one is not applied. Red today: vacuous, since nothing is ever applied; it becomes the guard that keeps precedence right when -model is wired.
False-positive controls: assert that a missing stored model leaves selectedModel empty (so "remembering" cannot be faked by defaulting to something), and keep the existing starred/gateway/history/theme loading assertions green so the new field does not disturb the shared load path.
Verification plan
- Red: run all four before implementing; record output.
- Green:
go test ./cmd/harnesscli/... -race.
- Full regression:
go test ./cmd/... ./internal/....
- Real path, required — a passing unit test does not prove the model survives a real restart:
scripts/install.sh, launch the TUI, /model to a non-default model, confirm the status bar, quit, restart, confirm the status bar again without interacting, then submit a run and confirm from the run's recorded model (go-code show <run-id>) that the request actually used it. Attach that output.
- Adjacent: confirm a non-TUI
harnesscli run is unaffected, and confirm the config file still round-trips its other fields by inspecting it before and after.
Rollout and rollback
Single PR to main; picked up on the next scripts/install.sh or Homebrew --HEAD. The new JSON fields are additive and ignored by older binaries, so downgrade is safe. Rollback is reverting the commit; a stale model key left in the file is inert.
Rollback trigger: a remembered model overriding an explicit instruction, or a start failing because of a stored value.
Documentation and handoff
website/docs/cli/ — if the persisted-config file is documented, add the new fields and the precedence rule.
docs/logs/engineering-log.md — the change, and the note that -model is currently ignored by the TUI, so that defect is recorded where the next person will find it.
Definition of done
Work type
Feature slice
Change class
User-facing feature
Why this matters
The TUI forgets which model you chose. Pick a model with
/model, quit, restart, and you are back on the server's default — so the choice has to be re-made every session. Owner request: "enable the cli to remember the last used model so when we restart we're using that one."This is not a missing store.
harnessclialready persists user choices to~/.config/harnesscli/config.json— starred models, gateway, API keys, command history, and the selected theme all survive a restart. The model is the conspicuous omission, and it is the one users change most often.Root of it:
newTUIConfig(cmd/harnesscli/main.go:562) never setsModel, soTUIConfig.Modelis always""andm.selectedModelstarts empty. The model then comes from whatever the daemon defaults to.Protected path
Entry point:
go-code/harnesscli --tui.Current path that must keep working:
newTUIConfigbuildsTUIConfig->tui.New(cfg)setsselectedModel: cfg.Model(model.go:501) andmodelSwitcher = modelswitcher.New(cfg.Model)-> the constructor then loadsharnessconfig.Load()and applies starred models, gateway, API keys, and history (model.go:512-518) ->/modelopens the switcher ->ModelSelectedMsg(model.go:5186) setsselectedModel,selectedProvider,selectedReasoningEffort, rebuilds the switcher, reapplies the context window, and refreshes the status bar -> runs are submitted withModel: m.selectedModel(model.go:2103,:4241).Must not change: an empty
selectedModelstill means "let the daemon choose", which is what happens on a first run with nothing remembered. The context-window recalculation on model change (issue #1306) must still run. Starred models, gateway, API keys, history, and theme must keep loading exactly as they do.Acceptance contract
harnessconfigstore gains fields.Current architecture and search evidence
cmd/harnesscli/config/config.go:10—ConfigwithStarredModels,Gateway,APIKeys,HistoryEntries,Theme;Load/Saveagainst~/.config/harnesscli/config.json, returning an emptyConfigwhen the file is absent.cmd/harnesscli/tui/config_persist.go:15—persistConfigField(mutate func(*harnessconfig.Config)) error, load-mutate-save. Five existing call sites (model.go:3949,:4013,:5225,:5264,:5385) — the established idiom for exactly this.cmd/harnesscli/tui/model.go:512-518— where persisted values are already applied during construction. The natural place to apply a remembered model.cmd/harnesscli/tui/model.go:5186—ModelSelectedMsg, the single point where a model choice is made. The natural place to persist.cmd/harnesscli/main.go:562—newTUIConfig, which omitsModel.cmd/harnesscli/main.go:149—flags.String("model", "", "model override for this run"). Default"", so "empty means no override" is already the codebase's convention and gives the precedence rule for free.Searched:
grep -rn "selectedModel" cmd/harnesscli/tui/model.go,grep -rn "persistConfigField",grep -rn "StarredModels",grep -n "TUIConfig{" -A 16 cmd/harnesscli/main.go. No existing model persistence anywhere —grep -rln "lastUsed|last_used|persistState|SaveState"returns nothing.Note found while searching, worth stating:
runTUInever receives the-modelflag, soharnesscli --tui -model Xsilently ignores-modeltoday. That is a separate defect. This slice must not make it worse, and its precedence guard should be written so the flag wins as soon as it is wired.Cross-surface impact map
Callers and data flow:
harnessconfig.Configgains fields; two touch points intui/model.go(apply at construction, persist on selection). No server, protocol, or provider change.Config/env/defaults:
~/.config/harnesscli/config.jsongainsmodel,provider,reasoning_effort, allomitempty. Older files without them load fine; the file stays readable by an older binary, which ignores unknown fields.API/CLI/wire formats/tools: none. Runs already carry
Model; they will simply carry a remembered value rather than an empty one.Persistence/schema/cache: additive JSON fields only. No migration. A hand-edited or corrupt file already degrades to an empty
Configvia the existingLoaderror path.Concurrency/lifecycle:
persistConfigFielddoes load-mutate-save on the Bubble Tea update goroutine, as the five existing call sites do. There is an atomic-write test (cmd/harnesscli/config/atomic_test.go), so the write path is already covered.Security/auth/permissions/privacy: a model ID and provider name are not secrets, and this file already stores API keys, so its permissions are already the sensitive question — unchanged here. Nothing new is written that was not already user-visible in the UI.
TUI/web/macOS/other clients: TUI only.
macappand the streaming (non-TUI) path are untouched — notably, a remembered model must NOT leak into non-TUIharnesscliruns, which have their own-modelhandling.Provider/model/tool catalog: read-only. A remembered model may no longer exist or may belong to a provider whose key has been removed; the UI already handles an unavailable model with a redirect (#1404), and that path must be exercised rather than assumed.
Deployment/observability/runbooks: none.
Compatibility: additive.
Existing tests/fixtures:
cmd/harnesscli/config/config_test.goandatomic_test.go; TUI tests that construct aModeland may now pick up a developer's real~/.config/harnesscli/config.jsonif the constructor reads it unconditionally — the existingharnessconfig.Load()call already has this property, so the tests either tolerate it or must be checked.Documentation:
website/docs/cli/if it documents the config file;docs/logs/engineering-log.md.Product and UX contract
Entry and exit: unchanged commands. The difference is which model is active on start.
States:
Copy: none needed for the happy path.
Accessibility/motion/responsive: no new UI.
Real interaction QA path: pick a non-default model with
/model, confirm the status bar shows it, quit, restart, confirm the status bar shows the same model without touching anything, and submit a run to confirm the request actually carries it — the status bar agreeing is not proof the run does.In scope
Model,Provider,ReasoningEffortfields onharnessconfig.Config.ModelSelectedMsgviapersistConfigField.Out of scope
-modelthroughrunTUIintoTUIConfig. It is a real defect and deserves its own issue; this slice only ensures the precedence guard is correct so that fix drops in cleanly.Coordination and dependencies
None blocking. Sits on top of #1415/#1420, which touched the same file but not these lines.
Test-first plan
TestConfigRoundTripsModelSelectionincmd/harnesscli/config— aConfigwith model, provider, and reasoning effort survivesSavethenLoad. Red today: the fields do not exist.TestModelSelectionIsPersistedintui— dispatchModelSelectedMsgand assert the stored config now holds all three values. Must use a redirected config path (or the existing test seam) so it never writes to a developer's real~/.config. Red today: nothing is written.TestRememberedModelAppliedAtStartupintui— with a stored model and an emptyTUIConfig.Model, the constructed model hasselectedModel,selectedProvider, andselectedReasoningEffortset from the store. Red today:selectedModelis"".TestExplicitModelBeatsRememberedModel— with a stored model AND a non-emptyTUIConfig.Model, the explicit value wins and the stored one is not applied. Red today: vacuous, since nothing is ever applied; it becomes the guard that keeps precedence right when-modelis wired.False-positive controls: assert that a missing stored model leaves
selectedModelempty (so "remembering" cannot be faked by defaulting to something), and keep the existing starred/gateway/history/theme loading assertions green so the new field does not disturb the shared load path.Verification plan
go test ./cmd/harnesscli/... -race.go test ./cmd/... ./internal/....scripts/install.sh, launch the TUI,/modelto a non-default model, confirm the status bar, quit, restart, confirm the status bar again without interacting, then submit a run and confirm from the run's recorded model (go-code show <run-id>) that the request actually used it. Attach that output.harnessclirun is unaffected, and confirm the config file still round-trips its other fields by inspecting it before and after.Rollout and rollback
Single PR to
main; picked up on the nextscripts/install.shor Homebrew--HEAD. The new JSON fields are additive and ignored by older binaries, so downgrade is safe. Rollback is reverting the commit; a stalemodelkey left in the file is inert.Rollback trigger: a remembered model overriding an explicit instruction, or a start failing because of a stored value.
Documentation and handoff
website/docs/cli/— if the persisted-config file is documented, add the new fields and the precedence rule.docs/logs/engineering-log.md— the change, and the note that-modelis currently ignored by the TUI, so that defect is recorded where the next person will find it.Definition of done
~/.config/harnesscli/config.jsongo test ./cmd/harnesscli/... -racegreen; full regression green-modelgap