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:
2026-02-20 21:06:20 -06:00
parent 15731b8f69
commit 9945be07b1
8 changed files with 868 additions and 223 deletions

View File

@@ -1,5 +1,5 @@
import { jsonResponse, errorResponse } from '../utils/cors'
import { PORT_GIT } from '../config'
import { PORT_GIT, PORT_TERMINAL } from '../config'
// ── Types ──
@@ -70,6 +70,23 @@ function generateId(prefix: string): string {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
}
// Notify terminal server (4103) about approval lifecycle → broadcasts state patches to all clients
function notifyAddApproval(agent: string, approval: Record<string, unknown>) {
fetch(`http://localhost:${PORT_TERMINAL}/add-approval`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent, approval })
}).catch(e => console.error('[HooksApproval] Failed to notify add-approval:', e.message))
}
function notifyResolveApproval(requestId: string, decision: string) {
fetch(`http://localhost:${PORT_TERMINAL}/resolve-approval`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requestId, decision })
}).catch(e => console.error('[HooksApproval] Failed to notify resolve-approval:', e.message))
}
// ── Permission (PreToolUse hook) ──
export async function handleHooksApprovalPermission(req: Request): Promise<Response | null> {
@@ -92,6 +109,16 @@ export async function handleHooksApprovalPermission(req: Request): Promise<Respo
timestamp: Date.now()
}))
// Track in centralized session state → broadcasts patch to all clients
notifyAddApproval(body.agent_name || 'main', {
requestId,
type: 'permission',
toolName: body.tool_name,
toolInput: body.tool_input,
cwd: body.cwd,
timestamp: Date.now()
})
// Long-poll: wait for UI decision or timeout
const result = await new Promise<unknown>((resolve) => {
const timer = setTimeout(() => {
@@ -161,6 +188,14 @@ export async function handleHooksApprovalPlan(req: Request): Promise<Response |
timestamp: Date.now()
}))
// Track in centralized session state → broadcasts patch to all clients
notifyAddApproval('main', {
requestId,
type: 'plan',
lastAssistantText,
timestamp: Date.now()
})
// Long-poll
const result = await new Promise<unknown>((resolve) => {
const timer = setTimeout(() => {
@@ -216,6 +251,9 @@ export async function handleHooksApprovalRespond(req: Request): Promise<Response
clearTimeout(pending.timer)
pendingRequests.delete(body.requestId)
// Notify all clients that this approval was resolved
notifyResolveApproval(body.requestId, body.decision)
// Build hookSpecificOutput per PermissionRequest docs
let hookOutput: Record<string, unknown>
@@ -283,6 +321,9 @@ export async function handleHooksApprovalRespondPlan(req: Request): Promise<Resp
clearTimeout(pending.timer)
pendingRequests.delete(body.requestId)
// Notify all clients that this approval was resolved
notifyResolveApproval(body.requestId, body.decision)
// Build Stop hook output per docs:
// "approve" plan = "block" stop (so Claude continues to implement)
// "reject" plan = let Claude stop (empty response)