feat(web): add session timeline#1788
Conversation
Greptile SummaryThis PR adds a session timeline to the session detail page, exposing a merged view of activity events (from SQLite via a new
Confidence Score: 4/5The feature is well-contained and degrades gracefully; the main rough edges are in the category/filter design rather than correctness. The API route and type definitions are solid. The timeline component works correctly for the six documented filter categories. The gap is that packages/web/src/components/SessionTimeline.tsx and packages/web/src/app/globals.css for the missing user_action category handling.
|
| Filename | Overview |
|---|---|
| packages/web/src/components/SessionTimeline.tsx | New timeline component; user_action category is unfilteratable and has no CSS marker, and the one-shot fetch goes stale for active sessions. |
| packages/web/src/app/api/sessions/[id]/events/route.ts | New GET endpoint; limit clamping, session existence check, error handling, and observability recording all look correct. |
| packages/web/src/app/globals.css | Adds timeline CSS; missing marker rules for user_action and other categories, leaving their dot colors undefined. |
| packages/web/src/lib/types.ts | Adds DashboardTimelineCategory, DashboardActivityEvent, and DashboardTimelineEvent types; definitions are clean and well-structured. |
| packages/web/src/tests/api-routes.test.ts | Adds test coverage for the new events route including JSON parsing, limit clamping, and 404 for unknown sessions; all correct. |
| packages/web/src/components/tests/SessionDetail.desktop.test.tsx | Extends the desktop layout test with timeline rendering and filter interaction; fetch mock is correctly gated by URL to avoid interfering with other tests. |
| packages/web/src/components/SessionDetail.tsx | Minimal change: adds SessionTimeline below the existing content area. |
| docs/observability.md | Adds documentation entries for the session timeline and /api/sessions/:id/events endpoint; accurate and concise. |
Sequence Diagram
sequenceDiagram
participant Browser
participant SessionDetail
participant SessionTimeline
participant EventsAPI as GET /api/sessions/:id/events
participant SessionManager
participant ActivityDB as SQLite (queryActivityEvents)
Browser->>SessionDetail: mount
SessionDetail->>SessionTimeline: render(session)
SessionTimeline->>EventsAPI: "fetch /api/sessions/:id/events?limit=80"
EventsAPI->>SessionManager: get(id)
SessionManager-->>EventsAPI: session or null
alt session not found
EventsAPI-->>SessionTimeline: 404
SessionTimeline->>SessionTimeline: setLoadState(error)
else session found
EventsAPI->>ActivityDB: "queryActivityEvents({projectId, sessionId, limit})"
ActivityDB-->>EventsAPI: raw events[]
EventsAPI->>EventsAPI: parseEventData per event
EventsAPI-->>SessionTimeline: "200 { events[] }"
SessionTimeline->>SessionTimeline: mergeTimelineEvents + sort desc
SessionTimeline->>SessionTimeline: render
end
Browser->>SessionTimeline: click filter chip
SessionTimeline->>SessionTimeline: "visibleEvents = filter(category)"
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
packages/web/src/components/SessionTimeline.tsx:30-38
**user_action events silently disappear under any specific filter**
`categoryForActivityEvent` can return `"user_action"` for `session.killed`, `session.spawn_started`, `session.spawned`, and any event sourced from `"api"` or `"ui"`. Because `"user_action"` is absent from `FILTERS`, these events are visible only in the "All" view and are completely hidden the moment an operator clicks any specific filter button (Lifecycle, PR/CI, etc.). A `session.killed` event — one of the most operationally significant state changes — can't be surfaced when the Lifecycle filter is active.
### Issue 2 of 4
packages/web/src/app/globals.css:2508-2527
**Missing CSS marker classes for `user_action` and `other` categories**
The CSS defines colored marker rules for the six filter-visible categories (`lifecycle`, `agent_report`, `pr`, `reaction`, `runtime`, `error`) but not for `user_action` or `other`. A timeline item rendered with class `session-timeline__item--user_action` has no matching rule, so its dot falls through to the default muted color, making it visually indistinguishable from the bare `other` bucket even though `user_action` events represent intentional operator actions.
### Issue 3 of 4
packages/web/src/components/SessionTimeline.tsx:156-174
**Timeline data goes stale for active sessions**
The `useEffect` fetches activity events once on mount and again only when `session.id` changes. For a long-running session that generates many new events after the page opens, the timeline won't update without a full page reload. The SSE-driven session state displayed above the timeline can advance through multiple lifecycle states while the timeline remains frozen at its initial snapshot. Polling (or re-using the existing SSE channel) would keep the timeline current without a user action.
### Issue 4 of 4
packages/web/src/components/SessionTimeline.tsx:25
`event.kind.endsWith("_failed")` is fully subsumed by the broader `event.kind.includes("failed")` check on the same line. The `endsWith` branch is dead code and can be removed for clarity.
```suggestion
if (event.level === "error" || event.kind.includes("failed")) {
```
Reviews (1): Last reviewed commit: "feat(web): add session timeline" | Re-trigger Greptile
| if ( | ||
| event.source === "api" || | ||
| event.source === "ui" || | ||
| event.kind === "session.killed" || | ||
| event.kind === "session.spawn_started" || | ||
| event.kind === "session.spawned" | ||
| ) { | ||
| return "user_action"; | ||
| } |
There was a problem hiding this comment.
user_action events silently disappear under any specific filter
categoryForActivityEvent can return "user_action" for session.killed, session.spawn_started, session.spawned, and any event sourced from "api" or "ui". Because "user_action" is absent from FILTERS, these events are visible only in the "All" view and are completely hidden the moment an operator clicks any specific filter button (Lifecycle, PR/CI, etc.). A session.killed event — one of the most operationally significant state changes — can't be surfaced when the Lifecycle filter is active.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/web/src/components/SessionTimeline.tsx
Line: 30-38
Comment:
**user_action events silently disappear under any specific filter**
`categoryForActivityEvent` can return `"user_action"` for `session.killed`, `session.spawn_started`, `session.spawned`, and any event sourced from `"api"` or `"ui"`. Because `"user_action"` is absent from `FILTERS`, these events are visible only in the "All" view and are completely hidden the moment an operator clicks any specific filter button (Lifecycle, PR/CI, etc.). A `session.killed` event — one of the most operationally significant state changes — can't be surfaced when the Lifecycle filter is active.
How can I resolve this? If you propose a fix, please make it concise.| grid-template-columns: 8px minmax(0, 1fr); | ||
| gap: 10px; | ||
| } | ||
|
|
||
| .session-timeline__marker { | ||
| width: 7px; | ||
| height: 7px; | ||
| margin-top: 6px; | ||
| border-radius: 999px; | ||
| background: var(--color-text-muted); | ||
| } | ||
|
|
||
| .session-timeline__item--lifecycle .session-timeline__marker { | ||
| background: var(--color-status-ready); | ||
| } | ||
|
|
||
| .session-timeline__item--agent_report .session-timeline__marker { | ||
| background: var(--color-status-working); | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing CSS marker classes for
user_action and other categories
The CSS defines colored marker rules for the six filter-visible categories (lifecycle, agent_report, pr, reaction, runtime, error) but not for user_action or other. A timeline item rendered with class session-timeline__item--user_action has no matching rule, so its dot falls through to the default muted color, making it visually indistinguishable from the bare other bucket even though user_action events represent intentional operator actions.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/web/src/app/globals.css
Line: 2508-2527
Comment:
**Missing CSS marker classes for `user_action` and `other` categories**
The CSS defines colored marker rules for the six filter-visible categories (`lifecycle`, `agent_report`, `pr`, `reaction`, `runtime`, `error`) but not for `user_action` or `other`. A timeline item rendered with class `session-timeline__item--user_action` has no matching rule, so its dot falls through to the default muted color, making it visually indistinguishable from the bare `other` bucket even though `user_action` events represent intentional operator actions.
How can I resolve this? If you propose a fix, please make it concise.| if (cancelled) return; | ||
| setActivityEvents(Array.isArray(payload.events) ? payload.events : []); | ||
| setLoadState("ready"); | ||
| }) | ||
| .catch(() => { | ||
| if (cancelled) return; | ||
| setActivityEvents([]); | ||
| setLoadState("error"); | ||
| }); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [session.id]); | ||
|
|
||
| const timelineEvents = useMemo( | ||
| () => mergeTimelineEvents(activityEvents, session.agentReportAudit ?? []), | ||
| [activityEvents, session.agentReportAudit], | ||
| ); | ||
| const visibleEvents = useMemo( |
There was a problem hiding this comment.
Timeline data goes stale for active sessions
The useEffect fetches activity events once on mount and again only when session.id changes. For a long-running session that generates many new events after the page opens, the timeline won't update without a full page reload. The SSE-driven session state displayed above the timeline can advance through multiple lifecycle states while the timeline remains frozen at its initial snapshot. Polling (or re-using the existing SSE channel) would keep the timeline current without a user action.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/web/src/components/SessionTimeline.tsx
Line: 156-174
Comment:
**Timeline data goes stale for active sessions**
The `useEffect` fetches activity events once on mount and again only when `session.id` changes. For a long-running session that generates many new events after the page opens, the timeline won't update without a full page reload. The SSE-driven session state displayed above the timeline can advance through multiple lifecycle states while the timeline remains frozen at its initial snapshot. Polling (or re-using the existing SSE channel) would keep the timeline current without a user action.
How can I resolve this? If you propose a fix, please make it concise.| ]; | ||
|
|
||
| function categoryForActivityEvent(event: DashboardActivityEvent): DashboardTimelineCategory { | ||
| if (event.level === "error" || event.kind.endsWith("_failed") || event.kind.includes("failed")) { |
There was a problem hiding this comment.
event.kind.endsWith("_failed") is fully subsumed by the broader event.kind.includes("failed") check on the same line. The endsWith branch is dead code and can be removed for clarity.
| if (event.level === "error" || event.kind.endsWith("_failed") || event.kind.includes("failed")) { | |
| if (event.level === "error" || event.kind.includes("failed")) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/web/src/components/SessionTimeline.tsx
Line: 25
Comment:
`event.kind.endsWith("_failed")` is fully subsumed by the broader `event.kind.includes("failed")` check on the same line. The `endsWith` branch is dead code and can be removed for clarity.
```suggestion
if (event.level === "error" || event.kind.includes("failed")) {
```
How can I resolve this? If you propose a fix, please make it concise.
Problem
Session detail showed the current state well, but operators still had to stitch together
ao events, metadata files, observability output, and agent-report audit entries to understand why a session changed state.What changed
GET /api/sessions/:id/eventsto expose recent activity events for one session, with a clampedlimitand session existence check.docs/observability.md.Why this approach
This reuses the existing SQLite activity-event log and agent-report audit trail instead of introducing new storage or changing lifecycle behavior. The UI is best-effort: if the activity DB is unavailable, the session page still renders and can show audit entries or an empty state.
Testing
pnpm --filter @aoagents/ao-web test -- SessionDetail.desktop api-routespnpm exec eslint packages/web/src/components/SessionTimeline.tsx packages/web/src/components/SessionDetail.tsx 'packages/web/src/app/api/sessions/[id]/events/route.ts' packages/web/src/lib/types.ts packages/web/src/__tests__/api-routes.test.ts packages/web/src/components/__tests__/SessionDetail.desktop.test.tsxpnpm exec prettier --check docs/observability.md packages/web/src/components/SessionTimeline.tsx packages/web/src/components/SessionDetail.tsx 'packages/web/src/app/api/sessions/[id]/events/route.ts' packages/web/src/lib/types.ts packages/web/src/__tests__/api-routes.test.ts packages/web/src/components/__tests__/SessionDetail.desktop.test.tsxNote:
pnpm --filter @aoagents/ao-web typecheckis blocked in this local checkout by a missing@aoagents/ao-plugin-runtime-processbuild artifact /node-ptyinstall (Cannot find module '@aoagents/ao-plugin-runtime-process'). The new timeline code type error found during the first run was fixed before pushing.