fix(terminal): prevent double session creation, restore sessions on reload

Three fixes for multi-session terminal bugs:

1. Race-condition double creation: The auto-create effect fired twice because
   loadSessions returned 0 while an earlier createSession was still in flight.
   Added hasAutoCreated guard ref to ensure only one auto-create happens.

2. Page reload spawns new sessions: After server restart, list_terminal_sessions
   filtered out DB-only sessions (no in-memory counterpart), so the frontend
   thought no sessions existed and auto-created new ones. Reverted the filter
   so DB rows are always returned. The WebSocket handler now restores the
   in-memory session from the DB row on demand when connecting.

3. No input after connection: The backend _read_loop would break on any send
   error, causing asyncio.wait to cancel the _write_loop. Made _read_loop
   retry up to 3 times before giving up, preventing transient send errors
   from killing input handling.

Quality gates: pytest (15/15 passed), tsc clean
This commit is contained in:
2026-05-28 19:45:59 +02:00
parent 9ccaae04db
commit b6e71e32f5
3 changed files with 64 additions and 29 deletions
+17 -15
View File
@@ -26,6 +26,7 @@ export const TerminalPage: React.FC = () => {
const [isFullscreen, setIsFullscreen] = useState(false);
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
const hasAutoCreated = useRef(false);
const {
sessions,
@@ -39,9 +40,16 @@ export const TerminalPage: React.FC = () => {
error,
} = useTerminalSessions(instanceId ?? "");
// Auto-create default session if none exist
// Auto-create default session if none exist (guard against double-fire)
useEffect(() => {
if (!loading && sessions.length === 0 && !error && instanceId) {
if (
!loading &&
sessions.length === 0 &&
!error &&
instanceId &&
!hasAutoCreated.current
) {
hasAutoCreated.current = true;
void createSession("Session 1");
}
}, [loading, sessions.length, error, instanceId, createSession]);
@@ -232,16 +240,13 @@ export const TerminalPage: React.FC = () => {
{sessions
.filter((session) => session.id === activeSessionId)
.map((session) => (
<div
key={session.id}
className="terminal-instance active"
>
<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}
ref={terminalRefs.current[session.id]}
instanceId={instanceId}
sessionId={session.id}
onClose={() => handleClose(session.id)}
isMobile={true}
/>
</div>
))}
@@ -291,10 +296,7 @@ export const TerminalPage: React.FC = () => {
{sessions
.filter((session) => session.id === activeSessionId)
.map((session) => (
<div
key={session.id}
className="terminal-instance active"
>
<div key={session.id} className="terminal-instance active">
<TerminalComponent
ref={terminalRefs.current[session.id]}
instanceId={instanceId}