Fix terminal snapshots reflowing after resize

Terminal snapshots now carry soft-wrap row metadata only to clients that advertise support, so restored output can resize like live output without breaking older clients.
This commit is contained in:
Mohamed Boudra
2026-05-31 19:40:15 +07:00
parent 794cbcef63
commit 4cc72a84e8
18 changed files with 538 additions and 146 deletions
+45
View File
@@ -40,6 +40,10 @@ export interface TerminalStateSnapshot {
export interface TerminalStateSnapshotOptions {
scrollbackLines?: number;
// Include per-row soft-wrap flags (gridWrapped/scrollbackWrapped) so the client
// can reflow restored content on resize. Gated on a client capability, so old
// clients never receive the extra fields.
includeWrapFlags?: boolean;
}
export interface TerminalSubscribeOptions {
@@ -359,6 +363,39 @@ function extractScrollback(
return scrollback;
}
// xterm marks a line `isWrapped` when it is a continuation of the PREVIOUS line.
// The snapshot carries the inverse, tmux-style flag — "this row continues onto the
// next row" — so the client can rejoin and reflow logical lines. So row y's flag is
// whether line y+1 is a wrapped continuation.
function lineContinuesToNext(terminal: TerminalType, absoluteRow: number): boolean {
return terminal.buffer.active.getLine(absoluteRow + 1)?.isWrapped === true;
}
function extractGridWrapped(terminal: TerminalType): boolean[] {
const baseY = terminal.buffer.active.baseY;
const wrapped: boolean[] = [];
for (let row = 0; row < terminal.rows; row++) {
wrapped.push(lineContinuesToNext(terminal, baseY + row));
}
return wrapped;
}
function extractScrollbackWrapped(
terminal: TerminalType,
options?: { scrollbackLines?: number },
): boolean[] {
const scrollbackLines = terminal.buffer.active.baseY;
const startRow =
typeof options?.scrollbackLines === "number"
? Math.max(0, scrollbackLines - options.scrollbackLines)
: 0;
const wrapped: boolean[] = [];
for (let row = startRow; row < scrollbackLines; row++) {
wrapped.push(lineContinuesToNext(terminal, row));
}
return wrapped;
}
function extractCursorState(terminal: TerminalType): TerminalState["cursor"] {
const coreService = (terminal as unknown as { _core?: { coreService?: Record<string, unknown> } })
._core?.coreService as
@@ -853,6 +890,14 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
}),
cursor: extractCursorState(terminal),
...(title ? { title } : {}),
...(snapshotOptions?.includeWrapFlags
? {
gridWrapped: extractGridWrapped(terminal),
scrollbackWrapped: extractScrollbackWrapped(terminal, {
scrollbackLines: snapshotOptions?.scrollbackLines,
}),
}
: {}),
};
}