From b9eec1013b186c1161c7eb5e15a02381adae02f8 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 21 Feb 2026 04:29:02 -0600 Subject: [PATCH] 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. --- .../transcript-debug/ChatContainer.vue | 7 + .../SessionLifecycleStatus.vue | 142 +++++++++++++++++- frontend/src/stores/session-state.ts | 7 + server/services/session-state.ts | 48 +++++- 4 files changed, 200 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/transcript-debug/ChatContainer.vue b/frontend/src/components/transcript-debug/ChatContainer.vue index 7879a83..1c59c43 100644 --- a/frontend/src/components/transcript-debug/ChatContainer.vue +++ b/frontend/src/components/transcript-debug/ChatContainer.vue @@ -89,6 +89,12 @@ const lifecycleDetail = computed(() => { return sessionStore.agents[agent]?.lastHookDetail ?? '' }) +const hookHistory = computed(() => { + const agent = props.selectedAgent + if (!agent) return [] + return sessionStore.agents[agent]?.hookHistory ?? [] +}) + // ── Derived display values ── const permissionMode = computed(() => props.hookPermissionMode || '') const fullCwd = computed(() => props.conversation.metadata.cwd || '') @@ -664,6 +670,7 @@ function formatDuration(start: string, end: string): string { = { SessionEnd: { color: '#6b7280', label: 'Session ended', category: 'session' }, } +const CATEGORY_ORDER: Record = { + session: 0, user: 1, tool: 2, agent: 3, system: 4 +} + interface MockStep { event: LifecycleEvent duration: number @@ -69,14 +73,28 @@ const MOCK_SEQUENCE: MockStep[] = [ { event: 'SessionEnd', duration: 4000 }, ] +interface HookHistoryEntry { + event: string + timestamp: number + detail?: string +} + +interface EventCountEntry { + event: LifecycleEvent + count: number + color: string +} + const props = defineProps<{ currentEvent?: string | null eventDetail?: string + hookHistory?: HookHistoryEntry[] }>() const isMock = computed(() => !props.currentEvent || !(props.currentEvent in LIFECYCLE_DISPLAY)) const mockIndex = ref(0) +const mockHistory = ref<{ event: string }[]>([]) let mockTimer: ReturnType | null = null const activeEvent = computed(() => { @@ -92,8 +110,53 @@ const activeDetail = computed(() => { const displayInfo = computed(() => LIFECYCLE_DISPLAY[activeEvent.value]) const isProcessing = computed(() => !!displayInfo.value.processing) +// ── Badge counts ── + +function countEvents(entries: { event: string }[]): EventCountEntry[] { + const counts = new Map() + for (const entry of entries) { + counts.set(entry.event, (counts.get(entry.event) || 0) + 1) + } + + const result: EventCountEntry[] = [] + for (const [event, count] of counts) { + if (event in LIFECYCLE_DISPLAY) { + result.push({ + event: event as LifecycleEvent, + count, + color: LIFECYCLE_DISPLAY[event as LifecycleEvent].color, + }) + } + } + + result.sort((a, b) => { + const ca = LIFECYCLE_DISPLAY[a.event].category + const cb = LIFECYCLE_DISPLAY[b.event].category + if (CATEGORY_ORDER[ca] !== CATEGORY_ORDER[cb]) { + return CATEGORY_ORDER[ca] - CATEGORY_ORDER[cb] + } + return a.event.localeCompare(b.event) + }) + + return result +} + +const realCounts = computed(() => countEvents(props.hookHistory || [])) +const mockCounts = computed(() => countEvents(mockHistory.value)) +const displayCounts = computed(() => isMock.value ? mockCounts.value : realCounts.value) + +// ── Mock mode ── + function advanceMock() { - mockIndex.value = (mockIndex.value + 1) % MOCK_SEQUENCE.length + const nextIndex = (mockIndex.value + 1) % MOCK_SEQUENCE.length + const nextEvent = MOCK_SEQUENCE[nextIndex].event + + if (nextEvent === 'SessionStart') { + mockHistory.value = [] + } + + mockHistory.value = [...mockHistory.value, { event: nextEvent }] + mockIndex.value = nextIndex scheduleMock() } @@ -104,6 +167,7 @@ function scheduleMock() { watch(isMock, (mock) => { if (mock) { + mockHistory.value = [{ event: MOCK_SEQUENCE[mockIndex.value].event }] scheduleMock() } else if (mockTimer) { clearTimeout(mockTimer) @@ -112,7 +176,10 @@ watch(isMock, (mock) => { }) onMounted(() => { - if (isMock.value) scheduleMock() + if (isMock.value) { + mockHistory.value = [{ event: MOCK_SEQUENCE[mockIndex.value].event }] + scheduleMock() + } }) onBeforeUnmount(() => { @@ -125,6 +192,24 @@ onBeforeUnmount(() => {