feat(tabs): draw every lineage family, emphasize the selected tab's

Drawing only the selected tab's family hid every other connection until you
clicked into one. All families are drawn again (still as routed trees); the
selected tab's families (what it spawned, and the family it was spawned into)
get .lineage-family--focus: 2.5px instead of 1.5px, full opacity instead of
0.75, a larger end dot, and drawn last so nothing covers them.

Lanes follow strip order (cycling through CodemanLineage.MAX_LANES, 3), never
the selection, so a family's lines never move when you click a tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 01:35:42 +02:00
parent 09daf0fe49
commit 107e87e457
10 changed files with 154 additions and 85 deletions
+1 -1
View File
@@ -177,7 +177,7 @@ Further detail: `resumeHistorySession()` is what sends `codexConfig.resumeSessio
**Rendering is an additional LAYER, not a second pass** (`session-lineage.js`, loadorder 15.6): `_updateConnectionLinesImmediate()` (subagent-windows.js) calls `_appendLineageConnectionLines(svg, rects)` at its tail, exactly like ultracode's two layers, so all of them share ONE batched read→write reflow and the same `tab:<id>` rect cache.
**Only the SELECTED tab's family is drawn** (2026-10-06): the family the active tab spawned, plus the family it was spawned into (its parent and siblings); a tab that is both a child and a parent shows both, in separate lanes. Nothing is drawn while a web tab holds the stage. Drawing every family at once is what the owner called "too confusing": one parent with ten children across a five-row wrapped strip drew ten glowing dashed beziers through every lower row's labels and into the terminal text. ⚠️ Because the drawing now depends on the selection, `_updateActiveTabImmediate()` redraws whenever any lineage exists (`_lineageTotalEdges > 0`), and the incremental tab render redraws on the same condition (a child's `parentSessionId` or working state can arrive there). `_lineageEdgeCount` counts what is DRAWN and still gates the strip-scroll redraw. ⚠️ **A selection keeps resizing tabs AFTER that redraw**: the active tab reveals its gear and close icons by transitioning their padding (~150ms), which can move tabs or re-wrap a row, so the strip's `transitionend` (size properties only: `width`, `max-width`, `padding*`) triggers one more coalesced redraw. Without it the trunk hung under the tab's old position (measured ~10px off, more after a re-wrap).
**Every family is drawn, and the SELECTED tab's is emphasized** (2026-10-07): the family the active tab spawned, plus the family it was spawned into (its parent and siblings), get `<g class="lineage-family lineage-family--focus">`: 2.5px instead of 1.5px, full opacity instead of 0.75, a larger end dot, and drawn LAST so no other family covers it. A tab that is both a child and a parent emphasizes both; a web tab or a tab without lineage emphasizes nothing. ⚠️ Drawing ONLY the selected family was built first and rejected by the owner the same day ("I wanna see all the connections always but when I select one … make thicker"), so do not reintroduce a filter. ⚠️ Lanes follow STRIP order (`index % CodemanLineage.MAX_LANES`, 3, since a 12px row gap fits three lanes at 3.5px), never the selection: only the draw order changes, so a family's lines never jump when you click a tab; families sharing a lane are told apart by colour. ⚠️ Because the emphasis depends on the selection, `_updateActiveTabImmediate()` redraws whenever any lineage exists (`_lineageTotalEdges > 0`), and the incremental tab render redraws on the same condition (a child's `parentSessionId` or working state can arrive there). `_lineageEdgeCount` counts what is DRAWN and still gates the strip-scroll redraw. ⚠️ **A selection keeps resizing tabs AFTER that redraw**: the active tab reveals its gear and close icons by transitioning their padding (~150ms), which can move tabs or re-wrap a row, so the strip's `transitionend` (size properties only: `width`, `max-width`, `padding*`) triggers one more coalesced redraw. Without it the trunk hung under the tab's old position (measured ~10px off, more after a re-wrap).
**Geometry is a tree routed through the gaps** (`computeLineageTree()` + `computeLineageRows()`, constants.js, pure and unit-tested): one rounded orthogonal tree per spawning tab. Every route starts at the PARENT's bottom-center and ends at one child's bottom-center, so a parent's routes share their first stretch exactly and overlay into one trunk with a branch per child; that is also what keeps a working child's dashes in phase with its siblings along the shared part. A route moves horizontally only inside a row gap (the gap under each endpoint's row, at the row's TALLEST tab, since the active tab is 2px taller) and vertically only along a tab's own stem or the SPINE, a channel left of every row that joins the gaps of different rows. In the vertical rail the same idea is one shared track down `--lineage-vertical-gutter`. `test/session-lineage-lines.test.ts` pins "never crosses a tab" as a property over a five-row fixture. ⚠️ **The room is reserved, not borrowed**: `.session-tabs.lineage-tree` (styles.css) adds bottom padding for the last row's gap, and on a wrapped strip a 12px row gap plus a 20px left channel for the spine. `_syncLineageGutter()` toggles it from the top of `updateTabOverflowMode()`, BEFORE the wrap is measured, and keys it on whether ANY family exists, never on the selection: the header's height is the terminal's height, so a class that followed the selection would resize the PTY on every tab switch. ⚠️ **Opacity lives on the family `<g class="lineage-family">`, not on the paths**: translucent paths overlapping along the trunk would stack into a brighter line than the branches. Working routes are appended first so an idle sibling's solid stroke covers the shared trunk. Style is deliberately quiet (1.5px, rounded, a 1px dark outline instead of the old double glow): the lines no longer cross text, so they no longer need a glow to survive it, and a dash now means "this child is working".
+1 -1
View File
@@ -89,7 +89,7 @@ every session or only the active tab.
| Vertical Rail Order | *By activity* (default) sorts the rail the way the home screens are sorted; *Manual* keeps your tab order and drag-reordering. |
| Tall Tabs | Taller tab strip. |
| Pop-out Button on Tabs | Adds the detach control to tabs, with a per-tab override. |
| Spawn Lineage Lines | Lines from the selected tab to the sessions it spawned, and to the one that spawned it. Desktop only, on by default. |
| Spawn Lineage Lines | Lines from each tab to the sessions it spawned; the selected tab's family is drawn thicker. Desktop only, on by default. |
| Auto-name Sessions | Titles a new tab after its first prompt, keeping the case prefix (`w3-myapp: fix the login redirect`). Synced, off by default. See [The Dashboard](The-Dashboard#automatic-session-names). |
| Overview Home Screen | The phone home screen. On by default. |
+5 -4
View File
@@ -86,10 +86,11 @@ scrolled into view. It is not reordered to the front, so the `Alt+N` numbering s
### Lineage lines
When one session spawns another (an agent starting a worker through the API), selecting
either tab draws its family: lines from the parent to each child, in the parent's colour,
routed through the gaps between tab rows so they never cover a tab or the terminal. A dashed
branch means that child is working. It is how a fan-out of eight workers stays readable.
When one session spawns another (an agent starting a worker through the API), Codeman draws
lines from the parent to each child, in the parent's colour, routed through the gaps between
tab rows so they never cover a tab or the terminal. Every family is always shown; selecting
a tab draws its own family thicker and brighter. A dashed branch means that child is
working. It is how a fan-out of eight workers stays readable.
While any tab has spawned another, the strip keeps a little extra room between rows for the
lines, so switching tabs never changes the header height.
+6 -5
View File
@@ -24,13 +24,14 @@ eight workers looks like a stalled terminal for several minutes.
## Session lineage lines
Select a tab and the tab strip draws lines from it to every tab it spawned, and to the tab
that spawned it (with its siblings). That covers the other direction of fan-out: not
subagents inside one session, but whole sessions started by an agent through the API.
The tab strip draws lines from every tab to the tabs it spawned. That covers the other
direction of fan-out: not subagents inside one session, but whole sessions started by an
agent through the API.
The lines form one tree per spawning tab, in that tab's colour, and run only through the
gaps between tab rows, so they never cover a tab name or the terminal. A dashed branch
means that child is working. Tabs with no lineage draw nothing.
gaps between tab rows, so they never cover a tab name or the terminal. Select a tab and its
family (the tabs it spawned, or its parent and siblings) is drawn thicker and brighter. A
dashed branch means that child is working.
Desktop only, on by default, and toggled in **App Settings → Appearance**. Lines are skipped
for tabs scrolled out of view.