perf(chat): hidden panes stop polling, rows stop re-rendering, a covered terminal stops drawing

The Workspace keeps panes of other workspaces and tabs mounted (display:none),
and every one of them kept following the live stream as if shown.

- useOnScreen (IntersectionObserver): ConversationView, the outline and the
  700 ms menu scan skip while their pane is hidden and catch up once on show.
- A new_event refetches the tail at most every 1.5 s (leading + trailing).
- MessageList: WorkGroup/MessageRow/EditedFiles compare only their own blocks
  and results, so a tail append no longer re-renders every row.
- A terminal under the chat is moved off the viewport with a transform, so
  xterm pauses drawing without a resize.
- PaneChat and TerminalView are memoized; LaneConsolePane passes a stable onExit.
- RunningStep's clock and the outline poll skip unchanged or hidden updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 11:26:57 +07:00
parent 0c2f49876d
commit c3f83d3922
9 changed files with 337 additions and 39 deletions
+22 -3
View File
@@ -42,6 +42,7 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import {
memo,
useEffect,
useId,
useMemo,
@@ -153,9 +154,22 @@ interface TerminalViewProps {
/** Take the keyboard when it appears (default). Off for a split pane that
* is not the focused one, or keys would land in whichever mounted last. */
autoFocus?: boolean;
/** Another layer (the pane's chat) covers it. xterm stops drawing only when
* its IntersectionObserver says off screen, and a covered terminal is not —
* so it is moved out of the viewport with a transform, which keeps its
* layout size (no PTY resize; the chat still reads and answers its menus). */
covered?: boolean;
}
export function TerminalView({ runId, wsBaseUrl, onExit, autoFocus = true }: TerminalViewProps) {
/** Memoized: the Workspace re-renders its pane tree on every lane_update, and
* a terminal only cares about its own props (its owner keeps `onExit` stable). */
export const TerminalView = memo(function TerminalView({
runId,
wsBaseUrl,
onExit,
autoFocus = true,
covered = false,
}: TerminalViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
// Read through a ref: an inline `onExit` from the parent changes identity on
// every render, and as an effect dep it would tear the terminal down and
@@ -709,7 +723,12 @@ export function TerminalView({ runId, wsBaseUrl, onExit, autoFocus = true }: Ter
onPasteCapture={onPaste}
onFocus={() => notePaneFocus(runId)}
>
<div ref={containerRef} className="h-full w-full" data-testid="terminal-view" />
<div
ref={containerRef}
className="h-full w-full"
style={covered ? { transform: "translateX(-300vw)" } : undefined}
data-testid="terminal-view"
/>
<p id={hintId} className="sr-only">
{t("terminal.keyboardHint")}
</p>
@@ -810,4 +829,4 @@ export function TerminalView({ runId, wsBaseUrl, onExit, autoFocus = true }: Ter
)}
</div>
);
}
});