Skip to content

Harden TUI layout and mouse routing#226

Merged
anandh8x merged 9 commits into
mainfrom
tui-backend-hardening
Jun 16, 2026
Merged

Harden TUI layout and mouse routing#226
anandh8x merged 9 commits into
mainfrom
tui-backend-hardening

Conversation

@anandh8x

@anandh8x anandh8x commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • centralize chat frame geometry for header, transcript body, footer, composer, and status regions
  • route composer, overlay, and transcript mouse hit testing through shared region geometry
  • preserve current transcript rendering and bottom-anchored scroll behavior for the first slice of TUI hardening

Notes

This is the single draft PR for the TUI backend hardening work. Follow-up commits for the transcript viewport/cache work should stack onto this branch instead of opening separate PRs.

Tests

  • env GOCACHE=/tmp/zero-go-cache go test ./internal/tui
  • /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..."

Summary by CodeRabbit

  • Refactor
    • Updated alt-screen transcript UI to use geometry-aware framing and viewport-based scrolling for more accurate header/body/footer behavior.
    • Reworked transcript body rendering into structured line items to improve selection and overlay rendering consistency.
    • Centralized transcript scroll/visible-window calculations to clamp offsets reliably.
  • Bug Fixes
    • Improved mouse hit detection for transcript lines and overlay interactions, including correct region mapping and tighter click clamping.
    • Adjusted composer/status hit targeting to match visible areas.
  • Tests
    • Expanded unit and interaction tests, including tiny/degenerate terminal dimensions and overlay/selection mouse scenarios.

anandh8x added 2 commits June 16, 2026 21:28
Add a reusable transcript frame snapshot with terminal regions for header, body, footer, composer, and status lines. Mouse composer hit testing, overlay positioning, and transcript viewport mapping now consume the same frame geometry instead of recalculating pinned-header and clipped-footer offsets separately.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui
Return overlay hit coordinates relative to the overlay block and derive transcript hit rows from the shared body rect. This keeps the current scroll behavior while making mouse handling consume component-local geometry from the frame layout.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 08cdda34-10af-49a4-8bea-2bc6c6a05210

📥 Commits

Reviewing files that changed from the base of the PR and between b8931d2 and 0f2307f.

📒 Files selected for processing (2)
  • internal/tui/render_cache_test.go
  • internal/tui/transcript_selection.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/tui/transcript_selection.go

Walkthrough

Introduces tuiRect geometry helpers and expands transcriptFrameLayout to track per-region rectangles (header, body, footer, composer, status) with footer clipping metadata. Adds transcriptViewport scrolling primitives for bounded-range scroll math. Refactors transcript body rendering to use structured items and layout abstractions. Integrates frame computation, alt-screen scroll handling, mouse hit-testing, and transcript line selection to consume rect-based coordinates instead of manual line-count arithmetic. Validates with layout, viewport, transcript body, and mouse tests.

Changes

Rectangle-based TUI frame geometry with viewport scrolling

Layer / File(s) Summary
tuiRect type and transcriptFrameLayout expansion
internal/tui/model.go
Defines tuiRect with contains(x, y) and local(x, y) coordinate helpers. Expands transcriptFrameLayout to store per-region tuiRect fields (header, body, footer, composer, status) and footer clipping offsets. Updates scrollableTranscriptFrame to compute these rects and adds footerSubrect, footerLineRect helpers to map footer overlay content into clipped regions. Routes alt-screen rendering through scrollableTranscriptLayoutView using the computed layout.
Transcript viewport scrolling primitives
internal/tui/transcript_viewport.go
Introduces transcriptViewport and transcriptViewportWindow types to track total lines, height, and clamped offset. Provides newTranscriptViewport constructor with input sanitization, transcriptViewportForBody and transcriptViewportForLayout factories, and methods: maxOffset() (scroll range), scroll(delta) (clamped scrolling), window() (visible line range).
Transcript body item and layout abstractions
internal/tui/transcript_selection.go
Introduces transcriptBodyItemKind, transcriptBodyItem, transcriptBodyRenderedItem, and transcriptBodyLayout to replace manual string concatenation. Refactors model.transcriptBody to build structured items via m.transcriptBodyItems and layoutTranscriptBodyItems, which track per-item span geometry while aggregating rendered lines and selectable metadata. Updates selectable row rendering to precompute per-line metadata and skip styled assembly when transcript selection is inactive.
Alt-screen chat scroll handling via viewport
internal/tui/model.go
Refactors scrollChat to compute chatScrollOffset from chatTranscriptViewport().scroll(delta).offset with early return when viewport is unavailable. Updates chatScrollMetrics to return visible-line count and max offset from viewport logic. Adds chatTranscriptViewport to construct transcript body+frame layout and return viewport for alt-screen scrolling.
Mouse hit-testing refactored to rect geometry
internal/tui/mouse.go
Adds horizontal x offset to mouseOverlayHit. Simplifies mouseOverComposer to use direct frame.composerRect.contains(...) check. Rewrites overlayMouseRect to center overlays using frame.bodyRect in alt-screen mode and return zero-value tuiRect{} for non-positive heights. Updates overlayMouseHit and overlayMouseTop accordingly.
Transcript line selection and viewport start with rect-based mapping
internal/tui/transcript_selection.go
transcriptLineAtMouse builds the scrollable frame, computes viewport start via transcriptViewportStartForLayout, and maps mouse coordinates to body-local Y via frame.bodyRect.local(...) before matching transcript lines. Introduces transcriptViewportStartForLayout helper that derives height and Y-origin from frame.bodyRect instead of header-line counts. selectedTranscriptText uses transcriptBodyLayout(...).selectable.
Layout, viewport, transcript body, and mouse integration tests
internal/tui/layout_test.go, internal/tui/transcript_viewport_test.go, internal/tui/transcript_body_test.go, internal/tui/mouse_test.go, internal/tui/render_cache_test.go
Seven layout tests validate rect positioning (header/body/footer pinning), footer clipping in tiny terminals, degenerate dimension handling, composer hit-testing, overlay centering, overlay hit clamping, and transcript line selection with body region. Five viewport tests validate bottom-anchored windows, scroll clamping at bounds, non-scrollable offset behavior, zero/negative input normalization, and inflated offset clamping. Four transcript body tests validate empty-state, selectable line positioning, pending interim rendering, and visible lines slicing. One mouse test validates overlay blocking transcript selection. One render-cache test validates assistant row caching when selection is inactive.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Gitlawb/zero#220: Both PRs refactor the alt-screen transcript layout and hit-testing to use frame-derived geometry via scrollableTranscriptFrame—updating functions like transcriptViewportStart/transcriptLineAtMouse and composer/overlay mouse positioning in internal/tui/mouse.go/internal/tui/transcript_selection.go in a directly overlapping way.
  • Gitlawb/zero#204: Both PRs modify internal/tui/transcript_selection.go's coordinate/metadata used for selectable transcript hit-testing—this PR by switching to frame/viewport geometry mapping, and the related PR by adjusting renderSelectableAssistantRow rendering so selection math aligns with visual layout.
  • Gitlawb/zero#163: Both PRs modify the TUI alt-screen chat/scroll handling in internal/tui/model.go wiring for chatScrollOffset and wheel/PageUp/PageDown behavior.

Suggested reviewers

  • gnanam1990
  • Vasanthdev2004
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Harden TUI layout and mouse routing' directly and accurately describes the main change: centralizing frame geometry and refactoring mouse event routing through shared region geometry.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 tui-backend-hardening

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

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/tui/transcript_selection.go (1)

313-314: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block transcript selection while the MCP add wizard is open.

Clicks that miss a wizard item fall through from handleMouse into transcript selection, and this guard does not include m.mcpAddWizard. That lets users select/toggle transcript rows behind the modal.

🐛 Proposed fix
-	if !m.altScreen || m.height <= 0 || m.setup.visible || m.providerWizard != nil || m.mcpManager != nil || m.picker != nil || m.suggestionsActive() {
+	if !m.altScreen || m.height <= 0 || m.setup.visible || m.providerWizard != nil || m.mcpAddWizard != nil || m.mcpManager != nil || m.picker != nil || m.suggestionsActive() {
 		return transcriptSelectableLine{}, false
 	}
🤖 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/transcript_selection.go` around lines 313 - 314, The guard
condition that blocks transcript selection is missing a check for the MCP add
wizard state. In the condition that checks various modal/wizard states
(m.altScreen, m.height, m.setup.visible, m.providerWizard, m.mcpManager,
m.picker, m.suggestionsActive), add a check for m.mcpAddWizard != nil to prevent
transcript selection when the MCP add wizard is open. This will prevent clicks
that miss wizard items from falling through to transcript selection and allowing
row selection behind the modal.
internal/tui/mouse.go (1)

445-452: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp overlay mouse hits to the visible body window.

When an overlay is taller than frame.bodyRect.height, overlayViewportLines renders only the visible body rows, but overlayMouseHit still accepts top+len(lines). Clicks in the footer/composer can select invisible overlay rows.

🐛 Proposed fix
 func (m model) overlayMouseHit(msg tea.MouseMsg, overlay string, width int) (mouseOverlayHit, bool) {
 	lines := viewLines(overlay)
 	if len(lines) == 0 {
 		return mouseOverlayHit{}, false
 	}
 	left, lines, overlayWidth := normalizeOverlayBlock(lines, width)
 	if overlayWidth <= 0 || len(lines) == 0 {
 		return mouseOverlayHit{}, false
 	}
-	top := m.overlayMouseTop(len(lines), width)
-	if mouseY(msg) < top || mouseY(msg) >= top+len(lines) {
+	rect := m.overlayMouseRect(len(lines), width)
+	if rect.height <= 0 || mouseY(msg) < rect.y || mouseY(msg) >= rect.y+rect.height {
 		return mouseOverlayHit{}, false
 	}
 	if mouseX(msg) < left || mouseX(msg) >= left+overlayWidth {
 		return mouseOverlayHit{}, false
 	}
-	return mouseOverlayHit{x: mouseX(msg) - left, y: mouseY(msg) - top}, true
+	return mouseOverlayHit{x: mouseX(msg) - left, y: mouseY(msg) - rect.y}, true
 }
 
 func (m model) overlayMouseRect(overlayHeight int, width int) tuiRect {
 	if overlayHeight <= 0 {
 		return tuiRect{}
 	}
 	if m.altScreen && m.height > 0 {
 		frame := m.scrollableTranscriptFrame(m.pinnedTitleBar(width), m.footerView(width))
+		visibleHeight := minInt(overlayHeight, frame.bodyRect.height)
+		if visibleHeight <= 0 {
+			return tuiRect{}
+		}
 		return tuiRect{
-			y:      frame.bodyRect.y + maxInt(0, (frame.bodyRect.height-overlayHeight)/2),
+			y:      frame.bodyRect.y + maxInt(0, (frame.bodyRect.height-visibleHeight)/2),
 			width:  width,
-			height: overlayHeight,
+			height: visibleHeight,
 		}
 	}

Also applies to: 459-469

🤖 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/mouse.go` around lines 445 - 452, The overlayMouseHit function
currently checks if mouseY is within the entire overlay height using
top+len(lines), but this doesn't account for the visible body window when the
overlay is taller than the frame.bodyRect.height. The mouse Y boundary check
needs to be clamped to the visible body window instead of the full overlay.
Modify the condition that checks mouseY(msg) >= top+len(lines) to use the actual
visible height (capped by frame.bodyRect.height) instead of the total
len(lines). Apply this same fix to the similar mouse boundary check code that
appears around lines 459-469.
🤖 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/mouse.go`:
- Around line 445-452: The overlayMouseHit function currently checks if mouseY
is within the entire overlay height using top+len(lines), but this doesn't
account for the visible body window when the overlay is taller than the
frame.bodyRect.height. The mouse Y boundary check needs to be clamped to the
visible body window instead of the full overlay. Modify the condition that
checks mouseY(msg) >= top+len(lines) to use the actual visible height (capped by
frame.bodyRect.height) instead of the total len(lines). Apply this same fix to
the similar mouse boundary check code that appears around lines 459-469.

In `@internal/tui/transcript_selection.go`:
- Around line 313-314: The guard condition that blocks transcript selection is
missing a check for the MCP add wizard state. In the condition that checks
various modal/wizard states (m.altScreen, m.height, m.setup.visible,
m.providerWizard, m.mcpManager, m.picker, m.suggestionsActive), add a check for
m.mcpAddWizard != nil to prevent transcript selection when the MCP add wizard is
open. This will prevent clicks that miss wizard items from falling through to
transcript selection and allowing row selection behind the modal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 463dbc26-54f4-41ee-a4da-3e27dde0ea82

📥 Commits

Reviewing files that changed from the base of the PR and between 1e25fde and 32e6038.

📒 Files selected for processing (3)
  • internal/tui/model.go
  • internal/tui/mouse.go
  • internal/tui/transcript_selection.go

anandh8x added 2 commits June 16, 2026 21:39
Add focused tests for the shared frame snapshot: pinned header/body/footer regions, tiny-terminal footer clipping, composer mouse hit regions, overlay centering, and transcript mouse row mapping through the body region.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui
Introduce a line-based transcript viewport adapter that preserves Zero's existing bottom-anchored scroll offset. Render slicing, chat scroll metrics, scrollChat, and transcript selection viewport mapping now share the same clamping and visible-window calculation.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui

@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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/tui/layout_test.go (1)

40-65: 💤 Low value

Consider edge case: zero or negative dimensions.

While the tiny terminal test covers height=3, consider adding tests for zero or negative dimensions to ensure the layout system handles degenerate cases gracefully without panics.

🤖 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/layout_test.go` around lines 40 - 65, The test
TestTranscriptFrameLayoutClipsFooterInTinyTerminal only validates behavior for a
tiny terminal (height=3), but does not cover edge cases with zero or negative
dimensions. Add additional test cases to verify that the layout system methods
(scrollableTranscriptFrame, pinnedTitleBar, footerView, and chatWidth) handle
degenerate inputs gracefully without panics when width or height are zero or
negative values. Ensure these edge case tests confirm the system either returns
valid results or fails in a controlled manner.
internal/tui/transcript_viewport_test.go (1)

5-46: 💤 Low value

Consider edge cases: zero and negative dimensions.

While the tests cover normal and inflated values, consider adding tests for edge cases:

  • Zero total lines (totalLines=0)
  • Zero viewport height (height=0)
  • Negative values (if not validated elsewhere)

These would ensure the viewport primitives handle degenerate inputs gracefully.

🤖 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/transcript_viewport_test.go` around lines 5 - 46, Add test
functions to cover edge cases for the transcript viewport that are not currently
tested in the existing test suite. Create new test functions following the
existing pattern (TestTranscriptViewport*) to verify behavior when totalLines is
zero, when height is zero, and potentially when negative values are passed to
newTranscriptViewport. Each test should verify that the viewport and window
calculations handle these degenerate inputs gracefully and produce sensible
results, checking properties like offset, window.start, window.end,
window.height, and window.maxOffset as appropriate for each edge case.
🤖 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.

Inline comments:
In `@internal/tui/layout_test.go`:
- Around line 32-34: The containment check in the test is incomplete because it
only validates that the top-left corner of composerRect is inside footerRect
using the contains method, but does not verify that the entire rectangle
(including its right and bottom edges) stays within the footer bounds. Fix this
by enhancing the validation condition to additionally check that the
bottom-right corner of composerRect (at coordinates x + width and y + height) is
also contained within footerRect, ensuring the entire rectangle is fully
contained within the footer.

---

Nitpick comments:
In `@internal/tui/layout_test.go`:
- Around line 40-65: The test TestTranscriptFrameLayoutClipsFooterInTinyTerminal
only validates behavior for a tiny terminal (height=3), but does not cover edge
cases with zero or negative dimensions. Add additional test cases to verify that
the layout system methods (scrollableTranscriptFrame, pinnedTitleBar,
footerView, and chatWidth) handle degenerate inputs gracefully without panics
when width or height are zero or negative values. Ensure these edge case tests
confirm the system either returns valid results or fails in a controlled manner.

In `@internal/tui/transcript_viewport_test.go`:
- Around line 5-46: Add test functions to cover edge cases for the transcript
viewport that are not currently tested in the existing test suite. Create new
test functions following the existing pattern (TestTranscriptViewport*) to
verify behavior when totalLines is zero, when height is zero, and potentially
when negative values are passed to newTranscriptViewport. Each test should
verify that the viewport and window calculations handle these degenerate inputs
gracefully and produce sensible results, checking properties like offset,
window.start, window.end, window.height, and window.maxOffset as appropriate for
each edge case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e79b6124-1e19-4f2f-8c7f-94c8e4078cce

📥 Commits

Reviewing files that changed from the base of the PR and between 32e6038 and 2a27b43.

📒 Files selected for processing (5)
  • internal/tui/layout_test.go
  • internal/tui/model.go
  • internal/tui/transcript_selection.go
  • internal/tui/transcript_viewport.go
  • internal/tui/transcript_viewport_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/tui/transcript_selection.go

Comment thread internal/tui/layout_test.go Outdated
anandh8x added 2 commits June 16, 2026 21:50
Introduce a compatibility transcript body item layer for title, empty state, separators, transcript rows, pending prompts, streaming interim content, and spec review prompts. The body still renders to the same line/selectable output, but layout now records item spans and local selectable offsets for the next visible-range step.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui

Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..."
Clamp overlay mouse hit regions to the visible transcript body and block transcript selection while the MCP add wizard is open. Strengthen layout and viewport edge-case tests, including full composer containment and degenerate dimensions.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui

Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..."

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@internal/tui/mouse_test.go`:
- Around line 489-505: The test
TestMCPAddWizardBlocksTranscriptSelectionBehindOverlay uses x=0 in the
testMouseClick call, which may miss the transcript text hitboxes entirely
regardless of overlay state, causing the test to pass vacuously without actually
proving the overlay blocks selection. Replace the x=0 coordinate parameter in
the testMouseClick call with textStart, which represents a known selectable
transcript text column, to ensure the click targets transcript text that would
be selectable if not for the overlay blocking it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ba210d5a-a894-4bac-83ec-b86ee1ef4832

📥 Commits

Reviewing files that changed from the base of the PR and between fe8d07a and f3dfbfb.

📒 Files selected for processing (5)
  • internal/tui/layout_test.go
  • internal/tui/mouse.go
  • internal/tui/mouse_test.go
  • internal/tui/transcript_selection.go
  • internal/tui/transcript_viewport_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/tui/transcript_selection.go
  • internal/tui/mouse.go

Comment thread internal/tui/mouse_test.go

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Strong, well-structured hardening pass. The tuiRect geometry + region rects (header / body / footer / composer / status) replace a lot of ad-hoc offset math with one model, and the coverage — degenerate 0/negative dims, tiny-terminal clipping, footer clip, overlay clamping, frame-body click mapping — is exactly what this kind of layout code needs.

Highlights:

  • mouseOverComposer collapsing to frame.composerRect.contains(x, y) is a big readability win over the old manual clip math.
  • overlayMouseRect clamping the overlay to the visible body height (with the test that a click below the visible rows misses) fixes a real class of off-screen-overlay hit bugs.
  • layoutTranscriptBodyItems with typed spans (empty / row / separator / pending-interim) and selectable lines shifted by item start is a much cleaner transcript-body model than threading bodyY through the render funcs.

A few things:

  • The coordination footprint is large. This rewrites the exact geometry/mouse surface (scrollableTranscriptFrame, overlayMouseTopoverlayMouseRect, transcriptViewportStart, mouseOverComposer, transcriptLineAtMouse) that several in-flight changes also touch — #224 and a couple of other TUI changes (a clickable/selectable permission popup, and a right-docked panel that reserves horizontal space from the chat area). Whatever lands first forces a real rebase on the rest, so it's worth sequencing this deliberately. One concrete hook: anything that reserves width from the chat (a side panel) will want the frame rects' width to come from a shared "content width" helper rather than chatWidth(m.width) directly — a natural follow-up on this geometry.
  • Since this is the draft foundation with follow-ups stacking onto the branch, keeping each stacked slice independently green (build + the touched tests) will make the eventual review of the stack much easier.
  • Minor: mouseOverlayHit now returns x — is a current caller using it, or is it staged for the follow-ups? If unused for now, a one-line note saves a future "why is this here?".

Solid base, and nicely tested. 👍

CodeRabbit caught that the MCP add wizard blocking test clicked at x=0, which could miss selectable transcript text and pass without proving the overlay guard. This updates the test helper to return both textStart and y, then clicks that known selectable point before asserting the wizard blocks transcript selection behind the modal.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui

Tested: git diff --check

Tested: GOCACHE=/tmp/zero-go-cache go test ./...
anandh8x added 2 commits June 16, 2026 22:25
Thread the structured transcriptBodyLayout through alt-screen rendering, scroll metrics, and transcript mouse lookup instead of repeatedly joining and splitting the rendered body string. This preserves the current line-based viewport behavior while making the body layout the source of truth for visible windows and selectable line lookup.

This is an incremental step toward visible-item rendering and item-level caching; it does not change the transcript visual output or input flow.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui

Tested: git diff --check

Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..."
Selectable user and assistant rows need selectable metadata for mouse interactions, but when no transcript selection is active their rendered strings are identical to the normal row render path. Route those rendered strings through renderRow so stable rows reuse the existing row cache while selection-active frames still render highlight-aware output.

This keeps transcript behavior unchanged and adds a regression test that verifies selectable assistant rows produce stable metadata while hitting the render cache on the second render.

Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui

Tested: git diff --check

Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..."
@anandh8x
anandh8x marked this pull request as ready for review June 16, 2026 17:56
@github-actions

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: 0f2307f86452
Changed files (9): internal/tui/layout_test.go, internal/tui/model.go, internal/tui/mouse.go, internal/tui/mouse_test.go, internal/tui/render_cache_test.go, internal/tui/transcript_body_test.go, internal/tui/transcript_selection.go, internal/tui/transcript_viewport.go, internal/tui/transcript_viewport_test.go

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

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Approve — clean hardening pass with addressed review findings

This is a strong refactor that replaces a lot of ad-hoc Y-offset math with a typed, region-based geometry model. The two open CodeRabbit findings are already fixed in the current 0f2307f tip; @gnanam1990's human review was positive with one operational note and one unused-field note. No blockers.

What's good

  • tuiRect + transcriptFrameLayoutheaderRect, bodyRect, footerRect, composerRect, statusRect are explicit rectangles with contains/local helpers. The frame computation in scrollableTranscriptFrame (model.go:1345-1392) lays them all out and the assertions in layout_test.go lock the invariant (no gaps, footer bottom == terminal height, status is the last visible footer line).
  • mouseOverComposer collapsed to one line — the old code did its own footer clip + top + mouseY math; the new code is frame.composerRect.contains(mouseX(msg), mouseY(msg)). Big readability win.
  • overlayMouseRect clamps to visible bodymouse.go:464-471 clamps the overlay to frame.bodyRect.height, so a click below the visible rows correctly misses. Fixes a real off-screen-overlay hit bug class.
  • transcriptBodyLayout is the right shapelines + selectable + typed spans (title/empty/separator/row/pending-prompt/pending-interim/spec-review). The body is no longer a string threaded with bodyY offsets; it's a structured layout the viewport can window over (transcript_viewport.go) and the mouse can hit-test against.
  • transcriptViewport is a pure value typetranscript_viewport.go is 57 LoC, no model dependency, table-driven tests for zero/negative inputs and clamp behavior. Easy to reason about.
  • CodeRabbit #1 already fixedlayout_test.go:34 now contains the full composer-rect containment check (top-left AND bottom-right corners inside footerRect). The "Addressed in commit f3dfbfb" stamp on the inline comment confirms it.
  • CodeRabbit #2 already fixedmouse_test.go:491 now uses textX, textY := firstTranscriptTextMousePoint(t, m) (line 562), which returns the actual textStart column, not x=0. The new test TestMCPAddWizardBlocksTranscriptSelectionBehindOverlay clicks at the real selectable position.
  • Test coverage is stronglayout_test.go (185 LoC, 4 tests), transcript_viewport_test.go (85 LoC, 4 tests including a table-driven edge case pass), transcript_body_test.go (86 LoC, 3 tests), render_cache_test.go (20 LoC, 1 new test). Each commit on the branch added 30-100 LoC of tests.

Non-blocking notes (skip if you want)

  1. mouseOverlayHit.x is added but unused (mouse.go:5, set at line 452). The struct is constructed and returned, but every caller reads only hit.y (verified: selectSuggestionAtMouse, selectMCPManagerAtMouse, selectMCPAddWizardAtMouse, selectModelPickerAtMouse, selectProviderWizardAtMouse, etc.). @gnanam1990 flagged this; agreed. Two options:

    • Drop the field now (1-line change in the struct + 1-line change at the return site). Keeps the surface area tight.
    • Leave it with a one-line // Reserved for the clickable permission popup / right-docked panel follow-ups comment** so the next reader knows it's staged, not stale.

    I lean toward option 2 since the field's name and return-site comment in the diff suggest it's deliberate staging. If you don't have a follow-up PR that uses it, drop it. Otherwise comment it.

  2. PR coordination footgun (raised by @gnanam1990) — this PR rewrites the exact geometry/mouse surface that #224 (resume picker), a planned clickable permission popup, and a planned right-docked side panel all touch. The right-docked panel is the one that would change chatWidth(m.width) — anything that reserves width from the chat will want the frame rects' width to come from a shared "content width" helper, not chatWidth(m.width) directly. If the side-panel PR is in flight, this PR should land first so the side panel stacks on top of the rect-based model. If the panel PR lands first, the rebase will be mechanical but tedious. Worth a quick sync with whoever's working on the panel.

  3. Inlined chatWidth(m.width) in the frame construction (model.go:1380) and overlayMouseRect (mouse.go:471). A future side-panel refactor that takes a content-width helper will want to thread it through here. Not a blocker for this PR; just worth a // TODO: thread content width here when the right-docked panel lands for the next reader.

CI

All 6 smoke checks green (ubuntu / macos / windows / performance / security / CodeRabbit). "Zero Review" re-ran and is IN_PROGRESS as of this review; expect it to clear. Local ./internal/tui/... is green except for the pre-existing env-fragile TestModelCommandPersistsSelectedModelToUserConfig from PR #220 (fails only in dev envs with ANTHROPIC_* set, not this PR's fault).

Recommended follow-ups (separate PRs, NOT this one)

  • The clickable permission popup and the right-docked side panel that @gnanam1990 mentions. Both benefit from the rect-based model this PR introduces; both can land as independent stacks on this branch as the PR description suggests.
  • A TestTranscriptFrameLayoutHandlesTransitions test (tiny terminal ↔ full terminal) to lock in the clip math. The current tests cover tiny and full separately; an explicit transition test would be one more tripwire.

Solid foundation for the TUI backend hardening sequence. ✅

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.

3 participants