2026-02-17 13:10:43 +01:00
# Abstractions
Key abstractions and domain models in Laputa.
2026-03-08 19:58:28 +01:00
## Design Philosophy
Laputa's abstractions follow the **convention over configuration ** principle: standard field names and folder structures have well-defined meanings and trigger UI behavior automatically. This makes vaults legible both to humans and to AI agents — the more a vault follows conventions, the less custom configuration an AI needs to navigate it correctly.
The full set of design principles is documented in [ARCHITECTURE.md ](./ARCHITECTURE.md#design-principles ).
## Semantic Field Names (conventions)
These frontmatter field names have special meaning in Laputa's UI:
| Field | Meaning | UI behavior |
|---|---|---|
2026-03-28 11:03:01 +01:00
| `title:` | Human-readable title (synced with filename) | Breadcrumb, sidebar. Filename = `slugify(title).md` |
2026-03-08 19:58:28 +01:00
| `type:` | Entity type (Project, Person, Quarter…) | Type chip in note list + sidebar grouping |
| `status:` | Lifecycle stage (active, done, blocked…) | Colored chip in note list + editor header |
| `url:` | External link | Clickable link chip in editor header |
| `date:` | Single date | Formatted date badge |
| `start_date:` + `end_date:` | Duration/timespan | Date range badge |
| `goal:` + `result:` | Progress | Progress indicator in editor header |
| `Workspace:` | Vault context filter | Global workspace filter |
| `Belongs to:` | Parent relationship | Relationship chip in Properties panel |
| `Related to:` | Lateral relationship | Relationship chip in Properties panel |
2026-03-28 11:03:01 +01:00
Relationship fields are detected dynamically — any frontmatter field containing `[[wikilink]]` values is treated as a relationship (see [ADR-0010 ](adr/0010-dynamic-wikilink-relationship-detection.md )).
2026-03-08 19:58:28 +01:00
2026-03-24 16:42:45 +01:00
### System Properties (underscore convention)
Any frontmatter field whose name starts with `_` is a **system property ** :
- It is **not shown ** in the Properties panel (neither for notes nor for Type notes)
- It is **not exposed ** as a user-visible property in search, filters, or the UI
- It **is editable ** directly in the raw editor (power users can access it if needed)
- It is used by Laputa internally for configuration, behavior, and UI preferences
Examples:
```yaml
_pinned_properties: # which properties appear in the editor inline bar (per-type)
2026-03-27 16:20:31 +01:00
- key: status
icon: circle-dot
2026-03-24 16:42:45 +01:00
_icon: shapes # icon assigned to a type
_color: blue # color assigned to a type
_order: 10 # sort order in the sidebar
_sidebar_label: Projects # override label in sidebar
```
**This convention is universal** — apply it to all future system-level frontmatter fields. When a new feature needs to store configuration in a note's frontmatter (especially in Type notes), use `_field_name` to keep it hidden from normal user-facing surfaces while still stored on-disk as plain text.
The frontmatter parser (Rust: `vault/mod.rs` , TS: `utils/frontmatter.ts` ) must filter out `_*` fields before passing `properties` to the UI.
2026-02-17 13:10:43 +01:00
## Document Model
All data lives in markdown files with YAML frontmatter. There is no database — the filesystem is the source of truth.
### VaultEntry
2026-03-13 19:12:20 +01:00
The core data type representing a single note, defined in Rust (`src-tauri/src/vault/mod.rs` ) and TypeScript (`src/types.ts` ).
```mermaid
classDiagram
class VaultEntry {
+String path
+String filename
+String title
+String? isA
+String[] aliases
+String[] belongsTo
+String[] relatedTo
+Record~string,string[]~ relationships
+String[] outgoingLinks
+String? status
+Number? modifiedAt
+Number? createdAt
+Number wordCount
+String? snippet
+Boolean archived
+Boolean trashed
+Number? trashedAt
+Record~string,string~ properties
}
class TypeDocument {
+String icon
+String color
+Number order
+String sidebarLabel
+String template
+String sort
+Boolean visible
}
class Frontmatter {
+String type
+String status
+String url
+String[] belongsTo
+String[] relatedTo
+String[] aliases
...custom fields
}
VaultEntry --> Frontmatter : parsed from
VaultEntry --> TypeDocument : isA resolves to
VaultEntry "many" --> "1" TypeDocument : grouped by type
```
2026-02-17 13:10:43 +01:00
```typescript
// src/types.ts
interface VaultEntry {
2026-03-07 04:05:40 +01:00
path: string // Absolute file path
filename: string // Just the filename
title: string // From first # heading, or filename fallback
2026-03-08 18:00:56 +01:00
isA: string | null // Entity type: Project, Procedure, Person, etc. (from frontmatter `type:` field)
2026-03-07 04:05:40 +01:00
aliases: string[] // Alternative names for wikilink resolution
belongsTo: string[] // Parent relationships (wikilinks)
relatedTo: string[] // Related entity links (wikilinks)
relationships: Record<string, string[]> // All frontmatter fields containing wikilinks
outgoingLinks: string[] // All [[wikilinks]] found in note body
status: string | null // Active, Done, Paused, Archived, Dropped
2026-02-17 13:10:43 +01:00
modifiedAt: number | null // Unix timestamp (seconds)
2026-03-28 11:03:01 +01:00
// Note: owner and cadence are now in the generic `properties` map
2026-02-17 13:10:43 +01:00
createdAt: number | null // Unix timestamp (seconds)
fileSize: number
2026-03-07 04:05:40 +01:00
wordCount: number | null // Body word count (excludes frontmatter)
snippet: string | null // First 200 chars of body
archived: boolean // Archived flag
trashed: boolean // Trashed flag
trashedAt: number | null // When trashed (for auto-purge)
properties: Record<string, string> // Scalar frontmatter fields (custom properties)
2026-02-17 13:10:43 +01:00
}
```
2026-03-08 18:00:56 +01:00
### Entity Types (isA / type)
2026-02-17 13:10:43 +01:00
2026-03-08 18:00:56 +01:00
Entity type is stored in the `type:` frontmatter field (e.g. `type: Quarter` ). The legacy field name `Is A:` is still accepted as an alias for backwards compatibility but new notes use `type:` . The `VaultEntry.isA` property in TypeScript/Rust holds the resolved value.
2026-03-15 23:40:47 +01:00
Type is determined **purely ** from the `type:` frontmatter field — it is never inferred from the file's folder location. All notes live at the vault root as flat `.md` files:
2026-02-17 13:10:43 +01:00
```
~/Laputa/
2026-03-15 23:40:47 +01:00
├── my-project.md ← type: Project (in frontmatter)
├── weekly-review.md ← type: Procedure
├── john-doe.md ← type: Person
├── some-topic.md ← type: Topic
├── ...
├── type/ ← type definition documents
2026-03-28 11:03:01 +01:00
└── config/ ← meta-configuration files (agents.md, etc.)
2026-02-17 13:10:43 +01:00
```
2026-03-15 23:40:47 +01:00
New notes are created at the vault root: `{vault}/{slug}.md` . Changing a note's type only requires updating the `type:` field in frontmatter — the file does not move. The `type/` folder exists solely for type definition documents, and `config/` for configuration files.
A `flatten_vault` migration command is available to move existing notes from type-based subfolders to the vault root.
2026-02-17 13:10:43 +01:00
2026-02-20 21:55:25 +01:00
### Types as Files
Each entity type can have a corresponding **type document ** in the `type/` folder (e.g., `type/project.md` , `type/person.md` ). Type documents:
2026-03-08 18:00:56 +01:00
- Have `type: Type` in their frontmatter (`Is A: Type` also accepted as legacy alias)
2026-03-07 04:05:40 +01:00
- Define type metadata: icon, color, order, sidebar label, template, sort, view, visibility
- Are navigable entities — they appear in the sidebar under "Types" and can be opened/edited like any note
2026-02-20 21:55:25 +01:00
- Serve as the "definition" for their type category
2026-03-07 04:05:40 +01:00
**Type document properties** (read by Rust and used in the UI):
| Property | Type | Description |
|----------|------|-------------|
| `icon` | string | Phosphor icon name (kebab-case, e.g., "cooking-pot") |
| `color` | string | Accent color: red, purple, blue, green, yellow, orange |
| `order` | number | Sidebar display order (lower = higher priority) |
| `sidebar_label` | string | Custom label overriding auto-pluralization |
| `template` | string | Markdown template for new notes of this type |
| `sort` | string | Default sort: "modified:desc", "title:asc", "property:Priority:asc" |
| `view` | string | Default view mode: "all", "editor-list", "editor-only" |
| `visible` | bool | Whether type appears in sidebar (default: true) |
2026-02-20 21:55:25 +01:00
**Type relationship**: When any entry has an `isA` value (e.g., "Project"), the Rust backend automatically adds a `"Type"` entry to its `relationships` map pointing to `[[type/project]]` . This makes the type navigable from the Inspector panel.
**UI behavior**:
2026-03-07 04:05:40 +01:00
- Clicking a section group header pins the type document at the top of the NoteList if it exists
2026-02-20 21:55:25 +01:00
- Viewing a type document in entity view shows an "Instances" group listing all entries of that type
2026-03-07 04:05:40 +01:00
- The Type field in the Inspector is rendered as a clickable chip that navigates to the type document
2026-02-20 21:55:25 +01:00
2026-02-17 13:10:43 +01:00
### Frontmatter Format
Standard YAML frontmatter between `---` delimiters:
```yaml
---
title: Write Weekly Essays
2026-03-28 11:03:01 +01:00
type: Procedure
2026-02-17 13:10:43 +01:00
status: Active
belongs_to:
2026-03-15 23:40:47 +01:00
- "[[grow-newsletter]]"
2026-02-17 13:10:43 +01:00
related_to:
2026-03-15 23:40:47 +01:00
- "[[writing]]"
2026-02-17 13:10:43 +01:00
aliases:
- Weekly Writing
---
```
2026-03-07 04:05:40 +01:00
Supported value types (defined in `src-tauri/src/frontmatter/yaml.rs` as `FrontmatterValue` ):
2026-02-17 13:10:43 +01:00
- **String**: `status: Active`
- **Number**: `priority: 5`
- **Bool**: `archived: true`
- **List**: Multi-line ` - item` or inline `[item1, item2]`
- **Null**: `owner:` (empty value)
2026-03-07 04:05:40 +01:00
### Custom Relationships
The Rust parser scans all frontmatter keys for fields containing `[[wikilinks]]` . Any non-standard field with wikilink values is captured in the `relationships` HashMap:
```yaml
---
Topics:
2026-03-15 23:40:47 +01:00
- "[[writing]]"
- "[[productivity]]"
2026-03-07 04:05:40 +01:00
Key People:
2026-03-15 23:40:47 +01:00
- "[[matteo-cellini]]"
2026-03-07 04:05:40 +01:00
---
```
2026-03-15 23:40:47 +01:00
Becomes: `relationships["Topics"] = ["[[writing]]", "[[productivity]]"]`
2026-03-07 04:05:40 +01:00
This enables arbitrary, extensible relationship types without code changes.
### Outgoing Links
All `[[wikilinks]]` in the note body (not frontmatter) are extracted by regex and stored in `outgoingLinks` . Used for backlink detection and relationship graphs.
2026-03-17 16:59:04 +01:00
### Title / Filename Sync
2026-02-17 13:10:43 +01:00
2026-03-17 16:59:04 +01:00
Every note has a `title` field in frontmatter that stores the human-readable title. The filename is always the slug of the title (`slugify(title).md` ). The two are kept in sync:
- **Source of truth**: filename (on open), user input (on rename inside Laputa)
- **`extract_title` ** reads `title` from frontmatter; falls back to deriving a title from the filename via `slug_to_title()` (hyphens → spaces, title-case). Never reads from H1. Logic in `vault/parsing.rs` .
- **On note open** (`sync_title_on_open` ): if `title` frontmatter is absent or desynced from the filename, it is auto-corrected (filename wins). Logic in `vault/title_sync.rs` .
- **On rename** (`rename_note` ): updates both `title` frontmatter and filename atomically, plus wikilinks across the vault. Always writes `title` to frontmatter.
2026-02-17 13:10:43 +01:00
2026-03-16 04:50:51 +01:00
### Title Field (UI)
The editor displays a dedicated `TitleField` component above the BlockNote editor. This is the primary title editing surface — the H1 block inside BlockNote is hidden via CSS. Changing the title field triggers `onTitleSync` , which updates the frontmatter `title:` field and renames the file to match `slugify(title).md` . The title field also responds to `laputa:focus-editor` events with `selectTitle: true` for new note creation.
2026-02-17 13:10:43 +01:00
### Sidebar Selection
Navigation state is modeled as a discriminated union:
```typescript
2026-03-07 04:05:40 +01:00
type SidebarFilter = 'all' | 'archived' | 'trash' | 'changes' | 'pulse'
2026-02-17 13:10:43 +01:00
type SidebarSelection =
2026-03-07 04:05:40 +01:00
| { kind: 'filter'; filter: SidebarFilter }
2026-02-17 13:10:43 +01:00
| { kind: 'sectionGroup'; type: string } // e.g. type: 'Project'
| { kind: 'entity'; entry: VaultEntry } // specific entity selected
| { kind: 'topic'; entry: VaultEntry } // topic selected
```
## File System Integration
### Vault Scanning (Rust)
2026-03-07 04:05:40 +01:00
`vault::scan_vault(path)` in `src-tauri/src/vault/mod.rs` :
2026-02-17 13:10:43 +01:00
1. Validates the path exists and is a directory
2026-03-16 04:50:51 +01:00
2. Scans root-level `.md` files (non-recursive)
2026-03-28 11:03:01 +01:00
3. Recursively scans protected folders: `type/` , `config/` , `attachments/`
2026-03-16 04:50:51 +01:00
4. Files in non-protected subfolders are **not indexed ** (flat vault enforcement)
5. For each `.md` file, calls `parse_md_file()` :
2026-03-07 04:05:40 +01:00
- Reads content with `fs::read_to_string()`
2026-02-17 13:10:43 +01:00
- Parses frontmatter with `gray_matter::Matter::<YAML>`
- Extracts title from first `#` heading
2026-03-15 23:40:47 +01:00
- Reads entity type from `type:` frontmatter field (`Is A:` accepted as legacy alias); type is never inferred from folder
2026-03-07 04:05:40 +01:00
- Parses dates as ISO 8601 to Unix timestamps
- Extracts relationships, outgoing links, custom properties, word count, snippet
2026-03-16 04:50:51 +01:00
6. Sorts by `modified_at` descending
7. Skips unparseable files with a warning log
A `vault_health_check` command detects stray files in non-protected subfolders and filename-title mismatches. On vault load, a migration banner offers to flatten stray files to the root via `flatten_vault` .
2026-02-17 13:10:43 +01:00
2026-03-07 04:05:40 +01:00
### Vault Caching
`vault::scan_vault_cached(path)` wraps scanning with git-based caching:
2026-03-08 22:58:00 +01:00
1. Reads cache from `~/.laputa/cache/<vault-hash>.json` (external to vault)
2026-03-07 04:05:40 +01:00
2. Compares cache version, vault path, and git HEAD commit hash
3. If cache is valid and same commit → only re-parse uncommitted changed files
4. If different commit → use `git diff` to find changed files → selective re-parse
5. If no cache → full scan
2026-03-08 22:58:00 +01:00
6. Writes updated cache atomically (write to `.tmp` , then rename)
7. On first run, migrates any legacy `.laputa-cache.json` from inside the vault
2026-03-07 04:05:40 +01:00
2026-02-17 13:10:43 +01:00
### Frontmatter Manipulation (Rust)
2026-03-07 04:05:40 +01:00
`frontmatter/ops.rs:update_frontmatter_content()` performs line-by-line YAML editing:
2026-02-17 13:10:43 +01:00
1. Finds the frontmatter block between `---` delimiters
2026-03-07 04:05:40 +01:00
2. Iterates through lines looking for the target key
2026-02-17 13:10:43 +01:00
3. If found: replaces the value (consuming multi-line list items if present)
2026-03-07 04:05:40 +01:00
4. If not found: appends the new key-value at the end
5. If no frontmatter exists: creates a new `---` block
2026-02-17 13:10:43 +01:00
The `with_frontmatter()` helper wraps this in a read-transform-write cycle on the actual file.
### Content Loading
2026-03-07 04:05:40 +01:00
- **Tauri mode**: Content loaded on-demand when a tab is opened via `invoke('get_note_content', { path })`
- **Browser mode**: All content loaded at startup from mock data
2026-02-17 13:10:43 +01:00
- Content for backlink detection (`allContent` ) is stored in memory as `Record<string, string>`
## Git Integration
2026-03-07 04:05:40 +01:00
Git operations live in `src-tauri/src/git/` . All operations shell out to the `git` CLI (not libgit2).
2026-02-17 13:10:43 +01:00
### Data Types
```typescript
interface GitCommit {
2026-03-07 04:05:40 +01:00
hash: string
shortHash: string
2026-02-17 13:10:43 +01:00
message: string
author: string
date: number // Unix timestamp
}
interface ModifiedFile {
path: string // Absolute path
relativePath: string // Relative to vault root
status: 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed'
}
2026-03-07 04:05:40 +01:00
interface PulseCommit {
hash: string
shortHash: string
message: string
date: number
githubUrl: string | null
files: PulseFile[]
added: number
modified: number
deleted: number
}
2026-02-17 13:10:43 +01:00
```
### Operations
2026-03-07 04:05:40 +01:00
| Module | Operation | Notes |
|--------|-----------|-------|
| `history.rs` | File history | `git log` — last 20 commits per file |
| `status.rs` | Modified files | `git status --porcelain` — filtered to `.md` |
| `status.rs` | File diff | `git diff` , fallback to `--cached` , then synthetic for untracked |
| `commit.rs` | Commit | `git add -A && git commit -m "..."` |
| `remote.rs` | Pull / Push | `git pull --rebase` / `git push` |
| `conflict.rs` | Conflict resolution | Detect conflicts, resolve with ours/theirs/manual |
| `pulse.rs` | Activity feed | `git log` with `--name-status` for file changes |
### Auto-Sync
`useAutoSync` hook handles automatic git sync:
- Configurable interval (from app settings: `auto_pull_interval_minutes` )
- Pulls on interval, pushes after commits
- Detects merge conflicts → opens `ConflictResolverModal`
2026-03-19 09:51:06 +01:00
- Tracks remote status (branch, ahead/behind via `git_remote_status` )
- Handles push rejection (divergence) → sets `pull_required` status
- `pullAndPush()` : pulls then auto-pushes for divergence recovery
- `ConflictNoteBanner` : inline banner in editor for conflicted notes (Keep mine / Keep theirs)
2026-02-17 13:10:43 +01:00
### Frontend Integration
2026-03-28 11:03:01 +01:00
- **Modified file badges**: Orange dots in sidebar
2026-03-07 04:05:40 +01:00
- **Diff view**: Toggle in breadcrumb bar → shows unified diff
- **Git history**: Shown in Inspector panel for active note
- **Commit dialog**: Triggered from sidebar or Cmd+K
- **Pulse view**: Activity feed when Pulse filter is selected
2026-03-19 09:51:06 +01:00
- **Pull command**: Cmd+K → "Pull from Remote", also in Vault menu
- **Git status popup**: Click sync badge → shows branch, ahead/behind, Pull button
- **Conflict banner**: Inline banner in editor with Keep mine / Keep theirs for conflicted notes
2026-02-17 13:10:43 +01:00
## BlockNote Customization
2026-03-07 04:05:40 +01:00
The editor uses [BlockNote ](https://www.blocknotejs.org/ ) for rich text editing, with CodeMirror 6 available as a raw editing alternative.
2026-02-17 13:10:43 +01:00
### Custom Wikilink Inline Content
2026-03-07 04:05:40 +01:00
Defined in `src/components/editorSchema.tsx` :
2026-02-17 13:10:43 +01:00
```typescript
const WikiLink = createReactInlineContentSpec(
{
type: "wikilink",
propSchema: { target: { default: "" } },
content: "none",
},
2026-03-07 04:05:40 +01:00
{ render: (props) => <span className="wikilink">...</span> }
2026-02-17 13:10:43 +01:00
)
```
### Markdown-to-BlockNote Pipeline
2026-03-13 19:12:20 +01:00
```mermaid
flowchart LR
A["📄 Raw markdown\n(from disk)"] --> B["splitFrontmatter()\n→ yaml + body"]
B --> C["preProcessWikilinks(body)\n[[target]] → ‹ token› "]
C --> D["tryParseMarkdownToBlocks()\n→ BlockNote block tree"]
D --> E["injectWikilinks(blocks)\n‹ token› → WikiLink node"]
E --> F["editor.replaceBlocks()\n→ rendered editor"]
style A fill:#f8f9fa ,stroke:#6c757d ,color:#000
style F fill:#d4edda ,stroke:#28a745 ,color:#000
2026-02-17 13:10:43 +01:00
```
2026-03-13 19:12:20 +01:00
> Placeholder tokens use `\u2039` and `\u203A` to avoid colliding with markdown syntax.
2026-03-07 04:05:40 +01:00
### BlockNote-to-Markdown Pipeline (Save)
2026-03-13 19:12:20 +01:00
```mermaid
flowchart LR
A["✏️ BlockNote blocks\n(editor state)"] --> B["blocksToMarkdownLossy()"]
B --> C["postProcessWikilinks()\nWikiLink node → [[target]]"]
C --> D["prepend frontmatter yaml"]
D --> E["invoke('save_note_content')\n→ disk write"]
style A fill:#cce5ff ,stroke:#004085 ,color:#000
style E fill:#d4edda ,stroke:#28a745 ,color:#000
2026-03-07 04:05:40 +01:00
```
2026-02-17 13:10:43 +01:00
### Wikilink Navigation
Two navigation mechanisms:
2026-03-07 04:05:40 +01:00
1. **Click handler ** : DOM event listener on `.editor__blocknote-container` catches clicks on `.wikilink` elements → `onNavigateWikilink(target)` .
2. **Suggestion menu ** : Typing `[[` triggers `SuggestionMenuController` with filtered vault entries.
2026-03-16 04:50:51 +01:00
Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts` ) uses multi-pass matching with global priority: filename stem (strongest) → alias → exact title → humanized title (kebab-case → words). No path-based matching — flat vault uses title/filename only. Legacy path-style targets like `[[person/alice]]` are supported by extracting the last segment.
2026-02-17 13:10:43 +01:00
2026-03-07 04:05:40 +01:00
### Raw Editor Mode
2026-02-17 13:10:43 +01:00
2026-03-07 04:05:40 +01:00
Toggle via Cmd+K → "Raw Editor" or breadcrumb bar button. Uses CodeMirror 6 (`useCodeMirror` hook) to edit the raw markdown + frontmatter directly. Changes saved via the same `save_note_content` command.
2026-02-17 13:10:43 +01:00
2026-03-28 11:03:01 +01:00
## Styling
2026-02-17 13:10:43 +01:00
2026-03-28 11:03:01 +01:00
The app uses a single light theme — the vault-based theming system was removed (see [ADR-0013 ](adr/0013-remove-theming-system.md )). Styling is defined in two layers:
2026-02-17 13:10:43 +01:00
2026-03-07 04:05:40 +01:00
1. **Global CSS variables ** (`src/index.css` ): App-wide colors via `:root` , bridged to Tailwind v4
2. **Editor theme ** (`src/theme.json` ): BlockNote typography, flattened to CSS vars by `useEditorTheme`
2026-02-17 13:10:43 +01:00
## Inspector Abstraction
2026-03-07 04:05:40 +01:00
The Inspector panel (`src/components/Inspector.tsx` ) is composed of sub-panels:
1. **DynamicPropertiesPanel ** (`src/components/DynamicPropertiesPanel.tsx` ): Renders frontmatter as editable key-value pairs:
- **Editable properties** (top): Type badge, Status pill with dropdown, boolean toggles, array tag pills, text fields. Click-to-edit interaction.
- **Info section** (bottom, separated by border): Read-only derived metadata — Modified, Created, Words, File Size. Uses muted styling with no interaction.
2026-03-08 18:00:56 +01:00
- Keys in `SKIP_KEYS` (`type` , `aliases` , `notion_id` , `workspace` , `is_a` , `Is A` ) are hidden from the editable section.
2026-03-07 04:05:40 +01:00
2. **RelationshipsPanel ** : Shows `belongs_to` , `related_to` , and all custom relationship fields as clickable wikilink chips.
3. **BacklinksPanel ** : Scans `allContent` for notes that reference the current note via `[[title]]` or `[[path]]` .
4. **GitHistoryPanel ** : Shows recent commits from file history with relative timestamps.
refactor: remove QMD semantic indexing — keep keyword search only
Remove the entire QMD search engine (semantic indexing, hybrid search,
vector embeddings) and replace with a simple walkdir-based keyword
search. QMD never worked reliably in production, added bundle bloat,
and showed "Index failed" in the status bar.
What changed:
- Rust: deleted indexing.rs, rewrote search.rs as pure keyword search
- Frontend: removed useIndexing hook, IndexingBadge, hybrid search
- Menu: removed "Reindex Vault" command from palette and menu bar
- Config: removed qmd from bundle resources and build script
- Deleted tools/qmd/ (QMD CLI tool, MCP server, tests)
- Updated docs (ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:18:05 +01:00
## Search
2026-03-07 04:05:40 +01:00
refactor: remove QMD semantic indexing — keep keyword search only
Remove the entire QMD search engine (semantic indexing, hybrid search,
vector embeddings) and replace with a simple walkdir-based keyword
search. QMD never worked reliably in production, added bundle bloat,
and showed "Index failed" in the status bar.
What changed:
- Rust: deleted indexing.rs, rewrote search.rs as pure keyword search
- Frontend: removed useIndexing hook, IndexingBadge, hybrid search
- Menu: removed "Reindex Vault" command from palette and menu bar
- Config: removed qmd from bundle resources and build script
- Deleted tools/qmd/ (QMD CLI tool, MCP server, tests)
- Updated docs (ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:18:05 +01:00
### Search
2026-03-07 04:05:40 +01:00
refactor: remove QMD semantic indexing — keep keyword search only
Remove the entire QMD search engine (semantic indexing, hybrid search,
vector embeddings) and replace with a simple walkdir-based keyword
search. QMD never worked reliably in production, added bundle bloat,
and showed "Index failed" in the status bar.
What changed:
- Rust: deleted indexing.rs, rewrote search.rs as pure keyword search
- Frontend: removed useIndexing hook, IndexingBadge, hybrid search
- Menu: removed "Reindex Vault" command from palette and menu bar
- Config: removed qmd from bundle resources and build script
- Deleted tools/qmd/ (QMD CLI tool, MCP server, tests)
- Updated docs (ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:18:05 +01:00
Keyword-based search scans all vault `.md` files using `walkdir` :
2026-03-07 04:05:40 +01:00
refactor: remove QMD semantic indexing — keep keyword search only
Remove the entire QMD search engine (semantic indexing, hybrid search,
vector embeddings) and replace with a simple walkdir-based keyword
search. QMD never worked reliably in production, added bundle bloat,
and showed "Index failed" in the status bar.
What changed:
- Rust: deleted indexing.rs, rewrote search.rs as pure keyword search
- Frontend: removed useIndexing hook, IndexingBadge, hybrid search
- Menu: removed "Reindex Vault" command from palette and menu bar
- Config: removed qmd from bundle resources and build script
- Deleted tools/qmd/ (QMD CLI tool, MCP server, tests)
- Updated docs (ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:18:05 +01:00
```typescript
2026-03-07 04:05:40 +01:00
interface SearchResult {
title: string
path: string
snippet: string
score: number
}
```
### Search Integration
`SearchPanel` component provides the search UI:
refactor: remove QMD semantic indexing — keep keyword search only
Remove the entire QMD search engine (semantic indexing, hybrid search,
vector embeddings) and replace with a simple walkdir-based keyword
search. QMD never worked reliably in production, added bundle bloat,
and showed "Index failed" in the status bar.
What changed:
- Rust: deleted indexing.rs, rewrote search.rs as pure keyword search
- Frontend: removed useIndexing hook, IndexingBadge, hybrid search
- Menu: removed "Reindex Vault" command from palette and menu bar
- Config: removed qmd from bundle resources and build script
- Deleted tools/qmd/ (QMD CLI tool, MCP server, tests)
- Updated docs (ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:18:05 +01:00
- Real-time results as user types (300ms debounce)
2026-03-07 04:05:40 +01:00
- Click result to open note in editor
- Shows relevance score and snippet
refactor: remove QMD semantic indexing — keep keyword search only
Remove the entire QMD search engine (semantic indexing, hybrid search,
vector embeddings) and replace with a simple walkdir-based keyword
search. QMD never worked reliably in production, added bundle bloat,
and showed "Index failed" in the status bar.
What changed:
- Rust: deleted indexing.rs, rewrote search.rs as pure keyword search
- Frontend: removed useIndexing hook, IndexingBadge, hybrid search
- Menu: removed "Reindex Vault" command from palette and menu bar
- Config: removed qmd from bundle resources and build script
- Deleted tools/qmd/ (QMD CLI tool, MCP server, tests)
- Updated docs (ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:18:05 +01:00
No indexing step required — search runs directly against the filesystem.
2026-03-07 04:05:40 +01:00
## Vault Management
### Vault Switching
`useVaultSwitcher` hook manages multiple vaults:
- Persists vault list to `~/.config/com.laputa.app/vaults.json`
- Switching closes all tabs and resets sidebar
- Supports adding, removing, hiding/restoring vaults
- Default vault: Getting Started demo vault
### Vault Config
2026-02-17 13:10:43 +01:00
2026-03-20 21:00:19 +01:00
Per-vault settings stored in `ui.config.md` at vault root:
2026-03-07 04:05:40 +01:00
- Editable as a normal note (YAML frontmatter)
- Managed by `useVaultConfig` hook and `vaultConfigStore`
- Settings: zoom, view mode, tag colors, status colors, property display modes
- One-time migration from localStorage (`configMigration.ts` )
### Getting Started / Onboarding
`useOnboarding` hook detects first launch:
- If vault path doesn't exist → show `WelcomeScreen`
- User can create Getting Started vault or open existing folder
- Welcome state tracked in localStorage (`laputa_welcome_dismissed` )
### GitHub Integration
Device Authorization Flow for GitHub-backed vaults:
- `GitHubDeviceFlow` component handles OAuth
- `GitHubVaultModal` for cloning existing repos or creating new ones
- Token persisted in app settings for future git operations
- `SettingsPanel` shows connection status with disconnect option
## Settings
App-level settings persisted at `~/.config/com.laputa.app/settings.json` :
```typescript
interface Settings {
openai_key: string | null
google_key: string | null
github_token: string | null
github_username: string | null
auto_pull_interval_minutes: number | null
}
```
2026-02-17 13:10:43 +01:00
2026-03-07 04:05:40 +01:00
Managed by `useSettings` hook and `SettingsPanel` component.
2026-03-25 16:22:51 +01:00
## Telemetry
### Components
- **`TelemetryConsentDialog` ** — First-launch dialog asking user to opt in to anonymous crash reporting. Two buttons: accept (sets `telemetry_consent: true` , generates `anonymous_id` ) or decline.
- **`TelemetryToggle` ** — Checkbox component in `SettingsPanel` for crash reporting and analytics toggles.
### Hooks
- **`useTelemetry(settings, loaded)` ** — Reactively initializes/tears down Sentry and PostHog based on settings. Called once in `App` .
### Libraries
- **`src/lib/telemetry.ts` ** — `initSentry()` , `teardownSentry()` , `initPostHog()` , `teardownPostHog()` , `trackEvent()` . Path scrubber via `beforeSend` hook. DSN/key from `VITE_SENTRY_DSN` / `VITE_POSTHOG_KEY` env vars.
- **`src-tauri/src/telemetry.rs` ** — Rust-side Sentry init with `beforeSend` path scrubber. `init_sentry_from_settings()` reads settings and conditionally initializes. `reinit_sentry()` for runtime toggle.
### Tauri Commands
- **`reinit_telemetry` ** — Re-reads settings and toggles Rust Sentry on/off. Called from frontend when user changes crash reporting setting.
2026-03-25 17:51:33 +01:00
---
## Update Channels & Feature Flags
### Settings
- **`update_channel` ** — `"stable"` (default/null) or `"canary"` . Stored in `Settings` struct, configurable in Settings panel under "Updates" section.
### Hooks
- **`useUpdater(channel?)` ** — Checks for updates. For stable: uses Tauri updater plugin. For canary: fetches `latest-canary.json` and opens release page for download.
- **`useFeatureFlag(flag)` ** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_<name>` ), then falls back to compile-time default. Type-safe via `FeatureFlagName` union.
### CI/CD
- **`.github/workflows/release.yml` ** — Stable builds from `main` . Produces `latest.json` on GitHub Pages.
- **`.github/workflows/release-canary.yml` ** — Canary builds from `canary` branch. Produces `latest-canary.json` on GitHub Pages. Releases are marked as prerelease.