feat(web): optional collapsible left session sidebar - #307
Merged
Conversation
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>
The script carried two absolute paths from the machine it was written on: a full scratchpad path including a session UUID, and /home/chaberl/projects as the synthetic sessions' working directory. This branch is pushed to a public fork, so they were visible to anyone. Screenshot output now defaults to tmpdir() and is overridable via SIDEBAR_SHOTS_DIR; the synthetic working directories are tmpdir()-based too, which also makes the harness run for anyone who checks the branch out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The synthetic session names end up in the harness screenshots, so shipping one contributor's project list into everyone else's review reads oddly. The mix of CLI modes is what the fixture actually needs — each renders a different badge — and that is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in christianhaberl#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>
Uh oh!
There was an error while loading. Please reload this page.
CreatureSurvive pushed a commit
to CreatureSurvive/Codeman
that referenced
this pull request
Aug 17, 2026
Five post-merge review items from PRs Ark0N#306 (clickable file paths) and Ark0N#307 (session sidebar): - constants.js FILE_PREVIEW_EXTENSIONS gains the media extensions it was missing vs the single-source sets in attachment-registry.ts (m4v ogv ogg oga m4a aac flac opus), so an in-workspace .m4a opens the preview player instead of the log viewer; new test/media-extension-parity.test.ts pins all three copies (constants.js, panels-ui.js, attachment-registry.ts) against each other. - FILE_PATH_LINK_PATTERN drops `etc` from its root alternation: /etc is unconditionally in DEFAULT_BLOCKED_TREES, so every /etc link 403'd. Negative cases added to the link-provider and response-viewer tests. - updateSidebarCount() counts the rows actually on the sidebar list (session rows + web-tab rows, minus filtered-out ones) instead of this.sessions.size, and applySidebarFilter() refreshes it so the count follows the filter box per keystroke. - The incremental-render connection-line gate now also fires in sidebar layout (this._lineageEdgeCount is permanently 0 there), matching the strip-scroll listener widened in Ark0N#307, so a badge changing row heights redraws subagent/ultracode connectors. - isSensitivePath() blocks ~/.claude.json, ~/.claude/settings.json and ~/.claude/settings.local.json (credential-bearing by schema), anchored to homedir() read at check time so case-level .claude/settings*.json files stay servable in the File Viewer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
An optional, collapsible left session sidebar as an alternative to the header tab strip, contributed by @christianhaberl (built and reviewed on his fork as christianhaberl#4, ported here to current master with his three commits and authorship preserved).
With many concurrent sessions the horizontal strip stops working: it wraps into multiple rows, eats header space, and the hover-grown close button shifts tabs under the cursor. The sidebar puts the session list in a vertical
<aside>with a filter box and a live count, collapsible to a 44px rail that keeps status dots and task/subagent/ultracode badges visible.Alt+Btoggles it.Nothing changes unless you opt in. The header strip stays the default; the switch lives in App Settings -> Layout -> Tabs -> Session List Layout (per-device, like the other display keys).
The load-bearing design decision
There is exactly ONE
#sessionTabselement.applySessionListLayout()re-parents that same node between the header host and the sidebar. Nothing is cloned or rebuilt, becauseapp.$()caches elements by id and never invalidates: a rebuilt container would leave every consumer (settings-ui, webview-tabs, the gesture bundle, the renderers, drag/keyboard handlers) silently writing into a detached orphan. Consequence: web tabs, Alt+N numbering, alerts, badges, drag-reorder and keyboard navigation all work in both layouts with zero renderer changes.Below 1024px the sidebar is an off-canvas drawer over the terminal (closed drawers get
inert+aria-hidden, selection dismisses it, swipes over it no longer switch sessions). The breakpoint is deliberately 1024, matching mobile.css'smediaattribute, NOTMobileDetection.getDeviceType(), which calls 768-1023px "desktop" and gave that band overlay CSS with docked logic.What the port to current master involved
The branch was cut ~211 commits ago, so beyond conflict resolution:
set-*surface (One settings surface: App Settings, Session Options and Add Case #278 restructured the modal after the branch was cut): aset-rowin Layout -> Tabs replaces the oldsettings-itemmarkup.computeLineagePath()'s whole geometry hangs a U-bridge from the horizontal strip's bottom edge, which has no meaning against a vertical list. A sideways lineage shape needs its own visual tuning and is a follow-up, not a by-product of re-parenting. The lineage strip-scroll listener now also redraws the subagent/ultracode connectors while the sidebar scrolls vertically (those DO adapt: right-edge anchors, horizontal beziers)._scrollActiveTabIntoView()(Mobile tab bar cannot reach rightmost tabs #257 landed after the branch): sidebar mode branches toscrollIntoView({block: 'nearest'})because the horizontalcomputeTabScrollLeftmath no-ops against a vertical scroller, and_fullRenderSessionTabs()now restoresscrollTopalongside the Mobile tab bar cannot reach rightmost tabs #257scrollLeftrestore, so ambient rebuilds (a badge appearing, a session created elsewhere) cannot yank a mid-scroll sidebar back to the top.Traps the original PR caught (worth knowing before touching this)
ESC binto the PTY:preventDefault()in the capture handler does not stop xterm, so the chord needs its own gate inattachCustomKeyEventHandler. The gate is registry-aware and only active in sidebar layout, so a rebind or the default layout keeps Meta-b reaching the terminal.codeman-sidebar-collapsedkey, not the settings blob, whichsaveAppSettings()rebuilds from DOM controls (a key without a control is wiped on every Save).Verification
typecheck,lint,format:check,check:frontend-syntax,check:public-assetsall green; both merged stylesheets re-validated with PostCSS.test/session-list-layout.test.ts(schema, re-parenting, cache identity, solo windows, collapse round-trip, drawer inert state, the 768-1023 band, filter lifecycle).app-settings-structure,session-options-structure,mobile-header-buttons-policy,home-sessions,skin-themes,session-lineage-lines.scripts/verify-session-sidebar.mts(the contributor's headless-Chromium harness: a real WebServer, 25 synthetic sessions) re-run against this port: all seven layout states correct at 1600/1000/393px, screenshots eyeballed.npm run test:cisweep green.Follow-ups (not in this PR)
docs/architecture-invariants.md, "Session list layout"), with the port decisions documented.