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}
{
- it("Todo 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("Todo List icon is an inline SVG (clipboard style)", () => {
- expect(TODO_LIST_ICON).toContain("clipboard");
- });
-});
-
// ---
describe("getStatusColor", () => {