feat(dev): global Claude model + effort selector for new lab sessions #157

Merged
dominik.polakovics merged 1 commit from afk/156 into main 2026-06-13 23:06:16 +02:00

Adds a global, persisted UI control that sets the Claude model and effort for every newly-spawned lab session — manual Start, New instance, and AFK runs. A change takes effect on the next spawn with no lab restart; already-running sessions keep the model fixed in their process, and the machine-wide interactive claude default (home-manager settings.json) is left alone.

What

  • spawn.go — the single server-side source of truth for the two closed allowlists: model family aliases (opus[1m]→"Opus (1M)", sonnet, fable, haiku) and effort levels (low, medium, high, xhigh, max), plus validateSpawnConfig. Family aliases replace the pinned claude-opus-4-8[1m] id so the list tracks the latest of each family and never goes stale.
  • Store — a single global (not per-project) spawnSettings, with a SpawnConfig() getter that returns the documented opus[1m]/max defaults when unset and a validated SetSpawnConfig that rejects out-of-allowlist values without persisting (a global bad value would otherwise break every future spawn).
  • Sessions--model/--effort are appended fresh per spawn from an injected accessor wired to Store.SpawnConfig, read at spawn time. Both spawn paths (manual Start and the AFK base argv) go through it, so AFK is covered for free and the two can't drift. The login flow is unchanged.
  • POST /spawn-config — validates both values via the store's setter, persists, and returns the standard #live fragment; a rejected value surfaces in the existing error banner and nothing is written.
  • UI — two labelled native <select>s placed outside #live (poll-proof, same rationale as the filter input), auto-saving on change through the existing intercepted-fetch path, with a no-JS Save fallback hidden once JS runs. Mobile-first: 44px native controls, 16px font (no iOS zoom).

Verification

  • go test ./..., go vet ./..., and gofmt are clean (tmux-gated tests run with tmux present, as in the Nix checkPhase).
  • New inline JS verified via the project's ephemeral jsdom approach: Save button hidden when JS is active; a select change fires a POST /spawn-config carrying the chosen model+effort through the fragment fetch path; the selects keep their value across a morph.
  • New Go tests cover the exact allowlist contents, defaults validity, the store getter/setter round-trip + rejection + blank-field fallback, the endpoint's accept/reject (both transports, incl. the bracketed opus[1m] url round-trip), dynamic resolution in baseStartArgv, and the render contract (options, pre-selected value, outside-#live placement).

Closes #156

Adds a global, persisted UI control that sets the Claude **model** and **effort** for every newly-spawned lab session — manual **Start**, **New instance**, and **AFK runs**. A change takes effect on the *next* spawn with no lab restart; already-running sessions keep the model fixed in their process, and the machine-wide interactive claude default (home-manager `settings.json`) is left alone. ## What - **`spawn.go`** — the single server-side source of truth for the two closed allowlists: model family aliases (`opus[1m]`→"Opus (1M)", `sonnet`, `fable`, `haiku`) and effort levels (`low`, `medium`, `high`, `xhigh`, `max`), plus `validateSpawnConfig`. Family aliases replace the pinned `claude-opus-4-8[1m]` id so the list tracks the latest of each family and never goes stale. - **Store** — a single *global* (not per-project) `spawnSettings`, with a `SpawnConfig()` getter that returns the documented `opus[1m]`/`max` defaults when unset and a validated `SetSpawnConfig` that rejects out-of-allowlist values **without persisting** (a global bad value would otherwise break every future spawn). - **Sessions** — `--model`/`--effort` are appended fresh per spawn from an injected accessor wired to `Store.SpawnConfig`, read at spawn time. Both spawn paths (manual `Start` and the AFK base argv) go through it, so AFK is covered for free and the two can't drift. The login flow is unchanged. - **`POST /spawn-config`** — validates both values via the store's setter, persists, and returns the standard `#live` fragment; a rejected value surfaces in the existing error banner and nothing is written. - **UI** — two labelled native `<select>`s placed *outside* `#live` (poll-proof, same rationale as the filter input), auto-saving on `change` through the existing intercepted-fetch path, with a no-JS **Save** fallback hidden once JS runs. Mobile-first: 44px native controls, 16px font (no iOS zoom). ## Verification - `go test ./...`, `go vet ./...`, and `gofmt` are clean (tmux-gated tests run with tmux present, as in the Nix `checkPhase`). - New inline JS verified via the project's ephemeral jsdom approach: Save button hidden when JS is active; a select `change` fires a `POST /spawn-config` carrying the chosen model+effort through the fragment fetch path; the selects keep their value across a morph. - New Go tests cover the exact allowlist contents, defaults validity, the store getter/setter round-trip + rejection + blank-field fallback, the endpoint's accept/reject (both transports, incl. the bracketed `opus[1m]` url round-trip), dynamic resolution in `baseStartArgv`, and the render contract (options, pre-selected value, outside-`#live` placement). Closes #156
lab spawned every new session with a hardcoded
--model claude-opus-4-8[1m] --effort max. Add a global, persisted UI control
that chooses the Claude model and effort for every newly-spawned session —
manual Start, New instance, and AFK runs alike. A change takes effect on the
next spawn with no restart; already-running sessions keep the model fixed in
their process, and the machine-wide interactive claude default is untouched.

- spawn.go: single source of truth for the two closed allowlists — model
  family aliases (opus[1m]/sonnet/fable/haiku, with labels) and effort levels
  (low..max) — plus validateSpawnConfig. Family aliases replace the pinned id
  so the list never goes stale.
- store: a global spawnSettings with a SpawnConfig getter (opus[1m]/max when
  unset) and a validated SetSpawnConfig that rejects out-of-allowlist values
  without persisting.
- sessions: --model/--effort are appended fresh per spawn from an injected
  accessor (wired to Store.SpawnConfig), read at spawn time so both spawn
  paths honour the current setting and can't drift.
- POST /spawn-config validates + persists and returns the live fragment;
  rejected values surface in the error banner.
- UI: two native <select>s outside #live (poll-proof, like the filter),
  auto-saving on change through the existing fetch path with a no-JS Save
  fallback; mobile-first 44px controls.

Closes #156
Author
Owner

This was generated by AI while landing a PR.

Validation — PASS

AFK contract: head afk/156main, body carries a valid Closes #156; no conflicts (mergeable).

Verification signal: this repo's only gate is the eval-only pre-commit dry-build, which does not compile or test lab's Go — so I ran the suite directly in an isolated worktree at the PR tip (e3f74f6):

  • gofmt -l clean · go vet ./... clean · go build ./... clean · go test ./...ok lab 42.8s (tmux-gated tests included; tmux present)
  • No go.mod / go.sum / *.nix changes → no vendorHash or build-time risk; the embedded template is exercised by the render tests.

Diff review against #156's Agent Brief:

  • Model allowlist exactly Opus (1M)→opus[1m], Sonnet→sonnet, Fable→fable, Haiku→haiku; effort low/medium/high/xhigh/max; no best/opusplan/sonnet[1m].
  • One source of truth (spawn.go) backs both the dropdown render and the validation.
  • Sessions.startCmd drops the baked flags; --model/--effort are appended fresh per spawn via the injected spawnConfig accessor, and both manual Start and the AFK base argv route through baseStartArgv — AFK covered, paths can't drift (a test proves the read is at spawn time, not construction).
  • Global setting persisted under an omitempty spawn key; SetSpawnConfig validates before writing and rejects out-of-allowlist values without persisting; unset → opus[1m] + max.
  • POST /spawn-config validates → persists → returns the #live fragment; a rejected value surfaces in the error banner.
  • UI: two native <select>s outside #live (poll-proof), auto-save on change via the existing fetch path, no-JS Save fallback hidden by JS; mobile-first (44px, 16px font).
  • Machine-wide claude default untouched.

Verdict: PASS. Awaiting free-text go-ahead to merge.

> *This was generated by AI while landing a PR.* ## Validation — PASS **AFK contract:** head `afk/156` → `main`, body carries a valid `Closes #156`; no conflicts (mergeable). **Verification signal:** this repo's only gate is the eval-only pre-commit dry-build, which does **not** compile or test lab's Go — so I ran the suite directly in an isolated worktree at the PR tip (`e3f74f6`): - `gofmt -l` clean · `go vet ./...` clean · `go build ./...` clean · `go test ./...` → `ok lab 42.8s` (tmux-gated tests included; tmux present) - No `go.mod` / `go.sum` / `*.nix` changes → no vendorHash or build-time risk; the embedded template is exercised by the render tests. **Diff review against #156's Agent Brief:** - Model allowlist exactly Opus (1M)→`opus[1m]`, Sonnet→`sonnet`, Fable→`fable`, Haiku→`haiku`; effort `low/medium/high/xhigh/max`; no `best`/`opusplan`/`sonnet[1m]`. - One source of truth (`spawn.go`) backs both the dropdown render and the validation. - `Sessions.startCmd` drops the baked flags; `--model/--effort` are appended fresh per spawn via the injected `spawnConfig` accessor, and **both** manual `Start` and the AFK base argv route through `baseStartArgv` — AFK covered, paths can't drift (a test proves the read is at spawn time, not construction). - Global setting persisted under an `omitempty` `spawn` key; `SetSpawnConfig` validates **before** writing and rejects out-of-allowlist values without persisting; unset → `opus[1m]` + `max`. - `POST /spawn-config` validates → persists → returns the `#live` fragment; a rejected value surfaces in the error banner. - UI: two native `<select>`s outside `#live` (poll-proof), auto-save on change via the existing fetch path, no-JS Save fallback hidden by JS; mobile-first (44px, 16px font). - Machine-wide `claude` default untouched. **Verdict: PASS.** Awaiting free-text go-ahead to merge.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!157
No description provided.