fix(dashboard): gate resume hydration on sanitized PTY payload

The resume wait notice cleared on the first nonempty raw PTY frame, but
the terminal is written sanitizer.next(text). The sanitizer collapses an
erase-only, all-newline, or partial-CSI resume frame to "", so a control-
only first frame hid the notice while xterm was still blank.

Gate hydration completion on the rendered payload actually written to the
terminal, and cover the control-only-first-frame case with a regression
test over the real sanitizer.

Co-authored-by: teknium1 <teknium@nousresearch.com>
This commit is contained in:
Austin Pickett 2026-07-31 09:28:55 -04:00
parent bb189bf6fe
commit 539e9b5c1b
2 changed files with 42 additions and 2 deletions

View file

@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { PtyResumeSanitizer } from "./pty-resume-sanitizer";
import {
PTY_RESUME_LOADING_MAX_MS,
shouldFinishResumeHydrationOnChunk,
@ -17,6 +18,40 @@ describe("shouldFinishResumeHydrationOnChunk", () => {
});
});
describe("resume hydration gate over the real sanitizer", () => {
// Regression: the gate must key off the payload actually written to xterm,
// not the raw frame. The sanitizer collapses an erase-only / all-newline /
// partial-CSI resume frame to "", so a nonempty raw first frame would
// otherwise clear the wait notice while the terminal is still blank.
const ESC = String.fromCharCode(27);
const CRLF = String.fromCharCode(13, 10);
const VISIBLE = `Hello world${CRLF}`;
const controlOnlyFirstFrames: Array<[string, string]> = [
["erase-only", `${ESC}[2K`],
["all-newline", `${CRLF}${CRLF}`],
["partial-CSI", `${ESC}[`],
];
it.each(controlOnlyFirstFrames)(
"keeps hydrating on a %s first frame, then finishes on visible replay",
(_label, firstFrame) => {
const sanitizer = new PtyResumeSanitizer();
// Raw first frame is nonempty, but nothing is written to xterm...
expect(firstFrame.length).toBeGreaterThan(0);
const firstRendered = sanitizer.next(firstFrame);
expect(firstRendered).toBe("");
expect(shouldFinishResumeHydrationOnChunk(firstRendered)).toBe(false);
// ...so the notice only clears once real replay output arrives.
const secondRendered = sanitizer.next(VISIBLE);
expect(secondRendered.length).toBeGreaterThan(0);
expect(shouldFinishResumeHydrationOnChunk(secondRendered)).toBe(true);
},
);
});
describe("shouldShowResumeLoadingOverlay", () => {
it("shows while a resume target is connecting or open and still hydrating", () => {
expect(

View file

@ -1058,8 +1058,13 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
: decoder.decode(new Uint8Array(ev.data as ArrayBuffer), {
stream: true,
});
term.write(resumeParam ? sanitizer.next(text) : text);
noteResumePtyChunk(text);
// Gate hydration on the payload actually written to xterm. The
// sanitizer can turn a nonempty erase-only / all-newline / partial-CSI
// resume frame into "" (pty-resume-sanitizer.ts); keying off raw `text`
// would hide the wait notice while the terminal is still blank.
const rendered = resumeParam ? sanitizer.next(text) : text;
term.write(rendered);
noteResumePtyChunk(rendered);
};
ws.onclose = (ev) => {