From c170cd9d6047b6c0c5ae83587347edafa4501682 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Thu, 10 Sep 2026 17:17:22 +0100 Subject: [PATCH] fix(maestro): restore sidebar footer, fix spawn modal and session card regressions Post-React-migration regressions: spawn submit button moved to modal footer, sidebar footer restored (help/theme/palette/status/spawn), session card stripped of redundant Tailwind, action buttons use proper leaf-act/leaf-actions CSS structure for hover-only visibility. Co-Authored-By: Claude Opus 4.6 --- .claude/settings.local.json | 5 +- web/src/App.tsx | 1 + web/src/components/dialogs/HelpOverlay.tsx | 8 +- web/src/components/sidebar/SessionCard.tsx | 12 +-- web/src/components/sidebar/Sidebar.test.tsx | 76 ++++++++++------- web/src/components/sidebar/Sidebar.tsx | 92 ++++++++++++++++++++- web/src/components/spawn/SpawnForm.tsx | 26 +++--- web/src/components/spawn/SpawnModal.tsx | 11 ++- web/src/contexts/ThemeContext.tsx | 24 ++++-- web/src/uiEvents.ts | 8 ++ 10 files changed, 202 insertions(+), 61 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9d765fc..5584f84 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,7 +21,10 @@ "Bash(git -C /home/naps62/tea/maestro push)", "Bash(curl:*)", "Bash(nix build:*)", - "Bash(cargo fmt:*)" + "Bash(cargo fmt:*)", + "Skill(impeccable)", + "Skill(impeccable:*)", + "Bash(node:*)" ] } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 1b58c27..d880949 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -314,6 +314,7 @@ function Shell() { activeId={focusedSessionId} openIds={openIds} previewId={previewedSessionId} + linkStore={linkStore} onSelect={(id) => focusSession(id)} onPreview={preview} onSpawn={openPlainSpawn} diff --git a/web/src/components/dialogs/HelpOverlay.tsx b/web/src/components/dialogs/HelpOverlay.tsx index f9a680c..66d2827 100644 --- a/web/src/components/dialogs/HelpOverlay.tsx +++ b/web/src/components/dialogs/HelpOverlay.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { CommandSet, defineCommands, keyCaps } from "../../commands.js"; +import { TOGGLE_HELP_EVENT } from "../../uiEvents.js"; import { groupedCommands } from "./model.js"; export function HelpOverlay() { @@ -18,8 +19,13 @@ export function HelpOverlay() { if (open && overlayKeys.handle(e)) return; helpKeys.handle(e); }; + const onToggle = () => setOpen((o) => !o); window.addEventListener("keydown", onKeydown); - return () => window.removeEventListener("keydown", onKeydown); + document.addEventListener(TOGGLE_HELP_EVENT, onToggle); + return () => { + window.removeEventListener("keydown", onKeydown); + document.removeEventListener(TOGGLE_HELP_EVENT, onToggle); + }; }, [open]); if (!open) return null; diff --git a/web/src/components/sidebar/SessionCard.tsx b/web/src/components/sidebar/SessionCard.tsx index f429b3f..0411c18 100644 --- a/web/src/components/sidebar/SessionCard.tsx +++ b/web/src/components/sidebar/SessionCard.tsx @@ -62,7 +62,7 @@ export function SessionCard(props: SessionCardProps) { return ( - + {renaming ? ( ) : ( {name} )} - + + + + {linkStore && } + + + ); +} + +/** Daemon status indicator: a coloured dot whose tooltip names the release. */ +function StatusDot({ store }: { store: DaemonLinkStoreApi }) { + const state = useStore(store, (s: DaemonLinkState) => s.state); + const build = useStore(store, (s: DaemonLinkState) => s.build); + const hostname = useStore(store, (s: DaemonLinkState) => s.hostname); + + return ( + + ); +} diff --git a/web/src/components/spawn/SpawnForm.tsx b/web/src/components/spawn/SpawnForm.tsx index 4acb015..8811351 100644 --- a/web/src/components/spawn/SpawnForm.tsx +++ b/web/src/components/spawn/SpawnForm.tsx @@ -104,9 +104,11 @@ export interface SpawnFormProps { /** Fired whenever any value changes, once the form's initial seed settles. */ onChange?(values: SpawnFormValues): void; onSuccess(session: Session): void; + /** When true, hides the built-in submit button (the modal provides its own). */ + externalSubmit?: boolean; } -export function SpawnForm({ seed, onChange, onSuccess }: SpawnFormProps) { +export function SpawnForm({ seed, onChange, onSuccess, externalSubmit }: SpawnFormProps) { const prefill = useRef(seed ?? {}).current; const formRef = useRef(null); @@ -240,7 +242,7 @@ export function SpawnForm({ seed, onChange, onSuccess }: SpawnFormProps) { const [advancedCollapsed, setAdvancedCollapsed] = useState(true); const [error, setError] = useState(""); - const [submitting, setSubmitting] = useState(false); + const [, setSubmitting] = useState(false); const [ready, setReady] = useState(false); const probeTokenRef = useRef(0); @@ -620,6 +622,7 @@ export function SpawnForm({ seed, onChange, onSuccess }: SpawnFormProps) { return ( ); } diff --git a/web/src/components/spawn/SpawnModal.tsx b/web/src/components/spawn/SpawnModal.tsx index 696d529..b33aae6 100644 --- a/web/src/components/spawn/SpawnModal.tsx +++ b/web/src/components/spawn/SpawnModal.tsx @@ -64,16 +64,23 @@ export function SpawnModal({ seed, onSuccess, onClose }: SpawnModalProps) {
- +
+
, diff --git a/web/src/contexts/ThemeContext.tsx b/web/src/contexts/ThemeContext.tsx index 7589cde..2362069 100644 --- a/web/src/contexts/ThemeContext.tsx +++ b/web/src/contexts/ThemeContext.tsx @@ -26,15 +26,23 @@ export type Palette = (typeof PALETTES)[number]; const PALETTE_KEY = "maestro-palette"; function storedTheme(): Theme | null { - const v = localStorage.getItem(THEME_KEY); - return v === "light" || v === "dark" ? v : null; + try { + const v = localStorage.getItem(THEME_KEY); + return v === "light" || v === "dark" ? v : null; + } catch { + return null; + } } function storedPalette(): Palette | null { - const v = localStorage.getItem(PALETTE_KEY); - return (PALETTES as readonly string[]).includes(v ?? "") - ? (v as Palette) - : null; + try { + const v = localStorage.getItem(PALETTE_KEY); + return (PALETTES as readonly string[]).includes(v ?? "") + ? (v as Palette) + : null; + } catch { + return null; + } } function systemPrefersLight(): boolean { @@ -89,7 +97,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { }, [palette]); const setTheme = useCallback((t: Theme) => { - localStorage.setItem(THEME_KEY, t); + try { localStorage.setItem(THEME_KEY, t); } catch { /* test env */ } setPinnedTheme(t); }, []); @@ -98,7 +106,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { }, [theme, setTheme]); const setPalette = useCallback((p: Palette) => { - localStorage.setItem(PALETTE_KEY, p); + try { localStorage.setItem(PALETTE_KEY, p); } catch { /* test env */ } setPaletteState(p); }, []); diff --git a/web/src/uiEvents.ts b/web/src/uiEvents.ts index 935993e..5acc5b0 100644 --- a/web/src/uiEvents.ts +++ b/web/src/uiEvents.ts @@ -56,3 +56,11 @@ export const TOGGLE_DRAWER_EVENT = "maestro-toggle-drawer"; export function requestToggleDrawer(): void { document.dispatchEvent(new CustomEvent(TOGGLE_DRAWER_EVENT)); } + +/** Toggle the help overlay from a button click (the `?` key is handled by + * HelpOverlay's own command set). */ +export const TOGGLE_HELP_EVENT = "maestro-toggle-help"; + +export function requestToggleHelp(): void { + document.dispatchEvent(new CustomEvent(TOGGLE_HELP_EVENT)); +}