diff --git a/docs/plans/2026-03-27-session-sidebar-implementation.md b/docs/plans/2026-03-27-session-sidebar-implementation.md new file mode 100644 index 0000000..0eff9fe --- /dev/null +++ b/docs/plans/2026-03-27-session-sidebar-implementation.md @@ -0,0 +1,1414 @@ +# Session Sidebar Implementation Plan + +> **Execution:** Use the subagent-driven-development workflow to implement this plan. + +**Goal:** Add a collapsible left-side sidebar to the terminal view showing a live session list with snapshot previews, usable as a session switcher without leaving the active terminal. + +**Architecture:** Pure frontend — no backend changes. Sidebar consumes session data already fetched by the poll loop. CSS-only animations (width transition on wide, transform slide on narrow). Responsive at 960px: side-by-side on wide screens, fixed overlay with click-away on narrow. + +**Tech stack:** Vanilla JS, CSS custom properties, Node test runner (`node:test`), no build step. + +**Design doc:** `muxplex/docs/plans/2026-03-27-session-sidebar-design.md` + +--- + +## Conventions + +- **Working directory for git:** `/home/bkrabach/dev/web-tmux` +- **Working directory for tests:** `/home/bkrabach/dev/web-tmux/muxplex` +- **Test command:** `node --test frontend/tests/test_app.mjs` +- **Test framework:** `node:test` + `node:assert/strict` (no Jest/Vitest) +- **Test file:** `muxplex/frontend/tests/test_app.mjs` — all sidebar tests go here +- **Import pattern:** CJS shim — `const app = require(join(__dirname, '..', 'app.js'))` +- **DOM stubs:** injected onto `globalThis.document` before require +- **App state naming:** `_camelCase` with leading underscore (`_viewingSession`, `_viewMode`) +- **Helper `$`:** `document.getElementById` alias — use throughout app.js +- **Helper `on`:** `element.addEventListener` alias — use for all event binding +- **Exports:** app.js has a `module.exports` block at lines 887–935 — add all new public functions there +- **Commits:** conventional format, atomic per task, run from `/home/bkrabach/dev/web-tmux` + +--- + +## Task 1: HTML structure — sidebar markup, toggle button, view-body wrapper + +**Files:** +- Modify: `muxplex/frontend/index.html` + +### Step 1: Add sidebar toggle button to expanded-header + +In `muxplex/frontend/index.html`, find lines 31–35 (the `expanded-header`). Insert the sidebar toggle button after `#back-btn` and before `#expanded-session-name`: + +Replace: +```html +
+ + +``` + +With: +```html +
+ + + +``` + +### Step 2: Wrap terminal-container in view-body and add sidebar + +Replace line 36: +```html +
+``` + +With: +```html +
+
+ + +
+
+
+``` + +**Important:** `#reconnect-overlay` (line 37) must remain OUTSIDE `.view-body` — it stays as a sibling of `.view-body` inside `#view-expanded`. + +### Step 3: Verify the final structure + +The `#view-expanded` section should now look like: +```html + +``` + +### Step 4: Commit + +```bash +cd /home/bkrabach/dev/web-tmux +git add muxplex/frontend/index.html +git commit -m "feat: add sidebar markup, toggle button, and view-body wrapper to expanded view" +``` + +--- + +## Task 2: CSS — view-body flex layout and --bg-surface variable + +**Files:** +- Modify: `muxplex/frontend/style.css` + +### Step 1: Add --bg-surface CSS variable + +In `muxplex/frontend/style.css`, find the `:root` block (line 3). Add `--bg-surface` after line 10 (`--bg-tile-hover`): + +Replace: +```css + --bg-tile-hover: #1A1F2B; /* --color-bg-surface */ +``` + +With: +```css + --bg-tile-hover: #1A1F2B; /* --color-bg-surface */ + --bg-surface: #1A1F2B; /* sidebar hover & active card background */ +``` + +### Step 2: Add view-body layout rules + +Find the `.terminal-container` rule (lines 367–372). Insert `.view-body` rules immediately BEFORE it: + +Insert before `.terminal-container {`: +```css +/* View body: flex-row region below the expanded header */ +.view-body { + display: flex; + flex-direction: row; + flex: 1; + min-height: 0; + overflow: hidden; +} + +``` + +### Step 3: Add flex properties to .terminal-container + +The existing `.terminal-container` rule (lines 367–372) already has `flex: 1` and `overflow: hidden`. Verify it contains: + +```css +.terminal-container { + flex: 1; + overflow: hidden; + background: #000; + padding: 0 4px; +} +``` + +Add `min-width: 0;` to prevent flex overflow: + +```css +.terminal-container { + flex: 1; + min-width: 0; + overflow: hidden; + background: #000; + padding: 0 4px; +} +``` + +### Step 4: Commit + +```bash +cd /home/bkrabach/dev/web-tmux +git add muxplex/frontend/style.css +git commit -m "feat: add view-body flex layout and --bg-surface variable" +``` + +--- + +## Task 3: CSS — sidebar container and collapse animation + +**Files:** +- Modify: `muxplex/frontend/style.css` + +### Step 1: Add sidebar container styles + +In `muxplex/frontend/style.css`, insert the following block immediately after the `.view-body` rules added in Task 2 (before `.terminal-container`): + +```css +/* ============================================================ + Session sidebar + ============================================================ */ + +/* Sidebar container — wide screens (>=960px): side-by-side */ +.session-sidebar { + width: 200px; + min-width: 200px; + background: var(--bg-secondary); + border-right: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + overflow: hidden; + flex-shrink: 0; + transition: width 0.25s ease, min-width 0.25s ease; +} + +.session-sidebar.sidebar--collapsed { + width: 0; + min-width: 0; +} + +/* Sidebar header */ +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.sidebar-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); +} + +.sidebar-list { + flex: 1; + overflow-y: auto; + overflow-x: hidden; +} + +.sidebar-collapse-btn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 18px; + padding: 2px 6px; + line-height: 1; + border-radius: 4px; +} +.sidebar-collapse-btn:hover { color: var(--text); } + +/* Sidebar toggle in expanded header */ +.sidebar-toggle-btn { + background: none; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-muted); + font-size: 14px; + width: 36px; + height: 36px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + margin-right: 8px; + flex-shrink: 0; +} +.sidebar-toggle-btn:hover { + border-color: var(--accent); + color: var(--text); +} + +``` + +### Step 2: Commit + +```bash +cd /home/bkrabach/dev/web-tmux +git add muxplex/frontend/style.css +git commit -m "feat: add sidebar container styles and collapse animation" +``` + +--- + +## Task 4: CSS — sidebar session card styles + +**Files:** +- Modify: `muxplex/frontend/style.css` + +### Step 1: Add session card styles + +In `muxplex/frontend/style.css`, append the following immediately after the sidebar toggle button rules (after `.sidebar-toggle-btn:hover`), still inside the "Session sidebar" section: + +```css +/* Sidebar session cards */ +.sidebar-item { + height: 120px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-subtle); + cursor: pointer; + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; +} + +.sidebar-item:hover { background: var(--bg-surface); } + +.sidebar-item--active { + background: var(--bg-surface); + border-left: 3px solid var(--accent); +} + +.sidebar-item-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 8px 4px; + flex-shrink: 0; + height: 32px; + gap: 4px; +} + +.sidebar-item-name { + font-size: 12px; + font-weight: 600; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; +} + +.sidebar-item-body { + flex: 1; + position: relative; + overflow: hidden; +} + +.sidebar-item-body pre { + position: absolute; + bottom: 0; + left: 0; + right: 0; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 10px; + line-height: 1.4; + color: var(--text-muted); + white-space: pre; + overflow: hidden; + margin: 0; + padding: 0 8px 6px; +} + +.sidebar-empty { + padding: 16px 12px; + color: var(--text-muted); + font-size: 12px; + text-align: center; +} + +``` + +### Step 2: Commit + +```bash +cd /home/bkrabach/dev/web-tmux +git add muxplex/frontend/style.css +git commit -m "feat: add sidebar session card styles" +``` + +--- + +## Task 5: CSS — responsive overlay at <960px + +**Files:** +- Modify: `muxplex/frontend/style.css` + +### Step 1: Add responsive media query + +In `muxplex/frontend/style.css`, add the following at the very end of the file (after the `.app-wordmark img` rule, which is the last rule currently at line 762): + +```css + +/* ============================================================ + Responsive: sidebar becomes overlay at <960px + ============================================================ */ + +@media (max-width: 959px) { + .session-sidebar { + position: fixed; + left: 0; + top: 0; + height: 100%; + z-index: 200; + width: 240px; + min-width: 240px; + transition: transform 0.25s ease; + transform: translateX(0); + box-shadow: 2px 0 16px rgba(0, 0, 0, 0.5); + } + + .session-sidebar.sidebar--collapsed { + width: 240px; + min-width: 240px; + transform: translateX(-100%); + } + + /* Hide internal chevron in overlay mode — header button is the only toggle */ + .sidebar-collapse-btn { + display: none; + } +} +``` + +### Step 2: Add sidebar to reduced-motion overrides + +Find the existing `@media (prefers-reduced-motion: reduce)` block (around line 726). Add the sidebar transition override inside it: + +After ` .toast { animation: none; }` and before the closing `}`, add: + +```css + + .session-sidebar { + transition: none; + } +``` + +### Step 3: Commit + +```bash +cd /home/bkrabach/dev/web-tmux +git add muxplex/frontend/style.css +git commit -m "feat: add responsive sidebar overlay at <960px and reduced-motion support" +``` + +--- + +## Task 6: `buildSidebarHTML` — session card HTML builder (TDD) + +**Files:** +- Modify: `muxplex/frontend/app.js` +- Modify: `muxplex/frontend/tests/test_app.mjs` + +### Step 1: Write the failing tests + +In `muxplex/frontend/tests/test_app.mjs`, add the following test group at the very end of the file (after the last `closeBottomSheet` test, around line 1648): + +```js + +// ─── buildSidebarHTML ──────────────────────────────────────────────────── + +test('buildSidebarHTML marks active session with sidebar-item--active class', () => { + const session = { name: 'work', snapshot: '', bell: null }; + const html = app.buildSidebarHTML(session, 'work'); + assert.ok(html.includes('sidebar-item--active'), 'active class missing'); +}); + +test('buildSidebarHTML does NOT mark inactive session with sidebar-item--active', () => { + const session = { name: 'work', snapshot: '', bell: null }; + const html = app.buildSidebarHTML(session, 'other'); + assert.ok(!html.includes('sidebar-item--active'), 'active class should not be present'); +}); + +test('buildSidebarHTML renders bell badge when unseen_count > 0', () => { + const session = { name: 'work', snapshot: '', bell: { unseen_count: 3 } }; + const html = app.buildSidebarHTML(session, null); + assert.ok(html.includes('tile-bell'), 'bell badge missing'); + assert.ok(html.includes('>3<'), 'bell count not rendered'); +}); + +test('buildSidebarHTML omits bell badge when unseen_count is 0', () => { + const session = { name: 'work', snapshot: '', bell: { unseen_count: 0 } }; + const html = app.buildSidebarHTML(session, null); + assert.ok(!html.includes('tile-bell'), 'bell badge should be absent'); +}); + +test('buildSidebarHTML HTML-escapes the session name', () => { + const session = { name: '', snapshot: '', bell: null }; + const html = app.buildSidebarHTML(session, null); + assert.ok(!html.includes('