mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
feat(web): optional collapsible left session sidebar
The header tab strip stops working past roughly a dozen sessions: it wraps into two or three rows, eats vertical space and still cannot be scanned. This adds a vertical session list in a left <aside> as an ALTERNATIVE layout — a filter box, a live count, and a 44px collapsed rail that keeps the ambient signal (status dot, task badge) visible. The strip is not removed. Settings -> Display -> Tab Bar -> Session List Layout switches between them and the default stays 'header', so existing users see no change until they opt in. Structure: one #sessionTabs element, two mount points. applySessionListLayout() re-parents the SAME node between #sessionTabsHost and #sessionSidebarList, which is why there is no second renderer and no duplicated wiring — app.$() caches getElementById results and never invalidates them, so a moved node keeps every existing consumer (settings-ui, webview-tabs, the generated gesture bundle, the mobile tests) working untouched. Notable integration points: - Below 1024px the sidebar is an off-canvas drawer overlaying the terminal; closed it gets inert + aria-hidden so it cannot be tabbed into, and touch swipes over it no longer switch sessions. - Subagent and ultracode windows anchor to the right edge of a sidebar row instead of its bottom, connector curves follow. - Alt+B toggles; the chord is gated out of the PTY so xterm cannot also write ESC b into a live session. - Collapse state lives in its own localStorage key (the settings blob is rebuilt from DOM controls on every save) and falls back to in-memory intent where storage throws. Verified: frontend syntax + public asset checks, tsc, eslint, 26 new jsdom tests, and a headless-Chromium harness (scripts/verify-session-sidebar.mts) that renders a synthetic 25-session fleet in both layouts at 1600/1000/393px and asserts mount point, widths, inert/aria state and row count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
@@ -744,9 +741,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');
|
||||
@@ -1221,6 +1220,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)
|
||||
|
||||
Reference in New Issue
Block a user