mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +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:
@@ -363,6 +363,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsCjkInput').checked = settings.cjkInputEnabled ?? defaults.cjkInputEnabled ?? false;
|
||||
document.getElementById('appSettingsExtendedKeyboardBar').checked = settings.extendedKeyboardBar ?? false;
|
||||
document.getElementById('appSettingsTabTwoRows').checked = settings.tabTwoRows ?? defaults.tabTwoRows ?? false;
|
||||
document.getElementById('appSettingsSessionListLayout').value =
|
||||
settings.sessionListLayout ?? defaults.sessionListLayout ?? 'header';
|
||||
// Claude CLI settings
|
||||
const claudeModeSelect = document.getElementById('appSettingsClaudeMode');
|
||||
const allowedToolsRow = document.getElementById('allowedToolsRow');
|
||||
@@ -1542,6 +1544,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
webglRendererEnabled: document.getElementById('appSettingsWebglRenderer').checked,
|
||||
extendedKeyboardBar: document.getElementById('appSettingsExtendedKeyboardBar').checked,
|
||||
tabTwoRows: document.getElementById('appSettingsTabTwoRows').checked,
|
||||
sessionListLayout: document.getElementById('appSettingsSessionListLayout').value,
|
||||
skin: document.getElementById('appSettingsSkin').value,
|
||||
// Claude CLI settings
|
||||
claudeMode: document.getElementById('appSettingsClaudeMode').value,
|
||||
@@ -1683,7 +1686,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.applyHeaderVisibilitySettings();
|
||||
this.applySkin();
|
||||
this.applyLocalization();
|
||||
this.applyTabWrapSettings();
|
||||
// Re-parents #sessionTabs between header host and sidebar if the layout
|
||||
// changed, then calls applyTabWrapSettings() itself — do not call both.
|
||||
this.applySessionListLayout();
|
||||
this._updateTokensImmediate(); // Re-render token display (picks up showCost change)
|
||||
this.applyMonitorVisibility();
|
||||
this.renderProjectInsightsPanel(); // Re-render to apply visibility setting
|
||||
@@ -1910,6 +1915,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
imageWatcherEnabled: false,
|
||||
ralphTrackerEnabled: false,
|
||||
tabTwoRows: false,
|
||||
sessionListLayout: 'header',
|
||||
cjkInputEnabled: false,
|
||||
terminalWheelLocalScrollback: false, // mobile scrolls via touch, not wheel
|
||||
webglRendererEnabled: false, // mobile always uses the DOM renderer
|
||||
@@ -2133,19 +2139,27 @@ Object.assign(CodemanApp.prototype, {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const defaults = this.getDefaultSettings();
|
||||
const deviceType = MobileDetection.getDeviceType();
|
||||
// The left sidebar is one vertical column with its own scroller: there is no
|
||||
// row to wrap into, and its rows are always tall (name + folder) because that
|
||||
// is the cheapest way to tell 25 sessions apart. Header strip keeps the old
|
||||
// rules unchanged. Kept here rather than only in applySessionListLayout() so
|
||||
// that a stray applyTabWrapSettings() call (this one is invoked from
|
||||
// saveAppSettings and from the resize path) cannot leave the sidebar wrapped.
|
||||
const sidebar = this.isSessionSidebarActive?.() === true;
|
||||
// Two-row tabs disabled on mobile/tablet — not enough screen space
|
||||
const twoRows = deviceType === 'desktop'
|
||||
const twoRows = !sidebar && deviceType === 'desktop'
|
||||
? (settings.tabTwoRows ?? defaults.tabTwoRows ?? false)
|
||||
: false;
|
||||
const showFolder = sidebar || twoRows;
|
||||
const prevTallTabs = this._tallTabsEnabled;
|
||||
this._tallTabsEnabled = twoRows;
|
||||
this._tallTabsEnabled = showFolder;
|
||||
const tabsEl = document.getElementById('sessionTabs');
|
||||
if (tabsEl) {
|
||||
tabsEl.classList.toggle('tabs-two-rows', twoRows);
|
||||
tabsEl.classList.toggle('tabs-show-folder', twoRows);
|
||||
tabsEl.classList.toggle('tabs-show-folder', showFolder);
|
||||
}
|
||||
// Re-render tabs if folder visibility changed (folder spans are generated in JS)
|
||||
if (prevTallTabs !== undefined && prevTallTabs !== twoRows) {
|
||||
if (prevTallTabs !== undefined && prevTallTabs !== showFolder) {
|
||||
this._fullRenderSessionTabs();
|
||||
}
|
||||
},
|
||||
@@ -2350,7 +2364,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
'showFontControls', 'showSystemStats', 'showTokenCount', 'showCost',
|
||||
'showLifecycleLog', 'showResponseViewer', 'showRedrawButton',
|
||||
'showMonitor', 'showProjectInsights', 'showFileBrowser', 'showSubagents',
|
||||
'subagentActiveTabOnly', 'tabTwoRows', 'localEchoEnabled', 'cjkInputEnabled', 'extendedKeyboardBar',
|
||||
'subagentActiveTabOnly', 'tabTwoRows', 'sessionListLayout', 'localEchoEnabled', 'cjkInputEnabled', 'extendedKeyboardBar',
|
||||
'skin', 'showPlanUsageLimits', 'showAttachmentsButton', 'showFileViewerButton', 'webglRendererEnabled',
|
||||
'language',
|
||||
'terminalWheelLocalScrollback',
|
||||
|
||||
Reference in New Issue
Block a user