diff --git a/.gitignore b/.gitignore index 6a66025..072b98d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ vite.config.ts.timestamp-* # Coverage reports /coverage + +# PRD task files (user-generated data, not source code) +hikari-tasks.json diff --git a/CODEBASE.md b/CODEBASE.md new file mode 100644 index 0000000..2b3afdd --- /dev/null +++ b/CODEBASE.md @@ -0,0 +1,458 @@ +# Hikari Desktop — Codebase Map + +> Auto-generated codebase overview. Last updated: 2026-03-06. + +## Overview + +Hikari Desktop is a **Tauri v2** desktop application that wraps the Claude Code CLI with a visual anime character avatar (Hikari) who appears on-screen and reacts in real-time to Claude's activity. When Claude is thinking, she thinks. When it's editing code, she codes. When it's using MCP tools, she glows with magical energy. + +The app supports multiple simultaneous conversations (tabs), each with its own isolated Claude CLI process. It provides a rich UI layer on top of Claude Code, including a built-in file editor, git panel, achievement system, cost tracking, session history, notifications, and more. + +**Repositories:** + +- Primary: `git.nhcarrigan.com` (Gitea) — `nhcarrigan/hikari-desktop` +- Mirror: `github.com/naomi-lgbt/hikari-desktop` + +**Current version:** `1.10.0` + +--- + +## Architecture + +The application follows a standard Tauri architecture: + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Frontend (WebView) │ +│ SvelteKit + Svelte 5 + TailwindCSS 4 + TypeScript │ +│ │ +│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │AnimeGirl│ │ Terminal │ │ InputBar │ │ Editor │ │ +│ │ Sprites │ │ View │ │ + Slash Cmds│ │CodeMirror│ │ +│ └────┬────┘ └────┬─────┘ └──────┬───────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ ┌────▼─────────────▼───────────────▼────────────────▼──────┐ │ +│ │ Svelte Stores (reactive state) │ │ +│ │ conversations · character · config · agents · stats … │ │ +│ └──────────────────────────┬───────────────────────────────┘ │ +│ │ tauri.ts (event listeners) │ +└─────────────────────────────┼────────────────────────────────┘ + │ Tauri IPC (invoke / emit) +┌─────────────────────────────┼────────────────────────────────┐ +│ Backend (Rust) │ +│ ┌──────────────────────────▼───────────────────────────────┐ │ +│ │ commands.rs (invoke handlers) │ │ +│ └──────────────────────────┬───────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────▼───────────────────────────────┐ │ +│ │ BridgeManager — HashMap │ │ +│ └──────────────────────────┬───────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────▼───────────────────────────────┐ │ +│ │ WslBridge — spawns `claude --output-format stream-json`│ │ +│ │ reads NDJSON stdout → emits events to frontend │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ config · stats · cost_tracking · sessions · git · clipboard │ +│ achievements · discord_rpc · notifications · snippets … │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## Directory Structure + +``` +hikari-desktop/ +├── src/ # SvelteKit frontend +│ ├── routes/ +│ │ ├── +page.svelte # Main app layout (root page) +│ │ ├── +layout.svelte # App-level layout wrapper +│ │ ├── +layout.ts # SvelteKit layout config (SSR disabled) +│ │ └── test-achievement/ # Dev-only achievement test page +│ ├── lib/ +│ │ ├── tauri.ts # Tauri event listeners + IPC bridge +│ │ ├── commands/ # Slash command definitions +│ │ ├── components/ # 60+ Svelte components +│ │ │ └── editor/ # CodeMirror-based file editor components +│ │ ├── notifications/ # Notification system +│ │ ├── sounds/ # Sound effect triggers +│ │ ├── stores/ # All Svelte reactive stores +│ │ ├── types/ # TypeScript type definitions +│ │ └── utils/ # Pure utility functions +│ ├── app.css # Global styles + CSS variables (themes) +│ └── app.html # HTML shell +│ +├── src-tauri/ # Tauri Rust backend +│ ├── src/ +│ │ ├── main.rs # Process entry point +│ │ ├── lib.rs # Tauri app setup + command registration +│ │ ├── types.rs # All shared Rust types + serialisation +│ │ ├── wsl_bridge.rs # Claude CLI process management + NDJSON parser +│ │ ├── bridge_manager.rs # Per-conversation WslBridge registry +│ │ ├── commands.rs # All #[tauri::command] handlers +│ │ ├── config.rs # Config read/write (tauri-plugin-store) +│ │ ├── stats.rs # Token usage + cost calculation +│ │ ├── cost_tracking.rs # Budget alerts + cost history (CSV export) +│ │ ├── achievements.rs # Achievement unlock logic +│ │ ├── sessions.rs # Conversation session persistence (JSON) +│ │ ├── git.rs # Git operations via CLI +│ │ ├── clipboard.rs # Clipboard history management +│ │ ├── notifications.rs # System notification dispatch +│ │ ├── discord_rpc.rs # Discord Rich Presence manager +│ │ ├── drafts.rs # Draft message persistence +│ │ ├── snippets.rs # Snippet library CRUD +│ │ ├── quick_actions.rs # Quick action CRUD +│ │ ├── debug_logger.rs # TauriLogLayer (routes tracing → frontend) +│ │ ├── temp_manager.rs # Temporary file lifecycle management +│ │ ├── tool_cache.rs # Tool call result caching +│ │ ├── tray.rs # System tray setup +│ │ ├── process_ext.rs # HideWindow trait (Windows console hiding) +│ │ ├── vbs_notification.rs # VBScript-based notification fallback (Windows) +│ │ ├── windows_toast.rs # Windows native toast notifications +│ │ └── wsl_notifications.rs# WSL notify-send bridge +│ ├── capabilities/ # Tauri permission capabilities +│ ├── tests/ # Rust integration tests +│ ├── Cargo.toml +│ ├── Cargo.lock +│ └── tauri.conf.json # Tauri app configuration +│ +├── static/ +│ ├── sprites/ # Anime character PNG sprites (one per state) +│ └── sounds/ # MP3 sound effects (connected, working, done…) +│ +├── check-all.sh # Full QA script (lint → format → types → test) +├── vitest.config.ts # Frontend test configuration +├── vitest.setup.ts # Tauri API mocks for tests +├── svelte.config.js # SvelteKit config (static adapter) +├── vite.config.js # Vite config +├── eslint.config.js # ESLint 9 flat config +├── tsconfig.json # TypeScript config +└── .gitea/workflows/ # CI/CD (Gitea Actions) +``` + +--- + +## Key Components + +### Backend (Rust) + +#### `wsl_bridge.rs` — Claude CLI Process Manager + +The most critical backend file. `WslBridge` spawns a single `claude` CLI process per conversation using `--output-format stream-json`, which causes Claude Code to emit NDJSON messages on stdout. A dedicated reader thread consumes stdout line-by-line, parses each line into a `ClaudeMessage` enum variant, and emits the appropriate frontend events. + +Key responsibilities: + +- Locates the `claude` binary (checks `~/.local/bin`, `~/.claude/local`, system paths, and falls back to a login-shell `which claude`) +- Detects WSL environment to handle cross-platform path differences +- Maps tool names to character states (Read/Glob/Grep → `searching`, Edit/Write → `coding`, `mcp__*` → `mcp`) +- Batches permission requests from a single assistant message +- Tracks token usage per session + +#### `bridge_manager.rs` — Multi-Conversation Orchestrator + +`BridgeManager` holds a `HashMap` keyed by `conversation_id`. This enables true parallel conversations — each tab has its own isolated Claude process. The manager is wrapped in `Arc>` (using `parking_lot`) and injected into Tauri's managed state. + +#### `types.rs` — Shared Type Definitions + +Defines the complete Claude stream-JSON protocol as Rust enums/structs: + +- `ClaudeMessage` — top-level message variants: `System`, `Assistant`, `User`, `StreamEvent`, `Result`, `RateLimitEvent` +- `ContentBlock` — `Text`, `Thinking`, `ToolUse`, `ToolResult` +- `CharacterState` — `Idle | Thinking | Typing | Searching | Coding | Mcp | Permission | Success | Error` +- All frontend event types (`OutputEvent`, `StateChangeEvent`, `PermissionPromptEvent`, `AgentStartEvent`, etc.) + +#### `commands.rs` — IPC Command Handlers + +Registers all Tauri commands exposed to the frontend. Over 80 commands covering: Claude process management, configuration, stats, sessions, git, clipboard, cost tracking, MCP servers, plugins, drafts, snippets, quick actions, file system operations, authentication, and notifications. + +#### `debug_logger.rs` — In-App Debug Console + +A custom `tracing` subscriber layer (`TauriLogLayer`) that captures all `tracing::info!/warn!/error!` calls and emits them as `debug:log` events to the frontend debug console — essential since production Windows builds have no stdout. + +--- + +### Frontend (TypeScript/Svelte 5) + +#### `src/routes/+page.svelte` — Root Layout + +The main page. Renders a two-panel layout: + +- **Left panel**: `` character display with state-reactive glow effects (trans pride gradient colours per state) +- **Right panel**: `` + `` (or `` when the editor is open) + +Also handles: global keyboard shortcuts, compact mode (280×400 mini widget), window close confirmation, Discord RPC updates, and background image loading. + +#### `src/lib/tauri.ts` — Event Bridge + +Sets up all Tauri event listeners on app mount. Translates backend events into store mutations: + +| Event | Action | +| ------------------------ | ----------------------------------------------------------------------- | +| `claude:connection` | Updates conversation connection status; sends greeting on first connect | +| `claude:state` | Updates character state; triggers per-conversation sound effects | +| `claude:output` | Appends lines to the correct conversation's terminal history | +| `claude:session` | Stores the Claude session ID | +| `claude:cwd` | Updates working directory (used by the editor) | +| `claude:permission` | Adds permission requests to conversation state | +| `claude:agent-start/end` | Updates agent monitor panel | +| `claude:question` | Stores pending user question | + +Also manages Discord RPC updates and the session greeting flow. + +#### `src/lib/stores/conversations.ts` — Core State Store + +The central state container. Each conversation (`Conversation` interface) tracks: + +- Terminal lines (`TerminalLine[]`) +- Connection status, session ID, working directory +- Character state, processing flag +- Granted/pending tool permissions +- Pending user questions +- Scroll position, attachments, draft text +- Sound tracking (per-conversation, prevents replays on tab switch) +- Conversation summary (for compaction) + +Tab names are randomly chosen from a curated list of whimsical names (Starfall, Moonbeam, Sakura, etc.). + +#### `src/lib/stores/claude.ts` — Backwards-Compat Facade + +A thin wrapper that re-exports `conversationsStore` methods under the original `claudeStore` API. Maintains backwards compatibility whilst the codebase migrated to multi-conversation support. + +#### `src/lib/stores/character.ts` — Character State Store + +Manages the global character state displayed by ``. Supports `setState()` (persistent) and `setTemporaryState(state, durationMs)` (auto-reverts to `idle` after a timeout — used for success/error flashes). + +#### `src/lib/utils/stateMapper.ts` — Stream → State Mapping + +Pure utility that maps Claude stream-JSON message types to `CharacterState` values. Tool categorisation mirrors the Rust side: search tools → `searching`, coding tools → `coding`, MCP tools → `mcp`, Task tool → `thinking`. + +#### `src/lib/components/` + +Key components beyond the basics: + +| Component | Purpose | +| --------------------------- | ------------------------------------------------------------- | +| `AnimeGirl.svelte` | Displays the character sprite, subscribes to `characterState` | +| `Terminal.svelte` | Renders the conversation message history | +| `InputBar.svelte` | User input with slash command menu, attachment support | +| `StatusBar.svelte` | Top bar: connection indicator, token/cost stats, controls | +| `ConversationTabs.svelte` | Multi-tab navigation with per-tab status indicators | +| `ConfigSidebar.svelte` | Settings panel (model, theme, notifications, budget, etc.) | +| `PermissionModal.svelte` | Handles tool permission grant/deny UI | +| `UserQuestionModal.svelte` | Renders `AskUserQuestion` prompts from Claude | +| `AgentMonitorPanel.svelte` | Live subagent tree with status badges | +| `GitPanel.svelte` | Git status, diff, stage/unstage, commit, push/pull | +| `editor/EditorPanel.svelte` | Full CodeMirror editor with file browser and tabs | +| `DiffViewer.svelte` | Syntax-highlighted diff display | +| `AchievementsPanel.svelte` | Achievement gallery | +| `CostSummary.svelte` | Cost breakdown by session/day/week/month | +| `MemoryBrowserPanel.svelte` | Browse Claude memory files | +| `McpManagementPanel.svelte` | MCP server configuration UI | +| `DebugConsole.svelte` | In-app log viewer (receives `debug:log` events) | +| `ThinkingBlock.svelte` | Collapsible extended thinking display | +| `ToolCallBlock.svelte` | Formatted tool use/result display | + +--- + +## Data Flow + +### User Sends a Message + +``` +User types → InputBar + → invoke("send_prompt", { conversationId, message }) + → BridgeManager.send_prompt(conversation_id, message) + → WslBridge.send_message() → writes JSON to Claude CLI stdin +``` + +### Claude Responds (NDJSON Stream) + +``` +Claude CLI stdout (NDJSON) + → WslBridge reader thread (line-by-line) + → serde_json::from_str::() + → match message type: + System(init) → emit claude:connection(connected) + claude:cwd + StreamEvent → emit claude:state(thinking|typing|searching|coding|mcp) + Assistant → emit claude:output(assistant|tool|thinking lines) + User(tool_result)→ emit claude:output(tool result lines) + Result(success) → emit claude:state(success) + claude:output(result) + Result(error) → emit claude:state(error) + RateLimitEvent → emit claude:output(rate-limit line) + PermissionRequest→ emit claude:permission +``` + +### Frontend Reacts + +``` +tauri.ts event listeners + → conversationsStore mutations + → Svelte reactivity propagates to components + → AnimeGirl.svelte: sprite changes to match characterState + → Terminal.svelte: new line appended + → StatusBar.svelte: token counts update + → ConversationTabs.svelte: tab glow colour updates +``` + +### Permission Flow + +``` +Claude requests tool permission + → WslBridge batches pending tool uses + → emit claude:permission (one or more requests) + → tauri.ts → claudeStore.requestPermissionForConversation() + → PermissionModal.svelte renders + → User clicks Allow/Deny + → invoke("answer_question", { conversationId, toolUseId, granted }) + → WslBridge.send_tool_result() → writes result to Claude stdin + → Claude CLI resumes +``` + +--- + +## State Machine + +The `CharacterState` enum drives both the sprite displayed and the panel glow colour: + +| State | Trigger | Sprite | Panel Glow | +| ------------ | --------------------------------- | ----------------------- | ---------------------- | +| `idle` | Connected, no activity | Standing with clipboard | None | +| `thinking` | Thinking block / Task tool | Hand on chin | Purple/trans gradient | +| `typing` | Text content block | At keyboard | Blue/trans gradient | +| `searching` | Read/Glob/Grep/WebSearch/WebFetch | Magnifying glass | Yellow/trans gradient | +| `coding` | Edit/Write/NotebookEdit | At monitor | Green/trans gradient | +| `mcp` | Any `mcp__*` tool | Magical blue energy | Trans pride vibrant | +| `permission` | Permission requested | Confused shrug | — | +| `success` | Result: success | Celebrating | Emerald/trans gradient | +| `error` | Result: error | Worried | Red/trans gradient | + +`success` and `error` are temporary states (3-second auto-revert to `idle`). + +--- + +## Dependencies + +### Frontend (key packages) + +| Package | Purpose | +| ------------------------------ | -------------------------------------------------------------- | +| `@sveltejs/kit` `svelte` | SvelteKit framework + Svelte 5 | +| `@tauri-apps/api` | Core Tauri IPC (`invoke`, `listen`) | +| `@tauri-apps/plugin-*` | FS, clipboard, notifications, dialog, shell, store, os, opener | +| `tailwindcss` v4 | Utility-first CSS | +| `codemirror` + `@codemirror/*` | Code editor with 20+ language modes | +| `marked` | Markdown → HTML rendering | +| `highlight.js` | Syntax highlighting in markdown blocks | +| `lucide-svelte` | Icon library | + +### Backend (key crates) + +| Crate | Purpose | +| -------------------------------- | ---------------------------------------- | +| `tauri` v2 | Desktop app framework | +| `tokio` | Async runtime | +| `serde` / `serde_json` | JSON serialisation/deserialisation | +| `parking_lot` | Fast mutex (used for `BridgeManager`) | +| `uuid` | Unique ID generation | +| `discord-rich-presence` | Discord RPC integration | +| `chrono` | Date/time handling for cost tracking | +| `semver` | Version comparison for update checks | +| `tempfile` | Temporary file management | +| `tracing` + `tracing-subscriber` | Structured logging | +| `dirs` | Cross-platform home directory resolution | +| `windows` (Windows-only) | Native toast notifications | + +### Dev / Tooling + +| Tool | Purpose | +| -------------------------------- | ----------------------------------------- | +| `vitest` + `@vitest/coverage-v8` | Frontend unit tests with v8 coverage | +| `@testing-library/svelte` | Component testing utilities | +| `jsdom` | DOM environment for tests | +| `eslint` v9 (flat config) | Linting | +| `prettier` | Formatting | +| `svelte-check` | TypeScript type checking for Svelte files | +| `cargo test` + `cargo llvm-cov` | Rust unit tests and coverage | + +--- + +## Development Notes + +### Running the App + +```bash +# Frontend dev server only +source ~/.nvm/nvm.sh && pnpm dev + +# Full Tauri app (Rust + frontend) +source ~/.nvm/nvm.sh && pnpm tauri dev +``` + +### Running Tests + +```bash +# All checks (lint → format → type-check → frontend tests → backend tests) +./check-all.sh + +# Frontend tests only +source ~/.nvm/nvm.sh && pnpm test + +# Frontend with coverage +source ~/.nvm/nvm.sh && pnpm test:coverage + +# Backend tests only +pnpm test:backend +``` + +### Building + +```bash +# Linux build +pnpm build:linux + +# Windows cross-compile (requires cargo-xwin) +pnpm build:windows +``` + +### Adding a New Tauri Command + +1. Add the handler function in the appropriate `src-tauri/src/*.rs` file with `#[tauri::command]` +2. Register it in `lib.rs` `invoke_handler![]` +3. Call it from the frontend via `invoke("command_name", { args })` in `src/lib/tauri.ts` or a store + +### Adding a New Frontend Store + +1. Create `src/lib/stores/my-store.ts` using `writable` or a factory function pattern +2. Create `src/lib/stores/my-store.test.ts` — all stores must have tests +3. Expose the store from the appropriate component + +### Claude Stream-JSON Protocol + +Claude Code is invoked with `--output-format stream-json --verbose`. See `src-tauri/src/types.rs` for the complete message type definitions. The key field distinguishing subagent messages from top-level messages is `parent_tool_use_id` on `Assistant` messages. + +### Multi-Conversation Architecture + +Each tab (`Conversation`) in `conversationsStore` has a unique `conversation_id` string. The backend `BridgeManager` maps these IDs to `WslBridge` instances. All Tauri events carry `conversation_id` in their payload so the frontend can route them to the correct conversation without affecting others. + +### WSL Detection + +`wsl_bridge.rs` detects WSL by checking `/proc/version` for "microsoft"/"wsl" strings, checking for `/proc/sys/fs/binfmt_misc/WSLInterop`, and checking `$WSL_DISTRO_NAME`. On native Windows builds, WSL detection always returns `false` (even if launched from a WSL terminal). + +### Character State Sound Rules + +Sound effects are managed in `src/lib/tauri.ts` per-conversation to prevent replays when switching tabs. The rules are: + +- Entering `thinking` from a clean state (`idle`/`success`/`error`) → reset all sound flags +- Entering `coding` or `searching` (first time per task) → play task-start sound +- Entering `success` after ≥2 seconds in a long-running phase → play completion sound +- Entering `error` → play error sound (always) +- Entering `permission` → play permission sound (always) + +### Workspace Trust Gate + +On first connection to a new working directory, the app checks for Claude hooks and prompts the user to trust the workspace. Trusted workspaces are persisted in `HikariConfig.trusted_workspaces`. + +### Configuration Storage + +All settings are persisted via `tauri-plugin-store` to a JSON file in the app data directory. The frontend `configStore` (`src/lib/stores/config.ts`) loads configuration on startup and provides reactive derived stores. Changes invoke `save_config` to persist to disk. diff --git a/src/lib/components/PrdPanel.svelte b/src/lib/components/PrdPanel.svelte new file mode 100644 index 0000000..274b5c4 --- /dev/null +++ b/src/lib/components/PrdPanel.svelte @@ -0,0 +1,364 @@ + + +
e.key === "Escape" && onClose()} +> +
e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + role="dialog" + aria-labelledby="prd-panel-title" + tabindex="-1" + > + +
+
+

+ PRD Creator +

+ {#if $isGenerating} + Generating tasks... + {:else if $isLoading} + Loading... + {:else if $isLoaded} + + {$tasks.length} task{$tasks.length === 1 ? "" : "s"} + + {/if} +
+ +
+ + +
+ {#if $isGenerating} + +
+
⚙️
+

Generating Tasks...

+

+ Claude is breaking down your goal into actionable tasks and writing + hikari-tasks.json. This will auto-reload when + complete. +

+
+ {:else if $isLoaded} + +
+
+ Goal: + {$goal} +
+ {#each $tasks as task, index (task.id)} +
+ +
+ #{index + 1} + + handleUpdateTask(task.id, "title", (e.target as HTMLInputElement).value)} + class="flex-1 bg-transparent text-sm font-medium text-[var(--text-primary)] border-b border-transparent hover:border-[var(--border-color)] focus:border-[var(--accent-primary)] focus:outline-none transition-colors py-0.5" + placeholder="Task title" + /> + +
+ + + +
+
+ + +
+ {/each} + {#if $tasks.length === 0} +

+ No tasks — all removed. Click Regenerate to start over. +

+ {/if} +
+ {:else} + +
+
+ + +
+

+ Claude will analyse your goal and write a + hikari-tasks.json file with 3–10 actionable tasks, each with + a detailed prompt ready to execute. +

+
+ {/if} +
+ + +
+
+ {workingDirectory}/hikari-tasks.json +
+
+ {#if $isLoaded} + + + + {:else if !$isGenerating} + + {/if} +
+
+
+
+ + diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index 6e55b24..e062815 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -31,6 +31,8 @@ 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 { conversationsStore } from "$lib/stores/conversations"; import { @@ -65,6 +67,7 @@ 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); @@ -587,6 +590,13 @@ /> +