From 44c67fcee7308eaad09d26402ac3fa461d3dcd13 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Mon, 31 Aug 2026 15:30:19 +0100 Subject: [PATCH 1/2] feat(maestro): move header actions to the sidebar footer Help, theme toggle, and Spawn now live pinned to the bottom of the sidebar instead of the top-right app header, keeping them next to the session list they act on. --- web/src/catalog.ts | 45 ++++++++++++++++++++++++++------------------- web/src/styles.css | 17 +++++++++++++++-- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/web/src/catalog.ts b/web/src/catalog.ts index 6c88d24..09d5e42 100644 --- a/web/src/catalog.ts +++ b/web/src/catalog.ts @@ -300,8 +300,8 @@ function renderEmpty(filtering: boolean, onSpawn: () => void): HTMLElement { /** * The global app header (full width, above the sidebar/content split): brand + - * the app-level actions (spawn, theme toggle). Kept out of the sidebar so these - * stay put and reachable regardless of which pane is showing. + * session tally. App-level actions (help, theme, spawn) live pinned to the + * sidebar's foot instead — see {@link renderSidebar}. */ export function renderTopbar(store: CatalogStore, onToggleSidebar?: () => void): HTMLElement { const header = document.createElement("header"); @@ -340,23 +340,6 @@ export function renderTopbar(store: CatalogStore, onToggleSidebar?: () => void): kit.title = "Open the design kit (DEV)"; actions.appendChild(kit); } - const helpBtn = document.createElement("button"); - helpBtn.type = "button"; - helpBtn.className = "btn btn-ghost btn-icon help-btn"; - helpBtn.textContent = "?"; - helpBtn.title = "Keyboard shortcuts (?)"; - helpBtn.setAttribute("aria-label", "Keyboard shortcuts"); - helpBtn.addEventListener("click", () => toggleHelp()); - actions.appendChild(helpBtn); - actions.appendChild(createThemeToggle()); - const spawnBtn = document.createElement("button"); - spawnBtn.type = "button"; - spawnBtn.className = "btn btn-primary spawn-btn"; - spawnBtn.textContent = "+ Spawn"; - // The keyboard pair sits right here in the tooltip (#67). - spawnBtn.title = "Draft pane: n — fast-forward spawn: N"; - spawnBtn.addEventListener("click", () => requestOpenSpawn()); - actions.appendChild(spawnBtn); return header; } @@ -425,6 +408,30 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S treeHost.className = "tree"; el.appendChild(treeHost); + // App-level actions (help, theme, spawn) live pinned to the sidebar's foot + // rather than the sidebar's own scrolling content, so they stay reachable + // regardless of tree scroll position. + const footer = document.createElement("div"); + footer.className = "sidebar-footer"; + const helpBtn = document.createElement("button"); + helpBtn.type = "button"; + helpBtn.className = "btn btn-ghost btn-icon help-btn"; + helpBtn.textContent = "?"; + helpBtn.title = "Keyboard shortcuts (?)"; + helpBtn.setAttribute("aria-label", "Keyboard shortcuts"); + helpBtn.addEventListener("click", () => toggleHelp()); + footer.appendChild(helpBtn); + footer.appendChild(createThemeToggle()); + const spawnBtn = document.createElement("button"); + spawnBtn.type = "button"; + spawnBtn.className = "btn btn-primary spawn-btn"; + spawnBtn.textContent = "+ Spawn"; + // The keyboard pair sits right here in the tooltip (#67). + spawnBtn.title = "Draft pane: n — fast-forward spawn: N"; + spawnBtn.addEventListener("click", () => requestOpenSpawn()); + footer.appendChild(spawnBtn); + el.appendChild(footer); + // Drag a session row into the canvas (issue #32). Delegated so it survives // tree re-renders; the controller handles drag-vs-tap (tap still navigates). treeHost.addEventListener("pointerdown", (e) => { diff --git a/web/src/styles.css b/web/src/styles.css index 681b2be..2303e3c 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -484,14 +484,27 @@ textarea { } } -/* Sidebar's own slim header: just the session search (app-level actions live - in the global top bar). */ +/* Sidebar's own slim header: just the session search. */ /* The search input IS the header — it spans edge to edge, flush, with just a bottom rule for separation. No wrapper padding, no boxed border/radius. */ .sidebar-search { flex: 0 0 auto; } +/* App-level actions (help, theme, spawn), pinned to the sidebar's foot. */ +.sidebar-footer { + display: flex; + align-items: center; + gap: var(--sp-2); + flex: 0 0 auto; + padding: var(--sp-2) var(--sp-3); + border-top: 1px solid var(--border); +} + +.sidebar-footer .spawn-btn { + flex: 1 1 auto; +} + .sidebar-search .search { width: 100%; max-width: none; From 511ee3be4e8cd2a441357e96cd70d2fd34f1f5c4 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Mon, 31 Aug 2026 15:31:00 +0100 Subject: [PATCH 2/2] fix(maestro): show tool icon in attach header instead of raw command Session header used to print the raw command line ("claude"); mirror the sidebar's icon-for-program treatment so both agree. Co-Authored-By: Claude Sonnet 5 --- web/src/attach.ts | 14 +++++++++++--- web/src/styles.css | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web/src/attach.ts b/web/src/attach.ts index 57a96d6..22ae5c3 100644 --- a/web/src/attach.ts +++ b/web/src/attach.ts @@ -6,6 +6,7 @@ import { attachWsUrl } from "./routes.js"; import { escapeHtml } from "./html.js"; import { INITIAL_RECONNECT_MS, MAX_RECONNECT_MS } from "./reconnect.js"; import { applyModsToChar, cursorSeq } from "./keys.js"; +import { splitCommand, toolIconSvg } from "./toolIcon.js"; import type { ClientControl, Uuid } from "./types.js"; /** @@ -54,6 +55,15 @@ export interface AttachInfo { command: string; } +/** Header echoes the sidebar: the command's leading program becomes its tool + * icon when we have a mark for it, remaining words stay as text. */ +function commandFragment(command: string): string { + const { tool, rest } = splitCommand(command); + const icon = tool ? toolIconSvg(tool) : null; + const text = icon ? rest : command; + return `${icon ?? ""}${text ? escapeHtml(text) : ""}`; +} + export function createAttachConnection( sessionId: Uuid, opts: AttachOptions = {}, @@ -65,9 +75,7 @@ export function createAttachConnection( // tooltip for when you really need it. const titleHtml = info ? `${escapeHtml(info.name)}` + - (info.command && info.command !== info.name - ? `${escapeHtml(info.command)}` - : "") + (info.command && info.command !== info.name ? commandFragment(info.command) : "") : `session ${escapeHtml(sessionId.slice(0, 8))}`; const titleAttr = info ? `${info.name} · ${info.command} (${sessionId})` : sessionId; el.innerHTML = ` diff --git a/web/src/styles.css b/web/src/styles.css index f6ebced..c97d6b0 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1279,6 +1279,9 @@ input.leaf-rename:focus { .attach-title .at-cmd { color: var(--muted); + display: inline-flex; + align-items: center; + gap: 4px; } .attach-title .at-cmd::before {