feat: paginated terminal replay with progressive background hydration#258
Conversation
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>
There was a problem hiding this comment.
💡 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".
src/components/TerminalView.tsx
Outdated
| 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)) |
There was a problem hiding this comment.
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 👍 / 👎.
src/components/TerminalView.tsx
Outdated
| const isTruncatedReplay = currentAttach?.usedMaxReplayBytes | ||
| && msg.reason === 'replay_window_exceeded' | ||
| && seqStateRef.current.pendingReplay |
There was a problem hiding this comment.
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>
Summary
maxReplayBytesto theterminal.attachprotocol — server sends only the tail frames that fit within the byte budget, using the existing gap mechanism for truncated framesTest plan
🤖 Generated with Claude Code