diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index b9bb9b6..e98ef44 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -388,8 +388,9 @@ watch(() => route.name, (newPage) => {
-
+
+
+
+
+ {{ error }}
+
+
@@ -333,6 +345,24 @@ function handleResume(sessionId: string, agent: AgentName) {
border-bottom-color: #6366f1;
}
+/* Error banner */
+.nsm-error {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: rgba(239, 68, 68, 0.12);
+ border-bottom: 1px solid rgba(239, 68, 68, 0.3);
+ color: #f87171;
+ font-size: 12px;
+ flex-shrink: 0;
+}
+
+.nsm-error svg {
+ flex-shrink: 0;
+ color: #ef4444;
+}
+
/* Body */
.nsm-body {
flex: 1;
diff --git a/frontend/src/composables/transcript-debug/useTranscriptDebug.ts b/frontend/src/composables/transcript-debug/useTranscriptDebug.ts
index 985cb9b..4be58bd 100644
--- a/frontend/src/composables/transcript-debug/useTranscriptDebug.ts
+++ b/frontend/src/composables/transcript-debug/useTranscriptDebug.ts
@@ -90,6 +90,10 @@ export function useTranscriptDebug() {
const awaitingNewSession = ref(false)
const pendingPrompt = ref(null)
+ const lastStartError = ref(null)
+
+ // Guard: when a FAB click triggers switchToTerminal before init(), init() must not overwrite
+ const pendingSwitchTarget = ref(null)
// ── Server registry HTTP helpers ──
@@ -151,8 +155,10 @@ export function useTranscriptDebug() {
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Unknown error' }))
console.error(`[TranscriptDebug] Failed to create terminal: ${err.error}`)
+ lastStartError.value = err.error || 'Failed to create terminal'
return
}
+ lastStartError.value = null
const { ephemeralSessionId } = await res.json()
@@ -235,6 +241,7 @@ export function useTranscriptDebug() {
async function switchToTerminal(transcriptSessionId: string) {
if (transcriptSessionId === activeTerminalSessionId.value) return
+ pendingSwitchTarget.value = transcriptSessionId
transitioning.value = true
transitionError.value = null
@@ -281,6 +288,7 @@ export function useTranscriptDebug() {
// Connect to the terminal
connectToTerminal(transcriptSessionId)
+ pendingSwitchTarget.value = null
transitioning.value = false
}
@@ -561,6 +569,14 @@ export function useTranscriptDebug() {
}
async function init() {
+ // If a FAB click already triggered switchToTerminal(), let it handle the connection.
+ // Only load sessions and connect realtime — don't auto-connect to a terminal.
+ if (pendingSwitchTarget.value) {
+ await fetchSessions()
+ connectRealtime()
+ return
+ }
+
// Derive initial state from server registry: if there's an alive terminal, sync to it
const aliveEntry = serverRegistry.value.find(e => e.alive)
if (aliveEntry) {
@@ -569,6 +585,12 @@ export function useTranscriptDebug() {
await fetchSessions()
+ // Re-check after async gap — switchToTerminal() may have started during fetchSessions
+ if (pendingSwitchTarget.value) {
+ connectRealtime()
+ return
+ }
+
let targetSession: string | null = null
if (aliveEntry) {
@@ -589,6 +611,12 @@ export function useTranscriptDebug() {
selectedSessionId.value = targetSession
await fetchSessionContent(targetSession)
+ // Final check before connecting — bail if user clicked a FAB during content fetch
+ if (pendingSwitchTarget.value) {
+ connectRealtime()
+ return
+ }
+
// Connect to existing terminal if one exists (clients never create terminals — only the server does)
const existing = serverRegistry.value.find(
e => e.transcriptSessionId === targetSession && e.alive
@@ -968,6 +996,7 @@ export function useTranscriptDebug() {
terminalReady,
hookMeta,
awaitingNewSession,
+ lastStartError,
openTerminals,
activeTerminalSessionId,
init,
diff --git a/server/services/terminal.ts b/server/services/terminal.ts
index f1d54eb..5b00cc6 100644
--- a/server/services/terminal.ts
+++ b/server/services/terminal.ts
@@ -1,7 +1,33 @@
import { spawn, type IPty } from '@skitee3000/bun-pty'
+import { existsSync } from 'fs'
+import { join } from 'path'
+import { homedir } from 'os'
import { PORT_TERMINAL, WORKING_DIR, SHELL, SHELL_ARGS, DEFAULT_SESSION_ID, MAX_BUFFER_LINES, MAX_TERMINALS } from '../config'
import { sessionState, type SessionStatePatch } from './session-state'
+// Agent transcript directories (mirrored from transcript-debug.ts)
+const AGENT_TRANSCRIPT_DIRS: Record = {
+ ejecutor: join(WORKING_DIR, '.claude-ejecutor', 'projects'),
+ nucleo000: join(WORKING_DIR, '.claude-nucleo000', 'projects'),
+ claude: join(homedir(), '.claude', 'projects')
+}
+
+const PROJECT_HASH = 'C--Users-jodar-agent-ui'
+
+function getTranscriptProjectDir(agent: string): string | null {
+ const baseDir = AGENT_TRANSCRIPT_DIRS[agent]
+ if (!baseDir || !existsSync(baseDir)) return null
+ const exact = join(baseDir, PROJECT_HASH)
+ if (existsSync(exact)) return exact
+ return null
+}
+
+function transcriptSessionExists(agent: string, sessionId: string): boolean {
+ const projectDir = getTranscriptProjectDir(agent)
+ if (!projectDir) return false
+ return existsSync(join(projectDir, `${sessionId}.jsonl`))
+}
+
interface TerminalSession {
id: string
pty: IPty
@@ -400,6 +426,18 @@ export function startTerminalServer() {
)
}
+ // Validate transcript session exists before resuming
+ const tsId = body.transcriptSessionId
+ if (tsId && tsId !== '__new__' && body.agent) {
+ if (!transcriptSessionExists(body.agent, tsId)) {
+ console.warn(`[Terminal] Transcript session not found: ${tsId} (agent: ${body.agent})`)
+ return Response.json(
+ { error: `Transcript session "${tsId}" not found. It may have been deleted.` },
+ { status: 404, headers: corsHeaders }
+ )
+ }
+ }
+
// Generate ephemeralSessionId server-side
const ephemeralSessionId = `pty-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`