Skip to content

feat(web): add session timeline#1788

Open
ChiragArora31 wants to merge 9 commits into
AgentWrapper:mainfrom
ChiragArora31:feat/session-timeline
Open

feat(web): add session timeline#1788
ChiragArora31 wants to merge 9 commits into
AgentWrapper: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

greptile-apps Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a session activity timeline to the session detail page, exposing a GET /api/sessions/:id/events endpoint that queries the existing SQLite activity-event log and merges it with the agent-report audit trail in a new SessionTimeline component.

  • New API route with session existence check, clamped limit (max 200), observability recording, and JSON-parsing of raw DB event data.
  • SessionTimeline component with per-category filter buttons, 5 s polling, a generation counter to prevent stale-response overwrites, and mergeTimelineEvents that sorts both event sources newest-first.
  • New types (DashboardActivityEvent, DashboardTimelineEvent, DashboardTimelineCategory), CSS classes for all eight categories, and tests covering race conditions, domain-prefixed failures, and the Actions/user_action filter path.

Confidence Score: 5/5

Safe to merge; the change is additive and degrades gracefully when the activity DB is unavailable.

The new timeline is purely additive — a fetch error leaves the existing session detail intact. The domain-before-error classification ordering is correct, all eight filter categories are present in FILTERS and have CSS marker rules, the generation counter prevents stale poll overwrites, and the route properly gates on session existence before querying. No logic defects were found.

globals.css — the shared runtime/error marker color is the only item worth revisiting before merge.

Important Files Changed

Filename Overview
packages/web/src/app/api/sessions/[id]/events/route.ts New route: session existence check, clamped limit, synchronous queryActivityEvents call, JSON-parse of raw event data, observability recording, and proper error-path fallback.
packages/web/src/components/SessionTimeline.tsx New component with 5 s polling, generation-counter race-protection, correct domain-before-error classification order, and all eight filter categories present in FILTERS array.
packages/web/src/app/globals.css Timeline CSS added; runtime and error marker rules share the same color token, making their dots visually identical to each other.
packages/web/src/lib/types.ts Three new exported types added cleanly; DashboardActivityEvent.data typed as unknown to accommodate both parsed objects and raw strings.
packages/web/src/components/tests/SessionTimeline.test.tsx New test suite covers race-condition protection, domain-prefixed failure classification, subprocess-vs-PR routing, and empty filter state messaging.
packages/web/src/components/tests/SessionDetail.desktop.test.tsx fetch mock updated to return a never-resolving Promise for /events requests in pre-existing tests; two new integration tests verify timeline rendering and the Actions filter.
packages/web/src/components/SessionDetail.tsx Single-line addition of SessionTimeline below the main session content area.
packages/web/src/tests/api-routes.test.ts Two new test cases covering successful event retrieval (with limit clamping verified) and 404 for unknown sessions.

Sequence Diagram

sequenceDiagram
    participant Browser as Browser (SessionTimeline)
    participant Route as GET /api/sessions/:id/events
    participant SM as SessionManager
    participant DB as SQLite (queryActivityEvents)
    participant SSE as SSE stream (session prop)

    Browser->>Route: "fetch /events?limit=80 (initial + every 5s)"
    Route->>SM: sessionManager.get(id)
    SM-->>Route: session (or null → 404)
    Route->>DB: "queryActivityEvents({ projectId, sessionId, limit })"
    DB-->>Route: ActivityEvent[]
    Route-->>Browser: "{ events: DashboardActivityEvent[] }"
    Browser->>Browser: mergeTimelineEvents(activityEvents, session.agentReportAudit)
    SSE-->>Browser: session.agentReportAudit updates (via parent prop)
    Browser->>Browser: useMemo recomputes timelineEvents
    Browser->>Browser: filter → visibleEvents → render list
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/web/src/app/globals.css:2536-2539
The `runtime` and `error` marker rules share the same color token (`--color-status-error`), so their dots are visually identical. An operator using the "Runtime" filter to investigate probe failures will see red dots — the same shade as "Errors" — even though these categories are now logically separated. Giving runtime events a distinct color (e.g., `--color-status-working` or a dedicated warning token) would let dot color reinforce the filter distinction.

```suggestion
.session-timeline__item--runtime .session-timeline__marker {
  background: var(--color-status-working);
}

.session-timeline__item--error .session-timeline__marker {
  background: var(--color-status-error);
}
```

### Issue 2 of 2
packages/web/src/components/SessionTimeline.tsx:12-14
`TIMELINE_LIMIT` in the component and `DEFAULT_LIMIT` in the route are the same value (80) but are separate, unlinked constants. If the component's constant is bumped without updating the route (or vice versa), the URL `?limit=N` will silently diverge from the server default that operators hitting the API directly would receive. Extracting the value to a shared location or adding an inline cross-reference comment would make the coupling explicit.

```suggestion
/** Must match DEFAULT_LIMIT in packages/web/src/app/api/sessions/[id]/events/route.ts */
const TIMELINE_LIMIT = 80;
/** Matches dashboard SSE refresh cadence so the timeline stays in sync with session state. */
const TIMELINE_REFRESH_MS = 5000;
```

Reviews (6): Last reviewed commit: "fix(web): clarify filtered timeline stat..." | Re-trigger Greptile

Comment thread packages/web/src/components/SessionTimeline.tsx
Comment thread packages/web/src/app/globals.css Outdated
Comment thread packages/web/src/components/SessionTimeline.tsx Outdated
Comment thread packages/web/src/components/SessionTimeline.tsx Outdated
ChiragArora31 and others added 2 commits May 11, 2026 21:49
- Add Actions/Other filters and marker colors for user_action and other
- Poll timeline every 5s without flickering loading state on refresh
- Remove redundant endsWith("_failed") check in category mapping

Co-authored-by: Cursor <cursoragent@cursor.com>
@ChiragArora31

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — addressed all four items in the latest push (branch rebased on current main).

Filters / markers: Added Actions and Other chips so user_action and other events are filterable, and gave each its own marker color in globals.css so operator-driven events are visually distinct.

Freshness: Timeline refetches every 5s (same cadence as session SSE) so active sessions stay current. Follow-up polls no longer flip the whole block into a loading state; only the initial load does, and a failed refresh keeps the last good snapshot.

Cleanup: Dropped the redundant endsWith("_failed") check in favor of kind.includes("failed").

Added a desktop test that kills a session via a user_action event and asserts it stays visible under Actions while lifecycle rows are filtered out.

Comment thread packages/web/src/components/SessionTimeline.tsx
Greptile: overlapping 5s fetches could apply out-of-order. Track a
monotonic generation per request and ignore completions that are not
the latest. Add SessionTimeline test covering slow-first/fast-second
resolution order.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ChiragArora31

Copy link
Copy Markdown
Contributor Author

Addressed the latest Greptile note on overlapping polls.

Each timeline fetch now captures a monotonically increasing generation; setActivityEvents / error handling only run when that generation still matches the latest-started request (in addition to the existing unmount cancelled guard). That way a slow earlier response cannot overwrite data from a newer poll.

Added SessionTimeline.test.tsx with a controlled fetch + mocked setInterval tick so the second request resolves before the first; the UI must keep the newer row.

Happy to have Greptile re-run on the new commit.

Comment thread packages/web/src/components/SessionTimeline.tsx
Runtime/reaction/PR/lifecycle kinds that include "failed" now stay in
their domain filters; level/kind error bucket applies only after those
checks. Adds SessionTimeline regression test for runtime.probe_failed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ChiragArora31

Copy link
Copy Markdown
Contributor Author

Updated categoryForActivityEvent as suggested: runtime → reaction → Actions → PR/CI → lifecycle, then the generic error / kind contains "failed" gate, then other.

So e.g. runtime.probe_failed, lifecycle.transition_failed, and reaction.trigger_failed stay visible under their respective chips instead of only under Errors.

Added a regression test that uses runtime.probe_failed and asserts it disappears under Errors but remains under Runtime.

@ChiragArora31

Copy link
Copy Markdown
Contributor Author

@harshitsinghbhandari please review

ChiragArora31 and others added 4 commits May 17, 2026 04:54
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…-1788

# Conflicts:
#	packages/web/src/__tests__/api-routes.test.ts
#	packages/web/src/components/SessionDetail.tsx
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