diff --git a/ui-electrobun/src/mainview/CommandPalette.svelte b/ui-electrobun/src/mainview/CommandPalette.svelte index 6780f0a..b4f7430 100644 --- a/ui-electrobun/src/mainview/CommandPalette.svelte +++ b/ui-electrobun/src/mainview/CommandPalette.svelte @@ -55,15 +55,17 @@ { id: 'zoom-out', labelKey: 'palette.zoomOut', shortcut: 'Ctrl+-', action: () => dispatch('zoom-out') }, ]; - let COMMANDS = $derived( - COMMAND_DEFS.map(d => ({ + // Build commands once — NOT $derived (creating new array per evaluation causes loops) + function buildCommands(): Command[] { + return COMMAND_DEFS.map(d => ({ id: d.id, label: t(d.labelKey as any), description: d.descKey ? t(d.descKey as any) : undefined, shortcut: d.shortcut, action: d.action, - })) - ); + })); + } + let COMMANDS = buildCommands(); let query = $state(''); let selectedIdx = $state(0); diff --git a/ui-electrobun/src/mainview/project-tabs-store.svelte.ts b/ui-electrobun/src/mainview/project-tabs-store.svelte.ts index f809d93..46fd019 100644 --- a/ui-electrobun/src/mainview/project-tabs-store.svelte.ts +++ b/ui-electrobun/src/mainview/project-tabs-store.svelte.ts @@ -1,13 +1,11 @@ /** * Project tabs store — per-project tab state. * - * Tracks which tab is active and which tabs have been activated (PERSISTED-LAZY - * pattern) for each project card. This allows cross-component access — e.g., - * StatusBar can show which tab is active, palette commands can switch tabs. + * Uses a version counter to signal changes instead of Map reassignment. + * Map reassignment (`_tabs = new Map(_tabs)`) caused infinite reactive loops + * in Svelte 5 because each call created a new object reference. */ -// ── Types ──────────────────────────────────────────────────────────────── - export type ProjectTab = | 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory' | 'comms' | 'tasks'; @@ -23,7 +21,9 @@ interface TabState { // ── State ──────────────────────────────────────────────────────────────── -let _tabs = $state>(new Map()); +// Plain Map — NOT reactive. We use _version to signal changes. +const _tabs = new Map(); +let _version = $state(0); // ── Internal helper ────────────────────────────────────────────────────── @@ -32,36 +32,35 @@ function ensureEntry(projectId: string): TabState { if (!entry) { entry = { activeTab: 'model', activatedTabs: new Set(['model']) }; _tabs.set(projectId, entry); - // Trigger reactivity by reassigning the map - _tabs = new Map(_tabs); + // Do NOT bump version here — this is a read-path side effect + // that would cause infinite loops when called from $derived } return entry; } -// ── Getters ────────────────────────────────────────────────────────────── +// ── Getters (read _version to subscribe to changes) ───────────────────── export function getActiveTab(projectId: string): ProjectTab { + void _version; // subscribe to version counter return _tabs.get(projectId)?.activeTab ?? 'model'; } export function isTabActivated(projectId: string, tab: ProjectTab): boolean { + void _version; return _tabs.get(projectId)?.activatedTabs.has(tab) ?? (tab === 'model'); } -// ── Actions ────────────────────────────────────────────────────────────── +// ── Actions (bump version to notify subscribers) ──────────────────────── export function setActiveTab(projectId: string, tab: ProjectTab): void { const entry = ensureEntry(projectId); entry.activeTab = tab; - entry.activatedTabs = new Set([...entry.activatedTabs, tab]); - // Trigger reactivity - _tabs = new Map(_tabs); + entry.activatedTabs.add(tab); + _version++; // signal change without creating new objects } -/** Remove tab state when a project is deleted. */ export function removeProject(projectId: string): void { - if (_tabs.has(projectId)) { - _tabs.delete(projectId); - _tabs = new Map(_tabs); + if (_tabs.delete(projectId)) { + _version++; } }