+
+
+
+ {{ entry.count }}
+
+
+
+
{
.lifecycle-ribbon.category-agent { border-top-color: rgba(192, 132, 252, 0.15); }
.lifecycle-ribbon.category-system { border-top-color: rgba(56, 189, 248, 0.15); }
+/* ── Badges ── */
+
+.lc-badges {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ flex-shrink: 0;
+ margin-right: 4px;
+ overflow: hidden;
+}
+
+.lc-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 12px;
+ height: 12px;
+ padding: 0 2px;
+ font-size: 8px;
+ font-weight: 700;
+ font-family: 'Courier New', monospace;
+ border: 1px solid;
+ border-radius: 2px;
+ line-height: 1;
+ white-space: nowrap;
+}
+
+.badge-enter-active {
+ transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+.badge-leave-active {
+ transition: opacity 0.15s ease;
+}
+
+.badge-enter-from {
+ opacity: 0;
+ transform: scale(0.7);
+}
+
+.badge-leave-to {
+ opacity: 0;
+}
+
+.badge-move {
+ transition: transform 0.2s ease;
+}
+
+/* ── Current event ── */
+
.lc-content {
display: flex;
align-items: center;
gap: 5px;
- width: 100%;
+ flex: 1;
+ min-width: 0;
}
.lc-dot {
diff --git a/frontend/src/stores/session-state.ts b/frontend/src/stores/session-state.ts
index 90d7467..43ee0cd 100644
--- a/frontend/src/stores/session-state.ts
+++ b/frontend/src/stores/session-state.ts
@@ -31,6 +31,12 @@ export interface PendingApproval {
timestamp: number
}
+export interface HookHistoryEntry {
+ event: string
+ timestamp: number
+ detail?: string
+}
+
export interface SessionNotification {
id: string
event: string
@@ -71,6 +77,7 @@ export interface AgentSessionState {
pendingApprovals: PendingApproval[]
terminal: AgentTerminalInfo
notifications: SessionNotification[]
+ hookHistory: HookHistoryEntry[]
lastHookEvent: string | null
lastHookDetail: string | null
}
diff --git a/server/services/session-state.ts b/server/services/session-state.ts
index 15ff32f..8513585 100644
--- a/server/services/session-state.ts
+++ b/server/services/session-state.ts
@@ -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 = {
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()