Compare commits

...

4 Commits

Author SHA1 Message Date
Test
7bcbf87067 docs: compress CLAUDE.md 360→93 lines — remove verbose explanations, keep rules
Removed: TDD rationale paragraphs, Phase 1 QA bullet lists (Claude infers from context),
verbose Vault Retrocompatibility pattern, design file node commands, menu bar structure
explanation, Push Workflow verbose anti-PR rationale.

Kept: all concrete rules, checklist commands, thresholds, scripts, gotchas.
2026-03-13 19:56:49 +01:00
Test
a23264eacb docs: convert remaining ASCII diagrams to Mermaid + add Mermaid rule to CLAUDE.md
ARCHITECTURE.md:
- System Overview → flowchart (React Frontend / Rust Backend / External Services)
- MCP Server Architecture → flowchart (index.js, vault.js, ws-bridge, transports)
- WebSocket Bridge → flowchart LR (Frontend ↔ ws-bridge ↔ vault)
- Vault Cache Three Strategies → flowchart (full scan / incremental / cache hit)
- Auto-Save Flow → flowchart LR
- Git Sync Flow → flowchart TD (auto-sync + manual commit paths)

CLAUDE.md:
- Added 'Documentation Diagrams' section: Mermaid preferred for all new diagrams,
  convert ASCII on sight, exception for spatial wireframe layouts
2026-03-13 19:22:36 +01:00
Test
18b65f1e59 docs: add Mermaid diagrams to ARCHITECTURE and ABSTRACTIONS
- Three Representations flowchart (Filesystem → Cache → React state)
- Startup Sequence diagram (Tauri → App → VaultLoader → Editor)
- AI Agent Event Flow sequence diagram (NDJSON stream + MCP tool calls)
- Search & Indexing flowchart (full vs incremental, three search modes)
- Markdown-to-BlockNote pipeline flowchart (load path)
- BlockNote-to-Markdown pipeline flowchart (save path)
- VaultEntry class diagram (with TypeDocument + Frontmatter relationships)
2026-03-13 19:12:20 +01:00
Test
52d68aa506 ci: add .codesceneignore — exclude tools/, e2e/, tests/, scripts/
tools/qmd/node_modules was being analyzed by CodeScene causing
artificially low average code health (worst performer at 2.41
was third-party npm code, not our code).

Also excluding e2e/, tests/, scripts/ which are support code
and should not influence production code health metrics.
2026-03-13 08:48:31 +01:00
4 changed files with 318 additions and 432 deletions

5
.codesceneignore Normal file
View File

@@ -0,0 +1,5 @@
# Exclude third-party tools and their dependencies from CodeScene analysis
tools/
e2e/
tests/
scripts/

325
CLAUDE.md
View File

@@ -1,276 +1,84 @@
# CLAUDE.md — Laputa App
## ⛔ BEFORE EVERY COMMIT — Non-negotiable checklist
Run all of these. If any fails, fix before committing. No exceptions.
## ⛔ BEFORE EVERY COMMIT
```bash
pnpm lint && npx tsc --noEmit # lint + types
pnpm test # unit tests
pnpm test:coverage # frontend ≥70% coverage
cargo test # Rust tests
pnpm lint && npx tsc --noEmit
pnpm test
pnpm test:coverage # frontend ≥70%
cargo test
cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
pre_commit_code_health_safeguard # CodeScene ≥9.2 — if it fails, fix structurally (see below)
pre_commit_code_health_safeguard # CodeScene ≥9.2 hotspot + ≥8.8 average
```
**CI is a safety net, not a discovery tool.** If CI catches something you didn't catch locally, that's a process failure. All these tools are available locally — use them while you code, not just at the end.
If `pre_commit_code_health_safeguard` fails: extract hooks, split components, reduce complexity. Never add `// eslint-disable`, `#[allow(...)]`, or `as any` to pass the gate.
## ⛔ BEFORE FIRING laputa-task-done — Two-phase QA (mandatory)
## ⛔ BEFORE FIRING laputa-task-done — Two-phase QA
### Phase 1: Playwright browser QA (headless, you do this yourself)
### Phase 1: Playwright (you do this)
Test every acceptance criterion using Playwright against the dev server **before** marking done. This catches 80% of bugs before Brian sees them.
Write a test in `tests/smoke/<slug>.spec.ts` that covers every acceptance criterion. The test must fail before your fix and pass after. Run it:
```bash
# 1. Start the dev server (use your worktree port)
pnpm dev --port <N> &
DEV_PID=$!
sleep 3 # wait for vite to be ready
# 2. Run Playwright smoke test for this task
sleep 3
BASE_URL="http://localhost:<N>" npx playwright test tests/smoke/<slug>.spec.ts
# 3. Or run all smoke tests
BASE_URL="http://localhost:<N>" pnpm playwright:smoke
kill $DEV_PID
```
**You must write a new Playwright test for this task** in `tests/smoke/<slug>.spec.ts` that covers every acceptance criterion. Do not rely only on existing smoke tests — they test the app in general, not your specific feature.
**If your task touches filesystem, git, AI, MCP, or any native Tauri command**: also test with `pnpm tauri dev` against `~/Laputa` (not demo vault). Use `osascript` keyboard events — no mouse, no `cliclick`.
**What to cover in your Playwright test:**
- Every acceptance criterion from the task spec → one `test()` block per criterion
- Every command palette entry → open `Cmd+K`, type the command name, verify it appears and executes
- Every keyboard shortcut → send keydown events, verify UI state changes
- Every UI element described in the spec → verify it renders, is focusable, responds to Tab
- Edge cases: empty state, long text, rapid keypresses
- **The happy path end-to-end**: simulate exactly what a user would do to use this feature
### Phase 2: Native QA (Brian does this after push)
**The test must fail before your fix and pass after.** If you can't write a test that demonstrates the bug is fixed, your test doesn't cover the right thing.
**Playwright is non-negotiable even if unit tests pass.** Unit tests verify code; Playwright verifies the user experience in the real browser. Both are required.
> **⚠️ Browser dev server limits**: the dev server uses mock Tauri handlers (`src/mock-tauri.ts`) — file system operations, git commands, and native dialogs are mocked. **If your task touches the filesystem, AI context pipeline, MCP server, git integration, or any Tauri command that reads/writes real files, Playwright alone is not enough.** You must also do Phase 1b.
### Phase 1b: Tauri dev QA (you do this for filesystem/native tasks)
If your task touches **any of the following**, you must also test with `pnpm tauri dev` against the real vault before firing done:
- File read/write (notes, cache, vault config)
- AI chat context (what the AI actually receives as input)
- MCP server / subprocess communication
- Git integration (commit, push, history, diff)
- Native dialogs or OS-level features
Brian installs the release build and runs keyboard-only QA. Phase 1 must pass first or the task goes to To Rework.
Fire done signal only after Phase 1 passes:
```bash
# Start Tauri dev app from your worktree
pnpm tauri dev --port <N> &
sleep 10 # wait for Tauri + Vite to boot
# Then test using osascript keyboard events (NO mouse/cliclick)
# Example:
osascript -e 'tell application "laputa" to activate'
osascript -e 'tell application "System Events" to keystroke "k" using command down'
# ...simulate the full user flow from the acceptance criteria
```
**What to verify in Phase 1b:**
- Open the feature on a real note in `~/Laputa` (not the demo vault)
- Walk through every acceptance criterion step by step using keyboard only
- Verify file changes with `git -C ~/Laputa diff` if the task writes files
- Verify AI responses actually contain note content (not empty) if the task touches AI context
**⚠️ Claude Code runs headless — you cannot see the screen.** Use `screencapture /tmp/qa-check.png` and then read/describe what you see if you need visual verification. Or rely on DOM state checks via osascript accessibility API.
### Phase 2: Native Tauri QA (Brian does this after you push)
Brian installs the release build and runs keyboard-only QA on the native app. You don't do Phase 2 — but Phase 1 must pass before you fire the done signal, or Brian's QA will fail and the task goes back to To Rework.
1. Acquire lockfile: `echo $$ > /tmp/laputa-qa.lock && trap "rm -f /tmp/laputa-qa.lock" EXIT`
2. Kill other instances: `pkill -x laputa 2>/dev/null || true; sleep 1`
3. Start app: `pnpm tauri dev` from worktree
4. Switch vault to `~/Laputa` (not demo)
5. Test the feature/fix with real mouse clicks (`cliclick`) on real notes
6. If task touches file save: verify `git -C ~/Laputa diff` shows changes
7. If QA fails → fix and re-run. Do NOT fire the signal until it passes.
**⚠️ QA ≠ tests. QA means using the app as a user.**
- "Tests pass" is NOT QA. Tests verify code, QA verifies the user experience.
- The QA comment must describe what you did as a user: "Opened app → Cmd+K → typed 'Trash' → pressed Enter → note disappeared from list → restarted app → note still not visible"
- Every QA comment must include: the exact keyboard/command palette steps used, what was visible before and after, and any edge case tested.
- If you cannot test a feature using keyboard only (osascript shortcuts + command palette), the feature is not keyboard-first → QA fails.
**⚠️ Phase 1 is YOUR quality gate, not a formality.**
Brian's Phase 2 QA is a *reinforcement* check, not the primary gate. If Brian finds a bug in Phase 2 that you could have caught in Phase 1, that is a Phase 1 failure — not a Phase 2 discovery. Before firing the done signal, ask yourself: "Did I actually verify, in Playwright, that the feature works end-to-end exactly as the spec describes?" If the answer is "I ran the smoke tests and they passed", that is not enough. You must run your task-specific test and verify the acceptance criteria one by one.
**⚠️ Test in a clean environment when the feature depends on state.**
If a feature involves indexing, fresh installs, first-time setup, or anything that only runs once:
- **Do not test in the existing dev vault** — it already has the state you're trying to test.
- **Create a new empty vault** for the test: Cmd+K → "New Vault" (or equivalent), pick a temp folder like `/tmp/test-vault-<slug>`, then test the full first-time flow from scratch.
- This applies to: search indexing, vault init, getting-started setup, any "on first open" logic.
- If you can't reproduce the fresh-install scenario locally, the feature is untestable → do not fire done.
Fire done signal only after QA passes:
```bash
rm -f /tmp/laputa-qa.lock
openclaw system event --text "laputa-task-done:<task_id>:<slug>" --mode now
```
## ⛔ CODE HEALTH — No shortcuts
If `pre_commit_code_health_safeguard` flags a file:
- **Understand why** — use `code_health_review` via CodeScene MCP
- Fix the structural problem (extract hooks, split components, reduce complexity)
- **Never** add a JSDoc comment, `#[allow(...)]`, `// eslint-disable`, or `as any` just to pass the gate
- It's fine to take longer. False quality is worse than no quality.
---
## Project
Tauri v2 + React + TypeScript desktop app. Reads a vault of markdown files with YAML frontmatter.
- **Spec**: `docs/PROJECT-SPEC.md`
- **Architecture**: `docs/ARCHITECTURE.md`
- **Abstractions**: `docs/ABSTRACTIONS.md`
- **Wireframes**: `ui-design.pen`
- **Luca's vault**: `~/Laputa/` (~9200 markdown files)
## Tech Stack
- Desktop: Tauri v2 (Rust backend)
- Frontend: React 18 + TypeScript + BlockNote editor
- Tests: Vitest (unit), Playwright (E2E), `cargo test` (Rust)
- Package manager: pnpm
## Architecture
- `src-tauri/src/` — Rust backend (file I/O, git, frontmatter parsing)
- `src/` — React frontend
- `src/mock-tauri.ts` — Mock layer for browser/test env (silently swallows Tauri calls — **not a substitute for native app testing**)
- `src/types.ts` — Shared TypeScript types
- **Spec**: `docs/PROJECT-SPEC.md` | **Architecture**: `docs/ARCHITECTURE.md` | **Abstractions**: `docs/ABSTRACTIONS.md`
- **Wireframes**: `ui-design.pen` | **Luca's vault**: `~/Laputa/` (~9200 markdown files)
- Stack: Rust backend, React + BlockNote editor, Vitest + Playwright + cargo test, pnpm
## How to Work
- **Never develop on `main`** — always on `task/<slug>` branch
- **Commit every 2030 min** — atomic commits, one concern per commit (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`)
- **Update docs/** when changing architecture, abstractions, or significant design (mandatory — see rule below)
- **Push directly to main** — no PRs ever. The pre-push hook runs all checks.
- **⛔ NEVER open a PR** — branches diverge and cause rebase churn.
- **⛔ NEVER use --no-verify**
- Commit every 2030 min: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`
## ⛔ DOCS — Keep docs/ in sync with code (mandatory)
## TDD (mandatory)
After any significant feature change, update the relevant `docs/` files **in the same commit**:
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write a failing regression test first, then fix. Exception: pure CSS/layout with no logic.
- **`docs/ARCHITECTURE.md`** — stack, system overview, component structure, Tauri commands, data flow, backend modules
- **`docs/ABSTRACTIONS.md`** — domain models, VaultEntry fields, entity types, key abstractions, integration patterns
- **`docs/GETTING-STARTED.md`** — directory structure, key files, common tasks, test commands, onboarding
## ⛔ Docs — Keep docs/ in sync
**What counts as "significant":**
- Adding a new Tauri command or backend module
- Adding a new major component, hook, or feature (not a bugfix)
- Changing the data model (VaultEntry fields, new types, new config files)
- Adding a new integration (API, service, transport)
- Changing the architecture (new panels, new state management, new build steps)
After adding a Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit. Use Mermaid for diagrams (not ASCII). Exception: spatial wireframe layouts.
**How to update:**
1. Read the relevant doc section before making changes
2. After your code changes, update the doc to reflect the new state
3. Commit doc changes together with the code — not in a separate follow-up commit
## Design File (UI tasks)
If unsure whether a change is "significant", err on the side of updating. Stale docs are worse than slightly verbose docs.
1. Open `ui-design.pen` first — study existing frames for visual language.
2. Design in light mode. Create `design/<slug>.pen` for the task.
3. On merge to main: merge frames into `ui-design.pen`, delete `design/<slug>.pen`.
## TDD — Red/Green/Refactor (mandatory)
## Vault Retrocompatibility
**Always use test-driven development.** No production code without a failing test first.
Every feature that depends on vault files must auto-bootstrap: check if file/folder exists on vault open, create with defaults if missing (silent, idempotent). Register with the central `Cmd+K → "Repair Vault"` command.
The loop:
1. **Red** — write a failing test that describes the behavior you want. Run it, confirm it fails for the right reason.
2. **Green** — write the minimum code to make the test pass. No more, no less.
3. **Refactor** — clean up the code (extract, rename, simplify) while keeping tests green.
4. **Commit** — one red/green/refactor cycle = one atomic commit.
5. Repeat.
## Keyboard-First + Menu Bar (mandatory)
**Why this matters:**
- Forces you to think about behavior before implementation
- Produces only code that's actually needed (no speculative abstractions)
- Tests written first are always behavioral and structure-insensitive by construction
- Tiny cycles = fast feedback, smaller diffs, easier to review
**For bug fixes:**
1. Write a failing test that reproduces the bug (this is the regression test)
2. Fix the bug until the test passes
3. Commit both together: `fix: [bug] — regression test added`
**For Rust:**
```bash
cargo watch -x test # run tests on every save
```
**For frontend:**
```bash
pnpm test --watch # run tests on every save
```
**When to deviate:** Pure UI layout/styling work with no logic is the only exception. Everything else — hooks, utilities, Rust commands, state management — must be TDD.
## Testing (quality bar)
- Unit tests must cover real business logic, not "component renders"
- Tests test **behavior** (what the code does), not **structure** (how it does it)
- Every bug fixed → regression test that would have caught it
- Every new feature → TDD from the start (see above)
- `pnpm test:coverage` and `cargo llvm-cov` must pass before committing
## Design File (every UI task)
Every task with UI changes needs a design file. Follow this process:
1. **Open `ui-design.pen` first** — study existing frames to understand the visual language, spacing, and component style before designing anything new.
2. **Design in light mode** — all existing designs use light mode. New frames must match. Never use dark mode for designs.
3. **Create `design/<slug>.pen`** for the new feature — additive only, NOT a copy of ui-design.pen.
4. **When merging to main** — merge your frames into `ui-design.pen` with proper layout:
- Place frames in a logical area (group by feature area, not stacked on top of each other)
- Leave at least 100px spacing between frames
- **Delete `design/<slug>.pen`** after merging — the frames now live in `ui-design.pen`
```bash
mkdir -p design
# Study schema first:
node -e "const f=JSON.parse(require('fs').readFileSync('ui-design.pen','utf8')); console.log(JSON.stringify(f.children[0],null,2))"
# Start fresh:
echo '{"children":[],"variables":{}}' > design/<slug>.pen
```
## Vault File Retrocompatibility (mandatory for every feature that adds vault files)
Laputa vaults are long-lived. New app versions must work on existing vaults that were created before a feature existed.
**Rule: never assume a vault file exists. Always auto-create if missing.**
Every feature that depends on a vault file or folder must:
1. **Auto-bootstrap on vault open** — check if the required file/folder exists; if not, create it with defaults. This must be silent and non-blocking.
2. **Be idempotent** — creating defaults must be safe to run multiple times (never overwrite user data).
3. **Expose a repair command** — add a `Cmd+K` command like "Restore Default Themes" or "Repair Vault Config" that explicitly re-creates missing files. Users can run this if something is broken.
**General "Repair Vault" command** — when adding a new vault file dependency, register it with the central repair system so that `Cmd+K → "Repair Vault"` fixes everything in one shot.
**Pattern:**
```
on vault open:
if file X does not exist → create X with defaults ← silent auto-repair
if file X exists but is malformed → log warning, use defaults (don't crash)
on "Repair Vault" command:
for each known vault file/folder:
if missing → create with defaults
if present → leave untouched (idempotent)
```
This principle applies to: themes, config files, type files, any `.laputa/` subfolder, or any file Laputa expects to find in a vault.
Every feature must be reachable via keyboard. Every new command palette entry must also appear in the macOS menu bar (File / Edit / View / Note / Vault / Window). This is a QA requirement.
## macOS / Tauri Gotchas
- `Option+N` on macOS → special chars (`¡`, `™`), not `key:'N'`. Use `e.code` or `Cmd+N`.
- Tauri menu accelerators: use `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")` — decorative text in labels doesn't register shortcuts.
- `Option+N` → special chars on macOS. Use `e.code` or `Cmd+N`.
- Tauri menu accelerators: `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")`.
- `app.set_menu()` replaces the ENTIRE menu bar — include all submenus.
- `mock-tauri.ts` silently swallows Tauri calls — not a substitute for native app testing.
## QA Scripts
@@ -278,63 +86,8 @@ This principle applies to: themes, config files, type files, any `.laputa/` subf
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/laputa-qa/scripts/shortcut.sh "command" "s"
bash ~/.openclaw/skills/laputa-qa/scripts/click.sh 400 300 # logical coords
```
## Menu Bar Discoverability (mandatory for every new command)
## Documentation Diagrams
The command palette is powerful but not discoverable — users must already know a command exists to find it. The macOS menu bar is where users discover what an app can do.
**Rule: every significant command palette entry must also appear in the menu bar.**
When adding a new command to the palette:
1. **Identify the right menu bar group** — File, Edit, View, Note, Vault, or create a new group if needed
2. **Add a menu item** with the same label as the palette command
3. **Show the keyboard shortcut** next to the menu item (if one exists)
4. **If no direct shortcut exists**, still add the menu item — it's discoverable and triggers the same action
The menu bar should be organized around what Laputa does:
- **File** — new note, open vault, switch vault, close
- **Edit** — undo, redo, find, note actions (rename, trash, duplicate)
- **View** — view modes, zoom, sidebar, panels
- **Note** — note-specific actions (move to trash, archive, properties)
- **Vault** — vault management (themes, config, repair, sync)
- **Window / Help** — standard macOS items
**This is a QA requirement:** before marking any task done, verify that every new command palette entry has a corresponding menu bar item.
## Keyboard-First Principle (mandatory for every new feature)
Every feature must be reachable via keyboard. This is both a UX requirement and a QA requirement — Brian tests the native app using keyboard only (osascript key events, no mouse).
**Before marking any task done:**
- Can the feature be triggered/used without touching the mouse?
- If it requires clicking a button, add a command palette entry or keyboard shortcut
- Document the shortcut in the command palette or menu bar
**If you add UI that is only reachable by mouse**, you must also add a keyboard path (command palette entry, shortcut, or Tab-navigable focus). No exceptions.
## Push Workflow (IMPORTANT — changed Feb 27, 2026)
**Push directly to main** — no PRs, no branches, no CI queue.
The pre-push hook runs all checks locally before the push goes through. This replaces remote CI.
```bash
# After QA passes and you're ready to ship:
git push origin main # pre-push hook runs automatically
```
### ⛔ NEVER open a Pull Request
PRs on separate branches diverge from main with every merge, requiring continuous rebases and creating unnecessary conflicts. Always push directly to main. If the push fails (disk full, test failure, etc.) — fix the problem, then push again. There is no scenario where opening a PR is the right fallback.
### ⛔ NEVER use --no-verify
```bash
# FORBIDDEN — will be caught and rejected:
git push --no-verify
git commit --no-verify # also forbidden for pre-push bypass
```
The hook runs: tsc, Vite build, frontend tests, frontend coverage, Rust coverage, Clippy, rustfmt, CodeScene. Fix any failures before pushing — do not skip.
If a check fails, fix the issue and push again. The hook is the gate — not remote CI.
Prefer Mermaid for all diagrams (`flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`). ASCII only for spatial wireframe layouts. GitHub renders Mermaid natively.

View File

@@ -32,7 +32,56 @@ All data lives in markdown files with YAML frontmatter. There is no database —
### VaultEntry
The core data type representing a single note, defined in Rust (`src-tauri/src/vault/mod.rs`) and TypeScript (`src/types.ts`):
The core data type representing a single note, defined in Rust (`src-tauri/src/vault/mod.rs`) and TypeScript (`src/types.ts`).
```mermaid
classDiagram
class VaultEntry {
+String path
+String filename
+String title
+String? isA
+String[] aliases
+String[] belongsTo
+String[] relatedTo
+Record~string,string[]~ relationships
+String[] outgoingLinks
+String? status
+String? owner
+Number? modifiedAt
+Number? createdAt
+Number wordCount
+String? snippet
+Boolean archived
+Boolean trashed
+Number? trashedAt
+Record~string,string~ properties
}
class TypeDocument {
+String icon
+String color
+Number order
+String sidebarLabel
+String template
+String sort
+Boolean visible
}
class Frontmatter {
+String type
+String status
+String url
+String[] belongsTo
+String[] relatedTo
+String[] aliases
...custom fields
}
VaultEntry --> Frontmatter : parsed from
VaultEntry --> TypeDocument : isA resolves to
VaultEntry "many" --> "1" TypeDocument : grouped by type
```
```typescript
// src/types.ts
@@ -315,25 +364,31 @@ const WikiLink = createReactInlineContentSpec(
### Markdown-to-BlockNote Pipeline
```
Raw markdown
splitFrontmatter() [yaml, body]
preProcessWikilinks(body) → replaces [[target]] with Unicode placeholder tokens
→ editor.tryParseMarkdownToBlocks() → BlockNote block tree
injectWikilinks(blocks) → walks tree, replaces placeholders with wikilink inline content nodes
editor.replaceBlocks()
```mermaid
flowchart LR
A["📄 Raw markdown\n(from disk)"] --> B["splitFrontmatter()\n→ yaml + body"]
B --> C["preProcessWikilinks(body)\n[[target]] → token"]
C --> D["tryParseMarkdownToBlocks()\n→ BlockNote block tree"]
D --> E["injectWikilinks(blocks)\ntoken → WikiLink node"]
E --> F["editor.replaceBlocks()\n→ rendered editor"]
style A fill:#f8f9fa,stroke:#6c757d,color:#000
style F fill:#d4edda,stroke:#28a745,color:#000
```
Placeholder tokens use `\u2039` and `\u203A` to avoid colliding with markdown syntax.
> Placeholder tokens use `\u2039` and `\u203A` to avoid colliding with markdown syntax.
### BlockNote-to-Markdown Pipeline (Save)
```
BlockNote blocks
→ editor.blocksToMarkdownLossy()
postProcessWikilinks() → restore [[target]] syntax from wikilink nodes
prepend frontmatter yaml
invoke('save_note_content', { path, content })
```mermaid
flowchart LR
A["✏️ BlockNote blocks\n(editor state)"] --> B["blocksToMarkdownLossy()"]
B --> C["postProcessWikilinks()\nWikiLink node → [[target]]"]
C --> D["prepend frontmatter yaml"]
D --> E["invoke('save_note_content')\n→ disk write"]
style A fill:#cce5ff,stroke:#004085,color:#000
style E fill:#d4edda,stroke:#28a745,color:#000
```
### Wikilink Navigation

View File

@@ -31,6 +31,22 @@ Vault data exists in three forms simultaneously:
These must never diverge permanently. If they do, the filesystem wins and the cache/state are rebuilt.
```mermaid
flowchart LR
FS["🗂️ Filesystem\n.md files on disk\n(source of truth)"]
Cache["⚡ Cache\n~/.laputa/cache/\n(fast startup index)"]
RS["⚛️ React State\nVaultEntry[]\n(in-memory session)"]
FS -->|"scan_vault_cached()"| Cache
Cache -->|"useVaultLoader on load"| RS
FS -->|"reload_vault (full rescan)"| RS
RS -.->|"write via Tauri IPC first"| FS
style FS fill:#d4edda,stroke:#28a745,color:#000
style Cache fill:#fff3cd,stroke:#ffc107,color:#000
style RS fill:#cce5ff,stroke:#004085,color:#000
```
#### Ownership rules
| Layer | Owner | Writes to | Reads from |
@@ -70,42 +86,52 @@ These must never diverge permanently. If they do, the filesystem wins and the ca
## System Overview
```
┌──────────────────────────────────────────────────────────────────┐
Tauri v2 Window │
┌──────────────────── React Frontend ────────────────────────┐ │
│ │
App.tsx (orchestrator) │ │
├── WelcomeScreen (onboarding / vault-missing) │ │
│ ├── Sidebar (navigation + filters + types) │ │
├── NoteList / PulseView (filtered list / activity) │ │
│ │ ├── Editor (BlockNote + tabs + diff + raw) │ │
├── Inspector (metadata + relationships) │ │
├── AIChatPanel (API-based chat) │ │
└── AiPanel (Claude CLI agent + tools) │ │
│ │ ├── SearchPanel (keyword/semantic/hybrid search) │ │
│ │ ├── SettingsPanel (API keys, GitHub, zoom, theme) │ │
├── StatusBar (vault picker + sync + version) │ │
├── CommandPalette (Cmd+K fuzzy command launcher) │ │
└── Modals (CreateNote, CreateType, Commit, GitHub) │ │
│ │ │ │
└──────────────┬──────────┬──────────────────────────────────┘ │
│ │ │ │
Tauri IPC│ Vite Proxy / WS │
┌──────────────▼────┐ ┌──▼────────────────────────────────┐ │
Rust Backend │ │ External Services │ │
lib.rs → 62 cmds │ │ Anthropic API (Claude chat) │ │
vault/ │ │ Claude CLI (agent subprocess) │ │
frontmatter/ │ │ MCP Server (ws://9710, 9711) │ │
│ │ git/ │ │ qmd (search/indexing engine) │ │
│ │ github/ │ │ GitHub API (OAuth, repos, clone) │ │
│ │ theme/ │ │ │ │
│ │ search.rs │ └───────────────────────────────────┘ │
│ │ indexing.rs │ │
│ │ claude_cli.rs │ │
│ └───────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```mermaid
flowchart TD
subgraph TW["Tauri v2 Window"]
subgraph FE["React Frontend"]
App["App.tsx (orchestrator)"]
WS["WelcomeScreen\n(onboarding)"]
SB["Sidebar\n(navigation + filters + types)"]
NL["NoteList / PulseView\n(filtered list / activity)"]
ED["Editor\n(BlockNote + tabs + diff + raw)"]
IN["Inspector\n(metadata + relationships)"]
AIC["AIChatPanel\n(API-based chat)"]
AIP["AiPanel\n(Claude CLI agent + tools)"]
SP["SearchPanel\n(keyword/semantic/hybrid)"]
ST["StatusBar\n(vault picker + sync + version)"]
CP["CommandPalette\n(Cmd+K launcher)"]
App --> WS & SB & NL & ED & SP & ST & CP
ED --> IN & AIC & AIP
end
subgraph RB["Rust Backend"]
LIB["lib.rs → 64 Tauri commands"]
VAULT["vault/"]
FM["frontmatter/"]
GIT["git/"]
GH["github/"]
THEME["theme/"]
SEARCH["search.rs + indexing.rs"]
CLI["claude_cli.rs"]
end
subgraph EXT["External Services"]
ANTH["Anthropic API\n(Claude chat)"]
CCLI["Claude CLI\n(agent subprocess)"]
MCP["MCP Server\n(ws://9710, 9711)"]
QMD["qmd\n(search engine)"]
GHAPI["GitHub API\n(OAuth, repos, clone)"]
end
FE -->|"Tauri IPC"| RB
FE -->|"Vite Proxy / WS"| EXT
end
style FE fill:#e8f4fd,stroke:#2196f3,color:#000
style RB fill:#fff8e1,stroke:#ff9800,color:#000
style EXT fill:#f3e5f5,stroke:#9c27b0,color:#000
```
## Four-Panel Layout
@@ -160,21 +186,38 @@ Full agent mode — spawns Claude CLI as a subprocess with tool access and MCP v
#### Agent Event Flow
```
User sends message in AiPanel
→ useAiAgent.sendMessage(text, references)
→ buildContextSnapshot(activeNote, linkedNotes, openTabs)
→ invoke('stream_claude_agent', { message, systemPrompt, vaultPath })
→ Rust spawns: claude -p <msg> --output-format stream-json --mcp-config <json>
→ NDJSON lines parsed into ClaudeStreamEvent variants:
Init, TextDelta, ThinkingDelta, ToolStart, ToolDone, Result, Error, Done
→ Events emitted via Tauri: app_handle.emit("claude-agent-stream", &event)
→ Frontend listener routes events:
onText → accumulate response (revealed on Done)
onThinking → show reasoning block (collapsed on first text)
onToolStart → add AiActionCard with spinner
onToolDone → update card with output
onDone → reveal full response, detect file operations
```mermaid
sequenceDiagram
participant U as User (AiPanel)
participant FE as useAiAgent (Frontend)
participant R as claude_cli.rs (Rust)
participant C as Claude CLI
participant V as Vault (MCP)
U->>FE: sendMessage(text, references)
FE->>FE: buildContextSnapshot(activeNote, linkedNotes, openTabs)
FE->>R: invoke('stream_claude_agent', {message, systemPrompt, vaultPath})
R->>C: spawn claude -p <msg> --output-format stream-json --mcp-config <json>
loop NDJSON stream
C-->>R: Init | TextDelta | ThinkingDelta | ToolStart | ToolDone | Result | Done
R-->>FE: emit("claude-agent-stream", event)
alt TextDelta
FE->>FE: accumulate response (revealed on Done)
else ThinkingDelta
FE->>FE: show reasoning block (collapses on first text)
else ToolStart
FE->>FE: add AiActionCard with spinner
else ToolDone
FE->>FE: update card with output
else Done
FE->>FE: reveal full response
FE->>FE: detect file operations → reload vault if needed
end
end
C->>V: MCP tool calls (search_notes, read_note, edit_note…)
V-->>C: tool results
```
#### File Operation Detection
@@ -251,36 +294,31 @@ Registration is non-destructive (additive, preserves other servers) and uses `up
### Architecture
```
┌─────────────────────────────────────────────────────┐
MCP Server (Node.js)
│ │
│ index.js ─── stdio transport ──→ Claude Code │
│ │ Cursor │
│ ├── vault.js (9 vault operations) │
├── findMarkdownFiles ├── deleteNote │
│ │ ├── readNote ├── linkNotes │
│ │ ├── createNote ├── listNotes │
├── searchNotes ├── vaultContext │
├── appendToNote │
│ │ └── editNoteFrontmatter │
│ │ │
└── ws-bridge.js │
│ ├── port 9710: tool bridge ←→ AI clients │
│ └── port 9711: UI bridge ←→ Frontend │
│ │
│ Spawned by Tauri (mcp.rs) on app startup │
│ Auto-registered in ~/.claude/mcp.json │
└─────────────────────────────────────────────────────┘
```mermaid
flowchart TD
subgraph MCP["MCP Server (Node.js) — spawned by Tauri on startup"]
IDX["index.js"]
VAULT["vault.js\n(findMarkdownFiles, readNote, createNote,\nsearchNotes, appendToNote, editNoteFrontmatter,\ndeleteNote, linkNotes, listNotes, vaultContext)"]
WSB["ws-bridge.js"]
IDX -->|"stdio transport"| STDIO["Claude Code / Cursor"]
IDX --> VAULT
IDX --> WSB
WSB -->|"port 9710 — tool bridge"| AI["AI Clients\n(Claude Code, external)"]
WSB -->|"port 9711 — UI bridge"| FE["Frontend\n(useAiActivity)"]
end
TAURI["Tauri (mcp.rs)"] -->|"spawn on startup"| MCP
TAURI -->|"auto-register"| CFG["~/.claude/mcp.json\n~/.cursor/mcp.json"]
```
### WebSocket Bridge
The WebSocket bridge enables real-time vault operations from both the frontend and external AI clients:
```
Frontend (useMcpBridge) ←→ ws://localhost:9710 ←→ ws-bridge.js ←→ vault.js
MCP stdio tools ←→ ws://localhost:9711 ←→ Frontend UI actions (useAiActivity)
```mermaid
flowchart LR
FE["Frontend\n(useMcpBridge)"] <-->|"ws://localhost:9710"| WSB["ws-bridge.js"]
WSB <--> VAULT["vault.js"]
STDIO["MCP stdio tools"] <-->|"ws://localhost:9711"| FE2["Frontend UI actions\n(useAiActivity)"]
```
**Tool bridge protocol** (port 9710):
@@ -317,16 +355,28 @@ Search uses the external `qmd` binary (semantic search engine) with three modes:
### Indexing Flow
```
Vault opened
→ check_index_status() → parse qmd status output
→ if stale or missing:
→ start_indexing() (two phases):
Phase 1 (Scanning): qmd update — scan all .md files
Phase 2 (Embedding): qmd embed — generate vector embeddings
→ Progress streamed via Tauri "indexing-progress" event
→ Metadata saved to .laputa-index.json (last_indexed_commit, timestamp)
→ run_incremental_update() for subsequent changes
```mermaid
flowchart TD
A([Vault opened]) --> B[check_index_status]
B --> C{Index status?}
C -->|Fresh| D[run_incremental_update\ngit diff since last commit]
C -->|Stale / Missing| E
subgraph E[Full Indexing — start_indexing]
E1["Phase 1: qmd update\n(scan all .md files)"]
E2["Phase 2: qmd embed\n(generate vector embeddings)"]
E1 --> E2
end
E --> F[Save .laputa-index.json\nlast_indexed_commit + timestamp]
D --> G([Search ready])
F --> G
E2 -.->|failure is non-fatal| G
G --> H{Search mode}
H -->|keyword| I[qmd search]
H -->|semantic| J[qmd vsearch]
H -->|hybrid| K[qmd query]
```
Embedding failure is non-fatal — keyword search still works.
@@ -349,9 +399,19 @@ The vault cache (`src-tauri/src/vault/cache.rs`) accelerates vault scanning usin
### Three Cache Strategies
1. **Same Commit (Cache Hit)**: Git HEAD matches cached hash → only re-parse uncommitted changed files via `git status --porcelain`
2. **Different Commit (Incremental Update)**: Uses `git diff <old>..<new> --name-only` to find changed files + uncommitted changes → selective re-parse
3. **No Cache / Corrupt Cache (Full Scan)**: Recursive `walkdir` of all `.md` files → full parse
```mermaid
flowchart TD
A([scan_vault_cached]) --> B{Cache exists\nand valid?}
B -->|No / Corrupt| C["🔴 Full Scan\nwalkdir all .md files\n→ full parse"]
B -->|Yes| D{Git HEAD\nmatches cache?}
D -->|Same commit| E["🟢 Cache Hit\ngit status --porcelain\n→ re-parse only uncommitted changes"]
D -->|Different commit| F["🟡 Incremental Update\ngit diff old..new --name-only\n→ selective re-parse of changed files"]
C --> G[Write cache atomically\n.tmp → rename]
E --> G
F --> G
G --> H([VaultEntry[] ready])
```
## Theme System
@@ -432,56 +492,69 @@ Backend: `get_vault_pulse` Tauri command parses `git log` with `--name-status`.
### Startup Sequence
```
1. Tauri setup:
a. run_startup_tasks() → purge trash, migrate frontmatter, seed themes, migrate AGENTS.md, seed config files, register MCP
b. spawn_ws_bridge() → start MCP WebSocket bridge (ports 9710, 9711)
2. App mounts
3. useOnboarding checks vault exists → WelcomeScreen if not
4. useVaultLoader fires:
a. invoke('list_vault', { path }) → scan_vault_cached() → VaultEntry[]
b. Load modified files via invoke('get_modified_files')
c. useMcpStatus → register MCP if needed
d. useThemeManager → load and apply active theme
e. useIndexing → check index status, trigger incremental update if needed
5. User clicks note in NoteList
6. useNoteActions.handleSelectNote:
a. invoke('get_note_content') → raw markdown
b. Add tab { entry, content } to tabs state
c. Set activeTabPath
7. Editor renders BlockNoteTab:
a. splitFrontmatter(content) → [yaml, body]
b. preProcessWikilinks(body) → replaces [[target]] with tokens
c. editor.tryParseMarkdownToBlocks(preprocessed)
d. injectWikilinks(blocks) → replaces tokens with wikilink nodes
e. editor.replaceBlocks()
8. Inspector renders frontmatter parsed from content
```mermaid
sequenceDiagram
participant T as Tauri (Rust)
participant A as App.tsx
participant VL as useVaultLoader
participant MCP as MCP Server
participant U as User
T->>T: run_startup_tasks()<br/>(purge trash, seed themes, register MCP)
T->>MCP: spawn_ws_bridge() — ports 9710 + 9711
T->>A: App mounts
A->>A: useOnboarding — vault exists?
alt Vault missing
A-->>U: WelcomeScreen
else Vault found
A->>VL: useVaultLoader fires
VL->>T: invoke('list_vault') → scan_vault_cached()
T-->>VL: VaultEntry[]
VL->>T: invoke('get_modified_files')
VL->>T: useMcpStatus — register if needed
VL->>T: useThemeManager — load active theme
VL->>T: useIndexing — incremental update if stale
VL-->>A: entries ready
end
U->>A: clicks note in NoteList
A->>T: invoke('get_note_content')
T-->>A: raw markdown
A->>A: splitFrontmatter → [yaml, body]
A->>A: preProcessWikilinks(body)
A->>A: tryParseMarkdownToBlocks()
A->>A: injectWikilinks(blocks)
A-->>U: Editor renders note
```
### Auto-Save Flow
```
Editor content changes
→ useEditorSave detects change (debounced)
→ serialize BlockNote blocks → markdown
postProcessWikilinks → restore [[target]] syntax
invoke('save_note_content', { path, content })
→ Update tab status indicator
```mermaid
flowchart LR
A["✏️ Editor content changes"] --> B["useEditorSave\n(debounced)"]
B --> C["blocksToMarkdownLossy()"]
C --> D["postProcessWikilinks()\n→ restore [[target]] syntax"]
D --> E["invoke('save_note_content')"]
E --> F["💾 Disk write"]
F --> G["Update tab status indicator"]
```
### Git Sync Flow
```
useAutoSync (configurable interval, default from settings):
invoke('git_pull') → GitPullResult
→ if conflicts → ConflictResolverModal
→ if fast-forward → reload vault
→ invoke('git_push') → GitPushResult
```mermaid
flowchart TD
AS["useAutoSync\n(configurable interval)"] --> PULL["invoke('git_pull')"]
PULL --> PC{Result?}
PC -->|Conflicts| CM["ConflictResolverModal"]
PC -->|Fast-forward| RV["reload vault"]
PC -->|Up to date| DONE["idle"]
Manual commit:
→ CommitDialog → invoke('git_commit', { message })
→ invoke('git_push')
→ Reload modified files
AS --> PUSH["invoke('git_push')"]
MAN["Manual commit\n(CommitDialog)"] --> GC["invoke('git_commit', message)"]
GC --> GP["invoke('git_push')"]
GP --> RM["Reload modified files"]
```
## Vault Module Structure