Skip to content

feat: paginated terminal replay with progressive background hydration#258

Merged
mattleaverton merged 7 commits intodanshapiro:mainfrom
mattleaverton:investigate/terminal-replay-on-hydration
Mar 31, 2026
Merged

feat: paginated terminal replay with progressive background hydration#258
mattleaverton merged 7 commits intodanshapiro:mainfrom
mattleaverton:investigate/terminal-replay-on-hydration

Conversation

@mattleaverton
Copy link
Copy Markdown
Collaborator

Summary

  • Adds maxReplayBytes to the terminal.attach protocol — server sends only the tail frames that fit within the byte budget, using the existing gap mechanism for truncated frames
  • Visible tabs request truncated replay (128KB tail) on hydration, eliminating the fast-scroll replay on tab switch / page refresh
  • "Load more history" button appears when history was truncated; clicking it triggers a full replay (user opts into the scroll)
  • Background tabs progressively hydrate with full history (one at a time, neighbor-first) so they're ready when visited
  • Only shows "Load more" for byte-budget truncation, not for replay ring overflow where data is already gone

Test plan

  • Refresh page with multiple tabs — active tab loads instantly, no visible scroll replay
  • Tab with large history shows "Load more history" button at top
  • Clicking the button loads full history
  • Switching to an un-hydrated tab shows truncated recent content instantly
  • Background tabs progressively hydrate after active tab completes
  • Existing terminal attach/reconnect flows still work
  • All 2843 tests pass (2 pre-existing WSL failures on macOS unrelated)

🤖 Generated with Claude Code

mattleaverton and others added 6 commits March 30, 2026 17:08
Root cause analysis of the visible fast-scroll replay on tab switch,
plus design for server-side truncated replay, progressive background
hydration, and "load more history" UI affordance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-side support for byte-budgeted replay. When maxReplayBytes is
set on a terminal.attach message, the broker takes only the tail frames
from the replay ring that fit within the budget. Truncated frames are
reported via the existing terminal.output.gap mechanism.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Visible tabs now request truncated replay (128KB tail) instead of the
full replay ring. Gap from truncation is stored for the "load more
history" UI rather than printed as a generic gap message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows a button at the top of the terminal when history was truncated
due to maxReplayBytes. Clicking it triggers a full viewport_hydrate
to load the complete history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After the active tab completes hydration, background tabs are
progressively hydrated one at a time in neighbor-first order.
Hidden tabs register with a global hydration queue that triggers
full replay in the background (invisible since CSS-hidden).

When the user switches to an un-hydrated tab, it gets a truncated
fast attach (128KB tail). Already-hydrated tabs appear instantly
with full history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The button was incorrectly shown for replay ring overflow gaps where
the data no longer exists. Now it only appears when the attach used
maxReplayBytes, meaning the data is still in the ring and a full
re-attach will actually load more.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d048d9336

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const connectionStatus = useAppSelector((s) => s.connection.status)
const tab = useAppSelector((s) => s.tabs.tabs.find((t) => t.id === tabId))
const activeTabId = useAppSelector((s) => s.tabs.activeTabId)
const tabOrder = useAppSelector((s) => s.tabs.tabs.map((t) => t.id))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Memoize tab-order selector to avoid global rerender churn

Selecting s.tabs.tabs.map((t) => t.id) directly creates a new array on every Redux update, so TerminalView re-renders even when tab order is unchanged. In this component that processes terminal I/O and layout work, that turns unrelated store actions into repeated heavy renders across panes, which can degrade terminal responsiveness under normal activity. Please derive this with a memoized selector (or shallowEqual) so updates only fire when tab order actually changes.

Useful? React with 👍 / 👎.

Comment on lines +1750 to +1752
const isTruncatedReplay = currentAttach?.usedMaxReplayBytes
&& msg.reason === 'replay_window_exceeded'
&& seqStateRef.current.pendingReplay
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate load-more banner on true truncation, not all replay misses

This condition treats any attach-time replay_window_exceeded gap as truncation whenever maxReplayBytes was requested, but the same gap reason is also used when the replay ring has already overflowed. In long-lived terminals with evicted history, this will show “Load more history” even though the earliest range is unrecoverable, and the initial attach suppresses the explicit gap warning text. Add a discriminator (e.g., server flag or sequence check) so the banner appears only for recoverable byte-budget truncation.

Useful? React with 👍 / 👎.

…rder

- Adds 'replay_budget_exceeded' gap reason distinct from 'replay_window_exceeded'
  so the client can reliably distinguish recoverable truncation from ring overflow
- Client checks msg.reason === 'replay_budget_exceeded' instead of inferring
- Adds shallowEqual to tabOrder selector to prevent unnecessary rerenders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mattleaverton mattleaverton merged commit 6d30df4 into danshapiro:main Mar 31, 2026
1 check 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.

1 participant