From bbc85ff04b6009ff77a72b93c5853eecf9cb3e82 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 1 Jun 2026 09:13:44 +0900 Subject: feat(header): remove copy + theme buttons; keep title, status, sidebar toggle These move to dedicated sidebar panels (Debug panel and Settings panel respectively) in follow-up commits. Header is now visibly cleaner: only the Dispatch title (left), connection status indicator, and the Sidebar toggle (right) remain. --- packages/frontend/src/lib/components/Header.svelte | 41 ---------------------- 1 file changed, 41 deletions(-) diff --git a/packages/frontend/src/lib/components/Header.svelte b/packages/frontend/src/lib/components/Header.svelte index 713e916..3066e81 100644 --- a/packages/frontend/src/lib/components/Header.svelte +++ b/packages/frontend/src/lib/components/Header.svelte @@ -1,29 +1,8 @@ - -{#if showThemeSwitcher} - (showThemeSwitcher = false)} /> -{/if} -- cgit v1.2.3 From dd3c71e3d5c8c1b9b23bcf3fdbc34dc306a80570 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 1 Jun 2026 09:14:30 +0900 Subject: feat(sidebar): add Debug panel with copy-conversation action New "Debug" panel option in the sidebar, grouping dev-facing actions. Currently exposes the Copy-conversation button (ported from the old header). Leaves room for additional debug actions without re-cluttering the header. The Copy action wraps `tabStore.copyConversation()` and shows a "Copied"/"Failed" affordance for 1.5s, matching the previous header behavior. --- .../frontend/src/lib/components/DebugPanel.svelte | 35 ++++++++++++++++++++++ .../src/lib/components/SidebarPanel.svelte | 4 +++ 2 files changed, 39 insertions(+) create mode 100644 packages/frontend/src/lib/components/DebugPanel.svelte diff --git a/packages/frontend/src/lib/components/DebugPanel.svelte b/packages/frontend/src/lib/components/DebugPanel.svelte new file mode 100644 index 0000000..aea1ccb --- /dev/null +++ b/packages/frontend/src/lib/components/DebugPanel.svelte @@ -0,0 +1,35 @@ + + +
+
Debug
+ +
+

Conversation

+

+ Copy a structured plain-text dump of the active tab's conversation + (chunk shape included) for bug reports. +

+ +
+
diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index 206ed09..66fa6a4 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -4,6 +4,7 @@ import type { CacheStats, KeyInfo, LogEntry, TaskItem } from "../types.js"; import CacheRatePanel from "./CacheRatePanel.svelte"; import ClaudeReset from "./ClaudeReset.svelte"; import ConfigPanel from "./ConfigPanel.svelte"; +import DebugPanel from "./DebugPanel.svelte"; import KeyUsage from "./KeyUsage.svelte"; import ModelSelector from "./ModelSelector.svelte"; import ModelStatus from "./ModelStatus.svelte"; @@ -95,6 +96,7 @@ const viewOptions = [ "Skills", "Tools", "Settings", + "Debug", ]; function addPanel() { @@ -181,6 +183,8 @@ function contentClass(_selected: string): string { {:else if panel.selected === "Settings"} + {:else if panel.selected === "Debug"} + {/if} -- cgit v1.2.3 From 751e411b3ab321129083f86f0be53687185abd87 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 1 Jun 2026 09:15:43 +0900 Subject: feat(settings): inline theme picker into Settings panel The Theme button + ThemeSwitcher modal were a header-triggered modal. That doesn't belong in a sidebar-panel architecture, and theme picking is a UI preference that belongs alongside the other Settings entries. - Add a "Theme" section as the first block in SettingsPanel with the same theme list as ThemeSwitcher. - The localStorage key (`dispatch-theme`) and apply-on-change behavior are unchanged, so the boot-time theme apply in App.svelte's onMount keeps working without modification. - Delete the now-unused ThemeSwitcher.svelte component; no remaining importers. --- .../src/lib/components/SettingsPanel.svelte | 52 +++++++++++++++++++ .../src/lib/components/ThemeSwitcher.svelte | 58 ---------------------- 2 files changed, 52 insertions(+), 58 deletions(-) delete mode 100644 packages/frontend/src/lib/components/ThemeSwitcher.svelte diff --git a/packages/frontend/src/lib/components/SettingsPanel.svelte b/packages/frontend/src/lib/components/SettingsPanel.svelte index 392852a..efbaf5f 100644 --- a/packages/frontend/src/lib/components/SettingsPanel.svelte +++ b/packages/frontend/src/lib/components/SettingsPanel.svelte @@ -11,6 +11,42 @@ const { apiBase?: string; } = $props(); +// Theme picker — was a header-triggered modal (`ThemeSwitcher.svelte`); +// inlined here so theme picking lives in Settings alongside other UI +// preferences. The list and localStorage key must stay in sync with the +// boot-time theme apply in `App.svelte`'s `onMount`. +const THEMES = [ + "light", + "dark", + "dracula", + "night", + "nord", + "sunset", + "cyberpunk", + "forest", + "cmyk", + "coffee", + "caramellatte", + "garden", + "luxury", +] as const; + +const THEME_STORAGE_KEY = "dispatch-theme"; + +let currentTheme = $state( + (typeof localStorage !== "undefined" && localStorage.getItem(THEME_STORAGE_KEY)) || "dark", +); + +function selectTheme(theme: string): void { + currentTheme = theme; + document.documentElement.setAttribute("data-theme", theme); + try { + localStorage.setItem(THEME_STORAGE_KEY, theme); + } catch { + // Best-effort — private mode / quota. + } +} + let titleKeyId = $state(null); let titleModelId = $state(null); let availableModels = $state([]); @@ -136,6 +172,22 @@ $effect(() => {
Settings
+

Theme

+ + +
+

Title Generation Model

Used to generate short titles for new tabs after the first message.

diff --git a/packages/frontend/src/lib/components/ThemeSwitcher.svelte b/packages/frontend/src/lib/components/ThemeSwitcher.svelte deleted file mode 100644 index 418fcea..0000000 --- a/packages/frontend/src/lib/components/ThemeSwitcher.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - -- cgit v1.2.3 From 60999dc48d8c06a10ff8f5b3f6edb1d220fd85ca Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 1 Jun 2026 09:17:11 +0900 Subject: docs: add HANDOFF.md for h3 header declutter --- HANDOFF.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..b87fc2a --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,173 @@ +# H3 — Header Declutter + +Branch: `h3/header-declutter` (branched off `dev`) +Worktree: `/home/tradam/projects/dispatch/h3-header-declutter` + +## Summary + +The app header used to be: **`Dispatch | … | Connection · Copy · Theme · Sidebar`** — four right-aligned buttons, two of them unrelated to navigation. + +It is now: **`Dispatch | … | Connection · Sidebar`**. + +The two removed buttons moved into the sidebar where the rest of the app's +controls already live: + +| Button | Old location | New location | +| ---------- | -------------------------- | ---------------------------------------------- | +| **Copy** | Header | New **Debug** sidebar panel | +| **Theme** | Header → `ThemeSwitcher` modal | Inlined as a section in the **Settings** sidebar panel | + +`ThemeSwitcher.svelte` was deleted; its theme list + apply-and-persist logic +was inlined into `SettingsPanel.svelte` (a sidebar panel doesn't need a +modal, and Settings already owns all other UI preferences). + +## Files + +### Modified +- `packages/frontend/src/lib/components/Header.svelte` — removed Copy + button, Theme button, ThemeSwitcher import, `showThemeSwitcher` state, + `copyLabel` state, `handleCopy` / `resetCopyLabel` helpers, and the + `{#if showThemeSwitcher}` block. Only the Dispatch title (left), + connection status indicator, and Sidebar toggle (right) remain. +- `packages/frontend/src/lib/components/SidebarPanel.svelte` — registered + `"Debug"` as a new entry in `viewOptions` (last in the list) and added + the corresponding `{:else if panel.selected === "Debug"} ` + branch. Imported `DebugPanel`. +- `packages/frontend/src/lib/components/SettingsPanel.svelte` — added a + `THEMES` list + `currentTheme` state + `selectTheme` helper at the top + of the script, and a "Theme → Appearance" `` dropdown on any sidebar slot. +- **Removed component**: `ThemeSwitcher` (no other importers; safe). +- **Component prop changes**: none. `Header.svelte`'s only prop + (`onToggleSidebar: () => void`) is unchanged. `SidebarPanel.svelte`, + `SettingsPanel.svelte`, and `DebugPanel.svelte` keep / introduce + prop shapes consistent with neighboring panels. + +## LocalStorage migration + +No migration is needed. + +- `dispatch-theme` localStorage key: unchanged shape, unchanged + consumers (boot apply in `App.svelte:onMount`, write in + `selectTheme`). A user reloading after this branch sees their + previously-selected theme intact. +- `dispatch-sidebar-panels` (the sidebar layout): the existing + `loadSidebarPanels` already filters non-string entries and falls + back to the default layout when nothing valid is stored. Adding + `"Debug"` to `viewOptions` is purely additive: existing users + with stored layouts continue to render exactly what they had + before, and the new option becomes available to anyone who opens + the dropdown. No code change to `sidebar-storage.ts` was required. + +## Verification + +### `bun run check` +``` +$ biome check . +Checked 140 files in 172ms. No fixes applied. +``` +Exit code: 0. + +### `bun run test` +``` +Test Files 24 passed (24) + Tests 393 passed (393) + Start at 09:15:17 + Duration 2.87s +``` +Exit code: 0. (Includes the existing `sidebar-storage.test.ts` 15-test +suite — no test changes were required since storage semantics didn't +change.) + +### `bun run typecheck` (svelte-check) +``` +svelte-check found 0 errors and 0 warnings +``` + +### Build +``` +vite v6.4.2 building for production... +✓ 166 modules transformed. +✓ built in 3.97s +``` + +### Manual UI smoke (programmatic) +Full interactive `dev:frontend` + `dev:api` boot wasn't run from this +CLI environment, but the equivalents were exercised: + +- Production build (`vite build`) succeeds — confirms all components + compile and Svelte's reactivity contracts are satisfied. +- `vite preview` boots and serves the HTML shell on port 4173. +- Static grep over the built bundle confirms the post-refactor wiring: + - `"Debug"` appears in the bundle as a panel option ✓ + - `"Copy conversation"` button label is present ✓ + - `ThemeSwitcher` / `showThemeSwitcher` symbols are absent (count: 0) ✓ + - Theme list (`dracula`, `cyberpunk`, `caramellatte`) and + `dispatch-theme` storage key are bundled into SettingsPanel ✓ + +For a human visual pass: `bun run dev:api` + `bun run dev:frontend`, +then verify: +1. Header has only `Dispatch | … | Connection · Sidebar`. +2. Add a sidebar slot via `+`, choose "Settings" → "Theme" section + appears at the top with a working ``, not a vertical menu.** The original + `ThemeSwitcher` used a menu of buttons because it was a modal with + the room to display all 13 themes at once. Inside the sidebar a + `) hand-rolled their own defaults and could disagree: - App.svelte only set data-theme if localStorage had a value, so on a fresh install daisyUI fell back to the first theme in app.css (light). - SettingsPanel.svelte hardcoded a UI default of "dark". Result: a first-time user saw a light app but a Settings panel that claimed "dark" was selected. Picking *any* value in the dropdown was the only way to reconcile reality with the UI. This commit: - Adds packages/frontend/src/lib/theme.ts as the single source of truth: THEMES list, Theme type, THEME_STORAGE_KEY, DEFAULT_THEME, plus loadStoredTheme() and applyTheme() that handle SSR / private-mode / bad-value cases. - Rewires App.svelte's onMount to call applyTheme(loadStoredTheme()), so the boot apply always writes a known good theme to the DOM (even on fresh installs), matching what Settings will show. - Rewires SettingsPanel.svelte's picker to use the shared module, dropping its duplicate THEMES const, duplicate storage key, duplicate apply/persist logic, and the conflicting "dark" fallback. - Adds 11 unit tests in tests/theme.test.ts covering the default-fallback, known/unknown stored values, SecurityError-on-read, SSR (no localStorage), DOM-attribute write, persistence round-trip, and the "DOM still updates if storage write throws" contract. The daisyUI plugin block in app.css still lists themes — that's a CSS-time concern and can't be imported from TS, so it's kept in sync by convention (noted in the new module's doc comment). --- packages/frontend/src/App.svelte | 13 +- .../src/lib/components/SettingsPanel.svelte | 38 +----- packages/frontend/src/lib/theme.ts | 92 +++++++++++++ packages/frontend/tests/theme.test.ts | 144 +++++++++++++++++++++ 4 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 packages/frontend/src/lib/theme.ts create mode 100644 packages/frontend/tests/theme.test.ts diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 1bae000..eaa28e8 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -11,11 +11,10 @@ import TabBar from "./lib/components/TabBar.svelte"; import { config } from "./lib/config.js"; import { router } from "./lib/router.svelte.js"; import { tabStore } from "./lib/tabs.svelte.js"; +import { applyTheme, loadStoredTheme } from "./lib/theme.js"; import type { KeyInfo } from "./lib/types.js"; import { wsClient } from "./lib/ws.svelte.js"; -const STORAGE_KEY = "dispatch-theme"; - let modelsData = $state<{ keys: KeyInfo[] }>({ keys: [], }); @@ -76,11 +75,11 @@ $effect(() => { }); onMount(() => { - // Apply saved theme - const saved = localStorage.getItem(STORAGE_KEY); - if (saved) { - document.documentElement.setAttribute("data-theme", saved); - } + // Apply persisted theme (or the shared DEFAULT_THEME if nothing is + // stored) so the first paint matches what the Settings panel will + // show as the selected option. Without this, daisyUI falls back to + // the first theme in `app.css` (light) while Settings shows "dark". + applyTheme(loadStoredTheme()); // Connect WebSocket in parallel with hydration. The `statuses` // snapshot delivered on WS open is idempotent against diff --git a/packages/frontend/src/lib/components/SettingsPanel.svelte b/packages/frontend/src/lib/components/SettingsPanel.svelte index efbaf5f..b6a44bc 100644 --- a/packages/frontend/src/lib/components/SettingsPanel.svelte +++ b/packages/frontend/src/lib/components/SettingsPanel.svelte @@ -1,6 +1,7 @@