feat: add session activity minimap with click-to-navigate#248
Conversation
Add a togglable horizontal bar chart to the session detail view
showing message activity intensity over time. Clicking a bar scrolls
the message list to that point in the session.
Backend:
- New GET /api/v1/sessions/{id}/activity endpoint with adaptive
time-bucketed message counts (SQLite and PostgreSQL)
- Sub-second timestamp precision via julianday()/floor(EXTRACT)
- System and injected message filtering via SystemPrefixSQL
- Remove unused /minimap endpoint and all supporting code
Frontend:
- ActivityMinimap.svelte custom SVG bar chart component
- Session activity store with load versioning, stale response
rejection, and loaded lifecycle tracking
- Toggle button in SessionBreadcrumb with localStorage persistence
- Scroll-synced active bucket indicator via firstVisibleTimestamp
- Keyboard/ARIA accessibility, error/retry, no-data states
- Fix breadcrumb flicker on SSE refresh (sessionDir effect)
- Fix mainModel badge flicker during reload (stable model caching)
Tests:
- Backend: interval snapping, bucketing, edge cases, fractional
timestamps
- Frontend: bucket index derivation, store lifecycle, stale response
rejection, no-retry on error
- E2E: toggle, bar rendering, click-to-scroll, indicator movement
on reopen
Add julianday() IS NOT NULL filter to SQLite timestamp filter so malformed timestamps that produce NULL from julianday() are excluded instead of causing a scan error. Clear firstVisibleTimestamp to null when no visible item has a timestamp so the minimap indicator doesn't stick to an old bucket. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Docker-based PG tests mirroring the SQLite activity test suite: basic bucketing with system message exclusion, empty gaps, no messages, null timestamps, single message. Refactor fractional timestamp test to use shared seedActivitySession helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aring - SQLite: malformed timestamp string alongside valid rows is excluded from buckets without causing a scan error - PostgreSQL: prefix-detected injected user message (is_system=false but content starts with system prefix) excluded from buckets - Frontend: active indicator clears when firstVisibleTimestamp is set to null Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
- Move _stableMainModel out of $derived.by to avoid side-effect mutation; update it explicitly in loadSession/fullReload finally blocks - Scale SnapInterval dynamically for extreme durations so bucket count stays bounded (maxBuckets=50) - Compute duration from exact float timestamps instead of truncated anchor to preserve sub-second precision - Cache activeBucketIndex in a $derived before the render loop to avoid O(N^2) repeated computation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Assert bucket count in PG prefix-injected test to catch range leakage (excluded message extending min/max). Add comment to indicator clearing test explaining the store/E2E test split. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
When the minimap is closed and an SSE session update arrives, mark the activity cache as stale via invalidate(). The next time the minimap opens, load() sees the cache is invalid and refetches instead of showing stale buckets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
SnapInterval bucket count is floor(duration/interval)+1, so use maxBuckets-1 as the divisor when scaling. Also increment loadVersion in invalidate() so pending responses are discarded when SSE fires while the minimap is hidden. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Start a load, call invalidate(), resolve the stale response, then verify load() issues a fresh request instead of short-circuiting on the discarded cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
getVirtualItems() includes overscan rows above the viewport. Filter by scrollOffset so the minimap indicator tracks the first actually visible item, not an overscanned one above the fold. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use vi.end <= scrollTop to skip only rows fully above the viewport. The previous vi.start < scrollTop check also excluded the partially visible top row, shifting the indicator one row too far down. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
roborev: Combined Review (
|
…content change Explicitly clear _stableMainModel at the start of loadSession so it cannot carry over from the previous session. Track messages.messages.length in the minimap $effect so the active indicator recomputes after SSE content changes, not only on scroll or minimap toggle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Render tooltip in .activity-minimap instead of .minimap-chart to prevent clipping by overflow-x:auto. Position relative to the parent container. Add regression test verifying mainModel doesn't carry over when switching between sessions with different models. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Add position:relative to .activity-minimap so it becomes the containing block for the absolutely positioned tooltip. Drop scrollLeft from tooltip x calculation since the tooltip is no longer inside the scrolling element. Remove unused chartRef. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch tooltip to position:fixed with viewport coordinates from getBoundingClientRect(). This avoids all containing-block and padding-box offset issues. Remove position:relative from the container since it's no longer needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
|
This is good to go |
Summary
GET /api/v1/sessions/{id}/activityendpoint with adaptive time-bucketed message counts (SQLite and PostgreSQL)/minimapendpoint and all supporting codeDetails
Backend:
julianday()(SQLite) andfloor(EXTRACT(EPOCH ...))(PostgreSQL)SystemPrefixSQLFrontend:
ActivityMinimap.sveltecustom SVG bar chart with tooltips, keyboard/ARIA accessibility, error/retry statesfirstVisibleTimestamppublished fromMessageList(excludes overscanned rows)🤖 Generated with Claude Code