mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
fix(ui): make session lineage lines read as arcs, not straight threads
The lines that join a tab to the workers its codeman skill spawned were drawn with numbers tuned against two tabs sitting side by side, and they degraded in exactly the two situations the feature is actually used in. 1. A spawned worker is appended to the END of the strip, so the real span between a lead and its worker is 800-1500px. With the dip clamped at 44px that is a 33px sag: the arc reads as a straight line drawn across the terminal instead of a bracket hanging under the strip. The dip now grows at 0.085/px and clamps at 104. 2. When the desktop strip wraps (tabs-two-rows / tabs-auto-wrap), a parent on row 1 and its child on row 2 are ~14px apart, and the cross-row branch drew parent-bottom to child-TOP: a flat line hidden inside the row gap, with siblings overprinting each other. Both ends now anchor on the tab BOTTOM with the control points below the LOWER row, so a wrapped pair gets the same bracket a flat strip gets. That deletes the branch: one shape covers both. Visibility, at 1:1 rather than in a zoomed mockup: 2 -> 2.5px stroke, 4 4 -> 5 5 dashes (lineage-flow moves with them, -16 -> -20), opacity .55 -> .72, and a second wider glow so the contrast comes from the halo rather than from more weight, keeping the line under the subagent lines' 3px. A working child is bright (.95) outside the reduced-motion block, so turning motion off no longer also dims every worker's arc. Sibling nesting 6 -> 8px and the direction dot 3 -> 3.5px to match the heavier stroke. Verified at 1:1 in a harness driving the real styles.css and the real computeLineagePath over three layouts (adjacent workers, workers at the far end of a full strip, wrapped two-row strip) on a dark and a light skin. test/session-lineage-lines.test.ts pins both regressions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+45
-35
@@ -201,24 +201,43 @@ 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.
|
||||
//
|
||||
// Two shapes, because both endpoints live in ONE horizontal strip and the subagent
|
||||
// shape (tab-bottom → window-top) has nothing to aim at:
|
||||
// - same row: a shallow U-bridge HANGING BELOW the strip, so it reads as a
|
||||
// bracket joining two tabs rather than as a line crossing them. The dip grows
|
||||
// with horizontal distance and with `depth` (the child's index among its
|
||||
// siblings), so several children of one parent nest instead of overprinting.
|
||||
// - different rows (desktop `tabs-two-rows` / `tabs-auto-wrap`): the vertical
|
||||
// bezier the subagent lines already use, parent edge → child edge.
|
||||
// ONE shape, because both endpoints live in the same horizontal strip and the subagent
|
||||
// shape (tab-bottom → window-top) has nothing to aim at: a U-bridge HANGING BELOW the
|
||||
// strip, from the parent's bottom edge to the child's bottom edge, so it reads as a
|
||||
// bracket joining two tabs rather than as a line crossing them. The dip grows with
|
||||
// horizontal distance and with `depth` (the child's index among its siblings), so
|
||||
// several children of one parent nest instead of overprinting.
|
||||
//
|
||||
// ⚠ A WRAPPED STRIP USED TO GET ITS OWN SHAPE, AND THAT SHAPE WAS THE BUG. When the
|
||||
// desktop strip wraps (`tabs-two-rows` / `tabs-auto-wrap`) a parent on row 1 and its
|
||||
// child on row 2 are ~4px apart vertically, so the old parent-bottom → child-TOP bezier
|
||||
// had a 4px span to work with and drew a flat horizontal line inside the row gap
|
||||
// (reported as "they connect already, but the lines are straight and not easy visible"),
|
||||
// and three siblings drew three of them on top of each other. Aiming BOTH ends at the
|
||||
// tab BOTTOMS and putting the control points below the LOWER row gives the wrapped case
|
||||
// the same bracket as the flat case: it leaves the parent downward, crosses the lower
|
||||
// row once, and comes back up under the child. Same formula, no branch.
|
||||
//
|
||||
// Returns null when the edge must not be drawn: a missing/degenerate rect, or an
|
||||
// endpoint scrolled outside the strip. `.session-tabs` is `overflow-x: auto`, so a
|
||||
// scrolled-out tab still HAS a rect — one lying over the logo or the header
|
||||
// buttons. Skipping is honest; clamping would point at a tab that isn't there.
|
||||
// ⚠ THE DIP IS WHAT MAKES THE ARC AN ARC, and the first shipped numbers were tuned
|
||||
// against two tabs sitting side by side. A worker the agent skill starts is appended
|
||||
// to the END of the strip, so the real span between a lead and its worker is 800-1500px,
|
||||
// not 200, and a 44px cap over 1300px of span is a 33px sag, i.e. a line that reads as
|
||||
// STRAIGHT and crosses the terminal instead of bracketing under the strip. The dip now
|
||||
// keeps growing with the span (0.085/px, ~3x steeper against the old cap) so the bracket
|
||||
// survives the distance the feature is actually used at. The ceiling is what keeps a
|
||||
// full-width pair out of the terminal's fourth line: 104 + the sibling step lands the
|
||||
// deepest sag around y=140 on a 1080 screen, the same proportion two adjacent tabs get.
|
||||
const LINEAGE_DIP_BASE_PX = 14;
|
||||
const LINEAGE_DIP_PER_PX = 0.06;
|
||||
const LINEAGE_DIP_MIN_PX = 16;
|
||||
const LINEAGE_DIP_MAX_PX = 44;
|
||||
const LINEAGE_SIBLING_STEP_PX = 6;
|
||||
const LINEAGE_DIP_PER_PX = 0.085;
|
||||
const LINEAGE_DIP_MIN_PX = 22;
|
||||
const LINEAGE_DIP_MAX_PX = 104;
|
||||
// Siblings nest by this much. Widened with the stroke: at 2.5px plus its glow, arcs 6px
|
||||
// apart bled into one thick band instead of reading as three separate lines.
|
||||
const LINEAGE_SIBLING_STEP_PX = 8;
|
||||
const LINEAGE_STRIP_TOLERANCE_PX = 4;
|
||||
|
||||
function computeLineagePath(input) {
|
||||
@@ -250,29 +269,20 @@ function computeLineagePath(input) {
|
||||
const cBottom = cTop + ch;
|
||||
const sameRow = Math.abs(pTop + ph / 2 - (cTop + ch / 2)) <= Math.min(ph, ch) / 2;
|
||||
|
||||
let d;
|
||||
let endX;
|
||||
let endY;
|
||||
if (sameRow) {
|
||||
const y0 = Math.max(pBottom, cBottom);
|
||||
const span = Math.abs(cx - px);
|
||||
const dip =
|
||||
Math.min(LINEAGE_DIP_MAX_PX, Math.max(LINEAGE_DIP_MIN_PX, LINEAGE_DIP_BASE_PX + span * LINEAGE_DIP_PER_PX)) +
|
||||
depth * LINEAGE_SIBLING_STEP_PX;
|
||||
const yc = y0 + dip;
|
||||
d = `M ${r1(px)} ${r1(y0)} C ${r1(px)} ${r1(yc)}, ${r1(cx)} ${r1(yc)}, ${r1(cx)} ${r1(y0)}`;
|
||||
endX = cx;
|
||||
endY = y0;
|
||||
} else {
|
||||
const childBelow = cTop + ch / 2 > pTop + ph / 2;
|
||||
const y1 = childBelow ? pBottom : pTop;
|
||||
const y2 = childBelow ? cTop : cBottom;
|
||||
const mid = (y1 + y2) / 2;
|
||||
d = `M ${r1(px)} ${r1(y1)} C ${r1(px)} ${r1(mid)}, ${r1(cx)} ${r1(mid)}, ${r1(cx)} ${r1(y2)}`;
|
||||
endX = cx;
|
||||
endY = y2;
|
||||
}
|
||||
return { d, endX, endY, sameRow };
|
||||
// Both ends anchor on the tab BOTTOM, and the control points hang below whichever
|
||||
// row is lower, so one formula covers a flat strip and a wrapped one.
|
||||
const span = Math.abs(cx - px);
|
||||
const rowDrop = Math.abs(cBottom - pBottom);
|
||||
// ⚠ A wrapped pair needs the dip measured from the LOWER row, or the bracket would
|
||||
// only reach the row gap again. Adding the row offset also keeps the curve clear of
|
||||
// the row it crosses instead of grazing its bottom edge.
|
||||
const dip =
|
||||
Math.min(LINEAGE_DIP_MAX_PX, Math.max(LINEAGE_DIP_MIN_PX, LINEAGE_DIP_BASE_PX + span * LINEAGE_DIP_PER_PX)) +
|
||||
depth * LINEAGE_SIBLING_STEP_PX +
|
||||
rowDrop;
|
||||
const yc = Math.max(pBottom, cBottom) + dip;
|
||||
const d = `M ${r1(px)} ${r1(pBottom)} C ${r1(px)} ${r1(yc)}, ${r1(cx)} ${r1(yc)}, ${r1(cx)} ${r1(cBottom)}`;
|
||||
return { d, endX: cx, endY: cBottom, sameRow };
|
||||
}
|
||||
|
||||
// One decimal is plenty for a screen-space path and keeps the `d` string short.
|
||||
|
||||
Reference in New Issue
Block a user