Compare commits

...
Author SHA1 Message Date
netcatty-bot 28793ed798 fix(#3266): automated AI fix 2026-09-03 07:15:52 +00:00
5 changed files with 74 additions and 5 deletions

No files matched your search

+4
View File
@@ -212,6 +212,7 @@ import {
import {
releaseTerminalFlowBeforeHibernate,
} from "./terminal/runtime/terminalSessionAttachment";
import { resetTerminalLineTimestamps } from "./terminal/runtime/terminalLineTimestamps";
import {
flushPendingTerminalWritesBeforeHibernate,
hasPendingTerminalWrites,
@@ -1631,6 +1632,9 @@ const TerminalComponent: React.FC<TerminalProps> = ({
}
if (term) {
term.reset();
// The buffer was just wiped; drop ledger stamps anchored to the old
// rows so the reconnect gutter cannot paint stale timestamps.
resetTerminalLineTimestamps(term);
if (payload.snapshot) {
await new Promise<void>((resolve) => term.write(payload.snapshot, resolve));
}
@@ -3708,7 +3708,7 @@ test("ssh session restores cwd before startup command after attaching", async ()
assert.deepEqual(progressLogs, ["Restoring working directory: /srv/app dir"]);
});
test("local session resets terminal timestamp state when reusing a terminal", async () => {
test("local session keeps timestamp anchors for preserved scrollback when reusing a terminal", async () => {
const writes: string[] = [];
const markerLines: number[] = [];
const disposedMarkerLines: number[] = [];
@@ -3814,8 +3814,10 @@ test("local session resets terminal timestamp state when reusing a terminal", as
assert.equal(writes.length, 2);
assert.equal(writes[0], "unfinished");
assert.equal(writes[1], "fresh");
// Reconnect preserves the buffer, so the pre-restart stamp stays anchored
// (not disposed) and the fresh output records its own stamp.
assert.deepEqual(markerLines, [0, 0]);
assert.deepEqual(disposedMarkerLines, [0]);
assert.deepEqual(disposedMarkerLines, []);
});
test("session data waits for prior terminal writes before evaluating prompt line breaks", async () => {
@@ -1501,6 +1501,23 @@ export const resetTerminalLineTimestamps = (term: XTerm) => {
resetTimestampStore(getTimestampStore(term));
};
/**
* Reconnect-style reattach on a reused terminal: the xterm buffer (including
* scrollback preserved by the reconnect flow) survives the previous session,
* so keep ledger stamps anchored to that history and only reset per-session
* recording state (segmenter line tracking, same-second gate, pending prefix).
* A full reset here would leave preserved scrollback rows without timestamps.
*/
export const resetTerminalLineTimestampRecordingState = (term: XTerm): void => {
const store = getTimestampStore(term);
store.segmenter.reset();
store.timestampOnlyPrefix = "";
store.lastStampSecondKey = null;
store.recordsSincePrune = 0;
store.disposedPendingCompact = 0;
notifyTimestampStore(store);
};
export const onTerminalLineTimestampsChange = (
term: XTerm,
listener: () => void,
@@ -2051,6 +2051,49 @@ test("attachSessionToTerminal resets timestamp state for a reused terminal", ()
assert.equal(writes[1], "fresh");
});
test("attachSessionToTerminal keeps timestamps for scrollback preserved by reconnect", () => {
const { term } = createFakeTerm();
const ctx = {
...createContext(false, { showLineTimestamps: true }),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: () => () => {},
onSessionExit: () => () => {},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
const originalDateNow = Date.now;
try {
Date.now = () => new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
writeSessionData(ctx as never, term, "before reconnect\r\n");
// Reconnect reuses the same xterm (scrollback preserved by the caller).
attachSessionToTerminal(ctx as never, term, "session-2");
Date.now = () => new Date(2026, 0, 1, 12, 1, 30, 0).getTime();
writeSessionData(ctx as never, term, "after reconnect\r\n");
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:30" },
]);
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("attachSessionToTerminal clears the backend id before reporting exit", () => {
const { term } = createFakeTerm();
let onExit: ((evt: { reason?: string }) => void) | null = null;
@@ -23,7 +23,7 @@ import type {
} from "./createTerminalSessionStarters.types";
import { clearConnectionToken } from "./terminalDistroDetection";
import {
resetTerminalLineTimestamps,
resetTerminalLineTimestampRecordingState,
type TerminalLineTimestampPerfStep,
writeTerminalDataWithLineTimestamps,
} from "./terminalLineTimestamps";
@@ -409,7 +409,7 @@ export const getFlowController = (
return controller;
};
export const resetTerminalLineTimestampState = resetTerminalLineTimestamps;
export const resetTerminalLineTimestampState = resetTerminalLineTimestampRecordingState;
export const acknowledgeDroppedTerminalDisplayBytes = (
ctx: TerminalSessionStartersContext,
@@ -856,7 +856,10 @@ export const attachSessionToTerminal = (
teardownTerminalOutputPipeline(ctx, term, id, flow);
flushTerminalWriteCoalescer(term);
resetTerminalSyncBlockFilter(term);
resetTerminalLineTimestamps(term);
// Reattach keeps the xterm buffer (reconnect preserves scrollback), so only
// reset per-session recording state — a full reset would strip timestamps
// from preserved history.
resetTerminalLineTimestampRecordingState(term);
resetTerminalOutputPressure(term);
ctx.onSessionAttached?.(id);
const assistMode =