feat: server-first terminal creation, broadcast-only WS clients

- Add POST /create-terminal endpoint with MAX_TERMINALS=5 limit
- Server creates PTY, runs command, registers and broadcasts atomically
- Frontend startTerminal() calls server first, connects in reconnect mode
- Remove registerTerminalOnServer() — server handles registration
- Separate broadcast-only WS clients from PTY clients (no phantom "main" PTY)
- All broadcast functions use broadcastToAll() helper
- Fix resume existing flow to create terminal with --resume flag
This commit is contained in:
2026-02-21 00:17:24 -06:00
parent a6c68f1b9e
commit 2aec892f62
4 changed files with 171 additions and 125 deletions

View File

@@ -40,6 +40,9 @@ const {
switchAgent,
selectSession,
createNewSession,
startTerminal,
parkCurrentTerminal,
fetchSessionContent,
switchToTerminal,
closeTerminal,
disconnectRealtime,
@@ -468,7 +471,11 @@ async function handleModalResume(sessionId: string, agent: AgentName) {
if (agent !== selectedAgent.value) {
await switchAgent(agent)
}
selectSession(sessionId)
// Load transcript + create terminal with --resume
parkCurrentTerminal()
selectedSessionId.value = sessionId
await fetchSessionContent(sessionId)
await startTerminal(sessionId)
}
// ============================================================================

View File

@@ -92,30 +92,6 @@ export function useTranscriptDebug() {
// ── Server registry HTTP helpers ──
async function registerTerminalOnServer(
ephemeralSessionId: string,
transcriptSessionId: string,
agent: AgentName,
label: string,
command: string
) {
try {
await fetch(terminalApiUrl('/register-terminal'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ephemeralSessionId,
transcriptSessionId,
agent,
label,
command,
createdAt: new Date().toISOString()
})
})
// Server broadcasts change → all clients pick it up via WS
} catch { /* best effort */ }
}
async function updateTerminalOnServer(
ephemeralSessionId: string,
updates: { transcriptSessionId?: string; label?: string }
@@ -150,26 +126,44 @@ export function useTranscriptDebug() {
return sessionId.slice(0, 12) + '...'
}
function startTerminal(sessionId?: string) {
async function startTerminal(sessionId?: string) {
const key = sessionId || '__new__'
const cmd = sessionId
? `${AGENT_CMD[selectedAgent.value]} --resume "${sessionId}"`
: AGENT_CMD[selectedAgent.value]
const term = useEphemeralTerminal(cmd)
localTerminals.set(key, term)
activeTerminalSessionId.value = key
console.log(`[TranscriptDebug] startTerminal called — key=${key} cmd=${cmd} url=${terminalApiUrl('/create-terminal')}`)
term.start()
try {
// Server creates PTY, runs command, registers in registry, broadcasts
const res = await fetch(terminalApiUrl('/create-terminal'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent: selectedAgent.value,
transcriptSessionId: key,
label: sessionId ? getSessionLabel(sessionId) : 'New session',
command: cmd
})
})
// Register on server
registerTerminalOnServer(
term.ephemeralSessionId,
key,
selectedAgent.value,
sessionId ? getSessionLabel(sessionId) : 'New session',
cmd
)
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Unknown error' }))
console.error(`[TranscriptDebug] Failed to create terminal: ${err.error}`)
return
}
const { ephemeralSessionId } = await res.json()
// Connect to the existing PTY in reconnect mode
const term = useEphemeralTerminal(cmd, ephemeralSessionId)
localTerminals.set(key, term)
activeTerminalSessionId.value = key
term.start()
} catch (e: any) {
console.error('[TranscriptDebug] Terminal creation failed:', e.message)
}
}
function parkCurrentTerminal() {
@@ -281,7 +275,7 @@ export function useTranscriptDebug() {
pendingPrompt.value = initialPrompt?.trim() || null
awaitingNewSession.value = true
startTerminal() // no sessionId → brand new session
await startTerminal() // no sessionId → brand new session
}
// ── WebSocket realtime ──
@@ -949,6 +943,9 @@ export function useTranscriptDebug() {
switchAgent,
selectSession,
createNewSession,
startTerminal,
parkCurrentTerminal,
fetchSessionContent,
switchToTerminal,
closeTerminal,
connectRealtime,