Skip to content

Fast mode: /fast command, model marker, and transient toast#490

Closed
gnanam1990 wants to merge 3 commits into
mainfrom
feat/fast-command
Closed

Fast mode: /fast command, model marker, and transient toast#490
gnanam1990 wants to merge 3 commits into
mainfrom
feat/fast-command

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a /fast command and a first-class "fast mode" affordance to the TUI: switch the active model to its faster same-family variant, see at a glance which models offer one, and get transient feedback that stays out of the transcript.

Three focused commits:

  1. /fast [on|off] — toggle the session model between its base and fast variant (state is derived from model identity, not stored).
  2. Registry — a curated FastVariantID field as the single source of truth, plus a memoized DefaultRegistry().
  3. UI — an f marker + focused-row hint across the model/provider pickers and the active-model chip, and a transient toast for /fast feedback.

Registry: FastVariantID (data, not logic)

  • New FastVariantID field on ModelEntry, mirroring the existing UpgradeTargetID pattern: populated in the catalog via decorateModelDepth, validated at registry build (must resolve to a known model).
  • Registry.FastVariant / BaseVariant / HasFastVariant derive both directions from this one field — the previous hardcoded pair table is gone. Adding a pair is now a one-line catalog change.
  • Curated pairs (the only unambiguous 1:1, same-family, cheaper-tier, both-active pairs in the catalog):
    • claude-sonnet-4.5 → claude-haiku-4.5
    • gpt-4.1 → gpt-4.1-mini
    • gpt-4o → gpt-4o-mini
    • gemini-2.5-pro → gemini-2.5-flash
  • DefaultRegistry() is now memoized (sync.OnceValues): the default catalog is static, every accessor returns clones, and a built registry is never mutated, so the per-frame TUI callers (title chip, context gauge, pickers) stop re-cloning entries and recompiling fuzzy-match regexes on every render. This also removes a pre-existing per-frame cost in the context gauge.

UI: marker, hint, and toast

  • f marker on models that have a fast variant — on the /model picker rows, the /provider wizard rows, and the active-model chip in the title bar.
  • Focused-row hint (⚡ fast: <name> · /fast on): the pickers are keyboard-driven and the TUI has no floating-tooltip primitive, so the reveal is the selected-row detail line (works for keyboard-only users).
  • Transient toast: /fast feedback no longer appends a permanent transcript line — it shows a brand pill in the fixed-height row above the composer and auto-dismisses after 3s (seq-gated, reusing the existing copy-status self-clear pattern). Messages have a distinct voice: ⚡ Fast lane on · <model>, Fast lane off · <model>, ⚡ No fast lane for <model>, ⚡ Pick a model first · /model.

Coverage

Fast mode covers the four base models above (the curated registry's three providers: openai / anthropic / google). Runtime-configured providers and off-catalog models simply show no marker until they are catalogued with a FastVariantID — the field-driven design means they light up automatically once added, with no code change.

Testing

  • go build ./..., go vet ./..., gofmt clean.
  • go test ./... -race73 packages, 0 failures, 0 races.
  • New table-driven tests: registry (FastVariant/BaseVariant/HasFastVariant + catalog-pair invariants), markers/hint across all three surfaces, and the toast (raise / newline-flatten / dismiss).

Notes

Rebased on latest main (includes /new from #478); no conflicts remain. Team contribution.

Summary by CodeRabbit

  • New Features

    • Added a new /fast command to switch between standard and faster model variants.
    • Model selection screens now show when a faster variant is available, including a badge and hint text.
    • Fast-mode actions now display a short, temporary toast message instead of adding a permanent line.
  • Bug Fixes

    • Improved model switching behavior so unavailable, unknown, or deprecated variant links are handled safely.
    • Refreshed model registry loading so default model data is reused more efficiently.

/fast [on|off] switches the session model between its base and a faster
same-family variant (e.g. Sonnet<->Haiku, GPT-4.1<->GPT-4.1-mini). State is
DERIVED from the model identity, never stored: the session is "in fast mode"
iff the current model has a base variant.

- modelregistry: a config-driven base<->fast mapping table (fastVariantPairs)
  plus Registry.FastVariant / BaseVariant, mirroring the existing UpgradeTarget
  pattern. Adding a pair is a one-line data change — no logic edit. Populated
  for the models with a meaningful fast variant (Sonnet/Haiku, GPT-4.1/mini,
  GPT-4o/mini, Gemini-2.5 Pro/Flash), guarded by tests that every id resolves
  and no id is both a base and a fast.
- tui: /fast registered in commandDefinitions (model group) and dispatched via
  a pure planFastCommand decision + handleFastCommand, which delegates the real
  switch to handleModelCommand (provider rebuild, compaction guard, persistence)
  rather than mutating state directly. Notices use the existing appendSystem
  mechanism.
- Tests: table-driven over every case — invalid arg, no model set, no-op both
  ways, no fast variant, switch to fast (⚡) / back to base, switch-error
  surfaced, and the variant helpers.

Gate: make build / go vet ./... / go test ./... -race / make lint all green.
…istry

Add a FastVariantID field to ModelEntry (mirroring UpgradeTargetID),
populated in the catalog for the four same-family base/fast pairs and
validated at registry build. Refactor the /fast helpers to derive both
directions from this field instead of a hardcoded table, and add a
HasFastVariant predicate for UI callers.

Memoize DefaultRegistry with sync.OnceValues: the default catalog is
static, every accessor returns clones, and a built registry is never
mutated, so per-frame TUI callers no longer re-clone entries and
recompile fuzzy-match regexes each render.
Surface which models have a fast variant: a dim "f" badge on the
active-model chip, the /model picker, and the /provider wizard, plus a
focused-row "fast: <name>" hint (the pickers are keyboard-driven, so the
selected row is the reveal). All read Registry.FastVariant, so coverage
tracks the catalog automatically.

Give /fast a distinctive voice and stop it cluttering the transcript:
reword its messages and show the result as a transient toast pill in the
row above the composer, auto-dismissed after 3s (seq-gated, same pattern
as the copy-status flash).
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 533b41495d52
Changed files (13): internal/modelregistry/catalog.go, internal/modelregistry/models.go, internal/modelregistry/variants.go, internal/modelregistry/variants_test.go, internal/tui/commands.go, internal/tui/fast_command.go, internal/tui/fast_command_test.go, internal/tui/fast_marker.go, internal/tui/fast_marker_test.go, internal/tui/model.go, internal/tui/picker.go, internal/tui/provider_wizard.go, and 1 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@gnanam1990
gnanam1990 marked this pull request as draft July 4, 2026 13:45
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a fast/base model variant system: ModelEntry.FastVariantID with validation, Registry.FastVariant/BaseVariant/HasFastVariant lookups, curated fast-variant mappings, a cached DefaultRegistry, a new /fast TUI command with planning logic, and UI badges/hints/toasts surfacing fast variants in pickers, titles, and provider wizard.

Changes

Fast variant feature

Layer / File(s) Summary
Registry caching and data model
internal/modelregistry/catalog.go, internal/modelregistry/models.go
Caches DefaultRegistry() via sync.OnceValues, adds FastVariantID field with cross-entry validation in NewRegistry, and wires curated fastVariant mappings (claude-sonnet-4.5, gemini-2.5-pro, gpt-4.1, gpt-4o) in decorateModelDepth.
FastVariant/BaseVariant resolution
internal/modelregistry/variants.go, internal/modelregistry/variants_test.go
Implements FastVariant, BaseVariant, HasFastVariant registry methods with deprecated/unresolved handling, and covers them with resolution, ambiguity, and lookup tests.
/fast command and planning logic
internal/tui/commands.go, internal/tui/fast_command.go, internal/tui/fast_command_test.go
Adds commandFast kind/definition, fastPlan/planFastCommand/handleFastCommand/modelShortName, delegating actual switching to handleModelCommand, with extensive test coverage of terminal, switch, and error cases.
Fast badges, hints, and toast UI
internal/tui/fast_marker.go, internal/tui/fast_marker_test.go, internal/tui/model.go
Adds badge/hint rendering helpers and a sequence-gated toast (showFastToast/fastToastExpiredMsg/renderFastToast), wired into model state, footer rendering, and command dispatch.
Model picker and provider wizard display
internal/tui/picker.go, internal/tui/view.go, internal/tui/provider_wizard.go
Adds FastVariant field to pickerItem, decorates picker rows via registry lookup, updates overlay width/row/detail rendering, title bar badge, and provider wizard model rows/details.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • Gitlawb/zero#56: Directly touches DefaultRegistry() construction in the same catalog file.
  • Gitlawb/zero#219: Modifies the same titleModelSegment rendering logic in internal/tui/view.go.

Suggested reviewers: Vasanthdev2004

Straight talk for the reviewer: check that BaseVariant's reverse-scan doesn't produce ambiguous matches when multiple entries share a FastVariantID, confirm handleFastCommand correctly surfaces failure vs. success paths without silently swallowing errors, and verify the toast sequence-gating actually prevents stale dismissals racing with rapid /fast toggles.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main additions: the /fast command, UI marker, and transient toast.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fast-command

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/modelregistry/models.go (1)

378-389: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider hardening the FastVariantID validation at build time.

The self-reference and uniqueness invariants (TestCatalogFastVariantsResolve, TestFastVariantPairsUnambiguous) are currently only enforced by unit tests, not by NewRegistry itself — unlike the "resolves to a known model" check added here. A future catalog edit could introduce a self-referencing or ambiguous FastVariantID without the registry failing loudly, only surfacing via CI test runs.

♻️ Proposed hardening
 	for _, entry := range registry.models {
 		fastID := strings.TrimSpace(entry.FastVariantID)
 		if fastID == "" {
 			continue
 		}
-		if _, ok := registry.Get(fastID); !ok {
+		target, ok := registry.Get(fastID)
+		if !ok {
 			return Registry{}, fmt.Errorf("model %q fast variant %q does not resolve to a known model", entry.ID, fastID)
 		}
+		if target.ID == entry.ID {
+			return Registry{}, fmt.Errorf("model %q names itself as its fast variant", entry.ID)
+		}
 	}
+	seenFastTargets := make(map[string]string)
+	for _, entry := range registry.models {
+		fastID := strings.TrimSpace(entry.FastVariantID)
+		if fastID == "" {
+			continue
+		}
+		target, _ := registry.Get(fastID)
+		if prevBase, dup := seenFastTargets[target.ID]; dup {
+			return Registry{}, fmt.Errorf("models %q and %q both declare %q as their fast variant", prevBase, entry.ID, target.ID)
+		}
+		seenFastTargets[target.ID] = entry.ID
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/modelregistry/models.go` around lines 378 - 389, Strengthen
FastVariantID validation inside NewRegistry alongside the existing
registry.Get(fastID) check: verify each non-empty FastVariantID is not
self-referencing, and ensure fast-variant mappings are unambiguous across
registry.models. Use the existing FastVariantID handling in the registry build
path to reject duplicate or ambiguous fast targets and return an error
immediately, rather than relying only on TestCatalogFastVariantsResolve and
TestFastVariantPairsUnambiguous.
internal/tui/fast_command.go (1)

22-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Redundant initial assignment flagged by ineffassign.

enable := true on line 25 is always overwritten before use — every switch branch reassigns it (or returns early in default). golangci-lint correctly flags this; declare it unset and let the switch set it.

🧹 Proposed fix
-	enable := true
+	var enable bool
 	switch strings.ToLower(strings.TrimSpace(arg)) {
 	case "", "on":
 		enable = true
 	case "off":
 		enable = false
 	default:
 		return fastPlan{message: fmt.Sprintf("⚡ /fast takes on or off, not %q", strings.TrimSpace(arg))}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/fast_command.go` around lines 22 - 33, The initial assignment in
planFastCommand is redundant and gets overwritten before use, so golangci-lint
flags it. Update planFastCommand in fast_command.go by removing the default
enable initialization and let the switch on
strings.ToLower(strings.TrimSpace(arg)) set enable explicitly in the ""/"on" and
"off" branches, keeping the existing default usage-error return unchanged.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/modelregistry/models.go`:
- Around line 378-389: Strengthen FastVariantID validation inside NewRegistry
alongside the existing registry.Get(fastID) check: verify each non-empty
FastVariantID is not self-referencing, and ensure fast-variant mappings are
unambiguous across registry.models. Use the existing FastVariantID handling in
the registry build path to reject duplicate or ambiguous fast targets and return
an error immediately, rather than relying only on TestCatalogFastVariantsResolve
and TestFastVariantPairsUnambiguous.

In `@internal/tui/fast_command.go`:
- Around line 22-33: The initial assignment in planFastCommand is redundant and
gets overwritten before use, so golangci-lint flags it. Update planFastCommand
in fast_command.go by removing the default enable initialization and let the
switch on strings.ToLower(strings.TrimSpace(arg)) set enable explicitly in the
""/"on" and "off" branches, keeping the existing default usage-error return
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 783afd58-50af-4c08-8df0-253f28a3097b

📥 Commits

Reviewing files that changed from the base of the PR and between f401c66 and 533b414.

📒 Files selected for processing (13)
  • internal/modelregistry/catalog.go
  • internal/modelregistry/models.go
  • internal/modelregistry/variants.go
  • internal/modelregistry/variants_test.go
  • internal/tui/commands.go
  • internal/tui/fast_command.go
  • internal/tui/fast_command_test.go
  • internal/tui/fast_marker.go
  • internal/tui/fast_marker_test.go
  • internal/tui/model.go
  • internal/tui/picker.go
  • internal/tui/provider_wizard.go
  • internal/tui/view.go

@gnanam1990 gnanam1990 closed this Jul 4, 2026
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.

1 participant