Skip to content

feat(tui): remember the last used model across restarts - #1425

Merged
dennisonbertram merged 1 commit into
mainfrom
feat/1424-remember-model
Sep 8, 2026
Merged

feat(tui): remember the last used model across restarts#1425
dennisonbertram merged 1 commit into
mainfrom
feat/1424-remember-model

Conversation

@dennisonbertram

Copy link
Copy Markdown
Owner

Closes #1424

The problem

Pick a model with /model, quit, restart — back on the daemon default. newTUIConfig (cmd/harnesscli/main.go:562) never set Model, so selectedModel started empty every session.

Not a missing store: ~/.config/harnesscli/config.json already persists starred models, gateway, API keys, command history and theme. The model was the conspicuous omission, and it is the one users change most often.

What changed

harnessconfig.Config gains Model, Provider and ReasoningEffort, all omitempty so older files load unchanged and older binaries ignore them. ModelSelectedMsg persists all three through the existing persistConfigField idiom; the constructor applies them right where starred models, gateway, keys and history are already applied.

The three travel together because they are chosen together — a provider or reasoning effort without its model describes nothing, and restoring only the model would leave the status bar agreeing with itself while runs went elsewhere.

Precedence: applying is guarded on cfg.Model == "", so an explicitly requested model always wins. Remembering a preference must never override an instruction. Empty still means "let the daemon choose", so a first run and an unreadable config both behave exactly as today.

The bigger thing this exposed

Making the model persistent turned the TUI test suite into something that mutates the developer's machine. A test run wrote into my real ~/.config/harnesscli/config.json:

"model": "gpt-4.1-mini",
"provider": "openai"

A model no human had chosen. Three tests then failed against it — including one asserting "no model chosen" — and they failed for a reason that had nothing to do with what they test.

Fixed with a package TestMain that redirects HOME once before any test runs, plus a per-test reset of the scratch config in initModel.

Both parts are needed, and the obvious approach does not work:

  • t.Setenv in the shared initModel helper panics for any test calling t.Parallel, and several in this package do. Redirecting once in TestMain, before any test starts, is both parallel-safe and race-free.
  • The redirect alone is insufficient: one scratch HOME is shared by the whole package, so a test that selects a model still leaks into every test after it. Hence the per-test reset.

I also tried t.Setenv conditionally in the helper; it broke four feedback tests that set their own HOME and assert on files written under it.

Verification

Four tests, red first:

TestRememberedModelAppliedAtStartup   selectedModel = "", want the remembered "gpt-4.1-mini"
TestModelSelectionIsPersisted         stored Model = "", want "claude-sonnet-5"

TestNoRememberedModelLeavesDaemonDefault and TestExplicitModelBeatsRememberedModel are the controls — they passed before the change and must keep passing, so "remembering" cannot be faked by defaulting to something nobody picked, and a preference cannot outrank an instruction.

go test ./cmd/harnesscli/... -race green. My real config after a full race run — no model key, which is the check that the isolation actually holds:

['history_entries', 'starred_models', 'theme']

Live restart is being verified separately and I will post the result here. The unit tests prove persist and restore; they do not prove a real TUI session survives a quit. I will not claim it until I have watched it.

Out of scope

runTUI never receives the -model flag, so harnesscli --tui -model X ignores it today. That is a real defect, recorded in the engineering log, and deserves its own issue. The precedence guard here is written so wiring it needs no further change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@dennisonbertram

Copy link
Copy Markdown
Owner Author

Live restart verified

Driven through a pty against a real daemon on this branch's binaries, with HOME redirected to a scratch directory throughout.

Before any selection — status bar shows the placeholder, no model:

Type /model to select a model  •  Type /help for all commands

After selecting a model via /model:

Model: GPT-4.1

Scratch config after quitting:

{
  "model": "gpt-4.1",
  "provider": "openai"
}

After relaunching, without touching anything:

GPT-4.1

Step 8 matches step 4. The model survives the restart.

Two details worth recording rather than glossing:

reasoning_effort was not written, because GPT-4.1 is not a reasoning model and the picker never offered an effort to choose. That is the omitempty behaving correctly, not a dropped field — but it does mean this run exercised two of the three persisted values, not all three.

The switcher in this build is a flat fuzzy filter across providers, not the two-level provider drill-down I described in the issue. My description was wrong; the behavior under test is unaffected.

Hygiene: the real ~/.config/harnesscli/config.json was re-checked afterwards and still contains only starred_models, history_entries, theme — no model key. That is the check that the TestMain isolation actually holds outside the test suite too.

Pick a model with /model, quit, restart, and you were back on the daemon
default: newTUIConfig never set Model, so selectedModel started empty
every session.

harnessconfig already persisted starred models, gateway, API keys,
history and theme, so this adds fields to that store rather than
inventing one. Model, provider and reasoning effort are stored together
because they are chosen together — a provider without its model
describes nothing.

Applying is guarded on an empty requested model, so an explicit choice
always wins: remembering a preference must never override an
instruction. Empty still means "let the daemon choose", so a first run
and an unreadable config both behave exactly as before.

This exposed a worse problem than it fixed. Persisting the model meant
the TUI test suite began writing to the developer's real
~/.config/harnesscli/config.json — a run left a model there that no
human had chosen, and three tests then failed against it. A package
TestMain now redirects HOME once before any test runs, and initModel
resets the scratch config per test. t.Setenv in a shared helper cannot
do this: it panics for tests calling t.Parallel, and several here do.

Closes #1424

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
@dennisonbertram
dennisonbertram force-pushed the feat/1424-remember-model branch from 4613abb to 5fac571 Compare September 8, 2026 13:06
@dennisonbertram
dennisonbertram merged commit 6ef2f70 into main Sep 8, 2026
2 checks passed
@dennisonbertram
dennisonbertram deleted the feat/1424-remember-model branch September 8, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tui): remember the last used model across restarts

1 participant