feat: add hook event history with badge counts in SessionLifecycleStatus

Server persists hookHistory[] per agent (cap 500, resets on SessionStart),
synced realtime via session-state-patch. Frontend computes event counts
by macro type and renders color-coded badges at the ribbon start.
Mock mode also accumulates badges during demo sequence.
This commit is contained in:
2026-02-21 04:29:02 -06:00
parent 638b449f08
commit b9eec1013b
4 changed files with 200 additions and 4 deletions

View File

@@ -32,6 +32,12 @@ export interface PendingApproval {
timestamp: number
}
export interface HookHistoryEntry {
event: string
timestamp: number
detail?: string
}
export interface SessionNotification {
id: string
event: string
@@ -72,6 +78,9 @@ export interface AgentSessionState {
pendingApprovals: PendingApproval[]
terminal: AgentTerminalInfo
notifications: SessionNotification[]
hookHistory: HookHistoryEntry[]
lastHookEvent: string | null
lastHookDetail: string | null
}
export interface HookPayload {
@@ -114,6 +123,7 @@ export interface SessionStatePatch {
// ── Notification builders ──
const MAX_NOTIFICATIONS = 30
const MAX_HOOK_HISTORY = 500
const TTL_INFO = 3500
const TTL_WARNING = 5000
@@ -219,6 +229,9 @@ function createDefaultState(agent: string): AgentSessionState {
connectedClients: 0,
},
notifications: [],
hookHistory: [],
lastHookEvent: null,
lastHookDetail: null,
}
}
@@ -236,7 +249,7 @@ class SessionStateManager {
/** Process a raw hook event and return the patch to broadcast */
processHookEvent(payload: HookPayload): SessionStatePatch {
const agentName = payload.agent_name || 'main'
const agentName = payload.agent_name || 'claude'
const state = this.getOrCreateAgent(agentName)
const now = Date.now()
@@ -246,6 +259,8 @@ class SessionStateManager {
// Build patch
const patch: Partial<AgentSessionState> = {
lastActivity: now,
lastHookEvent: payload.hook_event_name || null,
lastHookDetail: payload.tool_name || payload.message || null,
}
// Only update status/tool if deriveStatus returned a result
@@ -310,6 +325,20 @@ class SessionStateManager {
const updatedNotifications = [...state.notifications, notification].slice(-MAX_NOTIFICATIONS)
patch.notifications = updatedNotifications
// Build hook history entry
const historyEntry: HookHistoryEntry = {
event: payload.hook_event_name || 'unknown',
timestamp: now,
}
const historyDetail = payload.tool_name || payload.message
if (historyDetail) historyEntry.detail = historyDetail as string
if (payload.hook_event_name === 'SessionStart') {
patch.hookHistory = [historyEntry]
} else {
patch.hookHistory = [...state.hookHistory, historyEntry].slice(-MAX_HOOK_HISTORY)
}
// Apply patch to state
Object.assign(state, patch)
@@ -372,6 +401,23 @@ class SessionStateManager {
return this.agents.get(agent) || null
}
/** Find agent name by session_id */
findAgentBySessionId(sessionId: string): string | null {
for (const [name, state] of this.agents) {
if (state.sessionId === sessionId) return name
}
return null
}
/** Find all agents matching a transcript_path */
findAgentsByTranscript(transcriptPath: string): string[] {
const matches: string[] = []
for (const [name, state] of this.agents) {
if (state.transcriptPath === transcriptPath) matches.push(name)
}
return matches
}
/** Clean up expired notifications (call periodically) */
cleanExpiredNotifications(): void {
const now = Date.now()