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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user