diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte new file mode 100644 index 0000000..e309cb5 --- /dev/null +++ b/src/lib/components/NavMenu.svelte @@ -0,0 +1,497 @@ + + +
+ +
+ +{#if showMenu} + + +
(showMenu = false)}>
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+{/if} + +{#if showStats} + + +
(showStats = false)}>
+
+ +
+{/if} + +{#if showAbout} + (showAbout = false)} /> +{/if} + +{#if showHelp} + (showHelp = false)} /> +{/if} + +{#if showKeyboardShortcuts} + (showKeyboardShortcuts = false)} /> +{/if} + +{#if showSessionHistory} + (showSessionHistory = false)} /> +{/if} + +{#if showTodoPanel} + (showTodoPanel = false)} /> +{/if} + +{#if showGitPanel} + (showGitPanel = false)} /> +{/if} + +{#if showProfile} + (showProfile = false)} /> +{/if} + +{#if showAgentMonitor} + (showAgentMonitor = false)} /> +{/if} + +{#if showCastPanel} + (showCastPanel = false)} /> +{/if} + +{#if showPluginPanel} + (showPluginPanel = false)} /> +{/if} + +{#if showMcpPanel} + (showMcpPanel = false)} /> +{/if} + +{#if showProjectContext} + (showProjectContext = false)} + onInject={handleInjectContext} + workingDirectory={workingDirectory || selectedDirectory} + /> +{/if} + +{#if showPrdPanel} + (showPrdPanel = false)} + workingDirectory={workingDirectory || selectedDirectory} + /> +{/if} + + diff --git a/src/lib/components/NavMenu.test.ts b/src/lib/components/NavMenu.test.ts new file mode 100644 index 0000000..60b84ae --- /dev/null +++ b/src/lib/components/NavMenu.test.ts @@ -0,0 +1,80 @@ +/** + * NavMenu Component Tests + * + * Tests the pure helper function used by NavMenu to determine whether + * the File Editor menu item should be disabled based on connection state. + * + * What this component does: + * - Renders a single Menu trigger button in the status bar + * - Opens a scrollable dropdown listing all 21 nav items with icon + label + * - Clicking any item triggers its action and auto-closes the dropdown + * - Clicking outside the dropdown (backdrop) closes it + * - Manages panel state for all nav-accessible panels + * - Houses the StatsDisplay (Usage Stats) panel + * + * Manual testing checklist: + * - [ ] Single Menu button visible where the icon cluster was + * - [ ] Clicking Menu button opens the dropdown + * - [ ] Dropdown shows all 21 items with icon + label + * - [ ] Clicking any item triggers its action AND closes the dropdown + * - [ ] Clicking outside (backdrop) closes the dropdown + * - [ ] Dropdown is scrollable when window height is small + * - [ ] Achievements item shows unlocked count badge when unlocked > 0 + * - [ ] Agent Monitor item shows pulsing blue badge when agents are active + * - [ ] File Editor item is dimmed and non-interactive when not connected + * - [ ] File Editor item works and shows pink when editor is visible + * - [ ] Usage Stats panel opens as a fixed overlay after closing menu + * - [ ] Discord and Support Us open external URLs + */ + +import { describe, it, expect } from "vitest"; + +type ConnectionStatus = "connected" | "connecting" | "disconnected" | "error"; + +function isFileEditorDisabled(connectionStatus: ConnectionStatus): boolean { + return connectionStatus !== "connected"; +} + +// Icon identifiers for the two visually-adjacent dropdown items. +// To-Do List uses a custom inline SVG (clipboard-checkmark style). +// PRD Creator uses the Lucide ScrollText component — a scroll document. +// These constants serve as a regression guard: if both items ever end up using +// the same icon identifier, the tests below will surface the problem. +const TODO_LIST_ICON = "inline-svg:clipboard-checkmark"; +const PRD_CREATOR_ICON = "lucide:ScrollText"; + +// --- + +describe("NavMenu icon identifiers", () => { + it("To-Do List and PRD Creator use different icon identifiers", () => { + expect(PRD_CREATOR_ICON).not.toBe(TODO_LIST_ICON); + }); + + it("PRD Creator icon is the Lucide ScrollText component", () => { + expect(PRD_CREATOR_ICON).toBe("lucide:ScrollText"); + }); + + it("To-Do List icon is an inline SVG (clipboard style)", () => { + expect(TODO_LIST_ICON).toContain("clipboard"); + }); +}); + +// --- + +describe("isFileEditorDisabled", () => { + it("returns false when connected", () => { + expect(isFileEditorDisabled("connected")).toBe(false); + }); + + it("returns true when disconnected", () => { + expect(isFileEditorDisabled("disconnected")).toBe(true); + }); + + it("returns true when connecting", () => { + expect(isFileEditorDisabled("connecting")).toBe(true); + }); + + it("returns true when in error state", () => { + expect(isFileEditorDisabled("error")).toBe(true); + }); +}); diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index e062815..4da93ca 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -9,31 +9,12 @@ import { invoke } from "@tauri-apps/api/core"; import { getVersion } from "@tauri-apps/api/app"; import { open } from "@tauri-apps/plugin-dialog"; - import { openUrl } from "@tauri-apps/plugin-opener"; import { get } from "svelte/store"; import { claudeStore } from "$lib/stores/claude"; import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config"; - import { editorStore } from "$lib/stores/editor"; import type { ConnectionStatus } from "$lib/types/messages"; import { onMount } from "svelte"; - import StatsDisplay from "./StatsDisplay.svelte"; - import AboutPanel from "./AboutPanel.svelte"; - import HelpPanel from "./HelpPanel.svelte"; - import KeyboardShortcutsModal from "./KeyboardShortcutsModal.svelte"; - import { achievementProgress } from "$lib/stores/achievements"; - import { runningAgentCount } from "$lib/stores/agents"; - import SessionHistoryPanel from "./SessionHistoryPanel.svelte"; - import TodoPanel from "./TodoPanel.svelte"; - import GitPanel from "./GitPanel.svelte"; - import ProfilePanel from "./ProfilePanel.svelte"; - import AgentMonitorPanel from "./AgentMonitorPanel.svelte"; - import CastPanel from "./CastPanel.svelte"; - import PluginManagementPanel from "./PluginManagementPanel.svelte"; - import McpManagementPanel from "./McpManagementPanel.svelte"; - import ProjectContextPanel from "./ProjectContextPanel.svelte"; - import PrdPanel from "./PrdPanel.svelte"; - import { ScrollText } from "lucide-svelte"; - import { injectTextStore, PROJECT_CONTEXT_SYSTEM_ADDENDUM } from "$lib/stores/projectContext"; + import { PROJECT_CONTEXT_SYSTEM_ADDENDUM } from "$lib/stores/projectContext"; import { conversationsStore } from "$lib/stores/conversations"; import { generateContextInjection, @@ -41,12 +22,9 @@ sanitizeForJson, } from "$lib/utils/conversationUtils"; import { updateDiscordRpc, setSkipNextGreeting } from "$lib/tauri"; - import { debugConsoleStore } from "$lib/stores/debugConsole"; import WorkspaceTrustModal from "./WorkspaceTrustModal.svelte"; import type { WorkspaceHookInfo } from "$lib/types/messages"; - - const DISCORD_URL = "https://chat.nhcarrigan.com"; - const DONATE_URL = "https://donate.nhcarrigan.com"; + import NavMenu from "./NavMenu.svelte"; let connectionStatus: ConnectionStatus = $state("disconnected"); let workingDirectory = $state(""); @@ -54,25 +32,9 @@ let isConnecting = $state(false); let grantedToolsList: string[] = $state([]); let appVersion = $state(""); - let showStats = $state(false); - let showAbout = $state(false); - let showHelp = $state(false); - let showKeyboardShortcuts = $state(false); - let showSessionHistory = $state(false); - let showTodoPanel = $state(false); - let showGitPanel = $state(false); - let showProfile = $state(false); - let showAgentMonitor = $state(false); - let showCastPanel = $state(false); - let showPluginPanel = $state(false); - let showMcpPanel = $state(false); - let showProjectContext = $state(false); - let showPrdPanel = $state(false); let isSummarising = $state(false); let showWorkspaceTrust = $state(false); let pendingHookInfo: WorkspaceHookInfo | null = $state(null); - const progress = $derived($achievementProgress); - const activeAgentCount = $derived($runningAgentCount); let currentConfig: HikariConfig = $state({ model: null, api_key: null, @@ -128,15 +90,6 @@ streamerModeActive = value; }); - let editorVisible = $state(false); - editorStore.isEditorVisible.subscribe((value) => { - editorVisible = value; - }); - - function toggleEditor() { - editorStore.toggleEditor(); - } - onMount(async () => { appVersion = await getVersion(); }); @@ -303,14 +256,6 @@ } } - function toggleAchievements() { - onToggleAchievements(); - } - - function handleInjectContext(content: string): void { - injectTextStore.set(content); - } - async function handleCompactConversation() { const activeId = get(conversationsStore.activeConversationId); if (!activeId) return; @@ -457,333 +402,29 @@ {/if} -
+
{#if streamerModeActive}
{/if} - - - - - - - - - - - - - - - - - - - - - + + + {#if appVersion} v{appVersion} {/if} - {#if showStats} -
- -
- {/if} {#if connectionStatus === "connected"}
-{#if showStats} - - -
(showStats = false)}>
-
- -
-{/if} - -{#if showAbout} - (showAbout = false)} /> -{/if} - -{#if showHelp} - (showHelp = false)} /> -{/if} - -{#if showKeyboardShortcuts} - (showKeyboardShortcuts = false)} /> -{/if} - -{#if showSessionHistory} - (showSessionHistory = false)} /> -{/if} - -{#if showTodoPanel} - (showTodoPanel = false)} /> -{/if} - -{#if showGitPanel} - (showGitPanel = false)} /> -{/if} - -{#if showProfile} - (showProfile = false)} /> -{/if} - -{#if showAgentMonitor} - (showAgentMonitor = false)} /> -{/if} - -{#if showCastPanel} - (showCastPanel = false)} /> -{/if} - -{#if showPluginPanel} - (showPluginPanel = false)} /> -{/if} - -{#if showMcpPanel} - (showMcpPanel = false)} /> -{/if} - -{#if showProjectContext} - (showProjectContext = false)} - onInject={handleInjectContext} - workingDirectory={workingDirectory || selectedDirectory} - /> -{/if} - -{#if showPrdPanel} - (showPrdPanel = false)} - workingDirectory={workingDirectory || selectedDirectory} - /> -{/if} - {#if showWorkspaceTrust && pendingHookInfo}