Extraction surfaces: sidebar coverage block, chat-bar ring, phase un-pill, panel death

The owner-locked extraction-UX redesign (dispatch 2026-09-24), UI
chunk over Platform's data layer (eed4b4e — GET /api/chats/[id]
carries `coverage`, the threshold is Graph ▸ extractionChunkTokens):

1. SIDEBAR BLOCK (components/extraction-coverage.tsx +
   sidebar's session menu): the coverage info block at the TOP of
   the context menu — uncovered tokens, uncovered messages, running
   state — then the Extract affordance (optimistic "extracting… /
   Running" until the server says running; 409 = benign, the trigger
   raced us), fetched ON MENU OPEN (the block mounts with the menu —
   on-demand, never eagerly), polled at 2s while a run is in flight.
   Visually separated from Rename/Delete by a hairline; those stay
   the two session-management actions. Failed read → nothing (never
   fake zeros).

2. RING METER (CoverageRing, chat top bar, before the PhaseChip):
   a 16px donut filling uncoveredTokens / extractionChunkTokens —
   "how close this session is to its next extraction run". Click →
   the exact numbers (covered/uncovered tokens, messages, the
   threshold, lastFoldedAt via timeAgo). Refresh chains: mount, the
   turn-settle bump (chat.tsx coverageTick), the settings signal
   (threshold/kill-switch re-read live), a 2s drain-poll while a run
   is in flight, and the ACTIVITY FEED — a run for this session
   appearing or settling in the dock's feed re-reads coverage (runs
   fire from other surfaces — the sidebar menu, the turn pipeline —
   and no turn-settle bump announces them; the runs-empty-transition
   the graph view rides). Vanishes entirely when the graph is off or
   before the first honest read. Running = the whole ring PULSES
   (implementer's visual call: motion is the working-state signal in
   this app's quiet register).

3. PHASECHIP UN-PILL: the phase label is a flat tag — no border, no
   rounding, no padding. The Plan chip is the only button-shaped
   thing in the top bar. Drag-surface (pointer-events-none) and aria
   semantics unchanged; the transcript PhaseDivider keeps its look.

4. PANEL DEATH: components/graph-coverage.tsx, its graph-view
   mount, and tests/render-graph-coverage.test.tsx die whole.
   Confirmed: after this, NOTHING client-side consumes GET
   /api/graph/coverage — the only remaining client use of the
   coverage endpoints is the Extract POST (kept, by design); the
   routes stay (Platform's call).

Pure seams exported for tests: ringFill (clamp math),
CoverageMenuBody, CoverageRingGlyph, CoverageRingDetail.
901/901.

Live-verified on :3001: ring fill arcs match the API byte-for-byte
(483/4096 = 4.45/37.70; 1991/4096 = 18.32/37.70); the sidebar block
reads "2k uncovered / 4 messages unextracted"; a real Extract run
went optimistic → dock run → menu settled "fully extracted" AND the
ring drained to the empty arc (the feed chain); graph off → the ring
vanishes; the flat phase label renders chrome-free beside the Plan
pill.
This commit is contained in:
amosblomqvist
2026-09-24 15:26:22 +03:00
parent 2e9fd01c0c
commit bc1f668e2a
9 changed files with 585 additions and 309 deletions
+20 -8
View File
@@ -8,6 +8,7 @@ import { DeleteSessionDialog } from "@/components/delete-session-dialog";
import type { ChatDeleteMode } from "@/src/core";
import { PanelLeftCloseIcon, PanelLeftOpenIcon, PlusIcon, SearchIcon, SettingsIcon } from "lucide-react";
import { ActivitySection } from "@/components/activity-dock";
import { CoverageMenuBlock } from "@/components/extraction-coverage";
import { LhMark } from "@/components/lh-mark";
import { BrailleSpinner } from "@/components/braille-spinner";
import { KeyboardShortcuts } from "@/components/keyboard-shortcuts";
@@ -376,11 +377,13 @@ function SessionsSidebar() {
const listRef = useRef<HTMLDivElement>(null);
/* ── Wave 6 chunk A: context menu + inline rename + deletion ──
Right-click a session row → MenuShell with exactly two actions
(owner ruling): Rename (inline, below) and Delete (the two-option
dialog). The anchor is the row that was right-clicked — assigned
into a ref before the state flip so the shell measures it on its
first (hidden) pass. */
Right-click a session row → MenuShell: the extraction coverage
block (owner-locked redesign, 2026-09-24 — numbers + the manual
Extract affordance, separated by a hairline) above the two
session-management actions (owner ruling): Rename (inline, below)
and Delete (the two-option dialog). The anchor is the row that was
right-clicked — assigned into a ref before the state flip so the
shell measures it on its first (hidden) pass. */
const [ctxChatId, setCtxChatId] = useState<string | null>(null);
const ctxAnchorRef = useRef<HTMLElement | null>(null);
const [renamingId, setRenamingId] = useState<string | null>(null);
@@ -648,16 +651,25 @@ function SessionsSidebar() {
</div>
</div>
{/* The context menu (wave 6): exactly two actions (owner
ruling — no more). MenuShell owns placement/focus/arrow
{/* The context menu (wave 6 + the extraction-UX redesign): the
coverage block above, then exactly two management actions
(owner ruling — no more). MenuShell owns placement/focus/arrow
cycling; the dismissal effect above owns the contract. */}
{ctxChatId !== null && (
<MenuShell
anchorRef={ctxAnchorRef}
testid="session-context-menu"
preferredSide="down"
menuClass="flex w-44 flex-col gap-0.5 rounded-xl border bg-card p-2 text-xs shadow-xl"
menuClass="flex w-52 flex-col gap-0.5 rounded-xl border bg-card p-2 text-xs shadow-xl"
>
{/* The extraction block (owner-locked redesign, 2026-09-24):
what the extractor hasn't covered yet + the manual Extract
affordance — visually separated from the management
actions below by a hairline. Fetched on menu open (the
block mounts WITH the menu); 409 = benign (already
running). Renders nothing on a failed read. */}
<CoverageMenuBlock chatId={ctxChatId} />
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
<button
type="button"
data-testid="ctx-rename"