From 6844cccd18d397a685686b598393665f1c8f96e2 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sat, 10 Oct 2026 04:37:49 +0200 Subject: [PATCH] fix(tabs): no connector from a parent row the search hid (#580) A floating subagent or ultracode window whose parent session's row the rail search hid drew its connector from the viewport's top-left corner. The hidden row is display:none, and getBoundingClientRect() still answers it with an all-zero DOMRect, which is truthy, so the `if (!tabRect) continue` paths never skipped it and _tabAnchor() put the line at (0, 0) ("M 0 0 C 350 0, 350 400, 700 400" in Chromium). The sidebar filter already had the same defect; #580 brought it to the rail, and the keystroke redraw from 5f373a93 now ran it on every keystroke that hides the parent. A new _paintedSessionTab() in app.js returns the parent's row only while getClientRects() is non-empty, and every place a floating window measures its parent tab goes through it: - the shared `tab:` rect cache, filled by the subagent connectors and by both ultracode connector layers (all three fill it, so guarding only the first would let the next one cache the zero rect itself); - the ultracode window's spawn position, which now cascades; - the subagent window's fly-from-tab spawn, the same defect, not in the report, now positioned as a window without a tab; - the ultracode genie on minimize, which now tears down at once. Lineage lines need nothing: they measure on a cache miss and computeTree already drops zero-size rects. Tests: in the gate, with the page laid out by hand (jsdom has no layout), a subagent window, an ultracode run window and an ultracode agent window from a hidden row draw no line and draw it again from the row once the search is cleared, an ultracode window spawns from a painted row and cascades from a hidden one, and the genie is skipped for a hidden row. In the browser suite, real Chromium shows the hidden row's rect is all zero and the connector is gone, then back after Clear. All four fail on the previous commit. CLAUDE.md and the invariants' Connectors bullet record the rule. Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 2 +- docs/architecture-invariants.md | 2 +- src/web/public/app.js | 14 +++ src/web/public/subagent-windows.js | 12 ++- src/web/public/ultracode-windows.js | 15 +-- test/tab-rail-search.browser.test.ts | 54 +++++++++- test/tab-rail-search.test.ts | 153 +++++++++++++++++++++++++++ 7 files changed, 238 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 07a5b6f4..be353a7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -337,7 +337,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L **Mobile tab strip scrolling** (issue #257): under 768px the tab strip scrolls horizontally, so the active tab must be kept reachable. `_updateActiveTabImmediate()` reveals it via `computeTabScrollLeft()` (constants.js, rect math on the strip's own `scrollLeft`, never `scrollIntoView()`, which scrolls the document under the fixed header); `_fullRenderSessionTabs()` must restore `scrollLeft` across rebuilds and re-reveal only when the active tab changed (`_lastRenderedActiveTabId`) or, grouped by state, the ACTIVE tab moved to another state band (`_noteActiveTabBand()`, both render paths, scrolling row only; another tab's move never scrolls). ⚠️ The phone-block `min-width` on `.session-tab.active .tab-name` keeps the tab's centre off the gear/close icons, sized for numberless tabs 10+ (floor 40px); do not shrink it. ⚠️ Never reintroduce hoisting the active session to the front of the strip. Tests: `test/mobile-tab-tap-zones.test.ts`, `test/tab-triage.test.ts`. → [architecture-invariants#mobile-tab-strip-scrolling](docs/architecture-invariants.md#mobile-tab-strip-scrolling) -**Session list layout: header strip or left sidebar** (`sessionListLayout`, default `header`; per-device via `displayKeys`, also in `SettingsUpdateSchema`): the list can move into a collapsible `