Conversation
…r, status chip) Generic building blocks for the Wave 2 dashboard tabs. Sessions uses them first; Cost, Scheduler, Memory, and Evolution reuse the same selectors. - .dash-table with sortable headers, hover rows, sheds cells below 540px - .dash-drawer right-slide dialog, full-width below 720px, reduced-motion aware, ARIA-ready structure (role/aria-modal set at the consumer site) - .dash-metric-strip + .dash-metric-card with serif values and skeleton - .dash-channel-bar stacked segment bar with 8-color palette - .dash-status-chip active/expired/error/paused/info variants - .dash-filter-bar + .dash-filter-select + .dash-filter-search All tokens from the existing vocabulary; no new colors or fonts.
Read-only endpoints backing the Sessions dashboard tab.
List (GET /ui/api/sessions?channel=&days=&status=&q=) returns every
session with chat enrichment (title, message_count, pinned,
deleted_at, forked_from) when channel_id = 'chat', plus a summary
block: total_sessions, total_cost_usd, avg_turns, active_count, and
by_channel counts with cost.
Detail (GET /ui/api/sessions/:session_key) returns the session plus
its cost_events ordered ASC by created_at, 404 with a stable
{ error } shape for missing keys.
Zod-validated query params: channel string (default 'all'),
days 1..365 or 'all' (default 7), status active/expired/all (default
all), q up to 100 chars. All SQL uses parameterized queries; a test
proves a DROP TABLE payload on q is treated as a literal.
19 tests cover filters, summary math, chat enrichment, detail 404,
SQL injection parameterization, 405 on POST, and 422 on invalid
enums.
Read-only view of every conversation across every channel, wired to
the new /ui/api/sessions endpoints.
UI:
- Filter bar: channel, window (1/7/30/90/all), status, debounced search
- Metric strip (total sessions, total cost, avg turns, active count)
- Stacked channel breakdown bar with legend
- Sortable table with channel glyph, conversation, turns, cost,
relative last-active, status chip
- Row click opens an ARIA-correct detail drawer with session overview,
chat enrichment, cost events table, and an Open in Slack / Open chat
session footer
- Skeleton-first render so deep links do not flash an empty list
- Empty, error (with retry), and loading states
- Client-side CSV export
- Keyboard: / focuses search, Esc closes drawer
- Deep-link #/sessions/<urlencoded session_key> opens the drawer
directly; back-nav collapses it
All operator-controlled data (conversation_id, session_key, chat
title, sdk_session_id) flows through ctx.esc() or textContent.
Verified visually that a session with <script>alert('x')</script> in
its conversation_id renders as text with zero script elements.
Wiring:
- dashboard.js: sessions moves from comingSoon to liveRoutes
- index.html: sidebar link promoted into the Workspace nav, route
div added, sessions.js script registered
- CSS palette selector extended so .dash-channel-glyph-dot picks up
the per-channel color alongside bar segments and legend swatches
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6ca174f52
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }; | ||
| document.addEventListener("keydown", drawerKeyHandler, true); | ||
| } |
There was a problem hiding this comment.
Remove stale Escape handlers when drawer re-renders
Each renderDrawer() call rewires interactions and adds a new capturing keydown listener on document, but previous listeners are never removed first. Because the drawer re-renders at least once per open (skeleton → loaded/error), older handlers survive and removeDrawerDom() only removes the most recent one, leaving leaked listeners that keep intercepting Escape globally after the drawer closes.
Useful? React with 👍 / 👎.
| COUNT(*) AS total, | ||
| COALESCE(SUM(s.total_cost_usd), 0) AS cost, | ||
| COALESCE(AVG(s.turn_count), 0) AS avg_turns, | ||
| SUM(CASE WHEN s.status = 'active' THEN 1 ELSE 0 END) AS active |
There was a problem hiding this comment.
Coalesce active_count in empty-result summaries
The totals query coalesces SUM(total_cost_usd) and AVG(turn_count) but not the active-session sum. In SQLite, SUM(...) over zero matching rows returns NULL, so summary.active_count is null whenever filters produce no rows, violating the numeric API contract and forcing every consumer to defensively patch this case.
Useful? React with 👍 / 👎.
Independent reviewer + Codex flagged: P1: drawer keydown handler accumulated on document because renderDrawer() fires twice per open (skeleton then content) and re-installed the listener without removing the previous one. Now detach the prior handler before installing a new one. P1: opening the drawer did not lock body scroll, so the wheel still moved the page behind the backdrop. Save the previous overflow on first open and restore on close. P2: SUM(CASE WHEN status = 'active' ...) returns NULL in SQLite when no rows match the filter, breaking the numeric API contract. Wrap in COALESCE(..., 0). Cleanup: remove dead drawerTrapHandler and state.initialized refs.
Bumps the version to 0.20.0 in every place it's referenced: - package.json (1) - src/core/server.ts VERSION constant - src/mcp/server.ts MCP server identity - src/cli/index.ts phantom --version output - README.md version + tests badges - CLAUDE.md tagline + bun test count - CONTRIBUTING.md test count Tests: 1,799 pass / 10 skip / 0 fail. Typecheck and lint clean. No 0.19.1 or 1,584-tests references remain in source, docs, or badges. v0.20 shipped eight PRs on top of v0.19.1: #71 entrypoint dashboard sync + / redirect + /health HTML #72 Sessions dashboard tab #73 Cost dashboard tab #74 Scheduler tab + create-job + Sonnet describe-assist #75 Evolution Phase A + Memory explorer tabs #76 Settings page restructure (phantom.yaml, 6 sections) #77 Agent avatar upload across 14 identity surfaces #79 Landing page redesign (hero, starter tiles, live pages list)
Summary
Read-only Sessions dashboard tab: lists every conversation across every channel (Slack, chat, scheduler, CLI, MCP, webhook, trigger, etc.) with filters, summary strip, channel breakdown, and a detail drawer with cost events.
Also establishes the shared dashboard primitives the next four Wave 2 tabs (Cost, Scheduler, Memory, Evolution) reuse:
.dash-table,.dash-drawer,.dash-metric-strip,.dash-metric-card,.dash-channel-bar,.dash-status-chip,.dash-filter-bar. All generic, no Sessions-specific coupling at the primitive layer.Spec:
local/2026-04-16-v0.20-next-level/research/02-wave-2-dashboard-tabs-extended.mdWhat ships
GET /ui/api/sessions?channel=&days=&status=&q=- list + summary, chat-enriched rows whenchannel_id = 'chat'GET /ui/api/sessions/:session_key- session + cost_events (404 with{ error }for missing keys)qis literalpublic/dashboard/sessions.js(self-registering module) with skeleton-first render, debounced search, sortable table, deep-link drawer, CSV exportrole="dialog",aria-modal="true",aria-labelledby, focus trap, focus restore on close, Esc and backdrop close/focuses search when not typing elsewhere,Esccloses the drawerctx.esc()ortextContent; noinnerHTMLconcatenation with raw dataTest plan
bun run lintcleanbun run typecheckcleanbun testgreen (1,629 pass, 0 fail)sessions.test.ts(19 tests) covers all filter combinations, summary math, chat enrichment, SQL injection parameterization, 404, 405, 422sessionsdataOut of scope