Skip to content

feat(web): add session timeline#1788

Open
ChiragArora31 wants to merge 1 commit intoComposioHQ:mainfrom
ChiragArora31:feat/session-timeline
Open

feat(web): add session timeline#1788
ChiragArora31 wants to merge 1 commit intoComposioHQ:mainfrom
ChiragArora31:feat/session-timeline

Conversation

@ChiragArora31
Copy link
Copy Markdown
Contributor

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

  • Added GET /api/sessions/:id/events to expose recent activity events for one session, with a clamped limit and session existence check.
  • Added a Session Detail timeline that merges activity events with the existing agent-report audit trail.
  • Added category filters for lifecycle, reports, PR/CI, reactions, runtime, and errors.
  • Documented the new timeline and events API in 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-routes
  • pnpm 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.tsx
  • pnpm 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.tsx

Note: pnpm --filter @aoagents/ao-web typecheck is blocked in this local checkout by a missing @aoagents/ao-plugin-runtime-process build artifact / node-pty install (Cannot find module '@aoagents/ao-plugin-runtime-process'). The new timeline code type error found during the first run was fixed before pushing.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR adds a session timeline to the session detail page, exposing a merged view of activity events (from SQLite via a new GET /api/sessions/:id/events endpoint) and the existing agent-report audit trail, with category filters and graceful degradation if the activity DB is unavailable.

  • New API route (route.ts): limit clamping to 200, session existence guard, JSON-string → object deserialization, and observability recording are all handled correctly.
  • SessionTimeline component: merges and sorts events client-side, renders with filter chips and relative timestamps; the user_action category is reachable by categoryForActivityEvent but has no filter button and no CSS marker rule, so events like session.killed are invisible under any specific filter and visually undifferentiated from the other bucket.
  • Timeline fetch: data is loaded once on mount with no subsequent refresh, so the view goes stale for long-running sessions without a page reload.

Confidence Score: 4/5

The 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 user_action events (session.killed, session.spawned, api/ui-sourced events) are reachable by the categorizer but have no filter button and no CSS marker class, meaning they silently vanish under every specific filter and look identical to uncategorised other events. The timeline also fetches once on mount with no refresh, so an active session's timeline goes stale while the rest of the page continues to update via SSE.

packages/web/src/components/SessionTimeline.tsx and packages/web/src/app/globals.css for the missing user_action category handling.

Important Files Changed

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)"
Loading
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

Comment on lines +30 to +38
if (
event.source === "api" ||
event.source === "ui" ||
event.kind === "session.killed" ||
event.kind === "session.spawn_started" ||
event.kind === "session.spawned"
) {
return "user_action";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +2508 to +2527
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +156 to +174
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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant