fix(terminal): preserve visible replay history defaults

This commit is contained in:
Mohamed Boudra
2026-10-30 18:43:42 +01:00
parent 7bfadca298
commit 3d3b5343a2
4 changed files with 69 additions and 24 deletions
@@ -13,15 +13,24 @@ import {
describe("terminal restore policy", () => {
test("resolves bounded restore snapshot options", () => {
expect(resolveTerminalRestoreSnapshotOptions({ mode: "live" })).toBeNull();
expect(resolveTerminalRestoreSnapshotOptions({ mode: "full-snapshot" })).toBeUndefined();
expect(resolveTerminalRestoreSnapshotOptions({ mode: "visible-snapshot" })).toEqual({
expect(
resolveTerminalRestoreSnapshotOptions({ restore: { mode: "live" }, chunked: false }),
).toBeNull();
expect(
resolveTerminalRestoreSnapshotOptions({ restore: { mode: "full-snapshot" }, chunked: false }),
).toBeUndefined();
expect(
resolveTerminalRestoreSnapshotOptions({
restore: { mode: "visible-snapshot" },
chunked: false,
}),
).toEqual({
scrollbackLines: 200,
});
expect(
resolveTerminalRestoreSnapshotOptions({
mode: "visible-snapshot",
scrollbackLines: 999,
restore: { mode: "visible-snapshot", scrollbackLines: 999 },
chunked: false,
}),
).toEqual({ scrollbackLines: 500 });
});
@@ -33,18 +33,25 @@ export function resolveRestoreAfterOutputOverflow(
};
}
export function resolveTerminalRestoreSnapshotOptions(
restore: TerminalRestoreOptions,
): TerminalReplayOptions | null | undefined {
export function resolveTerminalRestoreSnapshotOptions({
restore,
chunked,
}: {
restore: TerminalRestoreOptions;
chunked: boolean;
}): TerminalReplayOptions | null | undefined {
if (restore.mode === "live") {
return null;
}
if (restore.mode === "visible-snapshot") {
return {
scrollbackLines: resolveVisibleRestoreScrollbackLines(restore.scrollbackLines),
};
if (restore.mode === "full-snapshot") {
return chunked ? { scrollbackLines: restore.scrollbackLines } : undefined;
}
return undefined;
const scrollbackLines = restore.scrollbackLines ?? DEFAULT_VISIBLE_RESTORE_SCROLLBACK_LINES;
return {
scrollbackLines: chunked
? scrollbackLines
: Math.min(scrollbackLines, MAX_VISIBLE_RESTORE_SCROLLBACK_LINES),
};
}
export function encodeTerminalRestoreFrame(input: { slot: number; data: Uint8Array }): Uint8Array {
@@ -55,13 +62,6 @@ export function encodeTerminalRestoreFrame(input: { slot: number; data: Uint8Arr
});
}
function resolveVisibleRestoreScrollbackLines(value: number | undefined): number {
if (typeof value !== "number") {
return DEFAULT_VISIBLE_RESTORE_SCROLLBACK_LINES;
}
return Math.min(Math.max(0, value), MAX_VISIBLE_RESTORE_SCROLLBACK_LINES);
}
/** The daemon's only old-app adapter. Capable delivery never assembles the replay. */
export function createTerminalRestoreDelivery(input: {
slot: number;
@@ -782,10 +782,49 @@ describe("terminal-session-controller backpressure snapshot fallback", () => {
};
}
test.each([false, true])(
"preserves visible history defaults and explicit requests across delivery (chunked=%s)",
async (chunked) => {
const replayOpcodes = chunked
? [
TerminalStreamOpcode.ReplayBegin,
TerminalStreamOpcode.ReplayData,
TerminalStreamOpcode.ReplayEnd,
]
: [TerminalStreamOpcode.Restore];
for (const [restore, expected] of [
[{ mode: "visible-snapshot" }, [{ scrollbackLines: 200 }]],
[{ mode: "visible-snapshot", scrollbackLines: 0 }, [{ scrollbackLines: 0 }]],
[{ mode: "visible-snapshot", scrollbackLines: 5 }, [{ scrollbackLines: 5 }]],
[{ mode: "visible-snapshot", scrollbackLines: 500 }, [{ scrollbackLines: 500 }]],
[
{ mode: "visible-snapshot", scrollbackLines: 10000 },
[{ scrollbackLines: chunked ? 10000 : 500 }],
],
[{ mode: "full-snapshot" }, [chunked ? { scrollbackLines: undefined } : undefined]],
[{ mode: "live" }, []],
] as const) {
const fixture = await setup(() => 0, chunked, undefined, restore);
expect(fixture.replayOptions).toEqual(expected);
fixture.ready();
await waitForCoalescerFlush();
expect(fixture.replayOptions).toEqual([...expected, ...expected]);
expect(fixture.frames.map((frame) => frame.opcode)).toEqual(
restore.mode === "live" ? [] : replayOpcodes,
);
fixture.pushOutput("after-restore");
await waitForCoalescerFlush();
expect(fixture.frames.at(-1)?.opcode).toBe(TerminalStreamOpcode.Output);
expect(new TextDecoder().decode(fixture.frames.at(-1)!.payload)).toBe("after-restore");
}
},
);
test.each([false, true])(
"bounds recovery without changing attach demand (chunked=%s)",
async (chunked) => {
for (const [restore, history] of [
[{ mode: "visible-snapshot" }, 500],
[{ mode: "visible-snapshot", scrollbackLines: 10000 }, 500],
[{ mode: "full-snapshot" }, 500],
[{ mode: "visible-snapshot", scrollbackLines: 5 }, 5],
@@ -1085,10 +1085,7 @@ export class TerminalSessionController {
restore: TerminalRestoreOptions,
): Promise<SnapshotSendResult> {
const chunked = this.options.supportsChunkedReplay?.(activeStream.owner.source) === true;
const snapshotOptions =
chunked && restore.mode !== "live"
? { scrollbackLines: restore.scrollbackLines }
: resolveTerminalRestoreSnapshotOptions(restore);
const snapshotOptions = resolveTerminalRestoreSnapshotOptions({ restore, chunked });
if (snapshotOptions === null) {
return { shouldContinue: true, replay: null };
}