609 lines
27 KiB
Markdown
609 lines
27 KiB
Markdown
# Abstractions
|
||
|
||
Key abstractions and domain models in Tolaria.
|
||
|
||
## Design Philosophy
|
||
|
||
Tolaria'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 Tolaria's UI:
|
||
|
||
| Field | Meaning | UI behavior |
|
||
|---|---|---|
|
||
| `title:` | Legacy display-title fallback for older notes | Used only when a note has no H1; new notes do not write it automatically |
|
||
| `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 |
|
||
| `icon:` | Per-note icon (emoji, Phosphor name, or HTTP/HTTPS image URL) | Rendered on note title surfaces; editable from the Properties panel |
|
||
| `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 |
|
||
|
||
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)).
|
||
|
||
### 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 Tolaria internally for configuration, behavior, and UI preferences
|
||
|
||
Examples:
|
||
```yaml
|
||
_pinned_properties: # which properties appear in the editor inline bar (per-type)
|
||
- key: status
|
||
icon: circle-dot
|
||
_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.
|
||
|
||
## Document Model
|
||
|
||
All data lives in markdown files with YAML frontmatter. There is no database — the filesystem is the source of truth.
|
||
|
||
### VaultEntry
|
||
|
||
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 ⚠ legacy
|
||
+Number? trashedAt ⚠ legacy
|
||
+Record~string,string~ properties
|
||
}
|
||
|
||
class TypeDocument {
|
||
+String icon
|
||
+String color
|
||
+Number order
|
||
+String sidebarLabel
|
||
+String template
|
||
+String sort
|
||
+Boolean visible
|
||
}
|
||
|
||
class Frontmatter {
|
||
+String type
|
||
+String status
|
||
+String url
|
||
+String[] belongsTo
|
||
+String[] relatedTo
|
||
+String[] aliases
|
||
...custom fields
|
||
}
|
||
|
||
VaultEntry --> Frontmatter : parsed from
|
||
VaultEntry --> TypeDocument : isA resolves to
|
||
VaultEntry "many" --> "1" TypeDocument : grouped by type
|
||
```
|
||
|
||
```typescript
|
||
// src/types.ts
|
||
interface VaultEntry {
|
||
path: string // Absolute file path
|
||
filename: string // Just the filename
|
||
title: string // From first # heading, or filename fallback
|
||
isA: string | null // Entity type: Project, Procedure, Person, etc. (from frontmatter `type:` field)
|
||
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
|
||
modifiedAt: number | null // Unix timestamp (seconds)
|
||
// Note: owner and cadence are now in the generic `properties` map
|
||
createdAt: number | null // Unix timestamp (seconds)
|
||
fileSize: number
|
||
wordCount: number | null // Body word count (excludes frontmatter)
|
||
snippet: string | null // First 200 chars of body
|
||
archived: boolean // Archived flag
|
||
trashed: boolean // Kept for backward compatibility (Trash system removed — delete is permanent)
|
||
trashedAt: number | null // Kept for backward compatibility (Trash system removed)
|
||
properties: Record<string, string> // Scalar frontmatter fields (custom properties)
|
||
}
|
||
```
|
||
|
||
### Entity Types (isA / type)
|
||
|
||
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.
|
||
|
||
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:
|
||
|
||
```
|
||
~/Laputa/
|
||
├── 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
|
||
└── config/ ← meta-configuration files (agents.md, etc.)
|
||
```
|
||
|
||
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.
|
||
|
||
### 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:
|
||
|
||
- Have `type: Type` in their frontmatter (`Is A: Type` also accepted as legacy alias)
|
||
- 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
|
||
- Serve as the "definition" for their type category
|
||
|
||
**Type document properties** (read by Rust and used in the UI):
|
||
|
||
| Property | Type | Description |
|
||
|----------|------|-------------|
|
||
| `icon` | string | Type icon as a Phosphor 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) |
|
||
|
||
**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**:
|
||
- Clicking a section group header pins the type document at the top of the NoteList if it exists
|
||
- Viewing a type document in entity view shows an "Instances" group listing all entries of that type
|
||
- The Type field in the Inspector is rendered as a clickable chip that navigates to the type document
|
||
|
||
### Frontmatter Format
|
||
|
||
Standard YAML frontmatter between `---` delimiters:
|
||
|
||
```yaml
|
||
---
|
||
title: Write Weekly Essays
|
||
type: Procedure
|
||
status: Active
|
||
belongs_to:
|
||
- "[[grow-newsletter]]"
|
||
related_to:
|
||
- "[[writing]]"
|
||
aliases:
|
||
- Weekly Writing
|
||
---
|
||
```
|
||
|
||
Supported value types (defined in `src-tauri/src/frontmatter/yaml.rs` as `FrontmatterValue`):
|
||
- **String**: `status: Active`
|
||
- **Number**: `priority: 5`
|
||
- **Bool**: `archived: true`
|
||
- **List**: Multi-line ` - item` or inline `[item1, item2]`
|
||
- **Null**: `owner:` (empty value)
|
||
|
||
### 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:
|
||
- "[[writing]]"
|
||
- "[[productivity]]"
|
||
Key People:
|
||
- "[[matteo-cellini]]"
|
||
---
|
||
```
|
||
|
||
Becomes: `relationships["Topics"] = ["[[writing]]", "[[productivity]]"]`
|
||
|
||
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.
|
||
|
||
### Title / Filename Sync
|
||
|
||
Tolaria separates **display title** from the file identifier:
|
||
|
||
- **Display title resolution** (`extract_title` in `vault/parsing.rs`): first `# H1` on the first non-empty body line, then legacy frontmatter `title:`, then slug-to-title from the filename stem.
|
||
- **Opening a note is read-only**: selecting a note does not inject or auto-correct `title:` frontmatter.
|
||
- **Explicit filename actions** (`rename_note`): breadcrumb rename/sync actions update the filename and wikilinks across the vault. The editor body remains the title editing surface.
|
||
- **Untitled drafts** start as `untitled-*.md` and are auto-renamed on save once the note gains an H1.
|
||
|
||
### Title Surface (UI)
|
||
|
||
The BlockNote body is the only title editing surface:
|
||
|
||
- The first H1 is the canonical display title.
|
||
- There is no separate title row above the editor, even when a note has no H1.
|
||
- Notes without an H1 show the editor body and placeholder only.
|
||
- Filename changes are explicit breadcrumb actions, not a dedicated title-input side effect.
|
||
|
||
### Sidebar Selection
|
||
|
||
Navigation state is modeled as a discriminated union:
|
||
|
||
```typescript
|
||
type SidebarFilter = 'all' | 'archived' | 'changes' | 'pulse'
|
||
|
||
type SidebarSelection =
|
||
| { kind: 'filter'; filter: SidebarFilter }
|
||
| { 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)
|
||
|
||
`vault::scan_vault(path)` in `src-tauri/src/vault/mod.rs`:
|
||
|
||
1. Validates the path exists and is a directory
|
||
2. Scans root-level `.md` files (non-recursive)
|
||
3. Recursively scans protected folders: `type/`, `config/`, `attachments/`
|
||
4. Files in non-protected subfolders are **not indexed** (flat vault enforcement)
|
||
5. For each `.md` file, calls `parse_md_file()`:
|
||
- Reads content with `fs::read_to_string()`
|
||
- Parses frontmatter with `gray_matter::Matter::<YAML>`
|
||
- Extracts title from first `#` heading
|
||
- Reads entity type from `type:` frontmatter field (`Is A:` accepted as legacy alias); type is never inferred from folder
|
||
- Parses dates as ISO 8601 to Unix timestamps
|
||
- Extracts relationships, outgoing links, custom properties, word count, snippet
|
||
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`.
|
||
|
||
### Vault Caching
|
||
|
||
`vault::scan_vault_cached(path)` wraps scanning with git-based caching:
|
||
|
||
1. Reads cache from `~/.laputa/cache/<vault-hash>.json` (external to vault)
|
||
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
|
||
6. Writes updated cache atomically (write to `.tmp`, then rename)
|
||
7. On first run, migrates any legacy `.laputa-cache.json` from inside the vault
|
||
|
||
### Frontmatter Manipulation (Rust)
|
||
|
||
`frontmatter/ops.rs:update_frontmatter_content()` performs line-by-line YAML editing:
|
||
|
||
1. Finds the frontmatter block between `---` delimiters
|
||
2. Iterates through lines looking for the target key
|
||
3. If found: replaces the value (consuming multi-line list items if present)
|
||
4. If not found: appends the new key-value at the end
|
||
5. If no frontmatter exists: creates a new `---` block
|
||
|
||
The `with_frontmatter()` helper wraps this in a read-transform-write cycle on the actual file.
|
||
|
||
### Content Loading
|
||
|
||
- **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
|
||
- Content for backlink detection (`allContent`) is stored in memory as `Record<string, string>`
|
||
|
||
## Git Integration
|
||
|
||
Git operations live in `src-tauri/src/git/`. All operations shell out to the `git` CLI (not libgit2).
|
||
|
||
### Data Types
|
||
|
||
```typescript
|
||
interface GitCommit {
|
||
hash: string
|
||
shortHash: string
|
||
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'
|
||
}
|
||
|
||
interface GitRemoteStatus {
|
||
branch: string
|
||
ahead: number
|
||
behind: number
|
||
hasRemote: boolean
|
||
}
|
||
|
||
interface PulseCommit {
|
||
hash: string
|
||
shortHash: string
|
||
message: string
|
||
date: number
|
||
githubUrl: string | null
|
||
files: PulseFile[]
|
||
added: number
|
||
modified: number
|
||
deleted: number
|
||
}
|
||
```
|
||
|
||
### Operations
|
||
|
||
| 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`
|
||
- 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)
|
||
|
||
`useGitRemoteStatus` is the commit-time companion to `useAutoSync`:
|
||
- Re-checks `git_remote_status` when the Commit dialog opens and right before submit
|
||
- Converts `hasRemote: false` into a local-only commit path
|
||
- Keeps the normal push path unchanged for vaults that do have a remote
|
||
|
||
### Frontend Integration
|
||
|
||
- **Modified file badges**: Orange dots in sidebar
|
||
- **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
|
||
- **No remote indicator**: Neutral chip in the bottom bar when `GitRemoteStatus.hasRemote === false`
|
||
- **Pulse view**: Activity feed when Pulse filter is selected
|
||
- **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
|
||
|
||
## BlockNote Customization
|
||
|
||
The editor uses [BlockNote](https://www.blocknotejs.org/) for rich text editing, with CodeMirror 6 available as a raw editing alternative.
|
||
|
||
### Custom Wikilink Inline Content
|
||
|
||
Defined in `src/components/editorSchema.tsx`:
|
||
|
||
```typescript
|
||
const WikiLink = createReactInlineContentSpec(
|
||
{
|
||
type: "wikilink",
|
||
propSchema: { target: { default: "" } },
|
||
content: "none",
|
||
},
|
||
{ render: (props) => <span className="wikilink">...</span> }
|
||
)
|
||
```
|
||
|
||
### Code Block Highlighting
|
||
|
||
Defined in `src/components/editorSchema.tsx` and styled in `src/components/EditorTheme.css`:
|
||
|
||
- The schema overrides BlockNote's default `codeBlock` spec with `createCodeBlockSpec({ ...codeBlockOptions, defaultLanguage: "text" })` from `@blocknote/code-block`.
|
||
- Fenced code blocks now use BlockNote's supported Shiki-backed highlighter path, which renders `.shiki` token spans directly inside the editor DOM.
|
||
- Tolaria keeps `defaultLanguage: "text"` so unlabeled code blocks do not silently become JavaScript while still supporting the packaged language aliases such as `ts` → `typescript`.
|
||
- Inline-code chip styling remains scoped to `.bn-inline-content code`, so fenced `pre > code` nodes keep BlockNote's dark shell instead of inheriting the muted inline surface.
|
||
|
||
### Markdown-to-BlockNote Pipeline
|
||
|
||
```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
|
||
```
|
||
|
||
> Placeholder tokens use `\u2039` and `\u203A` to avoid colliding with markdown syntax.
|
||
|
||
### BlockNote-to-Markdown Pipeline (Save)
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["✏️ BlockNote blocks\n(editor state)"] --> B["blocksToMarkdownLossy()"]
|
||
B --> C["postProcessWikilinks()\nWikiLink node → [[target]]"]
|
||
C --> D["prepend frontmatter yaml"]
|
||
D --> E["invoke('save_note_content')\n→ disk write"]
|
||
|
||
style A fill:#cce5ff,stroke:#004085,color:#000
|
||
style E fill:#d4edda,stroke:#28a745,color:#000
|
||
```
|
||
|
||
### Wikilink Navigation
|
||
|
||
Two navigation mechanisms:
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
### Raw Editor Mode
|
||
|
||
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.
|
||
|
||
## Styling
|
||
|
||
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:
|
||
|
||
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`
|
||
|
||
## Inspector Abstraction
|
||
|
||
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.
|
||
- Keys in `SKIP_KEYS` (`type`, `aliases`, `notion_id`, `workspace`, `is_a`, `Is A`) are hidden from the editable section.
|
||
|
||
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.
|
||
|
||
## Search
|
||
|
||
### Search
|
||
|
||
Keyword-based search scans all vault `.md` files using `walkdir`:
|
||
|
||
```typescript
|
||
interface SearchResult {
|
||
title: string
|
||
path: string
|
||
snippet: string
|
||
score: number
|
||
}
|
||
```
|
||
|
||
### Search Integration
|
||
|
||
`SearchPanel` component provides the search UI:
|
||
- Real-time results as user types (300ms debounce)
|
||
- Click result to open note in editor
|
||
- Shows relevance score and snippet
|
||
|
||
No indexing step required — search runs directly against the filesystem.
|
||
|
||
## Vault Management
|
||
|
||
### Vault Switching
|
||
|
||
`useVaultSwitcher` hook manages multiple vaults:
|
||
- Persists vault list to `~/.config/com.tolaria.app/vaults.json` (reads legacy `com.laputa.app` on upgrade)
|
||
- Switching closes all tabs and resets sidebar
|
||
- Supports adding, removing, hiding/restoring vaults
|
||
- Default vault: Getting Started demo vault
|
||
|
||
### Vault Config
|
||
|
||
Per-vault settings stored locally and scoped by vault path:
|
||
- Managed by `useVaultConfig` hook and `vaultConfigStore`
|
||
- Settings: zoom, view mode, tag colors, status colors, property display modes, Inbox note-list column overrides, explicit organization workflow toggle
|
||
- 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 a new empty vault, open an existing folder, or clone the public Getting Started vault into a chosen folder
|
||
- Welcome state tracked in localStorage (`tolaria_welcome_dismissed`, with legacy fallback)
|
||
|
||
`useAiAgentsOnboarding(enabled)` adds a separate first-launch agent step:
|
||
- Reads a local dismissal flag for the AI agents prompt (with a legacy fallback to the older Claude-only key)
|
||
- Only shows after vault onboarding has already resolved to a ready state
|
||
- Persists dismissal locally once the user continues
|
||
|
||
### Remote Git Operations
|
||
|
||
Tolaria delegates remote auth to the user's system git setup:
|
||
- `CloneVaultModal` captures a remote URL and local destination
|
||
- `clone_repo` shells out to system git for clone operations
|
||
- Existing `git_pull` / `git_push` commands keep surfacing raw git errors
|
||
- No provider-specific token or username is stored in app settings
|
||
|
||
## Settings
|
||
|
||
App-level settings persisted at `~/.config/com.tolaria.app/settings.json` (reads legacy `com.laputa.app` on upgrade):
|
||
|
||
```typescript
|
||
interface Settings {
|
||
auto_pull_interval_minutes: number | null
|
||
telemetry_consent: boolean | null
|
||
crash_reporting_enabled: boolean | null
|
||
analytics_enabled: boolean | null
|
||
anonymous_id: string | null
|
||
release_channel: string | null // null = stable default, "alpha" = every-push prerelease feed
|
||
default_ai_agent: 'claude_code' | 'codex' | null
|
||
}
|
||
```
|
||
|
||
Managed by `useSettings` hook and `SettingsPanel` component. `default_ai_agent` is an installation-local preference that selects which supported CLI agent the AI panel, command palette AI mode, and status bar should target by default.
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## Updates & Feature Flags
|
||
|
||
### Hooks
|
||
- **`useUpdater(releaseChannel)`** — Channel-aware updater state machine. Checks the selected feed, surfaces available/downloading/ready states, and delegates install work to Rust.
|
||
- **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_<name>`), then falls back to telemetry-backed evaluation. Type-safe via `FeatureFlagName` union.
|
||
|
||
### Frontend helpers
|
||
- **`src/lib/releaseChannel.ts`** — Normalizes persisted channel values so legacy or invalid settings fall back to Stable, while Stable serializes back to `null`.
|
||
- **`src/lib/appUpdater.ts`** — Thin wrapper around the Tauri updater commands. Keeps the React hook free of endpoint-selection details.
|
||
|
||
### Rust
|
||
- **`src-tauri/src/app_updater.rs`** — Chooses the correct update endpoint (`alpha/latest.json` or `stable/latest.json`) and adapts Tauri updater results into frontend-friendly payloads.
|
||
|
||
### Tauri Commands
|
||
- **`check_for_app_update`** — Channel-aware update manifest lookup.
|
||
- **`download_and_install_app_update`** — Channel-aware download/install with streamed progress events.
|
||
|
||
### CI/CD
|
||
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main`. Publishes `alpha/latest.json` and refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed.
|
||
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-v*` tags. Publishes `stable/latest.json`.
|
||
- **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed.
|