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