fix: stop terminal open/switch from rewriting state per output chunk (#2536)
Opening a terminal rebuilt the WASM terminal twice and rewrote the persisted session snapshot on every streamed output chunk, so the cost grew with each open terminal and could crash under load. - Move PTY scrollback to a standalone buffers map keyed by directory and tab id; output no longer touches sessions, so the tab strip, the project-action monitor and the persist projection stay referentially stable while chunks stream. - Memoize partialize and add a dedup storage adapter that skips writes when the persisted projection is unchanged. - Reuse the idle terminal WebSocket across tab switches (15s grace) instead of re-authenticating and replaying the snapshot on every attach. - Key the viewport by directory + tab only so createSession no longer tears down and rebuilds the Ghostty terminal. - Reset the terminal in place on replay discontinuities instead of bumping the renderer generation. - Scan the chunk array from the end (O(1) per write) instead of findIndex. Adds regression tests for the buffer map, socket reuse and viewport key, and documents the store invariants.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useTerminalStore } from '@/stores/useTerminalStore';
|
||||
import { EMPTY_TERMINAL_BUFFER, useTerminalStore } from '@/stores/useTerminalStore';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { type TerminalStreamEvent } from '@/lib/api/types';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
@@ -97,7 +97,10 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
|
||||
|
||||
const terminalSessionId = activeTab?.terminalSessionId ?? null;
|
||||
const terminalLifecycle = activeTab?.lifecycle ?? 'idle';
|
||||
const bufferChunks = activeTab?.bufferChunks ?? [];
|
||||
// Scrollback is a leaf subscription: streaming output must not rerender the tab strip.
|
||||
const bufferChunks = useTerminalStore((s) => (
|
||||
effectiveDirectory && activeTabId ? s.getBuffer(effectiveDirectory, activeTabId).chunks : EMPTY_TERMINAL_BUFFER.chunks
|
||||
));
|
||||
const isConnecting = activeTab?.isConnecting ?? false;
|
||||
const previewUrl = activeTab?.previewUrl ?? null;
|
||||
|
||||
@@ -424,7 +427,8 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
|
||||
let terminalId = tab?.terminalSessionId ?? null;
|
||||
const terminalLifecycle = tab?.lifecycle ?? 'idle';
|
||||
const isActionTab = Boolean(tab?.label?.startsWith('Action:'));
|
||||
const hasBufferedOutput = (tab?.bufferLength ?? 0) > 0 || (tab?.bufferChunks?.length ?? 0) > 0;
|
||||
const buffer = useTerminalStore.getState().getBuffer(directory, tabId);
|
||||
const hasBufferedOutput = buffer.byteLength > 0 || buffer.chunks.length > 0;
|
||||
|
||||
if (!terminalId) {
|
||||
if (terminalLifecycle === 'exited') {
|
||||
@@ -793,12 +797,15 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
|
||||
|
||||
const xtermTheme = React.useMemo(() => convertThemeToXterm(currentTheme), [currentTheme]);
|
||||
|
||||
// Viewport identity is the tab, not the PTY session. Including the session id
|
||||
// here tore down and rebuilt the Ghostty terminal (WASM VT + canvas + font
|
||||
// atlas) a second time the moment `createSession` resolved, doubling the cost
|
||||
// of every terminal open. Session changes are handled by the chunk replay path.
|
||||
const terminalViewportKey = React.useMemo(() => {
|
||||
const directoryPart = effectiveDirectory ?? 'no-dir';
|
||||
const tabPart = activeTabId ?? 'no-tab';
|
||||
const terminalPart = terminalSessionId ?? 'no-terminal';
|
||||
return `${directoryPart}::${tabPart}::${terminalPart}`;
|
||||
}, [effectiveDirectory, activeTabId, terminalSessionId]);
|
||||
return `${directoryPart}::${tabPart}`;
|
||||
}, [effectiveDirectory, activeTabId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isTerminalVisible || useTouchTerminalInput) {
|
||||
|
||||
Reference in New Issue
Block a user