29 KiB
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.
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 | Humanized to Belongs to in the UI |
related_to: |
Lateral relationship | Humanized to Related to in the UI |
has: |
Contained relationship | Humanized to Has in the UI |
Relationship fields are detected dynamically — any frontmatter field containing [[wikilink]] values is treated as a relationship (see ADR-0010). Tolaria's own default relationship vocabulary uses snake_case on disk, but labels are humanized at render time and existing user-authored keys are left untouched.
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:
_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).
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
// 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
├── AGENTS.md ← canonical Tolaria AI guidance
├── CLAUDE.md ← compatibility shim pointing at AGENTS.md
├── ...
└── type/ ← type definition documents
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. Legacy config/ content is still recognized during migration and repair, but Tolaria's managed AI guidance now lives at the vault root.
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: Typein their frontmatter (Is A: Typealso 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:
---
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
- itemor 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:
---
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_titleinvault/parsing.rs): first# H1on the first non-empty body line, then legacy frontmattertitle:, 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-*.mdand 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:
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:
- Validates the path exists and is a directory
- Scans root-level
.mdfiles (non-recursive) - Recursively scans protected folders:
type/, legacyconfig/,attachments/ - Files in non-protected subfolders are not indexed (flat vault enforcement)
- For each
.mdfile, callsparse_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
- Reads content with
- Sorts by
modified_atdescending - 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:
- Reads cache from
~/.laputa/cache/<vault-hash>.json(external to vault) - Compares cache version, vault path, and git HEAD commit hash
- If cache is valid and same commit → only re-parse uncommitted changed files
- If different commit → use
git diffto find changed files → selective re-parse - If no cache → full scan
- Writes updated cache atomically (write to
.tmp, then rename) - On first run, migrates any legacy
.laputa-cache.jsonfrom inside the vault
Frontmatter Manipulation (Rust)
frontmatter/ops.rs:update_frontmatter_content() performs line-by-line YAML editing:
- Finds the frontmatter block between
---delimiters - Iterates through lines looking for the target key
- If found: replaces the value (consuming multi-line list items if present)
- If not found: appends the new key-value at the end
- 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 asRecord<string, string>
Git Integration
Git operations live in src-tauri/src/git/. All operations shell out to the git CLI (not libgit2).
Data Types
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_requiredstatus pullAndPush(): pulls then auto-pushes for divergence recoveryConflictNoteBanner: inline banner in editor for conflicted notes (Keep mine / Keep theirs)
useGitRemoteStatus is the commit-time companion to useAutoSync:
- Re-checks
git_remote_statuswhen the Commit dialog opens and right before submit - Converts
hasRemote: falseinto 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 for rich text editing, with CodeMirror 6 available as a raw editing alternative.
Custom Wikilink Inline Content
Defined in src/components/editorSchema.tsx:
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
codeBlockspec withcreateCodeBlockSpec({ ...codeBlockOptions, defaultLanguage: "text" })from@blocknote/code-block. - Fenced code blocks now use BlockNote's supported Shiki-backed highlighter path, which renders
.shikitoken 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 asts→typescript. - Inline-code chip styling remains scoped to
.bn-inline-content code, so fencedpre > codenodes keep BlockNote's dark shell instead of inheriting the muted inline surface.
Formatting Surface Policy
Defined in src/components/tolariaEditorFormatting.tsx and src/components/tolariaEditorFormattingConfig.ts:
SingleEditorViewdisables BlockNote's default formatting toolbar and/menu, then mounts Tolaria-owned controllers so the visible formatting surface matches Tolaria's markdown round-trip guarantees.- The formatting toolbar only exposes inline controls that persist through
blocksToMarkdownLossy()in Tolaria's save pipeline: bold, italic, strike, nesting, and link creation. Controls that BlockNote can render temporarily but Tolaria cannot faithfully persist, such as underline, color, alignment, and the block-type dropdown, are hidden instead of appearing to work and later disappearing. - The
/slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, and list blocks. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model.
Markdown-to-BlockNote Pipeline
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
\u2039and\u203Ato avoid colliding with markdown syntax.
BlockNote-to-Markdown Pipeline (Save)
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:
- Click handler: DOM event listener on
.editor__blocknote-containercatches clicks on.wikilinkelements →onNavigateWikilink(target). - Suggestion menu: Typing
[[triggersSuggestionMenuControllerwith 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). Styling is defined in two layers:
- Global CSS variables (
src/index.css): App-wide colors via:root, bridged to Tailwind v4 - Editor theme (
src/theme.json): BlockNote typography, flattened to CSS vars byuseEditorTheme
Inspector Abstraction
The Inspector panel (src/components/Inspector.tsx) is composed of sub-panels:
-
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.
-
RelationshipsPanel: Shows
belongs_to,related_to,has, and all custom relationship fields as clickable wikilink chips. Relationship labels are humanized for display, but stored keys remain unchanged. -
BacklinksPanel: Scans
allContentfor notes that reference the current note via[[title]]or[[path]]. -
GitHistoryPanel: Shows recent commits from file history with relative timestamps.
Search
Search
Keyword-based search scans all vault .md files using walkdir:
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 legacycom.laputa.appon 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
useVaultConfighook andvaultConfigStore - 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)
AI Guidance Files
Tolaria tracks managed vault-level AI guidance separately from normal note content:
AGENTS.mdis the canonical managed guidance file for Tolaria-aware coding agentsCLAUDE.mdis a compatibility shim that points Claude Code back toAGENTS.mduseVaultAiGuidanceStatusreadsget_vault_ai_guidance_statusand normalizes the backend state into four UI cases:managed,missing,broken, andcustomrestore_vault_ai_guidancerepairs only Tolaria-managed files; user-authored customAGENTS.md/CLAUDE.mdfiles are surfaced as custom and left untouched- The status bar AI badge and command palette consume that abstraction to expose restore actions only when the managed guidance is missing or broken
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 parent folder; Tolaria derives the final
Getting Startedchild path before cloning - Welcome state tracked in localStorage (
tolaria_welcome_dismissed, with legacy fallback)
useGettingStartedClone encapsulates the non-onboarding Getting Started action:
- Opens the same parent-folder picker used by onboarding
- Derives the final
.../Getting Starteddestination path - Surfaces the resolved path through the app toast after a successful clone
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:
CloneVaultModalcaptures a remote URL and local destinationclone_reposhells out to system git for clone operations- Existing
git_pull/git_pushcommands 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):
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 (setstelemetry_consent: true, generatesanonymous_id) or decline.TelemetryToggle— Checkbox component inSettingsPanelfor crash reporting and analytics toggles.
Hooks
useTelemetry(settings, loaded)— Reactively initializes/tears down Sentry and PostHog based on settings. Called once inApp.
Libraries
src/lib/telemetry.ts—initSentry(),teardownSentry(),initPostHog(),teardownPostHog(),trackEvent(). Path scrubber viabeforeSendhook. DSN/key fromVITE_SENTRY_DSN/VITE_POSTHOG_KEYenv vars.src-tauri/src/telemetry.rs— Rust-side Sentry init withbeforeSendpath 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. CheckslocalStorageoverride (ff_<name>), then falls back to telemetry-backed evaluation. Type-safe viaFeatureFlagNameunion.
Frontend helpers
src/lib/releaseChannel.ts— Normalizes persisted channel values so legacy or invalid settings fall back to Stable, while Stable serializes back tonull.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.jsonorstable/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 tomain. Publishesalpha/latest.jsonand refreshes the legacylatest.json/latest-canary.jsonaliases to the alpha feed..github/workflows/release-stable.yml— Stable releases fromstable-v*tags. Publishesstable/latest.json.- Beta cohorts are handled in PostHog targeting only. There is no beta updater feed.