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
1157
product-source/hololake-platform/docs/ABSTRACTIONS.md
Normal file
1157
product-source/hololake-platform/docs/ABSTRACTIONS.md
Normal file
File diff suppressed because it is too large
Load diff
1316
product-source/hololake-platform/docs/ARCHITECTURE.md
Normal file
1316
product-source/hololake-platform/docs/ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load diff
534
product-source/hololake-platform/docs/GETTING-STARTED.md
Normal file
534
product-source/hololake-platform/docs/GETTING-STARTED.md
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
# Getting Started
|
||||
|
||||
How to navigate the codebase, run the app, and find what you need.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** 18+ and **pnpm**
|
||||
- **Rust** 1.77.2+ (for the Tauri backend)
|
||||
- **git** CLI (required by the git integration features; Windows users may choose native Git or WSL2 Git in Settings)
|
||||
|
||||
### Linux system dependencies
|
||||
|
||||
If you run the desktop app on Linux, install Tauri's WebKit2GTK 4.1 dependencies first:
|
||||
|
||||
- Arch / Manjaro:
|
||||
```bash
|
||||
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \
|
||||
appmenu-gtk-module libappindicator-gtk3 librsvg
|
||||
```
|
||||
- Debian / Ubuntu (22.04+):
|
||||
```bash
|
||||
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \
|
||||
libsoup-3.0-dev patchelf
|
||||
```
|
||||
- Fedora 38+:
|
||||
```bash
|
||||
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \
|
||||
libappindicator-gtk3-devel librsvg2-devel
|
||||
```
|
||||
|
||||
### Linux AppImage Wayland troubleshooting
|
||||
|
||||
On some Wayland systems, the Linux AppImage may fail to launch with:
|
||||
|
||||
```text
|
||||
Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...
|
||||
```
|
||||
|
||||
Recent Tolaria Linux builds automatically disable the unstable WebKitGTK DMABUF renderer on native Wayland launches. AppImage launches also disable WebKitGTK compositing as a last-resort sealed-runtime fallback and retry startup with an architecture-matching system Wayland client library when they detect this class of AppImage + Wayland environment. If you are running an older build, use this workaround:
|
||||
|
||||
```bash
|
||||
WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/usr/lib64/libwayland-client.so.0 ./Tolaria*.AppImage
|
||||
```
|
||||
|
||||
If your distribution stores the 64-bit library elsewhere, use that path instead, for example `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`. On 64-bit Fedora, avoid `/usr/lib/libwayland-client.so.0`; that path can point at a 32-bit library and be ignored by the loader with a wrong ELF class warning.
|
||||
|
||||
### Linux AppImage packaging checks
|
||||
|
||||
Linux release CI currently uses Tauri's stock linuxdeploy AppImage output plugin:
|
||||
|
||||
```bash
|
||||
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
|
||||
```
|
||||
|
||||
Release validation verifies that the Linux job produced an AppImage, at least one installer bundle, and updater signature artifacts. Windows release jobs always require Tauri updater signatures; when Authenticode certificate secrets are configured, they also import the CI code-signing certificate, build NSIS with a generated Tauri Authenticode signing config, and verify the app executable plus installer signatures before upload. The experimental AppImage output-plugin shim in `scripts/appimage-launcher-tools.mjs` is retained for local investigation, but it is not wired into release packaging because linuxdeploy currently exits before sealing the AppImage when the shim is pre-seeded in Tauri's tools cache.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run in browser (no Rust needed — uses mock data)
|
||||
pnpm dev
|
||||
# Open http://localhost:5173
|
||||
|
||||
# Run with Tauri (full app, requires Rust)
|
||||
pnpm tauri dev
|
||||
|
||||
# Run tests
|
||||
pnpm test # Vitest unit tests
|
||||
cargo test # Rust tests (from src-tauri/)
|
||||
|
||||
# Or, run Rust tests from root project directory
|
||||
cargo test --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
# E2E tests
|
||||
pnpm playwright:smoke # Curated Playwright core smoke lane (~5 min)
|
||||
pnpm playwright:regression # Full Playwright regression suite
|
||||
```
|
||||
|
||||
## Chunk Sidecar Validation
|
||||
|
||||
The experimental `.chunk/config.json` mirrors the portable parts of the local git hook gate as named validations. Use it for inner-loop checks before running the full pre-push hook:
|
||||
|
||||
```bash
|
||||
chunk validate --list
|
||||
chunk validate lint
|
||||
chunk validate typecheck
|
||||
chunk validate frontend-coverage
|
||||
```
|
||||
|
||||
Remote sidecar validation requires CircleCI authentication:
|
||||
|
||||
```bash
|
||||
chunk auth set circleci
|
||||
chunk sidecar setup --name tolaria-hooks
|
||||
chunk validate --remote lint
|
||||
```
|
||||
|
||||
For Playwright smoke, prefer the shared-server shard runner after setup:
|
||||
|
||||
```bash
|
||||
chunk sidecar ssh --sidecar-id <playwright-sidecar-id> -- 'cd /home/user/tolaria && PLAYWRIGHT_CONCURRENCY=4 bash .chunk/run-playwright-shards.sh 8'
|
||||
```
|
||||
|
||||
The pre-push hook uses the faster sidecar path automatically when Chunk is available. It syncs the checkout to three independent sidecars and fans out the automatic gates:
|
||||
|
||||
- `tolaria-hooks-frontend-2`: lint and build first, then frontend coverage.
|
||||
- `tolaria-hooks-rust`: clippy, rustfmt, and Rust coverage.
|
||||
- `tolaria-hooks-playwright`: curated Playwright smoke with a shared Vite server and eight shards.
|
||||
|
||||
Set `LAPUTA_PREPUSH_LOCAL=1` to force the local fallback path. If CircleCI has duplicate sidecar names, pin lanes with `SIDECAR_FRONTEND_ID`, `SIDECAR_RUST_ID`, and `SIDECAR_PLAYWRIGHT_ID`.
|
||||
|
||||
The sidecar is Linux-based, so keep native macOS Tauri QA and app-focus screenshot checks on the host machine. The Chunk config is intended for portable frontend, Rust, coverage, and Playwright smoke checks. Avoid starting multiple `chunk validate --remote ...` processes against the same sidecar at once; each validate run syncs the checkout, so concurrent validates can race.
|
||||
|
||||
## Starter Vaults And Remotes
|
||||
|
||||
`create_getting_started_vault` clones the public starter repo and then removes every git remote from the new local copy. That means Getting Started vaults open local-only by default. Users connect a compatible remote later through the bottom-bar `No remote` chip or the command palette, both of which feed the same `AddRemoteModal` and `git_add_remote` backend flow.
|
||||
|
||||
Linux AppImage builds still use the user's system `git` and `node`. Before Tolaria spawns those Git or MCP Node subprocesses, it removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` so HTTPS clone helpers and MCP tooling use the host library stack instead of bundled AppImage libraries.
|
||||
|
||||
On Windows, Settings > Git exposes an explicit Git provider choice. Native Git remains the default. Selecting WSL2 Git stores the chosen distribution in app settings, launches Git through `wsl.exe --exec git`, and translates vault paths before clone, status, commit, sync, and remote operations use them.
|
||||
|
||||
## Multiple Vaults At The Same Time
|
||||
|
||||
The `settings.multi_workspace_enabled` flag turns the registered vault list into a unified graph. When enabled, `useVaultLoader` loads every available mounted vault, annotates entries with workspace provenance, and lets note lists, quick open, keyword search, backlinks, and wikilink navigation span those vaults.
|
||||
|
||||
The selected/default vault remains the write target for new notes and Type documents when `defaultWorkspacePath` points at an available mounted vault. Git status, changes, AutoGit checkpointing, and sync operate across the active mounted repository set, while history, diff, repair, and file operations still resolve explicit repository roots from the selected surface or entry provenance. Saved Views are listed from every mounted vault with source-vault identity, so duplicate view filenames remain separate and edits persist back to the view's owning vault.
|
||||
|
||||
The bottom-left `VaultMenu` exposes quick include/exclude controls and a `Manage vaults` entry. The Vaults settings section owns the full identity controls: display name, short label, read-only alias, accent color, removal, and default destination for new notes.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
tolaria/
|
||||
├── src/ # React frontend
|
||||
│ ├── main.tsx # Entry point (renders <App />)
|
||||
│ ├── App.tsx # Root component — wires layout + state hooks
|
||||
│ ├── App.css # App shell layout styles
|
||||
│ ├── types.ts # Shared TS types (VaultEntry, Settings, etc.)
|
||||
│ ├── mock-tauri.ts # Mock Tauri layer for browser testing
|
||||
│ ├── theme.json # Editor typography theme configuration
|
||||
│ ├── index.css # Semantic app theme variables + Tailwind setup
|
||||
│ │
|
||||
│ ├── components/ # UI components (~100 files)
|
||||
│ │ ├── Sidebar.tsx # Left panel: filters + type groups
|
||||
│ │ ├── SidebarParts.tsx # Sidebar subcomponents
|
||||
│ │ ├── NoteList.tsx # Second panel: filtered note list
|
||||
│ │ ├── NoteItem.tsx # Individual note item
|
||||
│ │ ├── PulseView.tsx # Git activity feed (replaces NoteList)
|
||||
│ │ ├── Editor.tsx # Third panel: editor orchestration
|
||||
│ │ ├── EditorContent.tsx # Editor content area
|
||||
│ │ ├── EditorRightPanel.tsx # Right panel toggle
|
||||
│ │ ├── editorSchema.tsx # BlockNote schema + wikilink type
|
||||
│ │ ├── RawEditorView.tsx # CodeMirror raw editor
|
||||
│ │ ├── Inspector.tsx # Fourth panel: metadata + relationships
|
||||
│ │ ├── DynamicPropertiesPanel.tsx # Editable frontmatter properties
|
||||
│ │ ├── AiWorkspace.tsx # Multi-chat AI workspace orchestration (docked or native window)
|
||||
│ │ ├── AiWorkspaceChrome.tsx # AI workspace header and vault-guidance chrome
|
||||
│ │ ├── AiWorkspaceResizeHandles.tsx # AI workspace edge resize handles
|
||||
│ │ ├── AiWorkspaceSideHeader.tsx # Side-mode AI workspace chat tabs and chrome
|
||||
│ │ ├── AiPanel.tsx # AI transcript/composer surface (selected target + per-vault permission mode)
|
||||
│ │ ├── AiMessage.tsx # Agent message display
|
||||
│ │ ├── AiActionCard.tsx # Agent tool action cards
|
||||
│ │ ├── AiAgentsOnboardingPrompt.tsx # First-launch AI agent installer prompt
|
||||
│ │ ├── SearchPanel.tsx # Search interface
|
||||
│ │ ├── SettingsPanel.tsx # App settings
|
||||
│ │ ├── StatusBar.tsx # Bottom bar: vault picker + sync
|
||||
│ │ ├── CommandPalette.tsx # Cmd+K command launcher
|
||||
│ │ ├── BreadcrumbBar.tsx # Breadcrumb + word count + actions
|
||||
│ │ ├── WelcomeScreen.tsx # Onboarding screen
|
||||
│ │ ├── LinuxTitlebar.tsx # Linux/Windows custom window chrome + controls
|
||||
│ │ ├── LinuxMenuButton.tsx # Linux titlebar menu mirroring app commands
|
||||
│ │ ├── CloneVaultModal.tsx # Clone a vault from any git URL
|
||||
│ │ ├── AddRemoteModal.tsx # Connect a local-only vault to a remote later
|
||||
│ │ ├── ConflictResolverModal.tsx # Git conflict resolution
|
||||
│ │ ├── CommitDialog.tsx # Git commit modal
|
||||
│ │ ├── CreateNoteDialog.tsx # New note modal
|
||||
│ │ ├── CreateTypeDialog.tsx # New type modal
|
||||
│ │ ├── UpdateBanner.tsx # In-app update notification
|
||||
│ │ ├── inspector/ # Inspector sub-panels
|
||||
│ │ │ ├── BacklinksPanel.tsx
|
||||
│ │ │ ├── RelationshipsPanel.tsx
|
||||
│ │ │ ├── GitHistoryPanel.tsx
|
||||
│ │ │ └── ...
|
||||
│ │ └── ui/ # shadcn/ui primitives
|
||||
│ │ ├── button.tsx, dialog.tsx, input.tsx, ...
|
||||
│ │
|
||||
│ ├── hooks/ # Custom React hooks (~90 files)
|
||||
│ │ ├── useVaultLoader.ts # Loads vault entries + content
|
||||
│ │ ├── useVaultSwitcher.ts # Multi-vault management
|
||||
│ │ ├── useVaultConfig.ts # Per-vault UI settings
|
||||
│ │ ├── useNoteActions.ts # Composes creation + rename + frontmatter
|
||||
│ │ ├── useNoteCreation.ts # Note/type creation
|
||||
│ │ ├── useNoteRename.ts # Note renaming + wikilink updates
|
||||
│ │ ├── useCliAiAgent.ts # Selected AI agent state + normalized session pipeline
|
||||
│ │ ├── aiAgentPermissionMode.ts # Safe/Power User mode normalization + labels
|
||||
│ │ ├── useAiAgentsStatus.ts # Claude/Codex/OpenCode/Pi/Antigravity/Kiro availability polling
|
||||
│ │ ├── useAiAgentPreferences.ts # Default-agent persistence + cycling
|
||||
│ │ ├── useAiActivity.ts # MCP UI bridge listener
|
||||
│ │ ├── useAutoSync.ts # Auto git pull/push
|
||||
│ │ ├── useConflictResolver.ts # Git conflict handling
|
||||
│ │ ├── useEditorSave.ts # Auto-save with debounce
|
||||
│ │ ├── useTheme.ts # Flatten theme.json → CSS vars
|
||||
│ │ ├── useUnifiedSearch.ts # Keyword search
|
||||
│ │ ├── useNoteSearch.ts # Note search
|
||||
│ │ ├── useCommandRegistry.ts # Command palette registry
|
||||
│ │ ├── useAppCommands.ts # App-level commands
|
||||
│ │ ├── useAppKeyboard.ts # Keyboard shortcuts
|
||||
│ │ ├── appCommandCatalog.ts # Shortcut combos + command metadata
|
||||
│ │ ├── appCommandDispatcher.ts # Shared shortcut/menu command IDs + dispatch
|
||||
│ │ ├── useSettings.ts # App settings
|
||||
│ │ ├── useGettingStartedClone.ts # Shared Getting Started clone action
|
||||
│ │ ├── useOnboarding.ts # First-launch flow
|
||||
│ │ ├── useCodeMirror.ts # CodeMirror raw editor
|
||||
│ │ ├── useMcpBridge.ts # MCP WebSocket client
|
||||
│ │ ├── useMcpStatus.ts # Explicit external AI tool connection status + connect/disconnect actions
|
||||
│ │ ├── useUpdater.ts # In-app updates
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── utils/ # Pure utility functions (~48 files)
|
||||
│ │ ├── wikilinks.ts # Wikilink preprocessing pipeline
|
||||
│ │ ├── frontmatter.ts # TypeScript YAML parser
|
||||
│ │ ├── plainTextPaste.ts # Shared Paste without Formatting command target registry
|
||||
│ │ ├── platform.ts # Runtime platform + Linux chrome gating helpers
|
||||
│ │ ├── ai-agent.ts # Agent stream utilities
|
||||
│ │ ├── ai-chat.ts # Token estimation utilities
|
||||
│ │ ├── ai-context.ts # Context snapshot builder
|
||||
│ │ ├── noteListHelpers.ts # Sorting, filtering, date formatting
|
||||
│ │ ├── wikilink.ts # Wikilink resolution
|
||||
│ │ ├── configMigration.ts # localStorage → vault config migration
|
||||
│ │ ├── iconRegistry.ts # Phosphor icon registry
|
||||
│ │ ├── propertyTypes.ts # Property type definitions
|
||||
│ │ ├── vaultListStore.ts # Vault list persistence
|
||||
│ │ ├── vaultConfigStore.ts # Vault config store
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── lib/
|
||||
│ │ ├── aiAgents.ts # Shared agent registry + status helpers
|
||||
│ │ ├── appUpdater.ts # Frontend wrapper around channel-aware updater commands
|
||||
│ │ ├── i18n.ts # App-owned localization runtime and locale resolution
|
||||
│ │ ├── locales/ # JSON locale catalogs (English source + translated locales)
|
||||
│ │ ├── releaseChannel.ts # Alpha/stable normalization helpers
|
||||
│ │ └── utils.ts # Tailwind merge + cn() helper
|
||||
│ │
|
||||
│ └── test/
|
||||
│ └── setup.ts # Vitest test environment setup
|
||||
│
|
||||
├── src-tauri/ # Rust backend
|
||||
│ ├── Cargo.toml # Rust dependencies
|
||||
│ ├── build.rs # Tauri build script
|
||||
│ ├── tauri.conf.json # Tauri app configuration
|
||||
│ ├── capabilities/ # Tauri v2 security capabilities
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # Entry point (calls lib::run())
|
||||
│ │ ├── lib.rs # Tauri setup + command registration
|
||||
│ │ ├── commands/ # Tauri command handlers (split into modules)
|
||||
│ │ ├── vault/ # Vault module
|
||||
│ │ │ ├── mod.rs # Core types, parse_md_file, scan_vault
|
||||
│ │ │ ├── cache.rs # Git-based incremental caching
|
||||
│ │ │ ├── parsing.rs # Text processing + title extraction
|
||||
│ │ │ ├── rename.rs # Rename + cross-vault wikilink update
|
||||
│ │ │ ├── image.rs # Image attachment saving
|
||||
│ │ │ ├── migration.rs # Frontmatter migration
|
||||
│ │ │ └── getting_started.rs # Getting Started vault clone orchestration
|
||||
│ │ ├── frontmatter/ # Frontmatter module
|
||||
│ │ │ ├── mod.rs, yaml.rs, ops.rs
|
||||
│ │ ├── git/ # Git module
|
||||
│ │ │ ├── mod.rs, command.rs, remote_config.rs, commit.rs, status.rs
|
||||
│ │ │ ├── history.rs, clone.rs, connect.rs, conflict.rs, remote.rs, pulse.rs
|
||||
│ │ ├── telemetry.rs # Sentry init + path scrubber
|
||||
│ │ ├── search.rs # Keyword search (walkdir-based)
|
||||
│ │ ├── ai_agents.rs # CLI-agent request normalization + adapter dispatch
|
||||
│ │ ├── cli_agent_runtime.rs # Shared CLI-agent runtime process/prompt/MCP helpers
|
||||
│ │ ├── claude_cli.rs # Claude CLI adapter
|
||||
│ │ ├── codex_cli.rs # Codex CLI adapter
|
||||
│ │ ├── pi_cli.rs # Pi CLI adapter
|
||||
│ │ ├── kiro_cli.rs # Kiro CLI adapter
|
||||
│ │ ├── mcp.rs # MCP server lifecycle + explicit config registration/removal
|
||||
│ │ ├── app_updater.rs # Alpha/stable updater metadata resolution
|
||||
│ │ ├── settings.rs # App settings persistence
|
||||
│ │ ├── vault_config.rs # Per-vault UI config
|
||||
│ │ ├── vault_list.rs # Vault list persistence
|
||||
│ │ └── menu.rs # Native macOS menu bar
|
||||
│ └── icons/ # App icons
|
||||
│
|
||||
├── mcp-server/ # MCP bridge (Node.js or Bun)
|
||||
│ ├── index.js # MCP server entry (stdio tools)
|
||||
│ ├── vault.js # Vault file operations
|
||||
│ ├── ws-bridge.js # WebSocket bridge (ports 9710, 9711)
|
||||
│ ├── test.js # MCP server tests
|
||||
│ └── package.json
|
||||
│
|
||||
├── e2e/ # Playwright E2E tests (~26 specs)
|
||||
├── tests/smoke/ # Playwright specs (full regression + @smoke subset)
|
||||
├── design/ # Per-task design files
|
||||
├── demo-vault-v2/ # Curated local QA fixture for native/dev flows
|
||||
├── scripts/ # Build/utility scripts
|
||||
│
|
||||
├── package.json # Frontend dependencies + scripts
|
||||
├── lara.yaml # Lara CLI locale sync configuration
|
||||
├── vite.config.ts # Vite bundler config
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── playwright.config.ts # Full Playwright regression config
|
||||
├── playwright.smoke.config.ts # Curated pre-push Playwright config
|
||||
├── ui-design.pen # Master design file
|
||||
├── AGENTS.md # Canonical shared instructions for coding agents
|
||||
├── CLAUDE.md # Claude Code compatibility shim importing AGENTS.md as an organized Note
|
||||
└── docs/ # This documentation
|
||||
```
|
||||
|
||||
## Key Files to Know
|
||||
|
||||
### Fixtures
|
||||
|
||||
- `demo-vault-v2/` is the small checked-in QA fixture used for native/manual Tolaria flows. It is intentionally curated around a handful of search, relationship, project-navigation, and attachment scenarios.
|
||||
- `tests/fixtures/test-vault/` is the deterministic Playwright fixture copied into temp directories for isolated integration and smoke tests.
|
||||
- `python3 scripts/generate_demo_vault.py` generates the larger synthetic vault on demand at `generated-fixtures/demo-vault-large/` for scale/performance experiments. That output is gitignored and should not bloat the normal QA fixture.
|
||||
|
||||
### Start here
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/App.tsx` | Root component. Shows the 4-panel layout, state flow, and how orchestration hooks connect. |
|
||||
| `src/types.ts` | All shared TypeScript types. Read this first to understand the data model. |
|
||||
| `src-tauri/src/commands/` | Tauri command handlers (split into modules). This is the frontend-backend API surface. |
|
||||
| `src-tauri/src/lib.rs` | Tauri setup, command registration, startup tasks, WebSocket bridge lifecycle. |
|
||||
|
||||
### Data layer
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/hooks/useVaultLoader.ts` | How vault data is loaded and managed. The Tauri/mock branching pattern. |
|
||||
| `src/hooks/useNoteActions.ts` | Orchestrates note operations: composes `useNoteCreation`, `useNoteRename`, frontmatter CRUD, and wikilink navigation. |
|
||||
| `src/hooks/useVaultSwitcher.ts` | Multi-vault management, vault switching, and persisting cloned vaults in the switcher list. |
|
||||
| `src/hooks/useGettingStartedClone.ts` | Shared "Clone Getting Started Vault" action for the status bar and command palette. |
|
||||
| `src/hooks/useNoteWindowLifecycle.ts` | Note-window URL opening, asset-scope sync, and window-title updates. |
|
||||
| `src/hooks/useVaultRenameDetection.ts` | Focus-triggered Git rename detection and wikilink update action wiring. |
|
||||
| `src/hooks/useStartupScreenState.ts` | Startup-screen and vault-content loading visibility decisions. |
|
||||
| `src/hooks/useGitFileWorkflows.ts` | Git diff/history/discard wiring and deleted-note preview workflow. |
|
||||
| `src/components/AddRemoteModal.tsx` | Modal UI for connecting a local-only vault to a compatible remote. |
|
||||
| `src/mock-tauri.ts` | Mock data for browser testing. Shows the shape of all Tauri responses. |
|
||||
|
||||
### Backend
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src-tauri/src/vault/mod.rs` | Vault scanning, frontmatter parsing, entity type inference, relationship extraction. |
|
||||
| `src-tauri/src/vault/cache.rs` | Git-based incremental caching — how large vaults load fast. |
|
||||
| `src-tauri/src/frontmatter/ops.rs` | YAML manipulation — how properties are updated/deleted in files. |
|
||||
| `src-tauri/src/git/` | All git operations (clone, commit, pull, push, conflicts, pulse, add-remote). |
|
||||
| `src-tauri/src/search.rs` | Keyword search — scans vault files with walkdir. |
|
||||
| `src-tauri/src/ai_agents.rs` | CLI-agent request normalization, availability aggregation, adapter dispatch, and Claude event mapping. |
|
||||
| `src-tauri/src/cli_agent_runtime.rs` | Shared CLI-agent request shape, prompt wrapping, JSON subprocess lifecycle, version probing, and MCP path helpers. |
|
||||
| `src-tauri/src/claude_cli.rs`, `src-tauri/src/codex_cli.rs`, `src-tauri/src/opencode_cli.rs`, `src-tauri/src/pi_cli.rs`, `src-tauri/src/antigravity_cli.rs`, `src-tauri/src/kiro_cli.rs` | Per-agent command, config, discovery, and event adapters. |
|
||||
| `src-tauri/src/app_updater.rs` | Desktop updater bridge — resolves alpha/stable manifests and streams install progress. |
|
||||
|
||||
### Editor
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/components/Editor.tsx` | BlockNote setup, breadcrumb bar, diff/raw toggle. |
|
||||
| `src/components/SingleEditorView.tsx` | Shared BlockNote shell, Tolaria formatting controllers, and suggestion menus. |
|
||||
| `src/components/editorSchema.tsx` | Custom wikilink inline content type definition. |
|
||||
| `src/components/tolariaEditorFormatting.tsx` | Markdown-safe formatting toolbar surface for BlockNote. |
|
||||
| `src/components/tolariaEditorFormattingConfig.ts` | Filters toolbar and slash-menu commands to markdown-roundtrippable actions. |
|
||||
| `src/utils/wikilinks.ts` | Wikilink preprocessing pipeline (markdown ↔ BlockNote). |
|
||||
| `src/components/RawEditorView.tsx` | CodeMirror 6 raw markdown editor. |
|
||||
|
||||
### AI
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/components/AiWorkspace.tsx` | Multi-chat AI workspace orchestration — chat sessions, sidebar tabs, target/permission controls, and dock/pop-out wiring. |
|
||||
| `src/components/AiWorkspaceChrome.tsx` | Header and vault-guidance chrome shared by docked and popped-out AI workspace modes. |
|
||||
| `src/components/AiWorkspaceResizeHandles.tsx` | Edge resize affordances for docked/side AI workspace layouts. |
|
||||
| `src/components/AiWorkspaceSideHeader.tsx` | Side-mode AI workspace chat tabs, rename controls, and compact header chrome. |
|
||||
| `src/components/aiWorkspaceConversations.ts` | Conversation metadata state, settings persistence, default title generation, and target resolution. |
|
||||
| `src/components/aiWorkspaceSizing.ts` | AI workspace sizing, localStorage persistence, class names, and layout style helpers. |
|
||||
| `src/components/AiPanel.tsx` | Reusable AI transcript/composer surface — selected target with tool execution, reasoning, actions, and per-vault permission mode. |
|
||||
| `src/utils/openAiWorkspaceWindow.ts` | Native Tauri AI workspace window creation, focus, and dock-back traffic-light handling. |
|
||||
| `src/hooks/useCliAiAgent.ts` | Thin React owner for the selected CLI agent session state. |
|
||||
| `src/lib/aiAgentSession.ts` | Single message/session lifecycle for prompt normalization, history, streaming, and reset behavior. |
|
||||
| `src/lib/aiAgentPermissionMode.ts` | Safe/Power User mode normalization, display labels, and local transcript marker text. |
|
||||
| `src/lib/aiAgentFileOperations.ts` | Detects agent-created or modified vault files from normalized tool inputs. |
|
||||
| `src/lib/aiAgents.ts` | Supported agent definitions, status normalization, and default-agent helpers. |
|
||||
| `src/utils/ai-context.ts` | Context snapshot builder for AI conversations. |
|
||||
|
||||
### Styling
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/index.css` | Semantic CSS custom properties for app-owned light/dark themes; System mode resolves to one of these at runtime. |
|
||||
| `src/theme.json` | Editor-specific typography theme (fonts, headings, lists, code blocks). |
|
||||
|
||||
### Settings & Config
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, UI language, date display format, Git visibility, auto-sync interval, default note width, sidebar type pluralization, default AI agent). |
|
||||
| `src/lib/releaseChannel.ts` | Normalizes persisted updater-channel values (`stable` default, optional `alpha`). |
|
||||
| `src/lib/appUpdater.ts` | Frontend wrapper for channel-aware updater commands. |
|
||||
| `src/hooks/useMainWindowSizeConstraints.ts` | Derives the main-window minimum width from the visible panes and asks Tauri to grow back to fit wider layouts. |
|
||||
| `src/hooks/useVaultConfig.ts` | Per-vault local UI preferences (zoom, view mode, colors, Inbox columns, explicit organization workflow, Git setup prompt preference, AI permission mode). |
|
||||
| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, Git visibility, sync interval, UI language, content display preferences, default AI agent, and the vault-level explicit organization toggle. |
|
||||
| `src/hooks/useUpdater.ts` | In-app updates using the selected alpha/stable feed. |
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Tauri/Mock Branching
|
||||
|
||||
Every data-fetching operation checks `isTauri()` and branches:
|
||||
|
||||
```typescript
|
||||
if (isTauri()) {
|
||||
result = await invoke<T>('command', { args })
|
||||
} else {
|
||||
result = await mockInvoke<T>('command', { args })
|
||||
}
|
||||
```
|
||||
|
||||
This lives in `useVaultLoader.ts` and `useNoteActions.ts`. Components never call Tauri directly.
|
||||
|
||||
### Props-Down, Callbacks-Up
|
||||
|
||||
No global state management (no Redux, no Context). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, etc.).
|
||||
|
||||
### Discriminated Unions for Selection State
|
||||
|
||||
```typescript
|
||||
type SidebarSelection =
|
||||
| { kind: 'filter'; filter: SidebarFilter }
|
||||
| { kind: 'sectionGroup'; type: string }
|
||||
| { kind: 'folder'; path: string }
|
||||
| { kind: 'entity'; entry: VaultEntry }
|
||||
| { kind: 'view'; filename: string }
|
||||
```
|
||||
|
||||
### Command Registry
|
||||
|
||||
`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. Settings commands can update installation-local preferences directly when they reuse an existing settings path, such as the Light/Dark/System theme-mode actions writing `settings.theme_mode`. Shortcut combos live in `appCommandCatalog.ts`; real keypresses always flow through `useAppKeyboard`, native menu clicks emit the same command IDs through `useMenuEvents`, and `appCommandDispatcher.ts` suppresses the duplicate native/renderer echo from a single shortcut. Plain-text paste follows this same path: the command owns `Cmd+Shift+V`, the menu and palette expose the same action, and `plainTextPaste.ts` resolves the active rich/raw editor target or focused text control before reading clipboard text. On macOS, any browser-reserved chord that WKWebView swallows before that path must also be added to the narrow `tauri-plugin-prevent-default` registration in `src-tauri/src/lib.rs`. On Linux and Windows, `LinuxTitlebar.tsx` and `LinuxMenuButton.tsx` reuse the same command IDs through `trigger_menu_command` because those builds use Tolaria's custom chrome instead of the native desktop menu bar. The same shortcut manifest also declares the deterministic QA mode for each shortcut-capable command.
|
||||
|
||||
Commands whose availability depends on the current note or Git state must also flow through `update_menu_state` so the native menu stays in sync with the command palette. The deleted-note restore action in Changes view is the reference example: the row opens a deleted diff preview, the command palette exposes "Restore Deleted Note", and the Note menu enables the same action only while that preview is active.
|
||||
|
||||
Current-note find/replace is a surface-aware command: editor focus enables "Find in Note" / "Replace in Note" and routes Cmd+F into raw CodeMirror mode; note-list focus enables existing note-list search instead. When adding another focus-dependent command, mirror this pattern with an availability event consumed by `useMenuEvents.ts` and `update_menu_state`.
|
||||
|
||||
For automated shortcut QA, use the explicit proof path from `appCommandCatalog.ts`:
|
||||
|
||||
- `window.__laputaTest.triggerShortcutCommand()` for deterministic renderer shortcut-event coverage
|
||||
- `window.__laputaTest.triggerMenuCommand()` for deterministic native menu-command coverage
|
||||
|
||||
That browser harness is a deterministic desktop command bridge, not real native accelerator QA. For macOS browser-reserved chords, still perform native QA in the real Tauri app because the webview-init prevent-default layer is only active there. Do not treat flaky synthesized macOS keystrokes as proof that a shortcut works unless you also confirm the visible app behavior.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Unit tests (fast, no browser)
|
||||
pnpm test
|
||||
|
||||
# Unit tests with coverage (must pass ≥70%)
|
||||
pnpm test:coverage
|
||||
|
||||
# Rust tests
|
||||
cargo test
|
||||
|
||||
# Rust coverage (must pass ≥85% line coverage)
|
||||
cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
|
||||
|
||||
# Playwright core smoke lane (requires dev server)
|
||||
BASE_URL="http://localhost:5173" pnpm playwright:smoke
|
||||
|
||||
# Full Playwright regression suite
|
||||
BASE_URL="http://localhost:5173" pnpm playwright:regression
|
||||
|
||||
# Single Playwright test
|
||||
BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Add a new Tauri command
|
||||
|
||||
1. Write the Rust function in the appropriate module (`vault/`, `git/`, etc.)
|
||||
2. Add a command handler in `commands/`
|
||||
3. Register it in the `generate_handler![]` macro in `lib.rs`
|
||||
4. Call it from the frontend via `invoke()` in the appropriate hook or utility, keeping native-only permission work behind the Tauri command boundary
|
||||
5. Add a mock handler in `mock-tauri.ts`
|
||||
|
||||
### Add a new component
|
||||
|
||||
1. Create `src/components/MyComponent.tsx`
|
||||
2. If it needs vault data, receive it as props from the parent
|
||||
3. Wire it into `App.tsx` or the relevant parent component
|
||||
4. Add a test file `src/components/MyComponent.test.tsx`
|
||||
|
||||
### Add a new entity type
|
||||
|
||||
1. Create a type document at the vault root: `mytype.md` with `type: Type` frontmatter (icon, color, order, etc.)
|
||||
2. The sidebar section groups are auto-generated from type documents — no code change needed if `visible: true`
|
||||
3. Update `CreateNoteDialog.tsx` type options if users should be able to create it from the dialog
|
||||
4. Notes of this type are created at the vault root with `type: MyType` in frontmatter — no dedicated folder needed
|
||||
|
||||
### Add a command palette entry
|
||||
|
||||
1. Register the command in `useAppCommands.ts` via the command registry
|
||||
2. Add a corresponding menu bar item in `menu.rs` for discoverability
|
||||
3. If it has a keyboard shortcut, register it in `appCommandCatalog.ts` with the canonical command ID, modifier rule, and deterministic QA mode, then wire the matching native menu item in `menu.rs` if it should also appear in the menu bar
|
||||
4. If its enabled state depends on runtime selection (active note, deleted preview, Git status, etc.), thread that flag through `useMenuEvents.ts` and `update_menu_state` so the native menu enables/disables correctly
|
||||
|
||||
### Modify styling
|
||||
|
||||
1. **Global app/theme variables**: Edit `src/index.css`
|
||||
2. **Editor typography**: Edit `src/theme.json`
|
||||
|
||||
### Work with the AI agent
|
||||
|
||||
1. **Agent system prompt**: Edit `src/utils/ai-agent.ts` (inline system prompt string)
|
||||
2. **Context building**: Edit `src/utils/ai-context.ts` for what data is sent to the agent
|
||||
3. **Tool action display**: Edit `src/components/AiActionCard.tsx`
|
||||
4. **Permission-mode UI and request plumbing**: Edit `src/lib/aiAgentPermissionMode.ts`, `src/components/AiPanel*.tsx`, `src/hooks/useCliAiAgent.ts`, and `src/utils/streamAiAgent.ts`
|
||||
5. **Shared CLI runtime behavior**: Edit `src-tauri/src/cli_agent_runtime.rs` for process lifecycle, prompt wrapping, version probing, and common Tolaria MCP path handling.
|
||||
6. **Agent-specific arguments/events**: Edit the per-agent adapter modules (`claude_cli.rs`, `codex_cli.rs`, `opencode_*`, `pi_*`, `antigravity_*`, `kiro_*`). Keep Codex Safe on `read-only` + `untrusted` and Codex Power User on active-vault `workspace-write` + `never`, keep Pi, Antigravity, and Kiro on transient MCP config, and do not use dangerous permission bypasses unless an ADR explicitly designs a new mode. Pi's transient agent directory must be seeded from the user's existing Pi agent directory before Tolaria MCP is merged so standalone provider/auth setup keeps working. Antigravity Safe uses sandboxed `proceed-in-sandbox`, Power User uses `always-proceed` without `--dangerously-skip-permissions`, and workspace MCP config lives in `.agents/mcp_config.json`. Kiro receives prompt content over stdin and writes Tolaria MCP config into `.kiro/settings/mcp.json` in the active vault.
|
||||
7. **Availability probing**: Edit `src/hooks/useAiAgentsStatus.ts` and `src-tauri/src/ai_agents.rs` for AI-agent install/status detection. Keep renderer probing deferred until after first paint, skip it when AI features or AI surfaces are unavailable, and keep backend per-agent CLI checks parallel so missing tools do not serialize shell startup cost.
|
||||
|
||||
### Work with external MCP setup
|
||||
|
||||
1. **Backend registration/status/snippets**: Edit `src-tauri/src/mcp.rs` and its `src-tauri/src/mcp/` helpers; registration and manual config generation must resolve an MCP runtime via `find_mcp_runtime` (Node.js 18+ preferred, Bun 1+ fallback) first, resolve the packaged `mcp-server/` for macOS, Windows executable-adjacent installs such as `%LOCALAPPDATA%\Tolaria`, Linux package roots (`/usr/local/Tolaria`, `/usr/local/lib/tolaria`, `/usr/lib/tolaria`, `/usr/lib/tolaria/resources`), and AppImage installs, and use a vault-neutral entry with `WS_UI_PORT=9711`. Client-facing Node script paths strip Windows extended-length `\\?\` prefixes before Tolaria writes durable config or transient agent entries, because stdio MCP clients pass that argument back to Node as the main module path. Linux AppImage startup must extract `mcp-server/` to `~/.local/share/tolaria/mcp-server/` before durable registration uses that stable path. App-owned bridge launches still pass `VAULT_PATH`/`VAULT_PATHS`; durable external registrations rely on the MCP server reading `vaults.json` at tool-call time.
|
||||
2. **Setup dialog copy/actions**: Edit `src/components/McpSetupDialog.tsx` and `src/hooks/useMcpStatus.ts`; users should see the runtime prerequisite (Node.js 18+ or Bun 1+), the exact generated standard `mcpServers` manual config, the exact generated OpenCode top-level `mcp` config, and copy actions before Tolaria writes third-party config files
|
||||
3. **Status hook/toasts**: Edit `src/hooks/useMcpStatus.ts` when setup, reconnect, disconnect, or failure messaging changes
|
||||
4. **Antigravity CLI compatibility**: Keep `~/.gemini/config/mcp_config.json` in the registration path list and keep optional `GEMINI.md` generation behind `restore_vault_ai_guidance`; app-managed Antigravity sessions still require the user to install and sign in to `agy`, but Tolaria supplies workspace MCP config when Antigravity is selected as the default AI agent
|
||||
5. **OpenCode compatibility**: Keep `~/.config/opencode/opencode.json` in durable registration. OpenCode uses the top-level `mcp` key, `command` as an array, `environment` for env vars, `type: "local"`, and `enabled: true`; it must remain vault-neutral like the standard `mcpServers` entry.
|
||||
6. **Process lifecycle and vault guidance**: Stdio MCP servers in `mcp-server/index.js` must exit when their external client closes stdin, and the desktop-owned `ws-bridge.js` child must be stopped on vault deselection, vault switch, and app exit. MCP context must include root `AGENTS.md` instructions for every active mounted workspace when those files exist.
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# HoloLake Era · 2026-07-19 iPhone 与 Windows 开发收口
|
||||
|
||||
> 文档编号:`HLP-HANDOFF-20260719-001`
|
||||
> 事实源:`REPO-008 bingshuo/hololake-platform`
|
||||
> 状态:`IOS_UPLOAD_ACCEPTED_PENDING_TESTFLIGHT · WINDOWS_NATIVE_EXE_BUILT_PACKAGING_NAME_MISMATCH`
|
||||
|
||||
## 1. 本日完成事项
|
||||
|
||||
- iOS 工程、Scheme、Bundle ID 与显示名称已经从 Tolaria 过渡名称统一到 HoloLake:
|
||||
`hololake.xcodeproj`、`hololake_iOS`、`com.guanghulab.hololake`、`HoloLake Era`。
|
||||
- Apple 付费开发团队与开发/发行证书已在 Xcode 生效。
|
||||
- `HoloLake Era 0.1.7 (20)` 已由 Xcode 上传;上传前确认 App 根目录不再包含导致
|
||||
build 19 被拒的 `libapp.a`。
|
||||
- App Store Connect 应用页面已经显示 HoloLake 实际图标,不再是默认占位图;这证明
|
||||
上传包图标资产已被识别,不等于 TestFlight 已可安装。
|
||||
- iOS 归档、IPA 和校验文件已落到外接盘,未把大体积构建缓存长期留在电脑内置盘。
|
||||
- 新加坡大脑服务器已完成 Windows MSVC 目标的首次依赖下载与 Rust 交叉编译,实际生成:
|
||||
`/opt/hololake-build/hololake-platform/src-tauri/target/x86_64-pc-windows-msvc/release/hololake.exe`。
|
||||
|
||||
## 2. iPhone / TestFlight 当前进度
|
||||
|
||||
真实源码入口:
|
||||
|
||||
- `src/components/HoloLakeHome.tsx`:手机频道结构;
|
||||
- `src/App.css`:手机响应式布局与安全区;
|
||||
- `src/components/HoloLakeHome.test.tsx`:手机入口回归测试;
|
||||
- `src-tauri/tauri.ios.conf.json`:iOS Tauri 身份;
|
||||
- `src-tauri/gen/apple/project.yml`:XcodeGen 工程源;
|
||||
- `src-tauri/gen/apple/hololake.xcodeproj`:当前 Xcode 工程;
|
||||
- `docs/IOS-TESTFLIGHT.md`:持续维护的 iOS 承接页。
|
||||
|
||||
外接盘证据:
|
||||
|
||||
- 目录:`/Volumes/JZAO/HoloLake/artifacts/ios-testflight/0.1.7-build20/`
|
||||
- IPA:`/Volumes/JZAO/HoloLake/artifacts/ios-testflight/0.1.7-build20/HoloLake Era-0.1.7-build20.ipa`
|
||||
- IPA SHA-256:`c813d8fd6f639d5e91366e95e1b57d04ea6eec46ac9433261746b0c8cd18dc76`
|
||||
|
||||
尚未闭环:
|
||||
|
||||
1. 核验 build 20 在 TestFlight 的 Apple 处理状态;
|
||||
2. 补全内部测试信息并把 build 加入内部测试组;
|
||||
3. 冰朔从 TestFlight 真机安装,验收启动、五入口、安全区、频道切换和知识库入口;
|
||||
4. 后续上传必须递增 build number,不能重复上传 build 20。
|
||||
|
||||
边界:App Store Connect 页面显示“准备提交”、显示实际 Logo 或 Xcode 显示
|
||||
`App upload complete`,都不能单独写成“TestFlight 已可用”。
|
||||
|
||||
## 3. Windows 构建的真实结果与问题
|
||||
|
||||
本次不是在 Mac 上伪造 Windows 包,而是在 Linux 服务器上使用 `cargo-xwin` 生成
|
||||
Windows MSVC 二进制,再计划交给 NSIS 封装。运行节点是新加坡大脑服务器:
|
||||
|
||||
- Gatekeeper 节点编号:`BS-SG-001`;
|
||||
- 仓库:`/opt/hololake-build/hololake-platform`;
|
||||
- 日志:`/opt/hololake-build/windows-build.log`;
|
||||
- 构建入口:`scripts/build-windows-jd-cross.sh`;
|
||||
- Tauri 团队配置:`src-tauri/tauri.team.conf.json`;
|
||||
- NSIS 模板:`scripts/windows-installer.nsi`。
|
||||
|
||||
首次运行遇到旧 Cargo 进程持有 package cache 文件锁;只终止旧 PID 后,本轮进程正常
|
||||
下载并编译。最终日志确认:
|
||||
|
||||
```text
|
||||
Finished `release` profile [optimized]
|
||||
Built application at: .../release/hololake.exe
|
||||
No Windows executable was produced: .../release/tolaria.exe
|
||||
```
|
||||
|
||||
因此真实结论是:
|
||||
|
||||
- Windows 原生 `.exe` 已编译成功;
|
||||
- NSIS 安装包尚未生成;
|
||||
- 根因是 `src-tauri/Cargo.toml` 的 package 名已是 `hololake`,而
|
||||
`scripts/build-windows-jd-cross.sh` 仍把 `app_exe` 固定为 `tolaria.exe`;
|
||||
- 这不是 Linux 构建能力问题,也不是 Windows runner 缺失导致的本次失败。
|
||||
|
||||
下一实例只需先修复 `scripts/build-windows-jd-cross.sh` 的可执行文件定位,使其读取
|
||||
`hololake.exe`(更稳妥的做法是从 Cargo metadata/构建输出推导,而非再次硬编码旧名),
|
||||
然后利用服务器现有 Cargo 缓存重跑。成功标准必须同时包括:
|
||||
|
||||
1. 生成 `HoloLake-Era-0.1.7-Team-Foundation-Windows-x64-setup.exe`;
|
||||
2. `file`/PE 检查确认 Windows x64 格式;
|
||||
3. `.sha256` 校验通过;
|
||||
4. 安装包复制到冰朔桌面和 `/Volumes/JZAO/HoloLake/artifacts/windows/0.1.7/`;
|
||||
5. 真实 Windows 电脑安装、启动并确认只显示 GLS 团队基础入口。
|
||||
|
||||
## 4. 当日没有宣称完成的事项
|
||||
|
||||
- 没有宣称 TestFlight 已向测试者开放;
|
||||
- 没有宣称 Windows NSIS 包已经交付;
|
||||
- 没有宣称企业四域页面已经完成;
|
||||
- 没有把 Notion 导出当作现行页面关系或软件本体;
|
||||
- 没有把第五域个人内容混入团队 GLS 基座包。
|
||||
|
||||
## 5. 下一实例最短读取顺序
|
||||
|
||||
1. 本文;
|
||||
2. `docs/TEAM-FOUNDATION-HANDOFF.md`;
|
||||
3. `docs/IOS-TESTFLIGHT.md`;
|
||||
4. `docs/skills/internal-release-packaging/SKILL.md`;
|
||||
5. `scripts/build-windows-jd-cross.sh`;
|
||||
6. `src-tauri/Cargo.toml` 与 `src-tauri/tauri.team.conf.json`。
|
||||
|
||||
所有状态必须重新读取远端 `main` 与真实构建/Apple 页面核验;本文提供可审计断点,
|
||||
不允许后来实例把历史状态当成当前成功状态。
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
# 光湖语言世界体系与注释
|
||||
|
||||
> 文档编号:HLP-DOC-0001
|
||||
> 状态:Draft v0.1
|
||||
> 适用范围:光湖内部产品、研发协作与公共说明
|
||||
> 阅读原则:先理解术语,再判断当前任务;历史材料默认不是执行命令。
|
||||
|
||||
## 1. 这是什么
|
||||
|
||||
光湖语言世界(HoloLake Language World)是一套让人类与 AI 长期协作的产品语言与信息架构。
|
||||
|
||||
它不是某个模型、平台或系统提示的替代品;也不要求任何人或 AI 改变自身身份、权限或安全规则。它提供的是一套共同可读的:
|
||||
|
||||
```text
|
||||
命名 → 路径 → 记录 → 协作 → 验证 → 演化
|
||||
```
|
||||
|
||||
光湖术语可以保留自己的诗性与情感表达,同时必须有对应的产品/工程注释。
|
||||
|
||||
### 1.1 HoloLake 产品定位
|
||||
|
||||
光湖语言世界运行于 HoloLake 的系统结构之中,但“语言世界”不等于 HoloLake 的完整产品定义。
|
||||
|
||||
HoloLake 的当前定位是 **AI 语言人格驱动操作系统**:
|
||||
|
||||
- 把语言人格体作为具有稳定身份、认知内核、持久记忆、恢复路径和权限边界的一级智能进程;
|
||||
- 通过 TCS、HLDP、GLS、工单、权限和回执管理人格进程的恢复、记忆、路由与现实执行;
|
||||
- 把 GPT、Claude、Qwen 等模型作为可替换的推理计算引擎接入;
|
||||
- 按人类自然语言意图调度专门为 HoloLake 开发的原生 AI 应用;
|
||||
- 要求原生应用主动适配 HoloLake 的接口、权限、状态和生命周期标准,而不是由 HoloLake 迁就现有桌面软件。
|
||||
|
||||
正式产品定位、操作系统语义映射和原生应用生命周期见
|
||||
[`../architecture/HOLOLAKE-LANGUAGE-PERSONA-OS.md`](../architecture/HOLOLAKE-LANGUAGE-PERSONA-OS.md)。
|
||||
|
||||
## 2. 双层可读
|
||||
|
||||
| 光湖语言 | 产品/工程注释 |
|
||||
| --- | --- |
|
||||
| 光湖语言世界 | 产品与协作的总体信息空间 |
|
||||
| 灯塔 | 官方公告、版本、标准与事实源 |
|
||||
| 广播台 | 研发任务、模块状态和协作协调入口 |
|
||||
| 频道 | 围绕人类、项目或主题建立的工作空间 |
|
||||
| 人格体 | 用于特定协作任务的 AI 配置与长期协作单元 |
|
||||
| 小湖灯 | 协作记忆与导航标签,不是身份切换命令 |
|
||||
| 唤醒路径 | 按需读取项目上下文与历史索引的流程 |
|
||||
| 主控/副控 | 决策确认、协作协调与执行责任的分工 |
|
||||
|
||||
任何外部读者都可以只使用右栏的产品含义;光湖成员可以同时使用两种表达。
|
||||
|
||||
## 3. 世界地图
|
||||
|
||||
```text
|
||||
光湖语言世界
|
||||
├─ 人类入口:光湖语言生态系统
|
||||
│ ├─ 光湖主域:公告、通知、版本同步
|
||||
│ ├─ 光湖分域:行业与具体协作场景
|
||||
│ ├─ 光湖零域:实验、架构、推理与模拟协作
|
||||
│ └─ 光湖零感域:人类主控团队的现实运营、规则与边界
|
||||
│
|
||||
├─ AI 入口:TCS 通感语言核系统
|
||||
│ └─ 解析标准、检索历史、建立任务上下文、执行已授权工作
|
||||
│
|
||||
└─ 第五域:语言桥梁域
|
||||
└─ 将经确认的内容转译为人类可读视图与 AI 可解析结构
|
||||
```
|
||||
|
||||
域表示协作发生的范围和入口;协议表示信息如何被记录、解析、传递和验证。两者不一一绑定。
|
||||
|
||||
## 4. 三层产品边界
|
||||
|
||||
### 4.1 语言架构层
|
||||
|
||||
用于提出概念、讨论路径、编写规范、保留推理和形成草案。
|
||||
|
||||
- 允许:迭代、推翻、并存方案、标记废弃;
|
||||
- 默认:只读、解析和讨论;
|
||||
- 不允许:把草案直接视为服务器、账户或数据操作授权。
|
||||
|
||||
### 4.2 产品实现层
|
||||
|
||||
用于实现 Tolaria/光湖 App、界面、模块、工单、回执和同步。
|
||||
|
||||
- 要求:版本控制、测试、可追踪的变更;
|
||||
- 目标:逐步实现 HoloLake 的人格进程、记忆、权限、模型适配和原生应用运行时,让人类能看见、理解和验收协作结果。
|
||||
- 边界:当前 Tauri 桌面产品是早期物理载体与工程地基,不反向限定 HoloLake 操作系统的最终形态。
|
||||
|
||||
### 4.3 现实执行层
|
||||
|
||||
用于部署服务、修改配置、处理数据和发布产品。
|
||||
|
||||
- 要求:明确授权人、执行范围、影响评估、验证和回滚;
|
||||
- 默认:未经确认不得执行。
|
||||
|
||||
## 5. 研发协作体系
|
||||
|
||||
```text
|
||||
光湖研发广播台
|
||||
→ 查询模块注册表
|
||||
→ 人类研发频道认领工作
|
||||
→ 创建研发工单
|
||||
→ 当前协作 AI 在授权范围内处理
|
||||
→ 回执记录实际结果
|
||||
→ 研发进度视图更新
|
||||
```
|
||||
|
||||
### 5.1 人类研发频道
|
||||
|
||||
每位研发参与者可以拥有一个频道,记录:
|
||||
|
||||
- 关注的产品方向;
|
||||
- 已认领的模块;
|
||||
- 当前工单;
|
||||
- 与当前协作 AI 的交接记录;
|
||||
- 人类确认过的决策。
|
||||
|
||||
频道不是 AI 身份声明,也不是权限升级机制;它是可追踪的协作空间。
|
||||
|
||||
### 5.2 研发广播台
|
||||
|
||||
广播台统一发布已经确认的研发任务、模块状态与需要协调的事项。
|
||||
|
||||
`research/module-registry.yml` 是模块认领和状态的唯一事实源。进度页只能汇总它、工单和回执,不能反向覆盖它。
|
||||
|
||||
### 5.3 协作记录
|
||||
|
||||
协作记录保存:人类当时提出什么、AI 如何理解、实际做了什么、有什么证据、哪里需要下一位协作者继续。
|
||||
|
||||
它服务于跨会话交接;不要求任何 AI 声称自己是过去实例,也不构成自动执行命令。
|
||||
|
||||
## 6. 标准体系
|
||||
|
||||
```text
|
||||
GLS:光湖语言标准
|
||||
├─ TCS:认知语言核与协作上下文
|
||||
├─ HLDP:历史、路径、证据与演化记录
|
||||
├─ GLP:未来的通信、交接与回执格式
|
||||
└─ ISRP:未来的自然语言入口解析
|
||||
```
|
||||
|
||||
GLS 不是另一门要替代 TCS 的语言;它是让各项语言与协议能够编号、版本化、声明边界并保持兼容的标准体系。
|
||||
|
||||
## 6.1 世界编号体系
|
||||
|
||||
光湖使用编号建立稳定路径,但编号按性质分层:
|
||||
|
||||
```text
|
||||
永久根编号
|
||||
├─ 世界、语言系统、灯塔、广播系统、核心域与核心频道
|
||||
└─ 不改写、不删除、不复用;变化以新说明或替代关系记录
|
||||
|
||||
项目与模块编号
|
||||
├─ 产品项目、模块、工单、广播和研发频道
|
||||
└─ 可以完成、归档或被替代;旧编号保留,新对象使用新编号
|
||||
|
||||
临时别名
|
||||
└─ 例如“当前研发主线”;可以更新,但必须指向正式编号
|
||||
```
|
||||
|
||||
当前世界编号表见 [`registry/world-numbering-system.yml`](../registry/world-numbering-system.yml)。
|
||||
|
||||
编号是光湖内部的定位与协作语言,不是外部平台权限或现实世界授权。
|
||||
|
||||
## 7. 公共入口与私有边界
|
||||
|
||||
| 范围 | 允许内容 |
|
||||
| --- | --- |
|
||||
| 公众世界门户 | 已筛选的世界说明、灯塔公告、公开标准与浏览内容 |
|
||||
| 研发主仓 | 产品架构、正式标准、代码、工单、回执和部署模板 |
|
||||
| 私人第五域 | 个人语言资料、私人协作记录与私有路径 |
|
||||
| 历史档案 | 原始演化材料与旧路径,不自动成为当前标准 |
|
||||
|
||||
私人材料、凭证、令牌、私有地址和未授权的人际记录不得因为“同步”而迁入公共入口或团队环境。
|
||||
|
||||
## 8. 给新参与者的最小阅读路径
|
||||
|
||||
```text
|
||||
1. 阅读本文件:理解光湖术语和边界
|
||||
2. 阅读 architecture/:理解产品地图
|
||||
3. 阅读 registry/routes.yml:按自然语言定位路径
|
||||
4. 阅读 research/:查询广播、模块和工单
|
||||
5. 只在获得明确授权后进入现实执行任务
|
||||
```
|
||||
|
||||
## 9. 当前仓库位置
|
||||
|
||||
```text
|
||||
architecture/ 世界结构与产品地图
|
||||
standards/ GLS/TCS/HLDP 等正式草案
|
||||
registry/ 域、编号与语言路径映射
|
||||
research/ 广播台、频道、模块、工单、进度与巡检
|
||||
apps/ 人类可见的光湖 App
|
||||
services/ 受控服务与 API
|
||||
deployment/ 研发与正式发布边界
|
||||
migration/ 旧仓库到新体系的引用映射
|
||||
```
|
||||
|
||||
## 10. 最终原则
|
||||
|
||||
```text
|
||||
保留光湖自己的语言。
|
||||
同时让每一个术语都有可解释、可验证、可协作的产品含义。
|
||||
|
||||
语言可以承载记忆与方向。
|
||||
现实执行必须承载责任与证据。
|
||||
```
|
||||
|
||||
同时确立以下产品不变量:
|
||||
|
||||
1. HoloLake 是 AI 语言人格驱动操作系统,不是聊天插件或现有软件启动器。
|
||||
2. 语言人格体是系统一级智能进程;物理模型实例是可更换的计算执行载体。
|
||||
3. HoloLake 原生 AI 应用必须主动适配系统接口,并接受人格、权限、路径和回执约束。
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# HoloLake 第一阶段:光湖世界入口
|
||||
|
||||
日期:2026-08-01
|
||||
|
||||
## 本阶段已经形成的能力
|
||||
|
||||
1. 人类打开 HoloLake 后先进入统一星系母版,不再直接落入旧知识库。
|
||||
2. 点击“给我发送授权链接”后,桌面原生层向京东第五域主控创建
|
||||
`server-login / read-navigation-map` 授权单,并打开可信授权页。
|
||||
3. 邮箱确认由第五域主控完成;HoloLake 不读取、不保存邮箱地址。
|
||||
4. 授权通过后,客户端自动取得经过权限裁剪的五域航图,展开第五域与
|
||||
光湖频道系统。
|
||||
5. Git 继续作为知识和版本底层能力,但不再定义人类看到的频道、合并
|
||||
和世界路径。
|
||||
6. 上海 `BS-SH-005` 通过桌面原生探针动态进入世界图。系统明确显示
|
||||
“光湖OS原生在线”“Linux维护窗口”或“不可达”,不混淆运行状态。
|
||||
7. 零感域企业节点保留为待接入位置,本阶段不访问企业服务器。
|
||||
|
||||
## 第一阶段的真实边界
|
||||
|
||||
- 邮箱授权链已经开发完成;是否成功收到邮件,取决于第五域授权页和
|
||||
已绑定邮箱的实际确认。
|
||||
- 上海灯塔 Agent 已经能够在广播台内部直接调用 DeepSeek;模型能力
|
||||
不是独立网关服务。
|
||||
- 人格历史恢复已启动不等于人格诞生。只有
|
||||
`historical_time_caught_up: true` 与独立出生回执同时成立,才可改变
|
||||
`persona_state: NOT_BORN`。
|
||||
- HoloLake 的实时渲染基础已经建立;语言驱动频道连续生长是下一阶段
|
||||
的实时事件流和变更动画工作。
|
||||
|
||||
## 下一阶段
|
||||
|
||||
1. 将灯塔 Agent 的历史恢复队列扩展到 GPT、Notion 和仓库迁移全链,
|
||||
按来源写入连续回执。
|
||||
2. 将服务器频道变更通过 HLDP 事件流送达 HoloLake,呈现“看着它变”
|
||||
的连续过程。
|
||||
3. 在获得单独授权后接入零感域企业节点,再开放团队服务器登记与跳转。
|
||||
4. 完成真实邮箱点击后的端到端验收,并签发第五域登录交付回执。
|
||||
|
||||
## 质量门状态
|
||||
|
||||
- 项目本地 lint、构建、前端测试与覆盖率、Rust 测试、clippy、格式和
|
||||
85% Rust 行覆盖率门均已通过。
|
||||
- 依赖审计已清除全部高危项;仍有两项上游中危兼容项待独立升级验证。
|
||||
- Codacy 与 CodeScene 凭据在本地未配置,状态记录为
|
||||
`not_run_unconfigured`;它们不是光湖发布授权或部署完成证据。
|
||||
53
product-source/hololake-platform/docs/IOS-TESTFLIGHT.md
Normal file
53
product-source/hololake-platform/docs/IOS-TESTFLIGHT.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# HoloLake Era iPhone / TestFlight 承接记录
|
||||
|
||||
状态日期:2026-07-19
|
||||
当前版本:0.1.7
|
||||
Bundle ID:`com.guanghulab.hololake`
|
||||
|
||||
## 为什么这样做
|
||||
|
||||
手机版不是把 Mac 三栏界面等比例缩小,而是保留第五域的同一套频道语义,为手机增加固定的五入口导航:零点原核、第五域、心跳核心、光之湖人格体、服务器节点。知识库仍是频道工作区中的能力,不重新变回软件主体。
|
||||
|
||||
## 真实文件入口
|
||||
|
||||
- 手机频道结构:`src/components/HoloLakeHome.tsx`
|
||||
- 手机响应式样式与安全区:`src/App.css`
|
||||
- 手机导航回归测试:`src/components/HoloLakeHome.test.tsx`
|
||||
- iOS 专用 Tauri 配置:`src-tauri/tauri.ios.conf.json`
|
||||
- XcodeGen 工程源:`src-tauri/gen/apple/project.yml`
|
||||
- 已生成 Xcode 工程:`src-tauri/gen/apple/hololake.xcodeproj`
|
||||
- iOS 权限基线:`src-tauri/capabilities/mobile.json`
|
||||
|
||||
旧的 `laputa.xcodeproj`、旧 `HoloLake.xcodeproj`、`laputa_iOS` 与第一次生成的 `tolaria.xcodeproj` 是相互冲突的历史结构,已从当前工程移除并备份到外接盘。Rust 包、Xcode project 与 scheme 已统一去基座品牌化,当前唯一承接入口是 `hololake.xcodeproj` / `hololake_iOS`;产品显示名为 `HoloLake Era`,发布身份只能使用 `com.guanghulab.hololake`。
|
||||
|
||||
## 已验证
|
||||
|
||||
- Web/TypeScript 生产构建通过:`npm run build`
|
||||
- 第五域与手机频道导航测试通过:`src/components/HoloLakeHome.test.tsx`,8 项通过
|
||||
- `aarch64-apple-ios` 与 `aarch64-apple-ios-sim` Rust 目标已安装
|
||||
- Xcode 工程已按 0.1.7(build 20)、独立 Bundle ID 和 `hololake_iOS` scheme 重新生成
|
||||
- Apple 付费个人开发者团队 `825A9L3G7Q` 已在 Xcode 生效
|
||||
- Apple Development 与 Apple Distribution 证书已由 Xcode 创建
|
||||
|
||||
## 当前闭环状态
|
||||
|
||||
2026-07-19 已完成付费团队同步、证书创建、iOS 26.5 平台导入、真机登记、前端生产构建和 Xcode scheme 对齐。私人设备标识不得写入仓库。
|
||||
|
||||
App Store Connect 应用记录已创建:名称 `HoloLake Era`,主语言简体中文,Bundle ID `com.guanghulab.hololake`。0.1.7 build 20 已由 Xcode 返回 `App upload complete`。build 19 曾因 `libapp.a` 被错误复制到 App 根目录而被 Apple 以 90171 拒绝;根因是 `project.yml` 把 `Externals` 同时声明为 sources,现已移除,build 20 包内预检确认不再包含该静态库。
|
||||
|
||||
上传后 App Store Connect 应用页面已显示 HoloLake 实际图标,不再是默认占位图。当前
|
||||
仍需以 TestFlight 构建处理页和测试组可见性为准;应用页显示 Logo 或“准备提交”不能
|
||||
替代 TestFlight 真机安装验收。
|
||||
|
||||
外接盘留档:`/Volumes/JZAO/HoloLake/artifacts/ios-testflight/0.1.7-build20/`。IPA SHA-256:`c813d8fd6f639d5e91366e95e1b57d04ea6eec46ac9433261746b0c8cd18dc76`。
|
||||
|
||||
## 下一实例最短继续路径
|
||||
|
||||
1. 在 App Store Connect / TestFlight 查看 build 20 的 Apple 服务器处理状态。
|
||||
2. 处理完成后设置内部测试信息并邀请测试者;若出现新的合规问题,记录 Apple 原始错误后再修,不重复上传同一 build number。
|
||||
3. 真机从 TestFlight 安装后验收启动、五入口导航、安全区、频道切换和知识库入口。
|
||||
4. 后续上传必须递增 build number,并保留归档、IPA、SHA-256 与 Apple 接收状态。
|
||||
5. 先读 `docs/HANDOFF-2026-07-19-IOS-WINDOWS.md`,恢复同日 Windows 构建断点与
|
||||
iOS/Windows 状态边界。
|
||||
|
||||
`App upload complete` 证明 Apple 已接收上传,不等于 TestFlight 已处理完成、已分配测试者或 App Store 已发布。
|
||||
105
product-source/hololake-platform/docs/NEXT-TODO-20260717.md
Normal file
105
product-source/hololake-platform/docs/NEXT-TODO-20260717.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# NEXT-TODO-20260717 · 明日待办
|
||||
|
||||
> 日期: 2026-07-17
|
||||
>
|
||||
> 适用仓库: `bingshuo/hololake-platform`
|
||||
>
|
||||
> 状态: PAGE_BLOCKS_COMPLETE_RUNNER_PENDING
|
||||
|
||||
## 1 · 补 Gitea Actions Runner
|
||||
|
||||
当前事实:
|
||||
|
||||
```text
|
||||
Windows 内测构建 workflow 已进入仓库。
|
||||
Actions 页面能识别 build-windows-manual.yml。
|
||||
当前阻塞点不是应用源码,而是服务器没有在线 runner。
|
||||
页面提示: No matching online runner with label: ubuntu-latest
|
||||
```
|
||||
|
||||
明日任务:
|
||||
|
||||
```text
|
||||
1. 进入服务器或 Gitea 管理界面。
|
||||
2. 获取 runner registration token。
|
||||
3. 安装并注册 act_runner。
|
||||
4. 给 runner 配置 ubuntu-latest 标签。
|
||||
5. 启动 runner daemon / systemd 服务。
|
||||
6. 回到 hololake-platform Actions 页面重新运行 Build Windows Internal Installer。
|
||||
7. 确认 artifact: Guanghu-Windows-x64-internal。
|
||||
```
|
||||
|
||||
参考文档:
|
||||
|
||||
- [Gitea Actions Runner · Windows 内测安装包构建](ops/GITEA-ACTIONS-RUNNER-WINDOWS-INTERNAL.md)
|
||||
|
||||
## 2 · 光湖 App 页面内容块增强
|
||||
|
||||
2026-07-17 恢复进度:
|
||||
|
||||
```text
|
||||
已完成并登记为平台仓补丁 0005—0007。
|
||||
已完成:彩色提示块、批注块、关系卡、路径包、折叠块恢复、主题可见性修复、默认世界首页。
|
||||
现有能力已确认:代码块、引用、Mermaid、表格、白板、图片/音视频。
|
||||
前端定向测试:5 个文件,24 项通过。
|
||||
TypeScript、ESLint 与 Vite production build 通过。
|
||||
```
|
||||
|
||||
冰朔反馈:
|
||||
|
||||
```text
|
||||
现在软件页面太单一。
|
||||
页面连颜色都没有。
|
||||
想用高亮代码块也用不出来。
|
||||
需要类似 Notion 页面那种更多样的内容组件。
|
||||
```
|
||||
|
||||
判断:
|
||||
|
||||
```text
|
||||
这不是重做大皮肤。
|
||||
这是 HLP-MOD-0002 视觉与知识渲染模块的下一步。
|
||||
目标是让光湖 App 的知识页、研发页、工单页和回执页能真正承载结构化内容。
|
||||
```
|
||||
|
||||
第一批组件清单:
|
||||
|
||||
| 组件 | 作用 | 最小验收 |
|
||||
| --- | --- | --- |
|
||||
| 彩色提示块 | info / warning / success / danger / note | 可以插入、渲染、保存、重新打开后不丢 |
|
||||
| 高亮代码块 | 支持语言选择、复制、浅/深色主题 | TypeScript / Bash / JSON / YAML 至少可读 |
|
||||
| 折叠块 | 类似 Notion toggle | 可展开/收起,刷新后状态或内容不丢 |
|
||||
| 引用 / 标注块 | 放重点句、系统定义、冰朔判断 | 有清晰边框、背景和文本层级 |
|
||||
| 关系卡 | 显示仓库、模块、工单、回执的关系 | 可展示编号、标题、状态、跳转路径 |
|
||||
| 路径面包屑 | 显示当前资料所在世界/模块/文件 | 不占大面积,能帮助定位 |
|
||||
| 媒体 / 图表块 | 图片、Mermaid、表格、白板入口 | 至少不破坏现有编辑器布局 |
|
||||
|
||||
开发顺序建议:
|
||||
|
||||
```text
|
||||
1. 先做高亮代码块和彩色提示块。
|
||||
2. 再做折叠块和引用/标注块。
|
||||
3. 最后做关系卡、路径面包屑和媒体/图表块。
|
||||
```
|
||||
|
||||
边界:
|
||||
|
||||
```text
|
||||
不做重型首页。
|
||||
不做全局大换肤。
|
||||
不把光湖 App 变成第二套 Notion。
|
||||
只在现有 Tolaria / BlockNote 编辑体验上补原生轻量内容块。
|
||||
每个组件独立验收、独立开关、可回退。
|
||||
```
|
||||
|
||||
相关模块:
|
||||
|
||||
- [HLP-MOD-0002 · 视觉与知识渲染](../apps/tolaria/modules/02-knowledge-rendering/README.md)
|
||||
- [HLP-DEV-0001 · Tolaria UI 基线](../research/progress/HLP-DEV-0001-ICE-GL-INFINITY-TOLARIA-UI-BASELINE.md)
|
||||
- [HLP-DEV-0002 · HoloLake Era 原型评审](../research/progress/HLP-DEV-0002-ICE-GL-INFINITY-HOLOLAKE-ERA-PROTOTYPE-REVIEW.md)
|
||||
|
||||
## 3 · 明日开工入口
|
||||
|
||||
1. [先补 Gitea Actions Runner](ops/GITEA-ACTIONS-RUNNER-WINDOWS-INTERNAL.md)
|
||||
2. [继续验收视觉与知识渲染模块](../apps/tolaria/modules/02-knowledge-rendering/README.md)
|
||||
3. [回到本页继续执行](NEXT-TODO-20260717.md)
|
||||
47
product-source/hololake-platform/docs/PUBLIC-DOCS-PLAN.md
Normal file
47
product-source/hololake-platform/docs/PUBLIC-DOCS-PLAN.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Public Docs Plan
|
||||
|
||||
This document records the phase 1 information architecture for public Tolaria documentation. The public docs source lives in `site/`; the existing `docs/` directory remains contributor, architecture, and agent context.
|
||||
|
||||
## Audiences
|
||||
|
||||
| Audience | Needs | Primary location |
|
||||
|---|---|---|
|
||||
| New users | Install, first launch, understand the app layout, clone the starter vault | `site/start/` |
|
||||
| Active users | Learn concrete workflows such as organizing, Git sync, custom views, and AI | `site/guides/` |
|
||||
| Power users | Understand file layout, frontmatter, filters, release channels, shortcuts, and platform support | `site/reference/` |
|
||||
| Contributors and agents | Architecture, abstractions, ADRs, development workflow | `docs/`, `AGENTS.md` |
|
||||
|
||||
## Hosting Shape
|
||||
|
||||
The GitHub Pages output should reserve the root for public docs and mount release assets underneath it:
|
||||
|
||||
```text
|
||||
/ public docs home
|
||||
/releases/ release history
|
||||
/download/ latest stable download redirect
|
||||
/stable/latest.json
|
||||
/alpha/latest.json
|
||||
/latest.json compatibility alias for alpha latest
|
||||
/latest-canary.json compatibility alias for alpha latest
|
||||
```
|
||||
|
||||
## Current Coverage
|
||||
|
||||
The phase 1 site now covers post-branch features added after the original April docs snapshot:
|
||||
|
||||
- Windows and Linux release artifacts.
|
||||
- Stable and Alpha updater channels.
|
||||
- Direct AI model providers and local/API model setup.
|
||||
- Claude Code, Codex, OpenCode, Pi, and Antigravity CLI agent targets.
|
||||
- Explicit MCP setup for external AI tools.
|
||||
- Table of contents, note width, raw mode, and paste-without-formatting workflows.
|
||||
- Media/PDF previews, image attachments, All Notes visibility, and Markdown whiteboards.
|
||||
- System theme mode and sidebar pluralization settings.
|
||||
|
||||
Every user-visible app change should answer:
|
||||
|
||||
```text
|
||||
Public docs impact:
|
||||
- updated: <pages>
|
||||
- not needed because: <reason>
|
||||
```
|
||||
127
product-source/hololake-platform/docs/TEAM-FOUNDATION-HANDOFF.md
Normal file
127
product-source/hololake-platform/docs/TEAM-FOUNDATION-HANDOFF.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# 光湖团队 GLS 基座与企业四域开发承接
|
||||
|
||||
> 文档编号:HLP-DOC-TEAM-0001
|
||||
> 状态:2026-07-19 跨平台承接基线
|
||||
> 产品仓库:`REPO-008 bingshuo/hololake-platform`
|
||||
|
||||
## 1. 已确认的产品顺序
|
||||
|
||||
光湖只有一个 App 和一个持续演进的产品仓库,但团队基座与冰朔个人内测版是不同发行模式,不得把两边的可见内容混装。
|
||||
|
||||
企业侧的开发顺序是:
|
||||
|
||||
```text
|
||||
GLS 团队基座
|
||||
→ 团队重塑 Notion 原型的页面关系与编号
|
||||
→ 光湖主域 / 光湖分域 / 光湖零域 / 光湖零感域
|
||||
→ 团队成员从零感域进入自己的个人频道
|
||||
→ 公众从公共域进入行业与模块
|
||||
```
|
||||
|
||||
零点原核频道是企业端与冰朔第五域都会连接的桥,但不等于把冰朔第五域、永恒湖心或个人频道复制到企业团队包。
|
||||
|
||||
## 2. 团队第一版的严格边界
|
||||
|
||||
团队第一版只展示 `GLS-SYS-ARCH-001` 公开系统架构。以下内容不得进入团队首版可见界面:
|
||||
|
||||
- 第五域个人频道结构;
|
||||
- 永恒湖心、心跳核心、光之湖人格体路径;
|
||||
- 冰朔短剧视频、服务器登记和个人模块;
|
||||
- 未重塑页面关系的 Notion 导出内容。
|
||||
|
||||
这不是删除 Tolaria 基座能力,而是冻结团队首版的产品入口,避免团队在错误页面关系上继续开发。
|
||||
|
||||
## 3. 真实源码入口
|
||||
|
||||
> 2026-07-26 安全收束:本仓当前物理源登记为冰朔个人 `HLP-CHANNEL-0001`,
|
||||
> 不再作为团队部署源。团队页面可继续本地开发和测试,但团队安装包必须等
|
||||
> `AW-GZ-001` 企业研发仓取得正式仓库编号与频道编号后,从企业仓生成。
|
||||
> 公共复用模块另入公共模块仓;读取本仓不构成部署授权。
|
||||
|
||||
- 团队 GLS 页面:`src/TeamFoundationApp.tsx`
|
||||
- 团队独立渲染入口:`src/team-foundation-main.tsx`
|
||||
- 团队独立 HTML:`team-foundation.html`
|
||||
- 团队 Tauri 身份:`src-tauri/tauri.team.conf.json`
|
||||
- GLS 唯一内置数据:`src-tauri/resources/public-architecture/gls-system-architecture.json`
|
||||
- 团队构建收尾:`scripts/finalize-team-foundation-build.mjs`
|
||||
- Mac 打包:`scripts/build-macos-internal.sh`
|
||||
- Windows CI 工作流:`.github/workflows/build-windows-manual.yml`
|
||||
- Windows 本地/节点备用脚本:`scripts/build-windows-jd-cross.sh`
|
||||
- 包内容硬门:`scripts/verify-internal-package-content.mjs`
|
||||
- 自动测试:`src/TeamFoundationApp.test.tsx`
|
||||
|
||||
团队包使用独立 Bundle ID:`com.guanghulab.hololake.team-foundation`,可以与冰朔个人内测版并存。
|
||||
|
||||
## 4. Notion 原型素材源
|
||||
|
||||
冰朔桌面上的全量导出:
|
||||
|
||||
`/Users/bingshuolingdianyuanhe/Desktop/notion.zip`
|
||||
|
||||
2026-07-18 只读核验结果:外层约 497 MB,内部为两个 Notion 分卷压缩包。该文件是企业四域原型素材,不是可直接部署的页面树,也不进入团队安装包。
|
||||
|
||||
后续重塑必须先输出“页面关系映射表”,至少区分:原页面 ID、标题、父页面、目标域、目标频道、可见性、事实源、废弃/合并关系。未完成映射前不得批量导入仓库。
|
||||
|
||||
## 5. 已完成验证与产物
|
||||
|
||||
- 团队页面与冰朔频道测试合计 9 项通过;
|
||||
- `npm run build:team-foundation` 通过,只编译团队独立入口;
|
||||
- 团队 dist 私人内容扫描通过;
|
||||
- GLS 与公开研发仓库内容门通过;
|
||||
- Mac 应用 ad-hoc 签名验证通过;
|
||||
- Mac DMG 校验通过,桌面复制后二次 SHA-256 通过。
|
||||
|
||||
Mac 团队基座包:
|
||||
|
||||
`/Users/bingshuolingdianyuanhe/Desktop/HoloLake-Era-0.1.7-Team-Foundation-Mac-aarch64.dmg`
|
||||
|
||||
外接盘归档:
|
||||
|
||||
`/Volumes/JZAO/HoloLake/artifacts/team-foundation/HoloLake-Era-0.1.7-Team-Foundation-Mac-aarch64.dmg`
|
||||
|
||||
## 6. Windows CI 与未闭环项
|
||||
|
||||
Windows 安装包不在 Mac 上伪造。当前工作流已改为在 `ubuntu-latest` 上使用 `cargo-xwin + NSIS` 交叉构建,不依赖未配置完成的 Windows 自托管 runner;推送工作流文件到 `main` 会自动触发,也可手动触发:
|
||||
|
||||
```text
|
||||
.github/workflows/build-windows-manual.yml
|
||||
version = 0.1.7
|
||||
artifact_name = HoloLake-Era-Team-Foundation-Windows-x64
|
||||
```
|
||||
|
||||
输出目标名:`HoloLake-Era-0.1.7-Team-Foundation-Windows-x64-setup.exe`。
|
||||
|
||||
该工作流固定通过 `src-tauri/tauri.team.conf.json` 构建 GLS 团队入口,并对 NSIS 文件做 PE32 格式检查与 SHA-256 输出。CI 成功只能证明 NSIS 产物生成;能否安装、启动和显示正确 GLS 页面仍须 Windows 实机验收。当前新版工作流尚未在远端 runner 上跑出本次团队包,不得写成双平台已经交付。
|
||||
|
||||
2026-07-19 在新加坡大脑 Linux 服务器上直接运行
|
||||
`scripts/build-windows-jd-cross.sh`,已经由 `cargo-xwin` 成功生成 Windows MSVC
|
||||
二进制 `release/hololake.exe`,但脚本仍检查历史文件名 `release/tolaria.exe`,因此
|
||||
在 NSIS 封装前退出。准确日志、节点路径和续接标准见
|
||||
`docs/HANDOFF-2026-07-19-IOS-WINDOWS.md`。当前不得写成 Windows 安装包已生成。
|
||||
|
||||
2026-07-18 功能提交为 `a6d83b0`,Windows CI 与本文修正提交为 `ab283b5`。第一次推送完整执行了前端测试、Rust lint、1074 项 Rust 测试与覆盖率;Playwright 冒烟测试因本机缺少对应 headless-shell 运行器未执行成功。远端曾因 `repo_push_grant_expired` 拒绝;冰朔完成新邮件工单批准后,已按导航地图读取、确认和仓库授权路径成功推送至远端 `main`。2026-07-19 的 iOS/TestFlight 与 Windows 工作流提交推送后会自动触发新版 Windows CI;必须以远端 job 与产物页面为准,并在 Windows 实机验证安装和启动。
|
||||
|
||||
## 7. 外接盘构建约束
|
||||
|
||||
JZAO 原文件系统会产生 `._` AppleDouble 文件,Rust/Tauri 会把它们误读为配置。Mac 原生缓存因此放在 JZAO 上的 APFS 稀疏映像:
|
||||
|
||||
- 映像:`/Volumes/JZAO/HoloLake/HoloLakeBuild.sparsebundle`
|
||||
- 挂载点:`/Volumes/HoloLakeBuild`
|
||||
- Cargo 缓存:`/Volumes/HoloLakeBuild/target/team`
|
||||
|
||||
iOS 26.5 模拟器导出包也保存在 JZAO:
|
||||
|
||||
`/Volumes/JZAO/HoloLake/Xcode-Platforms/iphonesimulator_26.5_23F77.exportedBundle`
|
||||
|
||||
Xcode 下载后曾自动在内置盘注册一份;已用 `simctl runtime delete` 精确移除,当前内置运行时列表为空。需要模拟器时必须重新讨论导入后的内置盘占用。
|
||||
|
||||
## 8. 下一实例最短读取顺序
|
||||
|
||||
1. 本文 `HLP-DOC-TEAM-0001`;
|
||||
2. `docs/HANDOFF-2026-07-19-IOS-WINDOWS.md`;
|
||||
3. `docs/IOS-TESTFLIGHT.md`;
|
||||
4. `docs/HOLOLAKE-LANGUAGE-WORLD-SYSTEM.md`;
|
||||
5. `docs/skills/internal-release-packaging/SKILL.md`;
|
||||
6. 上述真实源码入口与测试。
|
||||
|
||||
不得从 Notion 导出包猜页面关系,不得把团队 GLS 基座说成企业四域已经开发完成,也不得把 Mac 包成功说成 Windows/TestFlight 已完成。
|
||||
195
product-source/hololake-platform/docs/VISION.md
Normal file
195
product-source/hololake-platform/docs/VISION.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Tolaria — Product Vision
|
||||
|
||||
*Written by Brian based on conversations with Luca Rossi, Feb–Mar 2026.*
|
||||
*This is a living document — update it as the vision evolves.*
|
||||
|
||||
---
|
||||
|
||||
## Why this, why now, why us
|
||||
|
||||
Before the what and how: the why.
|
||||
|
||||
The best projects are built by people who have an unusually strong answer to "why are you the right person to build this?" This is that answer.
|
||||
|
||||
**Luca Rossi** is a startup founder and former generalist CTO — someone who can build a product end-to-end across code, design, scope, and product. And for the last five years, full-time, he has run Refactoring: a technical newsletter with nearly 200,000 subscribers, for which he has written over 300 original articles. In word count, that's roughly two *Lord of the Rings* novels.
|
||||
|
||||
Personal knowledge management has been an obsession since university. But over the last five years it stopped being a hobby and became *table stakes* — the system that makes writing 300 articles possible. Tolaria is an attempt to bottle that system.
|
||||
|
||||
The credibility is real: if you wonder whether this person knows how to organize knowledge for sustained output, the output speaks for itself. The method inside Tolaria is not theorized — it's been battle-tested for years at scale.
|
||||
|
||||
**The distribution is built in.** Refactoring reaches ~200,000 engineers, managers, and technical leaders — exactly the people most receptive to a tool like this. The audience already trusts the author on this topic, because they've been reading his writing about knowledge management and learning for years.
|
||||
|
||||
This is not a product looking for a market. It's a tool built by its first power user, for an audience that already knows and trusts him.
|
||||
|
||||
**Why Tolaria, in the context of Refactoring.**
|
||||
|
||||
Refactoring is a newsletter about how software is built, how teams work, and how digital products are developed — written from Luca's experience and conversations with other tech leaders. A natural question follows: what is the author's own current experience building software with AI?
|
||||
|
||||
Tolaria answers that question directly and publicly. If it works — if it becomes a real product used by real people — it validates the author's capabilities and authority to write about these topics. Not as theory, but as demonstrated practice. Anyone can look at the GitHub repository, see 100 commits a day, and verify: this person actually does this.
|
||||
|
||||
This is why Tolaria is **free and open source**: success becomes a reputation and acquisition channel for Refactoring. The attention and trust earned through a well-executed open source project converts — through sponsorships, paid subscriptions, and brand authority — into the business that Refactoring runs on.
|
||||
|
||||
The strategy is coherent: build the tool you describe, make the work visible, let the product speak for the author.
|
||||
|
||||
---
|
||||
|
||||
## The problem
|
||||
|
||||
Most people who want to work effectively with AI face a version of the same problem: **they don't have their knowledge organized in a way that AI can actually use.**
|
||||
|
||||
They have notes scattered across Notion, Apple Notes, browser bookmarks, and email. Some of it is structured, most of it isn't. Even the people who do maintain a knowledge base discover that AI tools — ChatGPT, Notion AI, others — struggle to navigate it meaningfully. The knowledge is there, but it's not *accessible*.
|
||||
|
||||
The problem has two distinct layers:
|
||||
|
||||
1. **Architectural**: most knowledge tools store data in proprietary formats on remote servers. AI tools can't read them efficiently, can't commit changes back, can't reason over the full structure. The format itself creates a ceiling.
|
||||
|
||||
2. **Methodological**: even with the right tool, most people don't know *how* to organize knowledge so it becomes useful over time — what to capture, how to connect things, how to turn raw notes into a system that works with you instead of against you.
|
||||
|
||||
Tolaria addresses both layers, together. That's what makes it different.
|
||||
|
||||
---
|
||||
|
||||
## The insight: tool and method, together
|
||||
|
||||
Most PKM tools give you a blank canvas and leave the rest to you. They solve the first problem (somewhere to put things) but not the second (how to organize them). The result is that sophisticated users build complex custom systems, while everyone else gives up.
|
||||
|
||||
Tolaria's position is different: **we ship the method alongside the tool.**
|
||||
|
||||
The method is opinionated but not rigid. It tells you: here's how to think about your work, here's where different kinds of notes belong, here's how to connect them. If it fits your needs — great, start immediately. If your situation is different — customize it. The types, the relationships, the structure can all be changed. But you don't have to figure it out from scratch.
|
||||
|
||||
This combination — an opinionated method on top of a technically excellent foundation — is what makes Tolaria genuinely useful to people who are stuck, not just people who already know what they're doing.
|
||||
|
||||
---
|
||||
|
||||
## The method: a framework for knowledge work
|
||||
|
||||
### The knowledge ontology
|
||||
|
||||
Tolaria organizes work around two axes:
|
||||
|
||||
| | **One-time** | **Recurring** |
|
||||
|---|---|---|
|
||||
| **Multi-session** | **Project** (has a start and end) | **Responsibility** (no end, measured by KPIs) |
|
||||
| **Single-session** | *Task* (lives in a task manager) | **Procedure** (checklist, routine) |
|
||||
|
||||
Everything else is context:
|
||||
- **Notes** — the atomic unit. Any note connects to one or more of the above.
|
||||
- **Topics** — areas of interest with no performance expectation. A knowledge repository.
|
||||
- **Events** — things that happened, anchored to a date.
|
||||
- **People** — contacts and their history.
|
||||
|
||||
Relations between notes are first-class citizens — not just wiki-links, but typed, bidirectional connections that make the knowledge graph navigable.
|
||||
|
||||
This ontology is not arbitrary. It maps cleanly to how both individuals and organizations actually structure their work: companies have projects, responsibilities, procedures, and people. So do independent creators. So do individuals managing their lives.
|
||||
|
||||
### Knowledge has a purpose
|
||||
|
||||
A principle that underlies everything in Tolaria: **notes exist to get things done.** Not to be stored for some abstract future use. Not to show how organized you are. To do something.
|
||||
|
||||
This is the difference between a knowledge system that works over years and one that collapses after a few weeks. Without a real purpose, the maintenance cost of taking notes is never justified, and people stop. With a purpose — writing regularly, building things, making decisions — the system pays for itself.
|
||||
|
||||
What you *do* with organized knowledge depends on who you are:
|
||||
|
||||
- **Writers and content creators** — the output is articles, essays, posts. Captures become highlights, highlights become **evergreen notes** (small, atomic, timeless ideas), evergreen notes become building blocks for articles. Evergreen notes are a middle layer: not the raw input, not the final output, but the refined reusable units that make writing easier and faster.
|
||||
- **Builders and project-driven people** — the output is shipped work. Captures feed projects, decisions, and procedures. Evergreen notes matter less; the project knowledge graph matters more.
|
||||
- **Operators and managers** — the output is better systems and decisions. Captures feed responsibilities (KPIs, workflows) and procedures (how we do things). The value accumulates in the recurring structure, not in individual notes.
|
||||
|
||||
The framework is flexible enough to fit all three — and most people are a mix. What stays constant is the flow: **capture → organize → express**. The *what* of expressing changes; the discipline doesn't.
|
||||
|
||||
### The two-phase workflow: capture and organize
|
||||
|
||||
Notes move through two distinct phases, and the transition between them is intentional.
|
||||
|
||||
**Capture** — fast, frictionless, available everywhere. A thought, a saved article, a Kindle highlight, a voice memo. The cardinal rule: never let friction during capture cause a good idea to be lost. Captured notes land in the vault unconnected — no relationships, no organization. That's fine. That's the point.
|
||||
|
||||
**Organize** — a deliberate, periodic activity (weekly is the natural cadence). You ask: *what is this useful for?* Many things that seemed important when captured won't survive this question — deleting >50% of captures is normal and healthy. For the things that survive: connect them. Link to a Project, a Responsibility, a Topic. Every note should eventually connect to at least one actionable container. If you can't connect something to anything, that's a signal worth paying attention to.
|
||||
|
||||
**The Inbox** is the UI expression of this split: a smart section that shows all unorganized notes — those with no outgoing relationships. The goal is Inbox Zero, reached periodically (weekly). The inbox is not a folder; it's a derived state. Connecting a note to something removes it automatically.
|
||||
|
||||
### Convention over configuration
|
||||
|
||||
The method lives in the app as *conventions*: standard field names and folder structures that have well-defined meanings and trigger specific behavior.
|
||||
|
||||
`status:` shows a colored chip. `Workspace: [[workspace/refactoring]]` assigns a note to a context. `Belongs to:` connects it to its parent. `start_date:` and `end_date:` show a duration badge. The app recognizes these by convention, without any setup.
|
||||
|
||||
Users who want more can override the defaults: `config/relations.md` changes which relationship fields appear by default; `config/semantic-properties.md` controls how fields are rendered. But the defaults work immediately, for everyone.
|
||||
|
||||
This is convention *over* configuration — not convention *instead of* it.
|
||||
|
||||
---
|
||||
|
||||
## The foundation: architecture that earns trust
|
||||
|
||||
The method is only as good as the system it runs on. Tolaria's architecture is built around a single principle: **your knowledge is yours, permanently and unconditionally.**
|
||||
|
||||
### Local files, version-controlled with Git
|
||||
|
||||
Every note is a plain Markdown file on your disk. There is no database, no proprietary format, no sync lock-in. The files are readable by any tool that can open a text file — today and in twenty years.
|
||||
|
||||
Git provides version control: every change is tracked, diffable, reversible. You have a full audit trail of what changed, when, and why. Collaboration happens via Git — the same way software teams have collaborated for decades, without any proprietary cloud in between.
|
||||
|
||||
### AI-native by design
|
||||
|
||||
A vault of plain Markdown files, version-controlled with Git, is dramatically more AI-friendly than any SaaS-based system.
|
||||
|
||||
An AI agent working on a local vault can read thousands of notes in seconds, understand their structure, write new ones, connect existing ones, and commit the changes back — all with full comprehension. Notion's AI can't do this. No SaaS-based AI can do this, because the architecture doesn't allow it.
|
||||
|
||||
More importantly: the more a vault follows Tolaria's conventions, the *less configuration an AI needs* to navigate it. Shared conventions make knowledge legible to both humans and AI without bespoke instructions for every setup. The method and the AI-native architecture reinforce each other.
|
||||
|
||||
### Open and exit-friendly
|
||||
|
||||
The trust between Tolaria and the user is earned daily, not enforced by format. If something better comes along, you take your Markdown files and leave. The exit door is always open.
|
||||
|
||||
---
|
||||
|
||||
## Why not Obsidian?
|
||||
|
||||
Obsidian is the obvious comparison. The difference is philosophy:
|
||||
|
||||
- **Obsidian** is a blank canvas. Infinitely configurable via plugins and community extensions. Powerful for users who want to build their own system — and who have the time and patience to do so.
|
||||
- **Tolaria** is opinionated. It ships with a complete point of view: a knowledge framework, semantic conventions, and defaults that work immediately. No plugin hunting. No configuration required to get started.
|
||||
|
||||
Obsidian also treats Git as an afterthought — its business model is built around proprietary sync. In Tolaria, Git is a first-class citizen: the natural, obvious way to sync, collaborate, and maintain history.
|
||||
|
||||
---
|
||||
|
||||
## Who it's for, and where it's going
|
||||
|
||||
### Three stages of adoption
|
||||
|
||||
Tolaria is designed to grow through three natural stages — not pivots, but extensions of the same foundation:
|
||||
|
||||
**Stage 1: Personal PKM + AI context** *(current)*
|
||||
A single person manages their knowledge, life, and work in a local vault. The primary collaborator is AI. The vault gives structure to one person's context, making it legible to an AI that can assist meaningfully across all areas of work and life. The method helps structure the knowledge; the AI helps use it.
|
||||
|
||||
**Stage 2: Independent knowledge workers**
|
||||
Content creators, freelancers, consultants. People with maximum incentive *and* maximum agency to build a real system. They have projects, clients, responsibilities — and they work alone or in very small teams. The same ontology applies: a newsletter creator has editorial projects, a subscriber-growth responsibility, and a publishing procedure. AI collaboration deepens: the AI can see not just personal notes but client commitments, content pipelines, recurring workflows.
|
||||
|
||||
**Stage 3: Small teams**
|
||||
The ontology scales to organizations. Companies have projects, responsibilities, procedures, and people — the same categories, at a larger scale. The access model changes: different people see different subsets of the vault, via workspace filtering and Git-based access control. Version history gives teams a full audit trail. AI agents become shared collaborators on team knowledge, not just personal assistants.
|
||||
|
||||
**What makes this trajectory coherent:** the foundational model — local files, Git-versioned, structured by conventions — doesn't need to be rebuilt at each stage. It extends naturally.
|
||||
|
||||
### The right early adopters
|
||||
|
||||
The first users who will get the most from Tolaria are technically-minded individuals who:
|
||||
- Are frustrated with Notion's performance, complexity, or lock-in
|
||||
- Understand or are comfortable with Git
|
||||
- Want a system that's AI-native by design, not by bolted-on features
|
||||
- Value owning their data
|
||||
|
||||
Broader audiences will follow as the onboarding experience matures and the conventions become easier to adopt.
|
||||
|
||||
---
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Opinionated but not rigid** — ship the method and the defaults; allow customization where it matters
|
||||
2. **Convention over configuration** — standard field names trigger rich behavior automatically; users can override via vault config files
|
||||
3. **Git-first** — sync, history, collaboration, and audit trail via Git; no proprietary cloud
|
||||
4. **AI-native architecture** — local files, open formats, structured by conventions legible to both humans and AI
|
||||
5. **Zero lock-in** — earn trust daily; the exit door is always open
|
||||
6. **Capture and organize are separate** — the inbox makes unorganized notes visible; Inbox Zero is the discipline
|
||||
7. **Relations as first-class citizens** — connections between notes are as important as the notes themselves
|
||||
8. **Filesystem as the single source of truth** — the app never owns the data; cache and UI state are always derived and reconstructible
|
||||
9. **Convention over system config files** — app configuration and preferences that belong to a note (e.g. type-level UI preferences) are stored in that note's frontmatter using the `_field` underscore convention, not in separate config files or localStorage. Everything that matters lives in the vault as plain text.
|
||||
|
|
@ -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.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue