feat: voice mic, pixel life layer, enhanced transcript-debug UX
VoiceMicButton component, PixelLife aquatic layer, improved UserMessageBubble with voice display, AgentBadge terminal switcher, ChatContainer voice integration, FloatingTranscriptDebug ocean life enhancements, and terminal registry support. Remove traefik config.
This commit is contained in:
@@ -33,6 +33,47 @@ const sessions = new Map<string, TerminalSession>()
|
||||
// Map WebSocket to sessionId
|
||||
const wsToSession = new Map<any, string>()
|
||||
|
||||
// ── Global terminal registry ──
|
||||
// Tracks metadata about transcript-debug terminals so all clients can see/connect to them
|
||||
|
||||
interface TerminalRegistryEntry {
|
||||
ephemeralSessionId: string // PTY session ID on this server
|
||||
transcriptSessionId: string // Claude transcript session being resumed (or '__new__')
|
||||
agent: string // ejecutor | nucleo000 | claude
|
||||
label: string // First user message or short description
|
||||
command: string // Full command that was run
|
||||
createdAt: string // ISO timestamp
|
||||
}
|
||||
|
||||
const terminalRegistry = new Map<string, TerminalRegistryEntry>() // keyed by ephemeralSessionId
|
||||
|
||||
function getRegistrySnapshot() {
|
||||
return Array.from(terminalRegistry.values()).map(entry => {
|
||||
const ptySession = sessions.get(entry.ephemeralSessionId)
|
||||
return {
|
||||
...entry,
|
||||
alive: !!ptySession,
|
||||
clients: ptySession?.clients.size ?? 0,
|
||||
bufferSize: ptySession?.outputBuffer.length ?? 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function broadcastRegistryChange() {
|
||||
const message = JSON.stringify({
|
||||
type: 'terminal-registry-change',
|
||||
registry: getRegistrySnapshot(),
|
||||
timestamp: Date.now()
|
||||
})
|
||||
let clientCount = 0
|
||||
for (const [, session] of sessions) {
|
||||
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 {
|
||||
let session = sessions.get(sessionId)
|
||||
|
||||
@@ -83,6 +124,12 @@ function getOrCreateSession(sessionId: string = DEFAULT_SESSION_ID): TerminalSes
|
||||
}
|
||||
sessions.delete(sessionId)
|
||||
|
||||
// Auto-remove from terminal registry
|
||||
if (terminalRegistry.has(sessionId)) {
|
||||
terminalRegistry.delete(sessionId)
|
||||
broadcastRegistryChange()
|
||||
}
|
||||
|
||||
// Mark agent as not running if this is an agent session
|
||||
if (sessionId.startsWith('agent-')) {
|
||||
const agentId = sessionId.replace('agent-', '')
|
||||
@@ -122,6 +169,13 @@ export function killSession(sessionId: string): boolean {
|
||||
}
|
||||
|
||||
sessions.delete(sessionId)
|
||||
|
||||
// Auto-remove from terminal registry
|
||||
if (terminalRegistry.has(sessionId)) {
|
||||
terminalRegistry.delete(sessionId)
|
||||
broadcastRegistryChange()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -301,6 +355,67 @@ export function startTerminalServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Terminal Registry endpoints ──
|
||||
|
||||
// List all registered terminals (global, for all clients)
|
||||
if (url.pathname === '/terminal-registry' && req.method === 'GET') {
|
||||
return Response.json({ registry: getRegistrySnapshot() }, { headers: corsHeaders })
|
||||
}
|
||||
|
||||
// Register a new terminal
|
||||
if (url.pathname === '/register-terminal' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await req.json() as TerminalRegistryEntry
|
||||
if (!body.ephemeralSessionId) {
|
||||
return Response.json({ error: 'ephemeralSessionId required' }, { status: 400, headers: corsHeaders })
|
||||
}
|
||||
terminalRegistry.set(body.ephemeralSessionId, {
|
||||
ephemeralSessionId: body.ephemeralSessionId,
|
||||
transcriptSessionId: body.transcriptSessionId || '',
|
||||
agent: body.agent || '',
|
||||
label: body.label || '',
|
||||
command: body.command || '',
|
||||
createdAt: body.createdAt || new Date().toISOString()
|
||||
})
|
||||
console.log(`[Terminal] Registered terminal: ${body.ephemeralSessionId} → ${body.transcriptSessionId} (${body.agent})`)
|
||||
broadcastRegistryChange()
|
||||
return Response.json({ success: true }, { headers: corsHeaders })
|
||||
} catch (e: any) {
|
||||
return Response.json({ error: e.message }, { status: 400, headers: corsHeaders })
|
||||
}
|
||||
}
|
||||
|
||||
// Update a registered terminal (e.g. re-key transcriptSessionId, update label)
|
||||
if (url.pathname === '/update-terminal' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await req.json() as Partial<TerminalRegistryEntry> & { ephemeralSessionId: string }
|
||||
const entry = terminalRegistry.get(body.ephemeralSessionId)
|
||||
if (!entry) {
|
||||
return Response.json({ error: 'Not found' }, { status: 404, headers: corsHeaders })
|
||||
}
|
||||
if (body.transcriptSessionId !== undefined) entry.transcriptSessionId = body.transcriptSessionId
|
||||
if (body.label !== undefined) entry.label = body.label
|
||||
if (body.agent !== undefined) entry.agent = body.agent
|
||||
if (body.command !== undefined) entry.command = body.command
|
||||
broadcastRegistryChange()
|
||||
return Response.json({ success: true }, { headers: corsHeaders })
|
||||
} catch (e: any) {
|
||||
return Response.json({ error: e.message }, { status: 400, headers: corsHeaders })
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister a terminal (does NOT kill the PTY — use /kill-session for that)
|
||||
if (url.pathname === '/unregister-terminal' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await req.json() as { ephemeralSessionId: string }
|
||||
const deleted = terminalRegistry.delete(body.ephemeralSessionId)
|
||||
if (deleted) broadcastRegistryChange()
|
||||
return Response.json({ success: true, deleted }, { headers: corsHeaders })
|
||||
} catch (e: any) {
|
||||
return Response.json({ error: e.message }, { status: 400, headers: corsHeaders })
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a WebSocket upgrade request
|
||||
const upgradeHeader = req.headers.get('upgrade')
|
||||
console.log(`[Terminal] Request: ${req.method} ${url.pathname}, Upgrade: ${upgradeHeader}`)
|
||||
|
||||
Reference in New Issue
Block a user