- 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
209 lines
5.1 KiB
TypeScript
209 lines
5.1 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
|
|
// ── Types (mirror server/services/session-state.ts) ──
|
|
|
|
export type AgentStatus =
|
|
| 'idle'
|
|
| 'processing'
|
|
| 'reading'
|
|
| 'writing'
|
|
| 'toolUse'
|
|
| 'toolDone'
|
|
| 'permissionRequest'
|
|
| 'notification'
|
|
| 'sessionStart'
|
|
|
|
export interface ActiveTool {
|
|
name: string
|
|
input: unknown
|
|
startedAt: number
|
|
}
|
|
|
|
export interface PendingApproval {
|
|
requestId: string
|
|
type: 'permission' | 'plan'
|
|
toolName?: string
|
|
toolInput?: unknown
|
|
cwd?: string
|
|
lastAssistantText?: string
|
|
timestamp: number
|
|
}
|
|
|
|
export interface SessionNotification {
|
|
id: string
|
|
event: string
|
|
title: string
|
|
detail: string
|
|
type: 'info' | 'success' | 'warning' | 'error'
|
|
timestamp: number
|
|
persistent: boolean
|
|
expiresAt: number | null
|
|
}
|
|
|
|
export interface AgentTerminalInfo {
|
|
ptySessionId: string | null
|
|
alive: boolean
|
|
bufferSize: number
|
|
connectedClients: number
|
|
}
|
|
|
|
export interface AgentSessionState {
|
|
agent: string
|
|
sessionId: string | null
|
|
model: string | null
|
|
cwd: string | null
|
|
transcriptPath: string | null
|
|
permissionMode: string | null
|
|
status: AgentStatus
|
|
currentTool: ActiveTool | null
|
|
lastActivity: number
|
|
lastStopResponse: string | null
|
|
pendingApprovals: PendingApproval[]
|
|
terminal: AgentTerminalInfo
|
|
notifications: SessionNotification[]
|
|
}
|
|
|
|
// WS message types
|
|
interface SessionStateSnapshot {
|
|
type: 'session-state-snapshot'
|
|
agents: Record<string, AgentSessionState>
|
|
}
|
|
|
|
interface SessionStatePatch {
|
|
type: 'session-state-patch'
|
|
agent: string
|
|
patch: Partial<AgentSessionState>
|
|
event: string
|
|
timestamp: number
|
|
}
|
|
|
|
interface TerminalRegistryChange {
|
|
type: 'terminal-registry-change'
|
|
registry: TerminalRegistryEntry[]
|
|
timestamp: number
|
|
}
|
|
|
|
export interface TerminalRegistryEntry {
|
|
ephemeralSessionId: string
|
|
transcriptSessionId: string
|
|
agent: string
|
|
label: string
|
|
command: string
|
|
createdAt: string
|
|
alive: boolean
|
|
clients: number
|
|
bufferSize: number
|
|
}
|
|
|
|
type SessionStateMessage = SessionStateSnapshot | SessionStatePatch | TerminalRegistryChange
|
|
|
|
// ── Store ──
|
|
|
|
export const useSessionState = defineStore('session-state', () => {
|
|
const agents = ref<Record<string, AgentSessionState>>({})
|
|
const terminalRegistry = ref<TerminalRegistryEntry[]>([])
|
|
const connected = ref(false)
|
|
const lastUpdate = ref(0)
|
|
|
|
// ── Computed helpers ──
|
|
|
|
const agentList = computed(() => Object.values(agents.value))
|
|
|
|
const agentNames = computed(() => Object.keys(agents.value))
|
|
|
|
function getAgent(name: string) {
|
|
return computed(() => agents.value[name] ?? null)
|
|
}
|
|
|
|
const anyPendingApprovals = computed(() =>
|
|
agentList.value.some(a => a.pendingApprovals.length > 0)
|
|
)
|
|
|
|
const totalPendingApprovals = computed(() =>
|
|
agentList.value.reduce((sum, a) => sum + a.pendingApprovals.length, 0)
|
|
)
|
|
|
|
const allPendingApprovals = computed(() =>
|
|
agentList.value.flatMap(a =>
|
|
a.pendingApprovals.map(p => ({ ...p, agent: a.agent }))
|
|
)
|
|
)
|
|
|
|
const visibleNotifications = computed(() => {
|
|
const now = Date.now()
|
|
return agentList.value
|
|
.flatMap(a => a.notifications.map(n => ({ ...n, agent: a.agent })))
|
|
.filter(n => n.persistent || !n.expiresAt || n.expiresAt > now)
|
|
.sort((a, b) => b.timestamp - a.timestamp)
|
|
.slice(0, 10)
|
|
})
|
|
|
|
// ── Message handler ──
|
|
|
|
function handleMessage(msg: SessionStateMessage) {
|
|
if (msg.type === 'session-state-snapshot') {
|
|
agents.value = { ...msg.agents }
|
|
lastUpdate.value = Date.now()
|
|
} else if (msg.type === 'session-state-patch') {
|
|
const current = agents.value[msg.agent]
|
|
if (current) {
|
|
agents.value = {
|
|
...agents.value,
|
|
[msg.agent]: { ...current, ...msg.patch }
|
|
}
|
|
} else {
|
|
// New agent appeared — treat patch as full state if it has required fields
|
|
agents.value = {
|
|
...agents.value,
|
|
[msg.agent]: msg.patch as AgentSessionState
|
|
}
|
|
}
|
|
lastUpdate.value = msg.timestamp
|
|
} else if (msg.type === 'terminal-registry-change') {
|
|
terminalRegistry.value = msg.registry
|
|
lastUpdate.value = msg.timestamp
|
|
}
|
|
}
|
|
|
|
// ── Actions ──
|
|
|
|
async function respondApproval(requestId: string, decision: string, reason?: string) {
|
|
await fetch('/api/hooks-approval/respond', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ requestId, decision, reason })
|
|
})
|
|
}
|
|
|
|
async function respondPlanApproval(requestId: string, decision: string, reason?: string) {
|
|
await fetch('/api/hooks-approval/respond-plan', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ requestId, decision, reason })
|
|
})
|
|
}
|
|
|
|
function setConnected(value: boolean) {
|
|
connected.value = value
|
|
}
|
|
|
|
return {
|
|
agents,
|
|
terminalRegistry,
|
|
connected,
|
|
lastUpdate,
|
|
agentList,
|
|
agentNames,
|
|
getAgent,
|
|
anyPendingApprovals,
|
|
totalPendingApprovals,
|
|
allPendingApprovals,
|
|
visibleNotifications,
|
|
handleMessage,
|
|
respondApproval,
|
|
respondPlanApproval,
|
|
setConnected,
|
|
}
|
|
})
|