feat(dashboard): show wait notice while resumed chat history loads

Cover the blank TUI + blinking-cursor window on session resume, then hide
the notice as soon as the first real PTY payload arrives so history can
stream in visibly.
This commit is contained in:
xxxigm 2026-07-30 17:18:06 +07:00 committed by Austin Pickett
parent 490f7048dd
commit bb189bf6fe

View file

@ -47,6 +47,12 @@ import {
shouldBlockPtyInput,
shouldReconnectPtyOnPageResume,
} from "@/lib/pty-reconnect";
import {
PTY_RESUME_LOADING_MAX_MS,
PTY_RESUME_LOADING_MESSAGE,
shouldFinishResumeHydrationOnChunk,
shouldShowResumeLoadingOverlay,
} from "@/lib/pty-resume-loading";
import {
MOBILE_REPLACEMENT_WINDOW_MS,
normalizePtyMobileInput,
@ -204,6 +210,10 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
const [ptyState, setPtyState] =
useState<PtyConnectionState>("connecting");
const ptyStateRef = useRef<PtyConnectionState>("connecting");
// True until the first real PTY payload arrives for a resumed session.
// Covers the blank terminal + blinking-cursor window so users don't think
// chat is broken; clears as soon as there is something to show.
const [resumeHydrating, setResumeHydrating] = useState(false);
const [lastCloseCode, setLastCloseCode] = useState<number | null>(null);
// NS-504: when the agent process exits cleanly (the user typed `/exit`, or
// started a new session that ended the current PTY child), the PTY socket
@ -892,12 +902,42 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
let onDataDisposable: { dispose(): void } | null = null;
let onResizeDisposable: { dispose(): void } | null = null;
let eraseSuppressionTimer: ReturnType<typeof setTimeout> | null = null;
let resumeMaxTimer: ReturnType<typeof setTimeout> | null = null;
const clearEraseSuppressionTimer = () => {
if (eraseSuppressionTimer) {
clearTimeout(eraseSuppressionTimer);
eraseSuppressionTimer = null;
}
};
const clearResumeLoadingTimers = () => {
if (resumeMaxTimer) {
clearTimeout(resumeMaxTimer);
resumeMaxTimer = null;
}
};
const finishResumeHydration = () => {
clearResumeLoadingTimers();
if (!unmounting) {
setResumeHydrating(false);
}
};
const noteResumePtyChunk = (chunkText: string) => {
if (!resumeParam || unmounting) {
return;
}
if (shouldFinishResumeHydrationOnChunk(chunkText)) {
finishResumeHydration();
}
};
if (resumeParam) {
setResumeHydrating(true);
resumeMaxTimer = setTimeout(
finishResumeHydration,
PTY_RESUME_LOADING_MAX_MS,
);
} else {
setResumeHydrating(false);
}
const forceFresh = forceFreshPtyRef.current;
forceFreshPtyRef.current = false;
// A connect attempt is now in flight — set synchronously (before the async
@ -1019,6 +1059,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
stream: true,
});
term.write(resumeParam ? sanitizer.next(text) : text);
noteResumePtyChunk(text);
};
ws.onclose = (ev) => {
@ -1180,6 +1221,8 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
imageUploadDisposed = true;
syncMetricsRef.current = null;
clearEraseSuppressionTimer();
clearResumeLoadingTimers();
setResumeHydrating(false);
onDataDisposable?.dispose();
onResizeDisposable?.dispose();
mobileInputCleanup?.();
@ -1354,6 +1397,11 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
const visibleBanner = banner ?? reconnectBanner;
const showReconnectOverlay =
ptyState === "reconnecting" || (ptyState === "closed" && !banner);
const showResumeLoadingOverlay = shouldShowResumeLoadingOverlay({
hasResumeTarget: Boolean(resumeParam),
ptyState,
hydrating: resumeHydrating,
});
const mobileModelToolsPortal =
isActive &&
narrow &&
@ -1488,6 +1536,19 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
</div>
)}
{showResumeLoadingOverlay && (
<div
className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center"
role="status"
aria-live="polite"
aria-label={PTY_RESUME_LOADING_MESSAGE}
>
<div className="max-w-[min(28rem,calc(100vw-3rem))] border border-current/30 bg-black/80 px-4 py-3 text-center text-xs tracking-wide text-white/85 shadow-lg">
{PTY_RESUME_LOADING_MESSAGE}
</div>
</div>
)}
{/* NS-504: the agent process exited (e.g. `/exit` or a new session).
Offer an in-place restart so the user never has to refresh the
whole page to get a working chat back. */}