mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
490f7048dd
commit
bb189bf6fe
1 changed files with 61 additions and 0 deletions
|
|
@ -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. */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue