fix(terminal): prevent xterm.js crash, websocket disconnect cascade, stale sessions
Three related bugs fixed: 1. Frontend xterm.js crash: TerminalPage rendered ALL sessions with display:none for inactive ones. xterm.js crashes when initialized in a hidden container (Viewport can't read dimensions). Fix: only render the active session's TerminalComponent using conditional rendering. 2. Backend websocket disconnect cascade: When client disconnected (due to #1), the server tried to send 'connected' status on dead socket, caught the WebSocketDisconnect in a generic except block, then tried to close() again causing RuntimeError. Fix: catch WebSocketDisconnect specifically and suppress close() errors. 3. Stale DB sessions: After server restart, DB still had old terminal session rows but no in-memory sessions. list_terminal_sessions returned these ghosts, causing the frontend to render dead tabs. Fix: skip DB-only sessions that have no live in-memory counterpart. Quality gates: pytest (15/15 passed), tsc clean, vitest (7/7 passed)
This commit is contained in:
@@ -229,23 +229,22 @@ export const TerminalPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="terminal-page-content">
|
||||
{error && <div className="terminal-error-banner">{error}</div>}
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`terminal-instance ${session.id === activeSessionId ? "active" : ""}`}
|
||||
style={{
|
||||
display: session.id === activeSessionId ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<TerminalComponent
|
||||
ref={terminalRefs.current[session.id]}
|
||||
instanceId={instanceId}
|
||||
sessionId={session.id}
|
||||
onClose={() => handleClose(session.id)}
|
||||
isMobile={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{sessions
|
||||
.filter((session) => session.id === activeSessionId)
|
||||
.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="terminal-instance active"
|
||||
>
|
||||
<TerminalComponent
|
||||
ref={terminalRefs.current[session.id]}
|
||||
instanceId={instanceId}
|
||||
sessionId={session.id}
|
||||
onClose={() => handleClose(session.id)}
|
||||
isMobile={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{sessions.length === 0 && !loading && (
|
||||
<div className="terminal-empty-state">
|
||||
<p>No terminal sessions. Press Alt+Shift+N to create one.</p>
|
||||
@@ -289,23 +288,22 @@ export const TerminalPage: React.FC = () => {
|
||||
/>
|
||||
<div className="terminal-page-content">
|
||||
{error && <div className="terminal-error-banner">{error}</div>}
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`terminal-instance ${session.id === activeSessionId ? "active" : ""}`}
|
||||
style={{
|
||||
display: session.id === activeSessionId ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<TerminalComponent
|
||||
ref={terminalRefs.current[session.id]}
|
||||
instanceId={instanceId}
|
||||
sessionId={session.id}
|
||||
onClose={() => handleClose(session.id)}
|
||||
isMobile={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{sessions
|
||||
.filter((session) => session.id === activeSessionId)
|
||||
.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="terminal-instance active"
|
||||
>
|
||||
<TerminalComponent
|
||||
ref={terminalRefs.current[session.id]}
|
||||
instanceId={instanceId}
|
||||
sessionId={session.id}
|
||||
onClose={() => handleClose(session.id)}
|
||||
isMobile={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{sessions.length === 0 && !loading && (
|
||||
<div className="terminal-empty-state">
|
||||
<p>No terminal sessions. Press Alt+Shift+N to create one.</p>
|
||||
|
||||
Reference in New Issue
Block a user