Status: partially shipped — E1/E3 done, E4 mostly done, E2 (search + switcher) not started; see §10 · Scope: extensions/levelcode-ai webview + a thin store · Builds on: levelcode-chat-sessions-design.md (the persistence spine) · Owns: everything a user sees, touches, and feels
The spine doc answers "how is a chat saved and resumed?" — append-only JSONL, project-slug directories, three-tier resume. It is right, and this document does not relitigate it. This document answers the harder, unspecced half: what is it like to live with your chat history? That half is where Copilot and Cursor are beatable, and where an award is won or lost.
An AI editor's session history is not a filing cabinet. It is the record of everything you and the agent have built together — the place you return to when you think "I fixed this exact bug three weeks ago, how?" Both incumbents treat it as a dropdown of titles. That is the opening.
LevelCode Sessions is a browsable memory of your work, rendered with the same restraint and craft as the editor around it. Not a feature bolted on — a first-class surface that looks like it was always meant to be there, moves like it costs nothing, and tells you the truth about what happened (including when a run was interrupted or a resume had to summarize to fit).
Three commitments, in priority order:
- Honest before beautiful. A session that was interrupted, reloaded, or resumed-from-summary says so — legibly, never as a silent cliff. Awwwards juries reward polish; developers reward trust. We refuse to trade one for the other.
- Instant before rich. Every primary action — resume the last chat, jump to a session, search — is reachable by keyboard in under a second, with zero mouse travel. Richness (previews, sparklines) is layered on top of a spine that is already fast.
- Native before novel. It must feel like it belongs inside VS Code and inside LevelCode's "classic" (atom.io) identity — theme-aware to the pixel — then it can do things Copilot and Cursor can't.
The spine doc already dissected the storage of each incumbent. Here we dissect the experience, because that is what we are competing on.
- The surface: a quick-pick (the same list widget as the command palette) titled "Show Chats." A flat, single-line-per-item list: title + relative time. That's it.
- Where it's weak: no preview of what a session was; no sense of size or activity; no in-place actions (rename/delete live in a
…submenu or separate commands); the quick-pick evaporates on focus loss, so you can't browse and work side by side; nothing communicates a session's state (finished vs abandoned mid-run). It is a picker, not a place.
- The surface: a History panel in the sidebar — a scrolling list of past chats/composers with an auto-title and a timestamp; click reopens; checkpoints let you rewind files within a chat.
- What's genuinely good: it's a persistent panel you can live next to; checkpoints are a real idea; titles are decent.
- Where it's weak: every row is visually identical — a title and a date, no information design; no way to see at a glance which session touched
RefundServiceor ran 40 tools vs 3; search is by title only (the SQLite store isn't greppable, so you can't find a session by a command it ran or a file it edited); storage is opaque, so power users reach for third-party exporters; and the whole thing is competent but characterless — it could be any app's history list.
- Claude Code (
--resumepicker): terminal-native, but the append-only JSONL model is the right substrate and its "compact to fit" moment is honest. We adopt the substrate (spine doc) and give it a GUI it never had. - Warp / Raycast / Linear: the bar for keyboard-first command surfaces and considered motion in developer tools. Linear's issue list and Raycast's root search are the interaction quality we benchmark the switcher against.
Copilot gives you a list of titles. Cursor gives you a better list of titles. LevelCode gives you a map of your work — every session legible at a glance (what it touched, how big it was, whether it finished), findable by anything it did (not just what it was named), and resumable in one keystroke.
- The card carries the story. A session row is not a line of text; it is a compact information object — title, when, model, size, what it touched, whether it finished — designed so the right session is recognizable without opening it.
- Two doors, one room. A panel for browsing (mouse, exploration, management) and a command-palette switcher for jumping (keyboard, muscle memory). Same data, same model, two access patterns — never make the user use the wrong one.
- Search what it did, not what it's called. Because storage is greppable JSONL (spine), search can index files touched, commands run, and models used — not just titles. This is the single biggest experiential lead over Cursor, and it falls out of the storage choice for free.
- Motion is feedback, never decoration. Every animation answers "what just happened / where did this come from / where did it go." A card lifts because it's focusable; a resume morphs because the session becomes the conversation; nothing moves to be impressive. All of it respects
prefers-reduced-motion. - Theme-truth. One Dark and One Light are designed with equal care; high-contrast defers entirely to
--vscode-*tokens. If it looks wrong in any of the three, it is wrong. - Tell the truth about time. Interrupted runs, reloads, and summarize-to-fit resumes are rendered, not hidden. Honesty is the brand.
Grounded in LevelCode "classic" (the atom.io identity already shipped on the account modal), so Sessions looks like the same product.
| Token | One Dark | One Light | High-contrast |
|---|---|---|---|
--cc-bg |
#282c34 |
#ffffff |
--vscode-editor-background |
--cc-surface |
#21252b |
#fafaf9 |
--vscode-editorWidget-background |
--cc-text |
#dcdfe4 |
#333333 |
--vscode-foreground |
--cc-text2 |
#abb2bf |
#555555 |
--vscode-foreground |
--cc-text3 |
#828997 |
#777777 |
--vscode-descriptionForeground |
--cc-line |
#3e4451 |
#e0e0e0 |
--vscode-contrastBorder |
--cc-accent |
#7d6bff |
#5b3fd6 |
--vscode-textLink-foreground |
Semantic accents (used sparingly, only for state): --cc-ok (One Dark #4ec9b0 / One Light green), --cc-warn (#d19a66), --cc-danger (--vscode-errorForeground). These encode session state (finished / interrupted / error) and never double as the brand accent.
- Sans (system UI stack) for titles and body —
-apple-system, BlinkMacSystemFont, "Segoe UI"…. - JetBrains Mono for all data: timestamps, turn counts, model ids, file chips, command snippets.
font-variant-numeric: tabular-numseverywhere digits align (counts, times, sizes) so columns don't shimmer. - Scale: card title 14px/600, meta 11.5px mono, section headers 11px mono uppercase +0.08em tracking (matches the existing "Synced · settings · skills" footer treatment).
.classic-cardradius 6px; interactive controls radius 4px (shipped conventions).- Elevation by 1px border + a single soft shadow on lift only — never resting shadows (flat by default, like the rest of the webview). Hover/focus raises
box-shadow: 0 6px 20px rgba(0,0,0,.18)(dark) with a--cc-accenthairline.
The landing-page chevron (three rising strokes over drifting portal circles, portalDrift 7s) is LevelCode's signature. Sessions uses it in exactly two places, so it stays special:
- the empty state (first run — a large, calm, drifting chevron over "Every chat you have is saved here"),
- the switcher's resting glyph (a small static chevron where a result thumbnail would be). It never decorates a populated list — populated lists are about the user's work, not our logo.
--mo-fast: 120ms(hover, focus, chip toggles)--mo-base: 220ms(card enter, panel open, filter reflow) — easingcubic-bezier(.2,.9,.25,1)--mo-morph: 320ms(the resume shared-element transition) — easingcubic-bezier(.2,.8,.2,1.1)(a whisper of overshoot)- Under
prefers-reduced-motion: reduce: all of the above collapse to opacity-only at--mo-fast, and the portal drift stops.
Reached by the clock icon in the chat header, ⇧⌘H, or LevelCode: Chat History. It is an in-webview overlay panel (not a separate VS Code view) so it inherits the chat's theme and can animate a session into the conversation on resume — a separate view can't do the shared-element morph.
Anatomy, top to bottom:
┌─ Sessions ───────────────────────────── ⌘F ✕ ┐
│ ⌕ Search sessions, files, commands… │ ← instant search, autofocus
│ [ This project ▾ ] [ ⌂ all ] [ ★ ] [ ⟳ edits ] [ ⚠ interrupted ] ← scope + filter chips
├────────────────────────────────────────────────┤
│ TODAY │ ← time buckets, mono uppercase
│ ┌──────────────────────────────────────────┐ │
│ │ Idempotent refunds via Redis keys ★ │ │ ← minimal card (see 4.2)
│ │ refund.rb · 2h │ │
│ └──────────────────────────────────────────┘ │
│ ⚠┌──────────────────────────────────────────┐ │ ← 2px left-edge = interrupted (only marker)
│ │ Debug the flaky payment webhook test │ │
│ │ webhook_spec.rb · 5h │ │
│ └──────────────────────────────────────────┘ │
│ YESTERDAY │
│ … │
└────────────────────────────────────────────────┘
- Time buckets: Today / Yesterday / This week / Earlier (then by month). Sticky bucket headers on scroll. This is how a human actually remembers work ("that was Tuesday"), and it beats a flat reverse-chron list.
- Density: the default is the calm two-line card (§4.2); hover/focus expands it to the rich detail, and a compact toggle (
⌘\) drops it to one mono line for Copilot-terseness. Density is a spectrum the user sets; the preference persists. - Virtualized list (§8) so 5,000 sessions scroll at 60fps.
- The panel does not steal the conversation — it slides over it from the right at
--mo-base, and the conversation stays warm underneath (dimmed 40%), so dismissing feels like closing a drawer, not navigating away.
The first draft of this card showed everything at once — title, model, turns, a sparkline, file chips, a state pill. That is dense, and dense is noise: on a list of a hundred sessions you scan titles, not sparklines. Awwwards-minimal is not fewer features — it is fewer things visible at once. So the card has two states, and the richness lives in the second.
At rest — two lines, one signal, silent when nothing's wrong:
┌────────────────────────────────────────────────┐
│ Idempotent refunds via Redis keys ★ │ title owns the card · ★ only when pinned
│ refund.rb · 2h │ one quiet line: primary file it edited · when
└────────────────────────────────────────────────┘
⚠│ Debug the flaky payment webhook test │ a 2px left-edge is the ONLY state marker,
│ webhook_spec.rb · 5h │ and only when noteworthy (here: interrupted)
- Title owns the card — 14px/500, one line, ellipsis, full title on hover. It is 90% of recognition; give it the room.
- One quiet meta line — muted mono: the single highest-signal cue, the primary file it edited, + relative time (
refund.rb · 2h). A chat-only session with no edits reads just2h ago. No model, no turn count, no sparkline, no chip row at rest. - State is silent when normal. A
donesession shows nothing — "done" is the default, and a✓ doneon every row is pure noise. Only a noteworthy run-state surfaces, as a 2px colored left-edge:⚠interrupted (--cc-warn),✕error (--cc-danger),↻resumed-from-summary (accent). Absence means it finished. One move, and ~90% of cards lose a chip. - Pin (★) appears only when pinned — a small accent star, right-aligned; otherwise absent.
Calmer than Cursor and more useful than Copilot: Copilot gives you title + time, full stop; we add the one cue a developer recognizes work by — the file it changed — and a state edge that speaks only when something is off.
On hover / focus / selection — the card earns its detail:
It expands (--mo-base) into a third zone, so nothing is lost, only deferred to the moment you're choosing:
┌────────────────────────────────────────────────┐
│ Idempotent refunds via Redis keys ★ │
│ refund.rb · redis_lock.rb · +3 │ all files it edited
│ Opus 5 · 41 turns · ⣀⣠⣴⣶⣦⣀ ✓ done │ model · size · sparkline · full state
│ [Resume ⏎] [Done d] [Rename] [Fork] [⋯] │ inline actions — no "…" menu
└────────────────────────────────────────────────┘
- model, turn count, the activity sparkline (drawn-in on expand, §5), all files, the full state, and inline actions (
⏎ d r f e ⌫). The sparkline still tells you shape — a long ramp vs a quick spike vs a cliff — but only when you're looking at that one card, not on every row. - Mouse: hover-intent (120ms) so a fast scroll-by doesn't flicker. Keyboard: the focused card is the expanded one.
Compact mode (⌘\) goes further the other way — one mono row of title · time — for people who want Copilot-terseness. Density is a spectrum the user sets; the default is this calm two-line card, not the dense original.
⌘P-for-sessions (bound ⌃⌘P to avoid the file picker). A centered command-palette overlay, Raycast/Linear-grade:
┌─────────────────────────────────────────────┐
│ ⌕ refund │
├─────────────────────────────────────────────┤
│ ▸ Idempotent refunds via Redis keys 2h ⏎ │ ← fuzzy match on title + files + commands
│ Refactor RefundService retries 3d │
│ Add refund webhook idempotency 1w │
├─────────────────────────────────────────────┤
│ ⏎ resume ⇧⏎ fork ⌘⏎ open in split │ ← footer legend
└─────────────────────────────────────────────┘
- Fuzzy across the greppable fields — typing
refund.rbfinds the session that edited it; typingbundle execfinds the one that ran it. This is the search Cursor structurally cannot do. - Sub-16ms keystroke response on the in-memory index (§8); results reorder with a
--mo-fastcross-fade, never a jarring repaint. - No mouse required, ever — up/down,
⏎resume,⇧⏎fork,escdismiss. This is the "resume last chat in one keystroke" promise made real.
- One field, three indexes: title (weighted highest), files-edited, commands-run. Match highlights show why a session matched ("matched
refund.rb"). - Scope switch:
This project(default) ↔All projects— because sometimes you remember the work, not the repo. - Filter chips (toggle, combine):
★ pinned,⟳ has edits,⚠ interrupted,by model ▾,date ▾. Chips animate in/out and the list reflows at--mo-base. Active filters are summarized in the empty-result state ("No interrupted sessions in the last 7 days — clear filters?").
Resuming is where LevelCode earns the award, because it is the one moment that is both a transition and a truth-telling.
-
The morph. On resume, the session card expands into the conversation — a shared-element transition (
--mo-morph, subtle overshoot): the card's title becomes the chat header title, its surface expands to fill, the panel dims away. The session doesn't "load"; it becomes the room. (Reduced-motion: a clean crossfade.) -
Verbatim (tier 1). The transcript replays instantly from JSONL. A hairline "· resumed" marker sits in the header; nothing else changes. The model sees exactly what it left.
-
Summarize-to-fit (tier 3), designed honestly. When the stored transcript exceeds the resume budget, before spending a token a calm sheet appears:
┌ This chat is larger than the model's window ┐ │ 214k tokens stored · Opus 5 fits ~128k │ │ │ │ Resume from a summary of the early turns │ │ (≈ $0.04, one call) — the full transcript │ │ stays in History, untouched. │ │ │ │ [ Resume from summary ] [ Start fresh ] │ └─────────────────────────────────────────────┘After summarizing, a single in-chat line — "Resumed from a summary of 41 earlier turns · view full transcript" — is a first-class, styled element (accent left-rule,
↻glyph), not a gray aside. Clicking "view full transcript" opens the verbatim history read-only. This is the honest version of "save by summarizing": we summarize to fit, never to store, and we show it. -
What died is said. If the session had background commands, MCP servers, or a running agent when it was last open, resume renders a quiet "since you left" strip: "3 background processes and 1 MCP server were stopped when this chat was last closed. The agent will re-establish anything it needs." — turning the spine's honesty events into UI. No incumbent does this; it is the difference between resuming a state and resuming a transcript.
New Chat is not "clear the screen." It seals the current session — a precise finalization — then opens a fresh one, and the window you were in becomes an inactive History entry with nothing lost that mattered. "What happens to my work when I switch?" is the question that decides whether people trust New Chat at all, so seal is specified exactly.
The state ledger — what the live window holds, and where each piece goes on seal:
| Live-window state | On New Chat (seal) | On resume |
|---|---|---|
| Transcript (user / assistant / agent turns) | already appended per-turn; seal force-flushes the last one | replayed verbatim (or summarized to fit, §4.5) |
| Applied file edits (Keep/Undo review) | kept on disk; review finalized; session records the era | already in your working tree; era noted |
| A running agent turn | confirm first, then abort → recorded interrupted |
shown as ⚠; never auto-resumed |
| Background commands / MCP servers | reaped (killed) → recorded stopped |
"since you left" strip; agent re-establishes deliberately |
| Checkpoints (per-turn file-restore) | boundaries recorded; file-restore is live-only | shown as markers (no cross-seal rewind — yet) |
| Composer draft (typed, unsent) | saved as a per-session draft (not a turn) |
restored into the input box |
Live-session pointer (workspaceState) |
moves to the new session | a window reload re-opens the right one |
| Model / provider mode | unchanged (these are global, not per-session) | unchanged |
Reading the table top to bottom is the whole design, but three rows deserve their reasoning:
- A running agent → confirm, don't silently abort. A mid-run agent may be halfway through editing files, so New Chat asks: "A run is in progress. Start a new chat? The run stops and this chat is saved to History." Only on confirm does it abort; the abort is an
interruptedevent, so the sealed card shows⚠and resume tells the truth. This is the one place seal pauses to ask — everywhere else it is silent and instant. - Background processes → reaped, never orphaned, never auto-restarted. Killing them matches today's
newChat()and avoids leaking processes across sessions; recording them as events is what lets the resumed session's "since you left" strip (§4.5) tell the agent what died, so it re-establishes state on purpose rather than inheriting a half-alive environment. - The composer draft → preserved. Half-typed text is the cheapest thing to lose and the most annoying; it rides the session file as a non-transcript
draftand comes back on resume. Small touch, disproportionate goodwill.
The new session starts clean — deliberately. Empty transcript, fresh checkpoint sequence, no inherited background state. It keeps only what is global (model, provider mode), never per-session context. New Chat is a clean slate by definition; if you want continuity you Resume, you don't New Chat.
Three verbs, kept distinct — the mental model that prevents the "where did my chat go?" panic:
- New Chat / Seal → this session freezes into History; a fresh one opens.
- Resume → a History session becomes the live one again (§4.5).
- Compact → the current session summarizes its own head to keep going in place (context management, same session); it does not start a new session and does not touch History. Seal ≠ Resume ≠ Compact, and the UI labels them so.
The gesture (taught once). The sealed session doesn't just vanish — its card flies to the History icon (--mo-base translate+scale to the clock; the icon pulses once). First time only, a one-line toast: "Saved to History (⇧⌘H)." Dismiss = never shown again. One gesture teaches the whole feature, then gets out of the way.
Multi-window. Two windows on one project each own their own live session; New Chat in one never touches the other. A session file is owned by one window at a time (lockfile beside it, 30s stale timeout, spine §UI); resuming a session already live elsewhere offers a read-only "open a copy?" — no merge semantics, ever.
Awwwards juries read the empty states. Ours have quiet personality without cuteness:
- First run: the drifting chevron/portal over "Every chat you have is saved here — automatically, on your machine, in plain files you can read." (The last clause is the BYOK/hackable brand, stated as reassurance.)
- No search results: "Nothing matches
refund.rbin this project." +Search all projects ↗+Clear filters. - Recovered index: if
index.jsonwas corrupt and rebuilt by scanning (spine §self-heal), a one-time, dismissible strip: "Rebuilt your history from disk — nothing was lost." Honesty as reassurance, again. - A single interrupted session at the top: gently offered — "Pick up where you left off?" on the most-recent interrupted session, once, on panel open.
- The chat header shows the live session title (click to rename inline) with a subtle live-dot when a run is active. A breadcrumb affordance (
Sessions ▸ Idempotent refunds…) lets you get back to the panel without the icon. - A hairline session age in the header footer area (
started 2h ago · 41 turns · autosaved) — quiet proof the autosave promise is being kept, so the user never wonders "is this being saved?"
Completion is where a naïve design does real harm, so it gets its own model. Two independent axes, and conflating them is the trap:
- Run state (derived, automatic) — what the agent did last:
done · interrupted · error · resumed-from-summary. This is the state pill (§4.2). Never user-set. - Lifecycle state (user intent) — what you decided about the session:
active → done/archived → trashed. This is "completion."
A session can be run-state interrupted yet lifecycle done (you gave up on it and filed it away). The two are drawn differently and never merged into one chip.
Four verbs, three of them cheap:
- Seal (automatic — not completion). New Chat finalizes the live session (index write, async title). A sealed session is still Active. Seal just means "no longer the one you're typing into"; it is not a judgment that the work is done.
- Done / Archive (the completion action). You mark a session complete: it leaves the Active list — the decluttering that is the entire point — and enters Archive, where it stays fully searchable and resumable. Reversible in one click. Implemented as a
{"kind":"label","lifecycle":"archived","t":…}event appended to the JSONL — no file move, no rewrite — so it is crash-safe and greppable like everything else. - Delete (separate, heavier, recoverable). Moves the file to the OS trash — never
unlink— and drops it from the index. For junk or privacy. Rare, because Archive already absorbs the clutter. - Pin (orthogonal). Keep a session prominent regardless of age; a pinned session is exempt from auto-archive.
The best-practice call: archive on complete, never remove. Removing a session when a user "finishes" it destroys the one thing the feature exists to provide — the ability to return to how you solved something. "I shipped the fix" is not "erase the record of it." Worse, an irreversible completion makes people afraid to mark done, so they never declutter and the feature fails at its single job. Storage is plain text and effectively free; there is no space pressure to delete. Every mature analog agrees: Gmail archives and trashes as separate acts; Arc auto-archives tabs; Linear and Things keep a completed view rather than deleting. So — Done = archive (reversible); Delete = a deliberate, separate, recoverable act.
Manual, plus opt-in smart-auto:
- Manual Done is primary — a checkmark on the card (
d), a bulk archive for a day of one-offs, and a gentle offer at New-Chat seal only for short, clearly-finished one-offs ("Archive this quick one?" — dismissible, never forced). - Auto-archive (
sessions.autoArchiveDays, default 30,0= off) — a session untouched for the window auto-archives, Arc-style, keeping Active fresh with zero janitorial work. It is honest (the card readsarchived · inactive 30d), exempts pinned, and never deletes. - Never auto-delete. Automatic destruction without consent is the one thing we don't do. An optional
sessions.trashArchivedAfterDays(default0/ off) exists for the rare aggressive-cleanup user — and even it only trashes (recoverable), only archived, never pinned.
Reversibility everywhere: archiving raises a toast with Undo (⌘Z); resuming an archived session offers "reopen" (un-archive); delete is trash, not unlink (spine §retention — one level of oops-protection).
UI: the panel shows Active by default; a scope pill toggles Archive (with its count — "142 archived") and Trash. Archived cards render dimmed and re-activate inline. Card actions become Resume · Done · Rename · Fork · Export · Delete (⏎ · d · r · f · e · ⌫). The run-state pill and a small lifecycle glyph (active / archived) stay visually distinct.
Decided: auto-archive ships on, at 30 days — the Arc magic that keeps History alive for free (sessions.autoArchiveDays: 30; pinned exempt; never deletes). The button reads "Done", with archive as the mechanism underneath — still a one-line change if a later usability pass prefers "Archive" on the label.
Each is specific, cheap, and reduced-motion-safe:
- Card focus lift (
--mo-fast): border →--cc-accent,translateY(-1px), soft shadow in. Communicates focusability; the whole list is arrow-navigable. - Resume morph (
--mo-morph): the shared-element card→conversation transition (§4.5). The single most memorable moment; the thing a juror screenshots. - Sparkline draw-in (
--mo-base, staggered 8ms/bar on hover/focus expand, §4.2): when a card opens to its rich detail the activity bars grow from baseline. It never draws on scroll — the resting row has no sparkline to animate — so a fast scroll stays calm. - Filter reflow (
--mo-base): cards that leave fade+collapse; remaining cards ease to new positions (FLIP). Turns filtering from a repaint into a legible rearrangement. - Seal-to-history fly (§4.6): the New Chat teaching moment.
- Search match shimmer (
--mo-fast): matched substrings get an accent underline that draws left-to-right — shows why a result matched. - Portal drift (
7s, empty state + switcher glyph only): the brand's ambient signature, used with extreme restraint.
Global rule: nothing animates on scroll at all — the sparkline lives in the hover-expanded card, not the resting row, so scrolling a history of your own work feels like paper, not a parallax site. Restraint is the awwwards move here, not maximalism.
Beyond the card and the search, the "wow" list — each grounded in the JSONL substrate so it's cheap to build:
- Fork a session ✅ (fork-from-end shipped) — resume a copy, leaving the original intact.
sessions.fork()seeds a new session with the original's conversation and drops itsendandlabelevents: a copiedendwould render a live fork asdonewhile you typed into it, and a copiedlabelwould have a fork of an archived session born invisible in the default Active scope, or silently taking a second pin. The copy is titled… (fork), recordsforkedFromin its meta (§4 provenance, and what a later branch-graph would draw from), and becomes live — a fork is a resume, into a copy, so it reuses the resume path wholesale. Per-turn fork still awaits the transcript picker (§411). The "what if I'd told it to do X instead" branch. Cursor's checkpoints rewind files; forking rewinds the conversation. Both, together, is new. - Files-touched search & chips (§4.2/4.4) — find work by what it changed. The structural lead over Cursor.
- Activity sparkline (§4.2) — session shape at a glance.
- The honesty layer as UI — interrupted / reloaded / summarized states are pills and strips, not silence (§4.5, §4.7).
- Pins & light tags —
★pin, plus optional freeform tags (#refactor,#spike) surfaced as filter chips. Small, opt-in, greppable. - "Copy as Markdown" / Export — because the storage is plain text, a session exports to a clean Markdown transcript in one action; a natural share/paste-into-PR flow (and the seed of the later LevelLinks "share a run").
- Cross-session recall (later, but designed-for) — since storage is greppable JSONL, an agent tool over your own history ("what did we decide about idempotency keys last week?") is a natural follow-up; the card/search IA here is what makes its results presentable.
The rich UI needs a few fields beyond a bare transcript. All are derivable by scanning the JSONL (so the index stays a rebuildable cache, per spine §Index), computed once at seal and cached in index.json:
| Card field | Source | Notes |
|---|---|---|
| title | title event / first user msg |
spine §Titles |
| when | meta.createdAt / last event t |
relative, tabular |
| model | last assistant/agent turn's model |
most-used, not last, if they differ |
| turns | count of user events |
|
| sparkline | tool-calls per turn from agent events |
array of small ints; ~1 byte each |
| files-edited | edit_file/write_file tool events |
de-duped, ordered by edit count |
| state | terminal event kind | done / interrupted / error / compact-on-resume |
| pinned / tags | a label event |
append-only, like titles |
Extension to index.json (still a cache, still rebuildable):
{ "id":"…", "title":"…", "createdAt":"…", "updatedAt":"…", "turns":41,
"model":"anthropic/claude-opus-5", "state":"done", "pinned":true,
"filesEdited":["app/services/refund.rb","app/models/redis_lock.rb"],
"spark":[1,3,2,5,8,6,4,2,1], "tags":["refactor"], "preview":"Add idempotency…" }The webview never reads JSONL directly for the list — it reads this index (fast, small). It reads the full JSONL only on resume/preview. Clean separation: index feeds recognition, JSONL feeds resumption.
- Virtualized list — render only the ~20 cards in view + a small buffer; recycle nodes. 5,000 sessions must scroll at 60fps and open in <100ms.
- Search index in memory — on panel open, the extension hands the webview a compact index (title + filesEdited + commands, per project). Fuzzy match runs in the webview against it; keystroke→results < 16ms. All-projects scope loads lazily on switch.
- Previews are lazy — the sparkline array is in the index (cheap); a full hover-preview of the transcript is fetched on hover-intent (150ms dwell), never eagerly.
- The index is a cache, never load-bearing (spine §self-heal) — a missing/corrupt index rebuilds by scanning; the UI shows the recovered strip (§4.7) and is otherwise unaffected.
-
Keyboard-complete. Every action has a binding; the panel and switcher are fully operable with no pointer. Focus is trapped in the panel while open, restored to the composer on close. Documented map:
Key Action ⇧⌘Hopen/close Sessions panel ⌃⌘Pswitcher (fuzzy jump) ↑ ↓move selection ⏎resume · ⇧⏎fork ·⌘⏎splitdmark done (archive) · ⇧dun-archiverfe⌫rename · fork · export · delete (on focused card) ⌘Ffocus search · ⌘\compact toggleescclose (restores composer focus) -
Screen readers: cards are a
role="listbox"ofoptions with anaria-labelcomposing the human summary ("Idempotent refunds, 2 hours ago, Opus 5, 41 turns, done, pinned"). Buckets aregroups with labels. The state pill has a text equivalent, never color-only. -
Reduced motion: §3 motion tokens collapse to opacity; the morph becomes a crossfade; portal drift stops.
-
Three theme kinds, equal care: One Dark / One Light designed deliberately; high-contrast defers entirely to
--vscode-*(the account-modal precedent) — the accessibility choice always wins over the brand palette.
Layered on the spine's Phase 1–4. Each phase ships something usable; none blocks on the next.
E1 — the panel & the card (M) — ✅ shipped. — the Sessions overlay, time buckets, the rich card (title/meta/state), resume (verbatim tier), New Chat seal + fly-to-history. Ships the moment History stops being a dropdown. Exit: three sessions, browsable, resumable, recognizable at a glance; theme-correct in all three kinds.
E2 — the switcher & search (M) — ❌ not started. There is no in-panel search input, no filter chips, no scope switch, and no ⌃⌘P jump — AI: Sessions is the only command and there are no keybindings. This is the largest remaining gap in the experience, and the one users will notice first once a project has more than a screenful of sessions: the panel can be scrolled but not searched. — ⌃⌘P fuzzy jump; in-panel search over title+files+commands; filter chips; scope switch. Ships the keyboard-first, search-what-it-did lead over Cursor. Exit: type a filename, resume the session that edited it, in three keystrokes.
E3 — the honesty layer & rich recognition (M) — ✅ shipped. Sparkline, files-touched chips, state pills, and the compact-on-resume plan + its honest note. — activity sparkline, files-touched chips, state pills, the interrupted/reload/summarize strips, the compact-on-resume sheet. Ships the trust story and the visual lead. Exit: an interrupted session is unmistakable in the list and on resume; a too-big session resumes via the honest sheet.
E4 — signature polish (S) —
Deliberately later: fork-visualized-as-a-branch-graph; cross-session recall agent tool; LevelLinks share-a-run; the M9 encrypted sync of the same files.
The webview has no runtime DOM test harness, so we test the way the shipped code already does — static invariants over the source (webviewCss.test.js style: "bugs no DOM test can see"), plus pure-logic units and a manual matrix.
- Static invariants (
test/sessionsUi.test.js): every session-card class that toggles at runtime has its[hidden]/state escape; the state-pill classes each map to a--cc-*semantic token (never a hard-coded hex); the panel's motion rules are all inside aprefers-reduced-motionguard; the keyboard map in code matches the documented table; the switcher publishes thependingApproval-style contract the key handler reads. - Pure units (
test/sessionsIndex.test.js): sparkline derivation from an events array; files-edited de-dupe/ordering; state classification from a terminal event; fuzzy-rank determinism; index build == index rebuilt-by-scan (the self-heal invariant). - Render-mock (the method used to land the timeline rail fix): a static HTML reproduction of the panel/card with the real classic CSS, rendered to eyeball dark/light/HC and reduced-motion — no live editor needed.
- Manual matrix (EXIT-TEST.md): resume-morph correctness, interrupted-state truthfulness, virtualized-scroll at 5k sessions, keyboard-only completion of every action, theme correctness ×3.
- Panel vs. VS Code sidebar view. This doc argues for an in-webview overlay (enables the resume morph, inherits theme). The cost: it lives inside the chat panel's width. A native sidebar view would be wider but loses the morph and the shared-theme guarantee. Recommendation: overlay; revisit if users want a persistent side-by-side list.
- How much tagging. Pins are clearly worth it; freeform tags risk becoming an unused feature. Recommendation: ship pins in E4, tags behind the greppable search first, promote to chips only if used.
- Sparkline: Unicode vs
<canvas>. Unicode is zero-cost and copy-pasteable; canvas is prettier and animatable (draw-in). Recommendation: canvas in the card (E3), Unicode fallback in the compact row and exports. - Fork UX depth. Fork-from-end is easy (E4); fork-from-an-arbitrary-turn needs a turn picker in the transcript. Recommendation: fork-from-end first; per-turn fork rides the later checkpoint-visualization work.
This document is the experience half of LevelCode Sessions; levelcode-chat-sessions-design.md is the persistence half. Together they specify a sessions feature that is honest, instant, native — and beats a dropdown and a list of titles.