node-popup¶
The i node popup — a reference lens on node intent; live work belongs to sessions.
raw source¶
The spec and the work it guides are one loop, but they have different durable identities. A node owns current intent and its history; a session owns a live worktree, agent, and terminal. A terminal therefore cannot be a node pane: one session can change several nodes, several sessions can touch one node, and the live surface must survive board navigation. The board makes intent immediately readable; the session interface is where that intent is changed in place.
expanded spec¶
The node popup is the i surface: a fixed pop-out (min(900px,90vw) × min(600px,84vh)) with tabs, opened
over the board and dismissed with Esc. It is reference-only (NodeView.jsx) — no work pane, no
embedded terminal — and it is a lens on the focus, not a pinned document: the popup renders whichever
node currently holds board focus (keyed to it, remounting on change), so a focus move while it is open —
keyboard-nav's Shift+nav walk — swaps the reference in place instead of forcing close-move-reopen; that
is how a run of sibling docs is read. Across such a move the pane selection survives: the new node opens
on the pane being read, and only when it lacks that pane does the popup fall back to the node's own
default — the first of its real tabs — which is exactly what keeps the edit-leads rule below intact (a
mid-change node greets with its edit tab even if the previous node was showing spec). The intent half is
the spec doc — an information board. A stat bar carries the
node's at-a-glance signals, the same the tile speaks: derived status, version, the aggregate eval
score (eval-score-badge), and the drift count when a governed file outran the spec
(source-of-truth) — so score and drift live in the popup now, not only on the tile. Below it the governed
files, then the body as a living current-state document (the two
labelled parts — raw source / expanded spec — when authored that way, else the flat body). Neither part is
an agent-authored current state — what's-done is read from the derived status, never narrated, because
agents hallucinate completion. The proof and
evolution of that intent live in the history tab.
An issues tab lists the unified issue work bound to this node — local and forge, open and closed alike,
with both counts on the tab face (the board shows only the lean counts; see dashboard-issues). Opening
the tab requests page 1 from paged-review with the fixed node: qualifier; no issue row rides the board.
Each entry is the shared compact IssueCard, clamped inside the popup and routed to the internal Issues
page selection (#/issues/<issue-id>), not directly to the forge. A long pane earns an extremely compact
embedded face of the canonical Issues filter: the same query parsing, conjunctive facet semantics, and
real issue fields as issues-view, projected through shared configuration/data adapters rather than a
popup-only filter implementation (review-filters). Short lists skip the affordance entirely. Its state belongs to the pane,
survives tab switches while the popup stays open, and never mints a competing page address; following a
result still lands on the canonical Issues detail route. The eval timeline mounts the corresponding
domain configuration through that same mechanism (eval-tab). An edit tab makes a
node's in-flight change reviewable from the board: it exists only while the node has a pending overlay,
and when it does it leads (first tab, editing-session count on its face), so a node mid-change — a
freshly-added ghost most of all, otherwise near-empty on spec/history — opens with its change front-and-
centre. It lazily fetches the unified diff of the node's spec.md in the editing worktree vs the fork point
(/api/edit), rendered with the history tab's diff view and memoised the same way — re-opening shows the
last diff at once, not a reload — but revalidated each open, since a pending change is live.
panesFor(node) is the single source of which tabs exist and their order — both the tab bar and App's
keyboard pane-nav read it, so number/Tab keys never cycle to a tab that isn't there. The tab CAPTIONS are
plain labels — no visible key-digit markers (the digit keys still switch panes; that vocabulary belongs to
keyboard-nav and the help legend, not stamped on every caption) — and the two review tabs tally their
state through ONE chip primitive: the shared review-chrome ReviewState icon + count, issues as
open/closed, eval as fresh pass/fail read from the same scenarioStates join every score surface uses
(eval-score-badge); a zero count simply doesn't render, on either tab. The compact filter row those
panes share leads with the ONE result summary — showing X of Y from paged-review's current-page item
count and full filtered total, never by treating the current 25 rows as the whole model — full words on desktop, a bare X/Y
under the phone breakpoint with the sentence kept in the aria-label; no second control, no facet echo, no
repeat of what the caption already tallies. When Y > X, a true View all anchor opens the canonical
Issues/Evals list with the same fixed node: and current compact query, so every result remains reachable.
panesFor also
registers an eval pane (a fourth reference face), but that pane's paged timeline data contract belongs
to eval-tab, just as the issues tab's content is
dashboard-issues'; this node owns the popup shell and the spec/history/issues/edit panes, so the eval pane's
reframe into a verdict-over-evidence timeline is that node's evolution, never node-popup drift. The history tab is
the one merged version log: the latest version sits expanded with its proof, older ones start collapsed and
reveal one at a time on the down gesture once you've finished the open one — scrolling past its end, or
a j/↓ keypress when there is nothing left to scroll (a short history with no scrollbar, or the bottom of
a long one). Tying reveal to the gesture, not to scroll movement alone, is what keeps a sub-page history from
dead-ending with older versions forever hidden (a header click also toggles by hand). Disclosure stays
strictly per-entry: there is no expand-all control or bulk-expand replacement, so the down gesture and
row-header toggle remain the complete interaction. The version log itself
fetches when the history tab first shows (lazy, like eval/edit — most popup opens never visit it) and
persists after, so returning to the tab stays instant. A version's proof is
the spec.md line diff it introduced, fetched lazily on expand — every version, memoised by hash (the
latest no longer shipped precomputed); a version with no recorded change says so plainly. That scaffold — scroll container,
latest-expanded reveal, click-toggle, and the per-row header-over-evidence shape — is data-agnostic and
shared: the eval pane (eval-tab) rides the same component (version rows + diff here, result rows
+ screenshot there). Its embedded filter projects the same result-kind discriminator as evals-feed,
never a popup-only legacy alias. The compact Evals filter projects verdict as its Fail/Pass section and
human review as the distinct Needs review / Reviewed facet, so moving the canonical page's primary visual
axis does not fork field semantics inside the popup.
The "change it in place" surface — the live terminal — belongs to the session doing the changing
(Enter; see session-console and command-box), keyed to that session rather than pinned to a node.
The panel sizes to itself, never to xterm's measured width (each pane scrolls its own content, no stray
horizontal scrollbar) — but that sizing lives in styles.css, the dashboard's shared stylesheet governed by
node-graph; this node owns only the popup component, so a style change elsewhere is never drift here.
The original intention survives as one work loop across two truthful surfaces: intent in the node popup,
live change in the session console.