Add refreshAgentState() to session-state store that fetches fresh state
(hookHistory, status, etc.) via GET /session-state/{agent} from the
terminal server. Called in switchToTerminal() to ensure the UI shows
accurate hook counts when changing between terminals.
249 lines
6.1 KiB
TypeScript
249 lines
6.1 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { terminalApiUrl } from '../config/endpoints'
|
|
|
|
// ── 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<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 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 })
|
|
})
|
|
}
|
|
|
|
async function refreshAgentState(agent: string) {
|
|
try {
|
|
const res = await fetch(terminalApiUrl(`/session-state/${agent}`))
|
|
if (!res.ok) return
|
|
const state = await res.json() as AgentSessionState
|
|
agents.value = { ...agents.value, [agent]: state }
|
|
lastUpdate.value = Date.now()
|
|
} catch { /* silent — WS patches will catch up */ }
|
|
}
|
|
|
|
function setConnected(value: boolean) {
|
|
connected.value = value
|
|
}
|
|
|
|
return {
|
|
agents,
|
|
terminalRegistry,
|
|
connected,
|
|
lastUpdate,
|
|
agentList,
|
|
agentNames,
|
|
getAgent,
|
|
anyPendingApprovals,
|
|
totalPendingApprovals,
|
|
allPendingApprovals,
|
|
isProcessing,
|
|
hasErrors,
|
|
visibleNotifications,
|
|
handleMessage,
|
|
refreshAgentState,
|
|
respondApproval,
|
|
respondPlanApproval,
|
|
setConnected,
|
|
}
|
|
})
|