From 78dc838f36218c279166556f91ba2fb82229790b Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 12:09:30 -0800 Subject: [PATCH] feat: project context panel for persistent PROJECT/REQUIREMENTS/ROADMAP/STATE files (#188) - Add projectContext store with load/save via existing Tauri file commands - Add ProjectContextPanel modal with tabbed editor, file-exists badges, and templates - Add injectTextStore signal so StatusBar can inject content directly into InputBar - Add PROJECT_CONTEXT_SYSTEM_ADDENDUM auto-appended to custom_instructions on connect - Add open-book button to StatusBar to open the panel - Add PROJECT.md example file for the hikari-desktop project itself - 34 tests covering all store methods, exports, and signal behaviour --- PROJECT.md | 45 +++ src/lib/components/InputBar.svelte | 9 + src/lib/components/ProjectContextPanel.svelte | 225 ++++++++++++++ src/lib/components/StatusBar.svelte | 35 ++- src/lib/stores/projectContext.test.ts | 287 ++++++++++++++++++ src/lib/stores/projectContext.ts | 155 ++++++++++ 6 files changed, 754 insertions(+), 2 deletions(-) create mode 100644 PROJECT.md create mode 100644 src/lib/components/ProjectContextPanel.svelte create mode 100644 src/lib/stores/projectContext.test.ts create mode 100644 src/lib/stores/projectContext.ts diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000..9a97193 --- /dev/null +++ b/PROJECT.md @@ -0,0 +1,45 @@ +# Project Overview + +## What is this project? + +Hikari Desktop is a Tauri-based desktop application that wraps Claude Code with a visual anime character companion (Hikari) who appears on screen. It provides a rich UI for interacting with Claude Code, including conversation management, agent monitoring, cost tracking, and more. + +The app was inspired by a Hatsune Miku mod for the ship AI in _The Outer Worlds_ — the idea of an AI assistant with an anime girl avatar that you can actually _see_. + +## Goals + +- Provide a beautiful, personalised interface for Claude Code +- Surface real-time status (thinking, typing, searching, etc.) through animated character sprites +- Track costs, context usage, and agent activity across sessions +- Support power-user workflows: multi-tab conversations, todo lists, git integration, MCP server management, session compaction, and more +- Build a foundation for autonomous task execution (agent orchestration, PRD-driven workflows) + +## Tech Stack + +- **Frontend**: Svelte 5 + TypeScript + Tailwind CSS +- **Backend**: Rust (Tauri v2) +- **Build**: Vite + pnpm +- **Testing**: Vitest (frontend) + cargo test (backend) +- **Linting**: ESLint + Prettier (frontend) + Clippy (backend) +- **IPC**: Tauri commands + events between Rust and Svelte + +## Architecture + +``` +hikari-desktop/ +├── src/ # Svelte frontend +│ └── lib/ +│ ├── components/ # UI components (panels, modals, status bar) +│ ├── stores/ # Svelte stores (state management) +│ ├── types/ # TypeScript type definitions +│ └── utils/ # Utility functions +├── src-tauri/ # Rust backend +│ └── src/ +│ ├── commands.rs # Tauri command handlers +│ ├── wsl_bridge.rs # Claude Code process management +│ ├── types.rs # Shared types & CharacterState enum +│ └── stats.rs # Cost tracking +└── public/ # Static assets (sprites, sounds) +``` + +Claude Code is launched as a child process via `WslBridge`, communicating via `--output-format stream-json` (NDJSON). Messages flow from the Rust backend to the Svelte frontend via Tauri events. diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index aa63b6a..024aedc 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -37,6 +37,7 @@ import DraftPanel from "$lib/components/DraftPanel.svelte"; import TextInputContextMenu from "$lib/components/TextInputContextMenu.svelte"; import { draftsStore } from "$lib/stores/drafts"; + import { injectTextStore } from "$lib/stores/projectContext"; import type { Attachment } from "$lib/types/messages"; const INPUT_HISTORY_KEY = "hikari-input-history"; @@ -178,6 +179,14 @@ } }); + // Project context injection — set by StatusBar via injectTextStore signal. + injectTextStore.subscribe((text) => { + if (text === null) return; + inputValue = inputValue.trim() ? text + "\n\n" + inputValue : text; + userHasTyped = true; + injectTextStore.set(null); + }); + function clearInput() { inputValue = ""; const activeId = get(claudeStore.activeConversationId); diff --git a/src/lib/components/ProjectContextPanel.svelte b/src/lib/components/ProjectContextPanel.svelte new file mode 100644 index 0000000..1a807ee --- /dev/null +++ b/src/lib/components/ProjectContextPanel.svelte @@ -0,0 +1,225 @@ + + +
e.key === "Escape" && onClose()} +> +
e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + role="dialog" + aria-labelledby="project-context-title" + tabindex="-1" + > + +
+
+

+ Project Context +

+ {#if $isLoading[$activeFile]} + Loading... + {:else if fileExists($activeFile)} + + ✓ File exists + + {:else} + + ✗ Not created + + {/if} + {#if hasUnsavedChanges} + Unsaved changes + {/if} +
+ +
+ + +
+ {#each PROJECT_FILES as file (file)} + + {/each} +
+ + +
+ +
+ + +
+
+ {workingDirectory}/{PROJECT_FILE_NAMES[$activeFile]} +
+
+ + + +
+
+
+
+ + diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index 0c59b7f..6e55b24 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -30,6 +30,8 @@ import CastPanel from "./CastPanel.svelte"; import PluginManagementPanel from "./PluginManagementPanel.svelte"; import McpManagementPanel from "./McpManagementPanel.svelte"; + import ProjectContextPanel from "./ProjectContextPanel.svelte"; + import { injectTextStore, PROJECT_CONTEXT_SYSTEM_ADDENDUM } from "$lib/stores/projectContext"; import { conversationsStore } from "$lib/stores/conversations"; import { generateContextInjection, @@ -62,6 +64,7 @@ let showCastPanel = $state(false); let showPluginPanel = $state(false); let showMcpPanel = $state(false); + let showProjectContext = $state(false); let isSummarising = $state(false); let showWorkspaceTrust = $state(false); let pendingHookInfo: WorkspaceHookInfo | null = $state(null); @@ -185,7 +188,8 @@ working_dir: targetDir, model: currentConfig.model || null, api_key: currentConfig.api_key || null, - custom_instructions: currentConfig.custom_instructions || null, + custom_instructions: + (currentConfig.custom_instructions ?? "") + PROJECT_CONTEXT_SYSTEM_ADDENDUM, mcp_servers_json: currentConfig.mcp_servers_json || null, allowed_tools: allAllowedTools, use_worktree: currentConfig.use_worktree ?? false, @@ -300,6 +304,10 @@ onToggleAchievements(); } + function handleInjectContext(content: string): void { + injectTextStore.set(content); + } + async function handleCompactConversation() { const activeId = get(conversationsStore.activeConversationId); if (!activeId) return; @@ -345,7 +353,8 @@ working_dir: workingDirectory || selectedDirectory, model: currentConfig.model || null, api_key: currentConfig.api_key || null, - custom_instructions: currentConfig.custom_instructions || null, + custom_instructions: + (currentConfig.custom_instructions ?? "") + PROJECT_CONTEXT_SYSTEM_ADDENDUM, mcp_servers_json: currentConfig.mcp_servers_json || null, allowed_tools: allAllowedTools, use_worktree: currentConfig.use_worktree ?? false, @@ -564,6 +573,20 @@ /> +