docs: add session sidebar design document
This commit is contained in:
@@ -0,0 +1,139 @@
|
|||||||
|
# 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 + 4–6 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.
|
||||||
Reference in New Issue
Block a user