import { defineStore } from 'pinia' import { ref, computed } from 'vue' // ── Types (mirror server/services/session-state.ts) ── export type AgentStatus = | 'idle' | 'thinking' | 'reading' | 'writing' | 'toolUse' | 'permissionRequest' | 'interrupted' | 'error' | 'sessionStart' | 'sessionEnd' 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 HookHistoryEntry { event: string timestamp: number detail?: string } 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 LastError { tool: string message: string interrupted: boolean timestamp: 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 lastError: LastError | null pendingApprovals: PendingApproval[] terminal: AgentTerminalInfo notifications: SessionNotification[] hookHistory: HookHistoryEntry[] lastHookEvent: string | null lastHookDetail: string | null } // WS message types interface SessionStateSnapshot { type: 'session-state-snapshot' agents: Record } interface SessionStatePatch { type: 'session-state-patch' agent: string patch: Partial 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>({}) const terminalRegistry = ref([]) 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 isProcessing = computed(() => agentList.value.some(a => !['idle', 'sessionStart', 'sessionEnd'].includes(a.status)) ) const hasErrors = computed(() => agentList.value.some(a => a.status === 'error' || a.status === 'interrupted') ) 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, isProcessing, hasErrors, visibleNotifications, handleMessage, respondApproval, respondPlanApproval, setConnected, } })