mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
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:
@@ -300,11 +300,11 @@ function computeTabScrollLeft(input) {
|
||||
// spawned (a worker started through the codeman agent skill, which passes its own
|
||||
// id as parentSessionId). Pure: the caller measures and appends, this decides.
|
||||
//
|
||||
// ONE TREE PER SPAWNING TAB, and only the selected tab's family is drawn (that
|
||||
// filter lives in session-lineage.js). Every route starts at the PARENT and ends
|
||||
// at one child, so a parent's routes share their first stretch exactly: overlaid,
|
||||
// they read as one trunk with a branch per child, and a dashed (working) route
|
||||
// stays in phase with its siblings along the shared part.
|
||||
// ONE TREE PER SPAWNING TAB. Every family is drawn, the selected tab's emphasized
|
||||
// (session-lineage.js). Every route starts at the PARENT and ends at one child, so
|
||||
// a parent's routes share their first stretch exactly: overlaid, they read as one
|
||||
// trunk with a branch per child, and a dashed (working) route stays in phase with
|
||||
// its siblings along the shared part.
|
||||
//
|
||||
// ⚠ ROUTES RUN IN THE GAPS, NEVER THROUGH A TAB. This replaced one bezier per
|
||||
// child hanging below the strip, which in a wrapped strip crossed every lower
|
||||
@@ -326,9 +326,12 @@ function computeTabScrollLeft(input) {
|
||||
// rect, lying over the logo or the header buttons. Skipping is honest; clamping
|
||||
// would point at a tab that is not there.
|
||||
const LINEAGE_CORNER_RADIUS_PX = 10;
|
||||
// Families drawn together (the selected tab is both a child and a parent) take
|
||||
// separate lanes: gap lines this far apart, spines LINEAGE_SPINE_STEP_PX apart.
|
||||
// Families drawn together take separate lanes: gap lines this far apart, spines
|
||||
// LINEAGE_SPINE_STEP_PX apart.
|
||||
const LINEAGE_LANE_STEP_PX = 3.5;
|
||||
// Every family is drawn at once, and a 12px row gap only fits this many lanes at
|
||||
// LINEAGE_LANE_STEP_PX; session-lineage.js cycles families through them.
|
||||
const LINEAGE_MAX_LANES = 3;
|
||||
const LINEAGE_SPINE_INSET_PX = 6;
|
||||
const LINEAGE_SPINE_STEP_PX = 4;
|
||||
// The last row has no row below it; with no strip rect to measure, its gap is
|
||||
@@ -1074,6 +1077,7 @@ if (typeof window !== 'undefined') {
|
||||
computeRows: computeLineageRows,
|
||||
CORNER_RADIUS_PX: LINEAGE_CORNER_RADIUS_PX,
|
||||
LANE_STEP_PX: LINEAGE_LANE_STEP_PX,
|
||||
MAX_LANES: LINEAGE_MAX_LANES,
|
||||
SPINE_INSET_PX: LINEAGE_SPINE_INSET_PX,
|
||||
VERTICAL_TRACK_INSET_PX: LINEAGE_VERTICAL_TRACK_INSET_PX,
|
||||
COLORS: LINEAGE_COLORS,
|
||||
|
||||
@@ -2153,7 +2153,7 @@
|
||||
<div class="set-row" id="appSettingsLineageLinesItem" data-search="lineage lines spawned worker parent connection">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Spawn Lineage Lines <span class="set-tag">desktop</span></span>
|
||||
<span class="set-row-desc">For the selected tab, draw lines to the sessions it spawned and to the session that spawned it.</span>
|
||||
<span class="set-row-desc">Draw lines from each session to the sessions it spawned. The selected tab's family is drawn thicker.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsLineageLines" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
* spawning tab, routed through the gaps between tab rows so it never crosses a label or
|
||||
* the terminal (geometry: `CodemanLineage.computeTree` in constants.js).
|
||||
*
|
||||
* ONLY THE SELECTED TAB'S FAMILY IS DRAWN: the family the active tab spawned, and the
|
||||
* family it belongs to as a child. Drawing every family at once is what the owner
|
||||
* called "too confusing" (2026-10-06: one parent with ten children across five wrapped
|
||||
* rows). Selection therefore has to redraw, which `_updateActiveTabImmediate()` does
|
||||
* whenever any lineage exists (`_lineageTotalEdges`).
|
||||
* EVERY FAMILY IS ALWAYS DRAWN, AND THE SELECTED TAB'S IS EMPHASIZED: the family the
|
||||
* active tab spawned, and the family it belongs to as a child, get the
|
||||
* `lineage-family--focus` group (thicker, full opacity, drawn last so nothing covers
|
||||
* it). Drawing ONLY the selected family was tried first and rejected by the owner
|
||||
* (2026-10-07: "I wanna see all the connections always"). Selection still has to
|
||||
* redraw to move the emphasis, which `_updateActiveTabImmediate()` does whenever any
|
||||
* lineage exists (`_lineageTotalEdges`).
|
||||
*
|
||||
* It is an ADDITIONAL LAYER on the existing SVG pass, not a second pass: the core
|
||||
* `_updateConnectionLinesImmediate()` (subagent-windows.js) calls
|
||||
@@ -123,28 +125,31 @@ Object.assign(CodemanApp.prototype, {
|
||||
return edges;
|
||||
},
|
||||
|
||||
/**
|
||||
* The families worth drawing for the current selection, each as
|
||||
* `{ parentId, edges }` in strip order: the family the selected tab spawned, and
|
||||
* the family it was spawned into (its parent plus its siblings). Empty when a web
|
||||
* tab holds the stage, or the selected tab has no lineage at all.
|
||||
*/
|
||||
_focusedLineageFamilies(edges) {
|
||||
const focus = this.activeWebviewId ? null : this.activeSessionId;
|
||||
if (!focus) return [];
|
||||
const parents = new Set();
|
||||
for (const edge of edges) {
|
||||
if (edge.parentId === focus || edge.childId === focus) parents.add(edge.parentId);
|
||||
}
|
||||
/** Every family as `{ parentId, edges }`, in strip order of first appearance. */
|
||||
_lineageFamilies(edges) {
|
||||
const families = new Map();
|
||||
for (const edge of edges) {
|
||||
if (!parents.has(edge.parentId)) continue;
|
||||
if (!families.has(edge.parentId)) families.set(edge.parentId, []);
|
||||
families.get(edge.parentId).push(edge);
|
||||
}
|
||||
return [...families].map(([parentId, familyEdges]) => ({ parentId, edges: familyEdges }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Parent ids of the families the selection emphasizes: the family the selected tab
|
||||
* spawned, and the family it was spawned into (its parent plus its siblings). Empty
|
||||
* when a web tab holds the stage, or the selected tab has no lineage at all.
|
||||
*/
|
||||
_lineageFocusParents(edges) {
|
||||
const parents = new Set();
|
||||
const focus = this.activeWebviewId ? null : this.activeSessionId;
|
||||
if (!focus) return parents;
|
||||
for (const edge of edges) {
|
||||
if (edge.parentId === focus || edge.childId === focus) parents.add(edge.parentId);
|
||||
}
|
||||
return parents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Colour for one family, from CodemanLineage.COLORS, keyed on the SPAWNING tab.
|
||||
*
|
||||
@@ -159,9 +164,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
* Assigned in FIRST-SEEN order and remembered per parent id. First-seen rather than
|
||||
* draw-index keeps a colour stable across re-renders, tab reorders and sibling
|
||||
* closes (the SVG is wiped and rebuilt constantly, so an index-based colour would
|
||||
* flicker). The draw pass claims a colour for EVERY family in strip order, drawn or
|
||||
* not, so which family happens to be selected first never decides who gets which
|
||||
* colour. An empty string means "no override": the CSS falls back to
|
||||
* flicker). The draw pass claims a colour for EVERY family in strip order before it
|
||||
* draws anything, so neither the draw order (the selected family goes last) nor
|
||||
* which family was selected first ever decides who gets which colour. An empty string means "no override": the CSS falls back to
|
||||
* --session-blue, so the first spawning tab keeps the skin-aware blue.
|
||||
*/
|
||||
_lineageColorFor(parentId) {
|
||||
@@ -207,10 +212,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
const edges = this._collectLineageEdges();
|
||||
this._lineageTotalEdges = edges.length;
|
||||
if (edges.length === 0) return;
|
||||
// Claim colours in strip order for every family, drawn or not (_lineageColorFor).
|
||||
// Claim colours in strip order for every family before drawing (_lineageColorFor).
|
||||
for (const edge of edges) this._lineageColorFor(edge.parentId);
|
||||
const families = this._focusedLineageFamilies(edges);
|
||||
if (families.length === 0) return;
|
||||
const families = this._lineageFamilies(edges);
|
||||
const focusParents = this._lineageFocusParents(edges);
|
||||
if (!rects) rects = new Map();
|
||||
|
||||
// PHASE 1 — reads.
|
||||
@@ -269,7 +274,19 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
// PHASE 2 — writes, from the cache only.
|
||||
resolvedFamilies.forEach(({ family, parentEndpoint, parentRect, children }, lane) => {
|
||||
// Lanes follow STRIP order, so a family keeps its lane when the selection moves;
|
||||
// only the DRAW order changes (the emphasized families last, on top). A row gap
|
||||
// fits a few lanes, so they cycle: families that share one are told apart by colour.
|
||||
const laneLimit = Math.max(1, (window.CodemanLineage && window.CodemanLineage.MAX_LANES) || 3);
|
||||
const laneCount = Math.min(laneLimit, resolvedFamilies.length);
|
||||
const drawOrder = resolvedFamilies
|
||||
.map((resolved, index) => ({
|
||||
...resolved,
|
||||
lane: index % laneCount,
|
||||
focus: focusParents.has(resolved.family.parentId),
|
||||
}))
|
||||
.sort((a, b) => a.focus - b.focus);
|
||||
for (const { family, parentEndpoint, parentRect, children, lane, focus } of drawOrder) {
|
||||
const geom = computeTree({
|
||||
parent: parentRect,
|
||||
children: children.map((c) => ({ id: c.edge.childId, rect: c.rect })),
|
||||
@@ -277,15 +294,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
tabs: tabRects,
|
||||
orientation,
|
||||
lane,
|
||||
laneCount: resolvedFamilies.length,
|
||||
laneCount,
|
||||
});
|
||||
if (!geom || geom.routes.length === 0) return;
|
||||
if (!geom || geom.routes.length === 0) continue;
|
||||
const byId = new Map(children.map((c) => [c.edge.childId, c]));
|
||||
const color = this._lineageColorFor(family.parentId);
|
||||
// One group per family: it carries the translucency, so the stretches its
|
||||
// routes share (the trunk) do not stack into a brighter line than the branches.
|
||||
// routes share (the trunk) do not stack into a brighter line than the branches,
|
||||
// and the emphasis for the selected tab's families (styles.css).
|
||||
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
group.setAttribute('class', 'lineage-family');
|
||||
group.setAttribute('class', 'lineage-family' + (focus ? ' lineage-family--focus' : ''));
|
||||
group.setAttribute('data-parent-tab', family.parentId);
|
||||
// Working routes go in FIRST, so an idle sibling's solid stroke covers the
|
||||
// shared trunk and only the working child's own branch shows its dashes.
|
||||
@@ -320,8 +338,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
dot.setAttribute('cx', String(route.endX));
|
||||
dot.setAttribute('cy', String(route.endY));
|
||||
// Resting radius; `lineage-dot-pulse` breathes it 2.5 → 3.3 while the child
|
||||
// works, so the two have to be changed together.
|
||||
// Fallback radius only: styles.css sizes the dot through `--lineage-dot-r`
|
||||
// (larger in an emphasized family), and `lineage-dot-pulse` breathes from it.
|
||||
dot.setAttribute('r', '2.5');
|
||||
dot.setAttribute('class', 'lineage-line-dot' + working + (proxied ? ' lineage-line-dot--proxied' : ''));
|
||||
dot.setAttribute('data-child-tab', child.edge.childId);
|
||||
@@ -330,7 +348,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
svg.appendChild(group);
|
||||
this._lineageEdgeCount += routes.length;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -10682,9 +10682,12 @@ kbd {
|
||||
}
|
||||
|
||||
/* ===== Session lineage lines (tab → tab it spawned, session-lineage.js) =====
|
||||
One quiet orthogonal tree per spawning tab, drawn only for the SELECTED tab's
|
||||
family and routed through the gaps between tab rows (computeLineageTree in
|
||||
constants.js). The routing room itself is reserved on the strip by
|
||||
One quiet orthogonal tree per spawning tab, every family always drawn and
|
||||
routed through the gaps between tab rows (computeLineageTree in constants.js).
|
||||
The SELECTED tab's families (what it spawned, and the family it was spawned
|
||||
into) are emphasized by `.lineage-family--focus`: thicker, full opacity, a
|
||||
larger end dot, drawn last (owner call 2026-10-07: see every connection always,
|
||||
thicker when selected). The routing room itself is reserved on the strip by
|
||||
`.session-tabs.lineage-tree` near the top of this file.
|
||||
|
||||
⚠ QUIET ON PURPOSE (owner call 2026-10-06, "B2"): 1.5px, solid, rounded
|
||||
@@ -10706,7 +10709,11 @@ kbd {
|
||||
parent's routes overlap exactly along the trunk, and translucent paths would
|
||||
stack there into a brighter line than the branches. */
|
||||
.lineage-family {
|
||||
opacity: 0.85;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.lineage-family--focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.connection-line.lineage-line {
|
||||
@@ -10720,6 +10727,11 @@ kbd {
|
||||
filter: var(--line-glow);
|
||||
}
|
||||
|
||||
/* The selected tab's families: thicker (the group above makes them opaque). */
|
||||
.lineage-family--focus .connection-line.lineage-line {
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
/* A working child's route is dashed even with motion turned off; the march
|
||||
below is the extra. Routes start at the PARENT, so dashes on the shared trunk
|
||||
stay in phase between working siblings. */
|
||||
@@ -10727,11 +10739,19 @@ kbd {
|
||||
stroke-dasharray: 4 3;
|
||||
}
|
||||
|
||||
/* The radius rides a variable so the pulse below can breathe from whichever size
|
||||
the dot rests at; the `r` attribute session-lineage.js sets is only a fallback. */
|
||||
.lineage-line-dot {
|
||||
--lineage-dot-r: 2.5px;
|
||||
r: var(--lineage-dot-r);
|
||||
fill: var(--lineage-color, var(--session-blue, #2b8fd9));
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.9));
|
||||
}
|
||||
|
||||
.lineage-family--focus .lineage-line-dot {
|
||||
--lineage-dot-r: 3.5px;
|
||||
}
|
||||
|
||||
/* A line re-anchored to a collapsed group's header (its row is hidden): quieter
|
||||
and dotted, so it reads as "somewhere in there" rather than a real row. */
|
||||
.connection-line.lineage-line--proxied {
|
||||
@@ -10768,15 +10788,15 @@ kbd {
|
||||
}
|
||||
}
|
||||
|
||||
/* Tied to the resting r="2.5" session-lineage.js sets on the dot. */
|
||||
/* Breathes from the dot's own resting radius (`--lineage-dot-r`). */
|
||||
@keyframes lineage-dot-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.75;
|
||||
r: 2.5;
|
||||
r: var(--lineage-dot-r);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
r: 3.3;
|
||||
r: calc(var(--lineage-dot-r) + 0.8px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user