feat(maestro): web UI persistent sidebar + content split w/ background connection pool (NAP-338)
ci / rust (push) Successful in 5m24s

Replace the full-screen route-swap SPA with a persistent two-pane layout:

- Global top bar (brand + Spawn + theme) above a sidebar (search + status +
  session tree, highlights the active session) and a content pane (welcome or
  the attached terminal). Selecting a row swaps only the content pane; the
  sidebar and its store subscription persist.
- New `connections.ts` ConnectionPool: attach connections (terminal + WS) are
  decoupled from the UI — navigating away parks a session (keeps it streaming)
  rather than tearing it down, so switching back is instant with scrollback
  intact. LRU-bounded (MAX_LIVE=6); explicit `⏻` disconnect distinct from nav.
- `attach.ts` → mountable `createAttachConnection`; `catalog.ts` exposes
  `renderTopbar` + `renderSidebar`; `main.ts` is the shell + hash router.
- Responsive: desktop side-by-side; mobile single-pane (list ↔ terminal).
  Single session viewed at a time (split panes deferred).

Typecheck + production build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 12:49:16 +00:00
co-authored by Claude Opus 4.8
parent 4247b5706e
commit 4c49abfdf2
5 changed files with 469 additions and 113 deletions
+134
View File
@@ -319,6 +319,92 @@ textarea {
font-size: var(--fs-15);
}
/* ---------- app shell: sidebar + content ---------- */
/* Global app header: brand + app-level actions (spawn, theme toggle), spanning
the full width above the split. */
.app-header {
display: flex;
align-items: center;
gap: var(--sp-3);
flex: 0 0 auto;
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--border);
background: color-mix(in oklab, var(--surface) 82%, transparent);
backdrop-filter: blur(8px);
}
.app-actions {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-left: auto;
}
.app-shell {
flex: 1 1 auto;
min-height: 0;
}
/* Desktop: persistent two-pane split. Sidebar holds the session list; the
content pane shows the welcome state or the attached terminal. */
@media (min-width: 860px) {
.app-shell {
display: grid;
grid-template-columns: clamp(280px, 26vw, 360px) minmax(0, 1fr);
}
}
.sidebar {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
overflow: hidden;
background: color-mix(in oklab, var(--surface) 26%, var(--bg));
border-right: 1px solid var(--border);
}
/* Sidebar's own slim header: just the session search (app-level actions live
in the global top bar). */
.sidebar-search {
flex: 0 0 auto;
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--border);
}
.sidebar-search .search {
width: 100%;
max-width: none;
}
.content {
min-width: 0;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
.content-welcome {
margin: auto;
max-width: 48ch;
}
/* Off-screen home for parked-but-connected terminals. Kept in the DOM (not
display:none) so the live WS keeps buffering into each terminal; on
re-mount we fit + the scrollback is already there, so switching is instant. */
.conn-cache {
position: fixed;
left: -99999px;
top: 0;
width: 1px;
height: 1px;
overflow: hidden;
}
/* ---------- catalog ---------- */
.catalog {
@@ -604,6 +690,20 @@ textarea {
box-shadow: inset 0 0 0 1px var(--accent-ring);
}
/* The session currently mounted in the content pane. */
.tree-leaf.is-active {
background: var(--accent-tint);
border-left-color: var(--accent);
}
.tree-leaf.is-active .leaf-name {
color: var(--ink);
}
.tree-leaf.is-active::after {
opacity: 0;
}
.tree-leaf[data-status="dead"] {
color: var(--muted);
}
@@ -800,6 +900,24 @@ textarea {
height: var(--control-h);
}
/* Desktop keeps the session list permanently in view, so the in-header "back"
arrow is redundant — the sidebar is the nav. Mobile (single-pane) keeps it. */
@media (min-width: 860px) {
.attach-header .back {
display: none;
}
}
.attach-header .disconnect {
color: var(--faint);
}
.attach-header .disconnect:hover {
color: var(--danger);
background: var(--danger-tint);
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}
.attach-title {
font-family: var(--font-mono);
font-size: var(--fs-13);
@@ -1253,6 +1371,22 @@ textarea {
}
}
/* Mobile: no room for a split, so show one pane at a time. The catalog route
shows the session list full-width; opening a session swaps to the terminal
(its header's ← returns to the list). The connection still lives in the pool,
so returning to it is instant. */
@media (max-width: 859px) {
.app-shell[data-route="catalog"] > .content {
display: none;
}
.app-shell:not([data-route="catalog"]) > .sidebar {
display: none;
}
.sidebar {
border-right: 0;
}
}
/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {