From d74bb2896038f7742a3d3e2b01ecc5a2f31e7ac2 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Fri, 13 Mar 2026 11:08:34 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20terminal=20never=20appears=20=E2=80=94?= =?UTF-8?q?=20pending=20session=20removed=20before=20WS=20connects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: TerminalInstance.onMounted() called sessions.removeSession() on the pending session, dropping sessions.length to 0. SessionContainer's v-if="hasSessions" went false, unmounting the entire terminal UI before the WebSocket could establish and add the real session. Fix: Added replaceSession() to session store. TerminalInstance no longer removes the pending session — instead passes its ID to connectToHost(), which swaps it in-place when the backend responds with the real session ID. Co-Authored-By: Claude Opus 4.6 --- frontend/components/terminal/TerminalInstance.vue | 7 ++----- frontend/composables/useTerminal.ts | 5 +++-- frontend/stores/session.store.ts | 9 +++++++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/components/terminal/TerminalInstance.vue b/frontend/components/terminal/TerminalInstance.vue index 080c242..90de85f 100644 --- a/frontend/components/terminal/TerminalInstance.vue +++ b/frontend/components/terminal/TerminalInstance.vue @@ -24,11 +24,8 @@ onMounted(() => { termInstance = createTerminal(termContainer.value) const { term, fitAddon } = termInstance - // Connect — useTerminal will call sessions.addSession with the real backend sessionId. - // Remove the pending placeholder first to avoid duplicate entries. - sessions.removeSession(props.sessionId) - - connectToHost(props.hostId, props.hostName, 'ssh', props.color, term, fitAddon) + // Connect — useTerminal will replace the pending session with the real backend sessionId + connectToHost(props.hostId, props.hostName, 'ssh', props.color, props.sessionId, term, fitAddon) }) onBeforeUnmount(() => { diff --git a/frontend/composables/useTerminal.ts b/frontend/composables/useTerminal.ts index e72e95a..f50d98c 100644 --- a/frontend/composables/useTerminal.ts +++ b/frontend/composables/useTerminal.ts @@ -55,7 +55,7 @@ export function useTerminal() { return { term, fitAddon, searchAddon, resizeObserver } } - function connectToHost(hostId: number, hostName: string, protocol: 'ssh', color: string | null, term: Terminal, fitAddon: FitAddon) { + function connectToHost(hostId: number, hostName: string, protocol: 'ssh', color: string | null, pendingSessionId: string, term: Terminal, fitAddon: FitAddon) { const wsUrl = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/terminal?token=${auth.token}` ws = new WebSocket(wsUrl) @@ -67,7 +67,8 @@ export function useTerminal() { const msg = JSON.parse(event.data) switch (msg.type) { case 'connected': - sessions.addSession({ id: msg.sessionId, hostId, hostName, protocol, color, active: true }) + // Replace the pending placeholder with the real backend session + sessions.replaceSession(pendingSessionId, { id: msg.sessionId, hostId, hostName, protocol, color, active: true }) // Send initial terminal size ws!.send(JSON.stringify({ type: 'resize', sessionId: msg.sessionId, cols: term.cols, rows: term.rows })) break diff --git a/frontend/stores/session.store.ts b/frontend/stores/session.store.ts index 9f338a6..34f1372 100644 --- a/frontend/stores/session.store.ts +++ b/frontend/stores/session.store.ts @@ -29,6 +29,15 @@ export const useSessionStore = defineStore('sessions', { this.activeSessionId = this.sessions.length ? this.sessions[this.sessions.length - 1].id : null } }, + replaceSession(oldId: string, newSession: Session) { + const idx = this.sessions.findIndex(s => s.id === oldId) + if (idx !== -1) { + this.sessions[idx] = newSession + } else { + this.sessions.push(newSession) + } + this.activeSessionId = newSession.id + }, setActive(id: string) { this.activeSessionId = id },