TUI polish and fixes - #220
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
🚧 Files skipped from review as they are similar to previous changes (7)
WalkthroughAdds ChangesConfig persistence for model selection
Pinned title bar refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/tui/startup.go (1)
58-63:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAlign empty-overlay render centering with frame-based mouse hit geometry.
overlayMouseTopnow usesscrollableTranscriptFrame(...), but this function still centers with a fixednormalizedStartupHeight(...)-5heuristic. When footer height changes, rendered overlay Y and click-hit Y can drift, causing row mis-selection.Proposed fix
func (m model) emptyStateWithOverlay(width int, overlay string) string { @@ - available := normalizedStartupHeight(m.height) - 5 - if m.titleBarInTranscriptBody() { - available -= 2 - } + available := normalizedStartupHeight(m.height) - 5 + if m.altScreen && m.height > 0 { + frame := m.scrollableTranscriptFrame(m.pinnedTitleBar(width), m.footerView(width)) + available = frame.bodyHeight + } + if m.titleBarInTranscriptBody() { + available -= 2 + } gap := maxInt(0, (available-len(lines))/2) return strings.Repeat("\n", gap) + strings.Join(lines, "\n") + strings.Repeat("\n", gap) }🤖 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/startup.go` around lines 58 - 63, The startup overlay centering uses a fixed heuristic (normalizedStartupHeight(m.height) - 5) to calculate available height, but this differs from the frame-based geometry used by overlayMouseTop which uses scrollableTranscriptFrame(...). When footer height changes, this causes the rendered overlay Y position and click-hit Y position to drift, resulting in row mis-selection. Replace the hardcoded available height calculation with a frame-based approach that derives the height from the same scrollableTranscriptFrame(...) calculation used in overlayMouseTop, ensuring the centering logic is consistent with the actual frame geometry used for mouse hit detection.
🤖 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.
Outside diff comments:
In `@internal/tui/startup.go`:
- Around line 58-63: The startup overlay centering uses a fixed heuristic
(normalizedStartupHeight(m.height) - 5) to calculate available height, but this
differs from the frame-based geometry used by overlayMouseTop which uses
scrollableTranscriptFrame(...). When footer height changes, this causes the
rendered overlay Y position and click-hit Y position to drift, resulting in row
mis-selection. Replace the hardcoded available height calculation with a
frame-based approach that derives the height from the same
scrollableTranscriptFrame(...) calculation used in overlayMouseTop, ensuring the
centering logic is consistent with the actual frame geometry used for mouse hit
detection.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 72e366cc-9ddd-41a5-a471-99dac630eff7
📒 Files selected for processing (6)
internal/tui/model.gointernal/tui/mouse.gointernal/tui/mouse_test.gointernal/tui/scroll_test.gointernal/tui/startup.gointernal/tui/transcript_selection.go
Zero automated PR reviewVerdict: No blockers found Blockers
Validation
ScopeHead: This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality. |
Review — TUI polish and fixesVerdict: LGTM with a couple of minor nits. Focused and well-tested. Strengths
Minor nits (non-blocking)
Heads-up: merge collisionThis touches |
# Conflicts: # internal/tui/mouse_test.go # internal/tui/transcript_selection.go
gnanam1990
left a comment
There was a problem hiding this comment.
Approving — clean, well-tested, and CI is green across all platforms. The viewport-math consolidation through a single scrollableTranscriptFrame is a nice improvement beyond the surface feature, and routing both /model <x> and the model picker through handleModelCommand means a selection persists either way.
A few non-blocking nits for a follow-up (or now — your call):
- Display vs. persisted value: the status line reports
target.modelIDwhilepersistSelectedModelwritesnextProfile.Model. They're equal in the covered path, so this isn't a known bug — but persistingtarget.modelID(or asserting the two match) removes any latent risk of saving a different value than was shown. - Project-only configs: persistence targets only
userConfigPath, so with just a project config the selection silently doesn't persist and nosaved:line appears. Consider surfacing something likesaved: no (no user config)so it isn't silently dropped. - Test coverage: the happy path is well covered; the
persistErrbranch (saved: no (...)) and the empty-userConfigPathno-op aren't. A small test for each would lock in the behavior. - Readability: the short-terminal clamping in
scrollableTranscriptFrame(drop the header first, then re-clamp the footer) is correct and the priority is sensible — a one-line comment on the precedence (composer wins, header sacrificed) would help future readers.
Optional: a quick manual alt-screen smoke — scroll (title stays pinned), click a reasoning toggle, drag-select transcript text, open the model picker — since the overlay/mouse coordinate edges are the kind of thing that passes unit tests but can still land a row off live.
Heads-up on merge ordering (not a blocker here): #224 also touches model.go / command_center.go, so whichever of #220/#224 lands second will need a rebase.
LGTM 👍
Three review threads to close out: **Cadence (~12 words/sec → ~1/sec)** @anandh8x and @gnanam1990 both flagged the on-every-tick advance as flicker. The spinner glyph should still spin fast (it conveys liveness), but the word needs to be readable. Solution: the model owns a `workingVerbTicks` counter and gates the advance at `WorkingWordsStepEvery = 12` spinner ticks (~960ms at 80ms cadence). The `workingWords` ring stays a dumb "advance one slot" type so unit tests can tick it rapidly without waiting for a real spinner. New test `TestWorkingVerbAdvancesOnStepEverySpinnerTicks` locks the cadence behaviour in. **Brand-name "inconsistency" (intentional)** @gnanam1990 noted the code says "Gitlawb / OpenFable" while the repo, binary, and PR description say "Zero". Author's call: the working-words PR ships the rebrand-ahead-of-the-rebrand verbs on purpose. Kept the existing strings (`gitlawbmaxxing`, `openfablemaxxing`, …) and tidied the comments so the trade-off is explicit ("a planned rename; the spinner brand is intentionally ahead of the rest of the project"). Also fixed a stale `model.go` comment that still said "zeroling" (superseded by `openfablemaxxing` in the previous force-push). **Taste call (pilled / aura-farming / maxxing)** Both reviewers flagged these as user-facing with no config escape. Author's call: ship as-is, with a comment on `vibeVerbs` calling out the trade-off explicitly so the next reader can see it was a deliberate decision rather than an oversight. **weightedRing comment vs implementation** CodeRabbit and @gnanam1990 both caught that the comment claimed brand verbs are "interleaved" but the implementation appends them as a sequential block at the end of the ring. Fixed the comment to match the implementation and added a note on how to implement true interleaving if it's ever wanted. **CodeRabbit nits** - `Reset()` now also checks `len(w.weighted) == 0` for consistency with `Tick()` and `Current()`. - `weightedRing` body simplified to `append(ring, brandVerbs...)` instead of the conditional loop (same result, clearer intent). **Competitor naming in comments** @gnanam1990 asked to trim the few "Claude Code" mentions in package comments. Replaced the explicit "Claude Code" references with neutral phrasing ("upstream Claude-Code-compatible spinners", "a few classic gerunds") that keeps the design rationale without naming the competitor. The user-facing verb list is untouched. Rebased onto current main (1e25fde) and resolved the spec_mode.go merge conflict by keeping both the fade-seed and the working-verb reset on the spec-impl path. Build + vet + gofmt clean. Full internal/tui/ test suite green except for the pre-existing env-fragile `TestModelCommandPersistsSelectedModelToUserConfig` from PR #220 (fails here only because of local env, not this PR). Co-Authored-By: Claude <noreply@anthropic.com>
* feat(tui): rotating "working word" for the liveness spinner The assistant interim block used to show a static "working…" placeholder while the model was generating. That felt dead on long runs. Replace it with a rotating gerund tuned for the Gitlawb / Zero brand: project-name verbs (gitlawbmaxxing, zeroling, …) weighted 1.5x so the brand word anchors the rotation, plus feature-as-verbs, gen-Z crowd-pleasers, and a few Claude-Code originals for variety. The rotation is a tiny ring buffer (working_words.go) advanced on each spinner.Tick so the verb and the glyph stay in lockstep. Brand words are duplicated in the ring, not randomized, so the cadence is deterministic and the first frame is always "gitlawbmaxxing" — easy to test, easy to reason about, and the first thing a long-running user sees is the project name. Reset() rewinds the ring when a new run starts (model.go: submit, spec_mode.go: spec approval → impl). Lowercase throughout to brand-differentiate from Claude Code's Title Case defaults. No config wiring (hardcoded list); can be a follow-up if users ask for customisation. Tests: 7 new tests in working_words_test.go (first frame, tick, wrap, reset, brand weighting, base-list integrity, nil safety) plus the existing interim-block test updated to assert "gitlawbmaxxing" as the new default. Full internal/tui/ suite green in 2.7s. * Address review feedback on working-words PR Three review threads to close out: **Cadence (~12 words/sec → ~1/sec)** @anandh8x and @gnanam1990 both flagged the on-every-tick advance as flicker. The spinner glyph should still spin fast (it conveys liveness), but the word needs to be readable. Solution: the model owns a `workingVerbTicks` counter and gates the advance at `WorkingWordsStepEvery = 12` spinner ticks (~960ms at 80ms cadence). The `workingWords` ring stays a dumb "advance one slot" type so unit tests can tick it rapidly without waiting for a real spinner. New test `TestWorkingVerbAdvancesOnStepEverySpinnerTicks` locks the cadence behaviour in. **Brand-name "inconsistency" (intentional)** @gnanam1990 noted the code says "Gitlawb / OpenFable" while the repo, binary, and PR description say "Zero". Author's call: the working-words PR ships the rebrand-ahead-of-the-rebrand verbs on purpose. Kept the existing strings (`gitlawbmaxxing`, `openfablemaxxing`, …) and tidied the comments so the trade-off is explicit ("a planned rename; the spinner brand is intentionally ahead of the rest of the project"). Also fixed a stale `model.go` comment that still said "zeroling" (superseded by `openfablemaxxing` in the previous force-push). **Taste call (pilled / aura-farming / maxxing)** Both reviewers flagged these as user-facing with no config escape. Author's call: ship as-is, with a comment on `vibeVerbs` calling out the trade-off explicitly so the next reader can see it was a deliberate decision rather than an oversight. **weightedRing comment vs implementation** CodeRabbit and @gnanam1990 both caught that the comment claimed brand verbs are "interleaved" but the implementation appends them as a sequential block at the end of the ring. Fixed the comment to match the implementation and added a note on how to implement true interleaving if it's ever wanted. **CodeRabbit nits** - `Reset()` now also checks `len(w.weighted) == 0` for consistency with `Tick()` and `Current()`. - `weightedRing` body simplified to `append(ring, brandVerbs...)` instead of the conditional loop (same result, clearer intent). **Competitor naming in comments** @gnanam1990 asked to trim the few "Claude Code" mentions in package comments. Replaced the explicit "Claude Code" references with neutral phrasing ("upstream Claude-Code-compatible spinners", "a few classic gerunds") that keeps the design rationale without naming the competitor. The user-facing verb list is untouched. Rebased onto current main (1e25fde) and resolved the spec_mode.go merge conflict by keeping both the fade-seed and the working-verb reset on the spec-impl path. Build + vet + gofmt clean. Full internal/tui/ test suite green except for the pre-existing env-fragile `TestModelCommandPersistsSelectedModelToUserConfig` from PR #220 (fails here only because of local env, not this PR). Co-Authored-By: Claude <noreply@anthropic.com> * Fix cadence test post-advance window (CodeRabbit) The final block of `TestWorkingVerbAdvancesOnStepEverySpinnerTicks` had two issues caught by CodeRabbit: 1. A `_ = m` no-op statement that asserted nothing. 2. A tautological check — it compared the post-advance verb against the *original* `before` value, which is always going to differ after the previous block already advanced. The real intent was to verify that the cadence counter *resets* after each advance (rather than carrying over), so a follow-up advance lands one full `WorkingWordsStepEvery` window later — not one window early due to a missed reset. Rewrote the trailing block to: - Capture the post-advance value explicitly (`afterAdvance`). - Drive another (WorkingWordsStepEvery - 1) ticks after the advance. - Assert the verb stayed at `afterAdvance` — catching the "counter doesn't reset" regression the original test was meant to guard against. This now meaningfully tests three regressions: - Gate removed (verb advances every tick) — caught by the first block. - Gate set to 1 (same effect) — caught by the first block. - Counter doesn't reset after advance (verb advances too eagerly) — caught by the new third block. Updated the test docstring to call out all three failure modes. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Tests
Summary by CodeRabbit
New Features
Bug Fixes
Tests