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
+247
View File
@@ -49,6 +49,9 @@
--ring-glow: 0 0 12px -2px rgba(56, 182, 240, 0.55);
--header-height: 36px;
--toolbar-height: 42px;
--sidebar-width: 260px;
--sidebar-width-collapsed: 44px; /* == --touch-target-min */
--sidebar-transition: 0.18s ease;
--glass-bg: rgba(31, 38, 48, 0.85);
--glass-border: rgba(255, 255, 255, 0.08);
--control-bg: rgba(255, 255, 255, 0.045);
@@ -2149,6 +2152,9 @@ html:not(.tabs-show-detach) .session-tab:not(.detached):not(.tab-show-detach) .t
/* ===== Solo (detached single-session) window chrome ===================== */
body.solo-mode .session-tabs,
body.solo-mode .session-tabs-host,
body.solo-mode .session-sidebar,
body.solo-mode .btn-sidebar-toggle,
body.solo-mode .header-system-stats,
body.solo-mode .header-tokens,
body.solo-mode .btn-notifications,
@@ -16597,3 +16603,244 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
font-size: 0.8rem;
padding: 4px 9px;
}
/* ============================================================
=== Collapsible session sidebar (opt-in layout) ===
Appended at top level ON PURPOSE: styles.css:12171-12390 is one
html:not([data-skin="og"]) { … } native-nesting block whose bare
selectors resolve at (0,2,x) and re-tone .session-tab with
!important. Everything below is LAYOUT ONLY (flex/size/overflow/
display) and sets no colour on .session-tab, so it composes with
every skin instead of fighting it. Keep it that way.
The list itself is not a second DOM tree: applySessionListLayout()
moves the one #sessionTabs element between #sessionTabsHost (header)
and #sessionSidebarList (this aside).
============================================================ */
/* Header host — wraps #sessionTabs so the strip can be hidden without
touching the element that gets re-parented. */
.session-tabs-host {
display: flex;
flex: 1;
min-width: 0;
}
html[data-session-list="sidebar"] .session-tabs-host {
display: none;
}
/* The header only needs flex-start to support the two-row strip; with the
strip gone the remaining header chrome should sit centered. */
html[data-session-list="sidebar"] .header {
align-items: center;
}
.session-sidebar {
display: none;
}
html[data-session-list="sidebar"] .session-sidebar {
display: flex;
flex-direction: column;
flex: 0 0 var(--sidebar-width);
width: var(--sidebar-width);
min-width: 0;
background: var(--bg-card);
border-right: 1px solid var(--border);
/* Own stacking context ABOVE .welcome-overlay (z-index 10, which is what a
user with no open session sees) but BELOW .toolbar (20) — raising it to or
past 20 makes the Run menu unclickable again. */
position: relative;
z-index: 11;
transition: flex-basis var(--sidebar-transition), width var(--sidebar-transition);
/* Deliberately NO contain:paint — .header has it, which is exactly why app.js
re-parents .subagent-dropdown to <body>. Leaving it off keeps per-row
dropdowns and the inline rename input paintable in place. */
}
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar {
flex-basis: var(--sidebar-width-collapsed);
width: var(--sidebar-width-collapsed);
}
.session-sidebar-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-shrink: 0;
padding: 0.4rem 0.6rem;
border-bottom: 1px solid var(--glass-border);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.session-sidebar-count {
font-variant-numeric: tabular-nums;
color: var(--text-dim);
}
.session-sidebar-filter {
display: flex;
flex-shrink: 0;
padding: 0.35rem 0.5rem;
}
.session-sidebar-filter-input {
width: 100%;
box-sizing: border-box;
padding: 0.3rem 0.45rem;
background: var(--bg-input);
border: 1px solid var(--control-border);
border-radius: var(--btn-radius);
color: var(--text);
font-family: inherit;
font-size: 0.75rem;
outline: none;
}
.session-sidebar-filter-input::placeholder {
color: var(--text-muted);
}
.session-sidebar-filter-input:focus-visible {
border-color: var(--accent);
}
/* Host for the relocated #sessionTabs. */
.session-sidebar-list {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* --- The relocated strip, now vertical --------------------------------- */
html[data-session-list="sidebar"] .session-sidebar .session-tabs {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 2px;
flex: 1;
min-height: 0;
max-height: none;
overflow-x: hidden;
overflow-y: auto;
padding: 0.25rem;
}
html[data-session-list="sidebar"] .session-sidebar .session-tab {
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 0.4rem 0.5rem;
border-radius: var(--btn-radius);
}
/* .tab-info is already column/overflow-hidden/min-width:0 — it only has to
claim the free width now that rows are full-width. */
html[data-session-list="sidebar"] .session-sidebar .tab-info {
flex: 1;
min-width: 0;
}
html[data-session-list="sidebar"] .session-sidebar .tab-name {
max-width: none;
}
/* Reveal-on-hover reads badly on a 40px-tall full-width row, so keep the row
actions permanently visible on the active session — no layout jitter when
the pointer crosses the list. */
html[data-session-list="sidebar"] .session-sidebar .session-tab.active .tab-gear,
html[data-session-list="sidebar"] .session-sidebar .session-tab.active .tab-detach,
html[data-session-list="sidebar"] .session-sidebar .session-tab.active .tab-close {
opacity: 1;
width: auto;
}
/* Drag-reorder indicators become horizontal edges. The class names stay
drag-over-left / drag-over-right (they read as before/after now) so app.js,
the base rules above and the generated gesture bundle need no renaming. */
html[data-session-list="sidebar"] .session-sidebar .session-tab.drag-over-left {
box-shadow: 0 -2px 0 0 var(--accent);
}
html[data-session-list="sidebar"] .session-sidebar .session-tab.drag-over-right {
box-shadow: 0 2px 0 0 var(--accent);
}
/* Sidebar filter box (applySidebarFilter toggles this class post-render).
Scoped to the sidebar layout on purpose: applySidebarFilter() already strips
the class whenever the filter box is off screen, and this prefix is the
second lock — a leaked class must never be able to hide tabs from the header
strip, which has no filter control to clear it with. */
html[data-session-list="sidebar"] .session-tab.tab-filtered-out {
display: none !important;
}
/* --- Collapsed rail ---------------------------------------------------- */
/* Collapsed is a 44px icon rail, not "hidden": the ambient signal (status dot,
task/subagent/ultracode badges) is the whole point of mission control and
must survive collapse. The rail is also its own reopen affordance — clicking
a row still switches session.
NOT surviving: the name, the folder and the `sh`/`oc`/`cx`/`gm` mode chip —
the chip is rendered inside .tab-info (app.js row template), which the rail
hides. Moving it out of .tab-info just to keep it would change the shared row
markup for both layouts; agent type stays a hover/expand affordance. */
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar-head,
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar-filter {
display: none;
}
/* 44px rail minus the list's 0.25rem padding either side minus the row's 1px
borders leaves ~34px of content box. Number (16) + gap (5.6) + dot (6) + gap
(5.6) + one badge (16) already overflows that, and .tab-number / .tab-status
are flex-shrink: 0 — with justify-content: center the excess gets clipped at
BOTH ends, so the digit and the badge are cut in half. Two fixes, both
needed: drop the Alt+N hint (it is a keyboard affordance that only reads in
the expanded list; Alt+N itself keeps working), and let whatever is left wrap
instead of clipping, so a row carrying several badges just gets taller. */
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .session-tab {
justify-content: center;
align-content: center;
flex-wrap: wrap;
row-gap: 2px;
padding: 0.4rem 0;
}
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .tab-info,
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .tab-number,
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .tab-gear,
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .tab-detach,
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .tab-close {
display: none;
}
/* The subagent badge carries a 4px left margin tuned for the horizontal strip;
in a centered 34px rail it pushes the row off-centre. */
html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .tab-subagent-badge {
margin-left: 0;
}
/* --- Toggle button ----------------------------------------------------- */
.btn-sidebar-toggle--hidden {
display: none !important;
}
/* .btn-icon-header:hover rotates 45deg globally — a panel glyph must not spin. */
.btn-sidebar-toggle:hover {
transform: none;
}
html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle svg {
transform: scaleX(-1);
}
@media (prefers-reduced-motion: reduce) {
.session-sidebar {
transition: none;
}
}