~bigbes/lethe

ref: 74314eeb68171bddd6ed722efaa9fc4164428990 lethe/docs/design_handoff_assistant_log/README.md -rw-r--r-- 21.8 KiB
74314eeb — Eugene Blikh docs(lethe-web-ui-login): record verify checks a month ago

#Handoff: assistant-log

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.


#About the Design Files

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.


#Fidelity

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.


#Files

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

#Top-level structure

┌──────────────────────────────────────────────────────────────────┐
│  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.


#Routes

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


#Design Tokens

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

#Surfaces

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

#Ink (text)

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

#Rules

Token Light Dark
--rule #d8d3c4 #2c2920
--rule-2 #e6e1d2 #24221a

#Top bar (always dark, even in light mode)

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)

#Accent — locked to OKLCH hue 120 (lime-green)

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.

#Status

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

#Tag-specific

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)

#Turn surfaces (Session view)

Token Light Dark
--turn-user #f5f0e0 #1f1d15
--turn-asst #ffffff #15140f
--turn-tool #f8f5e9 #211f17

#Tool identity colors (constant across themes — these are brand-like per-tool)

Tool Color
claude-code #c96442 (terracotta)
opencode #3b6e3b (forest)
crush #7a4ea8 (purple)
pi #b8902a (ochre)
kimi #2a6e9c (steel-blue)

#Typography

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

#Sizing

  • Compact row height: 22–24 px (3 px vertical padding)
  • Comfortable row height: 28–32 px (4 px vertical padding)
  • Compact is the default density — set on the root via the class density-compact
  • Card border-radius 4px, palette 6px, tags 3px, status dots 99px
  • Sparkline: 60–140 × 12–18 px inline SVG <polyline>, stroke-width: 1.2 (1.4 when .accent)

#Screens

#01 · Home (Recent)

  • Sub-bar: filter chips (since: 30d, tool: any, host: any, + filter)
  • Table head: STARTED · TOOL · HOST · SUMMARY · TURNS · TOK · CWD
  • Rows: one per session. Cursor row gets --accent-soft bg + 2 px --accent left border (the row's padding-left shifts from 14 to 12 to compensate).
  • Empty filter result: EmptyState with glyph and copy "no sessions match these filters"

#02 · Search results

  • Sub-bar: shows the active query as "<query>" in 12 px mono-bold + result count + "save as ★ saved search" action
  • Rows: hit type (turn | session | path), session ref, snippet with <mark class="fts"> wrapping query terms (lime bg, dark accent-on text)
  • Empty state: glyph and copy "no results — try a broader query"

#03 · Session view

┌────────────────────────────────────────────────────────┐
│ 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 }).

#04 · Projects index

Table head: PROJECT (cwd) · SESSIONS · TURNS · LAST ACTIVE · HOSTS · TOOLS USED. Hosts column shows Tag.host chips, tools column shows colored ToolDots.

#05 · Project view

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.

#06 · Stats

A 2-column grid (.stats-grid) of cards, with one full-width card (.full):

  1. Per-tool · last 30d (full): table — tool dot, name, sessions, turns, tokens, sparkline, % share
  2. Daily turns · last 60d (full): stacked-bar SVG chart, claude-code (accent) on bottom, opencode (forest) middle, crush (purple) top
  3. Activity heatmap · 12 weeks: GitHub-style heatmap (84 cells, accent fill, opacity scaled by activity)
  4. Top cwd · 30d: horizontal bar list — cwd in mono, accent bar, count right-aligned
  5. Hour-of-day distribution: 24-bin bar chart
  6. Host split: laptop vs workpc — mono % bars, host-specific colors

#07 · Health (ingestion)

  • Sub-bar: collectors label + status counters as tags (e.g., "● 7 ok", "● 1 warn")
  • Table head: · HOST · TOOL · SOURCE PATH · LAG · OUT · LAST POLL · EVENTS
  • Rows: status dot (ok/warn/stale), host, tool tag with ToolDot, 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
  • Footer strip: backfill progress (~3m, mono path, % bar, % text) + last-error log line in --err color

#08 · Settings

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


#Interactions & Behavior

#Keyboard map

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

#Filter chips

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

#⌘K command palette

  • Modal scrim (var(--scrim)), 80 px from top, 620 × 480 max
  • Single input at top (mono, 13 px); list below with cursor highlighting
  • Items have a kind prefix (uppercase mono, 70 px wide column): JUMP, PROJECT, SESSION, SEARCH
  • When the query is non-empty and doesn't match any item, append a synthetic SEARCH for "<query>" row that goes to /search?q=…
  • Keyboard: ↑↓ cursor, activate, Esc close
  • Footer: keyboard hints in mono (↑↓ navigate · ↵ open · esc close)

#Active row cursor

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.

#Tweaks panel

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:

  • Theme (light/dark) — persist to localStorage, default to prefers-color-scheme
  • Density (compact/comfortable) — persist
  • Show tool calls (per-session toggle, also persisted globally)

The "Accent on/off" and per-component tweaks were exploration controls; do not surface them.


#State Management

#Top-level App state (in 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.


#Data Shapes

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 };
}

#Theming

The prototype supports light + dark. <html data-theme="light|dark"> switches the entire token set. Implement:

  1. Read prefers-color-scheme on first load
  2. Persist user choice to localStorage['theme']
  3. Apply data-theme attribute on <html> (NOT <body> — the [data-theme] selector targets html)
  4. Listen for OS theme changes when no user override is set

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.


#Production checklist

  • [ ] Replace proto-data.jsx mock with real API/queries
  • [ ] Replace inline-React + Babel-standalone setup with the codebase's normal build (Vite/Next/etc.)
  • [ ] Strip tweaks-panel.jsx and the useTweaks plumbing
  • [ ] Wire real router (/, /search?q=…, /session/:id, /project/:cwd, /stats, /health, /settings/:section?)
  • [ ] Implement remaining filter dimensions (cwd, model, has-tool-calls) — popover already supports them, just unstub
  • [ ] Add real saved-searches store (currently a stub link)
  • [ ] Build out Settings → Auth, Backup, Export, Tags, Saved searches (placeholder cards in prototype)
  • [ ] Persist theme, density, showToolCalls to localStorage; sync theme with prefers-color-scheme on first load
  • [ ] Wire on cursored row across all list routes (Home is the only one wired in the prototype)
  • [ ] Real FTS in the search route — the prototype does substring matching against in-memory data
  • [ ] Confirm the per-tool color palette matches your branding (terracotta/forest/purple/ochre/steel-blue) — these are constant across themes
  • [ ] Decide whether the top bar stays "always dark" in light mode (the prototype keeps it dark for visual continuity); easy to flip via --topbar-bg / --topbar-fg

#Notes for the implementer

  • Density first: "compact" is the default. Don't loosen padding "for accessibility" — this UI is for terminal-dwellers who want max info per screen. Comfortable mode exists for users who explicitly opt in.
  • Mono for data, sans for prose. Never mix. If you find yourself rendering a number, path, ID, or timestamp in sans, that's a bug.
  • Accent is a spotlight, not a coat of paint. Use it for: active tab, primary chip, sparkline highlight, FTS hit, ASSISTANT turn left-bar, cursor row, "stale" indicators in Health. That's the full set.
  • No tooltips for column heads. The headers are uppercase 10 px mono; if a column needs explanation, the column itself is wrong.
  • Tool turns are evidence, not conversation. Render them in mono, smaller, dimmer, and let users hide them entirely.