Files
muxplex/docs/plans/2026-03-27-session-sidebar-design.md
2026-03-27 16:05:57 -07:00

140 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session Sidebar Design
## Goal
Add a collapsible left-side sidebar to the terminal (expanded) view that shows a live-updating session list with snapshot previews, usable as a session switcher while actively working in a terminal session.
## Background
The current workflow requires returning to the dashboard to switch sessions or see what's happening in other terminals. A persistent sidebar in the terminal view eliminates that round-trip, giving users at-a-glance visibility into all sessions while keeping the active terminal front and center.
## Approach
Pure frontend addition — no backend or coordinator changes. The sidebar consumes the same `sessions` data already fetched by the poll loop, reuses the existing `openSession()` function for switching, and relies entirely on CSS for layout transitions and responsive behaviour. New code is limited to a `renderSidebar()` function and associated styles.
## Architecture
### Layout
`view-expanded` becomes a flex row with two children:
```
┌──────────────────────────────────────────────────┐
│ view-expanded (display: flex; flex-direction: row) │
│ ┌────────────┐ ┌──────────────────────────────┐ │
│ │ .session- │ │ .terminal-container │ │
│ │ sidebar │ │ (flex: 1) │ │
│ │ (200px) │ │ │ │
│ │ │ │ │ │
│ │ .sidebar- │ │ │ │
│ │ header │ │ │ │
│ │ .sidebar- │ │ │ │
│ │ list │ │ │ │
│ └────────────┘ └──────────────────────────────┘ │
└──────────────────────────────────────────────────┘
```
### Data Flow
```
poll tick
├── renderGrid(sessions) ← dashboard (unchanged)
└── renderSidebar(sessions, name) ← sidebar (new, same data)
```
No extra network requests. `renderSidebar` skips work when the dashboard view is active.
## Components
### 1 — Layout and Structure
- `view-expanded` container: `display: flex; flex-direction: row`.
- `.session-sidebar`: fixed `width: 200px`, positioned left.
- `.sidebar-header`: title text + toggle button.
- `.sidebar-list`: `overflow-y: auto`, scrollable card list.
- `.terminal-container`: `flex: 1`, fills remaining width.
- Collapse: toggling `.sidebar--collapsed` sets `width: 0; overflow: hidden` with a CSS transition (`0.25s ease`).
- Toggle button: sidebar icon (`⋮⋮` or hamburger) in the expanded view header bar next to the existing back button. Secondary ``/`` chevron at the sidebar's right edge in wide (side-by-side) mode only.
- State persisted in `localStorage('muxplex.sidebarOpen')`.
### 2 — Session Cards
Each entry in `.sidebar-list` is a compact card:
```
┌─────────────────────────────────┐
│ ● session-name [bell 2] │ ← header row: name + badge
├─────────────────────────────────┤
│ last line of output │
│ $ some command running... │ ← snapshot preview, bottom-anchored
│ (blank) │
└─────────────────────────────────┘
```
- Card height: ~120px (header ~32px + 46 lines of 11px monospace preview).
- Snapshot preview: same `lastLines` data already on each session, same `position: absolute; bottom: 0` bottom-anchoring CSS already shipped, restyled for narrower column.
- Active session card: 3px left border in `var(--accent-cyan)`, slightly elevated background (`var(--bg-surface)`).
- Click handler: calls existing `openSession(name)`. Clicking the active session's card is a no-op.
- Session order: mirrors coordinator response order (bell-priority on mobile, natural order on desktop) — no new sorting logic.
### 3 — Data Flow and State
Two pieces of state:
| Variable | Type | Source | Purpose |
|---|---|---|---|
| `currentSession` | `string \| null` | Set by `openSession(name)`, cleared by `closeSession()` | Determines which card gets active highlight |
| `sidebarOpen` | `boolean` | `localStorage('muxplex.sidebarOpen')` | Controls `.sidebar--collapsed` CSS class |
- `renderSidebar(sessions, currentSession)` is called alongside the existing `renderGrid(sessions)` on every poll tick — same data, zero extra network requests.
- `renderSidebar` only does visible work when the terminal view is active. If the dashboard is showing, the sidebar DOM exists but rendering is skipped.
- On `openSession`, the sidebar gets an initial render before the first poll tick catches up.
- `sidebarOpen` toggle applies/removes `.sidebar--collapsed` — animation is CSS-only, no JS timers. Value written back to localStorage on every toggle.
### 4 — Responsive / Overlay Behaviour
**Breakpoint: 960px** viewport width.
| Mode | Width | Sidebar Position | Animation | Chevron |
|---|---|---|---|---|
| Side-by-side | ≥960px | In-flow, flex child | `width: 0/200px` | ``/`` at right edge |
| Overlay | <960px | `position: fixed; left: 0; top: 0; height: 100%` | `transform: translateX(-100%/0)` 0.25s ease | None |
- In overlay mode the terminal stays full-width — nothing shrinks.
- Clicking anywhere on `.terminal-container` closes the overlay.
- Single toggle button always lives in the expanded header bar regardless of mode.
- Default `sidebarOpen`: `true` on wide screens, `false` on narrow (when no localStorage value exists). Stored value respected on subsequent visits regardless of current screen width.
## Error Handling
- Empty sessions array: `renderSidebar` produces an empty list — no crash, no error state.
- Session names are HTML-escaped before rendering to prevent injection.
- `localStorage` read/write wrapped defensively — if unavailable, defaults apply and toggle still works for the current page session.
## Testing Strategy
Unit tests in `frontend/tests/test_app.mjs`, three groups:
### `buildSidebarHTML` output
- Active session card gets `.sidebar-item--active` class; non-active cards do not.
- Bell badge renders when `unseen_count > 0`; absent when 0 or null.
- Session name is HTML-escaped (injection check).
- Empty sessions array produces an empty list, no crash.
- Snapshot preview uses bottom-anchored last-lines approach.
### Sidebar state
- `sidebarOpen` written to `localStorage` on every toggle.
- Default value: `true` when no stored value exists and viewport is wide; `false` when narrow.
- `.sidebar--collapsed` class added/removed in sync with stored value.
### Session switching
- Sidebar card click calls `openSession(name)` with the correct session name.
- Clicking the active session's card is a no-op.
### Not unit-tested
CSS breakpoint behaviour and overlay slide animation are verified by visual inspection during implementation — not unit-testable.
## Open Questions
None — all sections validated.