Repository navigation
tui: lineage section in the sidebar (fork/subagent tree + activity stats) - #745
Merged
Merged
Conversation
Upgrade the fork log popup (C-x q / q) from a flat "N forks" status line into a live git-log-graph-style tree unifying fork lineage (forked_from, spec 0078) and subagent parent/child relationships (parent_session_id, spec 0014) for the selected session's full ancestry. - crates/cli/src/lineage.rs: App-independent tree construction + layout (recursive, multi-branch, depth/breadth capped with "+N more" markers), reusable by a future dockable panel. - crates/cli/src/app/lineage_popup.rs: wires the tree to live session data, keyboard navigation (j/k/arrows, Enter, m/d, Esc), and the existing merge/discard path (extracted into App::apply_fork_merge and shared with the C-x m minibuffer menu, not reimplemented). - crates/cli/src/ui.rs: renders the popup — distinct glyphs/styles for fork (⑂, dashed) vs subagent (▸, solid) edges, merged/discarded terminal-state styling, a ~1s ticker scoped to "popup open" for elapsed-time/cost freshness. - Jumping into a merged fork's node instead lands on its parent, where the merge actually injected its result message — same event, not a duplicate view. Adds specs/0079-fork-and-subagent-lineage-view.md.
Mirrors the session-list's own emacs-style NextSession/PrevSession convention inside the lineage popup, alongside the existing j/k/arrows.
Sessions with fork/subagent lineage to show now get a hover/click preview on their existing pane title-bar harness label: hovering reveals a small, session-attached box rendering the same tree data the C-x q / q modal shows (reused directly from crate::lineage, not re-derived), with a short grace period so the pointer can travel from the label down onto the preview body. Clicking the label toggles a persistent pin. Ordinary sessions with no lineage register no hit and render the label exactly as before. The modal stays completely unchanged - this is a second, lighter, read-only, session-attached surface layered on top of the existing label, not a replacement. Visibility/hover-grace/pin state mirrors the shape of the (separately deprecated) session-widget hover/pin system as independent state, per specs/0080.
Delete the global C-x q / q fork/subagent lineage modal entirely and port its keyboard vocabulary (j/k/arrows/C-n/C-p navigate, Enter jumps in, m/d merge/discard, Esc backs out) onto the per-session lineage preview instead, reached via a click inside the preview's body or the new C-x Tab toggle (both keymap profiles). There is now exactly one lineage UI, not two. Also sharpens the preview's existing hover/pin styling into two visibly distinct intensities (light bold hover vs. strong bold+REVERSED pin) and gives the preview's own border the same focused/unfocused treatment every other pane border uses. Esc clears keyboard focus only, without un-pinning, so a preview the user explicitly pinned stays visible after they're done navigating it. specs/0079 is marked superseded; its still-true tree-construction and interaction rules are folded into specs/0080, which now documents the combined hover/pin/focus surface.
…eview Three bugs in the lineage preview (spec 0080): - `C-x Tab` opened but never closed. Its own prefix key (`C-x`) wasn't in the focused-preview's key vocabulary, so the "unhandled key clears focus and falls through" contract cleared focus on the FIRST key of the chord, before `Tab` ever arrived to complete it — by the time the chord resolved, focus already read as "not focused" and the toggle re-opened instead of closing. Both keys of the chord (Ctrl-X, Tab) now fall through without touching focus, so the toggle handler sees the true prior state. - Clicking away from a focused preview didn't clear focus — there was no code path for it at all. Falling through every lineage-specific click check (harness label, body, border) with none matching now clears focus, mirroring how any other focused element in this UI behaves. (Un-pinning still only happens via the harness label or C-x Tab, same as before — a click elsewhere loses focus, not visibility.) - The preview slid right along with the Program popup while terminal focus was active (`C-x C-o`), since it was anchored to the popup's own `safe_block_inner` — derived from the popup's `rect`, which `program_popup_visible_rect` shifts right for the slide. It now anchors to a rect derived from `base_rect` (the pane's fixed boundary) instead, computed before `block` is consumed so it survives the slide untouched. Regression tests added for all three, including two that go through the real `on_key`/`on_mouse` dispatch (not the underlying methods directly) since that's exactly where the C-x Tab bug lived.
The previous fix for "click elsewhere loses focus" lived inside handle_left_click, which only runs for clicks that reach it. A click over a pane whose child has grabbed the mouse (DECSET ?1000h — common for interactive CLIs like Claude Code) is instead handed straight to that child's PTY by forward_mouse_to_child and returns before handle_left_click ever runs, on any pane including the same session's own content underneath a focused preview. So clicking "elsewhere" often did nothing, exactly the class of bug PR #735 already hit and fixed for the tutorial card. Moved the check earlier, into on_mouse itself, right alongside the title-rename commit-on-click-away logic it mirrors: a side effect only (it doesn't consume the event), so the click still proceeds to forward_mouse_to_child, pane-focus switching, or wherever else it was headed. The handle_left_click copy stays as a second layer for whatever reaches it directly.
…ail annotations Instead of each node in the fork/subagent lineage preview showing its own cumulative message count and elapsed time, activity stats now render as separate, non-selectable rows on the rail, positioned between the markers that bound them: a node's own creation, each fork child's fork-out point, each fork child's merge-back point (only for an actual merge — a discard never injects anything into the parent's transcript, so it isn't a boundary), and "now" (or the node's own terminal point, if it has one). A node's own line is now just rail + edge glyph + status + harness [+ title] [+ merged/discarded marker]. This is possible with no new fetches because `SessionSummary::event_count`, `ForkedFrom::transcript_seq`, and the new `ForkMerge::merged_seq` are all the same transcript sequence counter, so segment boundaries and their deltas are plain arithmetic over data already in memory. `merged_seq` is stamped by `SessionManager::merge()` from the PARENT's event_count at merge time — the parent-timeline counterpart to `ForkedFrom::transcript_seq`. Design decisions left open by the brief: - Subagent children don't stamp a parent-timeline position the way forks do, so they never act as boundary markers on the parent's timeline — they're just recursed into in place. A node whose only children are subagents falls out of the same loop with one whole-life trailing segment, same as a true leaf. - A childless node still gets exactly one segment — its whole life, from creation to "now" (or to its own merge time, if it's itself a fork that has since merged/discarded) — so every node's activity is visible somewhere, not just nodes with forks. - A zero-length window (two consecutive checkpoints land on the same transcript_seq) is skipped everywhere, not just the trailing "to now" case, so e.g. root doing nothing while a fork was outstanding renders no "0 msgs" line. - Cost (`SessionSummary::cost_usd`) is dropped from the lineage view entirely rather than attributed to a segment — it's a single cumulative total with no per-checkpoint snapshot the way event_count has. `crates/cli/src/lineage.rs`: `flatten` now takes the sessions slice (needed to derive segments) and interleaves a new non-selectable `LineageRowKind:: Segment` row kind into the flattened output; `rail_prefix` renders segments with a plain continuing bar, never a branch connector; `stats_label` is replaced by `segment_label`. `crates/cli/src/ui.rs`'s `render_lineage_row` drops the per-node stats call and adds a `Segment` render arm styled distinctly (`theme.dim`) from a node's per-state label color. `crates/protocol/src/lib.rs` adds `ForkMerge::merged_seq` (`#[serde(default)]` for backward compatibility with merge records persisted before this field existed). `crates/daemon/src/session.rs`'s `merge()` looks up the fork's parent and stamps its current event_count. Verified the existing depth/breadth-cap and narrow-terminal preview tests still pass unmodified with the extra segment rows — the preview's height/scroll math already scales with row count, no adjustment needed. Updated specs/0080-lineage-preview-on-harness-label.md with the new per-segment design.
…ail to timeline-style vertical lines
A click landing on the lineage preview body reached forward_mouse_to_child
before handle_left_click whenever the pane underneath had a mouse-tracking
child (very common — Claude Code and others enable DECSET ?1000h), so the
click never focused the preview at all: it just got forwarded into the PTY
underneath and swallowed. is_over_lineage_preview's own doc comment already
stated the intent ("swallow clicks ... so it doesn't act as a click-
through") — this closes the other half of that: exclude the preview's own
area from forward_mouse_to_child's hit-test, mirroring how card_owns_event
already protects the tutorial card. New regression test drives the real
Down-then-Up on_mouse path through a mouse-tracking pane and confirms both
that focus is gained AND that the click does NOT also forward to the child
(verified failing without the fix, then passing with it).
Also switches the lineage rail from a git-log-graph `├─`/`└─` branch-corner
style to a pure vertical `│` connector, mirroring the `:::timeline`
markdown extension's rendering convention used elsewhere in this UI. A
node's own edge glyph (◆/⑂/▸) already marks "a branch starts here", so the
corner glyph was redundant; is_last now only affects whether rows NESTED
under a given row draw a continuing rail in that column, never the row's
own prefix. Updated the two rail-prefix tests in lineage.rs and spec 0080's
worked example (which also corrected a pre-existing inaccuracy: "↩ merged"
renders inline on the fork's own row, not as a separate line).
…pt design
Replaces the flat rail-prefixed row list with a 2D diagram implementing
the concept sketch from the program doc: each session is a bordered box
(status glyph + title/harness [+ terminal marker]); its own timeline is a
vertical lane hanging below the box; a fork branches off the parent's
lane with a labeled arrow (├─⑂ fork ──▸) into its own box placed to the
right; a merged fork returns to the parent's lane with a labeled merge
arrow (│◂─ ↩ merge ──┘); subagents branch with a ▸ subagent arrow and
never return. Turn info renders ON the lanes between the markers that
bound each window, and — new with this layout — the parent's activity
WHILE a merged fork was out renders side-by-side with the fork's own
lane, level with it, exactly as the concept draws it. (The old linear
walk silently dropped that window: its checkpoint jumped from fork-out
seq to merged_seq without emitting the gap, undercounting the parent's
own totals. The new layout emits it, so every parent message is now
attributed to exactly one visible window.)
lineage.rs's row model changes from {depth, is_last, rails, kind} rows
with a rail_prefix() to role-tagged span runs (Rail / Border / Edge /
Segment / Node / More) cut from a character-grid canvas, still free of
ratatui types so the full diagram is unit-tested as plain text —
including a snapshot test locking the concept scenario's exact rendering
and a wide-char (CJK title) box-alignment test. flatten() now takes
now_ms (labels are composed at layout time; the preview rebuilds from
live state every frame, so they never go stale). ui.rs's
render_lineage_row shrinks to a role→theme-style mapping; the unfocused
preview no longer wraps lines (wrapping would shear the drawing — wide
diagrams clip at the preview's edge); the render guard switches from
row-count to selectable-node-count since a lone session now produces
several rows of box. Keyboard selection lands on box label rows — the
only selectable rows — so the existing nav/merge/discard vocabulary and
all hover/pin/focus mechanics carry over unchanged.
Box labels follow the concept's "<status> <name> (<harness>)" shape,
falling back to just "<status> <harness>" for untitled sessions, with
titles truncated at 24 chars. Cost/tokens remain absent from turn info:
there is still no per-checkpoint snapshot to attribute them to a window
(unchanged decision from the segment-stats round).
Spec 0080 updated: new "boxed-lane diagram" section and the worked
example replaced with the concept-shaped rendering.
…election highlight, lane/label geometry Five refinements to the boxed-lane lineage diagram, per review: 1. The keyboard-selection highlight now covers exactly the selected session's box rectangle — its border fragments and label, across all three of its rows — instead of a full-width List bar. LineageSpan:: Border gained a session_id tag so the renderer can pick out one box's fragments even on rows shared with wiring; the focused branch drops ratatui's List highlight_style entirely and applies the style inside render_lineage_row. 2. Fork and subagent arrow labels render at the same brightness (theme.dim) — the word already distinguishes them; the subagent label's harness accent read as an unrelated emphasis. 3. Branch arrows put a space between the shaft and the glyph: "├─ ⑂ fork ──▸" / "├─ ▸ subagent ──▸". 4. Rows now follow the node's timeline in strict chronological order. layout_node collects the node's fork-out / merge-back / subagent-spawn events, sorts them by time (branches before merges on a tie), and walks them top to bottom — so fork A, fork B, merge A renders exactly those three connectors in that order, instead of grouping A's merge arrow with A's block. A fork whose merge comes later keeps its lane column "live": later branches allocate columns to its right and their arrow shafts bridge over its bar (the bar wins the crossing cell, the dashes break around it — put_if_empty), and the lane back-fills down to its merge arrow when that event finally renders. Each turn-info window now emits at its CLOSING marker's row, which also replaces the previous side-by-side "during" placement with plain sequential rows. 5. Geometry per the review sketch: the lane hangs one column in from the box's left edge (was two), and turn info outdents two columns from the lane, flowing over it — matching the original concept's "(turn info)" interrupting the line. The snapshot test is recomputed for the new geometry (every column position re-verified by hand); a new test locks the fork-A/fork-B/ merge-A ordering, the live-lane column stacking, the arrow bridging, and the chronological segment sequence; another locks the box-scoped highlight (only the selected box's spans may carry the highlight bg). Spec 0080 updated to match.
… final window
Turn-info lines now render as "• N msgs · elapsed" with the bullet
sitting ON the lane (where the bar would be) and the text to its right —
replacing the outdent-two-columns placement — matching the concept
sketch's "• (turn info)" notation. A node's FINAL turn-info line appends
a terminal-outcome glyph when its session has ended: ✓ for Done (styled
matrix_flash_good, the checklist marks' "done" color) and ✗ for Errored
(theme.warning). Mid-timeline windows and still-live sessions keep the
plain bullet; a fork's merged/discarded outcome is deliberately not
repeated on its turn info — the merge arrow and the box label's
"↩ merged" / "✗ discarded" marker already carry it.
Two new span roles (SegmentBullet, SegmentOutcome{ok}) keep the glyphs
independently styleable; put_segment now owns the whole line's layout
and returns its right edge, and child-box column allocation accounts for
the bullet's two extra columns. Snapshot recomputed (hand-verified);
new tests cover ✓ on Done, ✗ on Errored, no glyph while live, and that
only the final window of a Done parent carries the glyph — never its
mid-timeline windows or a still-open fork's.
The previous layout was chronological only within a single node's own event list, and a child's whole block (box + its turn info) was emitted at branch time — so a fork's own turn-info line rendered right below its box even though that window actually closed much later (at its merge), and a lane's terminal moment (session going Done/Errored) always sank to the bottom regardless of when it happened. Events across DIFFERENT lanes never interleaved at all. The layout is now a flat, global timeline: the recursive layout_node is replaced by layout_tree, which flattens the tree into lanes (collect_ lanes), builds one queue of every event in the whole diagram — box appearances (fork-out / subagent spawn / root creation), merge-backs, and lane ends (discard time, Done/Errored at last_event_at, or "now" for live lanes) — sorts it by time (branches before merges on ties, child times clamped to parents' so clock skew can't invert the tree), and walks it top to bottom. Consequences: - Every arrow and every turn-info line sits at its position in global event-time order: a subagent that finished before a later fork branched shows its ✓-marked final window above that fork's arrow. - A turn-info window renders at its CLOSING event's row, on its own lane — so windows closing at the same instant share one row side by side, restoring the concept sketch's paired "(turn info) (turn info)" for a merged fork's life next to its parent's while-it-was-out window, and putting all live lanes' trailing windows together on the final "now" row. - Lane columns are allocated at box time past every live lane's widest possible label (a per-lane pre-pass dry-runs the event walk to compute max label widths), so side-by-side turn info can never collide; lane bars are back-filled at the end from each box to its lane's last row. Snapshot recomputed (the merge row now carries both windows); a new test locks the terminal-state-between-events placement; the ordering tests' expected sequences updated to the global-timeline order with comments explaining each window's closing event. Spec 0080 updated.
…sting, compact rows
Four refinements to the lineage diagram:
1. A merged fork's box label drops its "↩ merged" marker — the merge
arrow and its final window's ✓ already carry the outcome. A discarded
fork keeps "✗ discarded" (a discard draws no arrow).
2. The terminal-outcome glyph now LEADS a lane's final turn-info line in
place of the bullet ("✓ 2 msgs · 3m20s") instead of trailing it, and
covers every terminal event kind: fork merged → ✓, fork discarded →
✗, session Done → ✓, session Errored → ✗; live lanes keep the plain
bullet.
3. Lane columns nest by close time (new assign_offsets pre-pass):
overlapping sibling lanes stack outward with the later-terminating
lane further out, regardless of branch order — so an inner lane's
merge/end arrow returns to the parent without crossing an outer lane
that's still running, and closing arrows nest instead of crossing.
Sequential siblings whose lifetimes don't overlap still reuse the
inner column. Column offsets are now assigned statically upfront
(footprints computed bottom-up over the lane tree) rather than
greedily at branch time, since the ordering needs lookahead: a lane
that branches first but closes last must leave room inside itself.
A new test locks the case where A branches before B but stays open
while B merges — B nests inside A and B's merge arrow ends before
A's column begins.
4. Rows pack tight: the blank spacer rows around turn-info lines and
arrows are gone. The canonical single-merged-fork diagram drops from
15 rows to 10, with the lane bars that remain coming only from box
sides, arrows, and the end-fill.
Snapshot recomputed; spec 0080 and the module docs updated to match.
…, box wrap, styling Eleven refinements to the lineage preview, per review: - Session boxes wrap long names: the label wraps at a max interior width (28 cols) onto extra box rows up to a 2-line cap, then ellipsizes; box bounds and the whole layout account for variable box heights. - Forks are never dimmed for being forks: the session LIST no longer dims forked_from sessions (archived stays muted), and lineage box labels always use live-state styling; a discarded fork keeps its strikethrough on top of its state color. - The selection highlight fills only the box INTERIOR (bg) and brightens the border LINE (fg only — no background behind border glyphs). - Boxes are mouse targets: flatten_with_boxes exposes each box's canvas bounds; the renderer maps them to screen hit-rects (clipped to the scrolled viewport). Hovering a box brightens its border; clicking it jumps to that session and closes the preview, exactly like Enter. Clicking the body outside any box still focuses the widget. - Opening the preview from a fork/subagent starts the keyboard selection on that session's own box, not the tree's root. - The preview scrolls vertically and horizontally (mouse wheel / horizontal wheel) in every mode; keyboard selection still drags the viewport while focused. Horizontal clipping slices span runs at the cut, padding a straddled wide char. - One blank padding row between the last content line and the bottom border. - The widget border uses the lineage accent (matrix_flash_good — the harness-label hover/toggle highlight color) instead of the session pane border colors; keyboard focus makes it bold. - Turn-info lines always get a lane-bar row above them; a lane's final window has nothing below (the lane ends), mid-timeline windows are followed by structural rows that carry the lane onward. - The preview is drag-resizable: left border adjusts width, bottom border height, corner both; the override sticks until closed. Resize participates in the drag-gesture guards like every other construct drag. - Initial size is content-based (diagram width/height + borders + padding, clamped to the pane) instead of a fixed 60%-width sliver — wider by default and never clipping a diagram that would fit. New tests: label wrap + ellipsis + border alignment, box bounds reporting, open-from-fork initial selection, box click-to-jump through handle_left_click, wheel scroll + border drag-resize through on_mouse, interior-vs-border highlight semantics, fork-labels-style-like-normal. Spec 0080 updated (border accent, sizing/scrolling/resizing, mouse targets, wrap, turn-info bar rule).
The box-hit viewport clip skipped boxes above, below, and left of the visible viewport but not ones starting past its RIGHT edge, so `right.min(view_right) - vis_x` underflowed (debug-build subtract-with- overflow panic at render time) whenever the diagram was wider than the widget — trivially reachable with a nested fork, whose box starts deep to the right, in a content-clamped or user-resized preview. Complete the skip guard with the missing `b.x >= view_right` side and make the clipped-edge arithmetic saturating so no future geometry can underflow. Regression test renders a fork-of-a-fork into a deliberately narrow (drag-resized) preview — verified to reproduce the exact panic without the fix — and asserts the fully-clipped box registers no hit while every registered hit stays inside the preview's inner rect.
…s + shift-wheel Layout: a lane had to stack outside EVERY overlapping earlier-closing sibling, so a fork branching late (after inner lanes had already closed) piled a new column onto the far right even though only a still-open, never-merging sibling overlapped it — the diagram grew one column per open fork. The outside-stacking constraint actually only matters for siblings that MERGE (their return arrow to the parent lane would cross a live lane inside them); overlapping siblings that never merge (open forks, discards, terminal sessions) draw no returning arrow. Slot assignment now takes the innermost slot whose occupants don't overlap, subject to staying outside overlapping MERGING siblings only — a late open fork reuses the column a merged fork freed, cutting the overall width (new test mirrors the reported screenshot shape: merged fork + early open fork + late open fork now needs two slots, not three). Preview chrome: vertical and horizontal scrollbars render when the diagram overflows the viewport — background tints (track 30% / thumb 80%, the terminal scrollbar's opacity approximation) along the right inner column and the bottom-pad row, preserving diagram glyphs underneath. Shift+wheel scrolls sideways for terminals without a horizontal wheel, alongside the existing ScrollLeft/Right support.
… the top border Adds a second visualization so the two layouts can be compared live: a compact git-graph-style view — one 2-column rail per session (columns reused once a lane closes, classic git-graph column allocation), events as one-line entries in the same global time order the boxed layout uses, ├─┐ / ├─┘ connectors curving between rails (breaking around live rails they cross), and all labels/turn-info in a single left-aligned text column right of the rails: ● ● root (claude) • 12 msgs · 5m00s ├─┐ ⑂ ● idea A (claude) • │ 3 msgs · 3m20s │ ✓ 2 msgs · 3m20s ├─┘ ↩ merge • 5 msgs · 5m00s A toggle button on the preview's top border (labeled with the current mode) switches between "boxes" and "rails". flatten_rails reuses the whole lane/event pipeline (collect_lanes, build_events, checkpoint walk) and emits the same row/span model, so keyboard selection, box hover / click-to-jump hit rects, scrolling, and resizing all work identically in both modes; toggling resets scroll offsets since the geometries don't correspond. Tests: a rails-mode snapshot of the canonical merged-fork scenario, rail-column reuse for sequential forks, and an end-to-end toggle test (click switches mode, diagrams genuinely differ, interactive surfaces stay registered, click again switches back — re-reading the toggle hit since the content-sized preview moves when the diagram shrinks). Spec 0080 updated.
1. Selecting or hovering a session now lights up its ENTIRE timeline in
both view modes. Every rail glyph, lane bar, connector, and turn-info
span is tagged with its owning session (rail/bar/arrow glyphs moved
from the anonymous Rail role to Border{session_id}; the three
Segment* roles gained a session_id), and the renderer brightens
everything owned by the selected/hovered session — rails, branch and
merge arrows, bullets, window text (outcome glyphs keep their hue and
go bold), plus a bold label on hover. Nothing belonging to other
lanes changes.
2. Boxes mode is more compact:
- Arrow labels are icon-only: ├─⑂──▸ for forks, ▸ for subagents,
│◂─↩──┘ for merges — the words "fork"/"subagent"/"merge" are gone,
shrinking the minimum branch offset (especially for subagents,
whose word was 10 columns wide).
- Column allocation now matches git-graph in both modes: pure
innermost-non-overlapping interval reuse, dropping the rule that
forced a lane outside every overlapping MERGING sibling. A merge
arrow crossing a live lane placed inside it bridges over its bar,
exactly like git-graph merge lines — trading the never-cross
guarantee for narrower diagrams, per review.
Snapshot and arrow-text expectations updated; new test locks the
ownership-scoped highlight (hovering f lights only f's spans); the
right-edge clip regression test nests one level deeper since the
compact layout made its two-level diagram fit the narrow viewport.
Spec 0080 updated.
Turn-info labels no longer reserve horizontal space in the boxed-lane layout. Previously every child column had to clear its parent's widest window label (max_seg_w + 5), pushing boxes far right of short arrows — the reported screenshot showed first-level boxes starting ~20 columns out. Now: - Column offsets are the arrows' minimum (8) plus packed slot widths, where a lane's footprint is just its box and its children — the per-lane max-label-width dry-run pass is deleted entirely. - A window's text simply runs underneath any lane bar it crosses: the text wins the cells and the bar shows a gap on that row (the same "lines go under" reading git-graph gives crossing edges). - Two windows that would collide on a shared closing row (a merge's parent+fork pair, an End group's simultaneous closes) stagger onto adjacent rows: emit left-to-right, bumping to the next row whenever a lane's marker would land inside the previous window's text. Arrow glyphs also gain breathing room per review: `├─ ⑂ ──▸`, `│◂─ ↩ ──┘`. The canonical snapshot shrinks from a 38-column diagram to 20 columns; tests that assumed shared closing rows now assert the staggered adjacent-row layout, and the outcome-glyph test anchors on span ownership instead of row position. Spec 0080 updated.
…text-color border; activity-capped elapsed 1. Session labels in both lineage modes now style like the session list: only the status glyph carries the live-state color (a Done session's check mark goes blue-ish), while the name/harness text keeps the default text color. The glyph is split into its own NodeStatus span in both draw_box and the rails label painter; selection fill and hover bold treat it as part of the label. 2. The preview widget's border switches from the lineage accent to the default foreground text color — still distinct from the session panes' border colors, bold while keyboard-focused. 3. A live lane's final turn-info window now measures its elapsed up to the session's last activity (last_event_at) instead of ticking toward "now" — the number reads as execution time for the turns in the window, not idle wall-clock. The lane's row position on the timeline still sits at "now"; sessions without a recorded last_event_at keep the open-ended fallback. New tests: Done glyph blue / name text-colored in both modes; live elapsed frozen at last_event_at (an hour of idle adds nothing). Border and highlight tests updated for the new colors and the NodeStatus span. Spec 0080 updated.
…t, renamed modes 1. The hover/selection highlight color now matches the preview widget's own border (default text color, bold) so highlighted lines read as the same chrome. 2. The mode toggle labels rename: "boxes" → "lineage", "rails" → "lineage (compact)". The preview's minimum width now accounts for the toggle so the longer label never vanishes on tiny diagrams. 3. Boxed-lane vertical lines start two columns in from the box's left edge (was one). 4. Box placement is now minimal-x, eliminating the large empty regions the slot-footprint model produced (the reported "second (claude)" pushed far right past the widest sibling). Since every event gets fresh rows, box rectangles can never collide — so the whole footprint/slot machinery (assign_offsets, per-lane child lists, x_off) is deleted. The only hard constraints kept: the branch arrow's minimum reach from the parent's lane, and lane-column uniqueness among lifetime-overlapping lanes (scan right by 2 until free). Everything else overlaps: boxes reuse columns across rows, lane bars gap behind boxes/turn-info they pass under, and a merge arrow crossing a live lane bridges over its bar like git-graph merge lines (locked by a rewritten bridging test, since the "later-closing sits outside" rule this replaces no longer holds).
…es, highlight color
…ge previews; dim resting names 1. The ⑂ / ▸ branch-arrow glyphs now light up with their branching session: the Edge span gained a session_id (the ↩ merge glyph was already owned by the merging fork via its Border span), so selecting or hovering a session brightens every glyph on its timeline along with its rails, arrows, and turn info. 2. Pinned lineage previews survive TUI restarts: tui-state.json gains lineage_pinned_session_ids (restored on launch — pins for vanished sessions are harmless since the preview only renders with lineage to show) and lineage_view_compact (the boxes/rails toggle), the same pattern open_program_session_ids uses. Daemon restarts already preserved the in-memory pins. 3. Unhighlighted session names in both modes render slightly recessed (Modifier::DIM on the default text color), so the hovered/selected session pops; the status glyph keeps its full-intensity state color. A lane-spread placement experiment for this round was reverted after review confirmed the existing minimal-x behavior is as intended.
…ox spans Two boxed-lane layout refinements from screenshots: 1. When a box would be drawn directly on top of a live lane's very first bar row (fork branches, next event's box immediately covers its lane column — the lane seemed to start nowhere), one spacer row is inserted so the lane's timeline visibly begins under its own box before passing behind the covering box. 2. A lane now hangs anywhere under its box's interior span, not at a fixed +2 indent: placement picks the leftmost free column at least 6 columns away from every lifetime-overlapping lane (or the best separation available), so concurrent vertical lines stop running side by side — without widening the diagram, since the box's own span bounds the choice. Simple diagrams are unchanged (no competition → the classic 2-column indent wins). A consequence: concurrent sibling boxes now pack at the SAME minimal x (their rows never collide; only their lanes must differ), so the three column-order tests assert equality instead of ordering. New regression test drives the reported scenario (fork then an immediately-covering subagent box): asserts the spacer row shows the fork's bar under its own box and the two lanes sit ≥6 columns apart. Spec 0080 updated.
Turn-info durations previously measured the wall-clock gap between a window's boundary events, which counts idle sitting-at-a-prompt time. Now they show the session's actual compute time within the window: - protocol: SessionSummary grows busy_ms (banked working time) + busy_running_since_ms (open Running span) with busy_ms_at() to read the total mid-span; ForkedFrom.parent_busy_ms and ForkMerge.merged_busy_ms snapshot the parent's total at the two boundaries that split a timeline. All serde(default) — old records read as zero stamps. - daemon: set_state_tracked() routes every session-state mutation so entering Running opens a span and leaving banks it; merge() stamps merged_busy_ms from the parent's current total. - client: fork_session stamps parent_busy_ms the same way. - cli: each lane carries its fork/merge/lifetime busy snapshots; every turn-info window (both layouts) labels the delta between its two boundary snapshots, and a lane's final window drains the remainder of the lifetime total. Windows without busy data (records that predate tracking) keep the legacy wall-clock span. Spec 0080 updated with the compute-time semantics and fallback.
The "N msgs" figure was a raw transcript-sequence delta, so tool blocks, status rows, and PTY ordering markers all inflated it. Windows now count actual Message events: - protocol: SessionSummary.message_count (daemon-maintained tally of persisted Message events), ForkedFrom.parent_message_count and ForkMerge.merged_message_count boundary snapshots — the message-only counterparts to event_count / transcript_seq / merged_seq. All serde(default). - daemon: increments the tally as Message events persist, zeroes it on session reset, stamps it at merge, and recounts it from the transcript at load so it self-heals for records saved before the field existed (or that lagged a crash). - client: fork_session stamps the parent's tally at fork. - cli: both layouts label each window with the message delta between its boundary snapshots; records without tallies fall back to the raw transcript-event delta (unchanged legacy rendering). Window existence/skip logic still keys on transcript events, so diagram structure is unchanged. Spec 0080 updated with the count semantics.
The floating hover/pin lineage preview anchored to the pane title's harness label is replaced by a collapsible section of the left sidebar, stacked sessions | lineage | operator (spec 0081; 0080 superseded): - The section renders the SELECTED session's fork/subagent tree whenever it has lineage to show — no hover trigger, no pin, no per-session open state. Master-detail: it follows the list selection and highlights the selected session's node. - Bare Tab (list pane focused) switches keyboard focus sessions ⇄ lineage — an on_key intercept scoped to list focus, so PTY sessions keep receiving Tab. C-x Tab still toggles section focus from anywhere. The row vocabulary (j/k/C-n/C-p, Enter jump, m/d merge/discard, Esc) carries over unchanged. - Header row: collapse toggle (click), view-mode toggle (full ⇄ compact; compact is now the default — it's built for the narrow column). Body: box click jumps, click focuses, wheel scrolls (shift = horizontal), scrollbar tints; content-capped height (never below the list minimum, never more than half the rows region). - Deleted: hover grace timers, pin state, drag-resize, floating-box anchoring, harness-label trigger + hit plumbing, and the mouse-forwarding exclusions the overlay needed. - Persistence: lineage_pinned_session_ids replaced by a global lineage_collapsed flag; lineage_view_compact now defaults on. - Session list: forks already nested indented under their parent — now locked in with a list_items-level test. Tests rewritten for the section (render placement, header collapse, mode toggle, box-jump, body-focus, blur, wheel, right-edge clipping, Tab focus switch, C-x Tab chord, fork indentation).
A fork of a fork was invisible in the list: excluded from the top level for having forked_from, but never pushed under its parent either (fork nesting was single-level). push_session now walks fork descendants depth-first (one indent level, matching the flat subagent convention, with a depth cap as a cycle guard), so every live fork appears under its ancestor row. Spec 0081 notes the recursive nesting.
Conflict: crates/protocol/src/lib.rs — both sides appended after SessionSummary (busy_ms_at impl vs NativeSubagentRef); kept both. New SessionSummary literals on each side gained the other's fields (busy/message tallies on the native-subagent literal; native_subagent on the lineage test fixtures).
…ight Ten refinements to the sidebar lineage section (spec 0081 updated): - Header is now a ─ rule carrying the label and controls, matching the operator panel's title bar: ' ⑂ lineage ' label (accent+bold only while the section owns keyboard focus), 'full ⇄ compact' mode toggle, and a −/+ collapse button at the right end. The bare rule doubles as a height drag handle (up grows); the dragged height persists in tui-state.json alongside collapse + mode. - One blank padding row above and below the diagram. - Focus highlight is exclusive between the sidebar's regions: focusing the lineage section takes the highlight off the sessions title bar (session_rows_focused) and vice versa. - Selecting a subagent no longer hides the section: has_lineage now counts a session's own upward parent link, not just downward edges and its fork origin. - Hit targets widened from boxes to every owned cell — lane bars, branch glyphs, turn-info markers/text — so hovering any of them highlights the session across the diagram and clicking jumps to it (span-based hits via LineageSpan::owner()). - Horizontal scrolling: ScrollLeft/Right kept, and a plain wheel now scrolls sideways when sideways is the only overflowing axis (few rows, wide diagram — the common compact-mode shape). - Status glyphs animate with the session list's shared spinner and gate; a working session's live turn-info bullet spins in phase (historical bullets stay static). Tests: header rule + padding render, collapse button toggle, drag resize + height persistence round-trip, plain-wheel sideways fallback, ScrollLeft/Right, subagent-selection keeps the section, lane/turn-info click jump, focus exclusivity, spinner substitution (node + live bullet, static elsewhere).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gives fork/subagent lineage a permanent home in the TUI: a collapsible
lineage section in the left sidebar — stacked
sessions | lineage | operator— showing the selected session's full lineage tree withper-window activity stats. (The feature went through two earlier shapes on
this branch — a full-screen popup, then a floating hover/pin preview
anchored to the pane title's harness label — both now deleted; specs 0079
and 0080 are superseded by spec 0081.)
The lineage section
(it was forked from a parent, or has fork/subagent descendants) and
disappears entirely otherwise — no hover trigger, no pin. Master–detail:
it follows the list selection and highlights the selected session's node.
diagram (each session a box with its vertical timeline lane, fork/merge
arrows, minimal-x lane packing) and git-graph-style compact rails
(the default — built for the narrow column).
• N msgs · Tbetween lineageboundaries,
✓/✗on terminal windows.Ncounts actual chat messages(daemon-maintained tally, snapshotted at fork/merge boundaries, transcript
recount on load);
Tis summed compute time (Running-state spans),not wall clock. Records predating the tracking fall back to
transcript-event counts and wall-clock spans.
full ⇄ compacttoggles the mode.Body: click a session box to jump to it (a merged fork jumps to its
parent, where the merge result lives), click elsewhere to focus, wheel
scrolls (shift = horizontal), scrollbar tints on overflow.
Content-capped height: never squeezes the list below its minimum, never
takes more than half the rows region.
Tab(list pane focused) switches focus sessions ⇄lineage — scoped as an
on_keyintercept so PTY sessions keep receivingTab.
C-x Tabtoggles section focus from anywhere. While focused:j/k/arrows/C-n/C-pnavigate,Enterjumps in,m/dmerge/discard (same path as
C-x m),Escbacks out; any other keyfalls through to normal routing.
tui-state.json).Session list
recursively — a fork of a fork appears too — never as their own
top-level rows; parents badge their live fork count (
⑂N).Protocol / daemon (for the stats)
SessionSummary.busy_ms/busy_running_since_ms/message_count,ForkedFrom.parent_busy_ms/parent_message_count,ForkMerge.merged_busy_ms/merged_message_count— allserde(default),maintained by the daemon on state transitions/persisted events, stamped
at fork (client) and merge (daemon) boundaries.
Tests
agentd-cli818 + reconnect,agentd --lib192, protocol 47, client 6 —all green. New coverage: section render placement, header collapse, mode
toggle, box-jump, body-focus/blur, wheel scroll, right-edge hit clipping,
Tab/C-x Tab focus flows, recursive fork indentation, busy-time and
message-count window labels (both layouts) with legacy fallbacks, daemon
span accumulation + boundary stamps + load recount.
🤖 Generated with Claude Code