A self-hosted, single-user log aggregator for CLI AI assistants (Claude Code, opencode, crush, pi, kimi). Polls per-tool collectors on each host, normalizes turns/sessions/tools into a single timeline, exposes search + per-project + per-host views.
This bundle ships Direction 4 — "dense data UI": a GitHub/Linear-style information-dense interface, designed for power scanning. Tight rows, monospace data, sparklines, ⌘K palette, keyboard nav.
The files in this bundle are design references created in HTML — a clickable prototype showing the intended look, behavior, and information architecture. They are not production code to copy directly.
The task is to recreate these HTML designs in the target codebase's existing environment — using its established patterns, component primitives, routing, and data layer. If no environment exists yet, the recommended stack is React + TypeScript + Vite, with state via Zustand or React Query, and CSS-modules or vanilla-CSS-with-tokens (the prototype uses raw CSS custom properties and that approach maps cleanly).
The prototype uses inline JSX via Babel-standalone for portability — that is not how it should ship.
High-fidelity. Colors, typography, spacing, layout, density, copy, keyboard map, and interactions are all final. Pixel-match the prototype.
The prototype's mock data (proto-data.jsx) is illustrative — replace with real data from the backend, but preserve the shapes described in the Data Shapes section below.
| Path | Purpose |
|---|---|
Prototype.html |
Entry point — sets up React, defines App, wires routing, keyboard nav, theme/density tweaks |
prototype.css |
All design tokens (:root and [data-theme="dark"]), every UI primitive's styles |
proto-shell.jsx |
TopBar (brand, search trigger, tab nav) and RouterCtx |
proto-atoms.jsx |
Shared primitives: Tag, ToolDot, Spark (inline-SVG sparkline), StatusDot, Sub (filter chip bar), EmptyState |
proto-data.jsx |
Mock data — sessions, turns, projects, hosts, collectors, tool colors |
proto-home.jsx |
Recent sessions screen (route home) |
proto-search.jsx |
Search results with FTS <mark> highlighting (route search) |
proto-session.jsx |
Single-session view: turn-list aside + linear transcript (route session) |
proto-pages.jsx |
Stats, Health, Projects index, Project view, Settings — all in one file |
proto-palette.jsx |
⌘K command palette |
tweaks-panel.jsx |
In-design tweak panel (developer/preview tool — do not ship) |
original-spec.md |
The original direction spec we built from |
┌──────────────────────────────────────────────────────────────────┐
│ TopBar ← dark, monospace, breadcrumb + search + tabs
├──────────────────────────────────────────────────────────────────┤
│ Sub-bar (per route) ← filter chips, range, "since" controls
├──────────────────────────────────────────────────────────────────┤
│ Body (flex: 1) ← the route's screen, scrolls
├──────────────────────────────────────────────────────────────────┤
│ Footer strip (some routes only) ← e.g. Health backfill progress
└──────────────────────────────────────────────────────────────────┘
Fixed-position elements: ⌘K palette overlay, Tweaks panel, hint chip
The shell never scrolls — only .body does. Top bar, sub-bar, and (where present) footer strip are flex: none.
| Route name | Description | URL pattern (recommended) |
|---|---|---|
home |
Recent sessions across all projects/hosts | / |
search |
Search results with FTS highlights | /search?q=… |
session |
One session: turn list + transcript | /session/:id |
projects |
All projects, ranked by recent activity | /projects |
project |
One project (cwd) view | /project/:cwd |
stats |
Aggregate stats (per-tool, daily, hour-of-day, top-cwd, host split) | /stats |
health |
Collector ingestion health table | /health |
settings |
Settings (Sources / Display / Auth / Backup / Export / Tags / Saved searches) | /settings/:section? |
The prototype implements routing with a single React state object {name, ...args} plus a RouterCtx. Replace with the codebase's existing router (React Router, TanStack Router, etc.).
All colors flow through CSS custom properties. Light mode is the :root block in prototype.css; dark mode is the [data-theme="dark"] block on <html>.
| Token | Light | Dark | Use |
|---|---|---|---|
--paper |
#fdfcf8 |
#15140f |
Main body background |
--paper-2 |
#f3f0e7 |
#211f17 |
Hover row, active tweak nav item |
--paper-3 |
#fbfaf2 |
#1a1813 |
Sub-bar bg, table head bg, footer strip bg |
--paper-4 |
#ffffff |
#1d1b14 |
Card body, alternating-row "white" stripe |
| Token | Light | Dark | Use |
|---|---|---|---|
--ink |
#1c1a17 |
#e8e3d4 |
Primary text |
--ink-2 |
#4a453d |
#b8b2a0 |
Secondary text, USER role label |
--ink-3 |
#7a7468 |
#807a6b |
Muted text (column heads, meta) |
--ink-4 |
#b3ad9f |
#4a4538 |
Dim/placeholder, USER turn left-bar |
| Token | Light | Dark |
|---|---|---|
--rule |
#d8d3c4 |
#2c2920 |
--rule-2 |
#e6e1d2 |
#24221a |
| Token | Light | Dark |
|---|---|---|
--topbar-bg |
#1c1a17 |
#0d0c08 |
--topbar-fg |
#fdfcf8 |
#e8e3d4 |
--topbar-input-bg |
rgba(255,255,255,0.06) |
rgba(255,255,255,0.04) |
--topbar-input-bd |
rgba(255,255,255,0.15) |
rgba(255,255,255,0.10) |
| Token | Light | Dark | Use |
|---|---|---|---|
--accent |
oklch(0.78 0.18 120) |
oklch(0.78 0.16 120) |
Active tab underline, accent chip bg, selected row left-border, FTS mark bg, sparkline highlight, ASSISTANT turn left-bar |
--accent-soft |
oklch(0.94 0.05 120) |
oklch(0.30 0.08 120) |
Selected row bg, palette active item bg, settings nav active bg |
--accent-ink |
oklch(0.30 0.10 120) |
oklch(0.86 0.16 120) |
Text on --accent-soft; ASSISTANT role label |
--accent-on |
oklch(0.20 0.08 120) |
oklch(0.18 0.06 120) |
Text on --accent (chip text, FTS mark text) |
The four accent vars are also written inline on <html> by the App effect — this is so the "Accent off" tweak can mute them at runtime. In production you can drop the inline writes if you don't need a runtime accent toggle.
| Token | Light | Dark | Use |
|---|---|---|---|
--ok / --ok-bg |
#3b6e3b / #d6efd6 |
#6fb46f / #1f3327 |
Healthy collector, "ok" tag |
--warn / --warn-bg |
#c89a3a / #fdebcc |
#d4a648 / #3a2e15 |
Warning state, schema drift |
--err / --err-bg |
#b04a2a / #f5dccf |
#d97052 / #3a1f15 |
Error, stale collector, last-error log |
| Token | Light | Dark | Use |
|---|---|---|---|
--tag-bg |
#ebe7d8 |
#2a2820 |
Default neutral tag bg |
--tag-host-bg / --tag-host-fg |
#d9e6dd / #2f5a3f |
#1f3327 / #8fc09e |
Host tag (laptop, workpc) |
| Token | Light | Dark |
|---|---|---|
--turn-user |
#f5f0e0 |
#1f1d15 |
--turn-asst |
#ffffff |
#15140f |
--turn-tool |
#f8f5e9 |
#211f17 |
| Tool | Color |
|---|---|
claude-code |
#c96442 (terracotta) |
opencode |
#3b6e3b (forest) |
crush |
#7a4ea8 (purple) |
pi |
#b8902a (ochre) |
kimi |
#2a6e9c (steel-blue) |
--mono: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
--sans: 'Inter', system-ui, sans-serif;
Type scale — body text 12 px (body), data rows 11.5 px, table heads 10 px UPPERCASE 0.05 em tracking, transcript body 13 px, palette input 13 px. Line-height 1.4 throughout, 1.55 in transcript prose. Mono is used for all data: timestamps, paths, token counts, IDs, breadcrumbs, host names. Sans is for prose only (transcript bodies, settings copy, empty-state messages).
density-compact4px, palette 6px, tags 3px, status dots 99px<polyline>, stroke-width: 1.2 (1.4 when .accent)since: 30d, tool: any, host: any, + filter)STARTED · TOOL · HOST · SUMMARY · TURNS · TOK · CWD--accent-soft bg + 2 px --accent left border (the row's padding-left shifts from 14 to 12 to compensate).EmptyState with glyph ∅ and copy "no sessions match these filters""<query>" in 12 px mono-bold + result count + "save as ★ saved search" actionturn | session | path), session ref, snippet with <mark class="fts"> wrapping query terms (lime bg, dark accent-on text)⌕ and copy "no results — try a broader query"┌────────────────────────────────────────────────────────┐
│ Sub-bar: breadcrumb (~/cwd short-summary… tool/host/branch tags)
├────────────┬───────────────────────────────────────────┤
│ Aside 240px│ Transcript (flex: 1, scrolls) │
│ (turn list)│ │
│ scrolls │ USER ← left-bar = --ink-4 │
│ │ ASSISTANT← left-bar = --accent (lime) │
│ │ TOOL ← no bar │
│ │ │
│ │ Each turn: 3 px left-border, role label │
│ │ in mono-bold uppercase, meta line in │
│ │ 10.5 px muted, body in 13 px prose │
│ │ (or 11.5 px mono for tool turns) │
└────────────┴───────────────────────────────────────────┘
Aside row layout: 24px # · 14px role-glyph · 1fr preview · 36px tok mono. Selected row gets --paper-2 bg + 2 px accent left border.
Tool turns are visually compressed — the "Show tool calls" tweak hides them entirely (.hide-tool-calls .turn.tool { display: none }).
Table head: PROJECT (cwd) · SESSIONS · TURNS · LAST ACTIVE · HOSTS · TOOLS USED. Hosts column shows Tag.host chips, tools column shows colored ToolDots.
Per-cwd view. If the cwd has no sessions, render the EmptyState with copy "no sessions in this project yet". Otherwise, sessions table identical to Home but scoped.
A 2-column grid (.stats-grid) of cards, with one full-width card (.full):
collectors label + status counters as tags (e.g., "● 7 ok", "● 1 warn") · HOST · TOOL · SOURCE PATH · LAG · OUT · LAST POLL · EVENTSToolDot, path mono-muted truncate, lag value (red --err if stale, yellow --warn if warn, muted otherwise), out-of-band events count (yellow if > 0), last-poll ago, lifetime events count~3m, mono path, % bar, % text) + last-error log line in --err colorTwo-column grid (180px sidebar / 1fr content). Sidebar items: sources · display · auth · backup · export · tags · saved searches. Active item: --accent-soft bg + --accent-ink color + 600 weight.
The prototype only ships Sources (per-host-per-tool config table with status, path, poll interval, events count) and Display (mirrors the in-design tweaks) at full fidelity. Auth/Backup/Export/Tags/Saved searches are placeholder cards — flesh out per real backend.
| Key | Where | Action |
|---|---|---|
⌘K / Ctrl+K |
global | open command palette |
Esc |
palette open | close palette |
↑ / ↓ |
palette open | move palette cursor |
↵ |
palette open | activate palette item |
j |
list routes (home, projects, project, search, health) | move row cursor down |
k |
list routes | move row cursor up |
↵ |
home | open session at cursor |
g h |
global | go to Home |
g p |
global | go to Projects |
g s |
global | go to Stats |
g i |
global | go to Health (i = ingestion) |
The g leader has an 800 ms timeout. Implement it as a ref + setTimeout pair (see App.onKeyDown in Prototype.html).
Each chip is <Tag class="click"> showing key: value. Click → popover opens at chip's position with possible values. + filter at the end opens a popover listing dimensions to add. × on a chip removes it. The popover is absolute-positioned, z-index: 40. Closes on outside click / Esc.
Dimensions to support: tool, host, since (1d/7d/30d/90d/all), cwd, model, has-tool-calls. (The prototype only wires the first three; add the rest to match the spec.)
var(--scrim)), 80 px from top, 620 × 480 maxkind prefix (uppercase mono, 70 px wide column): JUMP, PROJECT, SESSION, SEARCHSEARCH for "<query>" row that goes to /search?q=…↑↓ cursor, ↵ activate, Esc close↑↓ navigate · ↵ open · esc close)In list routes, j/k move a cursor index; the cursor row gets cursor class → --accent-soft bg + --accent left border. On ↵ (home only in the prototype — extend to other list routes in production), navigate to the cursored item.
The <TweaksPanel> from tweaks-panel.jsx is a design-time tool only. It is the in-design controls for theme, density, accent, etc. Strip it from production. The actual settings live under the Settings → Display route.
The runtime knobs that should remain user-visible in production:
prefers-color-schemeThe "Accent on/off" and per-component tweaks were exploration controls; do not surface them.
Prototype.html){
route: { name, ...args }, // home | search | session | …
paletteOpen: boolean,
cursor: number, // active row index in list routes
query: string, // search query (mirrored in URL)
tweaks: {
density: 'compact' | 'comfortable',
showToolCalls: boolean,
accent: boolean,
theme: 'light' | 'dark',
},
}
Replace with the codebase's normal store (Zustand, Redux, signals, etc.).
['sessions', { since, tool, host, cwd, search? }] → home / project / search
['session', id] → session view
['projects', { since }] → projects index
['stats', { range }] → stats screen
['health'] → collectors + last-poll
['settings', 'sources'] → per-host/tool config
All queries should support reactive invalidation when the user changes filters in the sub-bar.
These are the shapes the prototype consumes (see proto-data.jsx). The real backend should serve compatible JSON.
type Tool = 'claude-code' | 'opencode' | 'crush' | 'pi' | 'kimi';
type Host = 'laptop' | 'workpc';
type Role = 'user' | 'assistant' | 'tool';
interface Session {
id: string; // e.g. 'inflection-3'
tool: Tool;
host: Host;
cwd: string; // e.g. '~/code/tt-bundle'
branch?: string; // git branch when session started
model: string; // e.g. 'claude-opus-4-7'
started: string; // ISO timestamp
ended: string | null;
summary: string; // first user turn, truncated
turns: number;
tokensIn: number;
tokensOut:number;
hasError: boolean;
}
interface Turn {
i: number; // 1-indexed within session
role: Role;
body: string; // markdown for user/assistant; mono content for tool
tokensIn?: number; // assistant turns only
tokensOut?: number;
toolName?: string; // tool turns: 'read', 'grep', 'bash', 'edit', 'write', …
toolKind?: 'call' | 'result';
}
interface Project {
cwd: string;
sessions: number;
turns: number;
lastActive: string;
hosts: Host[];
tools: Tool[];
}
interface Collector {
host: Host;
tool: Tool;
sourcePath: string; // path the collector polls
status: 'ok' | 'warn' | 'stale';
lagSeconds: number; // how far behind real-time
outOfBand: number; // events received that didn't parse
lastPoll: string; // relative, e.g. '7s ago'
events: number; // lifetime
lastError?: { ts: string; msg: string };
}
The prototype supports light + dark. <html data-theme="light|dark"> switches the entire token set. Implement:
prefers-color-scheme on first loadlocalStorage['theme']data-theme attribute on <html> (NOT <body> — the [data-theme] selector targets html)The accent vars (--accent, --accent-soft, --accent-ink, --accent-on) are written inline on <html> by the App effect (so an "accent off" toggle could mute them at runtime). In production you can drop the inline writes — the CSS rules suffice.
proto-data.jsx mock with real API/queriestweaks-panel.jsx and the useTweaks plumbing/, /search?q=…, /session/:id, /project/:cwd, /stats, /health, /settings/:section?)cwd, model, has-tool-calls) — popover already supports them, just unstubtheme, density, showToolCalls to localStorage; sync theme with prefers-color-scheme on first load↵ on cursored row across all list routes (Home is the only one wired in the prototype)--topbar-bg / --topbar-fg