Skip to content

fix(tui): cache settled alt-screen transcript - #647

Merged
kevincodex1 merged 4 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-561-transcript-render-cache
Jul 20, 2026
Merged

fix(tui): cache settled alt-screen transcript#647
kevincodex1 merged 4 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-561-transcript-render-cache

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • advance the settled transcript frontier in alt-screen mode without emitting tea.Println
  • cache settled transcript body items so fullscreen history remains scrollable without rebuilding it every frame
  • build row context from only the live tail and preserve dynamic hover, selection, resize, and expand/collapse behavior
  • add regression coverage and a 5,000-turn benchmark for the settled alt-screen path

Root cause

Alt-screen mode returned from settleTranscript before advancing m.flushed. Since the shipped TUI always uses alt-screen mode, every redraw rebuilt transcript body descriptors and cross-row context for the complete conversation.

Impact

Settled alt-screen frames now reuse cached body descriptors while the live tail remains dynamic. On the issue benchmark, a settled 5,000-turn transcript takes about 110 ns/op with 0 B/op and 0 allocations/op on Windows/amd64.

Validation

  • focused frontier, transcript selection, hover, and rendering tests
  • go test ./... -run '^$'
  • go test ./internal/tui -run '^$' -bench '^BenchmarkIssue561SettledAltScreen$' -benchmem -benchtime=100x

Closes #561

Summary by CodeRabbit

  • Bug Fixes
    • Improved alternate-screen transcript behavior: settled content remains visible while the flush frontier advances correctly, and selection/hover rendering stays consistent.
    • Fixed stale in-place status updates (doctor/sandbox/compact spinners) by invalidating cached settled layouts when those rows change before the flush boundary.
    • Updated transcript row and file-selection changes to properly invalidate and rebuild alternate-screen settled caches (including clearing selection).
  • Performance
    • Reduced unnecessary rebuilding by caching alternate-screen settled transcript items and reusing them when frontier/width (or affected content) hasn’t changed.

@PierrunoYT
PierrunoYT marked this pull request as ready for review July 10, 2026 16:59
Copilot AI review requested due to automatic review settings July 10, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes the alt-screen flush frontier behavior and introduces a settled-prefix cache so Bubble Tea’s fullscreen (alt-screen) TUI no longer rebuilds transcript body descriptors and cross-row context for the entire history on every redraw. It advances m.flushed in alt-screen mode (while still skipping tea.Println) and reuses cached settled transcript items while keeping the live tail dynamic for hover/selection and streaming state.

Changes:

  • Advance the flush frontier in alt-screen mode and cache the settled transcript prefix as []transcriptBodyItem to avoid per-frame O(history) rebuilds.
  • Build rowContext only from the unflushed tail and preserve spacing context via flushedPreviousKind / flushedHavePreviousKind.
  • Add regression tests and a benchmark for the settled alt-screen path, plus a small allocation avoidance in buildRowContext for the frontier-at-tail steady state.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/tui/transcript_selection.go Adds settled-item reuse path for alt-screen, plus interaction state plumbing so cached item render closures reflect current hover/selection without rebuilding descriptors.
internal/tui/transcript_issue561_bench_test.go Adds a regression benchmark to measure settled alt-screen transcript body item construction at large transcript sizes.
internal/tui/rendering.go Avoids per-frame map allocations by returning an empty rowContext when called with an empty tail slice.
internal/tui/model.go Extends model state to track flush spacing context and store alt-screen settled cache metadata and shared interaction pointer.
internal/tui/flush.go Removes the alt-screen early return; advances the frontier in alt-screen while skipping printing, caches settled prefix items, and narrows buildRowContext to the tail.
internal/tui/flush_test.go Updates alt-screen flush behavior expectations and adds a regression test ensuring settled cache rebuild after expand/collapse toggles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai

coderabbitai Bot commented Jul 10, 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

Run ID: 7eaca97e-ab32-4863-aec8-538d1d81c6d7

📥 Commits

Reviewing files that changed from the base of the PR and between 5008f7d and 6ac1edd.

📒 Files selected for processing (5)
  • internal/tui/files_panel.go
  • internal/tui/flush_test.go
  • internal/tui/model.go
  • internal/tui/transcript_issue561_bench_test.go
  • internal/tui/transcript_selection.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/tui/transcript_issue561_bench_test.go
  • internal/tui/transcript_selection.go

Walkthrough

Alt-screen transcript settlement now advances the flush frontier, skips native scrollback emission, and caches settled body items. Rendering reuses cached prefixes and incremental context, with invalidation for settled-row changes. Tests and a benchmark cover these paths.

Changes

Alt-screen transcript rendering

Layer / File(s) Summary
Frontier advancement and settled cache
internal/tui/model.go, internal/tui/flush.go
Alt-screen settlement advances frontier bookkeeping, avoids tea.Println, and caches settled transcript items by width and frontier.
Incremental rendering and cache invalidation
internal/tui/transcript_selection.go, internal/tui/rendering.go, internal/tui/files_panel.go, internal/tui/command_center.go, internal/tui/sandbox_setup_command.go, internal/tui/session_controls.go
Rendering shares selection and hover state, processes the relevant transcript tail, reuses settled items, and invalidates cached rows when settled content, status, selection, or expansion changes.
Alt-screen behavior coverage
internal/tui/flush_test.go, internal/tui/transcript_issue561_bench_test.go
Tests verify frontier advancement and cache rebuilding across transcript updates; a benchmark measures settled alt-screen rendering.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Model
  participant settleTranscript
  participant rebuildAltScreenSettledItems
  participant transcriptBodyItems
  participant AltScreenView
  Model->>settleTranscript: settle transcript rows
  settleTranscript->>settleTranscript: advance flushed frontier
  settleTranscript->>rebuildAltScreenSettledItems: rebuild settled prefix
  rebuildAltScreenSettledItems->>transcriptBodyItems: build cached body items
  AltScreenView->>transcriptBodyItems: render transcript
  transcriptBodyItems-->>AltScreenView: reuse settled items and build tail
Loading

Possibly related PRs

  • Gitlawb/zero#158: Directly related alt-screen settled-row and flush-frontier changes.
  • Gitlawb/zero#328: Related transcript selection rendering changes involving finalizeTranscriptBodyRow.

Suggested reviewers: gnanam1990, vasanthdev2004, anandh8x

🚥 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 accurately summarizes the main change: caching settled alt-screen transcript rendering.
Linked Issues check ✅ Passed The PR advances the alt-screen frontier, processes only the unsettled tail, and adds cache/invalidation behavior requested by #561.
Out of Scope Changes check ✅ Passed The added cache invalidation, tests, and benchmark are directly tied to preserving settled alt-screen rendering behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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 (1)
internal/tui/transcript_issue561_bench_test.go (1)

21-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: guard against a silent cache miss so a future regression doesn't quietly turn this into an O(history) benchmark. A cheap pre-check (e.g. assert m.altScreenSettledFrontier == m.flushed && m.altScreenSettledWidth == width before the loop) keeps the benchmark measuring the intended fast path.

🤖 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_issue561_bench_test.go` around lines 21 - 29, Ensure
BenchmarkIssue561SettledAltScreen verifies the settled cache state before
timing: assert that m.altScreenSettledFrontier equals m.flushed and
m.altScreenSettledWidth equals width after setup and before the benchmark loop,
so cache misses cannot silently change the benchmark to O(history).
🤖 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/tui/transcript_issue561_bench_test.go`:
- Around line 21-29: Ensure BenchmarkIssue561SettledAltScreen verifies the
settled cache state before timing: assert that m.altScreenSettledFrontier equals
m.flushed and m.altScreenSettledWidth equals width after setup and before the
benchmark loop, so cache misses cannot silently change the benchmark to
O(history).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7bb8825d-3566-40bc-8e2b-74ddc6b163df

📥 Commits

Reviewing files that changed from the base of the PR and between 1af5882 and ca44988.

📒 Files selected for processing (6)
  • internal/tui/flush.go
  • internal/tui/flush_test.go
  • internal/tui/model.go
  • internal/tui/rendering.go
  • internal/tui/transcript_issue561_bench_test.go
  • internal/tui/transcript_selection.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 10, 2026

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

I built and vetted this and read all six changed files, and the caching design is sound invalidation covers new turns, resize, and expand/collapse of settled rows, and the steady-state fast path correctly bypasses the cache whenever a row is still live. The benchmark matches the stated goal.

But I found a regression that I think has to be fixed before this lands: in alt-screen mode the doctor connectivity spinner freezes.

setDoctorStatusRow (command_center.go:212-222) updates an existing status row in place with m.transcript[i] = row and returns. A rowSystem row is immediately settleable (flush.go:61-64), so once the frontier advances past the doctor row it gets snapshotted into altScreenSettledItems. On the next spinner tick (model.go:1986) doctorFrame changes and setDoctorStatusRow writes new text into m.transcript[i] but it never zeroes altScreenSettledWidth, and the rebuild guard in settleTranscript (flush.go:128) only fires on a frontier/width change. With none of those changing on an in-place text update, the rebuild is skipped and the View fast path (transcript_selection.go:249) serves the stale snapshot. I reproduced it: advancing doctorFrame 0 → 2 still renders the frame-0 glyph. The same repro passes on origin/main, where alt-screen re-rendered the tail every frame.

Fix is small: have setDoctorStatusRow invalidate the settled cache when it updates an existing row (zero altScreenSettledWidth on the in-place path, same as toggleTranscriptRow does). Worth checking other in-place mutators of already-settled rows for the same gap while you're there.

Everything else I'd merge as-is. Requesting changes just for this.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@gnanam1990 could you take a look here? I requested changes for a regression: in alt-screen mode the doctor connectivity spinner freezes, because setDoctorStatusRow updates a settled row in place without invalidating the settled-prefix cache, so the View fast path serves a stale snapshot. Would be good to have you confirm (or push back) before it's reworked.

…updates

setDoctorStatusRow, setSandboxSetupStatusRow, and setCompactStatusRow all
mutate an already-flushed transcript row in place (m.transcript[i] = row)
without invalidating the alt-screen settled cache. Since none of frontier,
width, or row count change on such an update, settleTranscript's rebuild
guard never fires and the View fast path serves the stale cached snapshot -
e.g. the doctor connectivity spinner freezes on its first frame.

Zero altScreenSettledWidth on the in-place path, mirroring the existing
pattern in toggleTranscriptRow, so the next settle rebuilds the cache.

Addresses PR Gitlawb#647 review feedback from Vasanthdev2004.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 17, 2026

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

The new commit closes the doctor/sandbox/compact spinner gap I flagged earlier, and all four in-place row mutators are covered now. But the invalidation surface is still incomplete — two more paths change settled-row rendering without advancing the frontier, so the cached prefix goes stale. Both are regressions: before this PR, alt-screen re-rendered the whole transcript every frame, so these were always fresh.

First, the FILES selection tint. selectFile (files_panel.go:354) sets m.selectedFile and scrolls to the edit card, but doesn't zero altScreenSettledWidth; the accent border is read from selectedFile (rendering.go:1412 via rowTouchesSelectedFile) and baked into the cached descriptor at rebuild time, so the first click on a FILES row (transcript_selection.go:1394) scrolls to the card but its border doesn't highlight until a resize or new turn. The Esc-clear at model.go:1373 has the same gap in reverse (a cached accent border won't clear). Fix: invalidate the cache in those paths, same if i < m.flushed { m.altScreenSettledWidth = 0 } style as the status-row setters.

Second, collapseRepeatedStatusCard (model.go:2440) drops two rows from the middle of the settled prefix without adjusting m.flushed or invalidating. After a repeated identical swarm_status, the dropped pair was already settled (frontier had stopped at the live new-call), so m.flushed still equals the new transcript length, the fast path fires, and the cache serves the old call+result cards while hiding the new ones — the collapse is defeated. Fix: when the collapse removes rows, decrement m.flushed by the count removed and zero altScreenSettledWidth.

The caching design and the 0-alloc benchmark are solid; it's just the invalidation surface that needs these two paths closed. Requesting changes for those.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

@Vasanthdev2004 The two remaining cache invalidation issues are fixed in f0f80b6, with regression coverage for FILES selection clearing and repeated status-card collapse. Could you please re-review when you have a chance?

Vasanthdev2004
Vasanthdev2004 previously approved these changes Jul 18, 2026

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

Approving — both invalidation gaps are closed. The setSelectedFile helper covers select, the Esc-clear, and the mouse path in one place, and rewinding m.flushed by the removed row count in the collapse path is exactly the fix I had in mind. The two regression tests plus the cache-miss guard in the benchmark are a nice touch.

One non-blocking note for a possible follow-up: setSelectedFile keys the invalidation on lastRowIndexForFile, but rowTouchesSelectedFile tints every settled result row that touched the file — so if a file's most recent edit card is still in the live tail while an older card for the same file is already settled, the older card's tint can go stale until the next rebuild. It's narrow and cosmetic (self-heals on the next frontier advance or resize), so I'm not holding the PR on it.

Thanks for the quick turnarounds on both rounds.

@kevincodex1 kevincodex1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@kevincodex1
kevincodex1 merged commit d74ceb1 into Gitlawb:main Jul 20, 2026
7 checks passed
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.

perf(tui): transcript re-renders full history every frame — flush frontier never advances in alt-screen

4 participants