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

7.3 KiB
Raw Blame History

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.