feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0001"
|
||||
title: "Tauri v2 + React as application stack"
|
||||
status: active
|
||||
date: 2026-02-14
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa is a desktop app for macOS (with iPad as a future target) that reads and writes a vault of markdown files. The app needs a native feel, filesystem access, git integration, and a rich text editor. A single developer (with AI assistance) is building it.
|
||||
|
||||
## Decision
|
||||
|
||||
Use **Tauri v2** (Rust backend + WebView frontend) with **React + TypeScript** for the UI, **BlockNote** for the editor, and **Vitest + Playwright** for testing.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Electron**: heavier runtime (~150MB), slower, but more mature ecosystem. Rejected — Tauri is lighter and has better native integration.
|
||||
- **SwiftUI**: best native macOS/iOS experience, but locks to Apple platforms only, no code sharing with a potential web version, and requires rewriting the entire UI. Rejected for the initial version — revisited in ADR-0005.
|
||||
- **Flutter**: cross-platform but WebView-based editor would have been poor; Dart ecosystem is thin for markdown tooling.
|
||||
- **Pure web app**: no filesystem access, no git, would require a backend server. Rejected — offline-first is a core principle.
|
||||
|
||||
## Consequences
|
||||
|
||||
- React frontend can be shared with a future web version
|
||||
- Rust backend provides safe, fast filesystem/git operations
|
||||
- Tauri v2 supports iOS (beta) — see ADR-0005 for iPad strategy
|
||||
- CodeScene code health monitoring applies to both Rust and TypeScript code
|
||||
- Claude Code can work on both layers without context switching
|
||||
- Triggers re-evaluation if: Tauri iOS proves unstable for production, or if SwiftUI becomes the primary target platform
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0002"
|
||||
title: "Filesystem as the single source of truth"
|
||||
status: active
|
||||
date: 2026-02-14
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs a persistence model. The core question: does the app own the data, or does the filesystem? This affects sync, conflict resolution, offline support, portability, and long-term trust with users.
|
||||
|
||||
## Decision
|
||||
|
||||
**The vault is the source of truth.** The app never owns the data — it only reads and writes `.md` files. All cache, React state, and in-memory representations are derived from the filesystem and must be reconstructible by deleting them. When in doubt, the file on disk wins.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Database-first (SQLite)**: faster queries, easier relationships. Rejected — creates lock-in, makes files unreadable outside the app, complicates sync.
|
||||
- **Cloud-first (proprietary sync)**: easier multi-device. Rejected — zero lock-in is a core principle; git handles sync.
|
||||
- **Hybrid (DB + files)**: DB as primary, files as export. Rejected — two sources of truth always diverge.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Notes are plain markdown files, readable and editable by any text editor
|
||||
- Git provides history, sync, and collaboration for free
|
||||
- Vault can be opened/edited externally without app corruption
|
||||
- App rebuilds cache on startup — acceptable cost for integrity guarantees
|
||||
- No "save" button needed — autosave writes to disk immediately
|
||||
- Triggers re-evaluation if: vault size grows to millions of files and filesystem scanning becomes a bottleneck
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0003"
|
||||
title: "Single note open at a time (no tabs)"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The app originally had a tab bar allowing multiple notes to be open simultaneously (similar to a browser or code editor). After building and shipping it, the tab model was found to add significant UI complexity, state management overhead, and confusion — without a proportional benefit for a notes app.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove the tab bar. Only one note is open at a time.** Navigation history (Back/Forward with Cmd+[/]) replaces tabs for moving between recently visited notes. Closed tab history and `useTabManagement` are removed.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep tabs**: familiar UX, allows comparing notes side by side. Rejected — adds ~2000 lines of complexity, confusing state (which tab is "active"?), and breaks the "editor is sacred" principle.
|
||||
- **Tabs + single-note toggle**: configurable per user. Rejected — doubles the state surface and testing burden.
|
||||
- **Split pane (two notes at once)**: useful for reference. Deferred — can be added later without tabs, via a dedicated split layout.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Removes ~2000 lines of code (`TabBar`, `useClosedTabHistory`, `useEditorTabSwap`, `tabLayout`)
|
||||
- `handleSelectNote` replaces the current note instead of adding a tab
|
||||
- Cmd+W (close tab) and Cmd+Shift+T (reopen closed tab) removed from shortcuts
|
||||
- Back/Forward navigation (Cmd+[/Cmd+]) preserves history without tab state
|
||||
- Significant simplification of `App.tsx` and editor state
|
||||
- Triggers re-evaluation if: multi-note workflows become a top user request
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0004"
|
||||
title: "Vault vs app settings for state storage"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As features were added, there was recurring ambiguity about where to persist configuration and state: in the vault (as frontmatter in `.md` files) or in app settings (`~/.config/com.laputa.app/settings.json` / localStorage). Without a clear rule, some decisions were inconsistent.
|
||||
|
||||
## Decision
|
||||
|
||||
**Ask: "Would the user want this to follow the vault across all their installations?"**
|
||||
|
||||
- If **yes** → store in the vault (as frontmatter in the relevant `.md` file, using the `_` convention for system properties)
|
||||
- If **no** → store in app settings
|
||||
|
||||
Examples:
|
||||
| Data | Storage | Reason |
|
||||
|------|---------|--------|
|
||||
| Note type icon (`_icon`) | Vault frontmatter | Follows the vault everywhere |
|
||||
| Note type color (`_color`) | Vault frontmatter | Follows the vault everywhere |
|
||||
| Note sort preference | Vault frontmatter (type file) | Per-vault, consistent across devices |
|
||||
| API keys (Anthropic, OpenAI) | App settings | Installation-specific |
|
||||
| GitHub token | App settings | Installation-specific |
|
||||
| Window size / zoom | App settings | Device-specific |
|
||||
| Editor zoom level | App settings | Device-specific |
|
||||
| Telemetry consent | App settings | Installation-specific |
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Everything in localStorage**: simple, but breaks cross-device sync for vault-level config.
|
||||
- **Everything in vault**: pure, but makes device-specific settings (zoom, window size) propagate to all devices — confusing.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Config that "belongs to a note or type" lives in frontmatter — readable/diffable in git
|
||||
- The `_` prefix convention (see ABSTRACTIONS.md) distinguishes system properties from user properties
|
||||
- App rebuilds from vault state on open — no stale config files to manage
|
||||
- Triggers re-evaluation if: vault files become too polluted with system frontmatter properties
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0005"
|
||||
title: "Tauri v2 iOS for iPad support (vs SwiftUI rewrite)"
|
||||
status: active
|
||||
date: 2026-03-27
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa runs on macOS via Tauri v2. The goal is to also support iPad without changing the stack or redesigning the app from scratch. The core question: extend the existing stack to iOS, or rewrite in SwiftUI for a fully native experience?
|
||||
|
||||
## Decision
|
||||
|
||||
**Use Tauri v2 iOS (beta) for the iPad prototype.** The React frontend stays identical. The Rust backend compiles for iOS with `#[cfg(desktop)]` / `#[cfg(mobile)]` guards for platform-specific features. Desktop-only features (git CLI, macOS menu bar, MCP server, Claude CLI) are stubbed or skipped on mobile.
|
||||
|
||||
The prototype (`feat: add iPad/iOS prototype via Tauri v2 mobile target`, build `b492`) successfully builds and runs on iPad Pro 13" simulator (iOS 18.3.1).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **SwiftUI rewrite**: best native macOS/iPad experience, full App Store integration, native TextKit 2 editor. Rejected for now — would discard all existing React code, Rust backend, 2200+ tests, and Claude Code's accumulated context. Worth revisiting if Laputa becomes iOS-first.
|
||||
- **Capacitor**: replaces Tauri layer, keeps React, but the Rust backend is lost entirely — git and file operations would need reimplementation in JS or Swift.
|
||||
- **React Native + WebView**: wraps the React app in a WebView. Too hacky, performance concerns, App Store review risks.
|
||||
|
||||
## Git on iPad
|
||||
|
||||
`git` CLI is unavailable on iOS. Options for production:
|
||||
- **Option A (recommended)**: `isomorphic-git` — pure JS git implementation, no native dependencies, runs in WebView. Replaces Rust git commands on mobile.
|
||||
- **Option B (prototype)**: Working Copy as iOS Files provider — user manages git separately.
|
||||
- **Option C**: iCloud Drive sync — no git history. Not recommended.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Zero frontend changes needed for basic iPad support
|
||||
- Desktop features (git, MCP, Claude CLI) unavailable on iPad until isomorphic-git is integrated
|
||||
- Tauri v2 iOS is still beta — production stability unknown
|
||||
- App Store distribution requires Apple Developer account and TestFlight
|
||||
- Triggers re-evaluation if: Tauri iOS remains unstable after 6 months, or iPad becomes the primary target (in which case SwiftUI rewrite becomes rational)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0006"
|
||||
title: "Flat vault structure (no type-based folders)"
|
||||
status: active
|
||||
date: 2026-03-15
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Originally, notes were organized into type-based subfolders (`project/`, `person/`, `topic/`, etc.). Changing a note's type required moving it between folders, which broke wikilinks, complicated wikilink resolution (paths vs titles), and created friction for users who wanted to reorganize their knowledge. It also made vault scanning more complex and introduced edge cases around folder creation/deletion.
|
||||
|
||||
## Decision
|
||||
|
||||
**All user notes live as flat `.md` files at the vault root. Type is determined solely from the `type:` frontmatter field — never inferred from folder location.** Only a small set of protected folders exist: `type/` (type definition documents), `config/` (meta-configuration), and `attachments/`.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Flat vault with frontmatter-only type — simple wikilink resolution (title/filename only), no file moves on type change, vault scanning restricted to root + protected folders. Downside: large vaults may look cluttered in Finder.
|
||||
- **Option B**: Keep type-based folders — familiar Obsidian-like structure. Downside: type changes require file moves, wikilink resolution needs path awareness, scanning is recursive and slower.
|
||||
- **Option C**: Hybrid (folders optional, type still from frontmatter) — maximum flexibility. Downside: two ways to do the same thing, confusing for AI agents and automation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Wikilink resolution is simplified to multi-pass title/filename matching — no path-based matching needed.
|
||||
- Changing a note's type is a frontmatter edit, not a file move.
|
||||
- A `flatten_vault` migration command and wizard were added for existing vaults with type folders.
|
||||
- `vault_health_check` detects stray files in non-protected subfolders.
|
||||
- `scan_vault` only indexes root-level `.md` files plus protected folders — non-protected subdirectories are ignored.
|
||||
- Re-evaluation trigger: if users need nested folder hierarchies for non-type organization (e.g., project-specific subdirectories).
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0007"
|
||||
title: "Title equals filename (slug sync)"
|
||||
status: superseded
|
||||
date: 2026-03-15
|
||||
superseded_by: "0044"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
With the move to a flat vault structure (ADR-0006), filenames became the primary identifier for notes. Previously, titles were extracted from the first H1 heading, which was fragile (users could delete or change the H1 without realizing it affected the note's identity). A clear, deterministic mapping between title and filename was needed.
|
||||
|
||||
## Decision
|
||||
|
||||
**Every note's filename is `slugify(title).md`. The `title` frontmatter field is the source of truth for the human-readable title. On note open, the system syncs the title field to match the filename if they've diverged (filename wins). On rename, both title and filename are updated atomically.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): `title = slugify(filename)` with bidirectional sync — deterministic, predictable, wikilinks resolve by title/filename stem. Downside: titles with special characters get simplified in filenames.
|
||||
- **Option B**: UUID-based filenames with title only in frontmatter — filenames never change. Downside: vault is unreadable in Finder/terminal, breaks the "plain markdown files" principle.
|
||||
- **Option C**: H1-based title extraction — no explicit title field. Downside: fragile, H1 can be accidentally deleted or changed, decoupled from filename.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `extract_title` reads from frontmatter `title:` field, never from H1. Falls back to `slug_to_title()` (hyphens → spaces, title-case).
|
||||
- `sync_title_on_open` auto-corrects desynced frontmatter on note open.
|
||||
- `rename_note` updates both `title:` frontmatter and filename atomically, plus cross-vault wikilink updates.
|
||||
- The H1 block inside BlockNote is hidden via CSS; a dedicated `TitleField` component above the editor is the primary title editing surface.
|
||||
- Slug collision detection prevents duplicate filenames.
|
||||
- Re-evaluation trigger: if users need filenames that don't match titles (e.g., short slugs for long titles).
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0008"
|
||||
title: "Underscore convention for system properties"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As Laputa added more features that store configuration in note frontmatter (pinned properties, type icons, colors, sidebar labels, sort order), the Properties panel became cluttered with internal fields that users shouldn't normally edit. A convention was needed to distinguish user-visible properties from system-internal ones.
|
||||
|
||||
## Decision
|
||||
|
||||
**Any frontmatter field whose name starts with `_` is a system property. It is hidden from the Properties panel, not exposed in search/filters, but remains editable in the raw editor. The frontmatter parser filters out `_*` fields before passing properties to the UI.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Underscore prefix convention (`_icon`, `_color`, `_order`, `_pinned_properties`) — simple, readable in raw files, universal rule. Downside: users must know the convention to access system fields.
|
||||
- **Option B**: Separate YAML block or nested `_system:` key — cleaner separation. Downside: more complex parsing, breaks flat key-value frontmatter model.
|
||||
- **Option C**: Store system properties in a separate sidecar file (`.meta.yml`) — complete separation. Downside: doubles the number of files, harder to keep in sync.
|
||||
|
||||
## Consequences
|
||||
|
||||
- All future system-level frontmatter fields must use the `_field_name` convention.
|
||||
- Both Rust (`vault/mod.rs`) and TypeScript (`utils/frontmatter.ts`) parsers filter `_*` fields before passing `properties` to the UI.
|
||||
- Power users can still access and edit system properties via the raw editor.
|
||||
- Type documents use `_icon`, `_color`, `_order`, `_sidebar_label`, `_pinned_properties`.
|
||||
- Re-evaluation trigger: if the number of system properties grows large enough to warrant a structured sub-object.
|
||||
|
||||
## Normalized system properties
|
||||
|
||||
| Canonical key | Old keys (read with fallback) | Written by |
|
||||
|---|---|---|
|
||||
| `_archived` | `Archived`, `archived` | Archive action |
|
||||
| `_trashed` | `Trashed`, `trashed` | Trash action |
|
||||
| `_trashed_at` | `Trashed at`, `trashed_at` | Trash action |
|
||||
| `_favorite` | — | Favorite toggle |
|
||||
| `_favorite_index` | — | Favorite reorder |
|
||||
|
||||
**Write rule**: always use the canonical `_`-prefixed key.
|
||||
**Read rule**: accept both canonical and legacy keys (case-insensitive). Do NOT rewrite on read — migration is a separate concern.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0009"
|
||||
title: "Keyword-only search (remove semantic indexing)"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa previously used QMD (a Go binary) for semantic vector indexing, enabling similarity-based search. This added significant complexity: a bundled Go binary requiring code-signing, an indexing step on vault open, status bar progress tracking, auto-install logic, and a separate `tools/qmd/` directory. The semantic search quality did not justify the operational burden, especially as the AI agent (with MCP vault tools) became a more natural way to do exploratory queries.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove QMD semantic indexing entirely and keep only keyword-based search. Search uses `walkdir` to scan all `.md` files, matching against titles and content with case-insensitive substring matching and relevance scoring.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Keyword-only search via `walkdir` — zero dependencies, no indexing step, instant results, no binary to sign/bundle. Downside: no fuzzy or semantic matching.
|
||||
- **Option B**: Keep QMD semantic search — richer search results, similarity matching. Downside: bundled Go binary, code-signing, indexing latency, maintenance burden.
|
||||
- **Option C**: Replace QMD with a Rust-native embedding library — no external binary. Downside: large model files, cold start time, still needs indexing.
|
||||
|
||||
## Consequences
|
||||
|
||||
- No external search binary to bundle, sign, or install.
|
||||
- No indexing step on vault open — search is instant.
|
||||
- `search_vault` Tauri command scans files directly with `walkdir`, runs in a blocking Tokio task.
|
||||
- Title matches rank higher than content-only matches; exact title matches rank highest.
|
||||
- The AI agent (via MCP `search_notes` tool) provides an alternative for exploratory/semantic queries.
|
||||
- Re-evaluation trigger: if users report keyword search is insufficient for large vaults (9000+ notes).
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0010"
|
||||
title: "Dynamic wikilink relationship detection"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs to support arbitrary relationship types between notes (e.g., `Topics:`, `Key People:`, `Depends on:`). Initially, a hardcoded list `RELATIONSHIP_KEYS` identified which frontmatter fields were relationships. This was fragile — adding a new relationship type required a code change, and users couldn't define their own.
|
||||
|
||||
## Decision
|
||||
|
||||
**The Rust parser dynamically detects relationship fields by scanning all frontmatter keys for values containing `[[wikilinks]]`. Any field with wikilink values is captured in the `relationships` HashMap — no hardcoded field name list needed.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Dynamic detection via `[[wikilink]]` presence — zero configuration, extensible, any field name works. Downside: fields with bracket-like content could false-positive (mitigated by the `[[...]]` double-bracket syntax).
|
||||
- **Option B**: Hardcoded `RELATIONSHIP_KEYS` list — simple, predictable. Downside: inflexible, requires code changes for new relationship types.
|
||||
- **Option C**: User-configurable relationship field list in vault config — flexible. Downside: configuration burden, doesn't work out of the box.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users can define arbitrary relationship types by adding wikilink values to any frontmatter field.
|
||||
- No code change needed when adding new relationship types — convention over configuration.
|
||||
- All relationship fields appear in the Inspector's RelationshipsPanel automatically.
|
||||
- The `relationships` HashMap in `VaultEntry` captures all dynamic relationships.
|
||||
- Standard fields (`belongs_to`, `related_to`) are still recognized for backward compatibility but not privileged.
|
||||
- Re-evaluation trigger: if false-positive detection becomes a problem (e.g., fields with literal `[[` content that aren't relationships).
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0011"
|
||||
title: "MCP server for AI tool integration"
|
||||
status: active
|
||||
date: 2026-02-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa's AI features (agent panel, chat) need structured access to vault data — searching notes, reading content, editing frontmatter, and steering the UI. Rather than building a bespoke API, the Model Context Protocol (MCP) provides a standardized tool interface that works with Claude Code, Cursor, and any MCP-compatible client.
|
||||
|
||||
## Decision
|
||||
|
||||
**Laputa ships a Node.js MCP server (`mcp-server/`) that exposes vault operations as 14 tools. It runs on stdio for external clients and on two WebSocket ports (9710 for tool calls, 9711 for UI actions) for the embedded Laputa frontend. Tauri spawns the server on startup and auto-registers it in Claude Code and Cursor configs.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Node.js MCP server with stdio + WebSocket dual transport — standard MCP compatibility, works with Claude Code/Cursor out of the box, WebSocket enables real-time UI steering. Downside: Node.js dependency, two extra ports.
|
||||
- **Option B**: Rust-native MCP server — no Node.js dependency. Downside: MCP SDK is JavaScript-first, Rust implementation would be custom and harder to maintain.
|
||||
- **Option C**: Custom REST/gRPC API — full control. Downside: no compatibility with existing AI tool ecosystems, each client needs a custom integration.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Vault tools (search, read, create, edit, delete, link) are available to any MCP-compatible client.
|
||||
- Auto-registration in `~/.claude/mcp.json` and `~/.cursor/mcp.json` means zero setup for users.
|
||||
- The WebSocket bridge enables real-time UI actions (highlight elements, open notes, set filters) from AI tools.
|
||||
- `mcp-server/` is bundled into release builds and spawned as a child process by `mcp.rs`.
|
||||
- Port conflicts on 9710/9711 are handled gracefully (EADDRINUSE tolerance).
|
||||
- Re-evaluation trigger: if MCP SDK gains a Rust implementation that eliminates the Node.js dependency.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0012"
|
||||
title: "Claude CLI subprocess for AI agent (replacing direct API)"
|
||||
status: active
|
||||
date: 2026-03-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The AI agent panel initially called the Anthropic API directly from Rust, managing tool calling loops manually. This required implementing tool execution, conversation state, and streaming — all complex to maintain. Claude CLI (`claude` binary) handles all of this natively, including MCP tool integration, conversation history, and streaming NDJSON output.
|
||||
|
||||
## Decision
|
||||
|
||||
**The AI agent panel spawns Claude CLI as a subprocess via `claude_cli.rs`, passing messages with `--output-format stream-json` and vault MCP config via `--mcp-config`. The frontend parses the NDJSON event stream (Init, TextDelta, ThinkingDelta, ToolStart, ToolDone, Result, Done) for real-time display.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Claude CLI subprocess with NDJSON streaming — built-in tool calling, MCP integration, conversation management, no API key needed (CLI handles auth). Downside: requires Claude CLI installed, subprocess management complexity.
|
||||
- **Option B**: Direct Anthropic API with manual tool loop — full control, no external dependency. Downside: must implement tool calling, retries, conversation state, MCP tool bridging.
|
||||
- **Option C**: Use Anthropic Agent SDK from Rust — structured agent framework. Downside: SDK is Python/TypeScript, no Rust support.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The AI agent gets full tool access (MCP vault tools + shell access) without custom tool-calling code.
|
||||
- `claude_cli.rs` manages subprocess lifecycle: spawn, stream events, kill on cancel.
|
||||
- The frontend (`useAiAgent` hook) processes NDJSON events for reasoning blocks, tool action cards, and response display.
|
||||
- File operation detection (from Write/Edit tool inputs) triggers automatic vault reload.
|
||||
- The simpler AI Chat panel still uses the Anthropic API directly for lightweight, no-tools conversations.
|
||||
- Re-evaluation trigger: if Anthropic releases a Rust Agent SDK or if Claude CLI streaming format changes significantly.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0013"
|
||||
title: "Remove vault-based theming system"
|
||||
status: superseded
|
||||
date: 2026-03-23
|
||||
superseded_by: "0081"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa had a vault-based theming system where themes were markdown notes in `theme/` with `type: Theme` frontmatter. Each property became a CSS variable. This included a `ThemeManager` hook, theme property editor, dark mode detection, live preview on save, and three built-in themes. The system was complex (spanning Rust seed/create/defaults modules, TypeScript hooks, and CSS variable bridging) and added significant maintenance burden for a feature that most users never customized beyond the defaults.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove the vault-based theming system entirely. The app uses a single, hardcoded light theme defined in CSS variables (`src/index.css`) and editor theme (`src/theme.json`).** The `theme/` folder, `ThemeManager` hook, theme Rust modules, theme property editor, and dark mode support were all deleted.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Remove theming, ship a single polished light theme — drastically reduced complexity, fewer files to maintain, no theme-related bugs. Downside: no user customization, no dark mode.
|
||||
- **Option B**: Keep theming but simplify — reduce to light/dark toggle only. Downside: still requires theme loading, CSS variable bridging, and live preview infrastructure.
|
||||
- **Option C**: Keep the full theming system — maximum flexibility. Downside: high maintenance cost for a rarely-used feature, frequent source of bugs (WKWebView reflow issues, CSS var sync).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Deleted: `src-tauri/src/theme/`, `src/hooks/useThemeManager.ts`, `ThemePropertyEditor.tsx`, theme-related commands, `_themes/` legacy support.
|
||||
- Single theme defined in `src/index.css` (CSS variables) and `src/theme.json` (editor typography).
|
||||
- No dark mode support — the app is light-only.
|
||||
- Protected folders reduced: `theme/` is no longer scanned by `scan_vault`.
|
||||
- Re-evaluation trigger: if dark mode becomes a hard requirement for accessibility or user demand.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0014"
|
||||
title: "Git-based incremental vault cache"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Scanning a vault of 9000+ markdown files on every app launch takes several seconds. A caching strategy was needed that could detect which files changed since the last scan and only re-parse those, while remaining correct even after external edits (e.g., from a text editor or git pull).
|
||||
|
||||
## Decision
|
||||
|
||||
**Use git as the change detection mechanism. The cache stores all `VaultEntry` objects in a JSON file at `~/.laputa/cache/<vault-hash>.json`. On load, it compares the cached git HEAD commit hash with the current one: if the same, only re-parse uncommitted changed files; if different, use `git diff` to find changed files and selectively re-parse. Full rescan only on cache miss or version bump.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Git-based incremental cache — leverages existing git infrastructure, precise change detection, handles both committed and uncommitted changes. Downside: requires git-tracked vault, cache invalidation logic is complex.
|
||||
- **Option B**: File modification time (`mtime`) based cache — works without git. Downside: unreliable across filesystems (iCloud, Dropbox), clock skew issues.
|
||||
- **Option C**: File hash (content-based) cache — always correct. Downside: must read every file to compute hash, defeating the purpose of caching.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Cache file stored outside the vault at `~/.laputa/cache/<vault-hash>.json` — never pollutes the user's git repo.
|
||||
- Writes are atomic (write to `.tmp` then rename) to prevent corruption.
|
||||
- Cache version (v5) is bumped on `VaultEntry` field changes to force full rescan.
|
||||
- Legacy `.laputa-cache.json` files inside the vault are auto-migrated and deleted on first run.
|
||||
- `reload_vault` command deletes the cache file before rescanning, guaranteeing fresh data.
|
||||
- Stale cache entries are pruned on vault open (files that no longer exist on disk).
|
||||
- Re-evaluation trigger: if non-git vaults (e.g., iCloud-only) need to be supported.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0015"
|
||||
title: "Auto-save with 500ms debounce"
|
||||
status: active
|
||||
date: 2026-03-19
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Manual save (Cmd+S) was the only way to persist editor changes. Users occasionally lost work when switching notes or closing the app without saving. An auto-save mechanism was needed that balanced responsiveness (no perceived lag) with disk I/O efficiency (not writing on every keystroke).
|
||||
|
||||
## Decision
|
||||
|
||||
**Notes auto-save with a 500ms debounce after the last keystroke. The `useEditorSave` hook watches for editor content changes and triggers a save after 500ms of inactivity. The same `save_note_content` Rust command is used for both auto-save and manual save.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): 500ms debounce auto-save — fast enough to feel instant, slow enough to batch rapid keystrokes. Downside: 500ms window where unsaved changes exist.
|
||||
- **Option B**: Save on every change (no debounce) — zero data loss risk. Downside: excessive disk writes, poor performance, frequent git diffs.
|
||||
- **Option C**: Save on note switch / app blur only — minimal disk writes. Downside: data loss if app crashes mid-edit, no live preview of changes in other views.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users never need to manually save (Cmd+S still works as an immediate save).
|
||||
- Auto-save triggers vault entry updates, keeping the note list, search, and relationships current.
|
||||
- The same save path handles wikilink extraction and frontmatter parsing after save.
|
||||
- Secondary windows (multi-window mode) each have their own auto-save via `useEditorSaveWithLinks`.
|
||||
- Re-evaluation trigger: if 500ms is too aggressive for low-powered devices or network-synced vaults.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0016"
|
||||
title: "Sentry crash reporting + PostHog analytics with consent"
|
||||
status: active
|
||||
date: 2026-03-25
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As Laputa approaches public release, crash reports and usage analytics are needed to identify bugs and understand feature adoption. However, as a personal knowledge management app that handles sensitive data, user privacy is paramount. Any telemetry must be opt-in with clear consent.
|
||||
|
||||
## Decision
|
||||
|
||||
**Integrate Sentry for crash reporting and PostHog for product analytics, both gated behind an explicit consent dialog on first launch. Users can toggle each independently in Settings. No telemetry is sent without affirmative consent.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Sentry + PostHog with consent dialog — industry-standard tools, separate crash/analytics toggles, privacy-respecting opt-in. Downside: two external dependencies, two services to manage.
|
||||
- **Option B**: Self-hosted error tracking — full data control. Downside: operational burden, limited analytics features.
|
||||
- **Option C**: No telemetry — simplest, most private. Downside: blind to crashes and usage patterns, harder to prioritize features.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `TelemetryConsentDialog` shows on first launch with accept/decline buttons.
|
||||
- Accepting generates an `anonymous_id` (no PII) and sets `telemetry_consent: true` in settings.
|
||||
- `useTelemetry` hook reactively initializes/tears down Sentry and PostHog based on settings.
|
||||
- Both frontend (`src/lib/telemetry.ts`) and Rust backend (`src-tauri/src/telemetry.rs`) have path scrubbers in `beforeSend` hooks to strip vault paths.
|
||||
- DSN/keys come from `VITE_SENTRY_DSN` / `VITE_POSTHOG_KEY` env vars.
|
||||
- `reinit_telemetry` Tauri command toggles Rust-side Sentry at runtime.
|
||||
- Re-evaluation trigger: if a unified telemetry platform (e.g., OpenTelemetry) could replace both services.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0018"
|
||||
title: "CodeScene code health gates in CI and git hooks"
|
||||
status: superseded
|
||||
date: 2026-03-13
|
||||
superseded_by: "0064"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Code complexity tends to increase over time, especially in fast-moving projects. Without automated enforcement, hotspot files (most-edited files) degrade in quality, making future changes harder and buggier. A quantitative code health metric was needed to prevent regression.
|
||||
|
||||
## Decision
|
||||
|
||||
**Enforce CodeScene code health scores as mandatory gates in pre-commit and pre-push hooks. Hotspot Code Health must be >= 9.5 and Average Code Health must be >= 9.31 (project-wide). Both gates block commit/push on failure.** The Boy Scout Rule ("leave every file better than you found it") is enforced as part of every task.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): CodeScene with hard gates — quantitative, automated, catches complexity before it merges. Downside: can slow development if scores are borderline, requires CodeScene API access.
|
||||
- **Option B**: Manual code review for complexity — human judgment. Downside: subjective, inconsistent, doesn't scale.
|
||||
- **Option C**: Linter-only rules (ESLint complexity, Clippy) — built-in, no external service. Downside: coarser metrics, no hotspot awareness, no project-wide average tracking.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Pre-commit hook runs vitest + CodeScene health check before every commit.
|
||||
- Pre-push hook runs the same checks plus Playwright smoke tests.
|
||||
- Developers must fix complexity regressions before committing — even in files they didn't directly modify if changes indirectly affected complexity.
|
||||
- Never use `// eslint-disable`, `#[allow(...)]`, or `as any` to pass the gate.
|
||||
- Common fixes: extract hooks, split large components, reduce function complexity, extract modules.
|
||||
- `.codesceneignore` excludes `tools/`, `e2e/`, `tests/`, `scripts/` from analysis.
|
||||
- Re-evaluation trigger: if CodeScene becomes unavailable or a better code health tool emerges.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0019"
|
||||
title: "GitHub device flow OAuth for vault sync"
|
||||
status: active
|
||||
date: 2026-02-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa supports git-backed vaults synced to GitHub. Users need to authenticate with GitHub to clone repos, push changes, and create new vault repositories. In a desktop app, the standard OAuth redirect flow is awkward (no web server to receive the callback). The Device Authorization Flow is designed for exactly this scenario.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use GitHub's Device Authorization Flow (OAuth device code grant) for GitHub authentication. The user sees a code, opens a browser to enter it, and the app polls for the token. Token is persisted in app settings for future git operations.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Device Authorization Flow — designed for desktop/CLI apps, no redirect URI needed, secure (user authenticates in their own browser). Downside: requires user to switch to browser and back.
|
||||
- **Option B**: Personal Access Token (PAT) entry — user generates token on GitHub, pastes it in Settings. Downside: poor UX, users must navigate GitHub settings, token scope management is manual.
|
||||
- **Option C**: OAuth redirect with local server — spawn a local HTTP server to receive the redirect. Downside: port conflicts, firewall issues, more complex implementation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `GitHubDeviceFlow` component handles the OAuth flow UI (device code display, polling, success/error states).
|
||||
- `GitHubVaultModal` enables cloning existing repos or creating new ones.
|
||||
- Token persisted in `settings.json` under `github_token` / `github_username`.
|
||||
- `SettingsPanel` shows connection status with disconnect option.
|
||||
- Uses Tauri opener plugin to launch the browser for user authentication.
|
||||
- Re-evaluation trigger: if Tauri gains native OAuth redirect support that's simpler than the device flow.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0020"
|
||||
title: "Keyboard-first design principle"
|
||||
status: active
|
||||
date: 2026-03-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa is a productivity tool for knowledge workers who spend most of their time typing. Mouse-heavy interactions interrupt flow. Every feature should be reachable without touching the mouse, and the app must be fully testable via keyboard events (important for Playwright automation and accessibility).
|
||||
|
||||
## Decision
|
||||
|
||||
**Every feature must be reachable via keyboard. Every command palette entry must also appear in the macOS menu bar (File / Edit / View / Note / Vault / Window). This is both a design principle and a QA requirement.** Navigation, note switching, panel toggling, search, and all commands work via keyboard shortcuts or the Cmd+K command palette.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Keyboard-first with menu bar parity — full keyboard accessibility, menu bar for discoverability, testable via Playwright keyboard events. Downside: more work per feature (must wire shortcut + menu item + command palette entry).
|
||||
- **Option B**: Mouse-primary with some shortcuts — faster to implement. Downside: poor flow for power users, harder to automate testing.
|
||||
- **Option C**: Keyboard-only (no menu bar) — simplest. Downside: poor discoverability, macOS HIG violation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `useCommandRegistry` + `useAppCommands` build a centralized command registry with labels, shortcuts, and handlers.
|
||||
- `CommandPalette` (Cmd+K) fuzzy-searches all registered commands.
|
||||
- `menu.rs` defines the native macOS menu bar with accelerators matching keyboard shortcuts.
|
||||
- `useAppKeyboard` registers global keyboard shortcuts.
|
||||
- `useMenuEvents` bridges menu bar clicks to command handlers.
|
||||
- QA uses `osascript` keyboard events for native testing — no mouse, no `cliclick`.
|
||||
- macOS gotcha: `Option+N` produces special characters — use `e.code` or `Cmd+N` instead.
|
||||
- Re-evaluation trigger: if a non-macOS platform (Windows, Linux) is supported and needs different menu/shortcut conventions.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0021"
|
||||
title: "Push directly to main (no PRs or branches)"
|
||||
status: active
|
||||
date: 2026-03-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Initially, the project used feature branches and PRs. With a single developer (assisted by Claude Code), the PR overhead — branch creation, rebase churn, merge conflicts from long-lived branches — slowed development without adding review value. The pre-commit and pre-push hooks already enforce tests, linting, type checking, and code health gates.
|
||||
|
||||
## Decision
|
||||
|
||||
**Push directly to main — no PRs, no feature branches. The pre-push hook runs all quality gates (tests, lint, type check, coverage, CodeScene health). Never use `--no-verify`.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Push to main with hook-enforced quality gates — fastest iteration, no rebase churn, hooks provide automated review. Downside: no PR-based review, harder to roll back a batch of changes.
|
||||
- **Option B**: Feature branches with PRs — standard team workflow, code review. Downside: rebase churn for a solo developer, PR overhead with no reviewer.
|
||||
- **Option C**: Feature branches without PRs (merge to main locally) — branch isolation without review overhead. Downside: still has merge conflicts, branches diverge.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Commit every 20-30 minutes with conventional commit prefixes (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`).
|
||||
- Pre-commit hook: vitest + CodeScene health check.
|
||||
- Pre-push hook: same + Playwright smoke tests.
|
||||
- No `--no-verify` ever — the hooks are the quality gate.
|
||||
- Reverting changes requires `git revert` (not force push).
|
||||
- Re-evaluation trigger: if a second developer joins and needs code review.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0022"
|
||||
title: "BlockNote as the rich text editor"
|
||||
status: active
|
||||
date: 2026-02-15
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs a rich text editor that can render markdown with YAML frontmatter, support custom inline content types (wikilinks), and provide a modern editing experience. The editor must handle the markdown-to-blocks-to-markdown round-trip without data loss.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use BlockNote as the primary rich text editor, with CodeMirror 6 as an alternative raw editing mode. Custom wikilink inline content is defined via `createReactInlineContentSpec`. Markdown round-tripping uses a pre/post-processing pipeline with placeholder tokens for wikilinks.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): BlockNote + CodeMirror 6 raw mode — BlockNote provides modern block-based editing, CodeMirror gives power users direct markdown access. Downside: wikilink round-tripping requires custom preprocessing pipeline.
|
||||
- **Option B**: ProseMirror directly — maximum control. Downside: much more boilerplate, no block-level abstractions, harder to maintain.
|
||||
- **Option C**: CodeMirror only (no rich text) — simplest, no round-trip issues. Downside: poor UX for non-technical users, no inline previews.
|
||||
- **Option D**: Monaco Editor — rich features, VS Code-like. Downside: heavy, designed for code not prose, no block-level structure.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Custom wikilink type defined in `editorSchema.tsx` via `createReactInlineContentSpec`.
|
||||
- Markdown-to-BlockNote pipeline: `splitFrontmatter()` → `preProcessWikilinks()` → `tryParseMarkdownToBlocks()` → `injectWikilinks()`.
|
||||
- BlockNote-to-Markdown pipeline: `blocksToMarkdownLossy()` → `postProcessWikilinks()` → prepend frontmatter.
|
||||
- Placeholder tokens use `‹` and `›` (U+2039/U+203A) to avoid colliding with markdown syntax.
|
||||
- Raw editor (CodeMirror 6) toggled via Cmd+K → "Raw Editor" or breadcrumb bar button.
|
||||
- The H1 block is hidden via CSS in favor of a dedicated `TitleField` component.
|
||||
- Re-evaluation trigger: if BlockNote's markdown round-tripping degrades or a better block editor emerges.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0023"
|
||||
title: "Repair Vault auto-bootstrap pattern"
|
||||
status: active
|
||||
date: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As Laputa adds features that depend on vault files (type definitions, config files, agents), users with existing vaults would miss these files. Manually creating them is error-prone. Features must work on both new and existing vaults without user intervention.
|
||||
|
||||
## Decision
|
||||
|
||||
**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). All bootstrap functions are registered with the central `Cmd+K → "Repair Vault"` command for manual re-creation.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Auto-bootstrap on vault open + manual Repair Vault command — works for new and existing vaults, idempotent, no user action needed. Downside: vault may accumulate files the user didn't explicitly create.
|
||||
- **Option B**: Require users to run a setup wizard — explicit, user-controlled. Downside: friction, users forget, new features don't work until setup is run.
|
||||
- **Option C**: Store defaults in app bundle, not vault — no vault files created. Downside: breaks the "vault as source of truth" principle, custom configs can't override defaults.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Type definitions (`type/project.md`, etc.) are seeded on vault open if missing.
|
||||
- Config files (`config/agents.md`, etc.) are seeded on vault open if missing.
|
||||
- `Repair Vault` command (Cmd+K) re-creates all expected files — useful after manual deletion or vault corruption.
|
||||
- All bootstrap operations are silent and idempotent — running twice has no effect.
|
||||
- `getting_started.rs` creates the Getting Started demo vault with all expected structure.
|
||||
- The `vault_health_check` command detects missing or misconfigured vault files.
|
||||
- Re-evaluation trigger: if the number of auto-created files becomes excessive or confusing for users.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0024"
|
||||
title: "Vault cache stored outside vault directory"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The vault cache was originally stored as `.laputa-cache.json` inside the vault directory. This caused problems: the cache file appeared in git status, polluted the user's repo, and could be accidentally committed. It also confused vault scanning (the cache file was itself a file in the vault).
|
||||
|
||||
## Decision
|
||||
|
||||
**Store the vault cache at `~/.laputa/cache/<vault-hash>.json`, outside the vault directory. The vault path is hashed (via `DefaultHasher`) to produce a deterministic filename. Legacy cache files inside the vault are auto-migrated and deleted on first run.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): External cache at `~/.laputa/cache/` — never pollutes the vault, no git issues, deterministic filename from vault path hash. Downside: separate cleanup needed if vault is deleted.
|
||||
- **Option B**: Cache inside vault with `.gitignore` — simpler, travels with the vault. Downside: .gitignore can be overridden, users may not have one, still appears in file listings.
|
||||
- **Option C**: No persistent cache (in-memory only) — simplest, no file management. Downside: full rescan on every app launch, slow for large vaults.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Cache path: `~/.laputa/cache/<vault-hash>.json` (e.g., `~/.laputa/cache/12345678.json`).
|
||||
- Writes are atomic: write to `.tmp` then rename.
|
||||
- Legacy `.laputa-cache.json` files inside the vault are auto-migrated and deleted.
|
||||
- `reload_vault` command deletes the cache file before rescanning.
|
||||
- The `.laputa/` directory also stores other app data (future: vault metadata, indexes).
|
||||
- Re-evaluation trigger: if vaults need to be portable between machines (cache would need to travel with the vault or be regenerated).
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0025"
|
||||
title: "type: as canonical field (replacing Is A:)"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The entity type field was originally stored as `Is A:` in frontmatter (e.g., `Is A: Project`), following a natural-language naming convention. This caused problems: the space and colon made it awkward to parse, `is_a` was used internally as the snake_case variant, and `type:` is the standard YAML convention for metadata classification. The field name also confused AI agents that expected standard YAML conventions.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use `type:` as the primary frontmatter field for entity types (e.g., `type: Project`). The legacy `Is A:` field is accepted as an alias for backward compatibility but new notes always use `type:`.** The internal TypeScript/Rust property remains `isA` for backward compatibility.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): `type:` as canonical with `Is A:` as legacy alias — clean, standard YAML convention, AI-readable. Downside: must maintain backward compatibility with existing vaults.
|
||||
- **Option B**: Keep `Is A:` as canonical — no migration needed. Downside: non-standard, awkward parsing, confusing for AI agents.
|
||||
- **Option C**: `kind:` or `category:` — avoids potential YAML type conflicts. Downside: less intuitive, still requires migration from `Is A:`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New notes use `type: Project` (not `Is A: Project`).
|
||||
- The Rust parser checks `type:` first, falls back to `Is A:` for legacy notes.
|
||||
- `VaultEntry.isA` property name kept for internal backward compatibility.
|
||||
- Type documents in `type/` folder use `type: Type` in their own frontmatter.
|
||||
- Repair Vault migrates legacy `Is A:` fields to `type:` when run.
|
||||
- Re-evaluation trigger: if YAML reserved word `type` causes parsing issues (not observed so far).
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0026"
|
||||
title: "Props-down callbacks-up (no global state management)"
|
||||
status: superseded
|
||||
date: 2026-02-15
|
||||
superseded_by: "0115"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
React apps commonly adopt global state management libraries (Redux, Zustand, Jotai, Context) to share state across components. For Laputa, the component tree is relatively shallow (App → panels → sub-components), and the data flow is predictable. Adding a state management library would increase complexity without proportional benefit.
|
||||
|
||||
## Decision
|
||||
|
||||
**No global state management (no Redux, no Context for data). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, `onCloseTab`, etc.). Local state uses `useState`/`useReducer`.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Props-down, callbacks-up — simple, predictable data flow, easy to trace state changes, no library dependency. Downside: prop drilling through deep trees, verbose parent components.
|
||||
- **Option B**: Redux/Zustand global store — centralized state, easy cross-component access. Downside: boilerplate, indirection, harder to trace state changes, over-engineering for a single-window app.
|
||||
- **Option C**: React Context for shared state — built-in, no library. Downside: re-renders on any context value change, performance issues with large state objects.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `App.tsx` is the state orchestrator — it holds vault entries, active note, sidebar selection, and all top-level state.
|
||||
- Components receive data and callbacks as props — no `useContext` for data access.
|
||||
- Hooks (`useVaultLoader`, `useNoteActions`, `useTabManagement`, etc.) encapsulate state logic but return values consumed by `App.tsx`.
|
||||
- Prop drilling is mitigated by composing hooks and keeping the component tree shallow.
|
||||
- Components are easy to test in isolation (just pass props).
|
||||
- Re-evaluation trigger: if the component tree deepens significantly or cross-cutting state becomes unmanageable with props.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0027"
|
||||
title: "Dual AI architecture (API chat + CLI agent)"
|
||||
status: superseded
|
||||
superseded_by: "0028"
|
||||
date: 2026-03-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs two distinct AI interaction modes: a lightweight chat for quick questions about the current note (no tool access, fast responses), and a full agent that can search, read, create, and modify vault notes via MCP tools. These have fundamentally different requirements — the chat needs low latency and simple streaming, while the agent needs tool calling, conversation state, and MCP integration.
|
||||
|
||||
## Decision
|
||||
|
||||
**Maintain two separate AI interfaces: AI Chat (AIChatPanel) uses the Anthropic API directly via Rust for simple streaming responses. AI Agent (AiPanel) spawns Claude CLI as a subprocess with MCP vault integration for full tool access.** Both share a context builder (`ai-context.ts`) that provides the active note and linked entries.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Dual architecture — optimized for each use case. Chat is fast and simple; agent is powerful with tool access. Downside: two codepaths to maintain.
|
||||
- **Option B**: Single agent for both — always use Claude CLI. Downside: overkill for simple questions, slower startup, unnecessary tool overhead.
|
||||
- **Option C**: Single API-based chat with manual tool calling — unified codebase. Downside: complex tool-calling loop implementation, no MCP integration.
|
||||
|
||||
## Consequences
|
||||
|
||||
- AI Chat: `AIChatPanel` + `useAIChat` hook → Rust `ai_chat` command → Anthropic API. Default model: Haiku 3.5 (fast, cheap).
|
||||
- AI Agent: `AiPanel` + `useAiAgent` hook → Rust `claude_cli.rs` → Claude CLI subprocess with MCP config.
|
||||
- Both panels share a toggle in the breadcrumb bar (Sparkle icon).
|
||||
- Context builder (`ai-context.ts`) provides structured JSON with active note, linked notes, open tabs, vault metadata.
|
||||
- Token budget: 60% of 180k context limit (~108k tokens max).
|
||||
- Chat requires an Anthropic API key in settings; agent uses Claude CLI's own authentication.
|
||||
- Re-evaluation trigger: if Anthropic releases an SDK that handles both simple chat and tool calling efficiently.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0028"
|
||||
title: "CLI agent only — no direct Anthropic API key"
|
||||
status: active
|
||||
date: 2026-03-29
|
||||
supersedes: "0027"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0027 introduced a dual AI architecture: a lightweight API-based chat (AIChatPanel) using the Anthropic API directly, and a full CLI agent (AiPanel) spawning Claude CLI as a subprocess with MCP tool access. In practice, the API chat was never shipped to users — the CLI agent covered all use cases and provided a superior experience through tool access and MCP integration. Maintaining two codepaths added complexity, and requiring users to manage an Anthropic API key created friction.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove the direct Anthropic API integration entirely. AI is available exclusively via CLI agent subprocesses (Claude Code, and in the future Codex or other CLI agents).** No API key field in settings. The CLI agent authenticates via its own mechanism (e.g. `claude` CLI login).
|
||||
|
||||
Removed:
|
||||
- `AIChatPanel` component, `useAIChat` hook
|
||||
- Rust `ai_chat` command and `ai_chat.rs` module
|
||||
- `anthropic_key` field from Settings (Rust and TypeScript)
|
||||
- Vite dev-server Anthropic API proxy (`aiChatProxyPlugin`, `aiAgentProxyPlugin`)
|
||||
|
||||
Kept:
|
||||
- `AiPanel` + `useAiAgent` — Claude CLI subprocess with MCP vault integration
|
||||
- Shared utilities in `ai-chat.ts` (`trimHistory`, `formatMessageWithHistory`, `streamClaudeChat`, etc.)
|
||||
- `Cmd+I` keyboard shortcut and menu item for toggling the AI panel
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Remove API chat, keep CLI agent only. Simplifies codebase, removes API key management, single codepath.
|
||||
- **Option B**: Keep both but hide API chat behind feature flag. Adds dead code weight without benefit.
|
||||
- **Option C**: Replace CLI agent with API chat + manual tool calling. Loses MCP integration and Claude CLI features.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users no longer need to obtain or manage an Anthropic API key
|
||||
- Existing saved API keys are silently ignored (the field no longer exists in the Settings struct; serde skips unknown fields on deserialization)
|
||||
- Future CLI agents (Codex, etc.) can plug into the same `AiPanel` architecture
|
||||
- If a lightweight chat mode is needed later, it should be built as a CLI agent mode, not a separate API integration
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0029"
|
||||
title: "Domain command builder pattern for useCommandRegistry"
|
||||
status: active
|
||||
date: 2026-03-30
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`useCommandRegistry` was a 224-line "brain method" (CodeScene hotspot) that defined all command palette commands inline: navigation, note actions, git operations, view toggles, settings, type management, and filter controls. This monolithic structure scored 39 on CodeScene's complexity scale (target: ≤9.5 for hotspots), making it increasingly hard to add new commands without touching the central file.
|
||||
|
||||
## Decision
|
||||
|
||||
**Split command definitions into focused domain modules under `src/hooks/commands/`, each exporting a `build*Commands(config)` factory function. `useCommandRegistry` becomes a thin assembler that calls each builder and merges the results.** Domain modules: `navigationCommands`, `noteCommands`, `gitCommands`, `viewCommands`, `settingsCommands`, `typeCommands`, `filterCommands`. Shared types live in `commands/types.ts`; public API re-exported from `commands/index.ts`.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Domain builder modules — each module owns its command shape and receives typed config. `useCommandRegistry` is pure assembly. All new files score 9.58–10.0. Downside: more files to navigate.
|
||||
- **Option B**: Split by file but keep one large hook calling sub-hooks — sub-hooks still need shared state passed down, similar coupling. No real complexity win.
|
||||
- **Option C**: Register commands imperatively via a global registry — decouples callers entirely. Downside: harder to trace, no TypeScript inference at the registration site, over-engineering for current scale.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Adding a new command means editing the relevant domain module (e.g. `noteCommands.ts`) only, not touching the assembler.
|
||||
- Each domain module receives only the config it needs — explicit, typed interface, no hook dependency.
|
||||
- `useCommandRegistry` reduced from 224 lines to a thin assembler.
|
||||
- Pattern is consistent with the Rust commands/ module split (ADR-0030).
|
||||
- Re-evaluation trigger: if command count grows to the point where the assembler itself becomes a complexity hotspot.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0030"
|
||||
title: "Rust commands/ module split by domain"
|
||||
status: active
|
||||
date: 2026-03-30
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`src-tauri/src/commands.rs` grew to 937 lines as Tauri command handlers accumulated for vault CRUD, git/GitHub sync, AI, system, and window operations. All commands shared a single file with no domain separation, making it hard to navigate, review, and extend. The file was a CodeScene hotspot dragging down overall code health.
|
||||
|
||||
## Decision
|
||||
|
||||
**Replace `commands.rs` with a `commands/` module split by domain: `vault.rs`, `git.rs`, `github.rs`, `ai.rs`, `system.rs`, and `mod.rs` (shared utilities + re-exports).** Each file owns the Tauri command handlers for its domain and the `#[cfg(desktop)]` / `#[cfg(mobile)]` stubs for platform-conditional availability. `mod.rs` is kept thin (≤100 lines) with no command logic — only re-exports and shared helpers (`expand_tilde`, `parse_build_label`).
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Domain-based module split — mirrors the TypeScript `hooks/commands/` pattern (ADR-0029). Each file is independently reviewable and scores well on code health. Downside: more files to navigate.
|
||||
- **Option B**: Split by platform (`desktop.rs`, `mobile.rs`) — aligns with `#[cfg(...)]` guards but mixes domain concerns. Harder to find a specific command.
|
||||
- **Option C**: Keep monolith but add section comments — zero file-count cost, but doesn't solve complexity or reviewability.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `github.rs` separates GitHub OAuth/API commands from git sync commands (`git.rs`), matching the underlying Rust module split (`github/` vs `git/`).
|
||||
- Platform stubs (`#[cfg(mobile)]` error returns) live alongside the desktop implementation in the same domain file.
|
||||
- `mod.rs` re-exports all command functions so `lib.rs` `invoke_handler!` registration is unchanged.
|
||||
- New Tauri commands go into the appropriate domain file; if no domain fits, create a new one rather than putting it in `mod.rs`.
|
||||
- Re-evaluation trigger: if a single domain file (e.g. `vault.rs`) itself grows beyond ~300 lines and becomes a hotspot.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0031"
|
||||
title: "Full App instance for secondary note windows"
|
||||
status: active
|
||||
date: 2026-03-31
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa supports opening a note in a secondary window ("Open in New Window"). The original implementation used a dedicated `NoteWindow` component — a thin shell that rendered only the editor, duplicating some App-level logic (vault loading, settings, keyboard shortcuts) in a simplified but diverging form. As the main App gained features (properties editing, zoom, command palette, keyboard shortcuts), the `NoteWindow` shell fell behind, requiring ongoing maintenance to keep parity.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove `NoteWindow` and render the full `App` component in secondary note windows.** The window type is detected at startup via URL query parameters (`?window=note&path=...&vault=...`). When in note-window mode, the App initialises with panels hidden (sidebar collapsed, inspector collapsed) and auto-opens the target note once vault entries load. The window title is kept in sync with the active note title via the Tauri window API.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Keep `NoteWindow` shell** (status quo): lower initial bundle weight per window, but divergence grows with every main-App feature. Rejected — maintenance cost dominates.
|
||||
- **Full `App` instance with URL-param mode** (chosen): complete feature parity for free; single code path for all window types. Trade-off: slightly heavier startup for secondary windows (full vault load), acceptable given local filesystem speed.
|
||||
- **IPC-driven secondary window (no vault reload)**: secondary window subscribes to primary window's vault state via Tauri events. Maximum efficiency, avoids double vault reads. Deferred — requires significant IPC plumbing; can be layered on top later without changing the rendering model.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Removes ~163 lines (`NoteWindow.tsx` deleted entirely)
|
||||
- Secondary note windows get full feature parity: all keyboard shortcuts, properties panel, zoom, command palette, diff mode, raw editor
|
||||
- `useLayoutPanels` gains an `initialInspectorCollapsed` option to support the hidden-panel initial state
|
||||
- A new `src/utils/windowMode.ts` utility encapsulates URL-param detection — single source of truth for window-type logic
|
||||
- Vault is loaded independently in each note window (no shared state with the main window); writes go to the same filesystem so eventual consistency is maintained via file-watching
|
||||
- Triggers re-evaluation if: multiple simultaneous note windows cause measurable vault-read contention, or if IPC-driven shared-state windows become a product requirement
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0032"
|
||||
title: 0032 Status Bar For Git Actions
|
||||
status: active
|
||||
date: 2026-03-31
|
||||
---
|
||||
[[Subfolder scanning and folder tree navigation]]
|
||||
|
||||
## Context
|
||||
|
||||
The Laputa sidebar originally surfaced git-related affordances — a "Changes" nav item (visible when modified files > 0), a "Pulse" nav item, and a "Commit & Push" button — alongside the note-type navigation filters and sections. This mixed two concerns in the sidebar: **navigation** (where to go) and **git status / actions** (what changed, what to do). As the sidebar grew, the git items created visual noise and made the nav hierarchy harder to scan.
|
||||
|
||||
## Decision
|
||||
|
||||
**Move Changes, Pulse, and Commit & Push out of the sidebar and into the bottom status bar.** The status bar shows a GitDiff icon with an orange count badge for modified files; a Pulse icon sits next to it. Commit & Push is accessible via an icon button beside the Changes indicator. The sidebar now contains only navigation items (filters and type sections).
|
||||
|
||||
## Options considered
|
||||
|
||||
* **Keep git items in sidebar** (status quo): familiar placement, visible at all times. Rejected — mixes navigation and action concerns; sidebar becomes harder to scan.
|
||||
* **Status bar** (chosen): consistent with app conventions (build number, sync status, vault switcher already live there); persistent but unobtrusive; follows macOS app patterns where status/action items live at window bottom.
|
||||
* **Toolbar / breadcrumb bar**: would require a new chrome layer or polluting the per-note breadcrumb with global git state. Rejected.
|
||||
|
||||
## Consequences
|
||||
|
||||
* Sidebar props `modifiedCount`, `onCommitPush`, `isGitVault` removed; sidebar renders navigation-only
|
||||
* `StatusBar` gains `onClickPending`, `onClickPulse`, `onCommitPush`, `isGitVault` props
|
||||
* Sidebar tests for Changes/Pulse/Commit button removed; StatusBar tests extended
|
||||
* Users find Commit & Push in the status bar (same location as sync indicators) rather than bottom of sidebar — small discoverability change, offset by status bar being always visible regardless of sidebar collapsed state
|
||||
* Triggers re-evaluation if: user research shows git actions are hard to discover in the status bar
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0033"
|
||||
title: "Subfolder scanning and folder tree navigation"
|
||||
status: active
|
||||
date: 2026-03-31
|
||||
---
|
||||
## Context
|
||||
|
||||
[[0032 Status Bar For Git Actions]]
|
||||
|
||||
Supersedes the scanning constraint in [ADR-0006](0006-flat-vault-structure.md) which limited vault indexing to root-level `.md` files plus protected folders (`attachments/`, `assets/`).
|
||||
|
||||
Users with folder-based workflows (PARA, Zettelkasten with folders, project directories) could not see or filter notes by directory. The vault scanner silently ignored all subdirectory `.md` files, making Laputa unsuitable for vaults with any folder structure.
|
||||
|
||||
## Decision
|
||||
|
||||
**Extend the Rust vault scanner to index **`.md`** files in all visible subdirectories, and expose the vault's folder tree via a new **`list_vault_folders`** Tauri command so the sidebar can render a collapsible FOLDERS section.**
|
||||
|
||||
Hidden directories (names starting with `.`, plus `.git` and `.laputa`) are excluded from both scanning and the folder tree.
|
||||
|
||||
## Options considered
|
||||
|
||||
* **Option A** (chosen): Scan all subdirectories with `walkdir`, expose separate `list_vault_folders` command — simple, no schema changes to VaultEntry, folder tree is lightweight and independent of the entry cache.
|
||||
* **Option B**: Add a `folder` field to VaultEntry and derive the tree on the frontend — couples folder metadata to the entry cache, complicates cache invalidation when folders are created/deleted without file changes.
|
||||
* **Option C**: Keep flat scanning, add a "virtual folders" feature that groups by path prefix from frontmatter — doesn't solve the core problem of missing notes in subdirectories.
|
||||
|
||||
## Consequences
|
||||
|
||||
* All `.md` files in the vault are now indexed regardless of depth — vaults with many non-note `.md` files (e.g. node_modules) will see spurious entries. Mitigation: hidden directories are already excluded; users can add a `.laputaignore` in the future if needed.
|
||||
* The git-based cache in `cache.rs` already uses `walkdir` for change detection, so this change aligns scanning with caching.
|
||||
* `SidebarSelection` gains a new `{ kind: 'folder'; path: string }` variant — all exhaustive switches on selection kind must handle it.
|
||||
* ADR-0006's "flat vault" principle is relaxed: notes can now live in subdirectories. Type definitions still live in `type/` at the root.
|
||||
* Re-evaluate if users request recursive folder filtering (currently only direct children are shown when a folder is selected).
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0034"
|
||||
title: "Git repo required — blocking modal enforces vault prerequisite"
|
||||
status: active
|
||||
date: 2026-04-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0014 (git-based vault cache) and ADR-0021 (push-to-main workflow) both assume the vault is a git repository, but neither codified it as a hard enforcement. In practice, opening a non-git folder silently degraded: the cache couldn't compute a commit hash, Pulse/Changes were empty, and commit/push commands failed. The failure mode was invisible to users.
|
||||
|
||||
## Decision
|
||||
|
||||
**When the app opens a vault that has no `.git` directory, a blocking modal prevents all app use until the user either initialises a git repository (git init + initial commit, offered as a one-click action) or selects a different vault. The check is performed by a new `is_git_repo` Tauri command. In browser/dev mode, the check fails open (modal is skipped).**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Hard block via modal on vault open — unambiguous, prevents silent failures, surfaces the fix immediately. Downside: breaks existing workflows for users with non-git vaults; requires a clear escape hatch (choose different vault).
|
||||
- **Option B**: Soft warning banner, allow using the app without git — avoids blocking users, but silent failures persist for Pulse/Changes/commit features.
|
||||
- **Option C**: Auto-init git on vault open without asking — less friction, but surprising; user may not want their vault in git.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Git is now a first-class prerequisite for Laputa vaults, not just implied by the cache strategy.
|
||||
- The `is_git_repo` command is intentionally lightweight (checks for `.git` existence only; does not validate remote or commit history).
|
||||
- The modal offers `git init` + an initial commit as a one-click path, lowering the barrier for new users.
|
||||
- Browser mode bypasses the check so dev/Storybook workflows are unaffected.
|
||||
- Re-evaluate if Laputa needs to support non-git vaults (e.g., iCloud-only, shared network drive); at that point ADR-0014 would also need revisiting.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0035"
|
||||
title: "Path-suffix wikilink resolution for subfolder vaults"
|
||||
status: active
|
||||
date: 2026-04-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0006 stated that wikilink resolution was "simplified to multi-pass title/filename matching — no path-based matching needed" because the vault was flat. ADR-0033 relaxed the flat-vault constraint by adding subfolder scanning. As a result, wikilinks like `[[docs/adr/0031-foo]]` or `[[adr/0031-foo]]` could not resolve to entries in subdirectories: the resolver only matched on `title` and `filename` stem, never on the vault-relative path.
|
||||
|
||||
The backlink detection in the Inspector also used a hardcoded `/Laputa/` path regex, which was wrong for any vault that isn't named "Laputa".
|
||||
|
||||
## Decision
|
||||
|
||||
**Add path-suffix matching as Pass 1 of wikilink resolution: a link target resolves to a `VaultEntry` if the entry's vault-relative path ends with the link string (with or without `.md`). Filename-stem matching (the previous Pass 1) becomes Pass 2. Inspector backlinks replace the hardcoded `/Laputa/` regex with a generic `targetMatchesEntry` path-suffix helper. Autocomplete pre-filter also matches against the full vault-relative path so subfolder names surface results.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Path-suffix as Pass 1, then filename match as Pass 2 — consistent with how Obsidian resolves links in multi-folder vaults, zero config. Downside: if two notes share the same filename in different folders, only the first (path-suffix) match wins.
|
||||
- **Option B**: Strict full-path matching only (disable title-stem resolution) — unambiguous, but breaks the majority of existing short-form `[[note-title]]` links.
|
||||
- **Option C**: Keep title-only matching, require full paths for subfolder notes — backwards-compatible, but forces users to always type full paths for subfolders, defeating the purpose of wikilinks.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Supersedes the "no path-based matching needed" clause from ADR-0006 (that assumption was contingent on the flat vault invariant, which ADR-0033 relaxed).
|
||||
- `relativePathStem` utility added in `wikilink.ts` to extract the vault-relative path stem from a full `VaultEntry`.
|
||||
- The Inspector's `targetMatchesEntry` helper is now the canonical way to test if a link resolves to an entry — use it everywhere instead of ad-hoc regex.
|
||||
- Wikilink autocomplete suggestions now surface notes in subfolders when users type a folder prefix (e.g. `[[adr/`).
|
||||
- Re-evaluate if path-suffix ambiguity (two files with the same name in different folders) becomes a user complaint.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0036"
|
||||
title: "External rename detection via git diff on focus regain"
|
||||
status: active
|
||||
date: 2026-04-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa handles in-app renames (rename.rs) and propagates wikilink updates across the vault. But notes can also be renamed externally — from Finder, another editor, or a git operation (e.g., `git mv`). In those cases, the app had no way to detect that a rename had occurred, leaving wikilinks broken and the vault inconsistent.
|
||||
|
||||
The app already uses git for the cache (ADR-0014) and requires git as a vault prerequisite (ADR-0034), making git diff a natural and already-available detection mechanism.
|
||||
|
||||
## Decision
|
||||
|
||||
**When the app window regains focus, run `git diff --diff-filter=R --name-status HEAD` to detect file renames that occurred since the last committed HEAD. If any renamed `.md` files are found, show a non-blocking banner ("X file(s) renamed — update wikilinks?"). Accepting triggers the existing vault-wide wikilink replacement logic (reused from rename.rs). Ignoring dismisses the banner without changes. New Tauri commands: `detect_renames` and `update_wikilinks_for_renames`.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Git diff on focus regain, non-blocking banner — uses existing infrastructure, non-disruptive, user retains control. Downside: only detects renames that are staged/committed; uncommitted renames via `git mv` are captured, but renames done purely in Finder (no git involvement) are not.
|
||||
- **Option B**: `FSEvents` / file-system watcher for rename events — catches all renames regardless of git. Downside: significantly more complex, requires Rust async machinery, false positives from editor temp files, and this feature is already planned as a separate enhancement.
|
||||
- **Option C**: Scan for broken wikilinks on focus — correct but O(n) and noisy; doesn't tell us the new filename.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Git's rename detection (`--diff-filter=R`) requires the rename to be git-tracked (either staged or committed); renames that happen outside git knowledge are not detected by this mechanism.
|
||||
- The on-focus check runs `git diff HEAD` which is fast but adds a small shell invocation overhead each time the window activates. This is acceptable for typical vault sizes.
|
||||
- `rename.rs` is now shared between in-app renames and external rename recovery — the replacement logic is the canonical entry point for wikilink bulk updates.
|
||||
- The banner is non-blocking and "Ignore" is always available — the user never loses work.
|
||||
- Re-evaluate if FS-level rename detection (outside git) becomes a priority; at that point this mechanism would be a fallback, not the primary strategy.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0037"
|
||||
title: "Language-based markdown syntax highlighting in raw editor"
|
||||
status: active
|
||||
date: 2026-04-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The raw editor (CodeMirror 6, introduced in ADR-0022) initially had a custom `frontmatterHighlight` extension that used regex-based decoration for YAML frontmatter and headings. Markdown body content had no syntax highlighting at all, making the raw editor feel like a plain textarea despite being a full CodeMirror instance.
|
||||
|
||||
Extending the custom regex-based approach to cover all markdown syntax (bold, italic, links, lists, blockquotes, code) would have been brittle and hard to maintain.
|
||||
|
||||
## Decision
|
||||
|
||||
**Replace the custom heading decoration in `frontmatterHighlight.ts` with `@codemirror/lang-markdown` (the official CodeMirror language package). A custom `HighlightStyle` maps CodeMirror highlight tags to visual styles for headings, bold, italic, strikethrough, links, lists, blockquotes, and inline code. The frontmatter YAML plugin is retained for YAML-specific colouring but its heading decoration is removed in favour of the language parser.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): `@codemirror/lang-markdown` with custom HighlightStyle — uses the official, maintained language parser; future highlight rules are one CSS declaration. Downside: adds a new npm dependency; the custom frontmatter plugin must be kept separately.
|
||||
- **Option B**: Extend the custom regex plugin to cover all markdown — no new dependency. Downside: regex-based tokenisation is fragile (e.g., nested formatting), already proving hard to maintain after the heading/frontmatter overlap bug.
|
||||
- **Option C**: Switch to a markdown-aware editor (e.g., Milkdown, Monaco) — full-featured. Downside: major migration, breaks the dual-editor architecture in ADR-0022, significant scope.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `@codemirror/lang-markdown` added to `package.json` — this is the only new runtime dependency introduced by this change.
|
||||
- `frontmatterHighlight.ts` is simplified (heading decoration removed); `markdownHighlight.ts` is the new extension responsible for body highlighting.
|
||||
- The two extensions are composed in `useCodeMirror.ts` — YAML frontmatter block is still styled by the custom plugin; everything else by the language parser.
|
||||
- Future syntax highlighting changes (e.g., task lists, tables) can be added by extending the `HighlightStyle` without modifying the parser.
|
||||
- Re-evaluate if `@codemirror/lang-markdown` conflicts with the custom frontmatter YAML handling as the editor evolves (e.g., if frontmatter block needs to be parsed as a code block rather than decorated text).
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0038"
|
||||
title: "Frontmatter-backed favorites with _favorite and _favorite_index"
|
||||
status: active
|
||||
date: 2026-04-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Users want to pin frequently-accessed notes to a dedicated FAVORITES section in the sidebar for quick navigation. The app needs a persistence mechanism for which notes are favorited and their display order.
|
||||
|
||||
## Decision
|
||||
|
||||
**Favorites are stored as two system properties in each note's YAML frontmatter: `_favorite: true` and `_favorite_index: <integer>`.**
|
||||
|
||||
- `_favorite`: boolean. Present and `true` = favorited. Absent = not favorited. Toggling off deletes the key entirely (no `_favorite: false`).
|
||||
- `_favorite_index`: integer. Controls display order in the FAVORITES sidebar section (lower = higher). Assigned automatically on favorite, updated on drag-to-reorder.
|
||||
- Both use the `_` prefix convention (ADR 0008) — they are system-owned and hidden from the Properties panel.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Frontmatter per-note (chosen)**: Each note carries its own favorite state. Portable across devices (synced via git). No separate metadata file. Cons: two extra frontmatter writes on reorder.
|
||||
- **Separate `.laputa/favorites.json` file**: Central list of favorite paths. Simpler reorder (one file write). Cons: not portable if `.laputa/` is gitignored; path references break on rename.
|
||||
- **SQLite/app-level metadata**: Fast queries. Cons: not synced via git; diverges from frontmatter-first data model established in ADR 0008.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Favorites survive vault sync via git — any client that reads frontmatter sees them.
|
||||
- Reorder writes `_favorite_index` to N files (one per affected note). Acceptable for typical favorites lists (< 20 items).
|
||||
- If `_favorite: true` exists but `_favorite_index` is absent, the note is appended to the end of the list.
|
||||
- Re-evaluate if favorites list exceeds ~50 items and reorder writes become a performance concern.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0039"
|
||||
title: "Use git history for note creation and modification dates"
|
||||
status: active
|
||||
date: 2026-04-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Filesystem metadata (`ctime`/`mtime`) is unreliable for a git-backed vault. After `git clone`, `git pull`, or iCloud sync, files appear "newly created" even when they have years of history. This causes incorrect sort ordering in the note list and wrong dates in the inspector panel.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use `git log` to determine the true creation and modification dates for notes.** A single batch `git log --format="COMMIT %aI" --name-only` command walks the full commit history and extracts:
|
||||
|
||||
- **modified_at** = author date of the most recent commit that touched the file
|
||||
- **created_at** = author date of the oldest commit that touched the file
|
||||
|
||||
The batch approach runs once per vault scan (not per-file), parsing the log output in a single linear pass. Results are stored in a `HashMap<String, GitDates>` keyed by vault-relative path and threaded through the existing `parse_md_file` / `scan_vault` / `scan_vault_cached` pipeline.
|
||||
|
||||
### Fallback to filesystem dates
|
||||
|
||||
- **Non-git vaults** (no `.git` directory): all notes use filesystem `mtime`/`ctime`.
|
||||
- **Uncommitted new files**: not in git log output, so filesystem dates are used automatically.
|
||||
- **Single-file reloads** (`reload_entry`): use filesystem dates since the file was just saved and the most accurate timestamp is the filesystem one.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Per-file `git log`**: Correct but O(n) subprocesses. Too slow for vaults with 500+ notes.
|
||||
- **Frontmatter dates** (e.g., `created: 2025-01-15`): Requires user discipline. Not automatic. Breaks when users forget to set them.
|
||||
- **Filesystem metadata** (current): Unreliable across clones, pulls, and cloud sync.
|
||||
- **Single batch `git log`** (chosen): One subprocess, O(n) parsing, correct dates for all committed files.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Note sort-by-created and sort-by-modified now reflect true git history, stable across clones and machines.
|
||||
- First vault scan runs one `git log` over the full history. For a vault with 1000 files and 500 commits, output is ~100KB and parses in <100ms.
|
||||
- Renamed files get `created_at` set to the rename commit date (not the original creation). Acceptable trade-off vs. the complexity of rename tracking.
|
||||
- `CACHE_VERSION` bumped from 9 to 10 to force a full rescan with git dates on upgrade.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0040"
|
||||
title: "Custom views as .yml files with client-side filter engine"
|
||||
status: active
|
||||
date: 2026-04-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Users want to save reusable filtered note lists (e.g., "Active Projects", "This Week's Events") as named sidebar items. These views need to persist across sessions, sync via git, and support arbitrary frontmatter conditions.
|
||||
|
||||
## Decision
|
||||
|
||||
**Custom views are stored as `.yml` files in `.laputa/views/` within the vault root.** Each file defines a named view with filter conditions, optional icon/color, and sort preferences.
|
||||
|
||||
### File format
|
||||
|
||||
```yaml
|
||||
name: Active Projects
|
||||
icon: rocket
|
||||
color: blue
|
||||
sort: "modified:desc"
|
||||
filters:
|
||||
all:
|
||||
- field: type
|
||||
op: equals
|
||||
value: Project
|
||||
- field: status
|
||||
op: not_equals
|
||||
value: done
|
||||
```
|
||||
|
||||
### Filter engine
|
||||
|
||||
Filters use a tree of AND/OR groups (`all`/`any`) containing conditions. Each condition specifies a `field`, `op` (operator), and optional `value`. Supported operators: `equals`, `not_equals`, `contains`, `not_contains`, `any_of`, `none_of`, `is_empty`, `is_not_empty`, `before`, `after`.
|
||||
|
||||
Field resolution: built-in fields (`type`, `status`, `title`, `archived`, `trashed`, `favorite`) map to VaultEntry struct fields. Unknown fields fall back to `entry.properties`, then `entry.relationships`.
|
||||
|
||||
Wikilink values like `[[target|Alias]]` are matched by stem (stripping brackets and pipe+alias).
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Rust backend** (`vault/views.rs`): YAML parsing via `serde_yaml`, filter evaluation, file CRUD. Three Tauri commands: `list_views`, `save_view_cmd`, `delete_view_cmd`.
|
||||
- **Frontend**: Client-side filter evaluation against the already-loaded `VaultEntry[]` array. The Rust `evaluate_view` exists for MCP/CLI access but is not the primary UI path.
|
||||
- **Sidebar**: VIEWS section between Favorites and Types, hidden when no views exist.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **SQLite views table**: Fast queries, but not portable via git and diverges from the file-first data model.
|
||||
- **Frontmatter on a special `.md` file**: Overloads the note format for a non-note concept.
|
||||
- **Standalone `.yml` files (chosen)**: Portable (synced via git), editable by hand or UI, naturally separated from note content.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New dependency: `serde_yaml` crate for YAML parsing.
|
||||
- `.laputa/views/` directory auto-created on first view save. Already excluded from vault scanning via `HIDDEN_DIRS`.
|
||||
- Views sync across devices via git. Conflicts resolved by standard git merge (YAML is line-based, merges well).
|
||||
- Sort persistence: changing sort while a view is selected writes `sort` back to the `.yml` file.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0041"
|
||||
title: "fileKind field — scan all vault files, not just markdown"
|
||||
status: active
|
||||
date: 2026-04-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa vaults often contain non-markdown files alongside notes: images, PDFs, YAML configs, JSON exports, scripts, etc. Previously the vault scanner only indexed `.md` files — all other files were invisible to the app. This made the Folder view incomplete: navigating a folder containing a `config.yml` or `photo.png` showed nothing, even though the file was physically there.
|
||||
|
||||
The need arose when adding a Folder tree view that is meant to mirror the actual filesystem structure. Users expect to see all files in a folder, as any file manager would show.
|
||||
|
||||
## Decision
|
||||
|
||||
**The vault scanner now indexes all files (not just `.md`). Every `VaultEntry` carries a `fileKind` field (`"markdown"`, `"text"`, or `"binary"`) that controls how the frontend renders and opens it.**
|
||||
|
||||
- **`"markdown"`**: full Laputa behavior — frontmatter parsing, BlockNote editor, title sync, type system.
|
||||
- **`"text"`**: filename as title, no frontmatter, opens in raw CodeMirror editor. Covers `.yml`, `.json`, `.ts`, `.py`, `.sh`, etc.
|
||||
- **`"binary"`**: filename as title, grayed out, non-clickable. Covers images, PDFs, binaries.
|
||||
- **Hidden files** (starting with `.`) are skipped regardless of extension.
|
||||
- **Non-folder views** (All Notes, type sections, Custom Views) still show only `"markdown"` entries.
|
||||
- **Folder view** shows all file kinds.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Single `VaultEntry` model with a `fileKind` discriminator. All files go through the same pipeline; rendering is gated by `fileKind`. Simple, incremental — existing code paths untouched for markdown files.
|
||||
- **Option B**: Separate data model for non-markdown files (e.g. `AssetEntry`). Cleaner type hierarchy, but requires duplicating list/filter/sort logic for two types across the codebase.
|
||||
- **Option C**: Only scan `.md` + explicitly listed extensions (e.g. `.yml`, `.json`). Simpler initial implementation, but requires ongoing maintenance of an allowlist and still misses user files. Abandoned in favor of a deny-list approach (only `.`-prefixed hidden files are excluded).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Non-markdown files are visible in Folder view — the app now behaves like a file manager in that context.
|
||||
- All views except Folder view continue to show only markdown files (the `isMarkdown` guard in `filterEntries`).
|
||||
- `countByFilter` / `countAllByFilter` exclude non-markdown entries to keep sidebar counters accurate.
|
||||
- The vault cache version was bumped to `11` to force a full rescan after this change.
|
||||
- Binary files have no click action — clicking does nothing (no editor opened).
|
||||
- Re-evaluation trigger: if users need to preview or edit binary files (e.g. images), a dedicated preview pane would need a separate ADR.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0042"
|
||||
title: "PostHog-based release channels and feature flags"
|
||||
status: active
|
||||
date: 2026-04-03
|
||||
supersedes: "0017"
|
||||
---
|
||||
## Context
|
||||
|
||||
ADR-0017 introduced canary/stable update channels with localStorage-based feature flags. This worked for local development but lacked remote flag management — promoting a feature from beta to stable required a code change and rebuild.
|
||||
|
||||
## Decision
|
||||
|
||||
**Replace localStorage feature flags with PostHog-based feature flags, evaluated per release channel (alpha/beta/stable). The release channel is a user-selectable setting; PostHog flag rules determine which features are visible for each channel.**
|
||||
|
||||
- **Alpha**: all features always enabled (no PostHog lookup needed, works offline)
|
||||
- **Beta**: sees features where the PostHog flag targets `release_channel = beta`
|
||||
- **Stable** (default): sees features where the PostHog flag targets `release_channel = stable`
|
||||
- Promotion = flipping a PostHog flag on the dashboard. Zero code changes, zero rebuilds.
|
||||
- `isFeatureEnabled(flagKey)` in `telemetry.ts` is the single evaluation point.
|
||||
- localStorage overrides (`ff_<name>`) still work for dev/QA testing (checked first).
|
||||
- Offline: PostHog caches flags in localStorage; alpha always works; first-launch-no-network falls back to hardcoded defaults.
|
||||
|
||||
## Options considered
|
||||
|
||||
* **Option A**: Keep localStorage-only flags (ADR-0017) — no server dependency, but no remote management.
|
||||
* **Option B** (chosen): PostHog feature flags — we already use PostHog for analytics, so no new dependency. Remote flag management, per-channel targeting, gradual rollouts via PostHog dashboard.
|
||||
* **Option C**: Dedicated feature flag service (LaunchDarkly, Unleash) — more powerful but adds a new vendor dependency.
|
||||
|
||||
## Consequences
|
||||
|
||||
* `release_channel` added to Settings (persisted via Tauri backend, not vault).
|
||||
* `useTelemetry` passes `release_channel` as a PostHog person property on identify.
|
||||
* `isFeatureEnabled()` checks channel → PostHog → hardcoded defaults.
|
||||
* `useFeatureFlag` hook updated to delegate to `isFeatureEnabled` (after localStorage override check).
|
||||
* ADR-0017 is superseded — the canary update channel remains, but feature gating moves from localStorage to PostHog.
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0042"
|
||||
title: "Trash auto-purge safety model"
|
||||
status: superseded
|
||||
date: 2026-04-05
|
||||
superseded_by: "0045"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The Trash view already shows a "Notes trashed more than 30 days ago will be permanently deleted" warning, but the app never actually enforces this. Users expect trashed notes to be cleaned up automatically after 30 days — if we advertise it, we must implement it.
|
||||
|
||||
This is one of the most dangerous operations in the app: a bug could cause irreversible data loss. The safety model must be explicit and conservative.
|
||||
|
||||
## Decision
|
||||
|
||||
**Auto-purge trashed notes older than 30 days on app launch and window focus (max once per hour), using OS trash (`trash::delete`) for soft-deletion, with mandatory 5-point safety validation per file and an audit log at `.laputa/purge.log`.**
|
||||
|
||||
### Safety checks (all must pass before deleting any file)
|
||||
|
||||
1. `_trashed: true` (or legacy aliases `Trashed`, `trashed`) is present in frontmatter and set to a truthy value
|
||||
2. `_trashed_at` (or legacy aliases `Trashed at`, `trashed_at`) is present and parseable as a date
|
||||
3. The parsed date is **strictly more than 30 days ago** (exactly 30 days = skip)
|
||||
4. The file exists on disk at the expected path
|
||||
5. The file's canonical path is inside the vault root (prevents path traversal)
|
||||
|
||||
If any check fails, the file is skipped with a warning log. The purge never aborts early — it processes all candidates independently.
|
||||
|
||||
### Deletion method
|
||||
|
||||
Use the `trash` crate (`trash::delete`) to move files to the OS trash (macOS Trash, Windows Recycle Bin) instead of `fs::remove_file`. This gives users a last-resort recovery path. If OS trash fails, fall back to `fs::remove_file` and log a warning.
|
||||
|
||||
### Trigger conditions
|
||||
|
||||
- On app launch (in `run_startup_tasks`)
|
||||
- On window focus (`WindowEvent::Focused(true)`) — throttled to max once per hour using a `Mutex<Instant>` timestamp
|
||||
|
||||
### Audit log
|
||||
|
||||
Every purge run appends to `.laputa/purge.log` with timestamp, files checked count, files purged count, and each purged file path. Users can inspect this file to audit what was deleted and when.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — OS trash via `trash` crate** (chosen): moves to OS trash, user can recover from Trash app. Adds a ~small dependency. Safe default.
|
||||
- **Option B — `fs::remove_file` (permanent)**: simpler, no dependency, but no recovery path. Too risky for an automatic background operation.
|
||||
- **Option C — Move to `.laputa/purged/` archive folder**: custom recovery mechanism, but clutters vault directory and users wouldn't know to look there.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users get the auto-cleanup behavior already advertised in the UI
|
||||
- Accidentally trashed notes have a second chance via OS Trash
|
||||
- The `trash` crate adds a platform-specific dependency (macOS: `NSFileManager`, Windows: `IFileOperation`, Linux: freedesktop spec)
|
||||
- The hourly throttle prevents excessive disk I/O on rapid focus/unfocus cycles
|
||||
- The purge log provides auditability but will grow over time (acceptable for a text log)
|
||||
- Re-evaluate if users report OS Trash filling up with vault files
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0043"
|
||||
title: "Reactive vault state: editor changes propagate immediately to all UI"
|
||||
status: active
|
||||
date: 2026-04-05
|
||||
---
|
||||
## Context
|
||||
|
||||
When a user edits frontmatter in the raw editor (or BlockNote preserves it), changes to metadata fields like `title`, `type`, `_favorite`, `_archived`, and `sidebar_label` must be reflected immediately across all UI components — sidebar sections, note list, breadcrumb bar, inspector, and tabs.
|
||||
|
||||
Previously, after `save_note_content`, only derived fields (`outgoingLinks`, `snippet`, `wordCount`) were updated in `vault.entries`. Frontmatter-derived fields were stale until a full vault reload.
|
||||
|
||||
## Decision
|
||||
|
||||
**All frontmatter changes are parsed in real-time and applied to `vault.entries` via `updateEntry()` during content editing, not after save.**
|
||||
|
||||
### How it works
|
||||
|
||||
1. **On every content change** (keystroke in raw editor, or BlockNote onChange), `useEditorSaveWithLinks.handleContentChange` is called.
|
||||
2. It invokes `contentToEntryPatch(content)` which parses frontmatter and maps known keys to `VaultEntry` fields.
|
||||
3. If the parsed patch differs from the previous one, `updateEntry(path, patch)` merges it into `vault.entries`.
|
||||
4. All UI components derive from `vault.entries` via React reactivity — they re-render automatically.
|
||||
|
||||
### Mapped fields
|
||||
|
||||
`contentToEntryPatch` maps these frontmatter keys to `VaultEntry` fields:
|
||||
|
||||
| Frontmatter key | VaultEntry field | Notes |
|
||||
|---|---|---|
|
||||
| `title` | `title` | |
|
||||
| `type` / `is_a` | `isA` | |
|
||||
| `status` | `status` | |
|
||||
| `_favorite` | `favorite` | |
|
||||
| `_favorite_index` | `favoriteIndex` | |
|
||||
| `_archived` / `archived` | `archived` | |
|
||||
| `_trashed` / `trashed` | `trashed` | |
|
||||
| `_organized` | `organized` | |
|
||||
| `color` | `color` | Type entries |
|
||||
| `icon` | `icon` | Type entries |
|
||||
| `order` | `order` | Type entries |
|
||||
| `sidebar_label` | `sidebarLabel` | Type entries |
|
||||
| `visible` | `visible` | Type entries |
|
||||
| `template` | `template` | Type entries |
|
||||
| `sort` | `sort` | Type entries |
|
||||
| `view` | `view` | Type entries |
|
||||
| `aliases` | `aliases` | |
|
||||
| `belongs_to` | `belongsTo` | |
|
||||
| `related_to` | `relatedTo` | |
|
||||
|
||||
### Inspector operations use a separate, more direct path
|
||||
|
||||
When the user edits frontmatter via the Inspector panel, `runFrontmatterAndApply` calls the Tauri command and immediately applies the result via `updateEntry()`. This path was already reactive before this ADR.
|
||||
|
||||
### View files (.yml)
|
||||
|
||||
View files are not markdown notes — they have no frontmatter delimiters. When a `.yml` file is saved, `onNotePersisted` triggers `reloadViews()` to refresh the sidebar view list.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Any new frontmatter key that should affect the UI must be added to `frontmatterToEntryPatch` and its delete counterpart.
|
||||
- Components must read note metadata from `vault.entries` (via props), never from local state that could diverge.
|
||||
- The `reload_vault_entry` Tauri command exists for full re-parsing from disk but is not needed in the normal editing flow — `contentToEntryPatch` handles it client-side.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0044"
|
||||
title: "H1 as primary title source — filename as stable identifier"
|
||||
status: active
|
||||
date: 2026-04-07
|
||||
supersedes: "0007"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0007 established that the `title:` frontmatter field is the source of truth for display titles, with filenames derived from it via slugification and kept in sync bidirectionally. This model had a key assumption: the user explicitly types a title before writing content.
|
||||
|
||||
In practice this created friction: new notes required a title upfront, the TitleField was always visible cluttering the editor, and the "title = filename slug" contract was fragile when users renamed files externally. The team wanted a more natural writing flow where you just start writing — like most text editors — and the title emerges from the document.
|
||||
|
||||
A pair of commits on 2026-04-06 (`377a3f8d`, `7daf6898`) implemented a fundamentally different model.
|
||||
|
||||
## Decision
|
||||
|
||||
**The first `# H1` heading in the note body is the canonical display title. The `title:` frontmatter field is legacy/backward-compat only. New notes are created with filename `untitled-{type}-{timestamp}.md` and no `title:` in frontmatter. On save, if the note has an H1, the file is auto-renamed to a slug derived from it (collision-safe with `-2`, `-3` suffixes).**
|
||||
|
||||
Title resolution priority (Rust `extract_title`):
|
||||
1. H1 on the first non-empty line of the body
|
||||
2. Frontmatter `title:` field (legacy, backward-compat)
|
||||
3. Slug-to-title derivation from filename stem
|
||||
|
||||
The `has_h1: bool` field on `VaultEntry` signals the frontend to hide `TitleField` and the icon picker when an H1 is present, since the H1 serves as the title surface.
|
||||
|
||||
The breadcrumb bar shows the **filename stem** (not display title) so users always know the actual file identifier.
|
||||
|
||||
Auto-rename (`auto_rename_untitled` Tauri command) fires on save for `untitled-*` files that gain an H1, converting them to a human-readable slug.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — H1 as primary title + auto-rename on save** (chosen): natural writing flow, filename eventually reflects content, TitleField hidden when H1 present. Downside: auto-rename can surprise users; breadcrumb must show filename to stay honest.
|
||||
- **Option B — Keep `title:` frontmatter as source of truth** (ADR-0007, now superseded): explicit, deterministic. Downside: forces upfront titling, TitleField always visible, friction for quick capture.
|
||||
- **Option C — UUID-based filenames, title only in H1**: filenames never change, no rename logic needed. Downside: vault unreadable in Finder/terminal, breaks the plain-files principle (ADR-0002).
|
||||
|
||||
## Consequences
|
||||
|
||||
- New notes start as `untitled-note-{timestamp}.md` — the vault may accumulate untitled files if users abandon drafts without writing an H1
|
||||
- `TitleField` component is hidden when `has_h1 = true`; icon picker is also hidden (icons only make sense on titled notes)
|
||||
- Frontmatter `title:` still parsed for backward-compat; existing vaults with explicit titles continue to work
|
||||
- Auto-rename on save introduces a file rename side-effect during editing — wikilinks pointing to the old filename may break until the rename propagates
|
||||
- The breadcrumb filename display makes the system more honest but slightly more technical for non-power users
|
||||
- Re-evaluate if users find auto-rename disorienting or if wikilink breakage during rename becomes a reliability concern
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0045"
|
||||
title: "Permanent delete with confirm modal — no Trash system"
|
||||
status: active
|
||||
date: 2026-04-07
|
||||
supersedes: "0042"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0042 designed a Trash auto-purge safety model (soft-delete with 30-day retention, OS trash via `trash` crate, audit log). This was built on top of a Trash system that treated deletion as a two-phase operation: move to trash → auto-purge after 30 days.
|
||||
|
||||
The Trash system was subsequently identified as unnecessary complexity: it required `trashed`/`trashedAt` frontmatter fields, sidebar filtering, editor banners, inspector components, dedicated smoke tests, and a `trash` crate dependency. The safety guarantee users actually need is a **confirmation prompt before irreversible action**, not a soft-delete buffer — especially given notes live in a git repo (vault git history is already a recovery mechanism per ADR-0034 and ADR-0014).
|
||||
|
||||
Commit `e581ad36` on 2026-04-06 removed the entire Trash system (123 files changed, ~3164 lines deleted).
|
||||
|
||||
## Decision
|
||||
|
||||
**Delete is permanent and immediate, gated only by a confirmation modal (`useDeleteActions`). Notes with `trashed: true` in existing vault frontmatter are treated as normal notes (the flag is ignored by the parser). The `trash` crate dependency is removed.**
|
||||
|
||||
The confirmation modal is the sole safety gate. No soft-delete, no Trash view, no auto-purge scheduler, no `.laputa/purge.log`.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — Permanent delete + confirm modal** (chosen): simple, honest, no hidden state. Git history provides recovery. Removes ~3000 lines of code and a platform-specific dependency. Downside: no in-app recovery path for users who don't know about git.
|
||||
- **Option B — OS Trash via `trash` crate** (ADR-0042, now superseded): soft-delete to OS Trash, user can recover from macOS Trash app. Downside: additional dependency, complex auto-purge scheduler, misleading "auto-purge" promise that was never actually implemented.
|
||||
- **Option C — `.laputa/deleted/` archive folder**: custom recovery mechanism inside vault. Downside: clutters vault, users wouldn't know to look there, still requires manual cleanup.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users who accidentally delete a note must recover from git history (`git checkout HEAD -- path/to/note.md`) — this is a power-user action
|
||||
- `trashed`/`trashedAt` frontmatter fields in existing vaults are silently ignored — no migration needed, no data loss
|
||||
- The `trash` crate is removed from `Cargo.toml` — build times improve marginally
|
||||
- Smoke tests for trash flows are deleted; delete-related test coverage is now purely the confirm modal behavior
|
||||
- The Trash view, sidebar filter, note banners, and bulk-trash actions are all gone — simpler UI surface
|
||||
- Re-evaluate if user feedback shows significant accidental deletion incidents, or if git-based recovery proves too inaccessible for non-technical users
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0046"
|
||||
title: "Starter vault cloned from GitHub at runtime — no bundled content"
|
||||
status: active
|
||||
date: 2026-04-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa ships an optional "Getting Started" vault to help new users understand types, properties, wikilinks, and relationships. Previously, all starter content (markdown files, view YAMLs) was stored inside the app repo under `getting-started-vault/` and written to disk via `create_getting_started_vault()`. This created friction: updating sample content required a new app release, the content grew stale quickly, and the bundled files added noise to the main repo.
|
||||
|
||||
## Decision
|
||||
|
||||
**The Getting Started vault is no longer bundled in the app repo. On first launch, if the user selects "Get started with a template", the app clones the public starter repo (`https://github.com/refactoringhq/laputa-getting-started.git`) into a user-chosen folder using the existing git clone infrastructure.**
|
||||
|
||||
- `getting_started.rs` now holds only the public repo URL constant and delegates to `clone_public_repo()`.
|
||||
- The `getting-started-vault/` directory has been removed from the app repo.
|
||||
- `create_getting_started_vault(targetPath)` takes an explicit target path (chosen via folder picker) instead of defaulting to Documents/Getting Started.
|
||||
- Clone failures show a user-friendly error with an inline "Retry download" button (`canRetryTemplate`, `retryCreateVault`).
|
||||
- A `clone_public_repo()` function was added to `github/clone.rs` to clone unauthenticated public repos without injecting OAuth tokens or configuring remote auth.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — Keep bundled content (status quo)**: Simple, works offline. Downside: content tied to app release cycle, repo noise, growing file count.
|
||||
- **Option B — Clone from GitHub at runtime (chosen)**: Content is always current; starter vault can be updated without an app release; removes ~25 markdown files + YAML from the main repo. Downside: requires network on first use; failure modes need UX handling (retry flow added).
|
||||
- **Option C — Download a zip archive**: Avoids a git clone, smaller payload. Downside: loses the clean git history in the cloned vault; adds a zip extraction code path.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New users need a network connection when selecting the template option. The empty vault and open-folder paths remain fully offline.
|
||||
- The starter repo (`laputa-getting-started`) becomes a separate maintenance artifact.
|
||||
- `LAPUTA_GETTING_STARTED_REPO_URL` env var allows overriding the URL in tests without hitting GitHub.
|
||||
- Onboarding UX now distinguishes three creation modes: `creatingAction: 'template' | 'empty' | null`, each with distinct button state and status copy.
|
||||
- Retry UX: `lastTemplatePath` is cached in `useOnboarding` so users can retry a failed clone to the same folder without re-picking it.
|
||||
- Re-evaluation trigger: if offline-first support becomes a priority, consider bundling a minimal vault again or shipping a fallback zip.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0047"
|
||||
title: "Regex mode for view filter conditions"
|
||||
status: active
|
||||
date: 2026-04-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The view filter engine (ADR 0040) supports operators like `contains`, `equals`, `not_contains`, `not_equals` with literal string matching. Power users who want pattern-based filtering (e.g., "all notes whose title matches a date pattern", "any property matching a URL regex") cannot express this with literals alone.
|
||||
|
||||
## Decision
|
||||
|
||||
**A `regex: true` flag is added to `FilterCondition`. When set, the `value` field is interpreted as a case-insensitive regular expression (via `regex::RegexBuilder` in Rust, and the native JS `RegExp` in TypeScript) for the operators that support it: `contains`, `equals`, `not_contains`, `not_equals`.**
|
||||
|
||||
- Regex is opt-in: the `regex` field defaults to `false` and is skipped during serialization when false (no noise in existing `.yml` files).
|
||||
- If the regex fails to compile, the condition evaluates to `false` rather than throwing.
|
||||
- For relationship fields, the regex is tested against all candidate forms: the raw wikilink string, the inner stem, and the alias (if present).
|
||||
- The `FilterBuilder` UI gains a regex toggle icon button next to value inputs for supported operators.
|
||||
- TypeScript `viewFilters.ts` mirrors the same regex logic for client-side evaluation.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — Add regex operator variants** (`regex_equals`, `regex_contains`): More explicit in YAML. Downside: doubles the operator set; no clear path to combine regex with `not_contains`.
|
||||
- **Option B — Per-condition `regex: bool` flag (chosen)**: Composable with existing operators; minimal schema change; serialization skips the field when false so existing views are unaffected.
|
||||
- **Option C — Full query language** (e.g., JMESPath or SQL `WHERE`): Maximum power. Out of scope; would replace rather than extend the filter engine.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New dependency: `regex` crate in Rust (already present for other vault modules; no net new dep).
|
||||
- Filter YAML files that use `regex: true` require Laputa ≥ this version to evaluate correctly; older versions silently ignore the flag (falling back to `regex: false` default via `#[serde(default)]`).
|
||||
- Regex evaluation has a small performance cost vs. literal matching. No memoization of compiled regexes per evaluation call — acceptable given vault sizes (< 10k notes).
|
||||
- Re-evaluation trigger: if regex performance becomes measurable, cache compiled `Regex` objects keyed by pattern string.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0048"
|
||||
title: "Relative date expressions in view filter conditions"
|
||||
status: active
|
||||
date: 2026-04-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The view filter engine (ADR 0040) supports `before` and `after` operators but previously compared values as raw strings, meaning users had to write absolute ISO dates (e.g., `2026-04-01`) that became stale immediately. Views like "notes modified in the last 7 days" required updating the date manually every week.
|
||||
|
||||
## Decision
|
||||
|
||||
**The `before` and `after` filter operators now accept relative date expressions in addition to absolute ISO dates. Both the Rust backend and the TypeScript client independently parse the expression before comparing.**
|
||||
|
||||
### Supported syntax
|
||||
|
||||
| Expression | Meaning |
|
||||
|---|---|
|
||||
| `today` | Start of the current day (00:00 UTC) |
|
||||
| `yesterday` | Start of yesterday |
|
||||
| `tomorrow` | Start of tomorrow |
|
||||
| `N days ago` / `N weeks ago` / `N months ago` / `N years ago` | Past relative |
|
||||
| `in N days` / `in N weeks` / `in N months` / `in N years` | Future relative |
|
||||
|
||||
Word-form amounts are also accepted: `one`, `two`, `three`, … `twelve`.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Rust** (`views.rs`): `parse_date_filter_timestamp()` resolves both field values and condition values to `i64` timestamps before comparing. Falls back gracefully when a value cannot be parsed.
|
||||
- **TypeScript** (`utils/filterDates.ts`): `parseDateFilterInput()` and `toDateFilterTimestamp()` mirror the same logic for client-side filter evaluation. `date-fns` is used for date arithmetic.
|
||||
- Both implementations use "start of day UTC" (00:00:00) as the anchor for relative expressions, consistent with how note creation/modification dates are stored.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — Store and evaluate absolute dates only**: No parsing cost. Downside: views become stale; users must update dates manually.
|
||||
- **Option B — Relative expressions resolved at evaluation time (chosen)**: Views stay perpetually current ("last 7 days" always means last 7 days). Downside: parallel implementation in Rust and TypeScript must stay in sync.
|
||||
- **Option C — Pre-resolve relative expressions to absolute dates on save**: Expressions are human-readable when authoring but stored as ISO strings. Downside: view files drift; loses the relative intent.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Relative expressions are evaluated at query time using the server/client clock. A view evaluated at 23:59 and 00:01 may return different results for "today".
|
||||
- Both parsers share the same resolution anchor (start-of-day UTC). Timezone-sensitive relative expressions (e.g., "yesterday in Tokyo") are not supported.
|
||||
- Existing `.yml` files with absolute ISO dates continue to work unchanged — the parser first tries ISO format before attempting relative parsing.
|
||||
- Re-evaluation trigger: if timezone-aware relative dates become a user need, the expression syntax and anchor logic need revisiting.
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0049"
|
||||
title: "Per-note icon property (_icon on individual notes)"
|
||||
status: active
|
||||
date: 2026-04-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa already supports type-level icons via the `_icon` system property on type documents (ADR 0008). Every note of a given type inherits the type's icon. Users needed a way to give individual notes a distinct visual identity without changing the type — e.g., marking a specific project with a rocket emoji, or a key person with a star icon — without creating a new type just for one note.
|
||||
|
||||
## Decision
|
||||
|
||||
**The `_icon` system property (already used by type documents) is now also supported on regular notes. When a note has an `_icon` value, it overrides the inherited type icon in all UI surfaces. The value may be an emoji, a Phosphor icon name, or an HTTP(S) image URL.**
|
||||
|
||||
### Resolution logic
|
||||
|
||||
`resolveNoteIcon(icon)` in `utils/noteIcon.ts` returns a discriminated union:
|
||||
|
||||
| Kind | Condition |
|
||||
|---|---|
|
||||
| `none` | Value is empty/null |
|
||||
| `emoji` | Value passes `isEmoji()` |
|
||||
| `image` | Value is an HTTP(S) URL |
|
||||
| `phosphor` | Value matches a registered Phosphor icon name |
|
||||
|
||||
The `NoteTitleIcon` component renders the correct element for each kind (span, `<img>`, or Phosphor SVG component).
|
||||
|
||||
### UI surfaces updated
|
||||
|
||||
- Editor breadcrumb bar (clicking the icon opens the `_icon` property editor)
|
||||
- Note list items (`NoteItem`)
|
||||
- Search panel results
|
||||
- Relationship chips (shows icons on wikilink chips)
|
||||
- Sidebar type sections
|
||||
- Backlinks / ReferencedBy panels
|
||||
- Inspector pinned area
|
||||
|
||||
### Editing
|
||||
|
||||
A custom event (`laputa:focus-note-icon-property`) is dispatched from the breadcrumb bar click to focus the `_icon` field in the Properties panel without scrolling. The field uses the existing property editor UI.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — Separate `_note_icon` property**: Avoids ambiguity with the type-level `_icon`. Downside: two names for the same concept depending on context; complicates the resolver.
|
||||
- **Option B — Reuse `_icon` on notes (chosen)**: Consistent with existing convention (ADR 0008); type docs and note docs follow the same schema. The distinction between type-level and note-level is determined by `is_a: Type` in the frontmatter, not by a different property name.
|
||||
- **Option C — Inline emoji in note title**: Zero-friction. Downside: title is also the filename (ADR 0044); emojis in filenames cause filesystem/git pain.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `_icon` on a note overrides the type icon everywhere. A note with no `_icon` continues to inherit the type icon (no behavior change for existing vaults).
|
||||
- The icon resolver (`resolveNoteIcon`) is shared between note icons and type icons; future changes to icon resolution affect both.
|
||||
- `iconRegistry.ts` grows with any new Phosphor icon additions — currently loaded eagerly. If the icon set grows large, lazy loading or a build-time icon map should be considered.
|
||||
- Re-evaluation trigger: if users request per-note color (the `_color` system property currently only applies to types), the same resolution pattern can be extended.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0050"
|
||||
title: "Deterministic shortcut command routing"
|
||||
status: active
|
||||
date: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa is keyboard-first, but shortcut execution had split ownership: `useAppKeyboard` handled some shortcuts in the renderer while `menu.rs` owned others as native Tauri menu accelerators. That split made QA unreliable. Browser tests could prove the renderer path, but not the native menu path, and flaky macOS key synthesis made `Cmd+Shift+L`, `Cmd+Shift+I`, and `Cmd+N` regressions easy to miss.
|
||||
|
||||
## Decision
|
||||
|
||||
**Keyboard shortcuts and native menu accelerators now dispatch through the same canonical app command IDs. Renderer-owned shortcuts call the shared dispatcher directly; native menu items emit the same IDs into the frontend, and tests get a deterministic menu-command trigger that exercises that route without relying on synthesized native keystrokes.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Shared command IDs plus deterministic menu-command trigger — keeps native desktop UX while making menu-owned commands testable in unit tests, Playwright, and native QA. Downside: one more command layer to maintain.
|
||||
- **Option B**: Move every shortcut to the renderer — simpler automated testing, but worse macOS menu-bar parity and weaker native UX.
|
||||
- **Option C**: Keep renderer and native shortcuts separate — lowest code churn, but continues to produce false confidence and shortcut regressions.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `appCommandDispatcher.ts` owns the canonical shortcut command IDs and the shared execution path used by `useAppKeyboard` and `useMenuEvents`.
|
||||
- Native menu routing remains explicit in `menu.rs`; adding or changing a native shortcut now requires wiring the accelerator and the matching command ID in one place.
|
||||
- Automated QA can trigger menu-owned commands deterministically through the shared `window.__laputaTest.triggerMenuCommand()` bridge in browser runs and through the native `trigger_menu_command` Tauri command in desktop runs.
|
||||
- Keyboard QA should prefer real menu selection or the deterministic menu-command trigger for native-owned shortcuts, and reserve synthesized keystrokes for renderer-owned shortcuts or true end-to-end spot checks.
|
||||
- This decision supersedes the blanket assumption in ADR 0020 that all shortcut verification can be treated as plain keyboard-event testing.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0051"
|
||||
title: "Shared shortcut manifest for testable routing"
|
||||
status: active
|
||||
date: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0050 moved renderer shortcuts and native menu events onto the same command dispatcher, but shortcut ownership still drifted across multiple places: `appKeyboardShortcuts.ts`, `appCommandDispatcher.ts`, command-palette metadata, and `menu.rs`. That made shortcut regressions easy to reintroduce because the same facts had to be updated manually in several files.
|
||||
|
||||
The riskiest failures were exactly the native-owned commands that matter most in a keyboard-first app: `Cmd+\` for raw editor, `Cmd+Shift+I` for properties, and `Cmd+Shift+L` for the AI panel. We need one declarative place that says which command owns which shortcut, whether the shortcut is renderer-owned or native-menu-owned, and how tests should trigger it.
|
||||
|
||||
## Decision
|
||||
|
||||
**Shortcut-capable app commands are now defined in a shared frontend manifest that owns command IDs, routing semantics, and shortcut ownership. Renderer keyboard handling resolves commands from that manifest, native menu routing dispatches the same command IDs, and deterministic QA for native-owned shortcuts targets those IDs rather than duplicating shortcut facts in ad hoc code paths.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Shared shortcut manifest plus shared dispatcher and deterministic menu-command QA. This reduces drift, improves CodeScene on the command router, and makes native-owned shortcuts provable without flaky macOS key synthesis. Downside: one more manifest to maintain.
|
||||
- **Option B**: Keep the shared dispatcher from ADR 0050 but continue storing shortcut ownership in separate key maps and menu lists. Lower churn, but it keeps the exact source of the regressions we reopened.
|
||||
- **Option C**: Move all shortcuts into renderer-only handlers. Easier to test, but weaker macOS menu-bar parity and worse native desktop UX.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `appCommandCatalog.ts` is now the frontend source of truth for shortcut-capable command IDs, ownership, modifier rules, and dispatch kind.
|
||||
- `appCommandDispatcher.ts` is reduced to route execution instead of carrying a large switch plus duplicated ownership metadata.
|
||||
- `useAppKeyboard.ts` resolves shortcuts from the shared manifest, including the distinction between `Cmd+Shift+L` (macOS-only) and `CmdOrCtrl+Shift+I/F/O`.
|
||||
- Native-menu smoke tests should use `window.__laputaTest.triggerMenuCommand()` or the Tauri `trigger_menu_command` bridge to prove the native command path. Renderer-only commands may still be proven with direct keyboard events.
|
||||
- This ADR supersedes ADR 0050 by replacing “shared command IDs are enough” with “shared command IDs plus shared shortcut ownership metadata are required.”
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0052"
|
||||
title: "Renderer-first shortcut execution with native-menu dedupe"
|
||||
status: active
|
||||
date: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0051 gave Laputa a shared shortcut manifest and shared command IDs, but it still treated many shortcuts as native-menu-owned at execution time. In practice that meant `useAppKeyboard` deferred commands like `Cmd+Shift+I`, `Cmd+Shift+L`, and `Cmd+\` whenever the app ran under Tauri, and automated QA had to prove those flows by injecting menu-command IDs instead of pressing the real keys.
|
||||
|
||||
That is not a strong enough QA story for a keyboard-first app. If a user presses a shortcut while the editor is focused, we need a deterministic way to prove the actual key combo works. At the same time, we still want a native macOS menu bar with working menu items and accelerators.
|
||||
|
||||
## Decision
|
||||
|
||||
**Renderer keyboard handling is now the primary execution path for all shortcut-capable app commands, including commands that also have native menu accelerators. Native menu clicks and accelerators still emit the same command IDs, but the shared dispatcher suppresses the duplicate native/renderer echo from a single keypress so the command runs exactly once.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Renderer-first shortcut execution plus native-menu dedupe. This keeps shortcuts testable with real key events in a Tauri-like environment while preserving menu-bar parity and clickable native menu items. Downside: the dispatcher has to understand and suppress paired native/renderer echoes.
|
||||
- **Option B**: Keep deferring native-owned shortcuts out of the renderer and prove them only through `trigger_menu_command`. Lower implementation churn, but it still leaves the real keystroke path unproven.
|
||||
- **Option C**: Remove native accelerators entirely and keep shortcuts renderer-only. Simplest to reason about, but weaker desktop UX and poorer macOS menu discoverability.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `appCommandCatalog.ts` remains the single manifest for command IDs and shortcut combos, but keyboard execution no longer depends on a separate owner flag.
|
||||
- `useAppKeyboard` handles the actual key event for every shortcut-capable command, even in Tauri mode.
|
||||
- `useMenuEvents` still handles menu clicks and test-triggered native command IDs, but shared dispatcher dedupe prevents a focused keypress from firing twice when the native menu accelerator also echoes back into the renderer.
|
||||
- Deterministic QA now has two complementary proofs:
|
||||
- real keyboard events in a Tauri-like environment for the actual shortcut combo
|
||||
- `trigger_menu_command` for the native menu click/accelerator command path
|
||||
- This ADR supersedes ADR 0051 by replacing “execution ownership lives in the manifest” with “shortcut combos live in the manifest, while execution is renderer-first and native menu dispatch is deduped.”
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0053"
|
||||
title: "Webview-init prevention for browser-reserved shortcuts"
|
||||
status: active
|
||||
date: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0052 made renderer-first shortcut handling the primary path for command execution, with native menu accelerators deduped afterward. That works for normal shortcuts, but native QA on macOS showed that `Cmd+Shift+L` still failed to reach the app even though the shared command path and the Note menu item both worked.
|
||||
|
||||
The gap is WKWebView itself: some browser-reserved chords are swallowed by the webview before the renderer-level shortcut listener can execute. That makes the shortcut untestable with the real native keypress even though the command bus is correct.
|
||||
|
||||
## Decision
|
||||
|
||||
**Laputa will keep renderer-first shortcut execution, but for macOS browser-reserved chords we will add a narrow Tauri webview-init prevention layer using `tauri-plugin-prevent-default` so the real keystroke reaches the shared command path.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Add a narrow `tauri-plugin-prevent-default` registration for only the known browser-reserved chords we actually use. This preserves ADR 0052, keeps the command bus unified, and fixes the real native keystroke path without broad shortcut capture.
|
||||
- **Option B**: Keep relying on renderer capture listeners alone. Simpler, but it fails for chords that WKWebView consumes before renderer code sees them.
|
||||
- **Option C**: Use a global shortcut plugin as the fallback path. This would catch the keystroke natively, but it reserves the chord outside Laputa and is too heavy for app-local shortcuts.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Shortcut ownership stays unified: command IDs and execution still live in the shared renderer/native command bus.
|
||||
- macOS-only browser-reserved chords now have one extra declaration point in `src-tauri/src/lib.rs`, and that list must stay intentionally small.
|
||||
- Native QA remains mandatory for any shortcut added to that list, because browser dev and mocked Tauri tests do not exercise the webview-init layer.
|
||||
- Re-evaluate this decision if Tauri/WKWebView exposes a better app-local native shortcut hook that does not require browser-reserved-key workarounds.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0054"
|
||||
title: "Deterministic shortcut QA matrix"
|
||||
status: active
|
||||
date: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0052 made renderer-first shortcut execution the primary runtime path, and ADR 0053 added a narrow macOS webview-init prevent-default layer for browser-reserved chords such as `Cmd+Shift+L`. Those decisions improved behavior, but the automated QA story was still muddy:
|
||||
|
||||
- browser smoke tests were describing a mocked desktop harness as if it were native Tauri QA
|
||||
- some tests used `page.keyboard.press()` for commands whose real desktop accelerators are intercepted or reserved by the browser shell
|
||||
- native menu command coverage existed, but the catalog did not declare which deterministic proof path each shortcut should use
|
||||
|
||||
That made it too easy to ship a shortcut with passing automation while overstating what the automation had actually proven.
|
||||
|
||||
## Decision
|
||||
|
||||
**Laputa will treat shortcut QA as an explicit part of the shared command manifest. Every shortcut-capable command must have a deterministic automated proof path, and the test harness must distinguish renderer shortcut-event proof from native menu-command proof instead of calling the browser harness “native Tauri QA”.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Add a deterministic shortcut QA matrix to the shared command catalog. Renderer shortcut handling can be exercised through synthetic `keydown` events generated from the manifest, while native menu commands are exercised through `trigger_menu_command`. Pros: deterministic, explicit, and honest about what is being proved. Cons: still requires real native QA for exact accelerator delivery on macOS.
|
||||
- **Option B**: Keep using ad hoc Playwright key presses and browser-side menu shims. Lower change cost, but still allows false claims about native coverage and still depends on browser-reserved shortcuts behaving nicely.
|
||||
- **Option C**: Block all shortcut work until full native Tauri automation exists. Strongest eventual guarantee, but it would leave the keyboard-first app without a usable deterministic QA strategy today.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `appCommandCatalog.ts` now owns not just command IDs and modifier rules, but also the deterministic QA mode for each shortcut-capable command.
|
||||
- Browser harness smoke tests must describe themselves as a desktop command bridge, not native app QA.
|
||||
- Renderer shortcut behavior can be verified deterministically without depending on browser chrome or flaky AppleScript key synthesis.
|
||||
- Native menu-command behavior can be verified deterministically through the Tauri command bridge.
|
||||
- Exact desktop accelerator delivery still requires real Tauri QA for commands flagged as needing manual native verification, especially browser-reserved macOS chords.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0055"
|
||||
title: "H1 is the only editor title surface"
|
||||
status: superseded
|
||||
date: 2026-04-11
|
||||
supersedes: "0044"
|
||||
superseded_by: "0068"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0044 moved Laputa to H1-as-title, but the frontend still carried a legacy fallback: when a note had no H1, `TitleField` and the old title section could reappear above the editor. That left two competing title surfaces in the product and made it possible for deleting an H1 to resurrect UI that was supposed to be gone.
|
||||
|
||||
The result was both behavioral drift and stale tests: some code paths still treated the dedicated title row as a valid editing surface even though the product direction is now keyboard-first writing directly in the document body.
|
||||
|
||||
## Decision
|
||||
|
||||
**The editor body is now the only title surface. Laputa never renders a separate title section above the editor, regardless of whether a note currently has an H1.**
|
||||
|
||||
Display-title behavior stays:
|
||||
1. First H1 in the body
|
||||
2. Legacy frontmatter `title:`
|
||||
3. Filename-derived fallback
|
||||
|
||||
But the UI no longer exposes a dedicated title field for cases 2 or 3. When a note has no H1, the editor simply shows normal body content or the empty-editor placeholder.
|
||||
|
||||
Filename operations remain explicit:
|
||||
- untitled notes still auto-rename from H1 on save
|
||||
- manual filename rename/sync remains in the breadcrumb
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): remove the fallback title section entirely. This makes the editor honest, removes a stale code path, and keeps title editing aligned with the keyboard-first document model.
|
||||
- **Option B**: keep the fallback title field for non-H1 notes. This preserves an alternate rename path, but it reintroduces the exact dual-surface ambiguity that ADR-0044 tried to escape.
|
||||
- **Option C**: hide the title section with CSS only. Low churn, but it leaves dead render/state paths in place and makes regressions like “delete H1 and old title row returns” easy to reintroduce.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Deleting an H1 no longer reveals any legacy title UI; the user stays in the editor body.
|
||||
- `TitleField` and the title-section render path are removed from the frontend.
|
||||
- Breadcrumb filename controls are now the only explicit file-identifier editing surface outside the editor body.
|
||||
- Older tests that asserted title editing through `TitleField` are obsolete and should be replaced by H1-title or breadcrumb-filename coverage.
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0056"
|
||||
title: "System git auth only — no provider-specific OAuth or repo APIs"
|
||||
status: active
|
||||
date: 2026-04-12
|
||||
supersedes: "0019"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria already uses the system `git` executable for the core remote workflow: commit, pull, push, status, history, and conflict resolution. The only provider-specific part left was GitHub authentication and repository management:
|
||||
|
||||
- GitHub Device Flow OAuth
|
||||
- persisted `github_token` / `github_username` settings
|
||||
- GitHub-only clone/create UI
|
||||
- GitHub API calls for repo listing and creation
|
||||
|
||||
That split made the product more complex than the actual user need. Tolaria's remote-sync users are developers who typically already have git configured via SSH keys, Git Credential Manager, Keychain helpers, or `gh auth`. The app was carrying a provider-specific auth stack even though the real transport path was already plain git CLI.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria does not implement provider-specific authentication or remote-repository APIs. All remote auth is delegated to the user's existing system git configuration, and cloning is a generic "paste any git URL" flow.**
|
||||
|
||||
Concretely:
|
||||
|
||||
- remove GitHub Device Flow commands and UI
|
||||
- remove persisted GitHub auth fields from app settings
|
||||
- remove GitHub repo list/create API integration
|
||||
- keep `clone_repo`, but make it a generic system-git clone command
|
||||
- keep commit / pull / push behavior unchanged apart from surfacing raw git errors directly
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A — Keep GitHub Device Flow OAuth** (ADR-0019, now superseded): polished GitHub-specific onboarding, but it preserves provider lock-in, token storage, and an entire second auth model beside system git.
|
||||
- **Option B — Replace OAuth with manual PAT entry**: smaller implementation than Device Flow, but still provider-specific, still stores credentials in app settings, and still teaches users the wrong abstraction.
|
||||
- **Option C — Pure system git auth** (chosen): one auth path, less code, works with any git host, and aligns the clone flow with the rest of Tolaria's git stack. Downside: users must already have git auth configured outside the app.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `CloneVaultModal` accepts any git URL and local destination path.
|
||||
- `clone_repo` shells out to system git without injecting provider tokens.
|
||||
- `git_push` / `git_pull` continue to rely on the same external git configuration; auth failures surface as raw git stderr.
|
||||
- `SettingsPanel` no longer contains a GitHub connection section.
|
||||
- Tolaria no longer stores git-provider credentials in `settings.json`.
|
||||
- GitHub, GitLab, Bitbucket, Gitea, and self-hosted remotes all work through the same product path.
|
||||
- Creating or listing remote repos from inside Tolaria is no longer supported; remote setup happens in the user's normal git tools.
|
||||
- The Getting Started vault still clones from a public remote URL, but it now goes through the same generic git clone path as every other vault import.
|
||||
|
||||
Re-evaluate if Tolaria later targets less technical users who cannot reasonably be expected to configure git outside the app.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0057"
|
||||
title: "Alpha/stable release channels with PostHog beta cohorts"
|
||||
status: superseded
|
||||
date: 2026-04-12
|
||||
superseded_by: "0066"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's updater and release docs still described a canary branch, a beta updater channel, and a single `latest.json` feed. That no longer matched the desired product model:
|
||||
|
||||
- `main` should continuously publish **alpha** builds.
|
||||
- **Stable** should be promoted manually by pushing `stable-vX.Y.Z` tags.
|
||||
- "Beta" users should be modeled in PostHog for targeting and analysis, not as a separate binary or updater feed.
|
||||
|
||||
The updater also needed semver-safe versioning when a user switches between Stable and Alpha. A date-based alpha version below the latest stable release would cause the updater to ignore newer alpha builds after a stable promotion.
|
||||
|
||||
This ADR supersedes ADR-0017's canary-branch updater model.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria exposes exactly two updater channels: `stable` and `alpha`. Stable is the default feed, while every push to `main` publishes a prerelease alpha build to `alpha/latest.json`, and manually promoted `stable-vX.Y.Z` tags publish stable builds to `stable/latest.json`. Beta audiences are handled in PostHog and are not a third updater channel.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Two updater channels (`stable`, `alpha`) plus PostHog beta cohorts. Pros: matches the product requirement, keeps CI simple, keeps updater semantics understandable, and separates release distribution from experimentation audiences. Cons: requires semver-aware alpha versioning and a small migration for legacy channel settings.
|
||||
- **Option B**: Keep the canary branch / canary channel model. Pros: no workflow redesign. Cons: no longer matches how releases are actually promoted and forces distribution strategy to depend on a long-lived branch.
|
||||
- **Option C**: Add a third updater channel for beta builds. Pros: direct binary segmentation. Cons: extra CI complexity, extra updater endpoints, and unnecessary duplication because beta targeting is already better handled by PostHog.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `release.yml` now publishes alpha prereleases from every push to `main`.
|
||||
- `release-stable.yml` publishes stable releases only from `stable-v*` tags.
|
||||
- `src-tauri/src/app_updater.rs` selects `alpha/latest.json` or `stable/latest.json` at runtime.
|
||||
- `release_channel` stays an app setting, but only `alpha` is stored explicitly; Stable serializes to the default `null` value.
|
||||
- Legacy or invalid persisted channel values fall back to Stable.
|
||||
- Alpha versions are prereleases of the next stable patch version (for example `1.2.4-alpha.202604122135.7` after stable `1.2.3`) so semver ordering remains valid across channel switches.
|
||||
- The legacy GitHub Pages aliases `latest.json` and `latest-canary.json` continue to mirror alpha for backward compatibility.
|
||||
- Beta rollouts and internal-user targeting are done in PostHog using person properties or cohorts rather than updater manifests.
|
||||
|
||||
## Advice
|
||||
|
||||
If a future release process needs more than two binary distribution rings, re-evaluate this decision only when PostHog cohorting is no longer sufficient and the extra operational cost of another updater feed is justified.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0058"
|
||||
title: "Claude Code first-launch onboarding gate"
|
||||
status: superseded
|
||||
superseded_by: "0062"
|
||||
date: 2026-04-12
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's AI features depend on the `claude` CLI being installed on the user's machine. New users arriving with no prior context could open the app, try AI-powered workflows, and get silent failures with no explanation.
|
||||
|
||||
A dedicated first-launch prompt was needed to:
|
||||
- Surface whether the `claude` CLI is already present.
|
||||
- Guide users to the install page if it is missing.
|
||||
- Not block experienced users who want to skip the check.
|
||||
|
||||
The existing `useOnboarding` hook already handles vault setup and resolves to a `ready` state, but it had no mechanism for a post-vault, pre-app step.
|
||||
|
||||
## Decision
|
||||
|
||||
**A one-time `ClaudeCodeOnboardingPrompt` is shown immediately after vault onboarding resolves to `ready`, before the main app shell renders. Dismissal is persisted in `localStorage` via `useClaudeCodeOnboarding`, so the gate appears exactly once per install.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Full-screen gate after vault onboarding, dismissed once and persisted in `localStorage`. Pros: cannot be missed on first launch, reuses `useClaudeCodeStatus` for live detection, zero impact on returning users. Cons: adds one extra render phase to the boot sequence.
|
||||
- **Option B**: Inline banner inside the main app. Pros: less intrusive. Cons: easy to ignore, harder to surface install link prominently.
|
||||
- **Option C**: Check at feature use time (show error when AI action fails). Pros: no new screen. Cons: poor UX — silent failure or cryptic error at the moment the user needs AI.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The app boot sequence now has four phases: loading → welcome (if needed) → Claude Code check (once) → main shell.
|
||||
- `useClaudeCodeOnboarding(enabled)` takes a boolean so the gate is skipped entirely in note windows and before vault onboarding completes.
|
||||
- The dismissal key (`tolaria:claude-code-onboarding-dismissed`) must be pre-set in Playwright storage state so smoke tests bypass the gate.
|
||||
- Re-evaluation warranted if the `claude` CLI gains an in-app auto-install path, making the manual prompt unnecessary.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0059"
|
||||
title: "Local-only git commits for vaults without a remote"
|
||||
status: active
|
||||
date: 2026-04-12
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0034 mandates a git repo for every vault, but never required a remote. In practice, the commit flow always attempted a `git push` after staging and committing. Users with purely local vaults (no remote configured) would hit a push error on every commit.
|
||||
|
||||
The fix required distinguishing between two commit modes at the point of user action:
|
||||
- **Push mode**: repo has a remote → commit then push (existing behavior).
|
||||
- **Local mode**: repo has no remote → commit only, no push attempted.
|
||||
|
||||
## Decision
|
||||
|
||||
**`useCommitFlow` detects the vault's remote status before opening the commit dialog and at commit time. When `hasRemote === false`, it commits locally and skips the push step entirely, showing "Committed locally (no remote configured)" as the confirmation toast.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Runtime detection via a new `useGitRemoteStatus` hook + `CommitMode` type (`push` | `local`). Pros: transparent to the user, no configuration needed, adapts if a remote is added later. Cons: adds an async remote-status check to the commit open flow.
|
||||
- **Option B**: Require all vaults to have a remote (keep blocking behavior). Pros: simpler model. Cons: breaks the valid use case of a local-only knowledge base; contradicts ADR-0056 which removed provider-specific OAuth.
|
||||
- **Option C**: Let the push fail silently and always show success. Pros: no new logic. Cons: misleading feedback; users wouldn't know the push was skipped vs. succeeded.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `useGitRemoteStatus` is a new hook that exposes `remoteStatus` and `refreshRemoteStatus`; it is called both when opening the commit dialog and after each commit.
|
||||
- `CommitDialog` now receives a `commitMode` prop and adjusts its CTA label accordingly (`Commit & Push` vs `Commit`).
|
||||
- The `commitAndPush` callback in `CommitFlowConfig` is replaced by `resolveRemoteStatus` + `vaultPath`; the actual git operations (`git_commit`, `git_push`) are invoked directly inside `useCommitFlow`.
|
||||
- Local-only commits fire `trackEvent('commit_made')` the same as push commits for analytics continuity.
|
||||
- Re-evaluation warranted if a remote is later added to a previously-local vault and the UX should prompt the user to push accumulated commits.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0060"
|
||||
title: "Network-aware UI gating for remote-dependent features"
|
||||
status: active
|
||||
date: 2026-04-13
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Some app features require an active internet connection (e.g., cloning the Getting Started vault template from GitHub). Prior to this decision, the UI would attempt the operation and surface a generic error only after failure. Users on first launch in offline environments got a confusing error when trying to use the template.
|
||||
|
||||
## Decision
|
||||
|
||||
**Introduce a `useNetworkStatus` hook that tracks `navigator.onLine` via `online`/`offline` DOM events, and use it to proactively gate UI surfaces that require a network.** Features that depend on remote access (clone, sync) show an explanatory message and disable their action button when the device is offline, rather than failing silently at execution time.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): `useNetworkStatus` hook + proactive UI disable — disables the action before the user tries it, with inline copy explaining the offline state.
|
||||
- **Option B**: Attempt and catch — let the operation run and surface the error in a toast. Simpler, but poor UX for first-launch users who don't know what went wrong.
|
||||
- **Option C**: Check connectivity with a ping on demand — more accurate but adds latency and complexity; `navigator.onLine` is sufficient for the use case.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Positive: cleaner first-run experience for offline users; no misleading error messages.
|
||||
- Positive: `useNetworkStatus` is a reusable hook for future remote-gated features.
|
||||
- Negative: `navigator.onLine` can return `true` on a captive-portal / no-internet network — the hook reflects OS-level connectivity, not end-to-end reachability. The operation may still fail with a network error, which must still be handled.
|
||||
- Re-evaluate if the app adds more remote features that need finer-grained reachability checks.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0061"
|
||||
title: "AI prompt bridge — module-level event bus for cross-component prompt routing"
|
||||
status: active
|
||||
date: 2026-04-13
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The AI panel is a sibling subtree to the command palette in the component tree. When the user submits a prompt from the command palette's AI mode, the AI panel (mounted elsewhere) needs to receive it and start processing. Props-down / callbacks-up wiring between the two would require threading state through multiple layers of unrelated components.
|
||||
|
||||
## Decision
|
||||
|
||||
**Introduce `aiPromptBridge.ts` as a module-level singleton event bus.** The bridge exposes `queueAiPrompt(text, references)` (write path) and `takeQueuedAiPrompt()` (consume path), backed by a module variable and a `CustomEvent` on `window` (`tolaria:ai-prompt-queued`). The command palette enqueues a prompt; the AI panel listens for the event, consumes the prompt via `takeQueuedAiPrompt`, and dispatches it to the agent. A companion `requestOpenAiChat()` function fires a separate `tolaria:open-ai-chat` event to open the panel before the prompt is sent.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): module-level singleton + `window` events — zero dependencies, no new global state manager, consistent with the existing `window.dispatchEvent` pattern already used for menu-command bridging.
|
||||
- **Option B**: Lift AI panel state to a shared ancestor (e.g., `App.tsx`) and pass `onPrompt` callback down — would require `App.tsx` to own AI agent state, bloating it further; conflicts with ADR-0026 (props-down principle).
|
||||
- **Option C**: Zustand / Jotai global store atom — adds a dependency and architecture overhead for a narrow, two-participant channel.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Positive: decouples command palette from AI panel with no shared ancestor coupling.
|
||||
- Positive: any future surface (e.g., wikilink context menu, note action bar) can call `queueAiPrompt` without tree-level wiring.
|
||||
- Negative: module-level mutable state is harder to test in isolation; tests must call `takeQueuedAiPrompt` to drain state between runs.
|
||||
- Negative: the event is fire-and-forget — if the AI panel is not mounted when the event fires, the prompt is silently dropped (currently not an issue as the panel is always mounted).
|
||||
- Re-evaluate if the number of AI entry points grows large enough to warrant a proper state management solution.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0062"
|
||||
title: "Selectable CLI AI agents with a shared panel architecture"
|
||||
status: active
|
||||
date: 2026-04-13
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's AI panel, onboarding flow, and status surfaces were built around a single CLI dependency: Claude Code. That worked for the first release, but it made every UI and backend seam agent-specific. Adding Codex as a second supported CLI agent would have duplicated large parts of the app: separate availability checks, a second onboarding path, another status badge, and yet another streaming hook.
|
||||
|
||||
The product direction is broader than a single vendor. Tolaria needs one AI panel that can target multiple local CLI agents while preserving the same MCP-backed vault tooling, the same note-context assembly, and a single install-local preference for which agent should be used by default.
|
||||
|
||||
## Decision
|
||||
|
||||
**Introduce a shared CLI-agent abstraction for Tolaria's AI surfaces.** The frontend now treats agents as a small registry (`claude_code`, `codex`) with labels, install URLs, availability state, and a persisted `default_ai_agent` setting. The AI panel, onboarding gate, command palette, and status bar all read from that shared model. On the backend, `ai_agents.rs` owns agent detection and streaming, dispatching to per-agent adapters: Claude still flows through `claude_cli.rs`, while Codex is launched through `codex exec --json` with Tolaria's MCP server injected via transient config flags.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): shared agent registry + backend adapter layer — one panel, one preference, one onboarding path, and a clear place to add future CLI agents.
|
||||
- **Option B**: keep the UI Claude-specific and bolt on Codex as a second special case — lowest short-term cost, but every new agent multiplies the number of bespoke checks, prompts, and command handlers.
|
||||
- **Option C**: split the product into separate per-agent panels — clearer ownership per integration, but fragments the UX and makes command-palette / status-bar interactions inconsistent.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Positive: new CLI agents can be added by implementing one backend adapter and registering one frontend definition.
|
||||
- Positive: onboarding and settings now explain the AI capability of the app at the product level rather than assuming Claude Code is the only valid path.
|
||||
- Positive: the default agent is installation-local, matching ADR-0004's rule that machine-specific tool preferences belong in app settings rather than the vault.
|
||||
- Negative: event normalization is now Tolaria-owned; backend adapters must translate each CLI's stream format into a common event model.
|
||||
- Negative: some user guidance becomes agent-specific again at the edge, such as install links and authentication errors (`claude` login vs `codex login`).
|
||||
- Re-evaluate if one agent needs capabilities the shared panel cannot express cleanly, or if Tolaria ever moves from CLI subprocesses to a dedicated local SDK/runtime.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0063"
|
||||
title: "BlockNote code-block package for editor syntax highlighting"
|
||||
status: active
|
||||
date: 2026-04-13
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria uses BlockNote for rich-text editing. Fenced code blocks already render on BlockNote's dark `pre > code` surface, but they were missing syntax highlighting and inherited the muted inline-code chip background from the global `code` selector in `EditorTheme.css`. The QA expectation is a dark code block with highlighted tokens and light code text, without regressing inline-code styling elsewhere in the editor.
|
||||
|
||||
BlockNote documents syntax highlighting as a schema concern: replace the default `codeBlock` spec with `createCodeBlockSpec(...)` and provide a Shiki highlighter. Tolaria also needs to preserve the existing default behavior for unlabeled code blocks, which should stay plain text instead of defaulting to JavaScript.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria overrides the default BlockNote `codeBlock` spec with `@blocknote/code-block`, keeps `defaultLanguage: "text"`, and scopes the muted inline-code chip styling away from fenced code blocks.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Use `@blocknote/code-block`** (chosen): first-party BlockNote path, ships supported language aliases and a bundled Shiki highlighter, renders `.shiki` token spans in-editor, and avoids maintaining a parallel ProseMirror plugin integration.
|
||||
- **Use a custom `createCodeBlockSpec({ createHighlighter })` bundle**: also valid, but Tolaria does not need a custom language/theme bundle beyond BlockNote's packaged setup right now.
|
||||
- **Keep BlockNote defaults and only fix CSS**: removes the nested gray chip bug, but leaves fenced code blocks unhighlighted and fails the product requirement.
|
||||
|
||||
## Consequences
|
||||
|
||||
Tolaria's highlighting now lives in the editor schema instead of an editor-side plugin hook. `src/components/editorSchema.tsx` swaps in `createCodeBlockSpec({ ...codeBlockOptions, defaultLanguage: "text" })`, which adds BlockNote's language selector plus Shiki token spans for supported fenced blocks. `EditorTheme.css` continues to keep the `pre > code` background transparent so BlockNote's dark code-block shell remains intact.
|
||||
|
||||
The tradeoff is one new first-party dependency and BlockNote's bundled language menu inside code blocks. If Tolaria later needs a narrower bundle, custom themes, or export-time highlighting parity, this ADR should be superseded with a custom Shiki bundle decision.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0064"
|
||||
title: "Ratcheted CodeScene thresholds as the quality gate baseline"
|
||||
status: active
|
||||
date: 2026-04-14
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0018 established CodeScene code-health gates so Tolaria could block regressions before code reached `main`. Since then, the codebase has improved materially and the tracked baseline in `.codescene-thresholds` has been ratcheted above the original 9.50 / 9.31 minimums.
|
||||
|
||||
Leaving ADR-0018 active would make the architecture record stale: the enforced thresholds are now stricter than the decision document says, and the current workflow intentionally tightens them as the project's sustained health improves.
|
||||
|
||||
## Decision
|
||||
|
||||
**Supersede ADR-0018 and treat `.codescene-thresholds` as the ratcheted policy baseline for Tolaria's CodeScene gate.** The current required minimums are `HOTSPOT_THRESHOLD=9.84` and `AVERAGE_THRESHOLD=9.45`. Thresholds move upward only when the repository can sustain a stricter baseline without immediately regressing.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Ratchet the enforced thresholds and document the new baseline** (chosen): keeps the ADRs aligned with the real gate, preserves the Boy Scout Rule, and makes code-health expectations stricter as the codebase improves.
|
||||
- **Keep ADR-0018 active and treat higher thresholds as an implementation detail**: lower documentation churn, but the active ADR would no longer describe the actual CI and hook policy.
|
||||
- **Remove numeric thresholds from ADRs entirely**: more durable on paper, but loses the explicit quality bar that developers are expected to maintain.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `.codescene-thresholds` is now the authoritative location for the current numeric gate values.
|
||||
- ADRs must be superseded again if Tolaria makes another meaningful policy jump in CodeScene thresholds.
|
||||
- Pre-push and related quality checks now enforce a stricter floor than ADR-0018 described.
|
||||
- The quality gate remains intentionally one-way: relaxing thresholds would require an explicit architectural reversal, not a quiet config edit.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0065"
|
||||
title: "Root-managed AI guidance files with Claude shim"
|
||||
status: active
|
||||
date: 2026-04-14
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria now supports multiple local CLI agents, but vault-level guidance still carried legacy assumptions. Existing vault bootstrap and repair flows centered on `config/agents.md`, while modern coding agents expect instructions at the vault root. That mismatch made managed guidance harder to reason about, left Claude Code compatibility implicit, and gave the UI no reliable way to distinguish between Tolaria-managed files that can be repaired and user-authored custom guidance that must be preserved.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria manages vault AI guidance at the vault root.** `AGENTS.md` is the canonical shared guidance file, `CLAUDE.md` is a compatibility shim that points Claude Code back to `AGENTS.md`, and Tolaria classifies both files as `managed`, `missing`, `broken`, or `custom` so repair flows restore only Tolaria-managed guidance without overwriting custom user files.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Root `AGENTS.md` as canonical plus a root `CLAUDE.md` shim** (chosen): matches current agent expectations, keeps one source of truth for shared instructions, and makes repair status explicit.
|
||||
- **Keep managed guidance under `config/agents.md`**: preserves the older structure, but hides a user-facing integration contract behind legacy config paths and keeps Claude compatibility indirect.
|
||||
- **Maintain separate full instruction files for each agent**: simple per tool, but duplicates instructions and increases drift risk whenever guidance changes.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New and repaired vaults now seed `AGENTS.md` and `CLAUDE.md` at the vault root.
|
||||
- Legacy `config/agents.md` content is migrated forward when safe, then the obsolete file is removed.
|
||||
- The status bar and command palette can expose a first-class restore action because backend guidance state is normalized.
|
||||
- Custom root guidance files are preserved instead of being silently overwritten by repair flows.
|
||||
- Tolaria keeps a single shared guidance document even while supporting multiple CLI agents.
|
||||
- Re-evaluate if supported agents stop relying on root-level files or if future agent integrations require materially different vault instructions instead of a shared source of truth.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0066"
|
||||
title: "Calendar-semver versioning for alpha and stable releases"
|
||||
status: active
|
||||
date: 2026-04-16
|
||||
supersedes: "0057"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0057 kept Tolaria on two updater channels and used "next stable patch" semver for alpha builds. That preserved ordering, but it no longer matched the agreed product naming:
|
||||
|
||||
- Alpha should display as `Alpha YYYY.M.D.N`
|
||||
- Alpha should ship the technical version `YYYY.M.D-alpha.N`
|
||||
- Stable should ship and display as `YYYY.M.D`
|
||||
|
||||
The naming change still needs to stay semver-safe when users switch between Stable and Alpha. A pure same-day calendar alpha would become older than a same-day stable promotion, so the workflow needs a monotonicity guard in addition to cleaner display strings.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria keeps exactly two updater channels (`stable` and `alpha`), but both now use calendar-semver release numbers.** Stable promotions use `stable-vYYYY.M.D` tags and stamp the technical version `YYYY.M.D`. Every push to `main` publishes an alpha build with technical version `YYYY.M.D-alpha.N` and display label `Alpha YYYY.M.D.N`.
|
||||
|
||||
If the latest stable tag already uses the current UTC calendar date, the alpha workflow advances to the next calendar day before assigning `-alpha.N`. That keeps alpha semver-newer than the most recent stable build even after a same-day promotion.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Calendar semver with a next-day safeguard** (chosen): matches the agreed naming, keeps user-facing labels clean, and preserves updater ordering across channel switches.
|
||||
- **Calendar semver without a safeguard**: simplest display model, but alpha can become semver-older than Stable after a same-day promotion.
|
||||
- **Keep ADR-0057's next-patch prerelease numbering**: semver-safe, but it does not match the agreed release naming or the product surfaces that should show calendar-based versions.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Release workflows now compute both a technical version and a display version.
|
||||
- User-facing version surfaces strip technical prerelease noise into clean labels (`Alpha YYYY.M.D.N` or `YYYY.M.D`).
|
||||
- Stable promotions must use `stable-vYYYY.M.D` tags instead of patch-based semver tags.
|
||||
- Alpha sequence numbers are scoped to a calendar core date and remain compatible with the updater manifests.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0067"
|
||||
title: "AutoGit idle and inactive checkpoints"
|
||||
status: active
|
||||
date: 2026-04-17
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria already had explicit git actions in the status bar (ADR-0032) and a remote-aware manual commit flow (ADR-0059), but git-backed vaults still depended on the user remembering to create checkpoints. That worked for deliberate commits, yet it left a gap for ordinary writing sessions where the app had already saved all note content but no git checkpoint had been recorded.
|
||||
|
||||
The new checkpointing behavior needed to stay conservative:
|
||||
|
||||
- never run for non-git vaults
|
||||
- never commit unsaved editor buffers
|
||||
- reuse the same remote detection and local-only fallback as the manual commit flow
|
||||
- avoid drift between timer-driven checkpoints and the status-bar quick commit action
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria introduces installation-local AutoGit settings plus a dedicated `useAutoGit` hook that triggers a shared `useCommitFlow.runAutomaticCheckpoint()` path after configurable idle or inactive thresholds.** The checkpoint runs only when the current vault is git-backed, there are pending saved changes (or local commits waiting to push), and no unsaved edits remain.
|
||||
|
||||
`useCommitFlow.runAutomaticCheckpoint()` is now the single checkpoint runner for both AutoGit and the status-bar quick commit action. That shared path generates deterministic automatic commit messages (`Updated N note(s)` / `Updated N file(s)`), commits locally when no remote exists, and can also do a push-only retry when commits already exist locally.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): A shared checkpoint runner used by both AutoGit timers and the quick commit action. Pros: one git policy, one message generator, one remote-handling path. Cons: adds another cross-cutting settings-driven hook.
|
||||
- **Option B**: A separate background AutoGit implementation. Pros: could evolve independently from the manual commit flow. Cons: high risk of drift in commit messages, push behavior, and remote handling.
|
||||
- **Option C**: Commit on every save. Pros: simplest trigger model. Cons: far too noisy for git history, especially with Tolaria's autosave model.
|
||||
|
||||
## Consequences
|
||||
|
||||
- App settings now persist `autogit_enabled`, `autogit_idle_threshold_seconds`, and `autogit_inactive_threshold_seconds` in installation-local settings storage.
|
||||
- `useAutoGit` tracks editor activity plus app focus/visibility state and triggers checkpoints after the configured thresholds.
|
||||
- Automatic checkpoints are blocked while unsaved edits exist, so AutoGit only records content that is already flushed through the normal save pipeline.
|
||||
- The bottom-bar quick commit action now reuses the same checkpoint runner after forcing a save, keeping manual and automatic checkpoint behavior aligned.
|
||||
- Vaults without a remote still benefit: AutoGit uses the existing local-only commit behavior from ADR-0059 instead of treating missing remotes as an error.
|
||||
- Re-evaluate if users need per-vault policy instead of installation-local policy, or if timer-driven checkpoints create too much git noise in real-world use.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0068"
|
||||
title: "H1-only title surface with optional untitled auto-rename"
|
||||
status: active
|
||||
date: 2026-04-17
|
||||
supersedes: "0055"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0055 removed the legacy title row and made the editor body the only title surface. That ADR also kept one strong filename behavior from ADR-0044: untitled notes would auto-rename from their first H1 on save.
|
||||
|
||||
That always-on rename rule turned out to be too rigid. Some users want the H1 to drive the displayed title immediately, but prefer to keep the synthetic `untitled-*` filename stable until they explicitly rename it from the breadcrumb bar. The product needed to preserve the H1-only editing model without forcing every installation into automatic filename changes.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria keeps the editor body as the only title surface, but untitled-note auto-rename from the first H1 becomes an installation-local setting (`initial_h1_auto_rename_enabled`) that defaults to enabled.**
|
||||
|
||||
When the setting is enabled, untitled notes continue to auto-rename on save as soon as a real H1 title exists. When disabled, Tolaria still treats the H1 as the canonical display title, but it leaves the filename unchanged until the user explicitly renames it through the breadcrumb controls.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Keep the current auto-rename behavior as the default, but make it an installation-local preference. Pros: preserves the fast path for most users while allowing opt-out for users who want stable temporary filenames. Cons: different installs can behave differently.
|
||||
- **Option B**: Keep auto-rename mandatory, as assumed by ADR-0055. Pros: one simple filename policy. Cons: surprises users who want title editing without immediate file renames.
|
||||
- **Option C**: Turn auto-rename off for everyone. Pros: filenames only change on explicit user action. Cons: leaves more `untitled-*` files around and adds friction to the common case.
|
||||
|
||||
## Consequences
|
||||
|
||||
- App settings now persist `initial_h1_auto_rename_enabled` in installation-local settings storage.
|
||||
- The save pipeline consults that setting before scheduling untitled-file renames.
|
||||
- Disabling untitled auto-rename does not restore any legacy title field or alternate title UI. H1 remains the only editor title surface.
|
||||
- When the setting is off, display title and filename can diverge for longer: the note may show a human H1 while the file remains `untitled-*` until explicit rename.
|
||||
- Settings UI and command/search affordances now expose this filename policy as a user preference rather than a hardcoded rule.
|
||||
- Re-evaluate if users later need this policy to be per-vault instead of installation-local, or if longer-lived untitled filenames create too much Finder/git noise.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0069"
|
||||
title: "Neighborhood mode for note-list relationship browsing"
|
||||
status: active
|
||||
date: 2026-04-19
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria already had a relationship-browsing state behind `SidebarSelection.kind === 'entity'`, but the product language and interaction model were still fuzzy. The pinned source note rendered as a special card instead of a normal note row, grouped relationship results were deduplicated across sections, and Cmd-click behaved like a legacy "open separately" affordance rather than a clear graph-navigation action.
|
||||
|
||||
The new note-list flow needed an explicit product concept for browsing related notes around a source note, plus keyboard semantics that matched the mouse flow. The team also wanted the list to preserve graph truth instead of collapsing overlapping relationships away when a note legitimately belonged to multiple groups.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria formalizes `SidebarSelection.kind === 'entity'` as Neighborhood mode.** The note list now treats the selected note as the neighborhood source, pins it at the top using the standard active note-row styling, shows outgoing relationship groups first and inverse/backlink groups after, keeps empty groups visible with count `0`, and allows the same note to appear in multiple groups when multiple relationships are true.
|
||||
|
||||
**Neighborhood navigation is a distinct pivot action.** Plain click and plain `Enter` open the focused note without replacing the current neighborhood. Cmd/Ctrl-click and Cmd/Ctrl-`Enter` open the note and pivot the note list into that note's Neighborhood.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Reuse the existing `entity` selection as Neighborhood mode** (chosen): keeps the state model localized, avoids a second nearly-identical note-list mode, and lets sidebar navigation exit Neighborhood by selecting any other sidebar target. Cons: code still uses the historical `entity` name internally.
|
||||
- **Add a new `neighborhood` selection variant**: clearer internal naming, but it duplicates the same source-note payload and would force wider selection-handling churn across the app for little product gain.
|
||||
- **Keep the old implicit entity-browsing behavior**: lowest short-term engineering effort, but it leaves the product terminology inconsistent and preserves interaction mismatches like deduped groups and non-pivot Cmd-click behavior.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Product, tests, and docs now refer to Neighborhood as a first-class note-list browsing mode.
|
||||
- The note list preserves overlapping graph evidence: one note can appear in multiple groups when multiple relationships are true.
|
||||
- Keyboard-only browsing now matches the pointer flow: arrow keys/open keep the current neighborhood, while Cmd/Ctrl-`Enter` pivots it.
|
||||
- Sidebar navigation remains the exit path from Neighborhood because the app still models the mode through the existing selection union.
|
||||
- Internal code still uses the `entity` discriminator, so future refactors should treat "entity selection" and "Neighborhood mode" as the same concept unless a broader navigation redesign justifies a new selection shape.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0070"
|
||||
title: "Starter vaults are local-first with explicit remote connection"
|
||||
status: active
|
||||
date: 2026-04-19
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0046 moved the Getting Started vault to a public GitHub repo cloned at runtime, and ADR-0059 established that Tolaria should support valid local-only vaults without treating a missing remote as an error.
|
||||
|
||||
That still left one mismatch: a freshly cloned starter vault inherited the template repo's `origin` remote. New users therefore landed in a vault that looked remote-backed by default, even though the intended workflow was to explore locally first and only connect a personal remote later. Keeping the starter remote also risked accidental pushes to the public template repo and gave Tolaria no safe place to reject incompatible remotes before tracking started.
|
||||
|
||||
## Decision
|
||||
|
||||
**After cloning the public starter vault, Tolaria removes every configured git remote so the vault opens local-only by default.** Users connect a remote later through an explicit Add Remote flow exposed from the `No remote` status-bar chip and the command palette.
|
||||
|
||||
**The new `git_add_remote` backend is the only path for attaching a remote to an existing local-only vault.** It adds `origin`, fetches the remote, rejects incompatible or ahead histories, and only starts tracking when the remote is safe for the current local repo.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Strip starter-vault remotes and add an explicit connect flow** (chosen): preserves a local-first onboarding experience, matches ADR-0059's local-only model, and prevents accidental coupling to the public template repo. Cons: users who want sync must do one extra explicit step.
|
||||
- **Keep the starter repo's remote attached**: simplest implementation, but it makes the template repo look like the user's real sync target and increases the risk of accidental pushes or confusing remote state.
|
||||
- **Force remote replacement during onboarding**: guarantees a personal remote up front, but adds too much setup friction to the Getting Started path and weakens Tolaria's offline/local-first story.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Fresh Getting Started vaults now behave like any other local-only vault: commit locally first, then opt into sync later.
|
||||
- The app gains a dedicated Add Remote UX (`AddRemoteModal`) plus a backend connection path (`git_add_remote`) instead of overloading clone or commit flows.
|
||||
- Remote attachment is safer: Tolaria can reject unrelated or incompatible histories before the vault starts tracking a remote.
|
||||
- The starter repo remains a distribution source only, not an ongoing sync destination.
|
||||
- Re-evaluate if Tolaria later needs a faster "publish this local starter vault to my own repo" flow that should prefill or streamline the Add Remote step.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0071"
|
||||
title: "External vault updates reload derived state and reopen the clean active note"
|
||||
status: superseded
|
||||
date: 2026-04-21
|
||||
superseded_by: "0111"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0002 makes the filesystem the source of truth, and ADR-0043 keeps locally edited frontmatter reactive inside the running UI. But external vault mutations still had a gap. A `git pull` or an AI agent edit could change notes on disk while the app kept showing stale note-list state, stale derived relationships, or an editor surface that still rendered the pre-refresh BlockNote document.
|
||||
|
||||
The fix needed to satisfy a few constraints at once:
|
||||
|
||||
- refresh all vault-derived UI, not just the main note list
|
||||
- preserve unsaved local edits instead of clobbering them with disk state
|
||||
- reopen the active note from disk when it is safe, even if another file changed, because backlinks, inverse relationships, and other derived surfaces can depend on the whole vault
|
||||
- handle the native editor case where an in-place file update requires a full tab reopen to show the fresh document reliably
|
||||
|
||||
## Decision
|
||||
|
||||
**All external vault mutations now reconcile through one shared refresh path that reloads vault-derived state and then conditionally reopens the active note from disk.**
|
||||
|
||||
Tolaria now routes post-pull refreshes and AI-agent file modifications through the same `refreshPulledVaultState()` helper.
|
||||
|
||||
That shared path does the following:
|
||||
|
||||
1. Reload `vault.entries`, folders, and saved views together.
|
||||
2. If there is no active note, stop after the reload.
|
||||
3. If the active note has unsaved local edits, keep the current editor buffer and do not replace it from disk.
|
||||
4. Otherwise, find the refreshed `VaultEntry` for the active note and replace the active tab with freshly loaded disk content.
|
||||
5. If the active file itself changed in place during the external update, close the tab before reopening it so BlockNote fully remounts onto the new document.
|
||||
6. If the active file no longer exists after the reload, close the open tab state instead of leaving a stale editor behind.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Shared external-refresh reconciler** (chosen): one policy for pulls and agent edits, consistent vault-derived UI, and explicit protection for unsaved local edits. Cons: more coupling between sync flows and tab management.
|
||||
- **Patch only the changed surfaces ad hoc**: smaller individual fixes, but high risk of drift between pull handling, agent handling, and future external-write paths.
|
||||
- **Always force a full app-level reload**: simplest correctness story, but too disruptive and more likely to throw away user context unnecessarily.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Any workflow that mutates the vault externally, such as git pulls or agent writes, should go through the shared refresh reconciler rather than reloading a single surface in isolation.
|
||||
- Clean active notes now converge back to on-disk truth automatically after external updates.
|
||||
- Unsaved local edits remain protected from external refreshes, even when the rest of the vault reloads.
|
||||
- Folder, saved-view, backlink, and inverse-relationship surfaces stay aligned with the refreshed vault, not just the editor tab.
|
||||
- Tolaria now treats "refresh after external mutation" as a first-class synchronization concern rather than a per-feature fix.
|
||||
- Re-evaluate if the editor gains a reliable in-place document reset API, because that could remove the need for the close-and-reopen step when the active file itself changed.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0072"
|
||||
title: "Confirmed vault paths gate startup state"
|
||||
status: active
|
||||
date: 2026-04-22
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's startup path was assuming that any incoming `vaultPath` was authoritative immediately. In practice, boot can pass through transient empty paths and stale paths that no longer correspond to the persisted active vault. That produced two classes of regressions:
|
||||
|
||||
1. `useVaultLoader` fired `reload_vault` and `get_modified_files` before a real vault path existed, generating avoidable warnings and backend calls for `""`.
|
||||
2. On fresh install or other non-persisted startup cases, a missing path could incorrectly render `vault-missing` instead of the intended welcome flow.
|
||||
|
||||
Tolaria's onboarding and vault-loading surfaces need the same invariant: only a confirmed vault identity should drive startup side effects or missing-vault error UI.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria now treats a vault path as authoritative at startup only after it is confirmed.** Vault-loading side effects no-op until the path is non-empty, and the `vault-missing` onboarding state is shown only when the missing path was the persisted active vault recorded in `load_vault_list`. Otherwise, startup falls back to `welcome`.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): gate startup effects and missing-vault UI on confirmed vault identity. This keeps boot deterministic, avoids empty-path backend calls, and preserves the product rule that fresh installs should land in Welcome rather than an error state.
|
||||
- **Option B**: treat any startup `vaultPath` as authoritative immediately. Simpler branching, but it keeps the existing race where transient or stale paths trigger warnings and the wrong onboarding state.
|
||||
- **Option C**: special-case each startup surface independently. Lower immediate churn, but it would duplicate boot logic and let `useOnboarding` and `useVaultLoader` drift again.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `useVaultLoader` must guard all startup work behind a real non-empty vault path.
|
||||
- `useOnboarding` must consult persisted vault state before deciding that a missing path represents a deleted active vault.
|
||||
- Fresh installs, cleared vault lists, and other startup flows without a confirmed active vault should resolve to `welcome`, even if an initial path probe fails.
|
||||
- Re-evaluate if Tolaria introduces deeper startup routing (for example multiple launch intents or restored workspaces) that needs a richer boot-state model than the current confirmed-path gate.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0073"
|
||||
title: "Persistent linkify protocol registry across editor remounts"
|
||||
status: active
|
||||
date: 2026-04-22
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria keeps a single editor shell alive while users swap notes and toggle between BlockNote and raw mode. The upstream BlockNote/Tiptap link stack assumes linkify protocol registration is effectively one-shot per editor lifetime, and Tiptap's link extension resets that registry on destroy.
|
||||
|
||||
In Tolaria's lifecycle, that behavior caused duplicate `linkifyjs: already initialized` warnings during note-open and editor-remount flows. The problem is cross-cutting: BlockNote and Tiptap both participate, and the failure only disappears when protocol registration survives teardown/remount cycles instead of being repeated opportunistically.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria patches the upstream BlockNote and Tiptap link packages so custom linkify protocols are pre-registered once per app runtime and are not reset on editor teardown.** The patched packages coordinate through `globalThis` flags, and Tolaria tracks them via `pnpm` patched dependencies rather than ad hoc runtime monkey-patching inside app code.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): maintain explicit `pnpm` patches for the affected upstream packages, pre-register the needed protocols once, and preserve the registry across remounts. This matches Tolaria's persistent editor shell and keeps the behavior deterministic in both dev and packaged builds.
|
||||
- **Option B**: keep upstream behavior and tolerate or suppress the warnings locally. Lower maintenance, but it leaves editor lifecycle correctness dependent on noisy duplicate initialization and makes future regressions harder to reason about.
|
||||
- **Option C**: add Tolaria-side runtime monkey-patches around editor mount/unmount. Avoids vendoring patches, but spreads dependency-specific lifecycle logic into application code and is more fragile across package upgrades.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `pnpm-workspace.yaml` now treats the relevant BlockNote and Tiptap link packages as patched dependencies, so upgrades must preserve or consciously replace those patches.
|
||||
- Editor teardown in Tolaria must not assume ownership of the global linkify protocol registry.
|
||||
- Smoke coverage for note open, editor remount, and raw-mode toggling must stay in place because the failure mode is lifecycle-specific rather than feature-specific.
|
||||
- Re-evaluate this ADR if upstream BlockNote/Tiptap gains a supported lifecycle-safe protocol-registration model that makes the Tolaria patches unnecessary.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0074"
|
||||
title: "Explicit external AI tool setup and least-privilege desktop scope"
|
||||
status: active
|
||||
date: 2026-04-22
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's first MCP integration optimized for zero setup: desktop startup auto-registered the Tolaria MCP server in Claude Code and Cursor config files, the Tauri asset protocol allowed every local path, and app-managed Codex sessions launched with the CLI's dangerous bypass flag. That made the product feel convenient, but it also widened trust by default in places that users could not see or consent to clearly.
|
||||
|
||||
The product direction now favors least-privilege defaults. Fresh installs should not silently edit third-party config files, external AI tool setup must be intentional and reversible, and the desktop shell should only expose the filesystem paths that the active vault actually needs.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria now treats external AI tool wiring as an explicit user action and keeps the desktop shell scoped to the active vault.**
|
||||
|
||||
- The app still spawns its local MCP WebSocket bridge on desktop startup, but it no longer auto-registers third-party MCP config files.
|
||||
- External MCP registration is exposed through a keyboard-accessible setup flow reachable from the command palette and status surfaces. Confirming the flow upserts Tolaria's MCP entry for the current vault; cancel leaves external config untouched; disconnect removes Tolaria's entry again.
|
||||
- The Tauri asset protocol remains enabled for local vault images, but its static config scope is empty. Tolaria grants recursive asset access only to the active vault at runtime when that vault is reloaded.
|
||||
- App-managed Codex sessions use the CLI's normal approval and sandbox path by default instead of opting into the dangerous bypass mode automatically.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Explicit setup + runtime vault-only scope** (chosen): aligns with least-privilege defaults, keeps command-palette discoverability, preserves image loading and external-tool support, and makes every privileged step visible and reversible.
|
||||
- **Keep startup auto-registration and global asset scope**: lowest friction, but it silently mutates third-party config and leaves the desktop shell effectively open to every local file path.
|
||||
- **Disable external MCP registration entirely**: safest on paper, but it removes a valuable workflow for Claude Code, Cursor, and other MCP-compatible tools that Tolaria intentionally supports.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Fresh installs no longer modify `~/.claude/mcp.json` or `~/.cursor/mcp.json` until the user confirms setup.
|
||||
- Switching vaults does not silently retarget external MCP clients; users reconnect explicitly when they want a different vault exposed.
|
||||
- Desktop asset access is constrained to the active vault instead of all filesystem paths, while note images and attachments continue to load normally.
|
||||
- The command palette and status bar now expose an explicit external AI tools setup/remove flow that supports keyboard-only QA.
|
||||
- Codex agent sessions are safer by default, at the cost of relying on the CLI's normal approval path instead of bypassing it automatically.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0075"
|
||||
title: "Crash-safe note rename transactions"
|
||||
status: active
|
||||
date: 2026-04-22
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's note rename path used a simple "write new file, then delete old file" flow. That was easy to implement, but it had three integrity problems called out by issue #205: it could leave a visible duplicate note if the app crashed between those steps, destination-path selection depended on check-then-use races, and backlink rewrite failures were collapsed into a generic updated-files count that made partial success look clean.
|
||||
|
||||
Rename is a core vault integrity operation. The app needs a flow that preserves a trustworthy visible state even when the process dies mid-rename, and it needs to surface any partial backlink rewrite failures clearly enough that users are not told everything updated when some linked files still need manual repair.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria now stages note renames through a hidden per-vault transaction directory and recovers unfinished transactions on the next vault scan.**
|
||||
|
||||
- A rename that changes the file path first writes a transaction manifest plus a hidden backup path inside `<vault>/.tolaria-rename-txn/`.
|
||||
- Tolaria moves the old note into that hidden backup, persists the new file with a no-clobber destination write, and then deletes the backup/manifest only after the new note exists.
|
||||
- If the process crashes before the new note is committed, the next `scan_vault` restores the hidden backup back to the original path before listing entries.
|
||||
- Manual filename renames keep their explicit conflict semantics, but the final destination is now claimed with a no-clobber write instead of relying on an existence check.
|
||||
- Backlink rewrites now return both the number of successful updates and the number of failed updates so the UI can warn about partial success instead of reporting a clean rename.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Hidden transaction directory + scan-time recovery** (chosen): keeps in-flight rename artifacts out of the visible vault model, gives Tolaria a deterministic recovery point after crashes, and lets the final destination use no-clobber persistence.
|
||||
- **Rename in place without transaction metadata**: simpler, but it cannot recover a half-finished rename reliably after process death and still leaves either duplicate or missing-note windows.
|
||||
- **Best-effort duplicate cleanup with no recovery path**: lowest implementation cost, but it leaves the user-visible vault state dependent on exact crash timing and does not meet the trustworthiness goal for rename operations.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every vault can now contain a hidden `.tolaria-rename-txn/` directory managed by Tolaria; scan and folder UI continue to ignore it because hidden directories are already excluded.
|
||||
- Rename results are richer: the frontend must treat `failed_updates > 0` as a warning state even when the rename itself succeeded.
|
||||
- Future changes to vault scanning or note rename behavior must preserve transaction recovery before entry listing, otherwise crash safety regresses.
|
||||
- The rename path no longer silently overwrites a destination discovered via a stale existence check; title-driven renames retry with suffixed filenames, while explicit filename renames fail cleanly on collision.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0076"
|
||||
title: "Note retargeting separates type changes from folder moves"
|
||||
status: active
|
||||
date: 2026-04-22
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0025 made `type:` the canonical classification field, and ADR-0033 reopened subfolders as a valid way to organize files in the vault. Once Tolaria exposed both type sections and the folder tree in the sidebar, note reorganization had an unresolved ambiguity: does retargeting a note mean changing its semantic type, moving its file, or both?
|
||||
|
||||
Without an explicit model, drag-and-drop and command-palette flows would need to duplicate their own validation and persistence logic, and Tolaria could easily drift back toward the old type-folder coupling that ADR-0006 deliberately removed.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria treats note retargeting as one shared interaction model with two distinct mutation paths: types change metadata, folders change file paths.**
|
||||
|
||||
- Retargeting a note to a type updates only the note's `type:` frontmatter. The file stays where it is.
|
||||
- Retargeting a note to a folder preserves the current filename and `type:` value, and moves the file through the same crash-safe rename transaction pipeline used for backend rename commands.
|
||||
- Drag/drop targets and command-palette actions both route through the same frontend retargeting abstraction so validation, dialogs, collision handling, and success/error behavior stay consistent.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Shared retargeting model with separate type-vs-folder semantics** (chosen): preserves ADR-0025/ADR-0006's decoupling of type from path, lets folder moves reuse ADR-0075's crash-safe rename guarantees, and keeps multiple UI surfaces behaviorally aligned.
|
||||
- **Treat folders as the source of truth for note type**: simpler mental model for some vaults, but it reintroduces path-based type inference and makes type changes depend on file moves again.
|
||||
- **Implement drag/drop and command-palette retargeting as separate flows**: lower short-term coordination cost, but it duplicates mutation rules and makes consistency regressions likely.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Type sections are semantic targets only; they must never imply a filesystem move.
|
||||
- Folder targets are physical move operations; they must preserve filename/title behavior, reject collisions, and rewrite path-based wikilinks through the shared rename pipeline.
|
||||
- Future note-retargeting surfaces should reuse the shared retargeting abstraction instead of introducing another mutation path.
|
||||
- Re-evaluate this ADR if Tolaria later supports bulk retargeting, folder rules that intentionally infer type, or another organization primitive that needs different semantics.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0077"
|
||||
title: "Concurrent-safe vault cache replacement"
|
||||
status: active
|
||||
date: 2026-04-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0014 and ADR-0024 established Tolaria's git-based persistent vault cache and moved it outside the vault directory. That cache was still being rewritten with a simple temp-file-and-rename flow.
|
||||
|
||||
Once Tolaria started reopening the same vault from multiple windows/processes more often, that write model became too optimistic: two scans could both build valid cache payloads from different moments in time, and the slower writer could still atomically replace a fresher cache written by another window. The cache needed cross-window safety without introducing a long-lived coordinator process or making vault open dependent on heavyweight IPC.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria now treats vault-cache replacement as a best-effort compare-and-swap operation instead of an unconditional atomic overwrite.**
|
||||
|
||||
- Each scan still builds the next cache payload in memory and writes it to a temp file first.
|
||||
- Before replacing the real cache file, Tolaria acquires a short-lived lock file for that vault cache path.
|
||||
- After the lock is acquired, Tolaria rechecks the on-disk cache fingerprint and only renames the temp file into place if another window/process has not already refreshed the cache.
|
||||
- If the cache changed underneath the current scan, Tolaria skips the replace and keeps the newer on-disk cache.
|
||||
- Stale cache-write locks are garbage-collected after a short timeout so a crashed writer does not block future refreshes.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Lock + fingerprint guarded replacement** (chosen): keeps the cache file external and file-based, avoids overwriting fresher cache state from another Tolaria window, and preserves graceful fallback to filesystem rescans. Cons: cache writes become best-effort rather than guaranteed after every scan.
|
||||
- **Keep unconditional temp-file + rename**: simplest implementation, but concurrent windows can regress the cache to an older view even though each individual replace is atomic.
|
||||
- **Centralized cache service or long-lived process mutex**: strongest coordination story, but too much operational complexity for a local desktop app and would create new failure modes around boot, process lifetime, and IPC.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Tolaria's cache correctness model is now "latest successful guarded replace wins," not "every scan must write a cache file."
|
||||
- Cache refreshes must tolerate a skipped write when another window/process already produced a fresher cache.
|
||||
- Temp-file writes and renames still provide corruption resistance, but freshness is protected separately by the writer lock and fingerprint check.
|
||||
- Cache-write failures remain non-fatal: Tolaria logs them and falls back to rebuilding from the filesystem when needed.
|
||||
- Re-evaluate if Tolaria later needs stronger cross-process coordination than lock-file + fingerprint checks can provide.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0078"
|
||||
title: "Scoped unsigned fallback for app-managed git commits"
|
||||
status: active
|
||||
date: 2026-04-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0021, ADR-0059, and ADR-0070 all assume Tolaria can create and advance a local git-backed vault without asking users to debug git internals first. In practice, inherited `commit.gpgsign` settings were breaking that promise: a missing or misconfigured GPG/SSH signing helper could block the initial `Initial vault setup` commit during onboarding and could also strand later app-triggered commits behind opaque signing failures.
|
||||
|
||||
Tolaria needed a policy that kept signed workflows intact when the user's signing setup actually works, while still ensuring app-managed git operations do not become unusable just because a desktop environment cannot reach the signing helper.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria uses a scoped unsigned fallback for app-managed commits instead of requiring signing to succeed unconditionally.**
|
||||
|
||||
- The onboarding/setup commit (`Initial vault setup`) always runs with `commit.gpgsign=false` for that single git invocation.
|
||||
- Normal app-managed `git_commit` calls still honor the user's existing git signing configuration first.
|
||||
- If a commit fails and Git's error matches a signing-helper failure, Tolaria retries that same app-managed commit once with signing disabled.
|
||||
- Tolaria does not rewrite the user's git config and does not broaden the retry to unrelated commit failures.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Scoped unsigned fallback for app-managed commits** (chosen): keeps onboarding and local commit flows resilient while still preserving signed commits when the user's environment supports them. Cons: some Tolaria-created commits may be unsigned on machines with broken signing setups.
|
||||
- **Require signing to succeed for every commit**: simplest policy, but it turns missing desktop GPG/SSH helpers into app-breaking failures during onboarding and normal use.
|
||||
- **Disable signing for all Tolaria-triggered commits**: maximally robust, but it would silently bypass working signing setups and weaken users' expected git security posture.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New vault creation is no longer blocked by inherited signing settings that only fail in Tolaria's app context.
|
||||
- Users with healthy signing setups still get signed Tolaria commits after the first normal attempt succeeds.
|
||||
- Signing-failure detection must stay narrow so Tolaria does not mask unrelated git errors behind an unsigned retry.
|
||||
- Tolaria's git integration now explicitly prefers "complete the app-managed commit safely" over "preserve signing at all costs" when the signing helper is unavailable.
|
||||
- Re-evaluate if Tolaria later exposes per-vault git policy controls or needs a richer user-facing explanation for when a fallback unsigned commit was used.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0079"
|
||||
title: "Linux window chrome and menu reuse"
|
||||
status: active
|
||||
date: 2026-04-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's desktop shell was designed around macOS window chrome. `titleBarStyle: "Overlay"` and `hiddenTitle: true` give the app a clean single-surface titlebar on macOS, but Linux ignores those flags and draws native GTK decorations and a native menu bar on top of the React UI. That creates a double-titlebar effect, mismatched theming, and inconsistent behavior between the main window and detached note windows.
|
||||
|
||||
We still need Linux to reuse Tolaria's existing command palette, shortcut manifest, and deterministic menu-command routing instead of inventing a Linux-only command path.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria uses custom React-rendered window chrome on Linux and routes its menu through the existing shared command IDs.**
|
||||
|
||||
- The main Tauri window disables server-side decorations on Linux during app setup.
|
||||
- Detached note windows set `decorations: false` when Linux chrome is active.
|
||||
- `LinuxTitlebar` renders the drag region, resize handles, and window controls for Linux windows.
|
||||
- `LinuxMenuButton` mirrors the app's File/Edit/View/Go/Note/Vault/Window menus, but dispatches the existing command IDs through `trigger_menu_command`.
|
||||
- The native Tauri menu bar is not mounted on Linux; macOS and other existing desktop targets keep the native menu.
|
||||
- Shared shortcuts remain defined in `appCommandCatalog.ts`, including `Cmd+Shift+L` on macOS and `Ctrl+Shift+L` on Linux through the same command manifest.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **React-rendered Linux chrome with shared command IDs** (chosen): keeps Linux visually aligned with Tolaria's existing shell and preserves one command-routing model across keyboard shortcuts, menu clicks, and QA helpers. Cons: Tolaria now owns Linux window chrome behavior directly.
|
||||
- **Keep native GTK decorations and menu bar on Linux**: cheaper to ship, but it breaks visual consistency and produces overlapping titlebar/menu surfaces that do not match the rest of the app.
|
||||
- **Introduce Linux-only command wiring for the custom menu**: would allow a Linux-specific implementation, but it would fork the shortcut/menu architecture and weaken deterministic QA.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Linux main windows and detached note windows now present one consistent titlebar surface controlled by Tolaria.
|
||||
- Menu commands, command palette actions, and deterministic QA still share the same command IDs, which limits platform-specific drift.
|
||||
- Linux packaging and CI must install WebKit2GTK 4.1 dependencies and produce Linux bundles explicitly.
|
||||
- Tolaria now owns Linux resize handles, maximize/minimize/close behavior, and titlebar drag-region behavior in the renderer, so regressions in those surfaces require direct tests.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0080"
|
||||
title: "Cross-platform desktop release artifacts and portable vault names"
|
||||
status: active
|
||||
date: 2026-04-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's release pipeline and file validation rules were still biased toward macOS. Alpha/stable releases only produced first-class macOS artifacts, stable download redirects assumed a DMG-only world, and vault file/folder validation allowed names that work on macOS/Linux but break on Windows clones and sync targets.
|
||||
|
||||
Shipping Windows as a supported desktop target requires both distribution and data portability to become explicit. A Windows installer is not enough if shared vault content can still produce invalid filenames on that platform, and cross-platform updater manifests must keep Tauri's signed updater artifact separate from the user-facing installer download.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria ships first-class macOS, Windows x64, and Linux x64 desktop artifacts, and its vault-facing filename rules are portable across those platforms by default.**
|
||||
|
||||
- Alpha and stable release workflows build and publish macOS, Windows x64, and Linux x64 artifacts from the same release tag/version computation.
|
||||
- `latest.json` manifests continue to point Tauri updater clients at signed updater artifacts through `url`, while manual installer/download links are exposed separately via platform-specific fields such as `dmg_url` and `download_url`.
|
||||
- The stable download page resolves the best current platform download from that manifest plus release assets, instead of assuming macOS-only DMG delivery.
|
||||
- Note filename renames, folder creation/rename flows, and custom view filenames all share one portable validation rule set that rejects Windows reserved device names, invalid characters, and trailing dot/space suffixes.
|
||||
- Shortcut labels shown in the UI are derived from the shared command manifest so non-macOS builds display `Ctrl`-style accelerators instead of macOS glyphs.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Cross-platform artifacts + portable filename rules** (chosen): makes Windows support real instead of nominal, keeps updater behavior compatible with Tauri, and prevents cross-OS vault breakage at the point of write. Cons: more CI matrix surface area and more platform-specific packaging constraints.
|
||||
- **Ship Windows installers but keep existing filename validation**: lowers immediate implementation cost, but Windows users would still hit invalid vault content created elsewhere and trust in sync portability would stay weak.
|
||||
- **Keep macOS-first updater/download metadata and infer other platforms from release assets only**: cheaper in the short term, but it weakens in-app update guarantees and makes the public download page depend on ad hoc asset naming rather than an explicit manifest contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Tolaria's release CI now owns packaging and artifact validation on three desktop platforms instead of one.
|
||||
- The public stable download page can redirect Windows/Linux users to real installers without special-case manual curation.
|
||||
- Vault content created through Tolaria stays portable across macOS, Linux, and Windows, which reduces sync-time surprises and broken clones.
|
||||
- Any future platform addition now needs both a release-artifact contract and an explicit portable-filename review instead of piggybacking on macOS assumptions.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0081"
|
||||
title: "Internal light and dark theme runtime"
|
||||
status: active
|
||||
date: 2026-04-24
|
||||
supersedes: "0013"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0013 removed the vault-authored theming system and made Tolaria light-only. That kept the app simpler, but dark mode has become a product requirement for long writing sessions and accessibility.
|
||||
|
||||
The previous theming system should not return in its old form: vault notes, live user-authored themes, and broad runtime editing created too much maintenance burden. Tolaria still needs a small app-owned theme architecture because the UI spans Tailwind/shadcn variables, BlockNote/Mantine surfaces, CodeMirror raw editing, syntax highlighting, and product-specific states such as selected rows, badges, warnings, and diff lines.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria will support internal app-owned light and dark themes through a semantic CSS-variable contract, with the user's theme mode persisted as installation-local app settings.**
|
||||
|
||||
The v1 theme runtime is deliberately smaller than a general theming system:
|
||||
|
||||
- Themes are defined by the app, not by vault-authored notes.
|
||||
- CSS custom properties remain the public runtime contract for product components, Tailwind v4, and shadcn/ui.
|
||||
- Typed TypeScript helpers may derive values for consumers that cannot read CSS variables directly, such as CodeMirror extensions.
|
||||
- Existing CSS variables stay available as compatibility aliases while the UI migrates toward semantic names.
|
||||
- The first persisted choices are `light` and `dark`; system-follow, high-contrast variants, custom themes, and per-vault themes are deferred.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Internal light/dark runtime with semantic tokens** (chosen): ships dark mode while keeping the product-owned theme surface small, testable, and compatible with existing CSS-variable usage.
|
||||
- **Reintroduce vault-authored theme notes**: flexible, but repeats the complexity removed by ADR-0013 and makes dark mode dependent on user-editable data.
|
||||
- **Ad hoc `.dark` overrides in components**: fastest initially, but would scatter color logic across the app and make future theme variants expensive.
|
||||
- **Single TypeScript theme object as source of truth**: attractive for validation, but the current app already relies on CSS variables for Tailwind, shadcn/ui, BlockNote CSS overrides, and many product components.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `src/index.css` owns the stable CSS custom-property contract for app chrome and shared states.
|
||||
- `src/theme.json` continues to describe editor typography, but editor-facing colors should resolve through the same semantic CSS variables used by the app shell.
|
||||
- `useTheme` remains responsible for editor theme flattening and can grow into the bridge between app theme mode and editor consumers.
|
||||
- App settings, not vault frontmatter, store the selected theme mode because it is an installation-local comfort preference.
|
||||
- Startup must avoid a light-mode flash when dark mode is selected, so the runtime needs a pre-React localStorage mirror and a minimal `index.html` prepaint style in addition to persisted Tauri settings.
|
||||
- Domain tokens should be introduced only when a surface needs a role that generic semantic tokens cannot express clearly.
|
||||
- Re-evaluate if Tolaria decides to support user-authored custom themes, per-vault themes, or system-synchronized mode as a first-class product requirement.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0082"
|
||||
title: "Markdown-durable math in notes"
|
||||
status: active
|
||||
date: 2026-04-26
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria notes are durable Markdown files, while the main editor uses BlockNote and raw mode uses CodeMirror. Users coming from technical note-taking tools expect inline math such as `$E=mc^2$` and display math such as `$$ ... $$` to render inside notes without turning the note into an app-only document format.
|
||||
|
||||
BlockNote does not currently ship a first-party math block in the local editor package. Tiptap now offers an official Mathematics extension that renders KaTeX nodes, but Tolaria's save path still depends on BlockNote's Markdown parser and `blocksToMarkdownLossy()` serializer. Adding opaque ProseMirror math nodes without an explicit Tolaria serializer would risk losing or rewriting the original Markdown source.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria will support note math through a Markdown placeholder round-trip owned by the editor pipeline.**
|
||||
|
||||
The initial implementation:
|
||||
|
||||
- Treats `$...$` as inline math and line-owned `$$...$$` / multiline `$$` blocks as display math.
|
||||
- Converts math source to temporary placeholders before BlockNote parses Markdown.
|
||||
- Replaces placeholders with Tolaria schema nodes that render via the existing `katex` dependency.
|
||||
- Serializes those schema nodes back to the original Markdown delimiters before saving or entering raw mode.
|
||||
- Uses KaTeX with `throwOnError: false` and `trust: false` so malformed or untrusted formulas remain visible rather than breaking the note.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Tolaria-owned placeholder round-trip with KaTeX rendering** (chosen): matches the existing wikilink architecture, preserves plain-text source, and avoids depending on BlockNote support for non-default ProseMirror math nodes.
|
||||
- **Tiptap Mathematics extension directly in BlockNote**: attractive because it is official Tiptap and KaTeX-backed, but it does not by itself solve Tolaria's BlockNote Markdown serializer contract.
|
||||
- **Raw-mode-only math support**: preserves source but fails the rich editor reading experience users expect.
|
||||
- **Store formulas as custom JSON/frontmatter metadata**: richer structured editing later, but violates the Markdown-first durability requirement.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `src/utils/mathMarkdown.ts` is the canonical parser/serializer bridge for note math.
|
||||
- The rich editor renders math as schema nodes; raw mode remains the most direct way to edit exact math source.
|
||||
- CodeMirror raw editing keeps the literal Markdown delimiters, so imported Obsidian-style notes remain understandable outside Tolaria.
|
||||
- Future equation editing helpers can be added on top of the same Markdown source contract instead of changing the storage model.
|
||||
- Re-evaluate direct Tiptap Mathematics integration only if it can be proven to preserve Tolaria's Markdown save path without custom lossy behavior.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0083"
|
||||
title: "Dual-architecture macOS release artifacts"
|
||||
status: active
|
||||
date: 2026-04-26
|
||||
supersedes: "0080"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0080 made Tolaria's desktop release pipeline cross-platform, but the macOS leg still shipped only Apple Silicon artifacts. That left Intel Mac users without a compatible build in both the alpha feed and stable releases, even though Tauri and Rust can produce `x86_64-apple-darwin` bundles from the same release workflow.
|
||||
|
||||
The updater manifest also needs to distinguish macOS CPU architectures. A generic `darwin` or macOS-only entry would make it too easy for an Intel installation to see an Apple Silicon updater bundle, and browser user agents cannot reliably tell Apple Silicon Macs apart from Intel Macs.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria publishes macOS release artifacts for both Apple Silicon (`darwin-aarch64`) and Intel (`darwin-x86_64`) in every alpha and stable release.**
|
||||
|
||||
- Alpha and stable workflows build the macOS matrix for `aarch64-apple-darwin` and `x86_64-apple-darwin`.
|
||||
- Alpha manifests include signed updater tarballs for `darwin-aarch64` and `darwin-x86_64`.
|
||||
- Stable manifests include both macOS updater tarballs and both manual DMG downloads, alongside the existing Windows x64 and Linux x86_64 entries.
|
||||
- Release jobs normalize macOS artifact filenames with the architecture suffix before publishing so GitHub release assets stay unambiguous.
|
||||
- The stable download page exposes separate Apple Silicon and Intel Mac links. When both Mac links exist, a generic macOS browser is not auto-redirected because user-agent architecture detection is unreliable.
|
||||
- The cross-platform filename portability decisions from ADR-0080 remain in force.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Publish separate Apple Silicon and Intel Mac artifacts** (chosen): gives each updater client an architecture-specific manifest key and gives users explicit manual download links. Cons: doubles the macOS release matrix and signing/notarization surface.
|
||||
- **Publish a universal macOS binary**: gives users one download, but requires lipo/re-sign/notarize coordination and reintroduces the artifact-combining complexity the release pipeline intentionally avoided.
|
||||
- **Keep Apple Silicon-only macOS releases**: keeps CI cheaper, but leaves Intel Mac users unsupported and makes the release artifacts inconsistent with Tolaria's desktop support goals.
|
||||
|
||||
## Consequences
|
||||
|
||||
- macOS release jobs now run one matrix entry per CPU architecture.
|
||||
- Release manifest consumers must treat `darwin-aarch64` and `darwin-x86_64` as distinct platform keys.
|
||||
- Stable manual downloads show two Mac choices instead of pretending browser detection can select the right CPU architecture.
|
||||
- Future macOS release changes must validate both updater and manual-download artifacts for both architectures.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0084"
|
||||
title: "App-owned localization foundation"
|
||||
status: active
|
||||
date: 2026-04-26
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria was effectively English-only. Users requested a general i18n foundation and Chinese-language support. We need a path that lets the UI adopt additional locales without pushing UI-language preferences into vault files or making every partially translated string a runtime failure.
|
||||
|
||||
## Decision
|
||||
|
||||
Tolaria owns a dependency-free frontend localization layer in `src/lib/i18n.ts`.
|
||||
|
||||
- English is the canonical fallback locale.
|
||||
- Simplified Chinese (`zh-Hans`) is the first additional locale.
|
||||
- `ui_language` is an installation-local app setting in `~/.config/com.tolaria.app/settings.json`; `null` means "follow system language when supported, otherwise English".
|
||||
- Missing translation keys fall back to English.
|
||||
- App-level chrome receives locale through props from `App.tsx`, following the existing props-down/callbacks-up architecture instead of introducing global React context.
|
||||
- Language switching is exposed in Settings and through command-palette actions.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Add an i18n dependency now**: useful long term, but unnecessary for the first locale and would add framework surface before we know Tolaria's locale workflow.
|
||||
- **Store language in the vault**: rejected because UI language is an installation preference, not content structure.
|
||||
- **Translate ad hoc strings inline**: rejected because it would make fallback behavior inconsistent and future locales expensive.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New UI strings should be added to `src/lib/i18n.ts` first and rendered through `translate()` / `createTranslator()` where the surface already receives locale.
|
||||
- Partially translated locales remain usable because English is the fallback for missing keys.
|
||||
- Locale choice changes UI chrome immediately after settings save or command-palette language commands without reopening the vault.
|
||||
- Larger feature surfaces can migrate to the shared localization module incrementally.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0085"
|
||||
title: "Non-git vaults open with explicit later Git initialization"
|
||||
status: active
|
||||
date: 2026-04-26
|
||||
supersedes: "0034"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0034 made Git a hard prerequisite for opening a vault because Git-backed cache, history, change, and sync flows failed invisibly when users opened plain Markdown folders. That protected Git features, but it blocked the common adoption path of opening an existing folder from Obsidian, iCloud, Dropbox, or a manually maintained notes directory.
|
||||
|
||||
Tolaria now needs the opposite default: browsing and editing Markdown should work immediately, while Git remains an explicit capability users can enable when they want history, sync, commits, or collaboration.
|
||||
|
||||
## Decision
|
||||
|
||||
**Open existing Markdown folders even when they are not Git repositories.** A non-git vault is a supported state, not an error state. On open, Tolaria asks whether to initialize Git; if the user dismisses the prompt, the app keeps working and the status bar permanently shows a `Git disabled` warning. Clicking that warning, or running `Initialize Git for Current Vault` from the command palette, reopens the setup action.
|
||||
|
||||
While a vault is not Git-backed:
|
||||
|
||||
- Git history, change, commit, sync, conflict, and remote actions are hidden or disabled.
|
||||
- Background auto-sync and AutoGit checkpoints do not run.
|
||||
- Markdown scanning, note browsing, note editing, search, and non-Git vault features continue normally.
|
||||
|
||||
`init_git_repo` remains the single backend command for enabling Git later. It creates the repository, writes Tolaria's default `.gitignore`, stages the vault, and creates the unsigned setup commit.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A (chosen): Supported non-git mode with explicit later initialization.** Best adoption path; keeps Git capabilities visible without blocking the basic notes workflow.
|
||||
- **Option B: Keep the ADR-0034 blocking modal.** Prevents Git feature ambiguity, but rejects valid plain-folder workflows.
|
||||
- **Option C: Auto-initialize Git when opening a plain folder.** Low friction, but surprising for users who do not want Tolaria to mutate folder metadata.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Existing Git-backed vaults keep the same history, commit, sync, and remote behavior.
|
||||
- UI surfaces must treat Git capability as stateful per vault, not as an app-wide invariant.
|
||||
- Tests need to cover both Git-backed and non-git vaults in browser mocks and native QA.
|
||||
- Future Git-dependent features must check the current vault's Git state before registering commands or running background work.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0086"
|
||||
title: "In-app image previews for binary vault files"
|
||||
status: active
|
||||
date: 2026-04-26
|
||||
supersedes: "0041"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0041 made the vault scanner index all visible files and introduced `fileKind` as `"markdown"`, `"text"`, or `"binary"`. Binary files were deliberately shown as inert entries until Tolaria had a dedicated preview model.
|
||||
|
||||
That made image references visible in folder views, but opening an image still felt outside the normal Tolaria workflow. Users need to inspect screenshots, diagrams, and other image assets while keeping their place in the vault.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria previews supported image files in the editor pane while keeping them as ordinary binary `VaultEntry` files.**
|
||||
|
||||
- The scanner keeps the existing `fileKind: "binary"` representation. Image previewability is inferred in the renderer from the file extension, not by introducing a proprietary image document type.
|
||||
- Opening a binary entry creates the same single active-tab state used for notes, but with empty content and no `get_note_content` text read.
|
||||
- `FilePreview` renders supported image extensions through Tauri's asset protocol (`convertFileSrc`) so the original file remains on disk.
|
||||
- Broken images and unsupported binary files render an explicit fallback state with an intentional "Open in default app" action instead of launching another app automatically.
|
||||
- Note-list rows use an image indicator for previewable image binaries. Unsupported binary rows remain muted and non-clickable in the normal list surface.
|
||||
- The preview surface is keyboard focusable and `Escape` returns focus to the note list, matching the app's keyboard-first navigation model.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The existing `VaultEntry` model and cache version do not need to change.
|
||||
- Supported image files can participate in normal selection/navigation context without being converted into Markdown notes.
|
||||
- Unsupported/broken binary files have a clear in-app state when reached through navigation paths that can select them.
|
||||
- Any future PDF, audio, or video preview should extend the same file-preview renderer rather than adding new vault-owned document representations.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0087"
|
||||
title: "JSON locale catalogs with Lara CLI synchronization"
|
||||
status: active
|
||||
date: 2026-04-27
|
||||
supersedes:
|
||||
- "0084"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0084 established an app-owned localization layer in `src/lib/i18n.ts` with English fallback and hand-maintained TypeScript dictionaries. That was enough for the first localized UI surface, but it does not scale well to a broader locale matrix or machine-assisted translation workflows.
|
||||
|
||||
We now want Tolaria to support a wider set of locales and to automate translation updates with Lara CLI while keeping the runtime dependency-light and preserving the existing English fallback behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
Tolaria will keep its app-owned runtime localization layer, but the translation source-of-truth moves to flat JSON catalogs in `src/lib/locales/`.
|
||||
|
||||
- `src/lib/locales/en.json` is the canonical source catalog.
|
||||
- Additional locale files use one JSON file per locale code (for example `zh-CN.json`, `fr-FR.json`).
|
||||
- `src/lib/i18n.ts` keeps fallback, interpolation, locale resolution, and props-down locale wiring, but it now loads locale catalogs from JSON files instead of TypeScript objects.
|
||||
- Lara CLI configuration lives in `lara.yaml`, and translation runs happen through repo scripts (`pnpm l10n:translate`, `pnpm l10n:translate:force`).
|
||||
- `scripts/validate-locales.mjs` verifies that every locale catalog present in the repo matches the English keyset and only contains flat string values.
|
||||
- Legacy stored preferences such as `zh-Hans` are normalized to the canonical `zh-CN` locale.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep TypeScript dictionaries and point Lara at `.ts` files**: possible, but JSON is the more standard interchange format for translation tooling and keeps diffs simpler for translators and reviewers.
|
||||
- **Adopt a full frontend i18n framework now**: rejected because Tolaria already has working locale propagation and fallback behavior, and the immediate need is better content management plus translation automation.
|
||||
- **Store translated strings outside the app repo**: rejected because Tolaria's chrome localization should stay versioned with the app code that consumes it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Translators and automation tools now work against plain JSON catalogs instead of editing source code.
|
||||
- The runtime keeps English fallback behavior, so a missing locale file or missing key does not break app chrome.
|
||||
- Locale additions become a data/config change first: add the locale metadata, run Lara, review JSON output, then ship.
|
||||
- Localization work now has a dedicated validation step that can run in CI or before commit.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0088"
|
||||
title: "Markdown-durable Mermaid diagrams in notes"
|
||||
status: active
|
||||
date: 2026-04-27
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria notes are plain Markdown files, while the rich editor uses BlockNote and raw mode uses CodeMirror. Users need fenced `mermaid` blocks to render as diagrams in the note surface without changing the canonical file format or hiding the source from raw editing.
|
||||
|
||||
BlockNote can parse fenced code blocks, but a generic highlighted code block does not provide diagram rendering. Rendering Mermaid directly from the Markdown fence also has to preserve the original fence source when notes are saved, copied through raw mode, closed, and reopened.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria will support Mermaid diagrams through a Markdown placeholder round-trip owned by the editor pipeline and rendered with the `mermaid` package.**
|
||||
|
||||
The implementation:
|
||||
|
||||
- Converts fenced `mermaid` blocks to temporary placeholders before BlockNote parses Markdown.
|
||||
- Replaces placeholders with a `mermaidBlock` schema block that stores both the original fenced source and the diagram body.
|
||||
- Renders the block through Mermaid in the rich editor.
|
||||
- Serializes `mermaidBlock` nodes back to their stored fenced Markdown before save, raw-mode entry, and editor-position snapshots.
|
||||
- Shows the original source as an inline fallback when Mermaid cannot render a diagram.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Tolaria-owned placeholder round-trip with Mermaid rendering** (chosen): matches the existing wikilink and math architecture, keeps Markdown as the source of truth, and gives Tolaria explicit control over serialization.
|
||||
- **Render all `mermaid` code blocks by overriding the generic code-block renderer**: smaller surface, but it couples diagram behavior to the code-highlighting package and makes exact source preservation harder.
|
||||
- **Raw-mode-only Mermaid support**: preserves source but fails the enhanced note reading experience users expect.
|
||||
- **Store parsed diagram metadata outside the Markdown body**: enables richer future editing, but violates the files-first model.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `src/utils/mermaidMarkdown.ts` is the canonical parser/serializer bridge for note diagrams.
|
||||
- Rich mode renders diagrams as schema-backed blocks; raw mode remains the direct source editor.
|
||||
- Invalid Mermaid source remains visible instead of breaking the editor surface.
|
||||
- `mermaid` is now a runtime dependency and should be upgraded deliberately with rendering regression coverage.
|
||||
- Future diagram controls, such as copy source or expand, can attach to the same `mermaidBlock` without changing storage.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0089"
|
||||
title: "Active vault filesystem watcher"
|
||||
status: active
|
||||
date: 2026-04-27
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria treats the filesystem as the source of truth, but before this decision the running app only noticed external file changes after a manual Reload Vault, a Git pull, or an AI-agent-specific refresh callback. Edits from another editor, terminal commands, another Tolaria window, or a non-pull Git operation could leave React state and the editor surface stale.
|
||||
|
||||
ADR-0071 already defines the safe reconciliation policy for external vault mutations: reload vault-derived state, protect unsaved local edits, and reopen the clean active note from disk when needed. Filesystem watching needed to reuse that policy instead of adding another ad hoc reload path.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria watches the active desktop vault with a native filesystem watcher and routes external change batches through the shared external-refresh reconciler.**
|
||||
|
||||
The desktop backend exposes `start_vault_watcher` and `stop_vault_watcher` commands backed by Rust `notify`. It watches the active vault recursively, ignores known non-content churn such as `.git/`, `node_modules/`, temp files, and `.tolaria-rename-txn`, then emits `vault-changed` events with the active vault path and changed paths.
|
||||
|
||||
The renderer owns batching and reconciliation. `useVaultWatcher` starts the backend watcher for the active main-window vault, debounces native events into one refresh, filters out recent app-owned saves, and calls `refreshPulledVaultState()`. Manual Reload Vault still uses `reload_vault` directly, but now exposes visible reload feedback.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Native active-vault watcher plus shared reconciliation** (chosen): keeps external changes visible without polling and preserves ADR-0071 behavior for clean and unsaved tabs. Cons: adds one native dependency and a long-lived watcher state.
|
||||
- **Frontend-only polling**: simpler backend surface, but wastes work on idle vaults and still needs careful active-note reconciliation.
|
||||
- **Direct `reloadVault()` on every native event**: easy to implement, but bypasses clean-tab reopen handling and can clobber the user experience around unsaved edits.
|
||||
- **Watch every configured vault**: could pre-warm state, but burns resources for inactive vaults and complicates event ownership across windows.
|
||||
|
||||
## Consequences
|
||||
|
||||
- External writes converge automatically into the visible vault state after a short debounce.
|
||||
- Active clean notes are refreshed through the same path as pull and AI-agent updates; unsaved local edits remain protected.
|
||||
- Tolaria app-owned saves are suppressed briefly so autosave does not trigger a full external refresh loop.
|
||||
- The status bar can show reload progress for manual and automatic refreshes.
|
||||
- The watcher is a desktop-only integration; mobile builds keep no-op command stubs until a mobile-specific filesystem strategy exists.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0090"
|
||||
title: "Pi CLI agent adapter"
|
||||
status: active
|
||||
date: 2026-04-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria already supports Claude Code, Codex, and OpenCode as local CLI agents in the AI panel. The next provider request is Pi Coding Agent support with the same first-class availability, settings, status, streaming, and MCP vault access.
|
||||
|
||||
Pi exposes non-interactive JSON events through `pi --mode json`, but Pi core intentionally does not include built-in MCP. Its supported MCP path is the `pi-mcp-adapter` extension, which reads MCP server definitions from Pi-compatible config files.
|
||||
|
||||
## Decision
|
||||
|
||||
Tolaria adds Pi as a supported CLI agent id (`pi`) and launches app-managed Pi sessions through a dedicated adapter module.
|
||||
|
||||
The adapter runs `pi --mode json --no-session` from the active vault cwd, closes stdin, and points `PI_CODING_AGENT_DIR` at a temporary directory containing Tolaria's `mcp.json`. That config loads the Tolaria MCP server through `pi-mcp-adapter`, pins `VAULT_PATH` to the selected vault, sets `WS_UI_PORT=9711`, uses lazy server lifecycle, and exposes the small Tolaria tool set directly.
|
||||
|
||||
Pi availability follows the existing desktop pattern: check the inherited `PATH`, the user's login shell, and common local/toolchain install locations. Pi authentication remains owned by the Pi CLI; Tolaria only surfaces setup errors and does not store provider API keys.
|
||||
|
||||
## Options Considered
|
||||
|
||||
- **Transient Pi adapter config** (chosen): gives app-launched Pi sessions Tolaria MCP access without mutating user or vault config files. Cons: requires the Pi MCP adapter extension path to be available to Pi.
|
||||
- **Write `.mcp.json` into the active vault**: simple for Pi discovery, but creates project files as a side effect of a chat session and can dirty user vaults.
|
||||
- **Rely on global `~/.pi/agent/mcp.json`**: matches Pi documentation, but silently retargets or depends on user-global state and conflicts with Tolaria's explicit integration boundary.
|
||||
- **Skip MCP for Pi**: easier to implement, but creates a weaker agent than the existing providers and violates the requirement for first-class MCP support.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The AI panel, settings, command palette, status bar, onboarding, and stream normalization now treat Pi as a first-class supported agent.
|
||||
- App-launched Pi sessions can use Tolaria vault MCP tools while remaining scoped to the selected vault.
|
||||
- Pi support stays isolated in Pi-specific modules instead of expanding the shared `ai_agents.rs` hotspot.
|
||||
- Users still need Pi itself, a configured Pi model/provider, and the Pi MCP adapter extension path available to the Pi CLI.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0091"
|
||||
title: "Gemini CLI external AI setup"
|
||||
status: active
|
||||
date: 2026-04-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria already supports explicit MCP setup for external desktop AI tools. Users asked for Gemini CLI support so the same active-vault MCP server can be registered where Gemini reads tool configuration, with optional Gemini-specific vault guidance.
|
||||
|
||||
Gemini CLI reads MCP server definitions from `~/.gemini/settings.json` and can load project guidance from `GEMINI.md`. Tolaria must support those conventions without silently rewriting global user settings or overwriting user-authored vault instructions.
|
||||
|
||||
## Decision
|
||||
|
||||
Tolaria adds `~/.gemini/settings.json` to the explicit external AI setup path list. The existing MCP entry shape is reused: `mcpServers.tolaria` runs the packaged stdio server through Node.js, pins `VAULT_PATH` to the selected vault, and sets `WS_UI_PORT=9711`.
|
||||
|
||||
Vault guidance keeps `AGENTS.md` as the canonical shared source. `restore_vault_ai_guidance` can create or repair a managed `GEMINI.md` shim that imports `AGENTS.md`, while bootstrap and repair flows continue to seed only required Tolaria guidance (`AGENTS.md` and `CLAUDE.md`) plus type scaffolding. Custom `GEMINI.md` files are classified as custom and are not overwritten.
|
||||
|
||||
The setup dialog documents that Gemini CLI still needs its own install and sign-in. Tolaria does not store Gemini credentials or model-provider API keys.
|
||||
|
||||
## Options Considered
|
||||
|
||||
- **Add Gemini to explicit MCP setup and optional guidance restore** (chosen): matches the existing consent boundary, preserves user settings, and gives Gemini the shared vault context.
|
||||
- **Generate `GEMINI.md` automatically for every vault**: simpler discovery, but turns an optional third-party compatibility file into startup side effect and dirties vaults for users who do not use Gemini.
|
||||
- **Document manual Gemini setup only**: avoids code changes, but leaves users to transpose paths and loses the active-vault safety already available for other MCP clients.
|
||||
- **Create a Gemini-specific MCP entry shape**: unnecessary because Gemini accepts the same `mcpServers` entry structure used by the existing Tolaria MCP server.
|
||||
|
||||
## Consequences
|
||||
|
||||
- External AI setup now writes/removes Tolaria's MCP entry in Claude, Gemini, Cursor, and generic MCP config files.
|
||||
- Gemini users can create a managed `GEMINI.md` shim without duplicating the canonical `AGENTS.md` guidance.
|
||||
- Existing vault bootstrap and repair remain non-invasive for users who do not use Gemini.
|
||||
- Native QA requires a local Gemini CLI install and authentication for an end-to-end Gemini prompt; otherwise the app can still verify the generated config and documentation paths.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0092"
|
||||
title: "Vault-scoped AI agent permission modes"
|
||||
status: active
|
||||
date: 2026-04-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0074 established explicit setup and least-privilege defaults for desktop AI tools. The in-app AI panel now supports multiple local CLI agents, and users need a clear per-vault way to choose whether an agent should stay in the narrow vault-safe profile or use broader local-work tools for that vault.
|
||||
|
||||
The mode must be visible at the point of use, must not mutate global CLI settings, and must not silently restore dangerous bypass flags. Existing transcripts should remain intact when the mode changes because a change applies to the next agent run, not to a process that is already streaming.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria stores an `ai_agent_permission_mode` per vault with values `safe` and `power_user`, defaulting missing or null values to `safe`, and passes that normalized mode through the AI panel stream request into each CLI adapter.**
|
||||
|
||||
The AI panel header displays the current mode and offers a compact Vault Safe / Power User control that is disabled while an agent run is active. Changing the mode preserves the transcript and inserts a local transcript marker.
|
||||
|
||||
Adapter mappings remain conservative:
|
||||
- Claude Code Safe keeps `acceptEdits`, strict Tolaria MCP config, and file/search/edit tools only; Power User adds Bash to the allowed tool list without using `--dangerously-skip-permissions`.
|
||||
- Codex keeps the active-vault `workspace-write` sandbox and `--ask-for-approval never` in both modes.
|
||||
- OpenCode uses transient `OPENCODE_CONFIG_CONTENT`; Safe denies bash and external directories, while Power User allows bash but still denies external directories.
|
||||
- Pi receives the mode on the adapter request path; both modes currently use the same transient MCP adapter config.
|
||||
|
||||
## Options Considered
|
||||
|
||||
- **Per-vault Safe / Power User modes** (chosen): makes the permission surface explicit where the agent is used and preserves least-privilege defaults for each vault.
|
||||
- **Global app setting**: simpler storage, but a single toggle can over-apply a power-user profile to unrelated vaults.
|
||||
- **Dangerous bypass mode**: maximizes CLI freedom, but violates ADR-0074's least-privilege boundary and needs a separate explicit security decision.
|
||||
- **Adapter-specific UI switches**: exposes too much implementation detail and makes cross-agent behavior harder to reason about.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Vault config normalization owns the safe default for old vaults and malformed values.
|
||||
- Agent requests now carry a permission mode through frontend and Rust boundaries, so new adapters must choose an explicit mapping.
|
||||
- Power User is intentionally not equivalent across agents; where an adapter lacks a safe broader local-work switch, both modes may map to the same conservative behavior and must document that with tests.
|
||||
- Any future dangerous mode requires a new ADR and separate UI language.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0093"
|
||||
title: "Shared CLI agent runtime adapters"
|
||||
status: active
|
||||
date: 2026-04-29
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria supports Claude Code, Codex, OpenCode, and Pi as local CLI agents in the AI panel. Each agent has different command-line arguments, configuration shape, and JSON event schema, but the Rust backend had grown repeated runtime plumbing around those differences: request shapes, prompt wrapping, subprocess launch, stdout JSON reading, stderr capture, exit handling, done events, version probing, and Tolaria MCP server path resolution.
|
||||
|
||||
That duplication made small runtime fixes expensive because they had to be repeated across several adapter files. It also kept Codex-specific command and event mapping inside `ai_agents.rs`, making the top-level module both an orchestrator and a bespoke adapter.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria uses `cli_agent_runtime.rs` as the shared runtime scaffold for app-managed CLI agents, while `ai_agents.rs` only normalizes and dispatches requests to per-agent adapter modules.**
|
||||
|
||||
The shared scaffold owns the common agent request shape, system/user prompt wrapping, JSON-line process lifecycle, normalized error/done handling for `AiAgentStreamEvent` adapters, version probing, and Tolaria MCP server path resolution. Per-agent modules keep the provider-specific pieces: binary discovery candidates, command arguments, transient config shape, authentication error wording, and JSON event mapping.
|
||||
|
||||
Codex now lives in `codex_cli.rs`, matching the Claude, OpenCode, and Pi adapter boundary. `ai_agents.rs` remains the Tauri-facing orchestrator that chooses an adapter and maps Claude's legacy event enum into the normalized event stream.
|
||||
|
||||
## Options Considered
|
||||
|
||||
- **Shared runtime scaffold with thin adapters** (chosen): reduces repeated process lifecycle code without hiding provider-specific command/config/event behavior.
|
||||
- **One trait object per agent**: more uniform on paper, but adds indirection without removing much current complexity.
|
||||
- **Leave each adapter self-contained**: keeps local readability for a single file, but new process, prompt, and MCP fixes continue to land in parallel.
|
||||
- **Fully generic event mapping**: over-abstracts the JSON schemas and makes provider-specific edge cases harder to test.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Runtime lifecycle fixes should usually start in `cli_agent_runtime.rs`.
|
||||
- New agent adapters should use the shared request/prompt/process helpers and keep only command, config, discovery, and event mapping local.
|
||||
- `ai_agents.rs` should not grow provider-specific runtime code again; it should normalize the frontend request, dispatch to an adapter, and map any legacy event shape.
|
||||
- The shared scaffold deliberately does not erase provider differences. Authentication messages, permission semantics, and transient config formats remain adapter-owned and must stay covered by adapter tests.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0094"
|
||||
title: "Gitignored content visibility as a command-boundary filter"
|
||||
status: active
|
||||
date: 2026-04-29
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria's vault scanner now indexes more of the real filesystem so Folder views, search, and reload flows can reflect what is actually in the vault. In Git-backed vaults, that includes generated, local-only, or machine-specific content that users intentionally hide through `.gitignore`.
|
||||
|
||||
Always showing Gitignored files makes Folder lists and search noisy, especially in vaults that contain exports, build artifacts, or personal local scratch files. But removing those files during scanning would make visibility dependent on cache shape, complicate toggling, and blur the distinction between "what exists in the vault" and "what this installation chooses to surface."
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria keeps the vault scan and cache complete, then applies Gitignored-content visibility at the command boundary before entries, folders, or search results reach React.**
|
||||
|
||||
- `hide_gitignored_files` is an installation-local app setting and defaults to `true`.
|
||||
- Visibility checks use batched `git check-ignore --no-index --stdin` so Tolaria follows normal Git ignore and negation semantics as closely as practical.
|
||||
- `list_vault`, `reload_vault`, `list_vault_folders`, and keyword search all apply the same filter when the setting is enabled.
|
||||
- Toggling the setting reloads the current vault surfaces instead of rebuilding a different cache format.
|
||||
- If a vault has no `.gitignore`, or Gitignored visibility is turned off, Tolaria shows the full scanned result.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Complete scan/cache + boundary filter** (chosen): keeps the filesystem model authoritative, makes toggling cheap and consistent, and avoids cache divergence.
|
||||
- **Skip Gitignored content during scan/cache**: reduces later filtering work, but makes visibility part of the persisted cache shape and complicates instant toggling.
|
||||
- **Always show Gitignored content**: simplest implementation, but too noisy for real Git-backed vaults and undermines users' existing ignore rules.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Gitignored visibility is a per-installation comfort preference, not vault-authored shared metadata.
|
||||
- Search, folder lists, and note reloads stay aligned because they all consult the same boundary filter.
|
||||
- The cache can still support future visibility changes without a data migration.
|
||||
- Users can reveal ignored content again immediately by disabling the setting.
|
||||
- Future features that expose vault file lists should apply the same boundary filter unless they intentionally need raw filesystem output.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0095"
|
||||
title: "Saved views use an explicit YAML order field"
|
||||
status: active
|
||||
date: 2026-04-29
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Saved Views already persist as user-editable YAML files in the vault and sync through Git. Filename ordering was stable, but it forced users to rename files just to change sidebar order and gave Tolaria no durable way to support drag reordering, move actions, or keyboard-first ordering controls.
|
||||
|
||||
The ordering choice also needs to travel with the view definition itself. Saved Views are part of the vault's shared information architecture, not a machine-local preference.
|
||||
|
||||
## Decision
|
||||
|
||||
**Each Saved View may store an optional top-level `order` number in its YAML definition, and Tolaria sorts views by that value before falling back to filename.**
|
||||
|
||||
- Lower `order` values render earlier in the sidebar and other Saved View lists.
|
||||
- Views without `order` sort after ordered views and then fall back to filename ordering for stability.
|
||||
- Reordering actions rewrite affected view files with a dense sequence of order values instead of encoding position in filenames.
|
||||
- The same persisted order supports drag handles, explicit move buttons, and command-palette ordering actions.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Explicit `order` field in the view YAML** (chosen): portable, Git-syncable, easy to inspect by hand, and consistent with the existing file-first view model.
|
||||
- **Filename-based ordering only**: no schema change, but makes reordering clumsy and couples user-visible structure to file naming.
|
||||
- **App-local ordering state**: easy to prototype, but breaks cross-device consistency and separates ordering from the view artifact users already version.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Saved View ordering becomes part of the vault and syncs naturally through Git.
|
||||
- Existing views remain valid; unordered files keep a stable fallback sort until reordered.
|
||||
- Reordering can touch multiple view files in one action because Tolaria normalizes the sequence.
|
||||
- Future Saved View features should treat `order` as part of the shared YAML schema rather than introducing a parallel ordering store.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0096"
|
||||
title: "Root-created type documents"
|
||||
status: active
|
||||
date: 2026-04-29
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria identifies type definitions by markdown frontmatter (`type: Type`), not by filesystem location. Older documentation and UI creation flows still treated `type/` as the canonical destination for new type documents, with a compatibility fallback for vaults that already used `types/`.
|
||||
|
||||
That folder-based creation policy conflicted with the broader vault model: notes are scanned from all non-hidden folders, type identity comes from metadata, and root `type.md` / `note.md` definitions are already used by repair and bootstrap flows.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria creates new type documents at the vault root.**
|
||||
|
||||
- A type document is any markdown note with `type: Type` in frontmatter.
|
||||
- New UI-created type documents use `{vault}/{slug}.md`.
|
||||
- Existing type documents in `type/`, `types/`, or other scanned folders remain valid and continue to drive templates, icons, colors, visibility, sorting, and sidebar grouping.
|
||||
- Creation does not silently migrate or move existing type documents.
|
||||
- Root filename collisions are handled as file collisions; Tolaria must not overwrite an existing note when creating a type document.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Root-created type documents** (chosen): matches the metadata-first model, removes special casing from creation, and aligns new types with root-managed default type scaffolding.
|
||||
- **Canonical `type/` folder**: avoids root filename collisions, but makes path special even though type identity is already defined by frontmatter.
|
||||
- **Preserve existing folder convention dynamically**: minimizes change for plural `types/` vaults, but creates inconsistent behavior across vaults and leaves `types/` only partially supported.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users can inspect and edit type documents as ordinary root notes by default.
|
||||
- Existing vaults with `type/` or `types/` type documents remain readable because vault scanning already includes non-hidden subdirectories.
|
||||
- If a root note with the same slug already exists, type creation fails with a collision message instead of writing into a fallback folder.
|
||||
- Legacy `type/` may remain hidden from the folder tree so old type documents do not duplicate the Types sidebar section.
|
||||
- Re-evaluate if users need a guided migration from folder-based type documents to root type documents.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0097"
|
||||
title: "Gemini CLI agent adapter"
|
||||
status: active
|
||||
date: 2026-04-29
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0091 added Gemini CLI to explicit external MCP setup, but Gemini was still absent from Tolaria's selectable app-managed AI agents. That left the AI panel able to generate Gemini-compatible MCP configuration while the actual agent picker, availability checks, install links, and streaming dispatch did not treat Gemini like Claude Code, Codex, OpenCode, or Pi.
|
||||
|
||||
Gemini CLI supports headless `--prompt` execution with JSON output, configurable approval modes, tool exclusion, and settings-file overrides through `GEMINI_CLI_SYSTEM_SETTINGS_PATH`. Those features are enough to launch Gemini from Tolaria without mutating the user's durable `~/.gemini/settings.json` during app-managed sessions.
|
||||
|
||||
## Decision
|
||||
|
||||
Tolaria adds Gemini CLI as a first-class `AiAgentId`. The frontend agent definitions, onboarding prompt, install links, default-agent normalization, status badge, command registry, settings persistence, and mock Tauri status payloads include `gemini`.
|
||||
|
||||
The desktop backend adds a Gemini adapter that:
|
||||
|
||||
- discovers `gemini` through the process path, login shell, and common local/toolchain install locations
|
||||
- runs `gemini --output-format json --approval-mode <mode> --prompt <prompt>` from the active vault
|
||||
- supplies Tolaria MCP through a temporary settings file referenced by `GEMINI_CLI_SYSTEM_SETTINGS_PATH`
|
||||
- uses Safe mode with `auto_edit`, an untrusted MCP entry, and `tools.exclude=["run_shell_command"]`
|
||||
- uses Power User mode with `yolo` and a trusted Tolaria MCP entry
|
||||
- maps Gemini JSON responses into Tolaria's existing AI panel stream events
|
||||
|
||||
The existing external MCP setup remains explicit and durable. The app-managed Gemini adapter uses transient settings so selecting Gemini in Tolaria does not rewrite the user's global Gemini config.
|
||||
|
||||
## Options Considered
|
||||
|
||||
- **Add Gemini as a first-class app-managed agent** (chosen): matches the existing agent picker and onboarding UI, uses Gemini's headless JSON mode, and keeps MCP setup vault-scoped.
|
||||
- **Keep Gemini as external MCP setup only**: avoids another adapter, but keeps the interface inconsistent and requires users to leave Tolaria for a flow that other agents support in-panel.
|
||||
- **Write app-managed Gemini config into `~/.gemini/settings.json`**: reuses the external setup path, but would blur the consent boundary and risk overwriting user preferences during normal AI panel usage.
|
||||
- **Use interactive Gemini sessions**: could preserve richer CLI state, but does not fit Tolaria's current one-request stream lifecycle and would make cleanup/auth/error handling harder.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Gemini appears anywhere users can choose, install, or switch local AI agents.
|
||||
- End-to-end native Gemini QA requires the Gemini CLI to be installed and authenticated, but missing/auth failures now produce agent-specific guidance.
|
||||
- Safe and Power User behavior is limited by Gemini's own approval/tool semantics; if Gemini changes those names, the adapter tests and docs need updating.
|
||||
- The durable MCP setup path and optional `GEMINI.md` shim continue to serve external Gemini usage outside Tolaria's AI panel.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0098"
|
||||
title: "In-app image and PDF previews for binary vault files"
|
||||
status: superseded
|
||||
date: 2026-04-29
|
||||
supersedes: "0086"
|
||||
superseded_by: "0110"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0086 introduced the `FilePreview` path for image binaries while keeping binary files as ordinary `VaultEntry` records. The same file-first model should now cover PDFs, because asset-heavy vaults often mix screenshots, diagrams, and document exports that users need to inspect without leaving Tolaria.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria previews supported image and PDF files in the editor pane while keeping them as ordinary binary vault files.**
|
||||
|
||||
- The scanner keeps the coarse `fileKind: "binary"` representation. Previewability stays a renderer concern inferred from the file extension in `src/utils/filePreview.ts`.
|
||||
- Supported images render with `<img>` and supported PDFs render with the webview PDF object renderer, both using Tauri asset URLs from `convertFileSrc`.
|
||||
- The Tauri CSP permits scoped asset URLs in `object-src` so PDF objects can load vault-backed files without broadening script, connect, or image policy.
|
||||
- PDF preview fallback content lives inside the PDF object so unsupported or failed renderers still expose an explicit "Open in default app" escape hatch.
|
||||
- Note-list rows for previewable images and PDFs remain clickable and carry file-specific indicators; unsupported binary rows stay muted and non-clickable.
|
||||
- `Escape` on the preview surface returns keyboard focus to the note list, matching the existing image-preview keyboard behavior.
|
||||
|
||||
## Consequences
|
||||
|
||||
- PDFs do not become notes and do not get Markdown editor semantics.
|
||||
- The asset preview surface can keep growing to additional safe binary formats without changing the vault scanner or persisted cache shape.
|
||||
- Broken PDFs may rely on the webview's own renderer failure state, but the surrounding Tolaria preview chrome still provides reveal, copy path, and default-app actions.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0099"
|
||||
title: "Cumulative vault asset scope for previews"
|
||||
status: active
|
||||
date: 2026-04-29
|
||||
supersedes: "0074 asset-protocol runtime scoping"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0074 moved the desktop asset protocol away from broad filesystem access and toward runtime vault scoping. The implementation tried to keep only the active vault in scope by calling Tauri's `forbid_directory` for vault roots that were no longer active.
|
||||
|
||||
Tauri's filesystem scope treats forbidden paths as permanent precedence rules: a forbidden path is denied even if it is later allowed again. After a user switched away from a vault and back, image and PDF previews could keep producing `403 Forbidden` responses for valid vault files until the app restarted.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria accumulates Tauri asset protocol access for vault roots loaded during the current app session and never forbids a previously loaded vault root at runtime.**
|
||||
|
||||
- `sync_vault_asset_scope` adds the canonical vault root and requested vault root when they are missing from the runtime asset scope.
|
||||
- The runtime asset scope remains narrower than global filesystem access because only vault roots that Tolaria has loaded are added.
|
||||
- Command paths still enforce the active vault boundary through the Rust command layer before reads, writes, external opens, and attachment imports.
|
||||
- Asset scope revocation is deferred to process exit, because Tauri does not expose a safe runtime unallow operation for directories.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Cumulative runtime vault scope** (chosen): keeps previews reliable after vault switches while preserving vault-only access in the current process.
|
||||
- **Continue forbidding previous vaults**: appears stricter, but Tauri forbids are not reversible and valid previews fail after switching back.
|
||||
- **Allow all filesystem paths**: avoids preview failures but returns to the broad asset protocol access that ADR-0074 intentionally removed.
|
||||
- **Replace `convertFileSrc` with a custom protocol**: could support exact active-vault revocation, but it would be a larger cross-cutting migration for editor images, file previews, and PDF rendering.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Images and PDFs from any vault loaded in the current session can keep rendering after vault switches.
|
||||
- The app process, not each vault switch, is the revocation boundary for asset URL access.
|
||||
- Active-vault command validation remains the primary guard for mutations and default-app opens.
|
||||
- Re-evaluate this if Tauri adds a public runtime unallow operation for asset protocol directories.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
type: ADR
|
||||
id: "0100"
|
||||
title: "Synthetic vault-root row in folder navigation"
|
||||
status: active
|
||||
date: 2026-04-30
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0033 introduced subfolder scanning and a collapsible folder tree backed by `list_vault_folders`, but the sidebar still had no first-class way to select the vault root itself. That left root-level files outside the folder-navigation model and pushed the UI toward one-off handling for the opened vault path.
|
||||
|
||||
The new sidebar behavior needs to show root-level files when the user clicks the vault name, while preserving the existing folder rename/delete model for real folders only.
|
||||
|
||||
## Decision
|
||||
|
||||
**Represent the vault root in the sidebar as a synthetic frontend-owned folder row rather than as a mutable backend folder.**
|
||||
|
||||
- `FolderTree` wraps backend folder nodes in a root row with `path: ""` and `rootPath` set to the opened vault path.
|
||||
- `SidebarSelection` keeps using `kind: 'folder'`, but root selection is encoded as the empty folder path plus `rootPath` metadata.
|
||||
- Root-level file filtering is handled in note-list helpers as a dedicated root case instead of pretending the vault root is an ordinary folder.
|
||||
- Rename/delete remain available only for real folders; the vault root row is navigable, not mutable.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Synthetic vault-root row in the renderer — keeps `list_vault_folders` focused on real folders, avoids backend schema churn, and reuses the existing folder-selection mental model.
|
||||
- **Option B**: Add a pseudo-folder to backend folder results — would couple presentation-only root behavior to command data and blur the distinction between the vault itself and mutable folders.
|
||||
- **Option C**: Keep root files outside folder navigation entirely — simpler, but leaves the sidebar with an incomplete navigation model and special cases elsewhere in the UI.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Folder navigation now has a single model for root and nested folder browsing.
|
||||
- Backend folder APIs stay unchanged: they describe actual folders, not UI-only rows.
|
||||
- Selection handling must treat `path: ""` as the vault-root case and use `rootPath` when computing direct-root file membership.
|
||||
- Re-evaluate if folder actions ever need to operate on the vault root itself, because that would likely require a separate command model instead of extending the synthetic row.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue