Merge christianhaberl's session-sidebar branch, ported to current master

Brings in https://github.com/christianhaberl/Codeman/pull/4 (three commits,
authorship preserved) and adapts it across the 211 commits master gained
since the branch was cut:

- App Settings control re-authored for the set-* surface (PR #278): a
  set-row in Layout -> Tabs, replacing the old settings-item markup the
  branch targeted. i18n description synced.
- Lineage arcs (PR #291, post-branch) are SKIPPED in sidebar layout:
  computeLineagePath()'s U-bridge geometry hangs from the horizontal
  strip's bottom edge and has no meaning against a vertical list. The
  lineage strip-scroll listener now also redraws subagent/ultracode
  connectors while the sidebar scrolls vertically.
- The desktop home tab rail (post-branch) defers to the sidebar: both dock
  the session list flush left, and the rail would render z-ordered under it.
- Active-row reveal unified into _scrollActiveTabIntoView() (#257 landed on
  master after the branch): sidebar mode branches to scrollIntoView
  block:'nearest', and _fullRenderSessionTabs() restores scrollTop alongside
  the #257 scrollLeft restore so ambient rebuilds cannot yank a mid-scroll
  sidebar back to the top.
- Mobile active-tab hoisting the branch guarded against no longer exists on
  master (removed by #257); kept master's order-stable render.

Verified: typecheck, lint, format:check, check:frontend-syntax,
check:public-assets, PostCSS parse of both merged stylesheets, the 26 new
jsdom tests, the structural guard suites, and the headless-Chromium harness
(scripts/verify-session-sidebar.mts) green across all seven layout states
at 1600/1000/393px against current master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-16 18:28:15 +02:00
20 changed files with 1656 additions and 56 deletions
+24 -12
View File
@@ -401,15 +401,12 @@ Object.assign(CodemanApp.prototype, {
continue;
}
// Draw curved line from TAB bottom-center to window top-center
const x1 = tabRect.left + tabRect.width / 2;
const y1 = tabRect.bottom;
const x2 = winRect.left + winRect.width / 2;
const y2 = winRect.top;
// Bezier curve control points for smooth curve
const midY = (y1 + y2) / 2;
const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
// Draw a curved line from the tab to the window. Header strip: tab
// bottom-center → window top-center (vertical). Sidebar: tab right-edge →
// window left-edge (horizontal), otherwise the curve loops backwards
// underneath the sidebar. _tabAnchor/_tabConnectorPath live in app.js.
const anchor = this._tabAnchor(tabRect);
const path = this._tabConnectorPath(anchor, winRect);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', path);
@@ -749,9 +746,11 @@ Object.assign(CodemanApp.prototype, {
win.style.top = `${finalY}px`;
win.style.bottom = 'auto';
} else if (flyFromTab) {
const tabRect = parentTab.getBoundingClientRect();
win.style.left = `${tabRect.left}px`;
win.style.top = `${tabRect.bottom}px`;
// Spawn at the tab: below it in header layout, to its RIGHT in sidebar
// layout — spawning at tabRect.left there would land on top of the sidebar.
const anchor = this._tabAnchor(parentTab.getBoundingClientRect());
win.style.left = `${anchor.spawnLeft}px`;
win.style.top = `${anchor.spawnTop}px`;
win.style.transform = 'scale(0.3)';
win.style.opacity = '0';
win.classList.add('spawning');
@@ -1226,6 +1225,19 @@ Object.assign(CodemanApp.prototype, {
dropdown.style.left = `${rect.left + rect.width / 2}px`;
dropdown.style.transform = 'translateX(-50%)';
dropdown.classList.add('open');
// Keep it on screen. A badge in the left sidebar — and above all one in the
// 44px collapsed rail — sits so far left that a centre-anchored dropdown
// hangs off the viewport. Measured after .open so it has a box; a no-op
// whenever the centred position already fits, so header layout is unchanged.
const dropRect = dropdown.getBoundingClientRect();
const overflowLeft = 8 - dropRect.left;
const overflowRight = dropRect.right - (window.innerWidth - 8);
if (overflowLeft > 0) {
dropdown.style.transform = `translateX(calc(-50% + ${Math.round(overflowLeft)}px))`;
} else if (overflowRight > 0) {
dropdown.style.transform = `translateX(calc(-50% - ${Math.round(overflowRight)}px))`;
}
},
// Schedule hide after delay (allows moving mouse to dropdown)