feat: centralize session state on terminal server
- Add SessionStateManager (server/services/session-state.ts) as source of truth for agent status, tools, approvals, and notifications - Integrate into terminal server with state patches broadcast via WS - Add /add-approval and /resolve-approval endpoints so approval lifecycle is tracked centrally and broadcast to all clients - Add permissionMode field to AgentSessionState - Frontend store (session-state.ts) + WS service (session-state-ws.ts) consume snapshots and patches from terminal server (4103) - Rewrite useGlobalApproval to derive pending approvals from centralized state — resolving on one client now clears all others - Migrate useTranscriptDebug: processing, hookMeta, serverRegistry now derived from session state store; remove 5s registry polling - hooks-approval.ts notifies terminal server on add/resolve
This commit is contained in:
76
frontend/src/services/session-state-ws.ts
Normal file
76
frontend/src/services/session-state-ws.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { endpoints } from '@/config/endpoints'
|
||||
import { useSessionState } from '@/stores/session-state'
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let reconnectDelay = 1000
|
||||
const MAX_RECONNECT_DELAY = 15000
|
||||
|
||||
function connect() {
|
||||
const store = useSessionState()
|
||||
|
||||
// Connect to terminal server (4103) — same base as terminal WS
|
||||
const url = endpoints.terminal
|
||||
console.log('[SessionStateWS] Connecting to', url)
|
||||
|
||||
ws = new WebSocket(url)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[SessionStateWS] Connected')
|
||||
store.setConnected(true)
|
||||
reconnectDelay = 1000
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.type === 'session-state-snapshot' || msg.type === 'session-state-patch' || msg.type === 'terminal-registry-change') {
|
||||
store.handleMessage(msg)
|
||||
}
|
||||
// Ignore other message types (terminal output, legacy broadcasts, etc.)
|
||||
} catch {
|
||||
// Non-JSON or malformed — ignore
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[SessionStateWS] Disconnected, reconnecting in', reconnectDelay, 'ms')
|
||||
store.setConnected(false)
|
||||
ws = null
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
// onclose will fire after onerror
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null
|
||||
connect()
|
||||
// Exponential backoff
|
||||
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY)
|
||||
}, reconnectDelay)
|
||||
}
|
||||
|
||||
/** Initialize the session state WebSocket. Call once on app mount. */
|
||||
export function initSessionStateWS() {
|
||||
if (ws) return // Already connected
|
||||
connect()
|
||||
}
|
||||
|
||||
/** Disconnect and stop reconnecting. */
|
||||
export function destroySessionStateWS() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
if (ws) {
|
||||
ws.onclose = null // Prevent reconnect
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
useSessionState().setConnected(false)
|
||||
}
|
||||
Reference in New Issue
Block a user