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:
@@ -40,6 +40,9 @@ const {
|
|||||||
switchAgent,
|
switchAgent,
|
||||||
selectSession,
|
selectSession,
|
||||||
createNewSession,
|
createNewSession,
|
||||||
|
startTerminal,
|
||||||
|
parkCurrentTerminal,
|
||||||
|
fetchSessionContent,
|
||||||
switchToTerminal,
|
switchToTerminal,
|
||||||
closeTerminal,
|
closeTerminal,
|
||||||
disconnectRealtime,
|
disconnectRealtime,
|
||||||
@@ -468,7 +471,11 @@ async function handleModalResume(sessionId: string, agent: AgentName) {
|
|||||||
if (agent !== selectedAgent.value) {
|
if (agent !== selectedAgent.value) {
|
||||||
await switchAgent(agent)
|
await switchAgent(agent)
|
||||||
}
|
}
|
||||||
selectSession(sessionId)
|
// Load transcript + create terminal with --resume
|
||||||
|
parkCurrentTerminal()
|
||||||
|
selectedSessionId.value = sessionId
|
||||||
|
await fetchSessionContent(sessionId)
|
||||||
|
await startTerminal(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -92,30 +92,6 @@ export function useTranscriptDebug() {
|
|||||||
|
|
||||||
// ── Server registry HTTP helpers ──
|
// ── 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(
|
async function updateTerminalOnServer(
|
||||||
ephemeralSessionId: string,
|
ephemeralSessionId: string,
|
||||||
updates: { transcriptSessionId?: string; label?: string }
|
updates: { transcriptSessionId?: string; label?: string }
|
||||||
@@ -150,26 +126,44 @@ export function useTranscriptDebug() {
|
|||||||
return sessionId.slice(0, 12) + '...'
|
return sessionId.slice(0, 12) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTerminal(sessionId?: string) {
|
async function startTerminal(sessionId?: string) {
|
||||||
const key = sessionId || '__new__'
|
const key = sessionId || '__new__'
|
||||||
const cmd = sessionId
|
const cmd = sessionId
|
||||||
? `${AGENT_CMD[selectedAgent.value]} --resume "${sessionId}"`
|
? `${AGENT_CMD[selectedAgent.value]} --resume "${sessionId}"`
|
||||||
: AGENT_CMD[selectedAgent.value]
|
: AGENT_CMD[selectedAgent.value]
|
||||||
|
|
||||||
const term = useEphemeralTerminal(cmd)
|
console.log(`[TranscriptDebug] startTerminal called — key=${key} cmd=${cmd} url=${terminalApiUrl('/create-terminal')}`)
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
localTerminals.set(key, term)
|
||||||
activeTerminalSessionId.value = key
|
activeTerminalSessionId.value = key
|
||||||
|
|
||||||
term.start()
|
term.start()
|
||||||
|
} catch (e: any) {
|
||||||
// Register on server
|
console.error('[TranscriptDebug] Terminal creation failed:', e.message)
|
||||||
registerTerminalOnServer(
|
}
|
||||||
term.ephemeralSessionId,
|
|
||||||
key,
|
|
||||||
selectedAgent.value,
|
|
||||||
sessionId ? getSessionLabel(sessionId) : 'New session',
|
|
||||||
cmd
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parkCurrentTerminal() {
|
function parkCurrentTerminal() {
|
||||||
@@ -281,7 +275,7 @@ export function useTranscriptDebug() {
|
|||||||
pendingPrompt.value = initialPrompt?.trim() || null
|
pendingPrompt.value = initialPrompt?.trim() || null
|
||||||
|
|
||||||
awaitingNewSession.value = true
|
awaitingNewSession.value = true
|
||||||
startTerminal() // no sessionId → brand new session
|
await startTerminal() // no sessionId → brand new session
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WebSocket realtime ──
|
// ── WebSocket realtime ──
|
||||||
@@ -949,6 +943,9 @@ export function useTranscriptDebug() {
|
|||||||
switchAgent,
|
switchAgent,
|
||||||
selectSession,
|
selectSession,
|
||||||
createNewSession,
|
createNewSession,
|
||||||
|
startTerminal,
|
||||||
|
parkCurrentTerminal,
|
||||||
|
fetchSessionContent,
|
||||||
switchToTerminal,
|
switchToTerminal,
|
||||||
closeTerminal,
|
closeTerminal,
|
||||||
connectRealtime,
|
connectRealtime,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const SHELL = process.platform === 'win32' ? 'powershell.exe' : 'bash'
|
|||||||
export const SHELL_ARGS = process.platform === 'win32' ? ['-NoLogo', '-NoProfile'] : []
|
export const SHELL_ARGS = process.platform === 'win32' ? ['-NoLogo', '-NoProfile'] : []
|
||||||
export const DEFAULT_SESSION_ID = 'main'
|
export const DEFAULT_SESSION_ID = 'main'
|
||||||
export const MAX_BUFFER_LINES = 10000
|
export const MAX_BUFFER_LINES = 10000
|
||||||
|
export const MAX_TERMINALS = 5
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
export const DB_PATH = 'agent-ui.db'
|
export const DB_PATH = 'agent-ui.db'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawn, type IPty } from '@skitee3000/bun-pty'
|
import { spawn, type IPty } from '@skitee3000/bun-pty'
|
||||||
import { PORT_TERMINAL, WORKING_DIR, SHELL, SHELL_ARGS, DEFAULT_SESSION_ID, MAX_BUFFER_LINES } from '../config'
|
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'
|
import { sessionState, type SessionStatePatch } from './session-state'
|
||||||
|
|
||||||
interface TerminalSession {
|
interface TerminalSession {
|
||||||
@@ -31,9 +31,12 @@ const AGENT_COMMANDS: Record<string, string> = {
|
|||||||
// Store active terminal sessions by ID (persistent across reconnections)
|
// Store active terminal sessions by ID (persistent across reconnections)
|
||||||
const sessions = new Map<string, TerminalSession>()
|
const sessions = new Map<string, TerminalSession>()
|
||||||
|
|
||||||
// Map WebSocket to sessionId
|
// Map WebSocket to sessionId (only for PTY-connected clients)
|
||||||
const wsToSession = new Map<any, string>()
|
const wsToSession = new Map<any, string>()
|
||||||
|
|
||||||
|
// Broadcast-only clients (no PTY, just receive state updates)
|
||||||
|
const broadcastClients = new Set<any>()
|
||||||
|
|
||||||
// ── Global terminal registry ──
|
// ── Global terminal registry ──
|
||||||
// Tracks metadata about transcript-debug terminals so all clients can see/connect to them
|
// Tracks metadata about transcript-debug terminals so all clients can see/connect to them
|
||||||
|
|
||||||
@@ -60,16 +63,25 @@ function getRegistrySnapshot() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast session state patch to ALL clients across ALL sessions
|
// Send a message to ALL clients: broadcast-only + PTY-connected
|
||||||
function broadcastSessionStatePatch(patch: SessionStatePatch) {
|
function broadcastToAll(message: string): number {
|
||||||
const message = JSON.stringify(patch)
|
let count = 0
|
||||||
let clientCount = 0
|
for (const ws of broadcastClients) {
|
||||||
|
try { ws.send(message); count++ } catch { /* skip */ }
|
||||||
|
}
|
||||||
for (const [, session] of sessions) {
|
for (const [, session] of sessions) {
|
||||||
for (const ws of session.clients) {
|
for (const ws of session.clients) {
|
||||||
try { ws.send(message); clientCount++ } catch { /* skip */ }
|
try { ws.send(message); count++ } catch { /* skip */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`[Terminal] State patch: ${patch.event} (${patch.agent}) → ${clientCount} clients`)
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast session state patch to ALL clients
|
||||||
|
function broadcastSessionStatePatch(patch: SessionStatePatch) {
|
||||||
|
const message = JSON.stringify(patch)
|
||||||
|
const count = broadcastToAll(message)
|
||||||
|
console.log(`[Terminal] State patch: ${patch.event} (${patch.agent}) → ${count} clients`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastRegistryChange() {
|
function broadcastRegistryChange() {
|
||||||
@@ -78,13 +90,8 @@ function broadcastRegistryChange() {
|
|||||||
registry: getRegistrySnapshot(),
|
registry: getRegistrySnapshot(),
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
let clientCount = 0
|
const count = broadcastToAll(message)
|
||||||
for (const [, session] of sessions) {
|
console.log(`[Terminal] Registry broadcast → ${count} clients (${terminalRegistry.size} entries)`)
|
||||||
for (const ws of session.clients) {
|
|
||||||
try { ws.send(message); clientCount++ } catch { /* skip */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[Terminal] Registry broadcast → ${clientCount} clients (${terminalRegistry.size} entries)`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateSession(sessionId: string = DEFAULT_SESSION_ID): TerminalSession {
|
function getOrCreateSession(sessionId: string = DEFAULT_SESSION_ID): TerminalSession {
|
||||||
@@ -370,6 +377,59 @@ export function startTerminalServer() {
|
|||||||
|
|
||||||
// ── Terminal Registry endpoints ──
|
// ── Terminal Registry endpoints ──
|
||||||
|
|
||||||
|
// Create a new terminal (server-first flow: PTY + registry + broadcast)
|
||||||
|
if (url.pathname === '/create-terminal' && req.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const body = await req.json() as {
|
||||||
|
agent: string
|
||||||
|
transcriptSessionId?: string
|
||||||
|
label?: string
|
||||||
|
command: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.command) {
|
||||||
|
return Response.json({ error: 'command required' }, { status: 400, headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce terminal limit
|
||||||
|
if (terminalRegistry.size >= MAX_TERMINALS) {
|
||||||
|
console.error(`[Terminal] Cannot create terminal: limit reached (${terminalRegistry.size}/${MAX_TERMINALS})`)
|
||||||
|
return Response.json(
|
||||||
|
{ error: `Terminal limit reached (max ${MAX_TERMINALS})` },
|
||||||
|
{ status: 429, headers: corsHeaders }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ephemeralSessionId server-side
|
||||||
|
const ephemeralSessionId = `pty-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
|
||||||
|
// Create PTY session
|
||||||
|
const session = getOrCreateSession(ephemeralSessionId)
|
||||||
|
|
||||||
|
// Write command to PTY (with delay for shell init)
|
||||||
|
setTimeout(() => {
|
||||||
|
session.pty.write(body.command + '\r')
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// Register in terminal registry
|
||||||
|
terminalRegistry.set(ephemeralSessionId, {
|
||||||
|
ephemeralSessionId,
|
||||||
|
transcriptSessionId: body.transcriptSessionId || '__new__',
|
||||||
|
agent: body.agent || '',
|
||||||
|
label: body.label || 'New session',
|
||||||
|
command: body.command,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`[Terminal] Created terminal: ${ephemeralSessionId} → ${body.transcriptSessionId || '__new__'} (${body.agent})`)
|
||||||
|
broadcastRegistryChange()
|
||||||
|
|
||||||
|
return Response.json({ success: true, ephemeralSessionId }, { headers: corsHeaders })
|
||||||
|
} catch (e: any) {
|
||||||
|
return Response.json({ error: e.message }, { status: 500, headers: corsHeaders })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// List all registered terminals (global, for all clients)
|
// List all registered terminals (global, for all clients)
|
||||||
if (url.pathname === '/terminal-registry' && req.method === 'GET') {
|
if (url.pathname === '/terminal-registry' && req.method === 'GET') {
|
||||||
return Response.json({ registry: getRegistrySnapshot() }, { headers: corsHeaders })
|
return Response.json({ registry: getRegistrySnapshot() }, { headers: corsHeaders })
|
||||||
@@ -485,9 +545,10 @@ export function startTerminalServer() {
|
|||||||
console.log(`[Terminal] Request: ${req.method} ${url.pathname}, Upgrade: ${upgradeHeader}`)
|
console.log(`[Terminal] Request: ${req.method} ${url.pathname}, Upgrade: ${upgradeHeader}`)
|
||||||
|
|
||||||
if (upgradeHeader?.toLowerCase() === 'websocket') {
|
if (upgradeHeader?.toLowerCase() === 'websocket') {
|
||||||
const sessionId = url.searchParams.get('session') || DEFAULT_SESSION_ID
|
const sessionId = url.searchParams.get('session') || null
|
||||||
const success = server.upgrade(req, { data: { sessionId } })
|
const isBroadcast = !sessionId
|
||||||
console.log(`[Terminal] WebSocket upgrade for session "${sessionId}": ${success ? 'success' : 'failed'}`)
|
const success = server.upgrade(req, { data: { sessionId, broadcast: isBroadcast } })
|
||||||
|
console.log(`[Terminal] WebSocket upgrade ${isBroadcast ? '(broadcast-only)' : `for session "${sessionId}"`}: ${success ? 'success' : 'failed'}`)
|
||||||
if (success) {
|
if (success) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -501,29 +562,14 @@ export function startTerminalServer() {
|
|||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
open(ws) {
|
open(ws) {
|
||||||
const sessionId = (ws.data as any)?.sessionId || DEFAULT_SESSION_ID
|
const data = ws.data as any
|
||||||
console.log(`[Terminal] Client connecting to session: ${sessionId}`)
|
const isBroadcast = data?.broadcast === true
|
||||||
|
|
||||||
try {
|
if (isBroadcast) {
|
||||||
const session = getOrCreateSession(sessionId)
|
// Broadcast-only client — no PTY, just receives state updates
|
||||||
session.clients.add(ws)
|
broadcastClients.add(ws)
|
||||||
wsToSession.set(ws, sessionId)
|
|
||||||
|
|
||||||
// Send connection info (include buffer size so client knows if replay is needed)
|
// Send session state snapshot + terminal registry
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'connected',
|
|
||||||
sessionId: session.id,
|
|
||||||
isNew: session.outputBuffer.length === 0,
|
|
||||||
hasHistory: session.outputBuffer.length > 0,
|
|
||||||
bufferSize: session.outputBuffer.length
|
|
||||||
}))
|
|
||||||
|
|
||||||
// DON'T auto-replay here!
|
|
||||||
// Client will request replay when terminal is visible and ready.
|
|
||||||
// This fixes xterm.js rendering issues with hidden containers.
|
|
||||||
console.log(`[Terminal] Client connected, buffer has ${session.outputBuffer.length} chunks (client will request replay)`)
|
|
||||||
|
|
||||||
// Send centralized session state snapshot
|
|
||||||
const snapshot = sessionState.getSnapshot()
|
const snapshot = sessionState.getSnapshot()
|
||||||
if (Object.keys(snapshot).length > 0) {
|
if (Object.keys(snapshot).length > 0) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@@ -532,6 +578,34 @@ export function startTerminalServer() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send current terminal registry
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'terminal-registry-change',
|
||||||
|
registry: getRegistrySnapshot(),
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log(`[Terminal] Broadcast client connected (${broadcastClients.size} broadcast clients)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PTY client — connect to an existing session
|
||||||
|
const sessionId = data?.sessionId || DEFAULT_SESSION_ID
|
||||||
|
console.log(`[Terminal] Client connecting to session: ${sessionId}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = getOrCreateSession(sessionId)
|
||||||
|
session.clients.add(ws)
|
||||||
|
wsToSession.set(ws, sessionId)
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'connected',
|
||||||
|
sessionId: session.id,
|
||||||
|
isNew: session.outputBuffer.length === 0,
|
||||||
|
hasHistory: session.outputBuffer.length > 0,
|
||||||
|
bufferSize: session.outputBuffer.length
|
||||||
|
}))
|
||||||
|
|
||||||
console.log(`[Terminal] Client joined session ${sessionId} (${session.clients.size} clients)`)
|
console.log(`[Terminal] Client joined session ${sessionId} (${session.clients.size} clients)`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[Terminal] Error:', e)
|
console.error('[Terminal] Error:', e)
|
||||||
@@ -539,6 +613,9 @@ export function startTerminalServer() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
message(ws, message) {
|
message(ws, message) {
|
||||||
|
// Broadcast-only clients don't have a PTY — ignore their messages
|
||||||
|
if (broadcastClients.has(ws)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(message as string)
|
const msg = JSON.parse(message as string)
|
||||||
const sessionId = wsToSession.get(ws)
|
const sessionId = wsToSession.get(ws)
|
||||||
@@ -589,13 +666,19 @@ export function startTerminalServer() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
close(ws) {
|
close(ws) {
|
||||||
|
// Check if this was a broadcast-only client
|
||||||
|
if (broadcastClients.delete(ws)) {
|
||||||
|
console.log(`[Terminal] Broadcast client disconnected (${broadcastClients.size} remaining)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PTY client cleanup
|
||||||
const sessionId = wsToSession.get(ws)
|
const sessionId = wsToSession.get(ws)
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
const session = sessions.get(sessionId)
|
const session = sessions.get(sessionId)
|
||||||
if (session) {
|
if (session) {
|
||||||
session.clients.delete(ws)
|
session.clients.delete(ws)
|
||||||
console.log(`[Terminal] Client left session ${sessionId} (${session.clients.size} clients remaining)`)
|
console.log(`[Terminal] Client left session ${sessionId} (${session.clients.size} clients remaining)`)
|
||||||
// Don't kill PTY - session persists
|
|
||||||
}
|
}
|
||||||
wsToSession.delete(ws)
|
wsToSession.delete(ws)
|
||||||
}
|
}
|
||||||
@@ -637,18 +720,7 @@ export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string, agent
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
let clientCount = 0
|
const clientCount = broadcastToAll(message)
|
||||||
for (const [, session] of sessions) {
|
|
||||||
for (const ws of session.clients) {
|
|
||||||
try {
|
|
||||||
ws.send(message)
|
|
||||||
clientCount++
|
|
||||||
} catch {
|
|
||||||
// Client disconnected, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Terminal] Claude status broadcast: ${status}${tool ? ` (${tool})` : ''} → ${clientCount} clients`)
|
console.log(`[Terminal] Claude status broadcast: ${status}${tool ? ` (${tool})` : ''} → ${clientCount} clients`)
|
||||||
|
|
||||||
// Note: session state is updated via broadcastClaudeHook which has full payload context.
|
// Note: session state is updated via broadcastClaudeHook which has full payload context.
|
||||||
@@ -669,18 +741,7 @@ export function broadcastClaudeHook(data: Record<string, unknown>) {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
let clientCount = 0
|
const clientCount = broadcastToAll(message)
|
||||||
for (const [, session] of sessions) {
|
|
||||||
for (const ws of session.clients) {
|
|
||||||
try {
|
|
||||||
ws.send(message)
|
|
||||||
clientCount++
|
|
||||||
} catch {
|
|
||||||
// Client disconnected, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Terminal] Claude hook broadcast: ${data.hook_event_name || 'unknown'}${data.tool_name ? ` (${data.tool_name})` : ''} → ${clientCount} clients`)
|
console.log(`[Terminal] Claude hook broadcast: ${data.hook_event_name || 'unknown'}${data.tool_name ? ` (${data.tool_name})` : ''} → ${clientCount} clients`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,18 +753,7 @@ export function broadcastPermissionRequest(data: Record<string, unknown>) {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
let clientCount = 0
|
const clientCount = broadcastToAll(message)
|
||||||
for (const [, session] of sessions) {
|
|
||||||
for (const ws of session.clients) {
|
|
||||||
try {
|
|
||||||
ws.send(message)
|
|
||||||
clientCount++
|
|
||||||
} catch {
|
|
||||||
// Client disconnected, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Terminal] Permission request broadcast: ${data.tool_name || 'unknown'} (${data.requestId}) → ${clientCount} clients`)
|
console.log(`[Terminal] Permission request broadcast: ${data.tool_name || 'unknown'} (${data.requestId}) → ${clientCount} clients`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,15 +765,6 @@ export function broadcastTranscriptUpdate(data: Record<string, unknown>) {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
let clientCount = 0
|
const clientCount = broadcastToAll(message)
|
||||||
for (const [, session] of sessions) {
|
|
||||||
for (const ws of session.clients) {
|
|
||||||
try {
|
|
||||||
ws.send(message)
|
|
||||||
clientCount++
|
|
||||||
} catch { /* skip */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Terminal] Transcript update: ${data.hookEvent || 'fetch'} (${(data.messages as any[])?.length || 0} msgs) → ${clientCount} clients`)
|
console.log(`[Terminal] Transcript update: ${data.hookEvent || 'fetch'} (${(data.messages as any[])?.length || 0} msgs) → ${clientCount} clients`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user