This file provides guidance to AI coding agents working with code in this repository.
Helmor is a local-first desktop app built with Tauri v2 (Rust backend) + React 19 + Vite + TypeScript. It provides a workspace management UI with its own SQLite database (~/helmor/ in release, ~/helmor-dev/ in debug), letting users browse workspaces/sessions/messages and send prompts to AI agents (Claude Code CLI, OpenAI Codex CLI) via streaming IPC.
bun install # Install deps (bun 1.3+). Also runs `bun install` in sidecar/ via postinstall.
bun run dev # Full desktop app: Tauri + Vite (localhost:1420 in webview)
bun run dev:analyze # Same as dev, with perf HUD (VITE_HELMOR_PERF_HUD=1)
bun run build # tsc + vite build (frontend bundle to dist/)
bun run typecheck # tsc --noEmit for frontend AND sidecar
bun run lint # biome check . + cargo clippy -- -D warnings
bun run lint:fix # biome --write + cargo clippy --fix + cargo fmtTests are three targets — bun run test runs all three (frontend -> sidecar -> rust). Pre-commit hook runs biome on JS/TS and clippy/fmt on Rust.
bun run test # All three suites
bun run test:frontend # vitest run (jsdom, @testing-library/react)
bun run test:sidecar # cd sidecar && bun test
bun run test:rust # cd src-tauri && cargo test
bun run test:rust:update-snapshots # INSTA_UPDATE=always
bun run test:watch # vitest watch (frontend only)Single test file: bun x vitest run src/App.test.tsx | cd sidecar && bun test src/foo.test.ts | cd src-tauri && cargo test --test pipeline_scenarios -- <name>
- Frontend (
src/): React 19 SPA in Tauri webview. State management handled by specialized hooks (useAppShellState,useSelectionController,useEditorEditMode,useGlobalShortcutHandlers,useAppBootstrap) undersrc/shell/. TanStack React Query + context providers. - Rust backend (
src-tauri/src/): Tauri host, SQLite database, spawns and supervises the sidecar. - Sidecar (
sidecar/): Bun + TypeScript, wraps@anthropic-ai/claude-agent-sdkand@openai/codex(CLI). Built tosidecar/dist/helmor-sidecarviabun build --compile. JSON event stream over stdout.
Message flow: user prompt -> Rust agents::streaming -> sidecar -> SDK -> stdout events -> Rust accumulator -> adapter + collapse -> ThreadMessageLike[] -> tauri::ipc::Channel -> React.
Feature-based layout. Each feature folder follows: index.tsx (main) + container.tsx (data/state) + hooks/ + tests.
| Path | Role |
|---|---|
App.tsx |
Minimal root (~18 lines). Delegates to AppProviders and AppShell. |
features/panel/ |
Chat thread container, header, message components, thread viewport. |
features/conversation/ |
Conversation renderer + use-streaming hook. |
features/composer/ |
Lexical-based message input. Plugins in editor/plugins/. |
features/editor/ |
Monaco file editor surface. |
features/inspector/ |
Right-side inspector (actions, changes sections). |
features/navigation/ |
Sidebar workspace groups. |
features/commit/ |
Commit button + lifecycle hook. |
features/settings/ |
Settings dialog + panels (CLI install, repo settings). |
shell/ |
Top-level layout, GitHub identity gate, panel resize hooks. State management now delegated to focused hooks in shell/hooks/ (e.g. use-app-shell-state.tsx, use-selection-controllers.ts) — App.tsx refactored from 1976 to 18 lines to improve re-render isolation. All files < 300 lines. |
components/ai/ |
AI-specific components (code block, file tree, reasoning). |
components/ui/ |
shadcn/ui primitives (base-nova). |
lib/api.ts |
IPC bridge -- every Tauri invoke() call wrapped as a typed function. |
lib/query-client.ts |
React Query keys + query options factories. |
lib/settings.ts |
App settings context with Tauri storage. |
| Module | Role |
|---|---|
lib.rs |
Tauri app builder. Registers commands, runs setup hook. |
commands/ |
Tauri command handlers split by domain (session, repository, workspace, editor, github, settings, system). |
agents/ |
Agent streaming + persistence (catalog, persistence, queries, streaming, support). |
pipeline/ |
Message pipeline: accumulator/ -> adapter/ + collapse -> ThreadMessageLike[]. Includes event_filter.rs, classify.rs, types.rs. |
workspace/ |
Workspace operations (branching, lifecycle, helpers) + files/ sub-module (editor, changes, types). |
git/ |
Git operations (ops, watcher). |
github/ |
GitHub integration (auth, CLI, GraphQL). |
models/ |
Persistence layer (db, repos, sessions, settings, workspaces). |
service.rs |
Service layer. |
sidecar.rs |
Sidecar process manager (spawn, stdio, graceful SIGTERM). |
schema.rs |
DB schema + idempotent migrations. |
mcp.rs |
MCP bridge integration. |
logging.rs |
Structured logging setup. |
data_dir.rs |
Data dir resolution. HELMOR_DATA_DIR env override. |
error.rs |
CommandError -- bridges anyhow::Error to Tauri IPC. |
index.ts (entry, stdin/stdout JSON) | session-manager.ts (base lifecycle) | claude-session-manager.ts | codex-session-manager.ts | codex-skill-scanner.ts | request-parser.ts | emitter.ts | abort.ts | images.ts | title.ts | logger.ts
Live streaming sidecar events --> accumulator --> adapter + collapse --> ThreadMessageLike[]
Historical reload DB rows --> convert_historical ----^
Both paths converge at IntermediateMessage[] and share adapter + collapse.
Storage shape: session_messages.content is JSON. Top-level type discriminates: user_prompt, user, assistant, system, error, result, item.completed (Codex), turn.completed. DB stores post-accumulator form. Claude SDK delivers delta-style blocks; accumulator APPENDs them.
🚨 Any change touching pipeline/, agents/ persistence, schema.rs, or the storage shape MUST have snapshot test coverage in src-tauri/tests/.
Three insta-based targets sharing tests/common/mod.rs:
pipeline_scenarios.rs-- Handcrafted edge cases (70+ tests). Normalized snapshots.pipeline_fixtures.rs-- Real DB sessions intests/fixtures/pipeline/, auto-discovered viainsta::glob!.pipeline_streams.rs-- Raw SDK stream-event JSONL intests/fixtures/streams/. Three-stage round-trip.
cd src-tauri && cargo test --tests # All integration tests
cd src-tauri && INSTA_UPDATE=always cargo test --tests # Accept new snapshots
cd src-tauri && cargo insta review # Interactive accept/reject
cd src-tauri && cargo run --example gen_pipeline_fixture -- <session_id> <name> # Capture real fixtureWhen a snapshot drifts: look at the diff first. Only accept after confirming the new shape is intended, not a regression.
- Path alias:
@/maps tosrc/ - Styling: Tailwind CSS v4 with oklch semantic color tokens (
bg-app-base,text-app-foreground, etc.) - UI: shadcn/ui (base-nova),
lucide-reacticons. No@assistant-ui/reactorreact-virtuoso-- removed, do not re-introduce. - Cursor: Every clickable element MUST have
cursor-pointer. This is already baked into base UI components (Button,SidebarMenuButton,CommandItem,DropdownMenuItem,ContextMenuItem, etc.). When adding custom clickable elements (e.g.<div onClick>), always includecursor-pointer. - Chat rendering:
streamdown+use-stick-to-bottom. Markdown overrides insrc/components/streamdown-components.tsx. - Rich text input: Lexical in
src/features/composer/editor/. - File editor: Monaco, lazy via
src/lib/monaco-runtime.ts. - Linting: Biome (tab indent).
lint-stagedenforces on pre-commit. - Testing: Vitest + jsdom (frontend),
bun test(sidecar), cargo test + insta (Rust). Tests co-located with source. - Changesets: A
.changeset/*.mdbody uses the smallest shape that fits — a single prose sentence (default for simple patch-level changes) or a prose summary line followed by-sub-items (only when ≥2 distinct user-visible changes are worth enumerating). Never start the body with-. See thehelmor-releaseskill for full format and rationale. - Data dir:
~/helmor/(release) or~/helmor-dev/(debug). Override:HELMOR_DATA_DIR. - macOS chrome: Overlay title bar, traffic lights at (16, 24). Drag via
data-tauri-drag-region. - Serde:
#[serde(rename_all = "camelCase")]-- JSON fields match TypeScript directly. - Backend → frontend notifications: Always go through
UiMutationEvent(src-tauri/src/ui_sync/events.rs). Add a typed variant, broadcast withcrate::ui_sync::publish(&app, ...), mirror the variant inUiMutationEventinsrc/lib/api.ts, and handle it insrc/shell/hooks/use-ui-sync-bridge.tsto invalidate the right React Query keys. Do NOT add ad-hocapp.emit("custom-event", ...)channels with their own component-levellisten(...)-- they fragment cache invalidation, skip the global bridge, and are easy to leak. - Clippy: Must pass
cargo clippy --all-targets -- -D warningswith zero warnings. - Perf:
VITE_HELMOR_PERF_HUD=1enables HUD + react-scan + long-frame tracker. - Logging: Dev defaults to
debug. Override:HELMOR_LOG=info|debug|error. JSONL logs in{data_dir}/logs/. - Bundled forge CLIs (
gh,glab): Pinned + SHA256-verified insidecar/scripts/stage-vendor.ts. To upgrade:- Bump
GH_VERSION/GLAB_VERSION. - Pull the new SHA256 from
…/checksums.txt(URLs in the file's header comment) and updateGH_SHA256/GLAB_SHA256. - Re-run
bun run buildinsidecar/— the changed SHA256 auto-forces a re-download + verify (no manual wipe). Downloaded archives now live in a shared, cross-worktree cache (the main worktree'ssidecar/.bundle-cache, overrideHELMOR_BUNDLE_CACHE); wipe that only if you want a forced clean fetch. Bump cadence: every release cycle if upstream has shipped a notable fix; immediately on security advisories. Pin so the auth-status JSON shape Helmor parses doesn't drift unexpectedly.
- Bump
- Bundled agent CLIs (
claude-code,codex): Pulled in viasidecar/package.jsonand staged intosidecar/dist/vendor/{claude-code,codex}/as platform-native binaries. Both upstreams ship per-platform npm sub-packages (@anthropic-ai/claude-code-darwin-{arm64,x64},@openai/codex-darwin-{arm64,x64}). Cross-arch CI staging downloads the tarball straight from the npm registry and verifies againstCLAUDE_CODE_SHA256/CODEX_SHA256instage-vendor.ts. To upgrade:- Bump the version in
sidecar/package.json,cd sidecar && bun install. - Compute the SHA256 of both arch tarballs (
shasum -a 256on the cached.tgz) and update the table instage-vendor.ts(key it under the new version string). - Run
bun run buildinsidecar/to verify — a changed SHA256 auto-forces a re-download (shared cache at the main worktree'ssidecar/.bundle-cache, overrideHELMOR_BUNDLE_CACHE; no manual wipe needed). Both binaries arebun build --compileoutput (~200 MB each on macOS), somaybeSignMacBinary(_, true)is required — JSC needsallow-jit/allow-unsigned-executable-memoryunder hardened runtime. Run pipeline snapshot tests after every claude-code bump (cd src-tauri && cargo test --tests); the SDK event shape is the contract Helmor's accumulator depends on.
- Bump the version in
Never let a single file grow into a monolith. This codebase just went through a painful refactoring precisely because too much logic was crammed into too few files. Follow these rules strictly:
- One responsibility per file. If a file handles two unrelated concerns, split it.
- Use module directories. When a module grows beyond ~300 lines, convert
foo.rstofoo/mod.rs+ sub-files, or splitfoo.tsxinto afoo/folder withindex.tsx+ focused sub-modules. Theagents/,pipeline/,workspace/,commands/directories are the reference pattern. - Frontend: feature folders. New features go into
src/features/<name>/withindex.tsx, optionalcontainer.tsx,hooks/, and tests. Shared components go intosrc/components/. Do NOT put feature-specific logic insrc/lib/orApp.tsx. - Backend: commands vs. domain logic. Tauri
#[command]handlers go incommands/. Business logic and domain operations go in their own modules (workspace/,agents/,git/, etc.). Do not mix IPC glue with domain logic. - When in doubt, split. It is always easier to merge two small files than to untangle a 1000-line monolith.
Hard rule: Use the Tauri MCP bridge (
tauri-plugin-mcp-bridge) only. Nochrome-devtoolsMCP, no/agent-browser. Helmor runs in Tauri webview only.
When the user asks to use Tauri MCP (including misspellings like "towery MCP"), debug the local dev build, simulate user actions, switch workspaces/sessions, type or send composer text, inspect the webview, take screenshots, trace IPC, or run visual end-to-end checks against Helmor, first use the project skill helmor-debug-operate (.agents/skills/helmor-debug-operate/SKILL.md). Treat it as the operation manual for controlling the local desktop build. Use helmor-cli instead only for terminal-first data/workspace automation where the UI is not under test.
When the user asks for an autonomous local-dev bug loop, repeated reproduction, temporary logging/instrumentation, self-directed debugging, or "fix and verify through the app", first use helmor-debug-loop (.agents/skills/helmor-debug-loop/SKILL.md). That loop skill must use helmor-debug-operate for all Tauri MCP app operations and must explicitly handle not reproduced, flaky, and blocked outcomes rather than pretending a reproduction succeeded.
The Helmor local-dev debug skill is a prompt, not a source of truth. It can be stale if the local UI changed without the skill being updated. If a skill recipe fails three times, stop repeating it mechanically: take fresh screenshots/snapshots, reason from the visible UI, and inspect the relevant source code if needed. If you discover a better path, missing pitfall, stale selector, or unverified workaround, record a candidate update under .agent-contexts/<task-slug>/skill-update-candidates.md with evidence and verification status, then ask the user for confirmation before editing the skill unless the current user request explicitly asks you to update it.
- Debug build only. MCP bridge is behind
#[cfg(debug_assertions)]. Alwaysbun run dev. - Open driver session first. Call
driver_session action=statusbeforestart. Default port9223, windowmain. - Sanity-check. Call
ipc_get_backend_stateafter connecting to confirm the right instance.
- UI state:
webview_screenshot->webview_dom_snapshot type=accessibility(prefer ref IDs for follow-ups) - User input:
webview_interact+webview_keyboard. Never dispatch synthetic events viawebview_execute_js. - IPC tracing:
ipc_monitor start-> trigger flow ->ipc_get_captured filter=<cmd>->ipc_monitor stop. Always stop when done. - Direct backend call: do not use Tauri MCP
ipc_execute_commandfor Helmor app commands; current bridge dynamic command execution returnsUnsupported Tauri command. Usehelmor-debug-operate's click-triggered Call App Commands helper, or drive the UI. - Async waits:
webview_wait_for type=ipc-event value=<event>for streaming/pipeline events. - Console/system logs:
read_logs source=consoleorsource=system filter=helmor. - JS eval:
webview_execute_js script="(() => <expr>)()"(IIFE, JSON-serializable return). Cannot see React state. - Styles:
webview_get_styles selector=... properties=[...]. - Element picker:
webview_select_elementorwebview_get_pointed_element(Alt+Shift+Click). - Window geometry:
manage_window action=list|info|resize.
- Release builds have no MCP bridge.
webview_screenshot= visible viewport only. Scroll first if needed.ipc_monitoris sticky -- stop it explicitly.- Tauri MCP does not see sidecar HTTP/WS traffic. Check JSONL logs in
{data_dir}/logs/. - Ref IDs are per-snapshot. Re-snapshot after UI state changes.