feat(FN-1021): stabilize terminal first-open bootstrap and WebSocket recovery

- Harden TerminalModal bootstrap to handle invalid/expired sessions on first open
- Add WebSocket reconnection logic for terminal sessions that become invalid mid-stream
- Create useTerminal hook with robust session lifecycle management
- Create useTerminalSessions hook for multi-session terminal coordination
- Add comprehensive tests for TerminalModal, useTerminal, and useTerminalSessions
- Document terminal first-open reliability behavior in dashboard README
This commit is contained in:
gsxdsm
2026-04-07 00:20:12 -07:00
parent ab064ebc26
commit 4097d9d250
8 changed files with 477 additions and 1 deletions

View File

@@ -19,6 +19,13 @@ export interface UseTerminalReturn {
onScrollback: (callback: (data: string) => void) => () => void;
/** Manually reconnect */
reconnect: () => void;
/**
* Register a callback for session-invalid events.
* Fires when the WebSocket closes with code 4004 (session-not-found),
* meaning the server no longer recognizes the session. The caller should
* create a new session rather than attempting reconnect.
*/
onSessionInvalid: (callback: () => void) => () => void;
}
interface WebSocketMessage {
@@ -86,6 +93,7 @@ export function useTerminal(sessionId: string | null): UseTerminalReturn {
const onExitCallbacksRef = useRef<Set<(exitCode: number) => void>>(new Set());
const onConnectCallbacksRef = useRef<Set<(info: { shell: string; cwd: string }) => void>>(new Set());
const onScrollbackCallbacksRef = useRef<Set<(data: string) => void>>(new Set());
const onSessionInvalidCallbacksRef = useRef<Set<() => void>>(new Set());
// Buffer for initial messages received before subscribers are registered.
// This ensures scrollback, connected info, and early shell output are
@@ -135,6 +143,17 @@ export function useTerminal(sessionId: string | null): UseTerminalReturn {
return () => onScrollbackCallbacksRef.current.delete(callback);
}, []);
/**
* Register a callback for session-invalid events.
* Fires when the server closes the WebSocket with code 4004, indicating
* the session no longer exists. Unlike transient disconnects, this is a
* permanent condition that requires creating a new session to recover.
*/
const onSessionInvalid = useCallback((callback: () => void) => {
onSessionInvalidCallbacksRef.current.add(callback);
return () => onSessionInvalidCallbacksRef.current.delete(callback);
}, []);
// Send input to terminal
const sendInput = useCallback((data: string) => {
const ws = wsRef.current;
@@ -288,6 +307,13 @@ export function useTerminal(sessionId: string | null): UseTerminalReturn {
// Don't reconnect for certain close codes
if (event.code === 4000 || event.code === 4004) {
setConnectionStatus("disconnected");
// Code 4004 means the server doesn't recognize the session — it's
// permanently invalid. Notify subscribers so they can create a new
// session rather than retrying the stale one.
if (event.code === 4004) {
onSessionInvalidCallbacksRef.current.forEach((cb) => cb());
}
return;
}
@@ -342,5 +368,6 @@ export function useTerminal(sessionId: string | null): UseTerminalReturn {
onConnect,
onScrollback,
reconnect,
onSessionInvalid,
};
}