Compare commits

..

1 Commits

Author SHA1 Message Date
lucaronin
c118472674 docs: add public site 2026-04-28 23:04:22 +02:00
700 changed files with 9628 additions and 61954 deletions

21
.claude/commands/start.md Normal file
View File

@@ -0,0 +1,21 @@
# /start
Start Tolaria in Tauri dev mode.
## Steps
1. Change to the Tolaria workspace:
```bash
cd /Users/luca/Workspace/tolaria
```
2. Start the native development app:
```bash
pnpm tauri dev
```
3. Keep the command running. Report the Vite local URL once it appears and leave the dev process alive for the user.
This is a local utility command, not a Laputa task. Do not run `/laputa-next-task`, CodeScene gates, Todoist updates, commits, or pushes for this command unless the user asks separately.

View File

@@ -1,2 +1,2 @@
HOTSPOT_THRESHOLD=10.0
AVERAGE_THRESHOLD=9.93
AVERAGE_THRESHOLD=9.89

View File

@@ -1,137 +0,0 @@
$ErrorActionPreference = "Stop"
$nsisUrl = "https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip"
$nsisSha1 = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D"
$tauriUtilsUrl = "https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/nsis_tauri_utils.dll"
$tauriUtilsSha1 = "75197FEE3C6A814FE035788D1C34EAD39349B860"
$tauriUtilsRelativePath = "Plugins\x86-unicode\additional\nsis_tauri_utils.dll"
$nsisRequiredFiles = @(
"makensis.exe",
"Bin\makensis.exe",
"Stubs\lzma-x86-unicode",
"Stubs\lzma_solid-x86-unicode",
"Include\MUI2.nsh",
"Include\FileFunc.nsh",
"Include\x64.nsh",
"Include\nsDialogs.nsh",
"Include\WinMessages.nsh",
"Include\Win\COM.nsh",
"Include\Win\Propkey.nsh",
"Include\Win\RestartManager.nsh"
)
function Get-UpperSha1 {
param([Parameter(Mandatory = $true)][string]$Path)
return (Get-FileHash -Algorithm SHA1 -LiteralPath $Path).Hash.ToUpperInvariant()
}
function Test-FileSha1 {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedSha1
)
return (Test-Path -LiteralPath $Path) -and ((Get-UpperSha1 -Path $Path) -eq $ExpectedSha1)
}
function Save-VerifiedDownload {
param(
[Parameter(Mandatory = $true)][string]$Uri,
[Parameter(Mandatory = $true)][string]$OutFile,
[Parameter(Mandatory = $true)][string]$ExpectedSha1
)
$parent = Split-Path -Parent $OutFile
New-Item -ItemType Directory -Force -Path $parent | Out-Null
$tempFile = "$OutFile.download"
for ($attempt = 1; $attempt -le 5; $attempt++) {
try {
Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath $tempFile
Invoke-WebRequest -Uri $Uri -OutFile $tempFile -TimeoutSec 120
$actualSha1 = Get-UpperSha1 -Path $tempFile
if ($actualSha1 -ne $ExpectedSha1) {
throw "SHA1 mismatch for $Uri. Expected $ExpectedSha1, got $actualSha1."
}
Move-Item -Force -LiteralPath $tempFile -Destination $OutFile
return
} catch {
Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath $tempFile
if ($attempt -eq 5) {
throw
}
$delaySeconds = [Math]::Min(30, 5 * $attempt)
Write-Warning "Download attempt ${attempt} failed: $($_.Exception.Message). Retrying in ${delaySeconds}s."
Start-Sleep -Seconds $delaySeconds
}
}
}
function Find-MissingFile {
param(
[Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)][string[]]$RelativePaths
)
foreach ($relativePath in $RelativePaths) {
if (-not (Test-Path -LiteralPath (Join-Path $Root $relativePath))) {
return $relativePath
}
}
return $null
}
if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
throw "LOCALAPPDATA is required to resolve Tauri's Windows tool cache."
}
$tauriToolsPath = Join-Path $env:LOCALAPPDATA "tauri"
$nsisPath = Join-Path $tauriToolsPath "NSIS"
$downloadRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
[System.IO.Path]::GetTempPath()
} else {
$env:RUNNER_TEMP
}
New-Item -ItemType Directory -Force -Path $tauriToolsPath | Out-Null
$missingNsisFile = Find-MissingFile -Root $nsisPath -RelativePaths $nsisRequiredFiles
if ($missingNsisFile) {
Write-Host "Tauri NSIS cache is missing $missingNsisFile; downloading NSIS 3.11."
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath $nsisPath
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath (Join-Path $tauriToolsPath "nsis-3.11")
$zipPath = Join-Path $downloadRoot "nsis-3.11.zip"
Save-VerifiedDownload -Uri $nsisUrl -OutFile $zipPath -ExpectedSha1 $nsisSha1
Expand-Archive -Force -LiteralPath $zipPath -DestinationPath $tauriToolsPath
$extractedNsisPath = Join-Path $tauriToolsPath "nsis-3.11"
if (-not (Test-Path -LiteralPath $extractedNsisPath)) {
throw "Downloaded NSIS archive did not contain the expected nsis-3.11 directory."
}
Move-Item -Force -LiteralPath $extractedNsisPath -Destination $nsisPath
} else {
Write-Host "Tauri NSIS cache already contains NSIS 3.11."
}
$tauriUtilsPath = Join-Path $nsisPath $tauriUtilsRelativePath
if (-not (Test-FileSha1 -Path $tauriUtilsPath -ExpectedSha1 $tauriUtilsSha1)) {
Write-Host "Downloading Tauri NSIS utility plugin."
Save-VerifiedDownload -Uri $tauriUtilsUrl -OutFile $tauriUtilsPath -ExpectedSha1 $tauriUtilsSha1
} else {
Write-Host "Tauri NSIS utility plugin is already cached."
}
$missingFile = Find-MissingFile -Root $nsisPath -RelativePaths ($nsisRequiredFiles + @($tauriUtilsRelativePath))
if ($missingFile) {
throw "Tauri NSIS toolchain is incomplete after prefetch; missing $missingFile."
}
Write-Host "Tauri NSIS toolchain ready at $nsisPath."

View File

@@ -14,12 +14,10 @@ permissions:
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
# Keep large production frontend builds below CI runner memory limits.
NODE_OPTIONS: --max-old-space-size=4096
jobs:
frontend-quality:
name: Frontend Tests & Quality Checks
test:
name: Tests & Quality Checks
runs-on: macos-15
steps:
@@ -38,11 +36,28 @@ jobs:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy, llvm-tools-preview
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Keep frontend and Rust quality gates in separate macOS jobs so the
# expensive Rust target cache restore no longer blocks the frontend lane.
# ── 0. Build check (catches type errors and bundler failures) ─────────
- name: TypeScript type check
run: pnpm exec tsc --noEmit
@@ -54,7 +69,8 @@ jobs:
run: pnpm docs:build
# ── 1. Coverage-backed tests ──────────────────────────────────────────
# The coverage command runs the canonical frontend test suite.
# The coverage commands run the same frontend and Rust test suites, so keep
# them as the canonical test lane instead of running every suite twice.
- name: Bundle MCP server resources (required by Tauri build)
run: node scripts/bundle-mcp-server.mjs
@@ -63,15 +79,25 @@ jobs:
run: pnpm test:coverage
# Thresholds configured in vite.config.ts — exits non-zero if coverage drops
- name: Upload frontend coverage to Codecov
- name: Rust tests + coverage (≥85% lines)
run: |
cargo llvm-cov \
--manifest-path src-tauri/Cargo.toml \
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
--lcov \
--output-path coverage/rust.lcov \
--fail-under-lines 85
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
- name: Upload coverage to Codecov
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@v5
with:
use_oidc: true
fail_ci_if_error: true
disable_search: true
files: ./coverage/lcov.info
flags: frontend
files: ./coverage/lcov.info,./coverage/rust.lcov
verbose: true
# OIDC avoids long-lived CODECOV_TOKEN secrets.
@@ -135,56 +161,6 @@ jobs:
- name: Lint frontend
run: pnpm lint
rust-quality:
name: Rust Tests & Quality Checks
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy, llvm-tools-preview
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Rust tests + coverage (≥85% lines)
run: |
mkdir -p coverage
cargo llvm-cov \
--manifest-path src-tauri/Cargo.toml \
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
--lcov \
--output-path coverage/rust.lcov \
--fail-under-lines 85
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
- name: Upload Rust coverage to Codecov
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@v5
with:
use_oidc: true
fail_ci_if_error: true
disable_search: true
files: ./coverage/rust.lcov
flags: rust
verbose: true
# OIDC avoids long-lived CODECOV_TOKEN secrets.
- name: Clippy (Rust)
run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings

View File

@@ -1,71 +0,0 @@
name: Deploy docs
on:
push:
branches: [main]
paths:
- ".github/workflows/deploy-docs.yml"
- "package.json"
- "pnpm-lock.yaml"
- "scripts/build-agent-docs.mjs"
- "site/**"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
env:
NODE_OPTIONS: --max-old-space-size=4096
jobs:
build:
name: Build VitePress site
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build docs
run: pnpm docs:build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: site/.vitepress/dist
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -4,14 +4,10 @@ on:
push:
tags:
- 'stable-v*'
- 'v20*'
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
# The production Vite bundle can exceed Node's default ~2GB heap on
# macOS arm64 runners while Tauri runs beforeBuildCommand.
NODE_OPTIONS: --max-old-space-size=4096
concurrency:
group: release-stable-${{ github.ref }}
@@ -38,24 +34,14 @@ jobs:
from datetime import date
tag = os.environ["GITHUB_REF_NAME"]
legacy_match = re.fullmatch(r"stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})", tag)
date_match = re.fullmatch(r"v(\d{4})-(\d{2})-(\d{2})", tag)
if date_match:
year, month, day = map(int, date_match.groups())
date(year, month, day)
version = f"{year}.{month}.{day}"
display_version = tag
elif legacy_match:
year, month, day = map(int, legacy_match.groups())
date(year, month, day)
version = f"{year}.{month}.{day}"
display_version = version
else:
raise SystemExit(f"Stable tags must use vYYYY-MM-DD or stable-vYYYY.M.D, got {tag}")
version = tag.removeprefix("stable-v")
match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})", version)
if not match:
raise SystemExit(f"Stable tags must use stable-vYYYY.M.D, got {tag}")
date(*map(int, match.groups()))
print(f"version={version}")
print(f"display_version={display_version}")
print(f"display_version={version}")
print(f"tag={tag}")
PY
@@ -288,8 +274,7 @@ jobs:
wget \
patchelf \
build-essential \
file \
rpm
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
@@ -341,7 +326,7 @@ jobs:
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
- name: Validate Linux bundles
run: |
@@ -349,7 +334,6 @@ jobs:
installers=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
)
signatures=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
@@ -357,7 +341,7 @@ jobs:
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage, deb or rpm bundle."
echo "::error::Linux build produced no AppImage or deb bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
@@ -372,7 +356,6 @@ jobs:
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
@@ -415,16 +398,6 @@ jobs:
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Cache Tauri Windows tools
uses: actions/cache@v4
with:
path: ~\AppData\Local\tauri
key: ${{ runner.os }}-tauri-tools-nsis-3.11-nsis-tauri-utils-0.5.3
- name: Prefetch Tauri NSIS toolchain
shell: pwsh
run: ./.github/scripts/prefetch-tauri-nsis.ps1
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
@@ -579,23 +552,16 @@ jobs:
- name: Generate release notes
run: |
NOTES_FILE="release-notes/${{ needs.version.outputs.tag }}.md"
if [ -f "$NOTES_FILE" ]; then
cat "$NOTES_FILE" > release_notes.md
PREV_TAG=$(git tag --list 'stable-v*' --sort=-version:refname | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
PREV_TAG=$(git for-each-ref --sort=-creatordate --format='%(refname:short)' refs/tags/v20* refs/tags/stable-v* | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}")
fi
{
echo "## What's Changed"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
} > release_notes.md
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}")
fi
{
echo "## What's Changed"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Stable release — manually promoted from \`main\`**"
@@ -603,7 +569,7 @@ jobs:
echo "**Includes macOS (Apple Silicon and Intel), Windows x64, and Linux x64 bundles**"
echo ""
echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*"
} >> release_notes.md
} > release_notes.md
- name: Build stable-latest.json
run: |
@@ -698,14 +664,12 @@ jobs:
updater-x86_64/*.app.tar.gz.sig
linux-x86_64-bundles/*.deb
linux-x86_64-bundles/*.deb.sig
linux-x86_64-bundles/*.rpm
linux-x86_64-bundles/*.AppImage
linux-x86_64-bundles/*.AppImage.sig
linux-x86_64-bundles/*.AppImage.tar.gz
linux-x86_64-bundles/*.AppImage.tar.gz.sig
linux-x86_64-bundles/*/*.deb
linux-x86_64-bundles/*/*.deb.sig
linux-x86_64-bundles/*/*.rpm
linux-x86_64-bundles/*/*.AppImage
linux-x86_64-bundles/*/*.AppImage.sig
linux-x86_64-bundles/*/*.AppImage.tar.gz
@@ -763,9 +727,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VITEPRESS_BASE="/${GITHUB_REPOSITORY#*/}/" pnpm docs:build
mkdir -p _site/alpha _site/stable _site/release-notes
mkdir -p _site/alpha _site/stable
cp -R site/.vitepress/dist/. _site/
if [ -d release-notes ]; then cp release-notes/*.md _site/release-notes/ 2>/dev/null || true; fi
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"

View File

@@ -4,18 +4,10 @@ on:
push:
branches:
- main
paths-ignore:
- ".husky/**"
- ".github/workflows/deploy-docs.yml"
- ".github/workflows/release.yml"
- "site/**"
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
# The production Vite bundle can exceed Node's default ~2GB heap on
# macOS arm64 runners while Tauri runs beforeBuildCommand.
NODE_OPTIONS: --max-old-space-size=4096
concurrency:
group: release-alpha-${{ github.ref }}
@@ -77,18 +69,10 @@ jobs:
else:
today = datetime.now(timezone.utc).date()
stable_date = None
stable_patterns = (
re.compile(r"^v(\d{4})-(\d{2})-(\d{2})$"),
re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$"),
)
stable_pattern = re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$")
stable_tags = lines([
"git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)",
"refs/tags/v20*", "refs/tags/stable-v*",
])
for stable_tag in stable_tags:
match = next((pattern.fullmatch(stable_tag) for pattern in stable_patterns if pattern.fullmatch(stable_tag)), None)
for stable_tag in lines(["git", "tag", "--list", "stable-v*", "--sort=-version:refname"]):
match = stable_pattern.fullmatch(stable_tag)
if not match:
continue
@@ -344,8 +328,7 @@ jobs:
wget \
patchelf \
build-essential \
file \
rpm
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
@@ -397,7 +380,7 @@ jobs:
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
- name: Validate Linux bundles
run: |
@@ -405,7 +388,6 @@ jobs:
installers=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
)
signatures=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
@@ -413,7 +395,7 @@ jobs:
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage, deb or rpm bundle."
echo "::error::Linux build produced no AppImage or deb bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
@@ -428,7 +410,6 @@ jobs:
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
@@ -471,16 +452,6 @@ jobs:
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Cache Tauri Windows tools
uses: actions/cache@v4
with:
path: ~\AppData\Local\tauri
key: ${{ runner.os }}-tauri-tools-nsis-3.11-nsis-tauri-utils-0.5.3
- name: Prefetch Tauri NSIS toolchain
shell: pwsh
run: ./.github/scripts/prefetch-tauri-nsis.ps1
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
@@ -744,14 +715,12 @@ jobs:
updater-x86_64/*.app.tar.gz.sig
linux-x86_64-bundles/*.deb
linux-x86_64-bundles/*.deb.sig
linux-x86_64-bundles/*.rpm
linux-x86_64-bundles/*.AppImage
linux-x86_64-bundles/*.AppImage.sig
linux-x86_64-bundles/*.AppImage.tar.gz
linux-x86_64-bundles/*.AppImage.tar.gz.sig
linux-x86_64-bundles/*/*.deb
linux-x86_64-bundles/*/*.deb.sig
linux-x86_64-bundles/*/*.rpm
linux-x86_64-bundles/*/*.AppImage
linux-x86_64-bundles/*/*.AppImage.sig
linux-x86_64-bundles/*/*.AppImage.tar.gz
@@ -809,9 +778,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VITEPRESS_BASE="/${GITHUB_REPOSITORY#*/}/" pnpm docs:build
mkdir -p _site/alpha _site/stable _site/release-notes
mkdir -p _site/alpha _site/stable
cp -R site/.vitepress/dist/. _site/
if [ -d release-notes ]; then cp release-notes/*.md _site/release-notes/ 2>/dev/null || true; fi
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"

5
.gitignore vendored
View File

@@ -45,7 +45,7 @@ final_selection.py
src-tauri/target
# Generated mcp-server bundle (built by scripts/bundle-mcp-server.mjs)
src-tauri/resources/mcp-server/
src-tauri/resources/
# Python cache
__pycache__/
@@ -76,6 +76,3 @@ CODE-HEALTH-REPORT.md
.env
.env.local
.env.*.local
# Local Codacy CLI runtime/config generated by the MCP server
.codacy/

View File

@@ -40,36 +40,8 @@ ensure_node_tooling
echo "🔍 Pre-commit checks..."
STAGED_FILES=$(git diff --cached --name-only)
APP_CHANGED=false
SITE_CHANGED=false
for FILE in $STAGED_FILES; do
case "$FILE" in
site/*)
SITE_CHANGED=true
;;
.github/workflows/*|.husky/*|docs/*|*.md)
;;
*)
APP_CHANGED=true
;;
esac
done
if [ "$APP_CHANGED" = false ]; then
if [ "$SITE_CHANGED" = true ]; then
echo " → docs build..."
pnpm docs:build
else
echo " → app checks skipped (docs/workflow/hooks only)"
fi
echo "✅ Pre-commit passed"
exit 0
fi
# Lint + types (only if TS files staged)
STAGED_TS=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx)$' || true)
STAGED_TS=$(git diff --cached --name-only | grep -E '\.(ts|tsx)$' || true)
if [ -n "$STAGED_TS" ]; then
echo " → lint + tsc..."
pnpm lint --quiet

View File

@@ -95,43 +95,12 @@ echo "================================================"
# ── Detect what changed ─────────────────────────────────────────────────
PUSH_TARGET=$(git rev-parse @{push} 2>/dev/null || echo "")
RUST_CHANGED=true
APP_CHANGED=true
SITE_CHANGED=false
if [ -n "$PUSH_TARGET" ]; then
CHANGED=$(git diff --name-only "$PUSH_TARGET"..HEAD)
APP_CHANGED=false
if ! echo "$CHANGED" | grep -qE '^(src-tauri/|Cargo)'; then
RUST_CHANGED=false
fi
for FILE in $CHANGED; do
case "$FILE" in
site/*)
SITE_CHANGED=true
;;
.github/workflows/*|.husky/*|docs/*|*.md)
;;
*)
APP_CHANGED=true
;;
esac
done
fi
if [ "$APP_CHANGED" = false ]; then
if [ "$SITE_CHANGED" = true ]; then
echo ""
echo "📚 Docs-only push detected; running docs build..."
pnpm docs:build
echo " ✅ Docs build OK"
else
echo ""
echo "⏭️ App checks skipped (docs/workflow/hooks only)"
fi
ELAPSED=$(($(date +%s) - START_TIME))
echo ""
echo "✅ Pre-push passed in ${ELAPSED}s"
exit 0
fi
# ── 0. TypeScript + Vite build ──────────────────────────────────────────

View File

@@ -22,7 +22,7 @@ Run `/laputa-next-task` — fetches next task (To Rework first, then Open), move
- Work on `main` branch — **no branches, no PRs, ever**. Pre-commit and pre-push block work from any other branch.
- Commit every 2030 min: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`
- **⛔ NEVER use --no-verify**
- For UI tasks: open `ui-design.pen` first, study visual language, design in light mode. You don't need Pencil to use it you can open it as a JSON file.
- For UI tasks: open `ui-design.pen` first, study visual language, design in light mode
### 1c. When done
@@ -47,14 +47,16 @@ bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/qa-native.png
Use `osascript` for keyboard interactions. Write result as Todoist comment (✅ or ❌). **⚠️ WKWebView:** `osascript keystroke` blocked inside editor — rely on Playwright for text input features.
After both phases pass, add a **completion comment** to the Todoist task. The comment must include:
- What was implemented (a few lines covering logic and UX/UI)
After both phases pass, add a **completion comment** to the Todoist task before running `/laputa-done`. The comment must include:
- What was implemented (12 lines)
- QA: what was tested and how (Playwright / native screenshot / osascript)
- Refactoring: any files refactored to meet the CodeScene gate (or "none needed")
- ADRs: any new/updated ADRs (or "none")
- Docs: any updated docs (ARCHITECTURE.md, ABSTRACTIONS.md, etc.) (or "none")
- Code health: final Hotspot and Average scores after push
Then run `/laputa-done <task_id>` → moves to In Review, notifies Brian, self-dispatches next task.
---
## 2. Development Process
@@ -71,22 +73,6 @@ Red → Green → Refactor → Commit. One cycle per commit. For bugs: write fai
**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests first. Prefer E2E over unit tests for user flows.
### Localization (mandatory for UI copy)
All user-facing UI labels/copy must live in `src/lib/locales/en.json` and be translated into every target listed in `lara.yaml`. When adding or changing interface copy:
```bash
pnpm l10n:translate
```
Use `pnpm l10n:translate:force` only when intentionally regenerating existing translations. Commit `src/lib/locales/*.json`, `lara.yaml`/`lara.lock` changes if produced, and verify placeholders/product names stayed intact.
### Product analytics (mandatory for meaningful features)
New features should almost always emit a PostHog event so we can see whether users actually discover and use them. Skip instrumentation only for very small changes where a dedicated event would create noise. Use clear, stable event names, avoid PII or note content, and include only safe metadata that helps evaluate adoption and failures.
When adding or changing a meaningful user-facing feature, include the event name(s) in the Todoist completion comment alongside QA, docs, and code health. If intentionally not instrumenting a feature, explain why in the completion comment.
### Code health (mandatory)
Pre-commit and pre-push hooks enforce **Hotspot Code Health** and **Average Code Health** ≥ thresholds in `.codescene-thresholds`. Both gates block commit/push. Thresholds are a **ratchet** — only go up. When pre-push sees improved remote scores, it updates `.codescene-thresholds`, stages it, and stops so you can commit the new floor with normal verified hooks before pushing again. Never add `// eslint-disable`, `#[allow(...)]`, or `as any`.
@@ -136,6 +122,10 @@ Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing:
- **Delete all test notes from disk** when done — do not leave untitled or temporary notes on the filesystem. Run `cd ~/Laputa && git checkout -- . && git clean -fd` to restore the vault to its last committed state.
- **Rationale:** test notes pollute the local vault over time, making it a collection of nonsensical untitled files. The vault must stay clean on disk, not just on the remote.
### UI design
Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task; on completion merge into `ui-design.pen` and delete it.
### UI components — mandatory rules
**Always use shadcn/ui components.** Never use raw HTML form elements (`<input>`, `<select>`, `<button>`, native `<input type="date">`, etc.) for user-facing UI. Every interactive element must use the shadcn/ui equivalent:
@@ -165,11 +155,10 @@ Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing:
- Tauri menu accelerators: `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")`
- `app.set_menu()` replaces the ENTIRE menu bar — include all submenus
- `mock-tauri.ts` silently swallows Tauri calls — not a substitute for native testing
### QA scripts
```bash
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh Tolaria
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/tolaria-qa/scripts/shortcut.sh "command" "s"
```

View File

@@ -2,7 +2,7 @@
# 💧 Tolaria
Tolaria is a desktop app for macOS, Windows, and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases:
Tolaria is a desktop app for Mac and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases:
* Operate second brains and personal knowledge
* Organize company docs as context for AI
@@ -27,29 +27,17 @@ You can find some Loom walkthroughs below — they are short and to the point:
- 🔬 **Open source** — Tolaria is free and open source. I built this for [myself](https://x.com/lucaronin) and for sharing it with others.
- 📋 **Standards-based** — Notes are markdown files with YAML frontmatter. No proprietary formats, no locked-in data. Everything works with standard tools if you decide to move away from Tolaria.
- 🔍 **Types as lenses, not schemas** — Types in Tolaria are navigation aids, not enforcement mechanisms. There's no required fields, no validation, just helpful categories for finding notes.
- 🪄**AI-first but not AI-only** — A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code, Codex CLI, and Gemini CLI setup paths, but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out.
- 🪄**AI-first but not AI-only** — A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code and Codex CLI (for now), but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out.
- ⌨️ **Keyboard-first** — Tolaria is designed for power-users who want to use keyboard as much as possible. A lot of how we designed the Editor and the Command Palette is based on this.
- 💪 **Built from real use** — Tolaria was created for manage my personal vault of 10,000+ notes, and I use it every day. Every feature exists because it solved a real problem.
## Installation
### Homebrew
Install via Homebrew on macOS:
```batch
brew install --cask tolaria
```
### Download from releases
Download the [latest release here](https://refactoringhq.github.io/tolaria/download/) for macOS, Windows, or Linux.
## Getting started
Download the [latest release here](https://refactoringhq.github.io/tolaria/download/).
When you open Tolaria for the first time you get the chance of cloning the [getting started vault](https://github.com/refactoringhq/tolaria-getting-started) — which gives you a walkthrough of the whole app.
The public user docs live in [`site/`](site/) and are published to GitHub Pages. Start with [Install Tolaria](site/start/install.md), then [First Launch](site/start/first-launch.md).
The public user docs live in [`site/`](site/) and are intended for GitHub Pages. Start with [Install Tolaria](site/start/install.md), then [First Launch](site/start/first-launch.md).
## Open source and local setup

View File

@@ -49,13 +49,7 @@
"laputa-qa-reference.md",
"attachments/laputa-reference.png"
]
},
{
"id": "rtl-mixed-direction",
"reason": "Arabic and mixed English/Arabic paragraphs keep rich editor and raw editor BiDi QA anchored to the fixture.",
"files": [
"rtl-mixed-direction-qa.md"
]
}
]
}

View File

@@ -1,17 +0,0 @@
---
type: Note
topics:
- "[[topic-writing]]"
---
# RTL Mixed Direction QA
مرحبا بالعالم. هذه فقرة عربية لاختبار اتجاه النص من اليمين إلى اليسار داخل محرر تولاريا.
English text should keep reading left to right when it appears next to Arabic content.
English then مرحبا بالعالم keeps both scripts readable on one line.
مرحبا بالعالم then English keeps the Arabic run anchored correctly while preserving the English words.
Use this note when checking rich editor and raw Markdown editor behavior for automatic LTR/RTL direction.

View File

@@ -4,7 +4,7 @@ Key abstractions and domain models in Tolaria.
## Design Philosophy
Tolaria's abstractions follow the **convention over configuration** principle: standard field names, types, and relationships 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.
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).
@@ -47,7 +47,6 @@ _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
_width: wide # rich-editor width override for this note
```
**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.
@@ -88,7 +87,6 @@ classDiagram
+Record~string,string[]~ relationships
+String[] outgoingLinks
+String? status
+String? noteWidth
+Number? modifiedAt
+Number? createdAt
+Number wordCount
@@ -137,7 +135,6 @@ interface VaultEntry {
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
noteWidth?: 'normal' | 'wide' | null // Rich-editor width mode from `_width`
modifiedAt: number | null // Unix timestamp (seconds)
// Note: owner and cadence are now in the generic `properties` map
createdAt: number | null // Unix timestamp (seconds)
@@ -160,19 +157,9 @@ interface VaultEntry {
|---|---|---|
| `markdown` or absent | `.md`, `.markdown` | Full Tolaria note model: frontmatter, BlockNote, raw editor, relationships, title sync |
| `text` | UTF-8 editable formats such as `.yml`, `.json`, `.ts`, `.py`, `.sh` | Opens through the raw editor without Markdown note semantics |
| `binary` | Images, audio, video, PDFs, archives, other non-text files | Stays a normal vault file; previewable media and PDFs open in `FilePreview`, unsupported or broken binaries show an explicit fallback |
| `binary` | Images, PDFs, archives, other non-text files | Stays a normal vault file; previewable images open in `FilePreview`, unsupported or broken binaries show an explicit fallback |
Asset previewability is inferred in the renderer from the filename extension (`src/utils/filePreview.ts`) rather than stored as a new persisted kind. Supported images render through `<img>`, supported audio/video render through native HTML media controls, and supported PDFs render through the webview's PDF object renderer, all backed by Tauri asset URLs. Runtime asset access is accumulated only for vault roots Tolaria has loaded in the current app session, because Tauri directory forbids cannot be safely reversed after a vault switch. The "open in default app" action re-enters the active-vault command boundary through `open_vault_file_external` before delegating to the native opener. This keeps the filesystem as source of truth and avoids converting assets into proprietary objects.
### Note Content Freshness
The renderer may cache recently opened or preloaded markdown content, but cached content is only a performance hint. `useTabManagement` can reuse cached text immediately when it carries the same `modifiedAt` and `fileSize` identity as the current `VaultEntry`; otherwise it validates the cached string with the `validate_note_content` Tauri command. That command re-enters the same vault path boundary checks as `get_note_content` and compares the cached text against the current on-disk file bytes. A mismatch, missing file, or unreadable file falls back to the normal fresh-read path and existing missing/unreadable recovery. Background note prefetch is bounded to a small number of concurrent native reads, and a note opened while queued is promoted to foreground instead of waiting behind the prefetch backlog. Note-open entry objects are re-normalized at the tab boundary, so transient reload or bridge payloads with missing display metadata fall back to filename/title defaults before editor chrome renders; entries without a usable path are ignored instead of opening a broken tab.
`useEditorTabSwap` may reuse BlockNote blocks that were already opened successfully or warmed from prefetched raw content, keyed by vault, path, and exact source content. Background warming is limited to likely next large Markdown notes and defers while the editor is unmounted, raw mode is active, or recent typing/navigation is still inside the foreground idle window. Every async editor swap carries a generation and source-content token so stale conversion results cannot overwrite newer file content or dirty editor state.
### Table of Contents Outline
The editor Table of Contents is derived from the live BlockNote document, not from saved Markdown text. `src/utils/tableOfContents.ts` reads structural `heading` blocks with stable ids and levels, extracts inline text from nested BlockNote content, and nests headings by level while preserving document order. `TableOfContentsPanel` receives a document revision from `Editor`, so rich-editor edits refresh the outline immediately without waiting for autosave or a vault reload. Selecting a heading focuses BlockNote and moves the cursor to that block id, while nested headings can be collapsed independently in panel-local UI state.
Image previewability is inferred in the renderer from the filename extension (`src/utils/filePreview.ts`) rather than stored as a new persisted kind. This keeps the filesystem as source of truth and avoids converting assets into proprietary objects.
### Entity Types (isA / type)
@@ -188,23 +175,20 @@ Type is determined **purely** from the `type:` frontmatter field — it is never
├── some-topic.md ← type: Topic
├── AGENTS.md ← canonical Tolaria AI guidance
├── CLAUDE.md ← compatibility shim pointing at AGENTS.md
├── GEMINI.md ← optional Gemini CLI shim pointing at AGENTS.md
├── project.md ← type: Type (definition document)
├── person.md ← type: Type (definition document)
├── ...
└── 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. Moving a note into a user folder is a separate filesystem concern: the folder path changes, but the note keeps the same filename and `type:` value. Legacy `type/` and `types/` folders are still scanned like other non-hidden vault folders, so existing type documents in those folders continue to work, but new type documents created by Tolaria are written at the vault root. Legacy `config/` content is still recognized during migration and repair, but Tolaria's managed AI guidance now lives at the vault root.
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. Moving a note into a user folder is a separate filesystem concern: the folder path changes, but the note keeps the same filename and `type:` value. 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**: any markdown note with `type: Type` in its frontmatter. Tolaria creates new type documents at the vault root (e.g., `project.md`, `person.md`) and still reads existing type documents from subfolders. Type documents:
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
- Define instance schema/defaults through ordinary custom frontmatter properties and relationship fields
- 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
@@ -213,7 +197,7 @@ Each entity type can have a corresponding **type document**: any markdown note w
| Property | Type | Description |
|----------|------|-------------|
| `icon` | string | Type icon as a Phosphor name (kebab-case, e.g., "cooking-pot") |
| `color` | string | Accent palette key (`red`, `purple`, `blue`, `green`, `yellow`, `orange`, `teal`, `pink`, `gray`) or a valid CSS color value such as `cyan`, `#22d3ee`, or `rgb(34, 211, 238)` |
| `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 |
@@ -221,9 +205,7 @@ Each entity type can have a corresponding **type document**: any markdown note w
| `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 `[[project]]`. This makes the type navigable from the Inspector panel while keeping location as an implementation detail.
**Instance schema/defaults**: Custom scalar properties and relationship fields on a type document define the expected shape for notes of that type. Existing instances do not get mutated when a type changes; the Inspector enriches their real frontmatter with gray placeholders for missing type-defined properties/relationships. Valued type fields are copied into frontmatter only when Tolaria creates a new instance of that type. Blank type fields stay as placeholders.
**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
@@ -285,9 +267,8 @@ Tolaria separates **display title** from the file identifier:
- **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 stage crash-safe note renames through a hidden `.tolaria-rename-txn/` transaction directory, recover unfinished renames on the next vault scan, update wikilinks across the vault, and surface any failed backlink rewrites instead of silently reporting partial success. The editor body remains the title editing surface.
- **Unicode-aware note stems** (`src/utils/noteSlug.ts`, `vault/rename.rs`): frontend and backend slugging preserve Unicode letters/digits in note filenames, untitled-rename detection, and fallback wikilink targets while still collapsing symbol-only titles to `untitled`.
- **Path identity rules** (`src/utils/notePathIdentity.ts`, `vault/path_identity.rs`): note creation, tab selection, rename bookkeeping, pull refresh, git history, and vault cache updates normalize path separators and macOS `/private/tmp` aliases through one owner. Case folding is reserved for collision/deduplication checks; active-note identity remains case-sensitive.
- **Portable filename validation** (`vault/filename_rules.rs`): note filenames, folder names, and custom view filenames all reject Windows-reserved device names, invalid characters, and trailing dot/space suffixes so a vault created on macOS/Linux still clones and syncs cleanly on Windows.
- **Recoverable save failures** (`useEditorSave`, `vault/file.rs`): invalid platform path syntax is reported as a clear retryable save error, while transient access-denied writes are retried briefly before surfacing failure. The editor keeps the unsaved buffer intact for another attempt.
- **Recoverable save failures** (`useEditorSave`, `vault/file.rs`): invalid platform path syntax is reported as a clear retryable save error, while the editor keeps the unsaved buffer intact for another attempt.
- **Untitled drafts** start as `untitled-*.md` and are auto-renamed on save once the note gains an H1.
### Title Surface (UI)
@@ -309,26 +290,19 @@ type SidebarFilter = 'all' | 'archived' | 'changes' | 'pulse'
type SidebarSelection =
| { kind: 'filter'; filter: SidebarFilter }
| { kind: 'sectionGroup'; type: string } // e.g. type: 'Project'
| { kind: 'folder'; path: string; rootPath?: string }
| { kind: 'folder'; path: string }
| { kind: 'entity'; entry: VaultEntry } // Neighborhood source note
| { kind: 'view'; filename: string }
```
`SidebarSelection.kind === 'folder'` is a first-class navigation target, not just a visual highlight.
- `FolderTree` keeps the folder interaction surface decomposed into `FolderTreeRow`, `FolderNameInput`, `FolderContextMenu`, and disclosure/context-menu hooks so nested row rendering, inline rename, and right-click actions stay isolated. The UI wraps backend folder nodes in a synthetic vault-root row with `path: ""` and `rootPath` set to the opened vault so root-level files can be listed without turning the vault root into a mutable folder. Non-mutating reveal/copy-path menu items stay callback-driven from `App` so filesystem convenience actions do not leak into folder mutation hooks.
- `src/components/sidebar/sidebarHooks.ts` owns the shared sidebar interaction primitives for menu positioning/dismissal and inline rename input behavior. Folder, Type, and saved View rows keep their domain-specific actions local, but use those primitives so right-click menus and rename fields have the same outside-click, Escape, focus, blur, and submit semantics.
- `FolderTree` keeps the folder interaction surface decomposed into `FolderTreeRow`, `FolderNameInput`, `FolderContextMenu`, and disclosure/context-menu hooks so nested row rendering, inline rename, and right-click actions stay isolated. Non-mutating reveal/copy-path menu items stay callback-driven from `App` so filesystem convenience actions do not leak into folder mutation hooks.
- `useFolderActions()` composes `useFolderRename()` and `useFolderDelete()` to keep folder mutations selection-aware while the rest of `App.tsx` only wires the resulting callbacks into `Sidebar` and the command registry.
- `useNoteRetargeting()` is the shared retargeting abstraction for note drops and command-palette actions. It owns the "can drop here?" checks, updates `type:` via frontmatter when a note lands on a type section, and delegates folder moves through the same crash-safe rename pipeline used by the backend rename commands.
- A successful folder rename reloads the folder tree plus vault entries, rewrites any affected folder-scoped tabs, and updates `SidebarSelection` to the new relative path when the renamed folder stays selected.
- Folder deletion clears pending rename state, confirms destructive intent, drops affected folder-scoped tabs, reloads vault data, and resets folder selection if the deleted subtree owned the current selection.
### Saved Views
Saved Views live as YAML files under `views/`. Their definition includes user-visible fields (`name`, `icon`, `color`), note-list preferences (`sort`, `listPropertiesDisplay`), filters, and an optional top-level `order` number. The `order` value is stored directly in the YAML document, not in Markdown frontmatter, and lower values render earlier in every saved-View list. Views without an explicit order sort after ordered views by filename for stable fallback behavior.
The renderer uses `viewOrdering` helpers to convert drag or command-palette move intent into dense order updates before saving each affected view file through `save_view_cmd`. The sidebar treats saved View rows like Type rows for direct customization: double-click starts inline rename, right-click opens edit/rename/icon-color/delete actions, and keyboard users can open that same menu from the focused row while command-palette actions remain responsible for saved View ordering.
### Neighborhood Mode
`SidebarSelection.kind === 'entity'` is Tolaria's Neighborhood mode for note-list browsing.
@@ -342,12 +316,6 @@ The renderer uses `viewOrdering` helpers to convert drag or command-palette move
- Plain click / `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.
## Command Surface
`src/shared/appCommandManifest.json` is the cross-runtime source for stable app command IDs, menu structure, display labels, accelerators, deterministic shortcut QA metadata, and native menu enablement groups. The renderer imports it through `src/hooks/appCommandCatalog.ts`, which derives `APP_COMMAND_IDS`, shortcut lookup maps, Linux titlebar menu sections, native-menu command membership, and test helpers. Tauri includes the same JSON in `src-tauri/src/menu.rs` and uses it to build custom menu items, emit overridden menu item IDs such as the quick-open alias as their primary command IDs, register the Windows main-window menu event bridge, and toggle state-dependent menu items from manifest groups.
Domain command builders still own context-sensitive command-palette entries, availability, and execution callbacks. The manifest owns metadata that must stay identical across native menus, renderer shortcuts, deterministic QA bridges, and the Linux fallback menu; OS-native menu items such as Undo, Copy/Paste, Services, Quit, and Window controls remain local to the native menu implementation.
## File System Integration
### Vault Scanning (Rust)
@@ -367,21 +335,17 @@ Domain command builders still own context-sensitive command-palette entries, ava
5. Sorts by `modified_at` descending
6. Skips unparseable files with a warning log
All Notes starts from Markdown notes and excludes Markdown files under `attachments/`. `src/utils/allNotesFileVisibility.ts` resolves the installation-local PDF, image, and unsupported-file toggles from app settings; `noteListHelpers` applies that policy only to All Notes filtering and counts. Folder/root browsing continues to show files from the selected folder independently of those All Notes toggles.
The folder tree hides only the dedicated `type/` directory, since note types already have their own sidebar section. Default vault folders such as `attachments/` and `views/` remain visible alongside user-created folders.
The folder tree hides the legacy `type/` directory, since those type documents already appear through the Types sidebar section. Default vault folders such as `attachments/` and `views/` remain visible alongside user-created folders under the synthetic vault-root row.
Command-facing vault content is filtered through `vault::filter_gitignored_entries`, `vault::filter_gitignored_folders`, and `vault::filter_gitignored_paths` when the app setting `hide_gitignored_files` is enabled. The cache still stores the complete scan; `list_vault`, `reload_vault`, `list_vault_folders`, and search apply the visibility filter at the boundary before React consumes entries. The filter batches paths through `git check-ignore --no-index --stdin`, drains stdout while stdin is still being written, and short-circuits root `.gitignore` detection before walking for nested ignore files, so large ignored folder sets cannot deadlock the native UI while preserving Git semantics as closely as the app can reasonably support.
Command-facing vault content is filtered through `vault::filter_gitignored_entries`, `vault::filter_gitignored_folders`, and `vault::filter_gitignored_paths` when the app setting `hide_gitignored_files` is enabled. The cache still stores the complete scan; `list_vault`, `reload_vault`, `list_vault_folders`, and search apply the visibility filter at the boundary before React consumes entries. The filter batches paths through `git check-ignore --no-index --stdin`, so negated and specific `.gitignore` patterns follow Git semantics as closely as the app can reasonably support.
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`.
Command-layer path access is fenced to the active vault before file operations reach the vault backend. `src-tauri/src/commands/vault/boundary.rs` canonicalizes the configured/requested vault root, rejects `..` escapes and absolute paths outside that root, and validates writable targets through the nearest existing ancestor so note reads, saves, deletes, view-file edits, folder mutations, and image attachment writes cannot step outside the active vault. If the active root itself cannot be canonicalized, the renderer treats `Active vault is not available` the same as no active vault: it clears stale vault state, drops prefetched note content, and shows the missing-vault recovery screen instead of continuing note/view requests against the disappeared path. Image attachment commands add the current vault root to the runtime asset scope after saving so files created under a previously missing `attachments/` directory can render immediately.
Command-layer path access is fenced to the active vault before file operations reach the vault backend. `src-tauri/src/commands/vault/boundary.rs` canonicalizes the configured/requested vault root, rejects `..` escapes and absolute paths outside that root, and validates writable targets through the nearest existing ancestor so note reads, saves, deletes, view-file edits, folder mutations, and image attachment writes cannot step outside the active vault. Image attachment commands refresh the runtime asset scope after saving so files created under a previously missing `attachments/` directory can render immediately.
Renderer attachment paths are normalized through `src/utils/vaultAttachments.ts`. That module is the single owner for converting between portable markdown references such as `attachments/image.png`, Tauri asset URLs, and absolute active-vault filesystem paths. Editor markdown rendering, raw-mode serialization, image upload/drop handling, file-block open actions, and parsed image cleanup all call this primitive instead of carrying their own asset URL prefixes, Windows path normalization, or `attachments/` join rules.
UI-only file actions operate on paths that are already selected or indexed in React state. Reveal-in-Finder and external-open calls route through the Tauri opener plugin, while copy-path uses the browser clipboard API; none of those actions mutate vault contents or bypass the backend write boundary.
UI-only file actions operate on paths that are already selected or indexed in React state. Reveal-in-Finder routes through the Tauri opener plugin, external-open routes through the `open_vault_file_external` command and active-vault boundary before invoking the native opener, and copy-path uses the browser clipboard API. Plain-text paste reads the desktop clipboard through `read_text_from_clipboard` in Tauri so macOS WKWebView clipboard permissions do not block the command; browser/mock mode falls back to the Web Clipboard API or mock handlers. None of those actions mutate vault contents or bypass the backend write boundary.
The local MCP WebSocket bridge follows the same active-vault boundary. `useVaultSwitcher` calls `sync_mcp_bridge_vault` after the persisted selection loads and after each vault switch; the desktop command starts/restarts the bridge with that vault's canonical path, or stops it when there is no selected vault. App exit uses the same child cleanup path and waits for the bridge process after killing it. MCP Node entrypoints require `VAULT_PATH` and fail clearly instead of falling back to `~/Laputa`. Manual MCP config export uses the same generated stdio entry as registration, so the copied snippet remains scoped to the active vault without writing third-party config files. Desktop snippet copy goes through the native `copy_text_to_clipboard` command, while browser/mock mode keeps using the Web Clipboard API. External-client stdio MCP processes also exit when stdin closes; their UI-bridge reconnect timers and WebSocket are canceled during shutdown so disconnected clients do not leave extra Node processes behind.
The local MCP WebSocket bridge follows the same active-vault boundary. `useVaultSwitcher` calls `sync_mcp_bridge_vault` after the persisted selection loads and after each vault switch; the desktop command starts/restarts the bridge with that vault's canonical path, or stops it when there is no selected vault. MCP Node entrypoints require `VAULT_PATH` and fail clearly instead of falling back to `~/Laputa`.
### Vault Caching
@@ -415,7 +379,7 @@ The `with_frontmatter()` helper wraps this in a read-transform-write cycle on th
## Git Integration
Git operations live in `src-tauri/src/git/`. All operations shell out to the `git` CLI (not libgit2). Path-producing commands use `core.quotePath=false` so Unicode note filenames stay as UTF-8 paths across status, history, cache invalidation, and rename detection.
Git operations live in `src-tauri/src/git/`. All operations shell out to the `git` CLI (not libgit2).
### Data Types
@@ -478,7 +442,7 @@ interface PulseCommit {
- Configurable interval (from app settings: `auto_pull_interval_minutes`)
- Pulls on interval, pushes after commits
- Awaits the post-pull vault refresh so toasts land after note-list state is fresh
- Reopens the clean active tab from disk only when the pull changed that active note, so unrelated updates do not remount the editor
- Reopens the clean active tab from disk after a successful pull update so the editor and note list stay aligned
- Detects merge conflicts → opens `ConflictResolverModal`
- Tracks remote status (branch, ahead/behind via `git_remote_status`)
- Handles push rejection (divergence) → sets `pull_required` status
@@ -487,7 +451,7 @@ interface PulseCommit {
### External Vault Refresh
External vault mutations are any disk writes Tolaria did not just perform through its own save path: Git pulls, AI-agent writes, filesystem watcher events, and edits from another app. These changes must route through `refreshPulledVaultState()` rather than calling `reloadVault()` in isolation. The shared refresh abstraction reloads entries, folders, and saved views together, preserves unsaved active-editor content, reopens a clean active note only when the changed-path list includes that note, and closes the active tab if the file disappeared. Unknown or unrelated watcher updates refresh vault-derived state without remounting the active editor. `useVaultWatcher` supplies changed filesystem paths to this abstraction after debouncing and after filtering recent app-owned saves. Overlapping entry reloads and modified-file polls are coalesced with a single trailing rerun so watcher and sync bursts do not stack native vault scans or Git status processes.
External vault mutations are any disk writes Tolaria did not just perform through its own save path: Git pulls, AI-agent writes, filesystem watcher events, and edits from another app. These changes must route through `refreshPulledVaultState()` rather than calling `reloadVault()` in isolation. The shared refresh abstraction reloads entries, folders, and saved views together, preserves unsaved active-editor content, reopens a clean active note from disk, and closes the active tab if the file disappeared. `useVaultWatcher` supplies changed filesystem paths to this abstraction after debouncing and after filtering recent app-owned saves.
`useGitRemoteStatus` is the commit-time companion to `useAutoSync`:
- Re-checks `git_remote_status` when the Commit dialog opens and right before submit
@@ -542,8 +506,8 @@ Defined in `src/components/editorSchema.tsx` and styled in `src/components/Edito
- 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 at creation time. Parsed unlabeled code blocks then run through Tolaria's lightweight language inference, while explicit fence languages and user dropdown choices still win.
- Inline-code chip styling remains scoped to `.bn-inline-content code`, so fenced `pre > code` nodes keep the dedicated code-block shell instead of inheriting the muted inline surface.
- 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 Math
@@ -556,42 +520,23 @@ Defined in `src/utils/mathMarkdown.ts`, `src/components/editorSchema.tsx`, and s
### Mermaid Diagrams
Defined in `src/utils/durableMarkdownBlocks.ts`, `src/utils/editorDurableMarkdown.ts`, `src/utils/mermaidMarkdown.ts`, `src/components/MermaidDiagram.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`:
Defined in `src/utils/mermaidMarkdown.ts`, `src/components/MermaidDiagram.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`:
- Fenced `mermaid` blocks become `mermaidBlock` schema nodes before BlockNote sees the Markdown body.
- Each `mermaidBlock` stores the original fenced Markdown plus the diagram body, so raw-mode entry and saves can restore the canonical source instead of serializing generated SVG.
- The rich editor renders diagrams with the `mermaid` package and uses the original source as an inline fallback when rendering fails.
- `serializeDurableEditorBlocks()` wraps the math-aware serializer so math, wikilinks, Mermaid diagrams, and whiteboards share the same Markdown-first save path.
- The `/mermaid` slash command inserts a placeholder rectangle diagram using the same schema-backed Markdown storage path, avoiding an invalid empty diagram state.
### Tldraw Whiteboards
Defined in `src/utils/durableMarkdownBlocks.ts`, `src/utils/editorDurableMarkdown.ts`, `src/utils/tldrawMarkdown.ts`, `src/components/TldrawWhiteboard.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`:
- Fenced `tldraw` blocks become `tldrawBlock` schema nodes before BlockNote sees the Markdown body.
- Each `tldrawBlock` stores a stable `boardId` plus the tldraw document snapshot JSON. Session state such as camera, selected tool, and current selection is not persisted into the note.
- The rich editor renders the block with the `tldraw` package and saves debounced document snapshot changes back into the block props, so normal Tolaria autosave writes the board into the `.md` file.
- Whiteboard prop writes re-resolve the live BlockNote block by id before mutating it, and disappear as no-ops if a note reload or mode switch has already removed that block.
- Mermaid and tldraw both register small codecs with the shared durable fenced-block pipeline; scanner, token, block injection, and mixed serialization mechanics live in one owner.
- The `/whiteboard` slash command inserts an empty tldraw block using the same Markdown-durable storage path. Preview images are intentionally omitted; thumbnails can be added later as derived cache artifacts.
- `serializeMermaidAwareBlocks()` wraps the math-aware serializer so math, wikilinks, and diagrams share the same Markdown-first save path.
### Formatting Surface Policy
Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tolariaEditorFormattingConfig.ts`:
- `SingleEditorView` disables BlockNote's default formatting toolbar, `/` menu, and side menu, then mounts Tolaria-owned controllers so the visible formatting surface matches Tolaria's markdown round-trip guarantees.
- `SingleEditorView` owns a whitespace mouse-selection bridge around BlockNote and its rich-editor scroll area: drag starts that land outside the editable text DOM are remapped through the ProseMirror view with clamped coordinates, while drags below the rendered document fall back to the document end. Drags that begin inside BlockNote's contenteditable surface, toolbars, side menu, dialogs, or non-primary mouse buttons stay on BlockNote/native handling.
- 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.
- Tolaria's formatting-toolbar controller also keeps file/image actions mounted across the tiny hover gap between an image block and the floating toolbar, and while the toolbar itself is hovered, so image controls remain usable instead of collapsing mid-interaction.
- `useEditorComposing` tracks editor-owned IME composition events and closes the floating formatting toolbar during composition plus a short post-composition settle window, keeping CJK candidate windows unobstructed without changing normal selection toolbar behavior.
- `createImeCompositionKeyGuardExtension()` intercepts composing `Enter` keydown events before BlockNote's list shortcuts see them, so Korean/Japanese/Chinese IMEs can commit text at the start of list items without Tolaria splitting the current bullet. It stops editor shortcut propagation only; it does not prevent the browser/IME default composition action.
- `useImageLightbox` listens for `dblclick` on the rich-editor container and opens `ImageLightbox` only when the event target resolves to a viewable BlockNote image. The target resolver handles media wrappers, ignores image captions/resize controls, missing sources, and tiny tracking-style images, preserving BlockNote's ordinary single-click image selection path.
- The `/` slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, list blocks, Mermaid diagrams, and whiteboards. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model.
- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface. Tolaria renders the add-block button outside the drag handle so the handle stays next to the block content. The side menu aligns itself to the first rendered text line for the hovered block, so H1/H2 typography, line-height, wrapping, and theme changes do not need per-heading offsets. Block reordering uses a Tolaria-owned pointer gesture and direct BlockNote block moves instead of HTML5 `DataTransfer`, keeping it independent from Tauri's native file-drop system. Block-handle actions re-resolve the current live BlockNote block before mutating or dragging, so note reloads and sync churn cannot leave controls acting on stale block references.
- BlockNote's table row/column handles are patched so stale or missing hovered-table state cancels the drag and hides handles instead of throwing. Add/remove row and column actions also validate the table position and cell indexes before resolving a ProseMirror `CellSelection`, so reloads or menu lag cannot turn stale handles into invalid table-selection positions. Checklist checkbox handlers also re-resolve the live block before updating `checked`, making delayed clicks after note reloads a no-op instead of a stale block mutation. Browser and native table regressions should exercise row and column dragging plus add-menu actions because the state is tracked per orientation.
- `SingleEditorView` wraps the BlockNote surface in a narrow render-recovery boundary for BlockNote's transient `Block doesn't have id` node-view failure. The boundary retries the BlockNote view once, records `editor_render_recovered`, and marks the recovered error so the React root handler does not send that handled case back to Sentry. Other render errors still propagate through the normal root error path.
- 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.
- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface.
- `useNoteWikilinkDrop()` is the shared editor-drop abstraction for dragging note rows into either editor mode. It reads the existing note-retargeting drag payload, resolves the vault-relative stem, and inserts a canonical `[[wikilink]]` without hijacking unrelated plain-text drags.
- `plainTextPaste.ts` is the shared plain-text paste target registry. Rich BlockNote and raw CodeMirror surfaces register focused insertion targets, while ordinary focused text controls use DOM selection replacement, so the `Cmd+Shift+V` command can preserve caret/selection behavior without each surface inventing its own clipboard reader.
- `useTauriDragDropEvent()` owns the shared Tauri window drag/drop subscription and duplicate-unlisten cleanup used by native drop features.
- `useNativePathDrop()` is the shared Tauri file/folder-drop abstraction for text inputs that need filesystem paths instead of attachment import. It consumes native window drag/drop events, gates them to the target element bounds or focused text selection, and lets AI composer / command-palette inputs insert formatted paths at the current cursor.
@@ -600,25 +545,25 @@ Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tola
```mermaid
flowchart LR
A["📄 Raw markdown\n(from disk)"] --> B["splitFrontmatter()\n→ yaml + body"]
B --> C["preProcessDurableEditorMarkdown(body)\nmermaid/tldraw fences → tokens"]
B --> C["preProcessMermaidMarkdown(body)\nmermaid fence → token"]
C --> D["preProcessWikilinks(body)\n[[target]] → token"]
D --> E["preProcessMathMarkdown(body)\n$...$ / $$...$$ → tokens"]
E --> F["tryParseMarkdownToBlocks()\n→ BlockNote block tree"]
F --> G["injectWikilinks + injectMathInBlocks + injectDurableEditorMarkdownBlocks\n tokens → schema nodes"]
F --> G["injectWikilinks + injectMathInBlocks + injectMermaidInBlocks\n tokens → schema nodes"]
G --> H["editor.replaceBlocks()\n→ rendered editor"]
style A fill:#f8f9fa,stroke:#6c757d,color:#000
style H fill:#d4edda,stroke:#28a745,color:#000
```
> Wikilink placeholder tokens use `\u2039` and `\u203A`; math, Mermaid, and tldraw placeholder tokens use ASCII sentinels with URI-encoded payloads.
> Wikilink placeholder tokens use `\u2039` and `\u203A`; math and Mermaid placeholder tokens use ASCII sentinels with URI-encoded payloads.
### BlockNote-to-Markdown Pipeline (Save)
```mermaid
flowchart LR
A["✏️ BlockNote blocks\n(editor state)"] --> B["blocksToMarkdownLossy()"]
B --> C["restoreWikilinks + serializeDurableEditorBlocks()\nschema nodes → Markdown source"]
B --> C["restoreWikilinks + serializeMermaidAwareBlocks()\nschema nodes → Markdown source"]
C --> D["prepend frontmatter yaml"]
D --> E["invoke('save_note_content')\n→ disk write"]
@@ -626,10 +571,6 @@ flowchart LR
style E fill:#d4edda,stroke:#28a745,color:#000
```
Rich-editor change events are coalesced before this serialization runs. `useEditorTabSwap` keeps the latest BlockNote state in the editor, schedules one Markdown serialization for a short idle window, and exposes an explicit flush hook for save, note switch, raw-mode entry, and destructive note actions. This keeps long notes from paying full-document Markdown serialization on every keystroke while preserving the disk-first save path.
Autosave then waits for a 1.5s idle window before invoking `save_note_content`. If an older save resolves after the user has already typed newer content, the older save is treated as stale and cannot clear the newer pending buffer or repaint tab state over it; the latest pending content remains scheduled for its own save.
### Wikilink Navigation
Two navigation mechanisms:
@@ -646,12 +587,6 @@ While the user types, `useEditorSaveWithLinks` derives a transient `VaultEntry`
Current-note find/replace is intentionally backed by raw CodeMirror mode. `Cmd+F`, "Find in Note", and "Replace in Note" switch the active Markdown/text note to raw mode, show the compact find bar above CodeMirror, and operate on the current note only. Plain text matching is case-insensitive by default, `Aa` toggles case sensitivity, `.*` toggles JavaScript-regex matching, and regex replacement supports capture groups through JavaScript replacement syntax.
### Rich Editor Width Modes
Rich Markdown editing supports `normal` and `wide` note widths. The effective mode is resolved in `App.tsx` from, in order, the current session's transient note-width cache, `VaultEntry.noteWidth` parsed from `_width`, and the installation-local `settings.note_width_mode` default. The breadcrumb toggle calls the same setter exposed through the command palette.
Per-note width is persisted as hidden `_width` frontmatter only when the note already has a valid or empty frontmatter block. Notes without frontmatter use the transient cache for the current session, so toggling width never creates frontmatter solely to store UI state. The width class is applied around `SingleEditorView` only; raw CodeMirror mode stays outside `.editor-content-wrapper` and remains full-width.
### Arrow Ligature Normalization
Typed ASCII arrow sequences are normalized consistently in both editor modes:
@@ -659,16 +594,14 @@ Typed ASCII arrow sequences are normalized consistently in both editor modes:
- Rich editor input mounts `createArrowLigaturesExtension()` (`src/components/arrowLigaturesExtension.ts`) into BlockNote and intercepts typed `beforeinput` events before ProseMirror commits the character.
- Raw editor input uses the CodeMirror `inputHandler` path in `useCodeMirror` so the same ligature rules apply while editing markdown source directly.
- Both paths delegate to the shared `resolveArrowLigatureInput()` helper in `src/utils/arrowLigatures.ts`, which prioritizes `<->` over partial matches, keeps paste literal, and lets escaped forms such as `\\->` and `\\<->` remain ASCII.
- The rich-editor extension treats stale, disconnected, or mid-reload ProseMirror views as a no-op. It never blocks the native input path unless it has already built and dispatched a valid ligature transaction.
## Styling
The app uses internal light and dark themes owned by Tolaria, with System as an installation-local preference that follows the OS appearance (see [ADR-0081](adr/0081-internal-light-dark-theme-runtime.md) and [ADR-0112](adr/0112-system-theme-mode.md)). The previous vault-authored theming system remains removed.
The app uses internal light and dark themes owned by Tolaria (see [ADR-0081](adr/0081-internal-light-dark-theme-runtime.md)). The previous vault-authored theming system remains removed; theme mode is an installation-local app preference.
1. **Global CSS variables** (`src/index.css`): Semantic app colors, borders, surfaces, and interaction states via `:root` / `[data-theme]`, bridged to Tailwind v4
2. **Editor theme** (`src/theme.json`): BlockNote typography, flattened to CSS vars by `useEditorTheme`
3. **Runtime theme bridge**: Resolves the selected preference to `light` / `dark`, applies `data-theme` and `.dark` for shadcn/ui, and subscribes to `prefers-color-scheme` while System is selected
4. **Theme mode commands**: Command-palette actions for Light, Dark, and System call the same `saveSettings` path as the Settings panel and persist only `settings.theme_mode`
3. **Runtime theme bridge**: Applies `data-theme` and `.dark` for shadcn/ui, while CodeMirror and editor-specific consumers derive any non-CSS-variable values from the same semantic contract
## Localization
@@ -689,12 +622,10 @@ 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, number fields, boolean toggles, array tag pills, text fields. Click-to-edit interaction.
- **Property display modes**: `text`, `number`, `date`, `boolean`, `status`, `url`, `tags`, and `color`. Numeric frontmatter values auto-detect as `number`, and custom scalar keys can be explicitly switched to `Number` through the property-type control.
- **Present empty properties**: A top-level frontmatter key with a blank scalar value (for example `start date:`) is treated as present and renders as an editable empty row. Only absent keys are omitted.
- **Type-derived placeholders**: For typed instances, missing custom properties declared on the type document render as gray editable placeholders. Editing one writes the value to the instance frontmatter; merely displaying it does not backfill the note.
- **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`, `has`, and all custom relationship fields as clickable wikilink chips. Relationship labels are humanized for display, but stored keys remain unchanged. For typed instances, missing relationship fields declared on the type document render as gray editable placeholders without copying any default relationship targets into existing notes.
2. **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.
3. **BacklinksPanel**: Scans `allContent` for notes that reference the current note via `[[title]]` or `[[path]]`.
@@ -738,27 +669,18 @@ No indexing step required — search runs directly against the filesystem.
Per-vault settings stored locally and scoped by vault path:
- Managed by `useVaultConfig` hook and `vaultConfigStore`
- Settings: zoom, view mode, editor mode, tag colors, status colors, property display modes, Inbox/All Notes note-list column overrides, explicit organization workflow toggle, AI agent permission mode (`safe` / `power_user`)
- Missing, null, and unknown AI agent permission modes normalize to `safe`; the AI panel can switch modes per vault, preserving the transcript and applying the new mode only to the next agent run
- Settings: zoom, view mode, editor mode, note layout, tag colors, status colors, property display modes, Inbox/All Notes note-list column overrides, explicit organization workflow toggle
- One-time migration from localStorage (`configMigration.ts`)
Installation-local layout state that should not sync through a vault stays in localStorage. `useLayoutPanels` stores the clamped sidebar, note-list, and inspector widths under `tolaria:layout-panels` so pane sizing survives app relaunches on the same machine.
### AI Guidance Files
Tolaria tracks managed vault-level AI guidance separately from normal note content:
- `AGENTS.md` is the canonical managed guidance file for Tolaria-aware coding agents
- `CLAUDE.md` is a compatibility shim that points Claude Code back to `AGENTS.md`
- `GEMINI.md` is an optional Gemini CLI compatibility shim that points Gemini back to `AGENTS.md`
- `useVaultAiGuidanceStatus` reads `get_vault_ai_guidance_status` and normalizes the backend state into four UI cases: `managed`, `missing`, `broken`, and `custom`
- `restore_vault_ai_guidance` repairs only Tolaria-managed files and creates the optional Gemini shim on explicit request; user-authored custom `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` files are surfaced as custom and left untouched
- `restore_vault_ai_guidance` repairs only Tolaria-managed files; user-authored custom `AGENTS.md` / `CLAUDE.md` files 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
Vault guidance is intentionally short and vault-specific. General Tolaria product behavior is delivered through the bundled agent docs resource instead:
- `scripts/build-agent-docs.mjs` compiles the public `site/` Markdown into `src-tauri/resources/agent-docs/`
- `src-tauri/resources/agent-docs/AGENTS.md` orients agents to the generated docs bundle, while `index.md`, section bundles, `all.md`, `search-index.json`, and `pages/` provide fast local lookup
- `get_agent_docs_path` exposes the resolved resource folder to the renderer, and `buildAgentSystemPrompt()` tells every app-managed CLI agent to read vault `AGENTS.md` first, then search the bundled docs for Tolaria behavior
### Getting Started / Onboarding
`useOnboarding` hook detects first launch:
@@ -775,7 +697,7 @@ Vault guidance is intentionally short and vault-specific. General Tolaria produc
`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
- Uses `get_ai_agents_status`, whose backend treats the app process path, login-shell path, and supported local/toolchain/app install locations, including nvm-managed Node installs plus Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources
- Uses `get_ai_agents_status`, whose backend treats the app process path, login-shell path, and supported local/toolchain/app install locations, including Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources
- Persists dismissal locally once the user continues
### Remote Git Operations
@@ -783,9 +705,6 @@ Vault guidance is intentionally short and vault-specific. General Tolaria produc
Tolaria delegates remote auth to the user's system git setup:
- `CloneVaultModal` captures a remote URL and local destination
- `clone_git_repo` and `create_getting_started_vault` both run system git clone work in blocking Tokio tasks so clone UIs stay responsive
- On macOS, system-git commands prefer the user's login-shell `git` and `PATH`, and `git_add_remote` preflights HTTPS remotes through `git credential fill` so Keychain can prompt/grant access before the first fetch or push
- On Linux AppImage launches, every system-git command removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` before spawning `git`, so helpers like `git-remote-https` bind against the host git/library stack instead of Tolaria's bundled WebKit/AppImage libraries
- On Linux AppImage Wayland launches, startup environment safeguards also set `GTK_IM_MODULE=fcitx` when common input-method variables already indicate fcitx and the user has not explicitly chosen a GTK IM module, allowing WebKitGTK editor input to reach fcitx5 on compositors such as niri without overriding deliberate `GTK_IM_MODULE` choices.
- `git_add_remote` uses the same system git path and refuses remotes whose history is unrelated or ahead of the local vault
- Existing `git_pull` / `git_push` commands keep surfacing raw git errors, and clone commands fail fast when git wants interactive terminal input
- No provider-specific token or username is stored in app settings
@@ -805,21 +724,14 @@ interface Settings {
analytics_enabled: boolean | null
anonymous_id: string | null
release_channel: string | null // null = stable default, "alpha" = every-push prerelease feed
theme_mode: 'light' | 'dark' | 'system' | null
theme_mode: 'light' | 'dark' | null
ui_language: AppLocale | null
note_width_mode: 'normal' | 'wide' | null
sidebar_type_pluralization_enabled: boolean | null // null = default true
default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | 'gemini' | null
default_ai_target: string | null // "agent:codex" or "model:<provider>/<model>"
ai_model_providers: AiModelProvider[] | null
default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | null
hide_gitignored_files: boolean | null // null = default true
all_notes_show_pdfs: boolean | null // null = default false
all_notes_show_images: boolean | null // null = default false
all_notes_show_unsupported: boolean | null // null = default false
}
```
Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette Light/Dark/System actions both update that same value. `system` remains a stored preference, while the runtime resolves it to `light` or `dark` for `data-theme` and app consumers. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `sidebar_type_pluralization_enabled` is installation-local and defaults to `true`; when false, type rows use exact type names unless the type document defines an explicit `sidebar_label` override. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings. Provider defaults and local/API grouping come from the shared `src/shared/aiModelProviderCatalog.json` catalog used by both renderer settings and the Tauri direct-model runtime. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation.
Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `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. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation.
## Telemetry
@@ -831,16 +743,9 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
- **`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` and `VITE_POSTHOG_KEY`; `VITE_SENTRY_RELEASE` is treated as the build version and only becomes Sentry's `release` for stable calendar builds (`YYYY.M.D`). Alpha/prerelease/internal builds tag `tolaria.build_version` and `tolaria.release_kind` without creating normal Sentry Releases entries.
- **`src/lib/telemetry.ts`** — `initSentry()`, `teardownSentry()`, `initPostHog()`, `teardownPostHog()`, `trackEvent()`. Path scrubber via `beforeSend` hook. DSN/release/key from `VITE_SENTRY_DSN`, `VITE_SENTRY_RELEASE`, and `VITE_POSTHOG_KEY` env vars.
- **`src/main.tsx`** — React root error callbacks (`onCaughtError`, `onUncaughtError`, `onRecoverableError`) forward component-stack context to `Sentry.reactErrorHandler()` for debuggable production React errors.
- **`src-tauri/src/telemetry.rs`** — Rust-side Sentry init with `beforeSend` path scrubber. `init_sentry_from_settings()` reads settings and conditionally initializes; stable calendar `CARGO_PKG_VERSION` values become Sentry releases, while alpha/prerelease/internal versions are kept as diagnostic tags only. `reinit_sentry()` for runtime toggle.
### Product Events
- **File previews** — `file_preview_opened`, `file_preview_action`, and `file_preview_failed` report only preview/action categories such as `image`, `pdf`, `unsupported`, `open_external`, `copy_path`, and `reveal`.
- **Inline image lightbox** — `inline_image_lightbox_opened` records that a rich-editor inline image was opened from double-click, without sending note paths, image URLs, alt text, or file names.
- **Code block copy** — `code_block_copied` records that the rich-editor code-block copy action was used, without sending note paths, languages, or code content.
- **AI agent sessions** — `ai_agent_message_sent`, `ai_agent_message_blocked`, `ai_agent_response_completed`, `ai_agent_response_failed`, and `ai_agent_permission_mode_changed` use only agent ids, permission modes, counts, and coarse status categories.
- **All Notes visibility** — `all_notes_visibility_changed` records only the toggled category and enabled state.
- **`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.
@@ -850,7 +755,7 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
## Updates & Feature Flags
### Hooks
- **`useUpdater(releaseChannel)`** — Channel-aware updater state machine. Checks the selected feed, surfaces checking/available/downloading/ready states, and delegates install work to Rust.
- **`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
@@ -866,6 +771,6 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
- **`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` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. macOS release assets use `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds.
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, Linux x86_64 `.deb` / AppImage artifacts, and a static public download page that starts the selected installer without replacing the page with a blank download navigation. Stable macOS DMG/updater assets use the same `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release.
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. macOS release assets use `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`.
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, and Linux x86_64 `.deb` / AppImage artifacts. Stable macOS DMG/updater assets use the same `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`.
- **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed.

View File

@@ -24,10 +24,7 @@ When deciding where to persist a piece of data, ask: **"Would the user want this
| Pinned properties per type | API keys (OpenAI, Google) |
| Sidebar label overrides | Auto-sync interval |
| Property display order | Window size / position |
| Per-note `_width` rich-editor width override | Default rich-editor note width |
| Vault-authored `.gitignore` patterns | Whether this installation hides Gitignored files |
| Per-vault All Notes note-list column overrides | All Notes PDF/image/unsupported file visibility |
| Type `_sidebar_label` overrides | Whether this installation auto-pluralizes type labels |
| Any user-visible customization of how content is organized or displayed | Any machine-specific or credential-type setting |
**Rule:** If the information is about *how the content is structured or presented* and the user would expect it to be consistent wherever they open their vault, store it in the vault (frontmatter of the relevant note, using the `_field` underscore convention for system properties). If it's about *this specific installation of the app*, store it in `~/.config/com.tolaria.app/settings.json` or localStorage.
@@ -35,12 +32,8 @@ When deciding where to persist a piece of data, ask: **"Would the user want this
Examples:
- ✅ Vault: `_pinned_properties` in a Type note (every device should show the same pinned properties)
- ✅ Vault: `_icon: shapes` in a Type note (icon is part of the type's identity)
- ✅ Vault: `_width: wide` in a note that already has frontmatter (per-note reading/editing preference)
- ✅ App settings: `zoom: 1.3` (machine-specific preference)
- ✅ App settings: `ui_language: "zh-CN"` (installation-specific UI language)
- ✅ App settings: `note_width_mode: "wide"` (installation-specific default for notes without an override)
- ✅ App settings: `sidebar_type_pluralization_enabled: false` (installation-specific sidebar label preference)
- ✅ App settings: `all_notes_show_images: true` (installation-specific All Notes file-category visibility)
### No hardcoded exceptions
@@ -87,27 +80,15 @@ flowchart LR
1. **Disk-first writes**: All functions that change vault data must write to disk (via Tauri IPC) *before* updating React state. This ensures that if the disk write fails, React state remains consistent with what's actually on disk.
2. **Optimistic UI with rollback**: Where responsiveness matters (e.g. `persistOptimistic` in `useNoteCreation`), state may update before disk confirmation — but a failure callback must revert the optimistic state.
3. **No orphan state updates**: Never call `updateEntry()` before the corresponding `handleUpdateFrontmatter()` or `handleDeleteProperty()` has resolved. Type metadata actions in `useEntryActions` follow this rule — create the missing type document if needed, write frontmatter first, then update state. If missing type creation collides with an existing note filename, the action stops after the existing creation toast instead of applying orphaned state.
3. **No orphan state updates**: Never call `updateEntry()` before the corresponding `handleUpdateFrontmatter()` or `handleDeleteProperty()` has resolved. The three functions in `useEntryActions` (`handleCustomizeType`, `handleRenameSection`, `handleToggleTypeVisibility`) follow this rule — disk write first, then state update.
4. **Recovery via reload**: If state ever diverges from disk (crash, external edit, race condition), `Reload Vault` (Cmd+K → "Reload Vault") invalidates the cache and does a full filesystem rescan via the `reload_vault` Tauri command, replacing all React state. The `reload_vault_entry` command can re-read a single file.
5. **Cache is disposable**: The `reload_vault` command deletes the cache file before rescanning, guaranteeing fresh data. The cache never contains data that doesn't exist on the filesystem.
6. **Visibility filters are command-boundary concerns**: Gitignored-content visibility is applied after scanning/caching, before entries, folders, or search results reach React. The cache remains complete so toggling the setting can show ignored content again without rebuilding a different cache shape. Large folder filtering runs on the blocking Tokio pool and drains `git check-ignore` output while feeding stdin so broad ignore matches cannot freeze the native UI thread.
6. **Visibility filters are command-boundary concerns**: Gitignored-content visibility is applied after scanning/caching, before entries, folders, or search results reach React. The cache remains complete so toggling the setting can show ignored content again without rebuilding a different cache shape.
#### External Change Detection
The main window starts a native watcher for the active vault through `start_vault_watcher` / `stop_vault_watcher` (`src-tauri/src/vault_watcher.rs`, backed by Rust `notify`). The watcher emits `vault-changed` events for content paths and ignores churn from `.git/`, `node_modules/`, temp files, and `.tolaria-rename-txn`. `useVaultWatcher` batches those events, suppresses recent app-owned saves, and sends the remaining external paths through `refreshPulledVaultState()` so folders, saved views, note-list state, and the clean active editor all refresh under the ADR-0071 unsaved-edit rules. `useVaultLoader.isReloading` drives the status-bar reload spinner for both manual and watcher-triggered reloads.
#### Progressive Vault Loading
Vault opening is allowed to render the main app shell while the full entry scan is still in flight. `useVaultLoader` keeps `isLoading` true until entries are ready, but folders and saved views load independently so the sidebar can become useful before the note index completes. The status bar uses the vault activity badge during this initial indexing state, while command-palette and editor-shell interactions remain mounted instead of being hidden behind the full app skeleton. The full skeleton is reserved for app-level capability checks such as the initial Git-state probe.
Large-vault reproduction and keyboard QA steps live in [LARGE-VAULT-LOADING-QA.md](./LARGE-VAULT-LOADING-QA.md).
#### Note Opening Fast Path
Note opening uses bounded in-memory fast paths for raw content and parsed editor blocks. `useTabManagement` owns the markdown/text prefetch cache and treats every cached value as a performance hint only: identity-matched entries (`modifiedAt` + `fileSize`) can be reused immediately, while identity-missing or identity-mismatched cached text is checked with `validate_note_content`, which compares the cached text with the current file bytes inside the validated vault boundary. If validation fails, Tolaria discards the cached entry and reads fresh disk content before swapping the editor.
The note list opportunistically preloads visible and adjacent markdown/text entries after a short delay. When a large warmed Markdown note resolves, `useEditorTabSwap` may parse it into a bounded parsed-block cache only after foreground editor work has been idle and the rich editor is mounted. Parsed blocks are keyed by vault, path, and exact source content; every async swap carries a generation/source-content token so stale conversion results cannot overwrite newer file content or dirty editor state. The editor never renders a preview surface that later morphs into BlockNote. See [ADR-0105](./adr/0105-editor-correctness-and-responsiveness-contract.md).
## Tech Stack
| Layer | Technology | Version |
@@ -117,7 +98,6 @@ The note list opportunistically preloads visible and adjacent markdown/text entr
| Editor | BlockNote | 0.46.2 |
| Code block highlighting | @blocknote/code-block | 0.46.2 |
| Diagram rendering | Mermaid | 11.14.0 |
| Whiteboard rendering | tldraw | 4.5.10 |
| Raw editor | CodeMirror 6 | - |
| Styling | Tailwind CSS v4 + CSS variables | 4.1.18 |
| UI primitives | Radix UI + shadcn/ui | - |
@@ -126,7 +106,7 @@ The note list opportunistically preloads visible and adjacent markdown/text entr
| Backend language | Rust (edition 2021) | 1.77.2 |
| Frontmatter parsing | gray_matter | 0.2 |
| Filesystem watcher | notify | 6.1 |
| AI (agent panel) | CLI agent adapters (Claude Code + Codex + OpenCode + Pi + Gemini) | - |
| AI (agent panel) | CLI agent adapters (Claude Code + Codex + OpenCode + Pi) | - |
| Search | Keyword (walkdir-based file scan) | - |
| Localization | App-owned runtime + JSON catalogs (`src/lib/i18n.ts`, `src/lib/locales/*.json`, `lara.yaml`) | English fallback + Lara CLI sync |
| MCP | @modelcontextprotocol/sdk | 1.0 |
@@ -144,7 +124,7 @@ flowchart TD
SB["Sidebar\n(navigation + filters + types)"]
NL["NoteList / PulseView\n(filtered list / activity)"]
ED["Editor\n(BlockNote + diff + raw)"]
IN["Right Panel\n(Inspector + TOC)"]
IN["Inspector\n(metadata + relationships)"]
AIP["AiPanel\n(selected CLI agent + tools)"]
SP["SearchPanel\n(keyword search)"]
ST["StatusBar\n(vault picker + sync + version)"]
@@ -161,11 +141,11 @@ flowchart TD
GIT["git/\n(commit, sync, clone)"]
SETTINGS["settings.rs"]
SEARCH["search.rs"]
CLI["ai_agents.rs\n+ CLI adapters"]
CLI["ai_agents.rs\n+ claude_cli.rs"]
end
subgraph EXT["External Services"]
CCLI["Claude / Codex / OpenCode / Pi / Gemini CLI\n(agent subprocesses)"]
CCLI["Claude / Codex / OpenCode / Pi CLI\n(agent subprocesses)"]
MCP["MCP Server\n(ws://9710, 9711)"]
GCLI["git CLI\n(system executable)"]
REMOTE["Git remotes\n(GitHub/GitLab/Gitea/etc.)"]
@@ -187,10 +167,10 @@ flowchart TD
```
┌────────┬─────────────┬─────────────────────────┬────────────┐
│Sidebar │ Note List │ Editor │ Right Panel
│Sidebar │ Note List │ Editor │ Inspector
│(250px) │ (300px) │ (flex-1) │ (280px) │
│ │ OR │ │ OR │
│ All │ Pulse View │ [Breadcrumb Bar] │ TOC
│ All │ Pulse View │ [Breadcrumb Bar] │ AI Chat
│ Changes│ │ │ OR │
│ Pulse │ [Search] │ # My Note │ AI Agent │
│ Inbox │ [Sort/Filt] │ │ │
@@ -206,21 +186,19 @@ flowchart TD
└──────────────────────────────────────────────────────────────┘
```
- **Sidebar** (220-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types: pointer users can drag the existing view row, double-click to rename it, or right-click for edit/rename/appearance/delete actions, while keyboard users can use the row context key for the same menu and command-palette move actions for ordering. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root.
- **Note List / Pulse View** (220-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Folder-backed lists also show non-Markdown files: previewable media and PDF binaries get file indicators and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count, rich-editor width toggle, and the secondary-overflow Table of Contents action, BlockNote rich text editor with wikilink support, Markdown-compatible inline/display math rendering, first-class Mermaid diagram blocks, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block`. Can toggle to diff view (modified files), raw CodeMirror view, or a wide rich-editor reading surface with preserved side margins; raw CodeMirror remains full-width and unaffected by note width mode. Inline rich-editor images open in a localized shadcn lightbox on double-click while normal single-click BlockNote selection remains untouched, and tiny tracking-style images are ignored. Binary image, audio, video, and PDF files render through `FilePreview` as ordinary vault files using Tauri asset URLs; editor-embedded audio and video use the same scoped asset sources through the CSP `media-src` allow-list. External-open actions call `open_vault_file_external` so the target is validated against the active vault before the native default app opens it. Unsupported/broken binaries show explicit fallback states and keyboard focus returns to the note list on `Escape`. Decomposed into `Editor` (orchestrator), `EditorContent`, `FilePreview`, `EditorRightPanel`, `TableOfContentsPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, and `useEditorSave`, plus the `useRawMode`/`RawEditorView` pair for markdown source editing. Rich BlockNote input and raw CodeMirror input both route typed `->`, `<-`, and `<->` through the shared `src/utils/arrowLigatures.ts` resolver so arrow ligatures stay consistent across mode switches while escaped ASCII sequences remain literal. Navigation history (Cmd+[/]) replaces tabs.
- **Right side panels** (200-500px or hidden): Properties, Table of Contents, and AI Agent are mutually exclusive panels mounted by `EditorRightPanel` and coordinated by `useRightPanelExclusion`. Properties shows frontmatter, relationships, instances, backlinks, and git history; Table of Contents is lazy-mounted only while open, derives a title-rooted H1/H2/H3 hierarchy through a debounced Web Worker per ADR-0109, and reuses folder-tree indentation/guide geometry with heading icons while resolving live BlockNote block IDs at click time for navigation; AI Agent keeps the selected CLI/API target controller mounted for tool execution and chat state. The breadcrumb bar toggles Table of Contents, AI, and Properties actions, and opening one replaces the others. Per-note `icon` is a suggested Properties field and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, Properties shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse), collapsible type-based section groups, and a dedicated folder tree. The folder tree shows user-created folders plus default vault folders such as `attachments/` and `views/`; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
- **Note List / Pulse View** (200-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Folder-backed lists also show non-Markdown files: previewable image binaries get an image indicator and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count and note-layout toggle, BlockNote rich text editor with wikilink support, Markdown-compatible inline/display math rendering, first-class Mermaid diagram blocks, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block`. Can toggle to diff view (modified files), raw CodeMirror view, or a wide-screen left-aligned note column while preserving the same readable max width. Binary image files render through `FilePreview` as ordinary vault files using Tauri asset URLs, with explicit unsupported/broken fallback states and keyboard focus returning to the note list on `Escape`. Decomposed into `Editor` (orchestrator), `EditorContent`, `FilePreview`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, and `useEditorSave`, plus the `useRawMode`/`RawEditorView` pair for markdown source editing. Rich BlockNote input and raw CodeMirror input both route typed `->`, `<-`, and `<->` through the shared `src/utils/arrowLigatures.ts` resolver so arrow ligatures stay consistent across mode switches while escaped ASCII sequences remain literal. Navigation history (Cmd+[/]) replaces tabs.
- **Inspector / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history) and AI Agent panel (the selected CLI agent with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. Per-note `icon` is a suggested Inspector property and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
Panels are separated by `ResizeHandle` components that support drag-to-resize. `useLayoutPanels` clamps the sidebar, note-list, and inspector widths before applying them, keeps the side panes from flex-shrinking below their protected widths, and persists the last chosen widths in installation-local localStorage under `tolaria:layout-panels`.
Panels are separated by `ResizeHandle` components that support drag-to-resize.
The main Tauri window derives its minimum width from the visible panes instead of a single fixed floor. `useMainWindowSizeConstraints` treats the editor-only shell as the 480px baseline, adds the current sidebar / note-list / expanded-inspector widths on top with minimum floors, and calls the native `update_current_window_min_size` command whenever view mode, inspector visibility, or restored pane widths change. That same native command can grow the current window back out when a wider pane combination is restored, but Windows keeps grow-to-fit disabled so opening or closing the Properties panel does not unmaximize or reposition the main window. Note windows skip this path and keep their dedicated 800×700 initial sizing.
The main Tauri window derives its minimum width from the visible panes instead of a single fixed floor. `useMainWindowSizeConstraints` treats the editor-only shell as the 480px baseline, adds sidebar / note-list / expanded-inspector allowances on top, and calls the native `update_current_window_min_size` command whenever view mode or inspector visibility changes. That same native command also grows the current window back out when a wider pane combination is restored, while note windows skip this path and keep their dedicated 800×700 initial sizing.
The main Tauri window also persists its last normal size and screen position in the app config directory as `window-state.json`. The state stores logical window points, while `window_state.rs` migrates older physical-pixel state on read so Retina and non-Retina launches restore the same user-facing bounds. On startup, the restored frame applies only to the main window and clamps to the currently available monitor work areas, so stale coordinates from a disconnected display fall back to a visible placement. Maximized, fullscreen, minimized, and detached note-window frames are not written as the restore baseline.
Tauri setup keeps launch-time filesystem and subprocess work off the window creation critical path. Legacy `~/Laputa` housekeeping and the initial persisted-vault MCP bridge sync run on named background threads, so large legacy vaults, stale active-vault paths, or slow process startup cannot beachball the macOS app before React mounts. React still resyncs the bridge from `useVaultSwitcher` after the persisted selection loads, and no selected vault stops the bridge. The HTML bootstrap also installs a Tauri-only one-shot watchdog: React reports readiness from an effect after the root commits, and if that readiness signal never arrives the WebView reloads once instead of leaving macOS users in an inert rendered shell.
Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that the desktop native menus use. The native app menu keeps macOS-only Services/Hide entries off Windows and Linux, registers a window-scoped menu event handler on Windows where Tauri delivers menu clicks through the main `WebviewWindow`, and cross-platform custom items such as Check for Updates emit Tolaria command IDs with visible updater feedback.
When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, and on Wayland sessions it re-execs once with the first architecture-matching system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The candidate order prefers Fedora-style `lib64` and Debian-style `x86_64-linux-gnu` paths before generic `/usr/lib`, and the ELF header is checked so a 64-bit Tolaria process does not retry with a 32-bit Wayland client library. If an AppImage Wayland session advertises fcitx through common input-method environment hints and the user has not already chosen `GTK_IM_MODULE`, Tolaria sets `GTK_IM_MODULE=fcitx` before WebKit starts so GTK/WebKit input contexts reach fcitx5 on compositors that do not reliably provide the Wayland input method frontend. The same AppImage path checks whether `fc-match` resolves the default emoji font to `Noto-COLRv1.ttf`; when the user has not provided `FONTCONFIG_FILE` or `FONTCONFIG_PATH`, Tolaria writes a cache-local fontconfig file that rejects only that matched font file and exports it before WebKit starts. The rendering overrides keep AppImage WebViews from blanking after accelerated compositing/DMA-BUF failures, the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER`, the fcitx GTK fallback preserves Chinese/Japanese/Korean IME composition for affected AppImage Wayland users, and the fontconfig guard avoids known WebKit crashes in COLRv1 emoji font rendering while leaving other emoji fonts available.
Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that macOS uses for native menu clicks.
When Tolaria is launched from a Linux AppImage, `run()` also injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` unless the user already set that variable. This keeps the workaround scoped to bundled WebKitGTK launches that are prone to Fedora/Wayland DMA-BUF crashes without changing native package installs.
## Multi-Window (Note Windows)
@@ -236,7 +214,7 @@ Notes can be opened in separate Tauri windows for focused editing. Secondary win
- `openNoteInNewWindow()` (`src/utils/openNoteWindow.ts`) creates a new `WebviewWindow` via the Tauri v2 JS API with URL query params (`?window=note&path=...&vault=...&title=...`)
- `main.tsx` checks `isNoteWindow()` at boot to route between `App` (main window) and `NoteWindow` (secondary window)
- `NoteWindow` (`src/NoteWindow.tsx`) is a minimal shell that loads vault entries, fetches note content, applies the theme, and renders a single `Editor` instance
- Each window has its own auto-save via `useEditorSaveWithLinks` (same 1.5s low-end-safe idle debounce, same Rust `save_note_content` command), and raw-editor typing also derives frontmatter-backed `VaultEntry` state in the renderer so Inspector and note-list surfaces react immediately without waiting for a full reload
- Each window has its own auto-save via `useEditorSaveWithLinks` (same 500ms debounce, same Rust `save_note_content` command), and raw-editor typing also derives frontmatter-backed `VaultEntry` state in the renderer so Inspector and note-list surfaces react immediately without waiting for a full reload
- Secondary windows are sized 800×700; macOS keeps the overlay title bar, while Linux mounts the shared React titlebar on undecorated windows
- Capabilities config (`src-tauri/capabilities/default.json`) grants permissions to both `main` and `note-*` window labels
@@ -246,15 +224,12 @@ Notes can be opened in separate Tauri windows for focused editing. Secondary win
Full agent mode — spawns the selected local CLI agent as a subprocess with tool access and MCP vault integration.
1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts` + `aiTargets.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, default-target selection, bundled-docs prompt injection, and the per-vault Safe / Power User permission mode shown in the panel header for coding agents
2. **Backend orchestration** (`ai_agents.rs`) — normalizes agent availability, streaming, and the request permission mode before dispatching to per-agent adapters
3. **Shared runtime scaffold** (`cli_agent_runtime.rs`) — owns the common request shape, prompt wrapping, JSON-line subprocess lifecycle, normalized error/done handling, version probing, and Tolaria MCP server path resolution used by app-managed CLI agents
4. **Agent adapters** — Shared prompts are mode-aware on every turn, including turns with note context snapshots: Vault Safe tells agents not to use or advertise shell, while Power User tells shell-capable agents to keep local commands scoped to the active vault. Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, and a scoped tool list: Safe enables file/search/edit tools only, while Power User adds Bash to the available tools and pre-approves Bash with `--allowedTools` without using dangerous bypass flags. Codex runtime specifics live in `codex_cli.rs`; Safe runs `codex --sandbox read-only --ask-for-approval untrusted exec --json`, while Power User runs `codex --sandbox workspace-write --ask-for-approval never exec --json` so shell execution stays enabled across repeated turns. OpenCode runs through `opencode run --format json` with transient permissions: Safe denies bash and external directories, while Power User allows bash but still denies external directories. Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`; both modes currently share the same transient MCP config and the prompt does not promise shell for Pi Power User. Gemini runs through `gemini --output-format stream-json --prompt` so assistant message chunks, tool calls, and final errors are mapped from the CLI event stream instead of relying on a buffered `response` field. Gemini Safe uses `auto_edit` plus `tools.exclude=["run_shell_command"]`; Power User intentionally uses `yolo` against a trusted transient Tolaria MCP entry. Codex, OpenCode, Pi, and Gemini all launch from the active vault cwd with closed stdin and transient MCP config. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags.
5. **MCP Integration** — Claude receives the generated MCP config file path, Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides using Tolaria's resolved Node path plus `VAULT_PATH` and `WS_UI_PORT`, OpenCode receives it through `OPENCODE_CONFIG_CONTENT`, Pi receives it through a temporary `PI_CODING_AGENT_DIR/mcp.json` consumed by `pi-mcp-adapter`, and Gemini receives it through a temporary settings file pointed at by `GEMINI_CLI_SYSTEM_SETTINGS_PATH`
1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, and default-agent selection
2. **Backend** (`ai_agents.rs`) — normalizes agent availability and streaming, dispatching to per-agent adapters
3. **Agent adapters** — Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, a file/search-only built-in tool list, hidden Windows subprocess launches, and closed stdin for print-mode subprocesses so Windows launches receive EOF; Codex runs through `codex --sandbox workspace-write --ask-for-approval never exec --json`; OpenCode runs through `opencode run --format json`; Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`. OpenCode and Pi both launch from the active vault cwd with closed stdin and transient MCP config. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags.
4. **MCP Integration** — Claude receives the generated MCP config file path, Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides, OpenCode receives it through `OPENCODE_CONFIG_CONTENT`, and Pi receives it through a temporary `PI_CODING_AGENT_DIR/mcp.json` consumed by `pi-mcp-adapter`
CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, nvm-managed Node installs, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. App-managed CLI spawns also expand the active vault path before using it as the subprocess working directory, then extend the child process `PATH` with the resolved binary directory plus those common toolchain directories, which lets GUI-launched macOS sessions run Homebrew/npm shims and their `node`-backed MCP subprocesses even when Finder/Dock did not inherit a terminal shell path.
CLI-agent system prompts also include a local Tolaria docs orientation when the bundled docs resource is present. `scripts/build-agent-docs.mjs` generates `src-tauri/resources/agent-docs/` from the public VitePress Markdown sources, including `index.md`, `AGENTS.md`, per-section bundles, `all.md`, `search-index.json`, and generated per-page files. Tauri bundles that folder as `agent-docs/`; `get_agent_docs_path` resolves the installed resource path, with a repository fallback for development, and `getAgentDocsPath()` caches it before each agent run. Agents are instructed to read the active vault's `AGENTS.md` for local conventions and search the bundled docs for Tolaria product behavior.
CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs.
#### Agent Event Flow
@@ -268,12 +243,12 @@ sequenceDiagram
U->>FE: sendMessage(text, references)
FE->>FE: buildContextSnapshot(activeNote, linkedNotes, openTabs)
FE->>R: invoke('stream_ai_agent', {agent, message, systemPrompt, vaultPath, permissionMode})
FE->>R: invoke('stream_ai_agent', {agent, message, systemPrompt, vaultPath})
R->>R: pick adapter for claude_code, codex, opencode, or pi
R->>C: spawn agent with MCP-enabled config
loop Normalized stream
C-->>R: Claude NDJSON, Codex JSONL, OpenCode JSON, Pi JSON, or Gemini JSONL events
C-->>R: Claude NDJSON, Codex JSONL, OpenCode JSON, or Pi JSON events
R-->>FE: emit("ai-agent-stream", event)
alt TextDelta
FE->>FE: accumulate response (revealed on Done)
@@ -303,29 +278,23 @@ The agent panel (`ai-context.ts`) builds a structured JSON snapshot from the act
```json
{
"activeNote": { "path", "title", "type", "frontmatter", "body", "wordCount", "bodyTruncated?" },
"openTabs": [{ "path", "title", "type", "frontmatter" }],
"noteList": [{ "path", "title", "type" }],
"vault": { "types", "totalNotes" },
"referencedNotes": [{ "title", "path", "type" }]
"activeNote": { "path", "title", "type", "frontmatter", "content" },
"linkedNotes": [{ "path", "title", "content" }],
"openTabs": [{ "title", "snippet" }],
"vaultMetadata": { "noteTypes", "stats", "filter" },
"references": [{ "title", "path", "type" }]
}
```
Large active notes are compacted into a head/tail body snapshot before they enter the CLI prompt. The snapshot records `bodyTruncated` metadata and instructs agents to call `get_note(path)` before content-sensitive edits or summaries, keeping lower-context OpenCode providers from failing on oversized active-note context while preserving access to the full note through MCP.
### Direct Model Targets
Tolaria also supports direct model targets for local servers and API providers. These targets are stored as app-level provider metadata and can be selected in Settings or the status bar alongside coding agents. `src/shared/aiModelProviderCatalog.json` is the shared source for provider defaults, local/API grouping, API-key environment placeholders, and runtime fallback base URLs; the renderer imports it through `aiTargets.ts`, and Tauri includes the same JSON in `ai_models.rs`. Direct model targets run in Chat mode: they receive the same note-context snapshot and conversation history, but they do not receive vault-write tools or shell access. The backend `stream_ai_model` command supports OpenAI-compatible chat completions and Anthropic Messages-compatible calls, including Ollama, LM Studio, OpenRouter, OpenAI, Anthropic, Gemini, and custom compatible endpoints.
Provider secrets are not written to `settings.json`. Hosted API targets can use Tolaria's local app-data secrets file (`ai-provider-secrets.json`, outside vaults/worktrees and owner-only on Unix) or reference an environment variable name. Local endpoints can omit authentication.
Token budget: 60% of 180k context limit (~108k tokens max). Active note gets priority, then linked notes, then truncation.
### Authentication
Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login; Codex surfaces a friendly prompt to run `codex login` when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings; direct provider secrets stay in local app data or user-managed environment variables.
Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login; Codex surfaces a friendly prompt to run `codex login` when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings.
## MCP Server
The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistants (Claude Code, Gemini CLI, Cursor, or any MCP-compatible client).
The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistants (Claude Code, Cursor, or any MCP-compatible client).
### Tool Surface (14 tools)
@@ -357,11 +326,10 @@ The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistan
Tolaria can register itself as an MCP server in:
- `~/.claude.json` and `~/.claude/mcp.json` (Claude Code compatibility across current CLI and legacy MCP-file setups)
- `~/.gemini/settings.json` (Gemini CLI)
- `~/.cursor/mcp.json` (Cursor)
- `~/.config/mcp/mcp.json` (generic MCP-compatible clients)
That setup is user-initiated through the status bar / command palette flow, not a startup side effect. Registration is non-destructive (additive, preserves other servers and Gemini settings), uses `upsert` semantics, and can be reversed by removing Tolaria's entry again. Tolaria verifies Node.js is available before writing config, writes an explicit `type: "stdio"` entry, pins `VAULT_PATH` to the active vault, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. The same generated entry is exposed as a manual JSON snippet in the MCP setup dialog and through the AI panel copy action, giving users a transparent fallback for MCP-compatible tools Tolaria does not auto-configure. In the desktop app, `useMcpStatus` copies that snippet through the native `copy_text_to_clipboard` command instead of the Web Clipboard API so macOS WKWebView permission policy cannot block setup. Packaged builds resolve `mcp-server/` from the installed resource directory next to the executable before falling back to macOS `Resources`, Linux package roots such as `/usr/local/Tolaria`, `/usr/lib/tolaria`, and `/usr/lib/tolaria/resources`, and AppImage paths. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`) and owns connect, disconnect, exact-snippet load, and copy-to-clipboard actions. Gemini CLI still owns its own install and sign-in; Tolaria writes the durable external MCP entry only on explicit setup, while app-managed Gemini sessions use transient settings and optional vault guidance. The desktop WebSocket bridge is started only when a persisted active vault exists and is resynced from React state on vault changes; no selected vault stops the bridge instead of falling back to `~/Laputa`. Stdio MCP server processes are owned by the external client that launched them: when that client closes stdin, Tolaria cancels UI-bridge reconnect timers, closes any UI WebSocket, and exits the Node process instead of keeping it alive in the background.
That setup is user-initiated through the status bar / command palette flow, not a startup side effect. Registration is non-destructive (additive, preserves other servers), uses `upsert` semantics, and can be reversed by removing Tolaria's entry again. Tolaria verifies Node.js is available before writing config, writes an explicit `type: "stdio"` entry, pins `VAULT_PATH` to the active vault, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. Packaged builds resolve `mcp-server/` from the installed resource directory next to the executable before falling back to macOS `Resources`, Linux bundle paths, and AppImage paths. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`). The desktop WebSocket bridge is started only when a persisted active vault exists and is resynced from React state on vault changes; no selected vault stops the bridge instead of falling back to `~/Laputa`.
### Architecture
@@ -408,12 +376,11 @@ flowchart LR
|----------|---------|
| `spawn_ws_bridge(vault_path)` | Spawns `ws-bridge.js` as child process with VAULT_PATH env |
| `sync_mcp_bridge_vault(vault_path?)` | Starts, restarts, or stops the desktop WebSocket bridge as the selected vault changes |
| `register_mcp(vault_path)` | Verifies Node.js, resolves the packaged `mcp-server/`, and writes Tolaria's explicit stdio entry to Claude Code, Gemini CLI, Cursor, and generic MCP configs on user request |
| `mcp_config_snippet(vault_path)` | Builds the exact `mcpServers.tolaria` JSON users can copy into any compatible client without writing third-party config files |
| `remove_mcp()` | Removes Tolaria's MCP entry from Claude Code, Gemini CLI, Cursor, and generic MCP configs |
| `register_mcp(vault_path)` | Verifies Node.js, resolves the packaged `mcp-server/`, and writes Tolaria's explicit stdio entry to Claude Code, Cursor, and generic MCP configs on user request |
| `remove_mcp()` | Removes Tolaria's MCP entry from Claude Code, Cursor, and generic MCP configs |
| `upsert_mcp_config(path, entry)` | Atomic config file update (create/merge, preserves others) |
The `WsBridgeChild` state wrapper in `lib.rs` ensures the bridge process is replaced on vault switches, stopped when no active vault is selected, and killed plus waited on app exit via the `RunEvent::Exit` handler. The same desktop layer keeps Tauri asset protocol access limited to vault roots loaded during the current app session; command calls remain active-vault scoped for reads, writes, and external opens.
The `WsBridgeChild` state wrapper in `lib.rs` ensures the bridge process is replaced on vault switches, stopped when no active vault is selected, and killed on app exit via the `RunEvent::Exit` handler. The same desktop layer now keeps the Tauri asset protocol scoped to the active vault instead of every filesystem path.
## Search
@@ -432,7 +399,7 @@ The vault cache (`src-tauri/src/vault/cache.rs`) accelerates vault scanning usin
### Cache File
`~/.laputa/cache/<vault-hash>.json` — stored outside the vault directory so it never pollutes the user's git repo. The vault path is normalized through `vault/path_identity.rs` before hashing, so macOS `/tmp` aliases and separator variants share the same cache identity. Stores: vault path, git HEAD commit hash, all VaultEntry objects. Version: v13 (bumped on VaultEntry field changes to force full rescan). Cache replacement is best-effort: Tolaria writes a temp file, fsyncs it, and renames it into place only after a short-lived writer lock plus an on-disk fingerprint check confirm another window/process has not already refreshed the cache. Failures are logged and the app falls back to rebuilding from the filesystem.
`~/.laputa/cache/<vault-hash>.json` — stored outside the vault directory so it never pollutes the user's git repo. The vault path is hashed (via `DefaultHasher`) to produce a deterministic filename. Stores: vault path, git HEAD commit hash, all VaultEntry objects. Version: v13 (bumped on VaultEntry field changes to force full rescan). Cache replacement is best-effort: Tolaria writes a temp file, fsyncs it, and renames it into place only after a short-lived writer lock plus an on-disk fingerprint check confirm another window/process has not already refreshed the cache. Failures are logged and the app falls back to rebuilding from the filesystem.
`<vault>/.tolaria-rename-txn/` — hidden, scan-ignored staging directory for crash-safe note renames. Tolaria stores temporary backup files plus one manifest per in-flight rename here. On the next vault scan, unfinished transactions are recovered before entries are listed so users do not see a missing note or a visible duplicate after a crash.
@@ -454,11 +421,11 @@ flowchart TD
## Styling
The app uses internal app-owned light and dark themes with an optional System preference (see [ADR-0081](adr/0081-internal-light-dark-theme-runtime.md) and [ADR-0112](adr/0112-system-theme-mode.md)). This is not the old vault-authored theming system from ADR-0013: users choose a mode, but themes are owned by the app.
The app uses internal app-owned light and dark themes (see [ADR-0081](adr/0081-internal-light-dark-theme-runtime.md)). This is not the old vault-authored theming system from ADR-0013: users choose a mode, but themes are owned by the app.
1. **Global CSS variables** (`src/index.css`): Semantic app colors, borders, surfaces, and interaction states. Bridged to Tailwind v4 via `@theme inline`.
2. **Editor theme** (`src/theme.json`): BlockNote-specific typography. Flattened to CSS vars by `useEditorTheme`; editor colors resolve through the same semantic app variables.
3. **Theme runtime**: Applies resolved `light` / `dark` values to `data-theme` and the shadcn-compatible `.dark` class before React consumers render, with a localStorage mirror to avoid startup flash when dark mode or System-on-dark is selected. Settings and command-palette theme actions both write the same installation-local `settings.theme_mode` value; `system` subscribes to `prefers-color-scheme` changes at runtime while explicit Light/Dark remain overrides.
3. **Theme runtime**: Applies `data-theme` and the shadcn-compatible `.dark` class before React consumers render, with a localStorage mirror to avoid startup flash when dark mode is selected.
## Localization
@@ -501,32 +468,28 @@ On first launch, `useOnboarding` checks if the default vault exists. If not, it
- **Open an existing folder** → system file picker; plain Markdown folders without `.git` open immediately in supported non-git mode
- **Get started with a template** → pick a parent folder, then call `create_getting_started_vault()` with the derived `.../Getting Started` child path so the cloned vault opens into the populated repo root immediately
If the selected vault disappears after startup, `useVaultLoader` re-checks `check_vault_exists` when reloads or vault-derived surfaces fail. A confirmed missing path clears cached entries, folders, views, modified-file state, and prefetched note content, then `App` reuses the `vault-missing` `WelcomeScreen` state so note and view actions cannot keep targeting the stale active vault.
When an opened folder is not yet a git repo, Tolaria shows a dismissible Git setup dialog and a persistent `Git disabled` status-bar warning. Markdown scanning, note browsing, note editing, and search continue normally. Git-dependent surfaces (history, changes, commit, sync, conflict resolution, remotes, AutoGit, and auto-sync) stay unavailable until the user explicitly initializes Git from the dialog, the status-bar warning, or the `Initialize Git for Current Vault` command-palette action.
When the user enables Git later, `init_git_repo` runs `git init`, ensures Tolaria's default `.gitignore`, stages the vault, and writes the initial `Initial vault setup` commit. Before app-managed setup and remote-connection commits, Tolaria ensures the vault has local `user.name` / `user.email` values, falling back to `Tolaria <vault@tolaria.md>` when the vault has no local Git identity yet. That app-managed setup commit explicitly disables commit signing for the single command so inherited global or local `commit.gpgsign` preferences cannot strand onboarding when GPG is missing or misconfigured. Later `git_commit` calls honor the user's signing configuration first, then retry the same app-managed commit once with `commit.gpgsign=false` only when Git reports a signing-helper failure, so working GPG/SSH signing setups continue to sign while broken GPG setups do not create repeated opaque commit failures.
Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code, Codex, OpenCode, Pi, and Gemini CLI are installed, offers per-agent install links when they are missing, and stores local dismissal so the prompt does not repeat on every launch.
Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code, Codex, OpenCode, and Pi are installed, offers per-agent install links when they are missing, and stores local dismissal so the prompt does not repeat on every launch.
`useGettingStartedClone` reuses the same parent-folder semantics for the status-bar / command-palette clone action, and `Toast` is rendered through the AI-agents onboarding gate so the resolved destination path stays visible right after a successful clone.
The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition.
The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition.
After the clone completes, Tolaria removes every configured git remote from the new starter vault. Getting Started vaults therefore open as local-only by default, and users opt into a remote later with the explicit Add Remote flow.
### Remote Clone & Auth Model
Tolaria no longer implements provider-specific OAuth or remote-repository APIs. All remote git work goes through the user's existing system git configuration. On macOS, git subprocesses prefer the user's login-shell `git` and `PATH` so Homebrew/Xcode Git, Git Credential Manager, and `git-credential-osxkeychain` resolve the same way they do in Terminal.
Tolaria no longer implements provider-specific OAuth or remote-repository APIs. All remote git work goes through the user's existing system git configuration.
**Flow:**
1. User opens `CloneVaultModal` from onboarding or the vault menu
2. User pastes any git URL and chooses a local destination
3. The `clone_git_repo()` Tauri command runs `git clone` inside a blocking Tokio task so the Tauri window stays responsive during slow or failing clones
4. Linux AppImage builds strip AppImage loader variables from system-git subprocesses before spawning `git`, keeping `git-remote-https` on the host git/library stack
5. `git_push()` / `git_pull()` continue to use the same system git path
6. On macOS, `git_add_remote()` asks Git's credential helper for HTTPS credentials before the first fetch so Keychain can grant access to the same saved credential item the shell uses
7. Clone commands disable interactive terminal / askpass prompts and surface the git failure back to the UI instead of freezing the app waiting for input
4. `git_push()` / `git_pull()` continue to use the same system git path
5. Clone commands disable interactive terminal / askpass prompts and surface the git failure back to the UI instead of freezing the app waiting for input
**Auth model:**
- SSH keys, Git Credential Manager, macOS Keychain helpers, `gh auth`, and other git helpers all work without app-specific setup
@@ -557,9 +520,9 @@ sequenceDiagram
participant MCP as MCP Server
participant U as User
T->>T: apply Linux AppImage WebKit env/preload safeguards<br/>(AppImage only)
T->>T: start background legacy vault housekeeping<br/>(does not block setup)
T->>MCP: start background initial ws-bridge sync<br/>(if active vault exists)
T->>T: apply Linux AppImage WebKit env override<br/>(AppImage only)
T->>T: run_startup_tasks()<br/>(migrate + seed only)
T->>MCP: spawn_ws_bridge() — ports 9710 + 9711
T->>A: App mounts
A->>A: useOnboarding — vault exists?
@@ -567,16 +530,10 @@ sequenceDiagram
A-->>U: WelcomeScreen
else Vault found
A->>VL: useVaultLoader fires
VL->>T: invoke('reload_vault') → allow requested vault roots in asset scope + scan_vault_cached()
VL->>T: invoke('reload_vault') → sync active vault asset scope + scan_vault_cached()
T-->>VL: VaultEntry[]
VL->>T: invoke('get_modified_files')
alt Runtime vault path disappears
VL->>T: invoke('check_vault_exists')
VL-->>A: unavailable vault path + cleared stale state
A-->>U: WelcomeScreen (vault missing)
end
A->>T: useMcpStatus — check explicit MCP setup state
A->>T: sync_mcp_bridge_vault(selected path)
VL-->>A: entries ready
end
@@ -584,10 +541,9 @@ sequenceDiagram
A->>T: invoke('get_note_content')
T-->>A: raw markdown
A->>A: splitFrontmatter → [yaml, body]
A->>A: preProcessDurableEditorMarkdown(body)
A->>A: preProcessWikilinks(body)
A->>A: tryParseMarkdownToBlocks()
A->>A: injectWikilinks + injectDurableEditorMarkdownBlocks(blocks)
A->>A: injectWikilinks(blocks)
A-->>U: Editor renders note
```
@@ -664,12 +620,12 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `parsing.rs` | Text processing: snippet extraction, markdown stripping, ISO date parsing, `extract_title` (H1 → legacy frontmatter → filename), `slug_to_title` |
| `title_sync.rs` | Legacy filename → `title` frontmatter sync helper; no longer used by the normal note-open flow |
| `cache.rs` | Git-based incremental vault caching (`scan_vault_cached`), git helpers |
| `ignored.rs` | Gitignored-content visibility filtering via batched, pipe-safe `git check-ignore` |
| `ignored.rs` | Gitignored-content visibility filtering via batched `git check-ignore` |
| `filename_rules.rs` | Cross-platform validation for note filenames, folder names, and custom view filenames |
| `rename.rs` | `rename_note` / `rename_note_filename` / `move_note_to_folder` — stage crash-safe file moves, update `title` frontmatter when needed, recover unfinished rename transactions, and report backlink rewrite failures |
| `image.rs` | `save_image` / `copy_image_to_vault` — save editor image attachments with sanitized filenames |
| `migration.rs` | `flatten_vault`, `vault_health_check`, `migrate_is_a_to_type` |
| `config_seed.rs` | Maintains vault AI guidance (`AGENTS.md`, `CLAUDE.md`, and optional `GEMINI.md` shims), migrates legacy `config/agents.md`, and repairs missing root type scaffolding such as `type.md` and `note.md` |
| `config_seed.rs` | Maintains vault AI guidance (`AGENTS.md` + `CLAUDE.md` shim), migrates legacy `config/agents.md`, and repairs missing root type scaffolding such as `type.md` and `note.md` |
| `getting_started.rs` | Clones and normalizes the public Getting Started starter vault |
## Rust Backend Modules
@@ -680,9 +636,8 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `frontmatter/` | YAML frontmatter read/write (`mod.rs`, `yaml.rs`, `ops.rs`) |
| `git/` | Git operations (`commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`, `clone.rs`, `connect.rs`) |
| `search.rs` | Keyword search — walkdir-based vault file scan with Gitignored-content visibility filtering |
| `ai_agents.rs` | CLI-agent request normalization and adapter dispatch |
| `cli_agent_runtime.rs` | Shared CLI-agent request, prompt, subprocess, version, and MCP path helpers |
| `claude_cli.rs`, `codex_cli.rs`, `opencode_cli.rs`, `pi_cli.rs`, `gemini_cli.rs` | CLI-agent command/config/event adapters |
| `ai_agents.rs` | Shared CLI-agent detection, stream normalization, and adapter dispatch |
| `claude_cli.rs` | Claude Code subprocess spawning + NDJSON stream parsing |
| `pi_cli.rs`, `pi_config.rs`, `pi_discovery.rs`, `pi_events.rs` | Pi subprocess launch, transient MCP adapter config, discovery, and JSON stream parsing |
| `mcp.rs` | MCP server spawning + explicit config registration/removal |
| `commands/` | Tauri command handlers (split into submodules) |
@@ -704,20 +659,18 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `rename_note` | Crash-safe note rename + `title` frontmatter update + cross-vault wikilinks + failed backlink counts |
| `move_note_to_folder` | Crash-safe folder move that preserves the filename, reloads the moved note, and rewrites path-based wikilinks |
| `create_vault_folder` | Create a folder relative to the active vault root |
| `list_vault_folders` | Build the folder tree on the blocking Tokio pool, then apply Gitignored-content visibility → `Vec<FolderNode>` |
| `rename_vault_folder` | Rename a folder relative to the active vault root and return old/new relative paths |
| `delete_vault_folder` | Permanently delete a folder subtree relative to the active vault root |
| `sync_note_title` | Legacy helper: rewrite `title` frontmatter from filename → `bool` (modified); not used by the normal note-open flow |
| `batch_archive_notes` | Archive multiple notes |
| `batch_delete_notes` | Permanently delete notes from disk |
| `reload_vault` | Allow the requested vault roots in the runtime asset scope, invalidate cache, full rescan from filesystem, then apply Gitignored-content visibility → `Vec<VaultEntry>` |
| `reload_vault` | Sync the active vault asset scope, invalidate cache, full rescan from filesystem, then apply Gitignored-content visibility → `Vec<VaultEntry>` |
| `reload_vault_entry` | Re-read a single file from disk → `VaultEntry` |
| `open_vault_file_external` | Validate an existing file against the active vault boundary, then open it with the system default app |
| `start_vault_watcher` / `stop_vault_watcher` | Start or stop native active-vault filesystem change events |
| `check_vault_exists` | Check if vault path exists |
| `create_empty_vault` | Create a git-backed vault, then seed root `AGENTS.md`, `CLAUDE.md`, `type.md`, and `note.md` defaults |
| `create_getting_started_vault` | Clone the public Getting Started vault, refresh Tolaria-managed guidance/config defaults, and keep the cloned repo clean |
| `get_vault_ai_guidance_status` | Report whether `AGENTS.md`, `CLAUDE.md`, and optional `GEMINI.md` guidance are managed, missing, broken, or custom |
| `get_vault_ai_guidance_status` | Report whether `AGENTS.md` and the `CLAUDE.md` shim are managed, missing, broken, or custom |
| `restore_vault_ai_guidance` | Restore any missing/broken Tolaria-managed guidance files without overwriting custom ones |
### Frontmatter
@@ -769,15 +722,11 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|---------|-------------|
| `stream_claude_chat` | Claude CLI chat mode (streaming) |
| `check_claude_cli` | Check if Claude CLI is available |
| `get_ai_agents_status` | Check Claude Code + Codex + OpenCode + Pi + Gemini availability |
| `get_agent_docs_path` | Resolve the bundled local Tolaria docs folder used in AI-agent system prompts |
| `stream_ai_agent` | Stream Claude Code, Codex, OpenCode, Pi, or Gemini through the normalized agent event layer |
| `register_mcp_tools` | Register MCP in Claude/Gemini/Cursor/generic config for the active vault |
| `remove_mcp_tools` | Remove Tolaria's MCP entry from Claude/Gemini/Cursor/generic config |
| `check_mcp_status` | Check whether the active vault is explicitly registered in Claude/Gemini/Cursor/generic config |
| `get_mcp_config_snippet` | Return the exact manual MCP JSON snippet for the active vault |
| `copy_text_to_clipboard` | Copy setup snippets through the native desktop clipboard command path |
| `read_text_from_clipboard` | Read current desktop clipboard text for command-driven plain-text paste |
| `get_ai_agents_status` | Check Claude Code + Codex + OpenCode + Pi availability |
| `stream_ai_agent` | Stream Claude Code, Codex, OpenCode, or Pi through the normalized agent event layer |
| `register_mcp_tools` | Register MCP in Claude/Cursor/generic config for the active vault |
| `remove_mcp_tools` | Remove Tolaria's MCP entry from Claude/Cursor/generic config |
| `check_mcp_status` | Check whether the active vault is explicitly registered in Claude/Cursor/generic config |
| `sync_mcp_bridge_vault` | Sync the desktop WebSocket bridge process to the selected vault, or stop it when no vault is selected |
The desktop MCP WebSocket bridge is intentionally local-only. `mcp-server/ws-bridge.js` binds both bridge ports to loopback, rejects non-loopback clients, accepts browser/Tauri origins only on the UI bridge, and rejects browser-origin requests on the tool bridge so remote pages cannot drive vault tools directly.
@@ -794,8 +743,8 @@ The desktop MCP WebSocket bridge is intentionally local-only. `mcp-server/ws-bri
| `save_vault_config` | Save per-vault UI config |
| `get_default_vault_path` | Get default vault path |
| `get_build_number` | Get app build number |
| `save_image` | Save base64 image to `attachments/` and ensure the vault root is in the runtime asset scope |
| `copy_image_to_vault` | Copy image file to `attachments/` and ensure the vault root is in the runtime asset scope |
| `save_image` | Save base64 image to `attachments/` and refresh the active vault asset scope |
| `copy_image_to_vault` | Copy image file to `attachments/` and refresh the active vault asset scope |
| `update_menu_state` | Update native menu checkmarks and enabled/disabled state for selection-dependent actions |
| `trigger_menu_command` | Emit a native menu command ID for deterministic shortcut QA |
| `update_current_window_min_size` | Update the active Tauri window's minimum size and optionally grow it to fit restored panes |
@@ -836,15 +785,15 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
| `useTabManagement` | Navigation history, note switching | Note navigation lifecycle |
| `useVaultSwitcher` | `vaultPath`, `extraVaults` | Vault switching |
| `useTheme` | Editor theme CSS vars and theme-mode bridge | Editor typography and app theme runtime |
| `useCliAiAgent` | `messages`, `status`, tool actions | Selected AI agent conversation backed by the shared session pipeline and vault permission mode |
| `useCliAiAgent` | `messages`, `status`, tool actions | Selected AI agent conversation backed by the shared session pipeline |
| `useAutoSync` | Sync interval, pull/push state | Git auto-sync |
| `useAutoGit` | Last activity timestamp, idle/inactive checkpoint triggers | Automatic commit/push checkpoints |
| `useCommitFlow` | Commit dialog state, shared manual/automatic checkpoint runner | Git commit/push orchestration |
| `useGitRemoteStatus` | `remoteStatus`, `refreshRemoteStatus()` | On-demand remote detection for commit UI |
| `useUnifiedSearch` | Query, results, loading state | Keyword search |
| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, AutoGit thresholds, default AI agent, Gitignored-content visibility, All Notes file visibility) | Persistent settings |
| `useVaultConfig` | Per-vault UI preferences, AI permission mode | Vault-specific config |
| `appCommandDispatcher` | Manifest-backed shortcut/menu command IDs | Shared execution path for renderer and native menu commands |
| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, AutoGit thresholds, default AI agent, Gitignored-content visibility) | Persistent settings |
| `useVaultConfig` | Per-vault UI preferences | Vault-specific config |
| `appCommandDispatcher` | Canonical shortcut/menu command IDs | Shared execution path for renderer and native menu commands |
Data flows unidirectionally: `App` passes data and callbacks as props to child components. No child-to-child communication — everything goes through `App`.
@@ -858,7 +807,6 @@ Data flows unidirectionally: `App` passes data and callbacks as props to child c
| Cmd+S | Save current note |
| Cmd+F | Find in current note when the editor is focused; otherwise note-list search can claim it |
| Cmd+Shift+F | Find in vault |
| Cmd+Shift+V | Paste without Formatting into the active supported editing surface |
| Cmd+[ / Cmd+] | Navigate back / forward (replaces tabs) |
| Cmd+Z / Cmd+Shift+Z | Undo / Redo |
| Cmd+19 | Switch to tab N |
@@ -869,14 +817,12 @@ Selection-dependent actions are wired through the command palette and the native
Shortcut routing is explicit:
- `src/shared/appCommandManifest.json` is the shared command/menu metadata source for command IDs, menu labels, accelerators, native-menu enablement groups, and deterministic QA flags
- `appCommandCatalog.ts` derives renderer command IDs, shortcut lookup maps, Linux menu sections, and QA metadata from that manifest
- `appCommandCatalog.ts` is the shared shortcut manifest for command IDs, modifier rules, and deterministic QA metadata
- `formatShortcutDisplay()` derives platform-accurate visible shortcut labels (`⌘` on macOS, `Ctrl` on Windows/Linux) from that same manifest so menus, tooltips, and command-palette copy stay aligned with real accelerators
- `useAppKeyboard` is the primary execution path for real shortcut keypresses, including Tauri runs
- macOS browser-reserved chords such as `Cmd+O`, `Cmd+F`, and `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path
- `Cmd+Shift+V` uses the same command path for "Paste without Formatting"; `plainTextPaste.ts` reads text through the native clipboard command in Tauri and inserts it through the active rich/raw editor target or the focused browser text control
- `Cmd+F` is surface-aware: editor focus opens current-note find/replace in raw CodeMirror, note-list focus preserves note-list search, and native menu enablement follows focus availability events so only one `Cmd+F` menu item is active
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same manifest-derived command IDs for native menu clicks, accelerators, and custom titlebar menu actions; on Windows, `menu.rs` also listens to main-window menu events because Tauri attaches the native menu to the `WebviewWindow`
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same command IDs for native menu clicks, accelerators, and custom titlebar menu actions
- `appCommandDispatcher.ts` suppresses the paired native-menu/renderer echo from a single shortcut so the command runs once
- Deterministic QA uses two explicit proof paths from the shared manifest:
- renderer shortcut-event proof through `window.__laputaTest.triggerShortcutCommand()`
@@ -906,8 +852,7 @@ push to main
→ generate alpha-latest.json with darwin-aarch64, darwin-x86_64, Linux, and Windows updater URLs
→ publish GitHub prerelease alpha-vYYYY.M.D-alpha.NNNN named Tolaria Alpha YYYY.M.D.N
→ pages job:
→ build VitePress public docs into the GitHub Pages root
→ build static HTML release history page at /releases/
→ build static HTML release history page
→ publish alpha/latest.json
→ refresh latest.json + latest-canary.json as compatibility aliases to alpha
→ preserve stable/latest.json
@@ -933,10 +878,8 @@ push stable-vYYYY.M.D tag
→ generate stable-latest.json with macOS Apple Silicon, macOS Intel, Linux, and Windows updater URLs plus platform-specific manual download URLs
→ publish GitHub release Tolaria YYYY.M.D
→ pages job:
→ build VitePress public docs into the GitHub Pages root
→ build static HTML release history page at /releases/
→ publish stable/latest.json
→ publish stable/download/ and download/ as permanent download pages that keep the browser page visible while the platform installer starts
→ publish stable/download/ and download/ as permanent redirect URLs for the latest stable platform installer
→ preserve alpha/latest.json
→ deploy to gh-pages
```
@@ -995,12 +938,11 @@ sequenceDiagram
- `anonymous_id` is a locally-generated UUID, never tied to identity
- `send_default_pii: false` on both SDKs
- PostHog: `autocapture: false`, `persistence: 'memory'`, no cookies
- Product events use categorical metadata only: file preview kind/action, AI agent id/permission mode/counts/status, and All Notes visibility category/enabled state.
**Architecture:**
- **Rust:** `sentry` crate initialized in `lib.rs::setup()` via `telemetry::init_sentry_from_settings()`
- **JS:** `@sentry/react` + `posthog-js` initialized lazily by `useTelemetry` hook; the React root also wires `onCaughtError`, `onUncaughtError`, and `onRecoverableError` through `Sentry.reactErrorHandler()` so production React invariants include component stack context when crash reporting is enabled.
- **Release grouping:** packaged release workflows pass `VITE_SENTRY_RELEASE` from the computed build version, but the app only assigns Sentry's `release` field for stable calendar builds (`YYYY.M.D`). Alpha/prerelease/internal builds omit `release` so they do not create normal Sentry Releases entries, while both frontend and Rust Sentry scopes tag `tolaria.build_version` and `tolaria.release_kind` for diagnostics.
- **Release grouping:** packaged release workflows pass `VITE_SENTRY_RELEASE` from the computed build version so frontend Sentry events group by the shipped alpha or stable version.
- **Settings:** `telemetry_consent`, `crash_reporting_enabled`, `analytics_enabled`, `anonymous_id` in `Settings` struct
- **Consent:** `TelemetryConsentDialog` shown when `telemetry_consent === null`
@@ -1055,7 +997,7 @@ Desktop-only modules gated at the crate level:
Desktop-only features gated at the function level in `commands/`:
- Git operations (commit, pull, push, status, history, diff, conflicts)
- Clone-by-URL via system git (`clone_repo`)
- CLI AI agent streaming (Claude, Codex, OpenCode, Pi, Gemini)
- CLI AI agent streaming (Claude, Codex, OpenCode, Pi)
- MCP registration and status
- Menu state updates

View File

@@ -29,22 +29,6 @@ If you run the desktop app on Linux, install Tauri's WebKit2GTK 4.1 dependencies
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 AppImages automatically disable unstable WebKitGTK AppImage rendering paths 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.
## Quick Start
```bash
@@ -69,8 +53,6 @@ pnpm playwright:regression # Full Playwright regression suite
`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`. Before Tolaria spawns that `git` process, it removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` so HTTPS clone helpers use the host git libraries instead of bundled AppImage libraries.
## Directory Structure
```
@@ -97,7 +79,7 @@ tolaria/
│ │ ├── RawEditorView.tsx # CodeMirror raw editor
│ │ ├── Inspector.tsx # Fourth panel: metadata + relationships
│ │ ├── DynamicPropertiesPanel.tsx # Editable frontmatter properties
│ │ ├── AiPanel.tsx # AI agent panel (selected CLI agent + per-vault permission mode)
│ │ ├── AiPanel.tsx # AI agent panel (selected CLI agent)
│ │ ├── AiMessage.tsx # Agent message display
│ │ ├── AiActionCard.tsx # Agent tool action cards
│ │ ├── AiAgentsOnboardingPrompt.tsx # First-launch AI agent installer prompt
@@ -132,8 +114,7 @@ tolaria/
│ │ ├── 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/Gemini availability polling
│ │ ├── useAiAgentsStatus.ts # Claude/Codex/OpenCode/Pi availability polling
│ │ ├── useAiAgentPreferences.ts # Default-agent persistence + cycling
│ │ ├── useAiActivity.ts # MCP UI bridge listener
│ │ ├── useAutoSync.ts # Auto git pull/push
@@ -159,7 +140,6 @@ tolaria/
│ ├── 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
@@ -208,11 +188,9 @@ tolaria/
│ │ │ ├── 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
│ │ ├── ai_agents.rs # Shared CLI-agent detection + stream adapters
│ │ ├── claude_cli.rs # Claude CLI subprocess management
│ │ ├── pi_cli.rs # Pi CLI subprocess management
│ │ ├── mcp.rs # MCP server lifecycle + explicit config registration/removal
│ │ ├── app_updater.rs # Alpha/stable updater endpoint selection
│ │ ├── settings.rs # App settings persistence
@@ -283,9 +261,9 @@ tolaria/
| `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/gemini_cli.rs` | Per-agent command, config, discovery, and JSON event adapters. |
| `src-tauri/src/ai_agents.rs` | Shared CLI-agent availability checks, adapter dispatch, and stream normalization. |
| `src-tauri/src/claude_cli.rs` | Claude CLI subprocess spawning + NDJSON stream parsing. |
| `src-tauri/src/pi_cli.rs` | Pi subprocess spawning through JSON mode and transient MCP adapter config. |
| `src-tauri/src/app_updater.rs` | Desktop updater bridge — selects alpha/stable manifests and streams install progress. |
### Editor
@@ -304,10 +282,9 @@ tolaria/
| File | Why it matters |
|------|---------------|
| `src/components/AiPanel.tsx` | AI agent panel — selected CLI agent with tool execution, reasoning, actions, and per-vault permission mode. |
| `src/components/AiPanel.tsx` | AI agent panel — selected CLI agent with tool execution, reasoning, and actions. |
| `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. |
@@ -316,19 +293,19 @@ tolaria/
| 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/index.css` | Semantic CSS custom properties for app-owned light/dark themes. |
| `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, auto-sync interval, default note width, sidebar type pluralization, default AI agent). |
| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, 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, AI permission mode). |
| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, sync interval, UI language, content display preferences, default AI agent, and the vault-level explicit organization toggle. |
| `src/hooks/useVaultConfig.ts` | Per-vault local UI preferences (zoom, view mode, colors, Inbox columns, explicit organization workflow). |
| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, sync interval, UI language, 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
@@ -364,7 +341,7 @@ type SidebarSelection =
### 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 Windows, native menu clicks arrive from the main `WebviewWindow`, so `src-tauri/src/menu.rs` must keep its window-scoped menu event handler in addition to the app-level handler. On Linux, `LinuxTitlebar.tsx` and `LinuxMenuButton.tsx` reuse the same command IDs through `trigger_menu_command` because the native GTK menu bar is intentionally not mounted. The same shortcut manifest also declares the deterministic QA mode for each shortcut-capable command.
`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. 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. 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, `LinuxTitlebar.tsx` and `LinuxMenuButton.tsx` reuse the same command IDs through `trigger_menu_command` because the native GTK menu bar is intentionally not mounted. 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.
@@ -409,7 +386,7 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
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
4. Call it from the frontend via `invoke()` in the appropriate hook
5. Add a mock handler in `mock-tauri.ts`
### Add a new component
@@ -421,7 +398,7 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
### Add a new entity type
1. Create a type document at the vault root: `mytype.md` with `type: Type` frontmatter (icon, color, order, etc.)
1. Create a type document: `type/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
@@ -443,14 +420,11 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
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_*`, `gemini_*`). Keep Codex Safe on `read-only` + `untrusted` and Codex Power User on active-vault `workspace-write` + `never`, keep Pi and Gemini on transient MCP config, and do not use dangerous permission bypasses unless an ADR explicitly designs a new mode. Gemini Power User intentionally uses Gemini's `yolo` mode per ADR-0103.
4. **Claude CLI arguments**: Edit `src-tauri/src/claude_cli.rs` (`run_agent_stream()`; keep app-managed launches on strict Tolaria MCP config, `acceptEdits`, and the scoped file/search tool list)
5. **Shared agent adapters / Codex/Pi args**: Edit `src-tauri/src/ai_agents.rs` plus the per-agent adapter modules (keep Codex sandboxed with active-vault `workspace-write`, keep Pi on transient MCP config, and do not use dangerous permission bypasses unless an ADR explicitly designs a new mode)
### Work with external MCP setup
1. **Backend registration/status/snippets**: Edit `src-tauri/src/mcp.rs`; registration and manual config generation must verify Node.js first, resolve the packaged `mcp-server/` for macOS, Windows, Linux package roots (`/usr/local/Tolaria`, `/usr/local/lib/tolaria`, `/usr/lib/tolaria`, `/usr/lib/tolaria/resources`), and AppImage installs, and use an explicit stdio entry with `VAULT_PATH` plus `WS_UI_PORT=9711`
2. **Setup dialog copy/actions**: Edit `src/components/McpSetupDialog.tsx` and `src/hooks/useMcpStatus.ts`; users should see the Node.js prerequisite, the exact generated manual config, and a copy action before Tolaria writes third-party config files
1. **Backend registration/status**: Edit `src-tauri/src/mcp.rs`; registration must verify Node.js first, resolve the packaged `mcp-server/` for macOS, Windows, Linux, and AppImage installs, and write an explicit stdio entry with `VAULT_PATH` plus `WS_UI_PORT=9711`
2. **Setup dialog copy/actions**: Edit `src/components/McpSetupDialog.tsx`; users should see the Node.js prerequisite and the manual config shape 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. **Gemini CLI compatibility**: Keep `~/.gemini/settings.json` in the registration path list and keep optional `GEMINI.md` generation behind `restore_vault_ai_guidance`; app-managed Gemini sessions still require the user to install and sign in to Gemini CLI, but Tolaria supplies transient MCP settings when Gemini is selected as the default AI agent
5. **Process lifecycle**: 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

View File

@@ -1,53 +0,0 @@
# Large Vault Loading QA
Use this when validating startup responsiveness for large vaults. The goal is to make the bottleneck reproducible without using a real user vault.
## Synthetic Vault
Create a disposable vault with many markdown files:
```bash
VAULT="$(mktemp -d /tmp/tolaria-large-vault.XXXXXX)"
mkdir -p "$VAULT/type" "$VAULT/archive" "$VAULT/assets"
cat > "$VAULT/type/project.md" <<'EOF'
---
is_a: Type
---
# Project
EOF
for i in $(seq -w 1 20000); do
cat > "$VAULT/project-$i.md" <<EOF
---
is_a: Project
status: Active
related_to:
- "[[project-00001]]"
---
# Project $i
Synthetic body $i with enough text to exercise parsing and snippets.
EOF
done
git -C "$VAULT" init
git -C "$VAULT" config user.email qa@example.invalid
git -C "$VAULT" config user.name "Tolaria QA"
git -C "$VAULT" add .
git -C "$VAULT" commit -m "seed large vault"
echo "$VAULT"
```
## Manual QA
1. Start Tolaria with `pnpm tauri dev`.
2. Open the synthetic vault path printed above.
3. Verify the main shell renders before the full note list finishes indexing.
4. Confirm the status bar shows vault activity while indexing is still in progress.
5. Use keyboard-only flows while indexing continues:
- Cmd+K opens the command palette.
- Cmd+P opens quick open; results may be partial or empty until indexing finishes.
- Create a new note with Cmd+N and type in the editor.
6. Wait for indexing to finish and verify the note list/search state is consistent.
The synthetic vault lives under `/tmp`; remove it after QA if it is no longer needed.

View File

@@ -8,7 +8,7 @@ This document records the phase 1 information architecture for public Tolaria do
|---|---|---|
| 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/` |
| Power users | Understand file layout, frontmatter, filters, shortcuts, and platform support | `site/reference/` |
| Contributors and agents | Architecture, abstractions, ADRs, development workflow | `docs/`, `AGENTS.md` |
## Hosting Shape
@@ -25,19 +25,6 @@ The GitHub Pages output should reserve the root for public docs and mount releas
/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 Gemini 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

View File

@@ -2,9 +2,8 @@
type: ADR
id: "0071"
title: "External vault updates reload derived state and reopen the clean active note"
status: superseded
status: active
date: 2026-04-21
superseded_by: "0111"
---
## Context

View File

@@ -1,35 +0,0 @@
---
type: ADR
id: "0091"
title: "Gemini CLI external AI setup"
status: active
date: 2026-04-28
---
## Context
Tolaria already supports explicit MCP setup for external desktop AI tools. Users asked for Gemini CLI support so the same active-vault MCP server can be registered where Gemini reads tool configuration, with optional Gemini-specific vault guidance.
Gemini CLI reads MCP server definitions from `~/.gemini/settings.json` and can load project guidance from `GEMINI.md`. Tolaria must support those conventions without silently rewriting global user settings or overwriting user-authored vault instructions.
## Decision
Tolaria adds `~/.gemini/settings.json` to the explicit external AI setup path list. The existing MCP entry shape is reused: `mcpServers.tolaria` runs the packaged stdio server through Node.js, pins `VAULT_PATH` to the selected vault, and sets `WS_UI_PORT=9711`.
Vault guidance keeps `AGENTS.md` as the canonical shared source. `restore_vault_ai_guidance` can create or repair a managed `GEMINI.md` shim that imports `AGENTS.md`, while bootstrap and repair flows continue to seed only required Tolaria guidance (`AGENTS.md` and `CLAUDE.md`) plus type scaffolding. Custom `GEMINI.md` files are classified as custom and are not overwritten.
The setup dialog documents that Gemini CLI still needs its own install and sign-in. Tolaria does not store Gemini credentials or model-provider API keys.
## Options Considered
- **Add Gemini to explicit MCP setup and optional guidance restore** (chosen): matches the existing consent boundary, preserves user settings, and gives Gemini the shared vault context.
- **Generate `GEMINI.md` automatically for every vault**: simpler discovery, but turns an optional third-party compatibility file into startup side effect and dirties vaults for users who do not use Gemini.
- **Document manual Gemini setup only**: avoids code changes, but leaves users to transpose paths and loses the active-vault safety already available for other MCP clients.
- **Create a Gemini-specific MCP entry shape**: unnecessary because Gemini accepts the same `mcpServers` entry structure used by the existing Tolaria MCP server.
## Consequences
- External AI setup now writes/removes Tolaria's MCP entry in Claude, Gemini, Cursor, and generic MCP config files.
- Gemini users can create a managed `GEMINI.md` shim without duplicating the canonical `AGENTS.md` guidance.
- Existing vault bootstrap and repair remain non-invasive for users who do not use Gemini.
- Native QA requires a local Gemini CLI install and authentication for an end-to-end Gemini prompt; otherwise the app can still verify the generated config and documentation paths.

View File

@@ -1,39 +0,0 @@
---
type: ADR
id: "0092"
title: "Vault-scoped AI agent permission modes"
status: active
date: 2026-04-28
---
## Context
ADR-0074 established explicit setup and least-privilege defaults for desktop AI tools. The in-app AI panel now supports multiple local CLI agents, and users need a clear per-vault way to choose whether an agent should stay in the narrow vault-safe profile or use broader local-work tools for that vault.
The mode must be visible at the point of use, must not mutate global CLI settings, and must not silently restore dangerous bypass flags. Existing transcripts should remain intact when the mode changes because a change applies to the next agent run, not to a process that is already streaming.
## Decision
**Tolaria stores an `ai_agent_permission_mode` per vault with values `safe` and `power_user`, defaulting missing or null values to `safe`, and passes that normalized mode through the AI panel stream request into each CLI adapter.**
The AI panel header displays the current mode and offers a compact Vault Safe / Power User control that is disabled while an agent run is active. Changing the mode preserves the transcript and inserts a local transcript marker.
Adapter mappings remain conservative:
- Claude Code Safe keeps `acceptEdits`, strict Tolaria MCP config, and file/search/edit tools only; Power User adds Bash to the allowed tool list without using `--dangerously-skip-permissions`.
- Codex keeps the active-vault `workspace-write` sandbox and `--ask-for-approval never` in both modes.
- OpenCode uses transient `OPENCODE_CONFIG_CONTENT`; Safe denies bash and external directories, while Power User allows bash but still denies external directories.
- Pi receives the mode on the adapter request path; both modes currently use the same transient MCP adapter config.
## Options Considered
- **Per-vault Safe / Power User modes** (chosen): makes the permission surface explicit where the agent is used and preserves least-privilege defaults for each vault.
- **Global app setting**: simpler storage, but a single toggle can over-apply a power-user profile to unrelated vaults.
- **Dangerous bypass mode**: maximizes CLI freedom, but violates ADR-0074's least-privilege boundary and needs a separate explicit security decision.
- **Adapter-specific UI switches**: exposes too much implementation detail and makes cross-agent behavior harder to reason about.
## Consequences
- Vault config normalization owns the safe default for old vaults and malformed values.
- Agent requests now carry a permission mode through frontend and Rust boundaries, so new adapters must choose an explicit mapping.
- Power User is intentionally not equivalent across agents; where an adapter lacks a safe broader local-work switch, both modes may map to the same conservative behavior and must document that with tests.
- Any future dangerous mode requires a new ADR and separate UI language.

View File

@@ -1,35 +0,0 @@
---
type: ADR
id: "0093"
title: "Shared CLI agent runtime adapters"
status: active
date: 2026-04-29
---
## Context
Tolaria supports Claude Code, Codex, OpenCode, and Pi as local CLI agents in the AI panel. Each agent has different command-line arguments, configuration shape, and JSON event schema, but the Rust backend had grown repeated runtime plumbing around those differences: request shapes, prompt wrapping, subprocess launch, stdout JSON reading, stderr capture, exit handling, done events, version probing, and Tolaria MCP server path resolution.
That duplication made small runtime fixes expensive because they had to be repeated across several adapter files. It also kept Codex-specific command and event mapping inside `ai_agents.rs`, making the top-level module both an orchestrator and a bespoke adapter.
## Decision
**Tolaria uses `cli_agent_runtime.rs` as the shared runtime scaffold for app-managed CLI agents, while `ai_agents.rs` only normalizes and dispatches requests to per-agent adapter modules.**
The shared scaffold owns the common agent request shape, system/user prompt wrapping, JSON-line process lifecycle, normalized error/done handling for `AiAgentStreamEvent` adapters, version probing, and Tolaria MCP server path resolution. Per-agent modules keep the provider-specific pieces: binary discovery candidates, command arguments, transient config shape, authentication error wording, and JSON event mapping.
Codex now lives in `codex_cli.rs`, matching the Claude, OpenCode, and Pi adapter boundary. `ai_agents.rs` remains the Tauri-facing orchestrator that chooses an adapter and maps Claude's legacy event enum into the normalized event stream.
## Options Considered
- **Shared runtime scaffold with thin adapters** (chosen): reduces repeated process lifecycle code without hiding provider-specific command/config/event behavior.
- **One trait object per agent**: more uniform on paper, but adds indirection without removing much current complexity.
- **Leave each adapter self-contained**: keeps local readability for a single file, but new process, prompt, and MCP fixes continue to land in parallel.
- **Fully generic event mapping**: over-abstracts the JSON schemas and makes provider-specific edge cases harder to test.
## Consequences
- Runtime lifecycle fixes should usually start in `cli_agent_runtime.rs`.
- New agent adapters should use the shared request/prompt/process helpers and keep only command, config, discovery, and event mapping local.
- `ai_agents.rs` should not grow provider-specific runtime code again; it should normalize the frontend request, dispatch to an adapter, and map any legacy event shape.
- The shared scaffold deliberately does not erase provider differences. Authentication messages, permission semantics, and transient config formats remain adapter-owned and must stay covered by adapter tests.

View File

@@ -1,37 +0,0 @@
---
type: ADR
id: "0094"
title: "Gitignored content visibility as a command-boundary filter"
status: active
date: 2026-04-29
---
## Context
Tolaria's vault scanner now indexes more of the real filesystem so Folder views, search, and reload flows can reflect what is actually in the vault. In Git-backed vaults, that includes generated, local-only, or machine-specific content that users intentionally hide through `.gitignore`.
Always showing Gitignored files makes Folder lists and search noisy, especially in vaults that contain exports, build artifacts, or personal local scratch files. But removing those files during scanning would make visibility dependent on cache shape, complicate toggling, and blur the distinction between "what exists in the vault" and "what this installation chooses to surface."
## Decision
**Tolaria keeps the vault scan and cache complete, then applies Gitignored-content visibility at the command boundary before entries, folders, or search results reach React.**
- `hide_gitignored_files` is an installation-local app setting and defaults to `true`.
- Visibility checks use batched `git check-ignore --no-index --stdin` so Tolaria follows normal Git ignore and negation semantics as closely as practical.
- `list_vault`, `reload_vault`, `list_vault_folders`, and keyword search all apply the same filter when the setting is enabled.
- Toggling the setting reloads the current vault surfaces instead of rebuilding a different cache format.
- If a vault has no `.gitignore`, or Gitignored visibility is turned off, Tolaria shows the full scanned result.
## Options considered
- **Complete scan/cache + boundary filter** (chosen): keeps the filesystem model authoritative, makes toggling cheap and consistent, and avoids cache divergence.
- **Skip Gitignored content during scan/cache**: reduces later filtering work, but makes visibility part of the persisted cache shape and complicates instant toggling.
- **Always show Gitignored content**: simplest implementation, but too noisy for real Git-backed vaults and undermines users' existing ignore rules.
## Consequences
- Gitignored visibility is a per-installation comfort preference, not vault-authored shared metadata.
- Search, folder lists, and note reloads stay aligned because they all consult the same boundary filter.
- The cache can still support future visibility changes without a data migration.
- Users can reveal ignored content again immediately by disabling the setting.
- Future features that expose vault file lists should apply the same boundary filter unless they intentionally need raw filesystem output.

View File

@@ -1,35 +0,0 @@
---
type: ADR
id: "0095"
title: "Saved views use an explicit YAML order field"
status: active
date: 2026-04-29
---
## Context
Saved Views already persist as user-editable YAML files in the vault and sync through Git. Filename ordering was stable, but it forced users to rename files just to change sidebar order and gave Tolaria no durable way to support drag reordering, move actions, or keyboard-first ordering controls.
The ordering choice also needs to travel with the view definition itself. Saved Views are part of the vault's shared information architecture, not a machine-local preference.
## Decision
**Each Saved View may store an optional top-level `order` number in its YAML definition, and Tolaria sorts views by that value before falling back to filename.**
- Lower `order` values render earlier in the sidebar and other Saved View lists.
- Views without `order` sort after ordered views and then fall back to filename ordering for stability.
- Reordering actions rewrite affected view files with a dense sequence of order values instead of encoding position in filenames.
- The same persisted order supports drag handles, explicit move buttons, and command-palette ordering actions.
## Options considered
- **Explicit `order` field in the view YAML** (chosen): portable, Git-syncable, easy to inspect by hand, and consistent with the existing file-first view model.
- **Filename-based ordering only**: no schema change, but makes reordering clumsy and couples user-visible structure to file naming.
- **App-local ordering state**: easy to prototype, but breaks cross-device consistency and separates ordering from the view artifact users already version.
## Consequences
- Saved View ordering becomes part of the vault and syncs naturally through Git.
- Existing views remain valid; unordered files keep a stable fallback sort until reordered.
- Reordering can touch multiple view files in one action because Tolaria normalizes the sequence.
- Future Saved View features should treat `order` as part of the shared YAML schema rather than introducing a parallel ordering store.

View File

@@ -1,37 +0,0 @@
---
type: ADR
id: "0096"
title: "Root-created type documents"
status: active
date: 2026-04-29
---
## Context
Tolaria identifies type definitions by markdown frontmatter (`type: Type`), not by filesystem location. Older documentation and UI creation flows still treated `type/` as the canonical destination for new type documents, with a compatibility fallback for vaults that already used `types/`.
That folder-based creation policy conflicted with the broader vault model: notes are scanned from all non-hidden folders, type identity comes from metadata, and root `type.md` / `note.md` definitions are already used by repair and bootstrap flows.
## Decision
**Tolaria creates new type documents at the vault root.**
- A type document is any markdown note with `type: Type` in frontmatter.
- New UI-created type documents use `{vault}/{slug}.md`.
- Existing type documents in `type/`, `types/`, or other scanned folders remain valid and continue to drive templates, icons, colors, visibility, sorting, and sidebar grouping.
- Creation does not silently migrate or move existing type documents.
- Root filename collisions are handled as file collisions; Tolaria must not overwrite an existing note when creating a type document.
## Options considered
- **Root-created type documents** (chosen): matches the metadata-first model, removes special casing from creation, and aligns new types with root-managed default type scaffolding.
- **Canonical `type/` folder**: avoids root filename collisions, but makes path special even though type identity is already defined by frontmatter.
- **Preserve existing folder convention dynamically**: minimizes change for plural `types/` vaults, but creates inconsistent behavior across vaults and leaves `types/` only partially supported.
## Consequences
- Users can inspect and edit type documents as ordinary root notes by default.
- Existing vaults with `type/` or `types/` type documents remain readable because vault scanning already includes non-hidden subdirectories.
- If a root note with the same slug already exists, type creation fails with a collision message instead of writing into a fallback folder.
- Legacy `type/` may remain hidden from the folder tree so old type documents do not duplicate the Types sidebar section.
- Re-evaluate if users need a guided migration from folder-based type documents to root type documents.

View File

@@ -1,42 +0,0 @@
---
type: ADR
id: "0097"
title: "Gemini CLI agent adapter"
status: active
date: 2026-04-29
---
## Context
ADR 0091 added Gemini CLI to explicit external MCP setup, but Gemini was still absent from Tolaria's selectable app-managed AI agents. That left the AI panel able to generate Gemini-compatible MCP configuration while the actual agent picker, availability checks, install links, and streaming dispatch did not treat Gemini like Claude Code, Codex, OpenCode, or Pi.
Gemini CLI supports headless `--prompt` execution with JSON output, configurable approval modes, tool exclusion, and settings-file overrides through `GEMINI_CLI_SYSTEM_SETTINGS_PATH`. Those features are enough to launch Gemini from Tolaria without mutating the user's durable `~/.gemini/settings.json` during app-managed sessions.
## Decision
Tolaria adds Gemini CLI as a first-class `AiAgentId`. The frontend agent definitions, onboarding prompt, install links, default-agent normalization, status badge, command registry, settings persistence, and mock Tauri status payloads include `gemini`.
The desktop backend adds a Gemini adapter that:
- discovers `gemini` through the process path, login shell, and common local/toolchain install locations
- runs `gemini --output-format json --approval-mode <mode> --prompt <prompt>` from the active vault
- supplies Tolaria MCP through a temporary settings file referenced by `GEMINI_CLI_SYSTEM_SETTINGS_PATH`
- uses Safe mode with `auto_edit`, an untrusted MCP entry, and `tools.exclude=["run_shell_command"]`
- uses Power User mode with `yolo` and a trusted Tolaria MCP entry
- maps Gemini JSON responses into Tolaria's existing AI panel stream events
The existing external MCP setup remains explicit and durable. The app-managed Gemini adapter uses transient settings so selecting Gemini in Tolaria does not rewrite the user's global Gemini config.
## Options Considered
- **Add Gemini as a first-class app-managed agent** (chosen): matches the existing agent picker and onboarding UI, uses Gemini's headless JSON mode, and keeps MCP setup vault-scoped.
- **Keep Gemini as external MCP setup only**: avoids another adapter, but keeps the interface inconsistent and requires users to leave Tolaria for a flow that other agents support in-panel.
- **Write app-managed Gemini config into `~/.gemini/settings.json`**: reuses the external setup path, but would blur the consent boundary and risk overwriting user preferences during normal AI panel usage.
- **Use interactive Gemini sessions**: could preserve richer CLI state, but does not fit Tolaria's current one-request stream lifecycle and would make cleanup/auth/error handling harder.
## Consequences
- Gemini appears anywhere users can choose, install, or switch local AI agents.
- End-to-end native Gemini QA requires the Gemini CLI to be installed and authenticated, but missing/auth failures now produce agent-specific guidance.
- Safe and Power User behavior is limited by Gemini's own approval/tool semantics; if Gemini changes those names, the adapter tests and docs need updating.
- The durable MCP setup path and optional `GEMINI.md` shim continue to serve external Gemini usage outside Tolaria's AI panel.

View File

@@ -1,30 +0,0 @@
---
type: ADR
id: "0098"
title: "In-app image and PDF previews for binary vault files"
status: superseded
date: 2026-04-29
supersedes: "0086"
superseded_by: "0110"
---
## Context
ADR-0086 introduced the `FilePreview` path for image binaries while keeping binary files as ordinary `VaultEntry` records. The same file-first model should now cover PDFs, because asset-heavy vaults often mix screenshots, diagrams, and document exports that users need to inspect without leaving Tolaria.
## Decision
**Tolaria previews supported image and PDF files in the editor pane while keeping them as ordinary binary vault files.**
- The scanner keeps the coarse `fileKind: "binary"` representation. Previewability stays a renderer concern inferred from the file extension in `src/utils/filePreview.ts`.
- Supported images render with `<img>` and supported PDFs render with the webview PDF object renderer, both using Tauri asset URLs from `convertFileSrc`.
- The Tauri CSP permits scoped asset URLs in `object-src` so PDF objects can load vault-backed files without broadening script, connect, or image policy.
- PDF preview fallback content lives inside the PDF object so unsupported or failed renderers still expose an explicit "Open in default app" escape hatch.
- Note-list rows for previewable images and PDFs remain clickable and carry file-specific indicators; unsupported binary rows stay muted and non-clickable.
- `Escape` on the preview surface returns keyboard focus to the note list, matching the existing image-preview keyboard behavior.
## Consequences
- PDFs do not become notes and do not get Markdown editor semantics.
- The asset preview surface can keep growing to additional safe binary formats without changing the vault scanner or persisted cache shape.
- Broken PDFs may rely on the webview's own renderer failure state, but the surrounding Tolaria preview chrome still provides reveal, copy path, and default-app actions.

View File

@@ -1,37 +0,0 @@
---
type: ADR
id: "0099"
title: "Cumulative vault asset scope for previews"
status: active
date: 2026-04-29
supersedes: "0074 asset-protocol runtime scoping"
---
## Context
ADR-0074 moved the desktop asset protocol away from broad filesystem access and toward runtime vault scoping. The implementation tried to keep only the active vault in scope by calling Tauri's `forbid_directory` for vault roots that were no longer active.
Tauri's filesystem scope treats forbidden paths as permanent precedence rules: a forbidden path is denied even if it is later allowed again. After a user switched away from a vault and back, image and PDF previews could keep producing `403 Forbidden` responses for valid vault files until the app restarted.
## Decision
**Tolaria accumulates Tauri asset protocol access for vault roots loaded during the current app session and never forbids a previously loaded vault root at runtime.**
- `sync_vault_asset_scope` adds the canonical vault root and requested vault root when they are missing from the runtime asset scope.
- The runtime asset scope remains narrower than global filesystem access because only vault roots that Tolaria has loaded are added.
- Command paths still enforce the active vault boundary through the Rust command layer before reads, writes, external opens, and attachment imports.
- Asset scope revocation is deferred to process exit, because Tauri does not expose a safe runtime unallow operation for directories.
## Options considered
- **Cumulative runtime vault scope** (chosen): keeps previews reliable after vault switches while preserving vault-only access in the current process.
- **Continue forbidding previous vaults**: appears stricter, but Tauri forbids are not reversible and valid previews fail after switching back.
- **Allow all filesystem paths**: avoids preview failures but returns to the broad asset protocol access that ADR-0074 intentionally removed.
- **Replace `convertFileSrc` with a custom protocol**: could support exact active-vault revocation, but it would be a larger cross-cutting migration for editor images, file previews, and PDF rendering.
## Consequences
- Images and PDFs from any vault loaded in the current session can keep rendering after vault switches.
- The app process, not each vault switch, is the revocation boundary for asset URL access.
- Active-vault command validation remains the primary guard for mutations and default-app opens.
- Re-evaluate this if Tauri adds a public runtime unallow operation for asset protocol directories.

View File

@@ -1,35 +0,0 @@
---
type: ADR
id: "0100"
title: "Synthetic vault-root row in folder navigation"
status: active
date: 2026-04-30
---
## Context
ADR-0033 introduced subfolder scanning and a collapsible folder tree backed by `list_vault_folders`, but the sidebar still had no first-class way to select the vault root itself. That left root-level files outside the folder-navigation model and pushed the UI toward one-off handling for the opened vault path.
The new sidebar behavior needs to show root-level files when the user clicks the vault name, while preserving the existing folder rename/delete model for real folders only.
## Decision
**Represent the vault root in the sidebar as a synthetic frontend-owned folder row rather than as a mutable backend folder.**
- `FolderTree` wraps backend folder nodes in a root row with `path: ""` and `rootPath` set to the opened vault path.
- `SidebarSelection` keeps using `kind: 'folder'`, but root selection is encoded as the empty folder path plus `rootPath` metadata.
- Root-level file filtering is handled in note-list helpers as a dedicated root case instead of pretending the vault root is an ordinary folder.
- Rename/delete remain available only for real folders; the vault root row is navigable, not mutable.
## Options considered
- **Option A** (chosen): Synthetic vault-root row in the renderer — keeps `list_vault_folders` focused on real folders, avoids backend schema churn, and reuses the existing folder-selection mental model.
- **Option B**: Add a pseudo-folder to backend folder results — would couple presentation-only root behavior to command data and blur the distinction between the vault itself and mutable folders.
- **Option C**: Keep root files outside folder navigation entirely — simpler, but leaves the sidebar with an incomplete navigation model and special cases elsewhere in the UI.
## Consequences
- Folder navigation now has a single model for root and nested folder browsing.
- Backend folder APIs stay unchanged: they describe actual folders, not UI-only rows.
- Selection handling must treat `path: ""` as the vault-root case and use `rootPath` when computing direct-root file membership.
- Re-evaluate if folder actions ever need to operate on the vault root itself, because that would likely require a separate command model instead of extending the synthetic row.

View File

@@ -1,35 +0,0 @@
---
type: ADR
id: "0101"
title: "Categorical product analytics events"
status: active
date: 2026-04-30
---
## Context
ADR-0016 established opt-in PostHog analytics and Sentry crash reporting, but new feature telemetry now spans file previews, inline image lightbox opens, AI agent session lifecycle events, and All Notes visibility toggles. Instrumenting those flows ad hoc would make it easy to leak vault-specific data such as note paths, filenames, prompt text, or image URLs.
Tolaria needs richer product telemetry for feature behavior while preserving the privacy bar expected from a local-first notes app.
## Decision
**Route product analytics through dedicated helper functions that emit only coarse categorical metadata.**
- Product events live behind `src/lib/productAnalytics.ts` instead of scattering raw `trackEvent()` payloads across feature code.
- Allowed payloads are constrained to categories and counts such as preview kind, action kind, AI agent id, permission mode, response/tool counts, status, and All Notes visibility category/enabled state.
- Product events must not include vault content, note titles, file paths, filenames, image URLs, prompt text, or other user-authored data.
- When a feature needs telemetry, it should add or extend a typed helper rather than sending free-form payloads inline.
## Options considered
- **Option A** (chosen): Typed categorical wrappers for product events — preserves useful product signals while keeping privacy constraints explicit and reviewable in one place.
- **Option B**: Let each feature call `trackEvent()` directly with whatever fields seem useful — faster short term, but inconsistent and too easy to let sensitive data leak into analytics.
- **Option C**: Avoid new product events entirely — safest from a privacy standpoint, but leaves the team blind to whether preview, AI, and list-visibility features are actually being used or failing.
## Consequences
- Telemetry reviews become easier because the allowed product-event surface is centralized.
- Future product analytics work should start by defining categorical event helpers, not by attaching raw note/file context.
- Some debugging detail is intentionally sacrificed; deep diagnosis should rely on consented crash reporting and local reproduction rather than richer analytics payloads.
- Re-evaluate if Tolaria ever needs a broader telemetry taxonomy or stronger compile-time guarantees around event schemas.

View File

@@ -1,29 +0,0 @@
---
type: ADR
id: "0102"
title: "Low-end-safe autosave idle window"
status: active
date: 2026-04-30
supersedes: "0015"
---
## Context
ADR-0015 chose a 500ms autosave debounce so normal edits would persist quickly without writing on every keystroke. GitHub issue #443 showed that this window is too aggressive on very weak Windows CPUs: if typing intervals exceed 500ms or a save takes long enough to overlap continued typing, Tolaria can start disk and derived-state work while the user is still entering text.
## Decision
**Tolaria autosaves after a 1.5s idle window and treats stale in-flight autosaves as obsolete when newer content arrives before they resolve.** Manual saves, note switches, raw-mode entry, and destructive actions still flush pending editor content immediately.
## Options Considered
- **Option A — 1.5s idle window plus stale-save protection** (chosen): reduces mid-typing saves on slow CPUs while keeping ordinary autosave behavior fast enough for reliability. It also prevents an older slow save from clearing or repainting over newer pending text.
- **Option B — Keep 500ms and only fix stale saves**: preserves the previous timing but still triggers repeated saves for slower typists and weaker machines.
- **Option C — Save only on blur or navigation**: minimizes background work but increases crash-loss risk during longer writing sessions.
## Consequences
- Autosave is less likely to compete with active typing on low-end Windows hardware.
- The unsaved window grows from roughly 500ms to roughly 1.5s after Tolaria receives the latest content change.
- Explicit flush paths remain immediate, so navigation, manual save, raw-mode transitions, and destructive actions still preserve pending edits before proceeding.
- Future changes to autosave timing should keep weak-CPU responsiveness and stale in-flight save behavior in the same test surface.

View File

@@ -1,32 +0,0 @@
---
type: ADR
id: "0103"
title: "Adapter-specific AI permission semantics"
status: active
date: 2026-04-30
supersedes: "0092"
---
## Context
ADR-0092 introduced per-vault Vault Safe / Power User modes, but the first implementation left too much room for adapter drift. Some agents can directly deny or allow Bash, some expose only a sandbox/approval profile, and Pi currently has no narrower app-managed switch beyond Tolaria's transient MCP configuration. The shared UI still needs a consistent product contract: Vault Safe must not encourage shell execution, while Power User should keep shell execution available across repeated agent turns where the selected adapter supports it.
## Decision
**Tolaria treats the permission mode as a product contract first and maps it conservatively per adapter.**
- Shared AI system prompts are mode-aware on every turn, including turns with note context snapshots.
- Vault Safe tells agents not to use or advertise shell, terminal, Bash, script execution, git, or command-line tools.
- Power User tells shell-capable agents that local shell commands are available for the active vault and should remain scoped to that vault.
- Claude Code Safe excludes Bash; Power User includes and pre-approves Bash without dangerous bypass flags.
- Codex Safe uses the CLI's read-only sandbox plus untrusted approval policy; Power User uses workspace-write plus never-ask approval so shell-capable Codex turns remain low-friction across the session.
- OpenCode Safe denies bash and external directories; Power User allows bash while still denying external directories.
- Pi keeps the same conservative transient MCP config in both modes until the Pi CLI exposes a reliable app-managed shell permission switch. The prompt must not promise shell for Pi Power User.
- Gemini Safe excludes `run_shell_command`; Gemini Power User intentionally uses `yolo` with trusted transient Tolaria MCP settings.
## Consequences
- Mode behavior is no longer described solely by generic UI copy; adapter docs and tests define the exact mapping.
- Codex Vault Safe remains a best-effort safe profile rather than a true built-in-tools-off mode, because Codex CLI currently exposes sandbox and approval controls but not a dedicated switch to remove shell tooling while preserving MCP.
- Future adapters must either implement both modes explicitly or document that Power User maps to the same conservative behavior.
- If Tolaria adds a stronger warning or dangerous mode later, it needs a separate ADR and UI language.

View File

@@ -1,48 +0,0 @@
---
type: ADR
id: "0104"
title: "Tauri frontend readiness watchdog"
status: active
date: 2026-05-01
---
## Context
Tolaria already keeps heavy filesystem and subprocess work off the Tauri window-creation path, but that alone does not protect against a different startup failure mode: the desktop WebView can render the static HTML shell while the React app never becomes interactive.
On macOS this showed up as an inert window that looked launched but never finished mounting the real app. The failure boundary is cross-layer:
- `index.html` can paint before React commits
- React root errors can happen before the app reports itself ready
- a plain reload is acceptable as a one-time recovery, but an automatic reload loop is not
- browser/mock runs should not inherit desktop-only recovery behavior
Tolaria needs a startup contract that distinguishes “HTML painted” from “frontend actually became interactive”, and a bounded recovery path when that contract is not satisfied.
## Decision
**Tolaria uses a Tauri-only frontend-readiness watchdog that reloads the WebView at most once if React never reports startup readiness.**
Concretely:
- `index.html` installs a Tauri-only startup timer before React loads
- React dispatches a readiness signal from a mounted effect after the app shell commits
- if readiness never arrives before the timeout, the WebView reloads once
- the same one-shot reload path is available to React root error handling before readiness is marked
- `sessionStorage` tracks whether the startup reload was already attempted so Tolaria does not loop forever
- browser/mock environments keep using ordinary browser clipboard/storage behavior and do not enable this desktop startup recovery path
## Options considered
- **Tauri-only readiness watchdog with one-shot reload** (chosen): directly addresses the inert-startup failure mode, keeps recovery local to the frontend, and avoids permanent reload loops. Cost: startup now depends on a small cross-layer contract between HTML bootstrap and React.
- **Do nothing and rely on manual relaunch**: simplest implementation, but leaves users stranded in a broken-looking app state with no automatic recovery.
- **Reload on any React root error without a readiness gate**: more aggressive, but too noisy; post-startup runtime errors should not trigger surprise reloads.
- **Move recovery entirely into native Rust window/bootstrap logic**: possible, but the failure signal lives in the frontend lifecycle, so native code would still need a readiness handshake.
## Consequences
- Tolaria now distinguishes successful frontend startup from merely rendering the HTML shell.
- Desktop startup recovery is bounded to a single retry per session, reducing the chance of trapping users in reload loops.
- `index.html`, `src/main.tsx`, and `src/utils/frontendReady.ts` form a shared startup contract that future bootstrap refactors must preserve.
- Any future change that delays app-shell mount beyond the watchdog timeout must re-evaluate the timeout and readiness trigger.
- If a startup failure persists after one retry, Tolaria still surfaces the broken state instead of hiding a deeper bug behind repeated reloads.

View File

@@ -1,39 +0,0 @@
---
type: ADR
id: "0105"
title: "Editor correctness and responsiveness contract"
status: active
date: 2026-05-01
---
## Context
Tolaria notes are durable Markdown files on disk, but the rich editor renders them through BlockNote blocks. Large notes exposed a tempting optimization: show a fast Markdown preview first, then hydrate BlockNote later. In practice, that creates two renderers for the same document, visible flicker, delayed click-time lag, and more places for stale async work to race with the currently selected note.
The editor must optimize for the product priorities in this order:
1. no crashes
2. no stale content, race-condition overwrites, or lost edits
3. responsive typing and cursor movement
4. fast note-list-to-editor loading
## Decision
**Tolaria keeps a single direct editor surface for Markdown notes and treats editor content swaps as generation-checked, source-content-checked operations.** Fast loading may use raw file-content prefetching and a bounded parsed-block cache, but it must not show a separate preview that later swaps into the editor. Parsed BlockNote blocks are reusable only when their source Markdown exactly matches the content being opened, and background parsing must run only after recent typing/navigation has gone idle.
## Options considered
- **Single direct editor surface with guarded swaps plus bounded caches** (chosen): preserves one visual representation of the document, rejects stale async parse results, validates or identity-checks cached disk content before opening, and keeps typing work debounced. Cons: very large notes can still wait on BlockNote conversion when they were not warmed.
- **Fast Markdown preview followed by hidden BlockNote hydration**: improves first paint but creates flicker, delayed edit-time stalls, and duplicated rendering semantics.
- **Unbounded or eager background BlockNote parsing for likely next notes**: can make some opens faster, but competes with typing/navigation and introduces stale parse-result hazards unless heavily scheduled, bounded, and invalidated.
- **Always raw mode for large notes**: strongest responsiveness for huge files, but changes the editing experience abruptly and should be an explicit fallback rather than the default.
## Consequences
- Async editor work must prove it still matches both the latest swap generation and the latest tab source content before touching BlockNote.
- Cached raw note content must be validated against disk before it is shown unless it carries the same `modifiedAt` and `fileSize` identity as the current `VaultEntry`, or the content was just authored by Tolaria in the current process.
- Cached parsed BlockNote blocks must be keyed by vault, path, and exact source content, cloned on read/write, and bounded by entry count plus source byte budget.
- Background parsed-block warming is allowed only for likely next large Markdown notes after a foreground idle window; active typing, raw mode, and editor mount state must defer it.
- Dirty local editor content remains authoritative. External filesystem refreshes may replace clean notes, but must not overwrite unsaved local edits.
- Per-keystroke editor work must stay minimal. Serialization, metadata derivation, autosave, and cache updates should be debounced, coalesced, or scheduled away from active typing.
- Future large-note optimizations should target true progressive/chunked conversion or explicit raw/read-only fallback states, not a visually different preview that morphs into BlockNote.

View File

@@ -1,31 +0,0 @@
---
type: ADR
id: "0106"
title: "Shared app command manifest"
status: active
date: 2026-05-02
---
## Context
Tolaria command metadata was split across several runtime surfaces: TypeScript owned shortcut lookup and command-palette shortcut display, Rust owned native menu IDs, labels, accelerators, aliases, and enablement groups, and the Linux titlebar fallback menu duplicated another command list. Adding or changing a command required carefully editing multiple files that could drift while still compiling.
The existing renderer-first shortcut model and native-menu dedupe remain correct, but they need a single source for metadata that must be identical across those surfaces.
## Decision
**Tolaria stores cross-runtime app command metadata in `src/shared/appCommandManifest.json`, and both the renderer and Tauri native menu derive their command/menu IDs, accelerators, menu labels, menu aliases, enablement groups, and deterministic QA metadata from it.** Context-sensitive command-palette builders still own availability and execution callbacks, and OS-native menu entries remain local to the native menu implementation.
## Options considered
- **Shared JSON manifest included by TypeScript and Rust** (chosen): works in both runtimes without code generation, keeps menu metadata reviewable, and lets tests validate drift directly.
- **Generate TypeScript and Rust constants from a schema**: gives stronger compile-time types but adds a build step and a generated-file maintenance burden for a small manifest.
- **Keep duplicated constants with more tests**: reduces immediate refactor scope, but still forces every command change through parallel manual edits.
## Consequences
- New app commands that appear in native menus or shortcut QA must be added to `src/shared/appCommandManifest.json`.
- `appCommandCatalog.ts` is responsible for turning the manifest into typed renderer helpers such as `APP_COMMAND_IDS`, shortcut lookup maps, Linux menu sections, and deterministic QA definitions.
- `src-tauri/src/menu.rs` includes the same manifest JSON, builds custom menu items from it, maps overridden menu item IDs such as `file-quick-open-alias` back to their primary command IDs, and resolves state-dependent enablement groups from manifest entries.
- Platform-native menu items such as Undo, Redo, Copy, Paste, Select All, Services, Quit, and Window controls stay in Rust because they are OS affordances, not Tolaria app commands.
- Command-palette builders continue to own dynamic labels, filtering, enabled state, and callbacks where those depend on current app state.

View File

@@ -1,43 +0,0 @@
---
type: ADR
id: "0107"
title: "Markdown-durable tldraw whiteboards in notes"
status: active
date: 2026-05-03
---
## Context
Tolaria notes are durable Markdown files, while whiteboard editing needs an interactive canvas with structured shape data. tldraw provides a mature React whiteboard runtime and exposes snapshot APIs that can persist the document without using browser-local `persistenceKey` storage.
The storage decision needs to preserve Tolaria's filesystem source-of-truth rule: deleting local caches must not lose a board, raw mode must expose the canonical source, and Git should track the board together with the surrounding note.
## Decision
**Tolaria will support whiteboards as Markdown-durable fenced `tldraw` blocks backed by tldraw document snapshots.**
The implementation:
- Converts fenced `tldraw` blocks into temporary placeholders before BlockNote parses Markdown.
- Replaces those placeholders with `tldrawBlock` schema blocks that store a stable board id and tldraw document snapshot JSON.
- Renders each block with the `tldraw` package inside the rich editor.
- Debounces tldraw document changes into BlockNote block props so Tolaria's normal autosave writes the snapshot into the `.md` file.
- Serializes `tldrawBlock` nodes back to fenced Markdown before save, raw-mode entry, and editor-position snapshots.
- Adds a `/whiteboard` slash command that inserts the same block format.
Session state such as camera position, selected shapes, and selected tools is not persisted into the note. Preview images are intentionally omitted from the initial design; they may be introduced later as derived cache artifacts for note lists, search results, or graph cards.
## Options considered
- **Markdown fenced block with tldraw document snapshot** (chosen): keeps the board in the note file, matches Tolaria's Mermaid/math round-trip model, and works for both full whiteboard notes and embedded boards inside larger notes.
- **tldraw `persistenceKey` / IndexedDB**: simplest app integration, but violates the vault-as-source-of-truth rule and would lose boards when browser storage is cleared.
- **Separate `.tldr` files embedded from Markdown**: keeps JSON out of prose notes, but fragments note ownership, makes embedded boards harder to move with their parent note, and complicates Git history for small board edits.
- **Preview-image-first storage**: useful for thumbnails, but not editable source. It would make the PNG an attractive but stale source of truth.
## Consequences
- `src/utils/tldrawMarkdown.ts` is the canonical parser/serializer bridge for whiteboard blocks.
- `src/components/TldrawWhiteboard.tsx` owns the tldraw runtime integration and only persists document snapshots.
- Raw mode remains a direct source editor for the fenced JSON.
- Embedded whiteboards and future full-note whiteboard templates share the same storage format.
- Asset support is deferred; when added, asset bytes should live in vault-relative attachment paths referenced from the tldraw asset records.

View File

@@ -1,32 +0,0 @@
---
type: ADR
id: "0107"
title: "Pointer-owned editor block reordering"
status: active
date: 2026-05-02
---
## Context
Tolaria uses BlockNote for rich Markdown editing, Tauri for native desktop file drops, and custom editor drop handlers for images and wikilinks. BlockNote's default block drag path depends on HTML5 drag events and `DataTransfer`. On macOS inside the Tauri webview, that makes block reordering fragile because the same browser-level drag system also carries native file/image drops into the editor.
Regressions tended to oscillate: fixes that restored block dragging could break image drops, and fixes that protected native drops could make block reordering fail or lose visual feedback. The drag handle also needs editor-specific affordances: a moving block preview, an insertion separator, stale-block protection, and typography-aware positioning for the side-menu controls.
## Decision
**Tolaria owns editor block reordering as a pointer gesture that directly moves BlockNote blocks, and leaves HTML5/native drag data paths for file, image, and external drops.** The drag side menu is responsible for resolving live BlockNote blocks, computing pointer hit targets, rendering drag affordances, and aligning the hover controls to the rendered text range of the hovered block.
## Options considered
- **Pointer-owned block reordering** (chosen): separates internal block moves from native drop payloads, works without `DataTransfer`, gives Tolaria deterministic visual affordances, and can be tested with Playwright pointer/mouse actions. Cons: Tolaria now owns a small amount of hit-testing, block-move, and affordance code around BlockNote.
- **Continue using BlockNote's HTML5 drag handler**: keeps less local code, but ties internal block moves to the same unstable drag channel used by native file drops in Tauri.
- **Patch native file drops around BlockNote drag events**: might repair individual regressions, but preserves the root coupling between editor-internal reordering and external drag payload handling.
- **Disable block dragging on macOS/Tauri**: avoids the conflict, but removes an important editing workflow.
## Consequences
- Internal block reordering must not depend on `DataTransfer` or `draggable=true`.
- File/image/wikilink drop behavior remains owned by the existing editor drop abstractions and native Tauri file-drop bridge.
- Block reordering tests should use pointer or mouse movement in Playwright, and should assert the moving preview and insertion separator while the drag is in progress.
- The side menu should align to measured rendered content rather than heading-specific pixel offsets, so future theme font-size and line-height changes do not need drag-control retuning.
- Changes to BlockNote DOM structure around `.bn-block-content`, `.bn-inline-content`, `.bn-side-menu`, or block container IDs require rechecking the pointer hit-testing and side-menu alignment tests.

View File

@@ -1,30 +0,0 @@
---
type: ADR
id: "0108"
title: "Direct model AI targets alongside coding agents"
status: active
date: 2026-05-03
---
## Context
Tolaria's AI panel originally targeted desktop coding-agent CLIs only. That works well for tool-capable vault editing, but it excludes users who run local model servers, users who prefer OpenAI or Anthropic APIs, and future mobile builds where desktop subprocesses cannot run.
## Decision
**Tolaria models AI selection as an AI target.** Targets can be desktop coding agents or direct model endpoints. Coding agents keep the existing Safe / Power User permission modes and tool access. Direct model targets run in Chat mode: they receive note context and conversation history, but they do not get vault-write tools or shell access.
Direct model provider metadata is stored in app settings. Provider API secrets are not stored in settings; hosted providers can either save a key in Tolaria's local app-data secrets file or read a key from a named environment variable. The local secrets file is outside the vault, outside project worktrees, and written with owner-only file permissions on Unix platforms. Local providers such as Ollama and LM Studio can run without a key.
## Options Considered
- **AI target abstraction** (chosen): supports agents, local models, hosted APIs, and mobile-compatible model runtimes without pretending all AI backends have the same capabilities.
- **Treat custom providers as OpenCode configuration only**: lower implementation cost on desktop, but does not help mobile and keeps API users dependent on a coding-agent install.
- **Direct API runtime with write tools immediately**: powerful, but would require Tolaria-owned tool loops, confirmations, retries, and safety semantics before the basic chat value is proven.
## Consequences
- Settings owns durable provider setup and default target selection.
- The status bar becomes a quick target switcher across agents and configured model targets.
- The AI panel explains capability differences: agents show Safe / Power User; direct model targets show Chat mode.
- Future work can migrate local secrets to OS keychain storage and add read/write tool loops without changing the top-level target model.

View File

@@ -1,25 +0,0 @@
# 0108. Sanitized Rendered Markup and Safe User Regex
Date: 2026-05-03
## Status
Accepted
## Context
Tolaria renders generated SVG/HTML from trusted libraries such as Mermaid and KaTeX, and it allows users to opt into regex matching in filters and editor find/replace. Codacy SRM flagged the raw markup insertion and direct regex construction as Critical XSS/DoS risks.
## Decision
Add direct runtime dependencies on `dompurify` and `safe-regex2`.
Rendered Mermaid SVG and KaTeX HTML must be sanitized before insertion and mounted through DOM nodes rather than React `dangerouslySetInnerHTML`. User-provided regex sources must be length-bounded and checked with `safe-regex2` before compilation.
SCA-reported vulnerable transitive dependencies are pinned through package-manager overrides so Codacy resolves patched floors until upstream dependencies adopt them naturally. This includes `protobufjs`, the MCP SDK web-server stack, and Vite's parser/build transitive stack. Rust lockfile-only updates keep OpenSSL, rustls-webpki, and tar on patched versions without changing public Tauri APIs.
## Consequences
Markup rendering now has an explicit sanitizer boundary that is shared by Mermaid and math rendering. User regex features remain available, but unsafe or overly large expressions fail validation instead of running in the UI thread.
The overrides should be removed once the dependency graph no longer pulls the vulnerable versions.

View File

@@ -1,32 +0,0 @@
---
type: ADR
id: "0109"
title: "Debounced worker-derived editor indexes"
status: active
date: 2026-05-04
---
## Context
Right side panels can need derived indexes of the active note, such as the Table of Contents hierarchy. These indexes are useful while editing, but rebuilding them synchronously during note opening or on every keystroke competes with the editor's main-thread work and violates ADR-0105's responsiveness contract.
The Table of Contents also needs live BlockNote block IDs for navigation, while the fastest and most stable source for the outline itself is the active note's Markdown content. Binding the outline rebuild directly to BlockNote document mutations makes typing and note swaps more expensive than necessary.
## Decision
**Derived editor indexes that are not required for the editor surface itself must be lazy, debounced, and built off the main thread when they can be derived from Markdown.** The Table of Contents does not build while its panel is closed; once opened, it uses a Web Worker to build its Markdown-derived H1/H2/H3 tree after a debounce, while live BlockNote block IDs are resolved only at click time for navigation.
## Options considered
- **Lazy debounced Web Worker for Markdown-derived indexes** (chosen): avoids any TOC work while the panel is closed, keeps outline parsing away from typing and note-opening work once opened, cancels stale panel updates, and lets the rendered editor remain the only editor surface. Cons: adds a small worker/client path and a title-only interim state.
- **Main-thread deferred rebuild with `setTimeout`**: avoids blocking the first render, but still runs on the UI thread and can still rebuild too often during active edits.
- **Synchronous rebuild from the BlockNote document**: simplest and gives immediate block IDs, but makes every BlockNote document update a potential side-panel rebuild.
- **Never update the TOC while editing**: safest for typing performance, but stale outlines make the panel misleading for active authoring.
## Consequences
- TOC tree state is driven by note identity plus debounced Markdown content, not by BlockNote document churn.
- Closing the TOC panel unmounts the panel and cancels pending debounce callbacks; no worker request is scheduled while the panel is closed.
- The TOC may briefly show only the note title after a note switch or edit burst; the full tree appears when the debounced worker result returns.
- Navigation remains tied to the live editor: block IDs are resolved from the current BlockNote document at click time and scrolled/focused then.
- Future derived side-panel indexes should follow the same pattern when they parse or scan note content and are not needed to render the editor itself.

View File

@@ -1,43 +0,0 @@
---
type: ADR
id: "0110"
title: "In-app media and PDF previews for binary vault files"
status: active
date: 2026-05-05
supersedes: "0098"
---
## Context
ADR-0098 extended Tolaria's file-first preview model from images to PDFs while keeping binary files as ordinary `VaultEntry` records. In practice, vaults also carry voice notes, interview recordings, screen captures, and short clips that users need to inspect in context without round-tripping through another app.
The existing binary preview architecture already had the important constraints in place:
- previewability should stay a renderer concern inferred from filename extension rather than a persisted schema field
- preview access should stay inside Tauri's scoped asset protocol instead of broad filesystem reads
- external-open actions must still re-enter the active-vault command boundary before delegating to the OS
Audio and video support should extend that same model rather than introducing a separate asset or media subsystem.
## Decision
**Tolaria previews supported image, audio, video, and PDF files in the editor pane while keeping them as ordinary binary vault files.**
- The scanner keeps the coarse `fileKind: "binary"` representation; `src/utils/filePreview.ts` infers preview support from safe extension allow-lists.
- `FilePreview` remains the single renderer-owned preview surface for supported binary files.
- Images continue to render through `<img>`, PDFs through the webview PDF object renderer, and audio/video through native HTML media controls, all backed by Tauri asset URLs from `convertFileSrc`.
- The Tauri CSP must allow scoped asset URLs in `media-src` for audio/video and in `object-src` for PDFs without broadening script or network permissions.
- Note-list rows for previewable media stay clickable and use file-specific affordances; unsupported binaries remain ordinary files with explicit fallback/open-external paths.
## Alternatives considered
- **Extend the existing FilePreview model to media** (chosen): keeps one binary-preview surface, reuses scoped asset access, and avoids new persisted file categories. Cons: native media controls are intentionally minimal.
- **Open audio and video only in the default app**: simpler implementation, but breaks in-context review for media-heavy vaults.
- **Introduce dedicated persisted media file kinds or a separate media library**: could support richer metadata later, but adds schema and scanner complexity for files that should remain normal vault entries.
## Consequences
- Audio and video do not become notes and do not get special persistence semantics.
- Tolaria's binary preview surface now covers the common safe media formats without changing cache shape, scanner output, or the filesystem-first model.
- Scoped runtime asset access and active-vault command validation remain the security boundary for binary previews and external-open actions.
- Re-evaluate this decision if Tolaria later needs editing, waveform/timeline tooling, subtitles, or transcoding, because those would justify a richer media-specific subsystem.

View File

@@ -1,49 +0,0 @@
---
type: ADR
id: "0111"
title: "Path-aware external vault refresh with focused-editor preservation"
status: active
date: 2026-05-05
supersedes: "0071"
---
## Context
ADR-0071 established a shared reconciliation path for external vault mutations so git pulls, AI-agent writes, and other non-local edits would reload vault-derived state and protect unsaved local changes. That policy was directionally right, but the original "reopen the clean active note" rule was too broad once Tolaria added a native filesystem watcher and more editor-mounted integrations.
Two problems emerged:
- unrelated external updates could remount a clean active editor even when the active file itself did not change
- remounting while focus was inside the rich or raw editor surface could drop cursor/focus state and disrupt native input flows even when the vault refresh itself was otherwise safe
Tolaria still needs refreshed entries, folders, views, backlinks, and other derived state after external writes. But it should only pay the cost of an active-editor remount when the changed-path batch actually requires one and the user is not actively focused inside the editor.
## Decision
**External vault refreshes now reload shared vault-derived state eagerly, but only remount the active editor when the active file itself changed and the editor is clean and unfocused.**
The shared `refreshPulledVaultState()` path now applies these rules:
1. Reload vault entries, folders, and saved views together for every external change batch.
2. If there is no active note, stop after the shared reload.
3. If the active note changed during the async reload, stop rather than reopening stale context.
4. If the active note has unsaved local edits, keep the current editor buffer mounted.
5. If focus is currently inside the rich or raw editor surface, keep that editor mounted even for otherwise clean notes.
6. If the active file disappeared, close the tab instead of leaving a stale editor behind.
7. Only close and reopen the active tab when the changed-path batch includes that active file and the previous guards did not apply.
Git pulls, AI-agent refresh callbacks, and filesystem-watcher batches should continue to converge through this single reconciliation helper instead of inventing separate reload policies.
## Alternatives considered
- **Path-aware refresh with focused-editor preservation** (chosen): keeps derived vault state fresh while avoiding unnecessary remounts and focus loss. Cons: a focused clean editor can temporarily lag on-disk content until a later safe remount.
- **Always reopen the clean active note after every external refresh**: strongest immediate convergence, but causes visible churn and drops editor focus for unrelated changes.
- **Skip shared reloads whenever the editor is focused**: preserves focus, but leaves folders, views, backlinks, and other derived state stale.
## Consequences
- Unrelated external vault updates no longer remount the active editor just because the note is clean.
- Focused rich/raw editor sessions preserve cursor and native input state across watcher- or agent-driven vault refreshes.
- The changed-path batch is now part of the external-refresh contract; callers should pass the best available file list instead of treating all refreshes as full active-note invalidations.
- A focused clean editor may intentionally continue showing pre-refresh content until a later safe reopen, trading immediate active-note convergence for editing continuity.
- `refreshPulledVaultState()` remains the single place to evolve external-refresh policy; future features should extend that helper rather than layering ad hoc editor reload behavior.

View File

@@ -1,41 +0,0 @@
---
type: ADR
id: "0112"
title: "System theme mode"
status: active
date: 2026-05-05
---
## Context
ADR-0081 introduced Tolaria's internal app-owned light and dark theme runtime and deliberately deferred system-follow mode. That kept the first dark-mode release small, but users now need Tolaria to match the operating system appearance automatically, including scheduled macOS light/dark changes.
The previous constraints still apply: themes are app-owned, not vault-authored; the renderer must avoid startup flashes; shadcn/ui, Tailwind variables, editor chrome, and secondary windows must keep sharing the same resolved light/dark contract.
## Decision
**Tolaria now treats `system` as a persisted theme preference that resolves to the current OS light/dark appearance at runtime.**
The selected preference can be `light`, `dark`, or `system`:
1. `settings.theme_mode` remains the source of truth for the installation-local preference.
2. The localStorage mirror stores the selected preference, including `system`, so the `index.html` prepaint script can resolve the correct appearance before React mounts.
3. `data-theme` and the shadcn `.dark` class always receive the resolved app theme, `light` or `dark`; they never receive `system`.
4. When `system` is selected, the renderer subscribes to `prefers-color-scheme` changes and reapplies the resolved theme without reopening the app.
5. Explicit `light` and `dark` choices remain overrides and do not follow OS changes.
Command-palette theme actions and the Settings panel both save the same preference path. Product analytics record preference changes with the selected mode only, without sending vault or note content.
## Alternatives considered
- **Persist `system` and resolve it into the existing light/dark runtime** (chosen): keeps ADR-0081's small app-owned theme surface while adding OS-follow behavior.
- **Store the resolved OS theme in settings**: avoids a third stored value, but silently converts System users into explicit Light/Dark users after every save.
- **Set `data-theme="system"` and branch in CSS**: would require every theme consumer to understand a third state and would break existing Tailwind/shadcn dark-mode assumptions.
- **Rely only on CSS `prefers-color-scheme` media queries**: helps static CSS, but does not update JavaScript consumers, command state, editor integrations, or the localStorage startup mirror consistently.
## Consequences
- Startup still avoids a light flash when the stored preference is `system` and the OS is dark.
- Secondary windows that mount the shared theme hook receive the same resolved appearance and update on OS changes.
- Code that reads `document.documentElement.dataset.theme` must treat it as a resolved `light` or `dark` value, not as the stored user preference.
- Future theme variants should preserve this split between selected preference and resolved app theme rather than widening `data-theme` to non-renderable preference values.

View File

@@ -1,43 +0,0 @@
---
type: ADR
id: "0113"
title: "Shared renderer attachment path normalization"
status: active
date: 2026-05-07
---
## Context
Tolaria already treats vault attachments as ordinary files under `attachments/`, and ADRs around previews and asset scoping rely on Tauri asset URLs to render them safely. In practice, attachment handling had started to fragment across the renderer: some flows joined `vaultPath + attachments/...`, some decoded `convertFileSrc` URLs directly, some handled Windows separators ad hoc, and some only worked for one editor surface.
That duplication turned attachment behavior into a drift risk. Opening file blocks, following editor links, serializing raw-mode Markdown, rewriting image URLs after vault switches, and copying dropped files into the vault all needed the same three representations to stay in sync:
- portable markdown references such as `attachments/report.pdf`
- Tauri asset URLs used by the renderer
- absolute filesystem paths inside the active vault
## Decision
**Tolaria centralizes attachment path conversion in a single renderer-owned primitive and keeps portable `attachments/...` references as the canonical cross-surface representation.**
Specifically:
1. `src/utils/vaultAttachments.ts` is the single owner for converting between portable attachment references, Tauri asset URLs, and active-vault filesystem paths.
2. Editor rendering, raw-mode serialization, image upload/drop flows, file-block open actions, and parsed-media cleanup must call that shared primitive instead of carrying local path/URL conversion logic.
3. Renderer code may derive absolute paths only relative to the current active vault and must reject asset URLs or relative paths that fall outside that boundary.
4. Tauri asset URLs remain a transport/rendering detail, not a persisted vault format.
## Alternatives considered
- **Shared renderer attachment-path primitive with portable persisted refs** (chosen): keeps behavior consistent across media rendering, editor actions, and vault switching while preserving Markdown portability.
- **Per-feature helpers for each attachment surface**: simpler locally, but repeats Windows/path-normalization rules and lets editor actions drift apart.
- **Persist absolute paths or Tauri asset URLs in Markdown/editor state**: would couple notes to one machine or one runtime session and make vault content less portable.
- **Push all attachment conversion into backend commands**: could reduce renderer logic, but the renderer still needs a shared local model for in-memory markdown rewriting, link activation, and preview URL handling.
## Consequences
- Attachment behavior becomes consistent across previews, editor links, toolbar/file-block opens, drag-drop imports, and markdown serialization.
- Vault content stays portable because persisted references remain `attachments/...` paths rather than machine-specific absolute paths or session-specific asset URLs.
- Cross-platform edge cases such as Windows separators, encoded asset URLs, and vault-switch rewrites now have one place to harden and test.
- The Rust command layer remains the write/read security boundary; this ADR only centralizes renderer-side normalization before those commands are called or asset URLs are rendered.
- Future attachment/media features should extend `vaultAttachments.ts` rather than introducing new ad hoc conversion helpers.

View File

@@ -70,7 +70,7 @@ proposed → active → superseded
| [0012](0012-claude-cli-for-ai-agent.md) | Claude CLI subprocess for AI agent | active |
| [0013](0013-remove-theming-system.md) | Remove vault-based theming system | superseded -> [0081](0081-internal-light-dark-theme-runtime.md) |
| [0014](0014-git-based-vault-cache.md) | Git-based incremental vault cache | active |
| [0015](0015-auto-save-with-debounce.md) | Auto-save with 500ms debounce | superseded → [0102](0102-low-end-safe-autosave-idle-window.md) |
| [0015](0015-auto-save-with-debounce.md) | Auto-save with 500ms debounce | active |
| [0016](0016-sentry-posthog-telemetry.md) | Sentry + PostHog telemetry with consent | active |
| [0017](canary-release-channel-and-local-feature-flags.md) | Canary release channel and feature flags | superseded → [0057](0057-alpha-stable-release-channels-and-beta-cohorts.md) |
| [0018](0018-codescene-code-health-gates.md) | CodeScene code health gates in CI | superseded → [0064](0064-ratcheted-codescene-thresholds.md) |
@@ -126,7 +126,7 @@ proposed → active → superseded
| [0068](0068-h1-only-title-surface-with-optional-untitled-auto-rename.md) | H1-only title surface with optional untitled auto-rename | active |
| [0069](0069-neighborhood-mode-for-note-list-relationship-browsing.md) | Neighborhood mode for note-list relationship browsing | active |
| [0070](0070-starter-vaults-local-first-with-explicit-remote-connection.md) | Starter vaults are local-first with explicit remote connection | active |
| [0071](0071-external-vault-refresh-and-clean-tab-reopen.md) | External vault updates reload derived state and reopen the clean active note | superseded → [0111](0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md) |
| [0071](0071-external-vault-refresh-and-clean-tab-reopen.md) | External vault updates reload derived state and reopen the clean active note | active |
| [0072](0072-confirmed-vault-paths-gate-startup-state.md) | Confirmed vault paths gate startup state | active |
| [0073](0073-persistent-linkify-protocol-registry-across-editor-remounts.md) | Persistent linkify protocol registry across editor remounts | active |
| [0074](0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md) | Explicit external AI tool setup and least-privilege desktop scope | active |
@@ -142,28 +142,3 @@ proposed → active → superseded
| [0085](0085-non-git-vault-support.md) | Non-git vaults open with explicit later Git initialization | active |
| [0088](0088-markdown-durable-mermaid-diagrams.md) | Markdown-durable Mermaid diagrams in notes | active |
| [0089](0089-active-vault-filesystem-watcher.md) | Active vault filesystem watcher | active |
| [0090](0090-pi-cli-agent-adapter.md) | Pi CLI agent adapter | active |
| [0091](0091-gemini-cli-external-ai-setup.md) | Gemini CLI external AI setup | active |
| [0092](0092-vault-ai-agent-permission-modes.md) | Vault-scoped AI agent permission modes | superseded -> [0103](0103-adapter-specific-ai-permission-semantics.md) |
| [0093](0093-shared-cli-agent-runtime-adapters.md) | Shared CLI agent runtime adapters | active |
| [0094](0094-gitignored-content-visibility-boundary-filter.md) | Gitignored content visibility as a command-boundary filter | active |
| [0095](0095-saved-view-order-field.md) | Saved views use an explicit YAML order field | active |
| [0096](0096-root-created-type-documents.md) | Root-created type documents | active |
| [0097](0097-gemini-cli-agent-adapter.md) | Gemini CLI agent adapter | active |
| [0098](0098-in-app-image-and-pdf-file-previews.md) | In-app image and PDF previews for binary vault files | superseded → [0110](0110-in-app-media-and-pdf-file-previews.md) |
| [0099](0099-cumulative-vault-asset-scope.md) | Cumulative vault asset scope for previews | active |
| [0100](0100-synthetic-vault-root-folder-row.md) | Synthetic vault-root row in folder navigation | active |
| [0101](0101-categorical-product-analytics-events.md) | Categorical product analytics events | active |
| [0102](0102-low-end-safe-autosave-idle-window.md) | Low-end-safe autosave idle window | active |
| [0103](0103-adapter-specific-ai-permission-semantics.md) | Adapter-specific AI permission semantics | active |
| [0104](0104-tauri-frontend-readiness-watchdog.md) | Tauri frontend readiness watchdog | active |
| [0105](0105-editor-correctness-and-responsiveness-contract.md) | Editor correctness and responsiveness contract | active |
| [0106](0106-shared-app-command-manifest.md) | Shared app command manifest | active |
| [0107](0107-markdown-durable-tldraw-whiteboards.md) | Markdown-durable tldraw whiteboards in notes | active |
| [0107](0107-pointer-owned-editor-block-reordering.md) | Pointer-owned editor block reordering | active |
| [0108](0108-sanitized-rendered-markup-and-safe-regex.md) | Sanitized rendered markup and safe user regex | active |
| [0109](0109-debounced-worker-derived-editor-indexes.md) | Debounced worker-derived editor indexes | active |
| [0110](0110-in-app-media-and-pdf-file-previews.md) | In-app media and PDF previews for binary vault files | active |
| [0111](0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md) | Path-aware external vault refresh with focused-editor preservation | active |
| [0112](0112-system-theme-mode.md) | System theme mode | active |
| [0113](0113-shared-renderer-attachment-path-normalization.md) | Shared renderer attachment path normalization | active |

View File

@@ -6,16 +6,7 @@ import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores([
'dist',
'coverage',
'site/.vitepress/cache/',
'site/.vitepress/dist/',
'src-tauri/resources/mcp-server/',
'src-tauri/target/',
'src-tauri/gen/',
'tools/',
]),
globalIgnores(['dist', 'coverage', 'src-tauri/resources/', 'src-tauri/target/', 'src-tauri/gen/', 'tools/']),
{
files: ['**/*.{ts,tsx}'],
extends: [

View File

@@ -30,23 +30,11 @@
(function () {
var key = 'tolaria-theme';
var legacyKey = 'laputa-theme';
var systemDarkQuery = '(prefers-color-scheme: dark)';
function normalizeTheme(value) {
return value === 'dark' || value === 'light' || value === 'system' ? value : null;
}
function prefersDarkTheme() {
try {
return typeof window.matchMedia === 'function' && window.matchMedia(systemDarkQuery).matches;
} catch (err) {
return false;
}
}
function resolveTheme(value) {
var mode = normalizeTheme(value) || 'light';
return mode === 'system' ? (prefersDarkTheme() ? 'dark' : 'light') : mode;
return value === 'dark' || value === 'light' ? value : null;
}
function applyTheme(value) {
var mode = resolveTheme(value);
var mode = normalizeTheme(value) || 'light';
document.documentElement.setAttribute('data-theme', mode);
document.documentElement.classList.toggle('dark', mode === 'dark');
}
@@ -62,50 +50,6 @@
}
})();
</script>
<script>
(function () {
var readyEventName = 'tolaria:frontend-ready';
var reloadAttemptKey = 'tolaria:startup-reload-attempted';
var startupTimeoutMs = 10000;
var isTauri = '__TAURI__' in window || '__TAURI_INTERNALS__' in window;
if (!isTauri) return;
function hasReloadAttempted() {
try {
return sessionStorage.getItem(reloadAttemptKey) === '1';
} catch (err) {
return true;
}
}
function markReloadAttempted() {
try {
sessionStorage.setItem(reloadAttemptKey, '1');
return true;
} catch (err) {
return false;
}
}
function clearReloadAttempt() {
try {
sessionStorage.removeItem(reloadAttemptKey);
} catch (err) {
// Storage can be unavailable in hardened WebView/privacy modes.
}
}
window.addEventListener(readyEventName, clearReloadAttempt, { once: true });
window.setTimeout(function () {
if (window.__tolariaFrontendReady === true) return;
if (hasReloadAttempted()) return;
if (!markReloadAttempted()) return;
window.location.reload();
}, startupTimeoutMs);
})();
</script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

214
lara.lock
View File

@@ -11,10 +11,9 @@ files:
command.openSettings: 638c05f4f159a403e04aebe94b46a2a8
command.openSettings.keywords: ab6b5e03395f4db955ed9cbfd4de33b7
command.openLanguageSettings: e5a425aa6222fab3df66c0e1e0ca4183
command.openLanguageSettings.keywords: 6a910000cbf23eac3c7078e518cae66a
command.openLanguageSettings.keywords: 68efcb7e847b1a9d472baa609824be80
command.useSystemLanguage: b7f0315c47d4a59faa2a49571fcf1fca
command.openH1Setting: 05b4f6edc0e98b29670e8ee902cdb9bf
command.toggleGitignoredFilesVisibility: 0a2d3ea4e8ff8b00a801183caeb97a03
command.contribute: 9887a4451812854f0f1b6f669a874307
command.checkUpdates: f77f38f684c8b974dd4a56bb321cfd5f
command.group.navigation: 846495f9ceed11accf8879f555936a7d
@@ -39,7 +38,6 @@ files:
command.note.newType: adce5108f18945cc502a06c02445e32d
command.note.newTypedNote: 1493eda772c2179cb6247169d00723b2
command.note.saveNote: 2a309cda46e95b00d2a5a8afb3cc0047
command.note.pastePlainText: 09e8075dbd91e11878f8f4a138df760c
command.note.findInNote: f72f8f93d8e6119d5d08b60eb3a7b3bc
command.note.replaceInNote: b008e2e20c5e4cd202f4386271d6bed1
command.note.deleteNote: 56f727a2ee11d15159f1aa6373314cb6
@@ -67,19 +65,11 @@ files:
command.view.toggleProperties: 54f12756a363401243d82cbd0466117e
command.view.toggleDiff: 148a10ef6f04f897e73289f529ecae86
command.view.toggleRaw: 9b622257cd6345ceaf58e42b1410899b
command.view.noteWidthNormal: d29ab88a55ae178bf4b5257201e51801
command.view.noteWidthWide: 403f3913ad71451a90787c235f1684de
command.view.defaultNoteWidthNormal: b045349e37329df23317db98cccd3263
command.view.defaultNoteWidthWide: 41f326763e3e3954912dfd45d04bd87c
command.view.leftLayout: 5046a7166675e2d0175b9da3befdcaca
command.view.centerLayout: 3bc6759c005178aae519aa3dd227cb74
command.view.toggleAiPanel: 4279cbf31767465f25feff6e7bdd336e
command.view.newAiChat: 1b0a886d6e77f628ec96c2d71cdb0a9b
command.view.toggleBacklinks: b3c4be2d2c07bb8df181355a2c3658a7
command.view.moveViewUp: 76f92d3250e028609349d825c672f13e
command.view.moveViewDown: 9deea30162b8d3234f8d52795ad7393f
command.view.moveNamedViewUp: b4ac634d13a09eacb680ef12c69dd1f7
command.view.moveNamedViewDown: 15302c02df26fe52def77636e9141711
command.view.zoomIn: ae4a8e406b707636392b6673fca0e6a6
command.view.zoomOut: 97326f8cd9df886a6b19af180fb7dcc9
command.view.resetZoom: 193ee8c32391fc5cc303a51617cfd046
@@ -91,10 +81,6 @@ files:
command.settings.setupExternalAi: 24eea679eb699763daa2e3f2a67fcf9a
command.settings.reloadVault: f6e506dad6b6cf2be24d9438d458ae54
command.settings.repairVault: cee560b1fd5ad9d1ff1c19a0a2afe77a
command.settings.useLightMode: a76d5b1c076983bc113d247f0520c166
command.settings.useDarkMode: ba7873c42ae00542ba71db9be3b6761f
command.settings.useSystemTheme: 6d8c69d916d3df8ceed8dfc154196d07
command.settings.toggleGitignoredFilesVisibility: 0a2d3ea4e8ff8b00a801183caeb97a03
command.ai.openAgents: 612abe804edd0f5ae228a534f77f3d23
command.ai.restoreGuidance: 23331fecc692d11d85cf24838ed273f2
command.ai.switchToAgent: 5bb02187e653b360ab7ed661403271f2
@@ -105,9 +91,7 @@ files:
settings.sync.title: 36f62a237420dcdc7096ad3a9018c4d9
settings.sync.description: 06a6957c40867be2c28783183dc1bf5f
settings.pullInterval: 4f95ca677398604bca241427261ed07b
settings.pullIntervalDescription: 4a75c15f44d59dc8cb100ec2d0693048
settings.releaseChannel: 2da37055e15a217f18bd403922085c3f
settings.releaseChannelDescription: c5ae9b7f25376df383d36603366ac1fa
settings.releaseStable: fa3aff3c185c6dc7754235f397c2099a
settings.releaseAlpha: 6132295fcf5570fb8b0a944ef322a598
settings.appearance.title: a1c58e94227389415de133efdf78ea6e
@@ -115,7 +99,6 @@ files:
settings.theme.label: d721757161f7f70c5b0949fdb6ec2c30
settings.theme.light: 9914a0ce04a7b7b6a8e39bec55064b82
settings.theme.dark: a18366b217ebf811ad1886e4f4f865b2
settings.theme.system: a45da96d0bf6575970f2d27af22be28a
settings.language.title: 4994a8ffeba4ac3140beb89e8d41f174
settings.language.description: e2bb9b523067fdc32a494b1d6fd97906
settings.language.label: 244c7b77926f077b863c33ff1244e1ec
@@ -127,94 +110,25 @@ files:
settings.autogit.enable: 9b205a4ae0e3ad4e6708155880ce9c75
settings.autogit.enableDescription: a7424a8f90c0b7f290a8144d12374554
settings.autogit.idleThreshold: 28016cc6fccd951a904ee3020cd733b6
settings.autogit.idleThresholdDescription: 521140491e8c9d5a1d7b7739bc333358
settings.autogit.inactiveThreshold: d28cb60d1e70e020ae8d1294e32dd474
settings.autogit.inactiveThresholdDescription: f9e43c90bd10bae937c2cfa5c2767f1c
settings.titles.title: 761443f70a5067ecf015c6fb3fae9cef
settings.titles.description: b94aeeca78dd376cc19b9aa019e53f6c
settings.titles.autoRename: 5383db8e790ebf5f956d9c59cb4c4f67
settings.titles.autoRenameDescription: cc28c28d8817736e658082a2261d9a6e
settings.vaultContent.title: 78b883ce9acb9c25e611158a518d9185
settings.vaultContent.description: a53487bf1c7898a1459dc1510e216f7d
settings.noteWidth.default: 66be0f882801fd19468181f31f58dc20
settings.noteWidth.defaultDescription: 2b5fb6d7dfa2b1da1e47bcac30f58e8d
settings.noteWidth.normal: 960b44c579bc2f6818d2daaf9e4c16f0
settings.noteWidth.wide: e7c770a61dbdf81ca922ae0260e327c1
settings.sidebarTypePluralization.label: 1e5bd615b1abbf39129ef241f5cf2249
settings.sidebarTypePluralization.description: 8d7bb0649fd7e637552429ea91298de8
settings.vaultContent.hideGitignored: 2c07ee6c8bfe746d356f44275d42f90e
settings.vaultContent.hideGitignoredDescription: e8b1ddfa416610f9d6eb299fa123a1d6
settings.allNotesVisibility.title: 0e07c3d48b91e1a4c42c1ff3e5751ec3
settings.allNotesVisibility.description: bc9caa48296281401aad694fed65a2d9
settings.allNotesVisibility.pdfs: 27ce5e1e1761ff362dbf75c76c6b4941
settings.allNotesVisibility.pdfsDescription: 8191ed4a095cdd60ef856ce0ff213a48
settings.allNotesVisibility.images: ea15460e63b6e8e92f8c03a79eeb6e4e
settings.allNotesVisibility.imagesDescription: 1704e10d5e5cdc17d2d2a5fb7b8969bf
settings.allNotesVisibility.unsupported: 257fe04419c9f6543a43e58b1edf9eb5
settings.allNotesVisibility.unsupportedDescription: a3b8aa66900c742f001cd4533b3df44a
settings.aiAgents.title: 27f08387b26e1ceeb1b60bd9c97e7a47
settings.aiAgents.description: 042a8037f1a4f90c76ce31a49fdff59c
settings.aiAgents.description: a13222e67eb121676ff6dfc7b085f02d
settings.aiAgents.default: 6af573559fd05d8c35bc57b41cc78e24
settings.aiAgents.defaultTarget: 5acfc74fd97a6dd2d67d15c66de5eed5
settings.aiAgents.agentGroup: 965530cbbec45b1754ed3ece120399f7
settings.aiAgents.localGroup: d34f4b997ce78da5aa57d657a5d6fcb5
settings.aiAgents.apiGroup: 589a257cbd265eaa7de93de8b4084dff
settings.aiAgents.installed: 73329564760013a7824ff9d5d1af91ff
settings.aiAgents.missing: ea21841da70e6405af19fabc4ff8bdd9
settings.aiAgents.ready: 909a648c713be25fcd050725d0a41e37
settings.aiAgents.notInstalled: 0ac3f76ef78bfdbab6331731ee56ba22
settings.aiAgents.apiReady: e5b06f6ef4ef5f4bc2848d46195e3f48
settings.aiAgents.apiLocalKey: 119de2f31867b273a134f05bc56b9e57
settings.aiAgents.apiEnv: e011db3dff54ed3deae37a9615e31ebf
settings.aiAgents.apiNoKey: 5bbccbfcd09b28902bbf5319dd6e6053
settings.aiAgents.installedTitle: 5cb9b5ae5ebc66652814dff4584705cc
settings.aiAgents.installedDescription: fb9ba9667725fb40fa1e5d58a2c02692
settings.aiAgents.noVersion: 89831adb7114946e916b9c31d57a1e33
settings.aiProviders.title: 63eb84d8510f9f086d282ec7d47adaa1
settings.aiProviders.description: 6c86c013eb6fb198b51e29ec47d66464
settings.aiProviders.localTitle: c7c20443d8aaf04271df9d713f5c02ce
settings.aiProviders.localDescription: 7abed40ef2e46741c64ff47e423cca39
settings.aiProviders.apiTitle: 66537b9e5200ebbbfbd091144d8afbcc
settings.aiProviders.apiDescription: 6ec773352e05a805af3abcebeff4e756
settings.aiProviders.kind: 27703c8f150ac4bb0a3a83a7857353af
settings.aiProviders.kind.ollama: c23e8db458397f37c8e8d98e94e55a18
settings.aiProviders.kind.lmStudio: 7b9b319662715e90583e823a6c3cfdbe
settings.aiProviders.kind.openAi: 0523b13262b12c215d8009938f5c14f1
settings.aiProviders.kind.anthropic: f431db65cea024a5f19eab835afefee2
settings.aiProviders.kind.gemini: 766cc4dd4d5005652e8514e3513683f8
settings.aiProviders.kind.openRouter: a0607683815b6585fb80e7cac72ac59a
settings.aiProviders.kind.compatible: 1a4dc90a183c26d9026c6ac7727aa4cd
settings.aiProviders.name: 49ee3087348e8d44e1feda1917443987
settings.aiProviders.baseUrl: ade86bc4899761ad46c52e381b6228bb
settings.aiProviders.model: 6bd36c36869288188863bd53a9bc5ed1
settings.aiProviders.key: 656a6828d7ef1bb791e42087c4b5ee6e
settings.aiProviders.keyPlaceholder: 57dbf3720af3f156018bfff7f5a99b23
settings.aiProviders.keyStorage: 3e5b8cfbc66d3a33dad3fda058bf5851
settings.aiProviders.keyStorage.local: 7c6189f734f3c66d162a3f06529af1d9
settings.aiProviders.keyStorage.env: a71b071971d7c54e9af381c9bea093d9
settings.aiProviders.keyStorage.none: 94c841331ccd5179a283487f96285c90
settings.aiProviders.keyEnv: cb72671bd5cf65512d4c94e8ad6182ef
settings.aiProviders.keySafety: 0d59d7e4def35e51c4bb785d74180afc
settings.aiProviders.keySafetyLocal: 8ef2ba0192ac725fa1a3e737e852387c
settings.aiProviders.localSafety: 60fb65e4e0d35cec3135584d8219e8fd
settings.aiProviders.add: a9f8e8b4ca3cbc4fc3bff5d59d6a5419
settings.aiProviders.addLocal: 681985f2e412f22fcd55a0553f6e4bee
settings.aiProviders.addApi: 767b46f2a51aa9dd050cb226cd86e192
settings.aiProviders.test: 602009d5aea1c1ef06fd777968f7e80e
settings.aiProviders.testing: 9d770c909c2c69b09eae2372c4cf405d
settings.aiProviders.testSuccess: 51677ed54231e41a4fe40850d90b64b2
settings.aiProviders.empty: 9378027dec7f93fcc5011c5e528bdf82
settings.aiProviders.defaultEndpoint: 666b355c305dd4c7b4649d3d7b4c5d11
settings.aiProviders.keyLocalSaved: f4a102d6a67212e21f0f31f0758aea81
settings.aiProviders.keyEnvSaved: bfed2d745dd76dbea9341d27e06c39f2
settings.aiProviders.noKey: 1ab089f80ef4dc6c1ce3e173f5b405a0
settings.workflow.title: 24f47cdbe9ddba774a7cc53e51d9032e
settings.workflow.description: aa9a07539b87cf407c3edc8a22732d45
settings.workflow.explicit: 54a5b5c27937d25271c3127d093e8361
settings.workflow.explicitDescription: ce523427b5dd0f9895f63d1fbb90ad24
settings.workflow.autoAdvance: 9ed337f5bc61603d19a1ef35f3a3a243
settings.workflow.autoAdvanceDescription: ae58ad8abf03df7cbdae8c3a725258f7
settings.privacy.title: aa96a21412def0d916f43b639424f8e4
settings.privacy.title: 0dd6c14e00cc9f45a68c1bca88d1317d
settings.privacy.description: 33d53059be620b58e38b8e5c5ab26b27
settings.privacy.crashReporting: b4efc5b61526566d431e99242f5c21b4
settings.privacy.crashReportingDescription: e9d7c7efce44adc08d0460be1aad5c42
@@ -224,90 +138,6 @@ files:
settings.cancel: ea4788705e6873b424c65e91c2846b19
settings.save: c9cc8cce247e49bae79f15173ce97354
common.cancel: ea4788705e6873b424c65e91c2846b19
common.create: 686e697538050e4664636337cc3b834f
common.save: c9cc8cce247e49bae79f15173ce97354
common.remove: 1063e38cb53d94d386f21227fcd84717
customize.color: cb5feb1b7314637725a2e73bdc9f7295
customize.icon: 817434295a673aed431435658b65d9a7
customize.searchIcons: 0688a8098567785a05cb4cf937cbb880
customize.noIconsFound: 89c25bea716a589b11599f2bbcf34054
customize.template: 278c491bdd8a53618c149c4ac790da34
customize.templatePlaceholder: 44482755791f96f37118f3a3bc66f668
customize.done: f92965e2c8a7afb3c1b9a5c09a263636
viewDialog.title.create: 01e53f288ebe0b7bdda19c8151aa7710
viewDialog.title.edit: 6369f1c063cb2c22efa584a19e98d97f
viewDialog.description.create: a9600940be61edfcad2b47fb72965406
viewDialog.description.edit: d4e6333950d5031c5f4ae766a1f1d886
viewDialog.nameLabel: 49ee3087348e8d44e1feda1917443987
viewDialog.namePlaceholder: dbb077d36957a33c4147eb24919192e1
viewDialog.filtersLabel: f3f43e30c8c7d78c6ac0173515e57a00
viewDialog.saveError: 80fe81f3ca09ea03af03fdb9ae809eda
viewDialog.selectedIcon: 5f98b507ccec0bbc51ca7e8a5b270250
viewDialog.selectedColor: 192ec1a9973f328c3620cd3fa670c745
ai.permission.safe.short: c6eea0560cd6f377e78dff2c85cc9122
ai.permission.safe.control: 386cc66605d50e5bf1fb726f97e55b55
ai.permission.safe.tooltip: 3bd635e2b2223da302994d77bfd7edfd
ai.permission.powerUser.short: d4a47db271fc1ef3cd17f7396326b057
ai.permission.powerUser.control: d4a47db271fc1ef3cd17f7396326b057
ai.permission.powerUser.tooltip: 2585ecdb79ca02a3ee779bb61552de03
ai.permission.modeAria: 3ff6346566a4c44fd5c7395907125d3e
ai.permission.changed: b5d06ad47e6053fe5a73f4571a40e00c
ai.panel.title: 4412225fe5b326643b8c2e1d9b7b10ae
ai.panel.status.checking: 118740af1c4521b917e29791d661db82
ai.panel.status.missing: fe07f8eac233c229d81cbe9aa25668a0
ai.panel.status.ready: 296a0ac791eab2df130789f55fdc109b
ai.panel.mode.chat: 55dcdf017b51fc96f7b5f9d63013b95d
ai.panel.mode.chatDescription: d7744ae39e7920a8eac7b21b58fce348
ai.panel.copyMcpConfig: 6a8c9fe401557c870e3a90bbfab01b25
ai.panel.mcpConfig: 53bac93d0314941c8d2c1163026f3ad9
ai.panel.newChat: 1b0a886d6e77f628ec96c2d71cdb0a9b
ai.panel.close: d00ba22b83762f5a6e66db0be9e916d7
ai.panel.linkedCount: 5292fda189a357c9597b1cb0e1b0900b
ai.panel.empty.checkingTitle: 0dde2076cb53fe497d05d231296d50bb
ai.panel.empty.checkingDescription: 31e41510d851c366860377ba0c956f0b
ai.panel.empty.missingTitle: a2b764da52cb43b191bb4ecfd8ea4d26
ai.panel.empty.missingDescription: 91825c7d86b1d588d44a33e1d94af950
ai.panel.empty.withContextTitle: 66f02985937083e4ef62b98cc9f25aee
ai.panel.empty.noContextTitle: e919aeb0a49b58e494e43e5f6f6597c9
ai.panel.empty.withContextDescription: dd8faefcc8160a2db800a41d6628facb
ai.panel.empty.noContextDescription: 6a6e610835808016d1f2790c3f991a54
ai.panel.placeholder.checking: 08fec77719c37d3d6279b2641b37b4d3
ai.panel.placeholder.missing: 547c492026d115608067988b7cfbb9a9
ai.panel.placeholder.ready: 31f5265c2e0432a7ca10d08e5c6f3114
ai.panel.send: 747c6a3564774149c989884e0c581d53
mcp.setup.manageTitle: d786c7b9ab9b2406258648931bca0e01
mcp.setup.setupTitle: b4b373f690c8a0008885b31e78e93a5c
mcp.setup.connectedDescription: a2a3be70c0ac9fa7ef112df5252249fa
mcp.setup.setupDescription: 8bf199ffbcf303a99abac078c0c5e094
mcp.setup.reconnect: 813ba447b5e64c17ff9b68c693358ca3
mcp.setup.connect: fb95777dbc9f7b22014dc360a2957652
mcp.setup.disconnect: 42ae25231906c83927831e0ef7c317ac
mcp.setup.connecting: 182e7eda4e721ad00737460b88701dee
mcp.setup.disconnecting: 11de134aefe51cb4a5c545b5a057a871
mcp.setup.nodeRequirement: ac58a35cf21d2204fed548c227f77594
mcp.setup.writeEntryDescription: 370aa4403d2ffc37f574503bc3cd00e6
mcp.setup.clientPathsDescription: 94565852412051c799e78d2f4525ff53
mcp.setup.geminiGuidanceDescription: 182cf5b84b2f56cb1cf41dcdb5e093d8
mcp.setup.manual.title: 2188ca52346158cbd103ca7ce7c12dd9
mcp.setup.manual.copy: 6a8c9fe401557c870e3a90bbfab01b25
mcp.setup.manual.loading: 11ee201c121c8e2015c065952474a3ea
mcp.setup.manual.unavailable: 8b4330b839c41bad378c86c91ee0b5cd
mcp.toast.connected: 7716c0f9dad41a7fdbef592074459239
mcp.toast.refreshed: 227d8721ebec3077792b1223c1f30e54
mcp.toast.disconnected: 9b5254079561087d679e35aa10c80638
mcp.toast.alreadyDisconnected: 645bec2e33bd9de2b334b704d9fa84cf
mcp.toast.configCopied: 458b50444b9a1cb069b00475c0951b8a
mcp.toast.configCopyFailed: b451cfb95f87458d142d026a8aca74cf
mcp.toast.setupFailed: 51f290962c08d8ba76bfc6c19552eaed
mcp.toast.disconnectFailed: ddc3387ab6e996ce15bc4950a3548e1e
mcp.error.clipboardUnavailable: 3b9ed3fe7dba627edb74e1cdaa02b2ee
save.toast.saved: 248336101b461380a4b2391a7625493d
save.toast.nothingToSave: 86ae8cef7371e639f3c007449a2f7d1f
save.toast.missingActiveVault: 49cf5064a7dce757f895950d8beeed68
save.error.failed: b55b9fefe93fabae3c277e4e7956a534
save.error.autoFailed: 09b2b131f07baddb5ba8f5a3ee6234c5
save.error.invalidPath: c96fb669715e86508f4c5e73da9f6995
savedViews.reordered: 531081a566b826eed2e714ce70a0de08
locale.en: 78463a384a5aa4fad5fa73e2f506ecfc
sidebar.nav.inbox: 3882d32c66e7e768145ecd8f104b0c08
sidebar.nav.allNotes: cd76cf851b08a9d2feafaf255bc1f4d5
@@ -318,16 +148,10 @@ files:
sidebar.group.folders: d86e2756f698bea5aac3e2276639f042
sidebar.action.createView: ba019414ea8c7fcdb4a83a70ec1bb639
sidebar.action.editView: acdf34bd08b0692b906d446e590b1eb9
sidebar.action.renameView: 429b903fddf39bb21887282d188c4b62
sidebar.action.deleteView: c8f4f9cd8ff820f955474e5c9f70ff39
sidebar.action.reorderView: ca2f42548fd57160d22fd0809bf8ed6e
sidebar.action.moveViewUp: e601b7f0901f12efb71f7821c0fdb79e
sidebar.action.moveViewDown: 6f86951085367864e3e4eb13f5c8688e
sidebar.action.customizeSections: 050a134cad1e9c6f04abe5d635592703
sidebar.action.renameSection: 8936914051df04e7550d0eeb4a31e0e0
sidebar.action.renameType: 327d51d97d4b0e3ddcb7afa39c4bf5fa
sidebar.action.customizeIconColor: 9a2f685c74d261ab483ef00dee5314fe
sidebar.action.deleteType: c52e15c44f6372c6ddb2758254a041ef
sidebar.action.createType: 6cfb8b8aa3fdce38e675819691b48f5c
sidebar.action.collapse: 00873bdddd457791609bff1243c40a6b
sidebar.action.expand: 8f98e9696f6eed958573012f52710faa
@@ -338,7 +162,6 @@ files:
sidebar.action.copyFolderPathMenu: 1779ec6018a385912c1a5499a1bbf2b2
sidebar.action.renameFolderMenu: deb1d8fa030292e7eda2c244b313aca2
sidebar.action.deleteFolderMenu: c78c889ac7396dc148fc859c9221e545
sidebar.view.name: 67b904f75d1872abba088a3442caff40
sidebar.folder.name: 710417c4e57a6a01500fe4c0c448ce3d
sidebar.folder.newName: e0ad5b2db20359a85de80c1231323bea
sidebar.folder.expand: b7545cb143816942ff096d890090fb6f
@@ -416,40 +239,23 @@ files:
editor.find.replaceAll: b1c94ca2fbc3e78fc30069c8d0f01680
editor.toolbar.rawReturn: 5ddcf5e0dc8b933b344bb6ca3d8e131d
editor.toolbar.rawOpen: 89ad60735a649b60dacd2301cda0c4ec
editor.toolbar.noteWidthNormal: 9bb55413ec536c4a8fa41c4c4464ab44
editor.toolbar.noteWidthWide: 1a744f406c9150f76739bd167c3e47ec
editor.toolbar.centerLayout: 3fabf7e9b285eeec90704d271f1049b7
editor.toolbar.leftLayout: acab6c8612816e012ff9f4220a42e485
editor.toolbar.removeFavorite: 7188286e36fc6c1892dd53aaa54237fb
editor.toolbar.addFavorite: 910885435dbc44a22b68cb45c79e4a7e
editor.toolbar.markUnorganized: 83fb1b23a3609fab493737fe7af0a198
editor.toolbar.markOrganized: 9d170c916afae3d7a82456838728ffa5
editor.toolbar.openNeighborhood: 80f659c99954ab7681266b5a1e5da6df
editor.toolbar.noDiff: b2c4189f90648aaeb5cd2e81f9bd8d94
editor.toolbar.loadingDiff: 430386d6aae3570fd399a133e41c7372
editor.toolbar.gitDiff: 51b46b6eac27ba484479246b875f76f5
editor.toolbar.showDiff: 08d579de8d3abdcdfce0953a797362c6
editor.toolbar.openAi: d2e93006570a66709c8ce0dc27082ef1
editor.toolbar.closeAi: cd46973ef73076d612dff9903ce54498
editor.toolbar.openTableOfContents: b733f053ea3fde4a9acd589ec7f58c10
editor.toolbar.closeTableOfContents: d3292972a10edd1a06a627f3552f760a
editor.toolbar.restoreArchived: 1bff5cf4943af64c05b2e9aeadccbc84
editor.toolbar.archive: 63dc964c32e715217b49f8739d49636b
editor.toolbar.delete: f48134a07b016a1de05d4ba6d2fbfb41
editor.toolbar.revealFile: 76f4ed52da9937ae432dcf5a108fabb4
editor.toolbar.copyFilePath: 64c6cec5ca57efbb8c9c93ae5fbccd27
editor.toolbar.moreActions: 89e19353176b94068dec4c768c25e70b
editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e
tableOfContents.title: f61d6c3e3733db355168b7e1aee6780b
tableOfContents.close: d3292972a10edd1a06a627f3552f760a
tableOfContents.empty: e4f02ad69cbbce1ec65d14215e3d5871
tableOfContents.emptyNoNote: 046d95682b747a30ed8a7b0b1d581629
tableOfContents.navLabel: 598b8ae10dfce818e73d3218daddbdae
tableOfContents.untitledHeading: 93c2dde207965b383b7b22af005ebe4f
tableOfContents.expandHeading: 6353ab44fe75f5dd935789bdab8551a0
tableOfContents.collapseHeading: 040b4c102283028831ea78713235e478
editor.codeBlock.copy: 8e477020fb3f7ab844b91ff1d7de8347
editor.imageLightbox.title: 2c5a15c875bcdc2478c4a5cad044e10c
editor.filename.rename: c31c2b468229232ad6287e734fe67d96
editor.filename.renameToTitle: bff34a6a2dd1eb87c59403946679237f
editor.filename.trigger: 4e9739c2f937c828bbf5d1f935fe7fb9
@@ -463,8 +269,6 @@ files:
inspector.title.properties: 9fc2d28c05ed9eb1d75ba4465abf15a9
inspector.title.propertiesShortcut: 427d1cbdc813cf54c5fac2508d38d407
inspector.title.closePropertiesShortcut: 97b953e45042d4143dd19624dc345d29
inspector.title.collidingProperties: bf71358d0af34696e88bea278eac046a
inspector.title.collidingPropertiesAria: ce5c3569f304cd69b0f33a6fc185f49b
inspector.empty.noNoteSelected: 046d95682b747a30ed8a7b0b1d581629
inspector.empty.noProperties: 6864166e0f65d81b1eb474e41fde8822
inspector.empty.initializeProperties: edf249d214b68f5afe8634c577b709c4
@@ -493,7 +297,6 @@ files:
inspector.info.words: 6f15b8d4b7287d60a8ea3d1c5cbadc84
inspector.info.size: 6f6cb72d544962fa333e2e34ce64f719
update.available: 992477975168b876be8e77ce2975e390
update.checking: a07813cc05571f23ce8dd7039583d7da
update.releaseNotes: 5dd03e8d039863e563e049be198c3fd3
update.updateNow: 99b1054c0f320be9f109c877a5efd0b2
update.dismiss: c8a59e7135a20b362f9c768b09454fdb
@@ -504,8 +307,6 @@ files:
status.zoom.reset: dbf36ab275e5fbd256590e65aa1ca9a3
status.feedback.contribute: 96ab5025e38aef43fdb73c9830795264
status.feedback.label: 9887a4451812854f0f1b6f669a874307
status.docs.open: 2066997d2262d4a9b79de68f255dc4a9
status.docs.label: a3907cd461d8739aa3266047bc4b8c0c
status.theme.light: 4abf27a209985375949aaa426c7c7f3d
status.theme.dark: 541be2a55a52b518b8e510c476c7cc95
status.settings.open: bae08226d065231df6f01b08cd681c9b
@@ -517,8 +318,6 @@ files:
status.vault.cloneGit: a7d0aef7c6237a36e54fd5a26e9c23fe
status.vault.cloneGettingStarted: 9953a11a477b441976a626a9acf5d7df
status.vault.notFound: 3119b7fa94c96209bca571b7c7ed0b7e
status.vault.reloading: 893da50ef3a9e01d8a99ff5d3ceb55e9
status.vault.reloadingTooltip: 4a884bd7d113797ad16f905f70742da8
status.vault.remove: 7f3b4f76df23626170940dbc55c70728
status.remote.noneConfigured: 18a4edd4e8d862ccb343937f45585fb1
status.remote.inSync: a56f571b4df55d88fb06e61e343b3c91
@@ -569,7 +368,6 @@ files:
status.ai.noAgentsTooltip: 1bdc02ec2dd7f9701b4371ffd5770318
status.ai.selectedMissing: a37f3dbc73725219e90d709fe0d3ad97
status.ai.defaultAgent: 79650d57d1678e56c75dbbcba6ea87f7
status.ai.defaultTarget: 65ea5bdb609833c4baffd2cbd0e86542
status.ai.restoreDetails: 47a913b58ce40add6521bc93e807476f
status.ai.withGuidance: 1eafd34810db6bba54dac7aa549b5182
status.ai.active: 059e3a3167c2ab00f4ca872e82a48d98
@@ -579,9 +377,6 @@ files:
status.ai.vaultGuidance: 7bb4644efe6ae7cec9993c8bd85f1519
status.ai.restoreGuidance: 23331fecc692d11d85cf24838ed273f2
status.ai.openOptions: d387b1ab67586c7a2d83db8900a4dd8e
status.ai.modelTargets: 8f3ceb96e088f8bdaecc236778401ac9
status.ai.localChat: 215a24e3d38fb8b68deea6dad0142326
status.ai.apiChat: 6dffd5b9997885efe701631cea5c470d
pulse.title: 16d2b386b2034b9488996466aaae0b57
pulse.today: 1dd1c5fb7f25cd41b291d43a89e3aefd
pulse.yesterday: ebfe9ce86e6e9fb953aa7a25b59c1956
@@ -607,8 +402,5 @@ files:
locale.ptPT: 71bfc0d79772120b52c1c0782450ff84
locale.es419: edbf64ac382b82a54795fb409beb54be
locale.zhCN: 1e81fc32fea0e9f3a978661e4b5e484d
locale.zhTW: d0bb8c23f6e4f8c8037b6774ba912036
locale.jaJP: f32ced6a9ba164c4b3c047fd1d7c882e
locale.koKR: d0bdb3cde477d82e766da05ebda50ccb
locale.vi: 7b80fae85640c16cdb0261bef0c27636
locale.plPL: c730389bc8d99e59c867766babdd48b5

View File

@@ -18,11 +18,8 @@ locales:
- pt-PT
- es-419
- zh-CN
- zh-TW
- ja-JP
- ko-KR
- vi
- pl-PL
files:
json:

View File

@@ -2,8 +2,9 @@
/**
* Tolaria MCP Server — lightweight vault tools for AI agents.
*
* These MCP tools provide Tolaria-specific capabilities alongside each
* app-managed agent's own Safe / Power User permission profile:
* The agent has full shell access (bash, read, write, edit).
* These MCP tools provide Tolaria-specific capabilities that
* native tools cannot replace:
*
* - search_notes: full-text search across vault notes
* - get_vault_context: vault structure overview (types, note count, folders)
@@ -29,69 +30,27 @@ const WS_UI_URL = `ws://localhost:${WS_UI_PORT}`
// Connect as a WebSocket CLIENT to the UI bridge (run by ws-bridge.js).
// The bridge relays messages to all other clients (the React frontend).
let uiSocket = null
let reconnectTimer = null
let shutdownStarted = false
const RECONNECT_INTERVAL_MS = 3000
function connectUiBridge() {
if (shutdownStarted) return
try {
const ws = new WebSocket(WS_UI_URL)
uiSocket = ws
ws.on('open', () => {
if (shutdownStarted) {
closeUiSocket()
return
}
uiSocket = ws
console.error(`[mcp] Connected to UI bridge at ${WS_UI_URL}`)
})
ws.on('close', () => {
if (uiSocket === ws) uiSocket = null
scheduleUiReconnect()
uiSocket = null
setTimeout(connectUiBridge, RECONNECT_INTERVAL_MS)
})
ws.on('error', () => {
// Silent — bridge may not be running yet, will retry
})
} catch {
scheduleUiReconnect()
setTimeout(connectUiBridge, RECONNECT_INTERVAL_MS)
}
}
function scheduleUiReconnect() {
if (shutdownStarted) return
clearUiReconnectTimer()
reconnectTimer = setTimeout(connectUiBridge, RECONNECT_INTERVAL_MS)
reconnectTimer.unref?.()
}
function clearUiReconnectTimer() {
if (!reconnectTimer) return
clearTimeout(reconnectTimer)
reconnectTimer = null
}
function closeUiSocket() {
const socket = uiSocket
uiSocket = null
if (!socket) return
socket.removeAllListeners()
socket.on('error', () => {})
if (socket.readyState === WebSocket.CONNECTING) {
socket.terminate?.()
return
}
try {
socket.close()
} catch {
// Ignore close races during process teardown.
}
socket.terminate?.()
}
connectUiBridge()
function broadcastUiAction(action, payload) {
if (!uiSocket || uiSocket.readyState !== WebSocket.OPEN) return
@@ -162,6 +121,15 @@ const TOOLS = [
},
]
const TOOL_HANDLERS = {
search_notes: handleSearchNotes,
get_vault_context: handleVaultContext,
get_note: handleGetNote,
open_note: handleOpenNote,
highlight_editor: handleHighlightEditor,
refresh_vault: handleRefreshVault,
}
async function handleSearchNotes(args) {
const results = await searchNotes(VAULT_PATH, args.query, args.limit)
const text = results.length === 0
@@ -198,25 +166,6 @@ function handleRefreshVault(args) {
return { content: [{ type: 'text', text: 'Vault refresh triggered' }] }
}
function callToolHandler(name, args) {
switch (name) {
case 'search_notes':
return handleSearchNotes(args)
case 'get_vault_context':
return handleVaultContext()
case 'get_note':
return handleGetNote(args)
case 'open_note':
return handleOpenNote(args)
case 'highlight_editor':
return handleHighlightEditor(args)
case 'refresh_vault':
return handleRefreshVault(args)
default:
throw new Error(`Unknown tool: ${name}`)
}
}
// --- Server setup ---
const server = new Server(
@@ -230,8 +179,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
const handler = TOOL_HANDLERS[name]
if (!handler) {
throw new Error(`Unknown tool: ${name}`)
}
try {
return await callToolHandler(name, args)
return await handler(args)
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
@@ -240,47 +193,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
})
async function shutdown(exitCode = 0) {
if (shutdownStarted) return
shutdownStarted = true
clearUiReconnectTimer()
closeUiSocket()
try {
await server.close()
} catch (error) {
console.error(`[mcp] Error while closing server: ${error.message}`)
}
process.exitCode = exitCode
setImmediate(() => process.exit(exitCode))
}
async function main() {
const transport = new StdioServerTransport()
server.onclose = () => {
void shutdown(0)
}
process.stdin.once('end', () => {
void shutdown(0)
})
process.stdin.once('close', () => {
void shutdown(0)
})
process.once('SIGINT', () => {
void shutdown(0)
})
process.once('SIGTERM', () => {
void shutdown(0)
})
connectUiBridge()
await server.connect(transport)
console.error(`Tolaria MCP server running (vault: ${VAULT_PATH})`)
}
main().catch((error) => {
console.error(error)
void shutdown(1)
})
main().catch(console.error)

View File

@@ -14,9 +14,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.13",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"version": "1.19.9",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -431,12 +431,12 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.2.tgz",
"integrity": "sha512-Ybv7bqtOgA914MLwaHWVFXMpMYeR1MQu/D+z2MaLYteqBsTIp9sY3AU7mGNLMJv8eLg8uQMpE20I+L2Lv49nSg==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
"ip-address": "10.0.1"
},
"engines": {
"node": ">= 16"
@@ -619,9 +619,9 @@
}
},
"node_modules/hono": {
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz",
"integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -670,9 +670,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -882,9 +882,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
"integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT",
"funding": {
"type": "opencollective",

View File

@@ -12,11 +12,5 @@
"@modelcontextprotocol/sdk": "^1.0.0",
"gray-matter": "^4.0.3",
"ws": "^8.19.0"
},
"overrides": {
"@hono/node-server": "1.19.13",
"express-rate-limit": "8.2.2",
"hono": "4.12.14",
"path-to-regexp": "8.4.0"
}
}

View File

@@ -1,12 +1,8 @@
import { describe, it, before, after } from 'node:test'
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import {
mkdtemp, mkdir, open, rm,
} from 'node:fs/promises'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
import { fileURLToPath } from 'node:url'
import {
findMarkdownFiles, getNote, searchNotes, vaultContext,
} from './vault.js'
@@ -15,15 +11,14 @@ import { evaluateBridgeRequest } from './ws-bridge.js'
let tmpDir
const ACTIVE_VAULT_ERROR = 'Note path must stay inside the active vault'
const MCP_SERVER_DIR = path.dirname(fileURLToPath(import.meta.url))
before(async () => {
tmpDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-test-'))
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-test-'))
await mkdir(path.join(tmpDir, 'project'), { recursive: true })
await mkdir(path.join(tmpDir, 'note'), { recursive: true })
await fs.mkdir(path.join(tmpDir, 'project'), { recursive: true })
await fs.mkdir(path.join(tmpDir, 'note'), { recursive: true })
await writeTextFile(path.join(tmpDir, 'project', 'test-project.md'), `---
await fs.writeFile(path.join(tmpDir, 'project', 'test-project.md'), `---
title: Test Project
is_a: Project
status: Active
@@ -34,7 +29,7 @@ status: Active
This is a test project for the MCP server.
`)
await writeTextFile(path.join(tmpDir, 'note', 'daily-log.md'), `---
await fs.writeFile(path.join(tmpDir, 'note', 'daily-log.md'), `---
title: Daily Log
is_a: Note
---
@@ -44,7 +39,7 @@ is_a: Note
Today I worked on the MCP server implementation.
`)
await writeTextFile(path.join(tmpDir, 'project', 'second-project.md'), `---
await fs.writeFile(path.join(tmpDir, 'project', 'second-project.md'), `---
title: Second Project
type: Project
status: Draft
@@ -59,7 +54,7 @@ Another project for testing list and context.
})
after(async () => {
await rm(tmpDir, { recursive: true, force: true })
await fs.rm(tmpDir, { recursive: true, force: true })
})
describe('findMarkdownFiles', () => {
@@ -213,79 +208,17 @@ describe('requireVaultPath', () => {
})
})
describe('stdio process lifecycle', () => {
it('exits when the MCP client closes stdin', async () => {
const child = spawn(process.execPath, ['index.js'], {
cwd: MCP_SERVER_DIR,
env: { ...process.env, VAULT_PATH: tmpDir, WS_UI_PORT: '65534' },
stdio: ['pipe', 'ignore', 'pipe'],
})
let stderr = ''
child.stderr.setEncoding('utf8')
child.stderr.on('data', chunk => {
stderr += chunk
})
await sleep(200)
child.stdin.end()
const exit = await waitForExit(child, 1_500)
if (!exit) {
child.kill()
await waitForExit(child, 1_000)
assert.fail(`MCP server stayed alive after stdin closed.\n${stderr}`)
}
assert.equal(exit.signal, null)
assert.equal(exit.code, 0, stderr)
})
})
async function assertRejectsOutsideVault(prefix, resolveNotePath) {
const outsideDir = await mkdtemp(path.join(os.tmpdir(), prefix))
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
const outsideNote = path.join(outsideDir, 'outside.md')
try {
await writeTextFile(outsideNote, '# Outside\n')
await fs.writeFile(outsideNote, '# Outside\n')
await assert.rejects(
() => getNote(tmpDir, resolveNotePath(outsideNote)),
{ message: ACTIVE_VAULT_ERROR },
)
} finally {
await rm(outsideDir, { recursive: true, force: true })
await fs.rm(outsideDir, { recursive: true, force: true })
}
}
async function writeTextFile(filePath, content) {
const handle = await open(filePath, 'w')
try {
await handle.writeFile(content, 'utf-8')
} finally {
await handle.close()
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
const timer = setTimeout(() => {
cleanup()
resolve(null)
}, timeoutMs)
child.once('exit', onExit)
function onExit(code, signal) {
cleanup()
resolve({ code, signal })
}
function cleanup() {
clearTimeout(timer)
child.off('exit', onExit)
}
})
}

View File

@@ -1,9 +1,8 @@
/**
* Vault operations — read-only helpers for Tolaria markdown vault.
* Write operations are handled by the app-managed agent's active permission
* profile and native file-edit tools when available.
* Vault operations — read-only helpers for Laputa markdown vault.
* Write operations are handled by the agent's native bash/write/edit tools.
*/
import { open, opendir, realpath } from 'node:fs/promises'
import fs from 'node:fs/promises'
import path from 'node:path'
import matter from 'gray-matter'
@@ -16,17 +15,17 @@ const ACTIVE_VAULT_ERROR = 'Note path must stay inside the active vault'
*/
export async function findMarkdownFiles(dir) {
const results = []
const items = await opendir(dir)
for await (const item of items) {
const items = await fs.readdir(dir, { withFileTypes: true })
for (const item of items) {
await collectMarkdownFile(results, dir, item)
}
return results
}
async function resolveVaultNotePath(vaultPath, notePath) {
const vaultRoot = await realpath(vaultPath)
const vaultRoot = await fs.realpath(vaultPath)
const requestedPath = resolveRequestedNotePath(vaultRoot, notePath)
const noteRealPath = await realpath(requestedPath)
const noteRealPath = await fs.realpath(requestedPath)
const relativePath = path.relative(vaultRoot, noteRealPath)
if (!isVaultRelativePath(relativePath)) {
@@ -51,7 +50,7 @@ export async function getNote(vaultPath, notePath) {
noteRealPath,
relativePath,
} = await resolveVaultNotePath(vaultPath, notePath)
const raw = await readUtf8File(noteRealPath)
const raw = await fs.readFile(noteRealPath, 'utf-8')
const parsed = matter(raw)
return {
path: relativePath,
@@ -74,7 +73,7 @@ export async function searchNotes(vaultPath, query, limit = 10) {
for (const filePath of files) {
if (results.length >= limit) break
const content = await readUtf8File(filePath)
const content = await fs.readFile(filePath, 'utf-8')
const filename = path.basename(filePath, '.md')
const titleMatch = extractTitle(content, filename)
if (!matchesSearchQuery(titleMatch, content, q)) continue
@@ -126,8 +125,7 @@ export async function vaultContext(vaultPath) {
async function collectMarkdownFile(results, dir, item) {
if (item.name.startsWith('.')) return
const full = resolveInside(dir, item.name)
if (!full) return
const full = path.join(dir, item.name)
if (item.isDirectory()) {
results.push(...await findMarkdownFiles(full))
return
@@ -140,16 +138,7 @@ async function collectMarkdownFile(results, dir, item) {
function resolveRequestedNotePath(vaultRoot, notePath) {
if (path.isAbsolute(notePath)) return notePath
const resolved = resolveInside(vaultRoot, notePath)
if (!resolved) throw new Error(ACTIVE_VAULT_ERROR)
return resolved
}
function resolveInside(root, target) {
const resolved = path.resolve(root, target)
const relative = path.relative(root, resolved)
if (isVaultRelativePath(relative)) return resolved
return null
return path.resolve(vaultRoot, notePath)
}
function isVaultRelativePath(relativePath) {
@@ -161,11 +150,11 @@ function matchesSearchQuery(title, content, query) {
}
async function readVaultContextNote(vaultPath, filePath) {
const raw = await readUtf8File(filePath)
const raw = await fs.readFile(filePath, 'utf-8')
const parsed = matter(raw)
const rel = path.relative(vaultPath, filePath)
const topFolder = extractTopFolder(rel)
const stat = await statFile(filePath)
const stat = await fs.stat(filePath)
const type = parsed.data.type || parsed.data.is_a || null
return {
@@ -189,8 +178,8 @@ async function readConfigFiles(vaultPath) {
const configFiles = {}
try {
const agentsPath = resolveInside(vaultPath, 'config/agents.md')
if (agentsPath) configFiles.agents = await readUtf8File(agentsPath)
const agentsPath = path.join(vaultPath, 'config', 'agents.md')
configFiles.agents = await fs.readFile(agentsPath, 'utf-8')
} catch {
// config/agents.md may not exist yet
}
@@ -198,24 +187,6 @@ async function readConfigFiles(vaultPath) {
return configFiles
}
async function readUtf8File(filePath) {
const handle = await open(filePath, 'r')
try {
return await handle.readFile('utf-8')
} finally {
await handle.close()
}
}
async function statFile(filePath) {
const handle = await open(filePath, 'r')
try {
return await handle.stat()
} finally {
await handle.close()
}
}
/**
* Extract title from markdown content (first H1 or frontmatter title).
* @param {string} content

View File

@@ -38,7 +38,6 @@ const TRUSTED_UI_ORIGINS = new Set([
/** @type {WebSocketServer | null} */
let uiBridge = null
let vaultPath = null
const UNKNOWN_TOOL = Symbol('unknown tool')
function activeVaultPath() {
vaultPath ??= requireVaultPath()
@@ -54,65 +53,30 @@ function broadcastUiAction(action, payload) {
}
async function readNoteTool(args) {
const note = await getNote(activeVaultPath(), args.path)
return { content: note.content, frontmatter: note.frontmatter }
}
function uiOpenNoteTool(args) {
broadcastUiAction('vault_changed', { path: args.path })
broadcastUiAction('open_note', { path: args.path })
return { ok: true }
}
function uiOpenTabTool(args) {
broadcastUiAction('vault_changed', { path: args.path })
broadcastUiAction('open_tab', { path: args.path })
return { ok: true }
}
function highlightTool(args) {
broadcastUiAction('highlight', { element: args.element, path: args.path })
return { ok: true }
}
function uiSetFilterTool(args) {
broadcastUiAction('set_filter', { filterType: args.type })
return { ok: true }
}
function refreshVaultTool(args) {
broadcastUiAction('vault_changed', { path: args?.path })
return { ok: true }
}
const TOOL_EXECUTORS = [
['open_note', readNoteTool],
['read_note', readNoteTool],
['search_notes', (args) => searchNotes(activeVaultPath(), args.query, args.limit)],
['vault_context', () => vaultContext(activeVaultPath())],
['ui_open_note', uiOpenNoteTool],
['ui_open_tab', uiOpenTabTool],
['ui_highlight', highlightTool],
['highlight_editor', highlightTool],
['ui_set_filter', uiSetFilterTool],
['refresh_vault', refreshVaultTool],
]
function callToolHandler(tool, args) {
const executor = TOOL_EXECUTORS.find(([name]) => name === tool)?.[1]
return executor ? executor(args) : UNKNOWN_TOOL
const TOOL_HANDLERS = {
open_note: (args) => getNote(activeVaultPath(), args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })),
read_note: (args) => getNote(activeVaultPath(), args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })),
search_notes: (args) => searchNotes(activeVaultPath(), args.query, args.limit),
vault_context: () => vaultContext(activeVaultPath()),
ui_open_note: (args) => { broadcastUiAction('vault_changed', { path: args.path }); broadcastUiAction('open_note', { path: args.path }); return { ok: true } },
ui_open_tab: (args) => { broadcastUiAction('vault_changed', { path: args.path }); broadcastUiAction('open_tab', { path: args.path }); return { ok: true } },
ui_highlight: (args) => { broadcastUiAction('highlight', { element: args.element, path: args.path }); return { ok: true } },
ui_set_filter: (args) => { broadcastUiAction('set_filter', { filterType: args.type }); return { ok: true } },
highlight_editor: (args) => { broadcastUiAction('highlight', { element: args.element, path: args.path }); return { ok: true } },
refresh_vault: (args) => { broadcastUiAction('vault_changed', { path: args?.path }); return { ok: true } },
}
async function handleMessage(data) {
const msg = JSON.parse(data)
const { id, tool, args } = msg
const handler = TOOL_HANDLERS[tool]
if (!handler) {
return { id, error: `Unknown tool: ${tool}` }
}
try {
const result = await callToolHandler(tool, args || {})
if (result === UNKNOWN_TOOL) {
return { id, error: `Unknown tool: ${tool}` }
}
const result = await handler(args || {})
return { id, result }
} catch (err) {
return { id, error: err.message }

View File

@@ -7,10 +7,9 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"agent-docs": "node scripts/build-agent-docs.mjs",
"bundle-mcp": "node scripts/bundle-mcp-server.mjs",
"docs:dev": "vitepress dev site --host 127.0.0.1",
"docs:build": "pnpm agent-docs && vitepress build site",
"docs:build": "vitepress build site",
"docs:preview": "vitepress preview site --host 127.0.0.1",
"lint": "eslint .",
"l10n:translate": "lara-cli translate",
@@ -21,7 +20,7 @@
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/autosave-low-end-typing.spec.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/frontmatter-date-picker.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/missing-active-vault-recovery.spec.ts tests/smoke/missing-string-metadata-open-note.spec.ts tests/smoke/multibyte-search-snippet.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/pull-refresh-open-note.spec.ts tests/smoke/type-derived-properties.spec.ts tests/smoke/vault-loading-skeleton.spec.ts tests/smoke/wikilink-path-fix.spec.ts",
"playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/wikilink-path-fix.spec.ts",
"playwright:regression": "playwright test tests/smoke/",
"playwright:integration": "playwright test --config playwright.integration.config.ts",
"test:coverage": "node scripts/run-vitest-coverage.mjs",
@@ -59,11 +58,9 @@
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.0",
"@tldraw/assets": "4.5.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dompurify": "3.4.2",
"katex": "^0.16.28",
"lucide-react": "^0.564.0",
"mermaid": "^11.14.0",
@@ -76,10 +73,8 @@
"react-virtuoso": "^4.18.1",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"safe-regex2": "5.1.1",
"tailwind-merge": "^3.4.1",
"tailwindcss": "^4.1.18",
"tldraw": "^4.5.10",
"tw-animate-css": "^1.4.0",
"unicode-emoji-json": "^0.8.0"
},
@@ -106,21 +101,9 @@
"jsdom": "^28.0.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.2",
"vite": "^7.3.1",
"vitepress": "^1.6.4",
"vitest": "^4.0.18",
"ws": "^8.19.0"
},
"pnpm": {
"overrides": {
"@hono/node-server": "1.19.13",
"express-rate-limit": "8.2.2",
"hono": "4.12.14",
"path-to-regexp": "8.4.0",
"picomatch": "4.0.4",
"postcss": "8.5.10",
"protobufjs": "7.5.6",
"rollup": "4.59.0"
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,24 +1,11 @@
diff --git a/dist/blocknote-react.js b/dist/blocknote-react.js
index d4d36cd4dbb5370c3b7431ffeb577e4870b39825..5a6c325bf0ff9a23f2023169d7fcf6319df91544 100644
index 6271d12d2cd5152e924c5bfe78f3b8904844dbfe..12bb96bb36d445a77d51a18d0de446a3f9f0ea59 100644
--- a/dist/blocknote-react.js
+++ b/dist/blocknote-react.js
@@ -154,8 +154,26 @@ const co = (e) => {
};
function so(e) {
let t = new DOMRect();
- const n = "getBoundingClientRect" in e ? () => e.getBoundingClientRect() : () => e.element.getBoundingClientRect();
- return () => "element" in e && (e.cacheMountedBoundingClientRect ?? !0) ? (e.element.isConnected && (t = n()), t) : n();
+ const n = () => "getBoundingClientRect" in e ? e.getBoundingClientRect() : e.element instanceof Element ? e.element.getBoundingClientRect() : t;
+ return () => {
+ if (!("element" in e) || !(e.cacheMountedBoundingClientRect ?? !0))
+ return n();
+ if (!(e.element instanceof Element))
+ return n();
+ if (e.element.isConnected)
+ t = n();
+ return t;
+ };
+}
@@ -157,6 +157,16 @@ function so(e) {
const n = "getBoundingClientRect" in e ? () => e.getBoundingClientRect() : () => e.element.getBoundingClientRect();
return () => "element" in e && (e.cacheMountedBoundingClientRect ?? !0) ? (e.element.isConnected && (t = n()), t) : n();
}
+function __bnSafeDomAtPos(e, t) {
+ const n = e.prosemirrorView;
+ if (!n || n.isDestroyed)
@@ -28,10 +15,11 @@ index d4d36cd4dbb5370c3b7431ffeb577e4870b39825..5a6c325bf0ff9a23f2023169d7fcf631
+ } catch {
+ return null;
+ }
}
+}
const z = (e) => {
var h, b, p;
@@ -216,9 +234,7 @@ const z = (e) => {
const { refs: t, floatingStyles: n, context: o } = Ze({
@@ -216,9 +226,7 @@ const Lt = (e) => {
const s = Ue(t, c.doc);
if (!s)
return;
@@ -42,39 +30,7 @@ index d4d36cd4dbb5370c3b7431ffeb577e4870b39825..5a6c325bf0ff9a23f2023169d7fcf631
if (a instanceof Element)
return {
element: a
@@ -2499,7 +2515,14 @@ function Kr(e) {
columns: d
} = e, m = H(
(C) => {
- a(), s(), u == null || u(C);
+ a();
+ try {
+ s();
+ } catch (x) {
+ console.warn("Ignored stale suggestion menu query cleanup:", x);
+ return;
+ }
+ u == null || u(C);
},
[u, a, s]
), { items: g, usedQuery: f, loadingState: h } = Yt(
@@ -2734,7 +2757,14 @@ function ti(e) {
onItemClick: u
} = e, d = H(
(p) => {
- a(), s(), u == null || u(p);
+ a();
+ try {
+ s();
+ } catch (C) {
+ console.warn("Ignored stale suggestion menu query cleanup:", C);
+ return;
+ }
+ u == null || u(p);
},
[u, a, s]
), { items: m, usedQuery: g, loadingState: f } = Yt(
@@ -3306,14 +3336,15 @@ const ii = (e, t = 0.3) => {
@@ -3306,14 +3314,15 @@ const pi = (e) => {
);
if (!m)
return {};
@@ -93,7 +49,7 @@ index d4d36cd4dbb5370c3b7431ffeb577e4870b39825..5a6c325bf0ff9a23f2023169d7fcf631
return p instanceof Element ? (d.cellReference = { element: p }, d.rowReference = {
element: f,
getBoundingClientRect: () => {
@@ -4371,7 +4402,7 @@ const zi = (e) => {
@@ -4371,7 +4380,7 @@ const El = (e) => {
const a = Ue(t, c.prosemirrorState.doc);
if (!a)
return;

1333
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
## New Features
- 📋 **Paste Without Formatting** — Paste copied text as plain content without bringing unwanted styling into a note.
- 🇵🇱 **Polish Language Support** — Use Tolaria with a new Polish interface translation.
- 🧭 **Refined Titlebar Navigation** — Navigate with clearer titlebar controls that feel more native on desktop.
## Improvements
-**Faster Note Loading** — Switch between notes more smoothly by reusing cached note content and parsed editor blocks.
- 🗂️ **Cleaner Sidebar and Menus** — Scan folders, sidebar sections, slash commands, icon choices, and release tabs with cleaner spacing and styling.
- 🖥️ **Better Cross-Platform Window Controls** — Use macOS and Linux window controls that better match each platform.
- ☀️ **Improved Light Mode Editing** — Read code blocks more comfortably when Tolaria is using the light theme.
## Stability and Fixes
- This release also improves editor reliability around block dragging, table handles, pasted Markdown, stale side-menu actions, and unrelated vault refreshes.
- Vault, type, and frontmatter handling were hardened to prevent freezes, filename collisions, parse failures, and stale saved-view deletes.
- Startup, Linux AppImage, release CI, and AI/Codex integration fixes are included in the full commit list.

View File

@@ -1,20 +0,0 @@
## New Features
- 🤖 **Direct AI Model Providers** — Use OpenAI, Anthropic, Gemini, OpenRouter, Ollama, LM Studio, or a custom OpenAI-compatible endpoint directly from Tolaria.
- 🖊️ **Markdown Whiteboards** — Create and edit tldraw-powered whiteboards that live as durable Markdown files in your vault.
- 🧭 **Table of Contents Panel** — Navigate long notes with a lazy, outline-style panel generated from the current document headings.
- 📄 **Readable Release Notes** — View stable releases through curated notes written for users instead of raw commit lists.
## Improvements
- 🧩 **Cleaner AI Settings** — Configure provider secrets, local models, and AI targets through a more focused settings experience.
- 💬 **Better Agent Context Handling** — Large AI agent contexts are compacted more safely, and Codex MCP tools are exposed more reliably.
- 🧱 **Improved Editor Blocks** — Code blocks can be copied more easily, block controls feel steadier, and rich exports avoid unsupported browser APIs.
- 🗂️ **Smarter Note and Type Handling** — Renames, type changes, frontmatter placeholders, and path identity now behave more consistently across vault flows.
- 🌍 **Better International Editing** — RTL text direction, IME composition, Korean sidebar copy, and unicode git paths all received focused polish.
## Stability and Fixes
- Embedded diagrams, tldraw whiteboard assets, stale checklist toggles, and stale note open or rename flows were hardened to avoid broken editor states.
- Vault recovery, missing active vault handling, fresh-note heading paste, and same-path type collisions now fail more gracefully.
- Linux AppImage startup, MCP resource discovery, release CI, Codacy security findings, and AI provider runtime paths all received stability fixes.

View File

@@ -1,18 +0,0 @@
## New Features
- 🌓 **System Theme Preference** — Let Tolaria follow the operating system light/dark appearance automatically.
-**Faster Date Editing** — Edit distant frontmatter dates more quickly with improved date controls.
- ↔️ **Set default note width** — Change default between normal and wide in the settings
- 🗣️ **Set default pluralization** Enable/disable plurlization of type names in the sidebar, from the settings
## Improvements
- 🖼️ **Richer Media Handling** — Clipboard images, native image attachments, media previews, and tldraw assets are handled more reliably inside vaults.
- ✍️ **Better Editor Behavior** — Selection, pasted content, numbered lists, wikilinks in tables, IME composition, table handles, and code-block copy actions were refined.
- 🗂️ **Safer Type and Property Flows** — Type filename collisions, built-in note type creation, type-derived placeholders, blank frontmatter properties, and date picker behavior were hardened.
- 🧠 **Smoother AI Panel UX** — AI target selection, composer focus, context compaction, direct provider setup, and agent spawning paths are more robust.
- 🪟 **Cross-Platform Polish** — Linux AppImage startup/input, Windows menu actions, remote credential handling, and window geometry persistence were improved.
## Stability and Fixes
- Fixed native image attachments in `2e7c5cb433f95d9d10939bcc38453dcc9b4e3ec2`.
- Kept the public installer page visible and improved readable release-note loading.
- Reduced stale-state bugs around vault refreshes, wikilink targets, history diffs, whiteboard blocks, checklist toggles, note rename/open flows, and suggestion cleanup.
- Strengthened release/build reliability with Node heap settings, split quality lanes, CodeScene threshold updates, and broader test coverage.

View File

@@ -1,193 +0,0 @@
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
const repoRoot = path.resolve(import.meta.dirname, '..')
const siteRoot = path.join(repoRoot, 'site')
const outputRoot = path.join(repoRoot, 'src-tauri', 'resources', 'agent-docs')
const sectionOrder = ['start', 'concepts', 'guides', 'reference', 'troubleshooting', 'download', 'releases']
const ignoredDirs = new Set(['.vitepress', 'public', 'node_modules', '.DS_Store'])
function titleFromSlug(slug) {
return slug
.replace(/[-_]+/g, ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase())
}
function stripFrontmatter(markdown) {
return markdown.replace(/^---\n[\s\S]*?\n---\n/, '')
}
function firstHeading(markdown, fallback) {
const match = markdown.match(/^#\s+(.+)$/m)
return match?.[1]?.trim() || fallback
}
export function normalizeDocPath(relativePath) {
return relativePath.replaceAll(path.win32.sep, '/')
}
export function sectionForFile(relativePath) {
const [firstPart] = relativePath.split('/')
if (firstPart === 'index.md') return 'home'
return firstPart.replace(/\.md$/, '')
}
async function listMarkdownFiles(dir, base = dir) {
const entries = await readdir(dir, { withFileTypes: true })
const files = []
for (const entry of entries) {
if (ignoredDirs.has(entry.name)) continue
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...await listMarkdownFiles(fullPath, base))
} else if (entry.isFile() && entry.name.endsWith('.md')) {
files.push(normalizeDocPath(path.relative(base, fullPath)))
}
}
return files
}
function sortDocs(files) {
return files.sort((a, b) => {
const sectionDiff = sectionOrder.indexOf(sectionForFile(a)) - sectionOrder.indexOf(sectionForFile(b))
if (sectionDiff !== 0) return sectionDiff
return a.localeCompare(b)
})
}
function docUrl(relativePath) {
const withoutExt = relativePath.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '')
return `/${withoutExt}`.replace(/\/$/, '/') || '/'
}
function formatDoc(doc) {
return `# ${doc.title}\n\nSource: ${doc.path}\nURL: ${doc.url}\n\n${doc.content}`
}
function groupDocsBySection(docs) {
const bySection = new Map()
for (const doc of docs) {
const docsInSection = bySection.get(doc.section) ?? []
docsInSection.push(doc)
bySection.set(doc.section, docsInSection)
}
return bySection
}
function buildIndex(docs) {
const bySection = groupDocsBySection(docs)
const lines = [
'# Tolaria Agent Docs',
'',
'These docs are generated from the public Tolaria documentation for local AI agent lookup.',
'',
'Start here, then use `rg` over this folder for specific Tolaria concepts and workflows.',
'',
]
for (const section of ['home', ...sectionOrder]) {
const docsInSection = bySection.get(section)
if (!docsInSection?.length) continue
lines.push(`## ${titleFromSlug(section)}`, '')
for (const doc of docsInSection) {
lines.push(`- [${doc.title}](pages/${doc.path})`)
}
lines.push('')
}
lines.push('## Generated Files', '')
lines.push('- `all.md`: all public docs concatenated for fast full-context reads.')
lines.push('- `search-index.json`: title, heading, section, path, and URL metadata for quick routing.')
lines.push('- `<section>.md`: one compact bundle per docs section.')
lines.push('- `pages/`: one generated Markdown file per public docs page.')
lines.push('')
return lines.join('\n')
}
function buildAgentInstructions() {
return `# AGENTS.md - Tolaria Docs Bundle
This folder contains local, generated Tolaria product docs for AI agents.
Use these docs when a user asks how Tolaria works, when you need product behavior, or before making Tolaria-specific assumptions.
Recommended lookup flow:
1. Read the active vault's AGENTS.md for vault-specific conventions.
2. Read this folder's index.md for the docs map.
3. Use \`rg\` over this folder for advanced concepts, workflows, shortcuts, Git, AutoGit, AI, types, properties, relationships, and troubleshooting.
Vault-specific AGENTS.md wins for local conventions. These bundled docs win for Tolaria product behavior.
`
}
function searchIndexFor(doc) {
const headings = [...doc.content.matchAll(/^#{2,3}\s+(.+)$/gm)].map((match) => match[1].trim())
return {
title: doc.title,
path: `pages/${doc.path}`,
url: doc.url,
section: doc.section,
headings,
}
}
async function main() {
const files = sortDocs(await listMarkdownFiles(siteRoot))
const docs = []
for (const relativePath of files) {
const raw = await readFile(path.join(siteRoot, relativePath), 'utf8')
const content = stripFrontmatter(raw).trim()
const fallbackTitle = titleFromSlug(path.basename(relativePath, '.md'))
docs.push({
content,
path: relativePath,
section: sectionForFile(relativePath),
title: firstHeading(content, fallbackTitle),
url: docUrl(relativePath),
})
}
await rm(outputRoot, { force: true, recursive: true })
await mkdir(outputRoot, { recursive: true })
await writeFile(path.join(outputRoot, 'AGENTS.md'), buildAgentInstructions())
await writeFile(path.join(outputRoot, 'index.md'), buildIndex(docs))
await writeFile(path.join(outputRoot, 'all.md'), docs.map(formatDoc).join('\n\n---\n\n'))
await writeFile(path.join(outputRoot, 'search-index.json'), `${JSON.stringify(docs.map(searchIndexFor), null, 2)}\n`)
for (const doc of docs) {
const outputPath = path.join(outputRoot, 'pages', doc.path)
await mkdir(path.dirname(outputPath), { recursive: true })
await writeFile(outputPath, formatDoc(doc))
}
const bySection = groupDocsBySection(docs)
for (const [section, docsInSection] of bySection) {
await writeFile(
path.join(outputRoot, `${section}.md`),
docsInSection.map(formatDoc).join('\n\n---\n\n'),
)
}
console.log(`Generated ${docs.length} agent docs in ${path.relative(repoRoot, outputRoot)}`)
}
const entrypointUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : ''
if (import.meta.url === entrypointUrl) {
main().catch((error) => {
console.error(error)
process.exit(1)
})
}

View File

@@ -1,11 +0,0 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { normalizeDocPath, sectionForFile } from './build-agent-docs.mjs'
test('normalizes Windows doc paths before section grouping', () => {
const docPath = normalizeDocPath('concepts\\ai.md')
assert.equal(docPath, 'concepts/ai.md')
assert.equal(sectionForFile(docPath), 'concepts')
})

View File

@@ -12,9 +12,6 @@ const forwardedArgs = process.argv.slice(2)
const hasFileParallelismOverride = forwardedArgs.some((arg) =>
arg === '--fileParallelism' || arg === '--no-file-parallelism'
)
const hasMaxWorkersOverride = forwardedArgs.some((arg) =>
arg === '--maxWorkers' || arg.startsWith('--maxWorkers=')
)
const maxAttempts = 2
const packageManagerExec = process.env.npm_execpath
@@ -49,11 +46,10 @@ async function runCoverageAttempt(attempt) {
const commandArgs = [
...baseCommandArgs,
// Keep coverage fast enough for CI while avoiding the unbounded worker
// contention that makes a few DOM-heavy suites time out under full
// file parallelism. Callers can still opt into serial or wider runs.
...(hasFileParallelismOverride ? [] : ['--fileParallelism']),
...(hasMaxWorkersOverride ? [] : ['--maxWorkers=4']),
// Vitest 4.0.18 occasionally crashes during coverage worker teardown
// after all files pass, so serialize file execution unless a caller
// explicitly opts into a different file-parallelism mode.
...(hasFileParallelismOverride ? [] : ['--no-file-parallelism']),
`--coverage.reportsDirectory=${runCoverageDir}`,
...forwardedArgs,
]

View File

@@ -5,14 +5,7 @@
*/
import http from 'http'
import {
closeSync,
createReadStream,
fstatSync,
openSync,
opendirSync,
readFileSync,
} from 'fs'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import matter from 'gray-matter'
@@ -23,71 +16,8 @@ const REPO_DIR = path.resolve(__dirname, '..')
const PORT = 5173
function isAllowedPath(p) {
return isInsideRelativePath(path.relative(REPO_DIR, p))
}
function isInsideRelativePath(relative) {
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
}
function resolveInside(root, target) {
const normalizedTarget = path.normalize(target)
if (path.isAbsolute(normalizedTarget)) return null
const candidate = path.normalize(`${root}${path.sep}${normalizedTarget}`)
return isInsideRelativePath(path.relative(root, candidate)) ? candidate : null
}
function readUtf8File(filePath) {
const fd = openSync(filePath, 'r')
try {
return readFileSync(fd, 'utf-8')
} finally {
closeSync(fd)
}
}
function pathStats(filePath) {
const fd = openSync(filePath, 'r')
try {
return fstatSync(fd)
} finally {
closeSync(fd)
}
}
function pathExists(filePath) {
try {
pathStats(filePath)
return true
} catch {
return false
}
}
function directoryEntries(dir) {
const directory = opendirSync(dir)
try {
const entries = []
let entry = directory.readSync()
while (entry) {
entries.push(entry)
entry = directory.readSync()
}
return entries
} finally {
directory.closeSync()
}
}
function streamFile(filePath) {
const fd = openSync(filePath, 'r')
return createReadStream(null, { fd, autoClose: true })
}
function staticAssetPath(url) {
const pathname = new URL(url, 'http://localhost').pathname
const requested = pathname === '/' ? 'index.html' : decodeURIComponent(pathname).replace(/^\/+/, '')
return resolveInside(DIST_DIR, requested) ?? path.normalize(`${DIST_DIR}${path.sep}index.html`)
const resolved = path.resolve(p)
return resolved.startsWith(REPO_DIR)
}
const MIME = {
@@ -104,9 +34,8 @@ const MIME = {
function findMarkdownFiles(dir) {
const results = []
try {
for (const entry of directoryEntries(dir)) {
const full = resolveInside(dir, entry.name)
if (!full) continue
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) results.push(...findMarkdownFiles(full))
else if (entry.name.endsWith('.md')) results.push(full)
}
@@ -122,9 +51,9 @@ function extractWikiLinks(value) {
function parseMarkdownFile(filePath) {
try {
const raw = readUtf8File(filePath)
const raw = fs.readFileSync(filePath, 'utf-8')
const { data: fm, content } = matter(raw)
const stat = pathStats(filePath)
const stat = fs.statSync(filePath)
const DEDICATED = new Set(['aliases','Is A','Belongs to','Related to','Status','Owner','Cadence','Created at'])
const relationships = {}
@@ -169,7 +98,7 @@ function serveVaultApi(url, res) {
if (params.pathname === '/api/vault/list') {
const dir = params.searchParams.get('path')
if (!dir || !isAllowedPath(dir) || !pathExists(dir)) {
if (!dir || !isAllowedPath(dir) || !fs.existsSync(dir)) {
res.writeHead(400); res.end(JSON.stringify({ error: 'bad path' })); return true
}
const entries = findMarkdownFiles(dir).map(parseMarkdownFile).filter(Boolean)
@@ -180,22 +109,22 @@ function serveVaultApi(url, res) {
if (params.pathname === '/api/vault/content') {
const file = params.searchParams.get('path')
if (!file || !isAllowedPath(file) || !pathExists(file)) {
if (!file || !isAllowedPath(file) || !fs.existsSync(file)) {
res.writeHead(400); res.end(JSON.stringify({ error: 'bad path' })); return true
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ content: readUtf8File(file) }))
res.end(JSON.stringify({ content: fs.readFileSync(file, 'utf-8') }))
return true
}
if (params.pathname === '/api/vault/all-content') {
const dir = params.searchParams.get('path')
if (!dir || !isAllowedPath(dir) || !pathExists(dir)) {
if (!dir || !isAllowedPath(dir) || !fs.existsSync(dir)) {
res.writeHead(400); res.end(JSON.stringify({ error: 'bad path' })); return true
}
const map = {}
for (const f of findMarkdownFiles(dir)) {
try { map[f] = readUtf8File(f) } catch {}
try { map[f] = fs.readFileSync(f, 'utf-8') } catch {}
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(map))
@@ -217,13 +146,13 @@ const server = http.createServer((req, res) => {
}
// Static files
let filePath = staticAssetPath(url)
if (!pathExists(filePath) || pathStats(filePath).isDirectory()) {
filePath = path.normalize(`${DIST_DIR}${path.sep}index.html`) // SPA fallback
let filePath = path.join(DIST_DIR, url === '/' ? 'index.html' : url)
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
filePath = path.join(DIST_DIR, 'index.html') // SPA fallback
}
const ext = path.extname(filePath)
res.writeHead(200, { 'Content-Type': MIME[ext] ?? 'application/octet-stream' })
streamFile(filePath).pipe(res)
fs.createReadStream(filePath).pipe(res)
})
server.listen(PORT, '0.0.0.0', () => {

View File

@@ -1,6 +1,4 @@
import {
closeSync, fstatSync, openSync, opendirSync, readFileSync,
} from 'node:fs'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -9,42 +7,7 @@ const localesDir = path.join(root, 'src/lib/locales')
const sourcePath = path.join(localesDir, 'en.json')
function readCatalog(filePath) {
return JSON.parse(readUtf8File(filePath))
}
function readUtf8File(filePath) {
const fd = openSync(filePath, 'r')
try {
return readFileSync(fd, 'utf8')
} finally {
closeSync(fd)
}
}
function directoryFiles(dirPath) {
const dir = opendirSync(dirPath)
try {
const files = []
let entry = dir.readSync()
while (entry) {
if (entry.isFile()) files.push(entry.name)
entry = dir.readSync()
}
return files
} finally {
dir.closeSync()
}
}
function ensureDirectory(dirPath) {
const fd = openSync(dirPath, 'r')
try {
if (!fstatSync(fd).isDirectory()) {
throw new Error(`${dirPath} is not a directory`)
}
} finally {
closeSync(fd)
}
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
}
function isFlatObject(value) {
@@ -111,8 +74,7 @@ const sourceCatalog = readCatalog(sourcePath)
assertFlatStringCatalog('en', sourceCatalog)
const sourceKeys = Object.keys(sourceCatalog).sort()
ensureDirectory(localesDir)
const localeFiles = directoryFiles(localesDir).filter((file) => file.endsWith('.json'))
const localeFiles = fs.readdirSync(localesDir).filter((file) => file.endsWith('.json'))
const issues = []
for (const file of localeFiles) {

View File

@@ -23,14 +23,17 @@ export default defineConfig({
themeConfig: {
logo: { src: "/landing/tolaria-icon.png", alt: "Tolaria" },
nav: [
{ text: "Features", link: "/#features" },
{ text: "Start", link: "/start/install" },
{ text: "Concepts", link: "/concepts/vaults" },
{ text: "Guides", link: "/guides/capture-a-note" },
{ text: "Downloads", link: "/download/" },
{ text: "Reference", link: "/reference/supported-platforms" },
{ text: "Releases", link: "/releases/" },
],
search: {
provider: "local",
},
socialLinks: [{ icon: "github", link: "https://github.com/refactoringhq/tolaria" }],
sidebar: [
{
text: "Start Here",
@@ -46,11 +49,9 @@ export default defineConfig({
items: [
{ text: "Vaults", link: "/concepts/vaults" },
{ text: "Notes", link: "/concepts/notes" },
{ text: "Editor", link: "/concepts/editor" },
{ text: "Properties", link: "/concepts/properties" },
{ text: "Types", link: "/concepts/types" },
{ text: "Relationships", link: "/concepts/relationships" },
{ text: "Files And Media", link: "/concepts/files-and-media" },
{ text: "Inbox", link: "/concepts/inbox" },
{ text: "Git", link: "/concepts/git" },
{ text: "AI", link: "/concepts/ai" },
@@ -65,12 +66,8 @@ export default defineConfig({
{ text: "Create Types", link: "/guides/create-types" },
{ text: "Build Custom Views", link: "/guides/build-custom-views" },
{ text: "Connect A Git Remote", link: "/guides/connect-a-git-remote" },
{ text: "Manage Git", link: "/guides/commit-and-push" },
{ text: "Use The AI", link: "/guides/use-ai-panel" },
{ text: "Configure AI Models", link: "/guides/configure-ai-models" },
{ text: "Use The Table Of Contents", link: "/guides/use-table-of-contents" },
{ text: "Use Media Previews", link: "/guides/use-media-previews" },
{ text: "Manage Display Preferences", link: "/guides/manage-display-preferences" },
{ text: "Commit And Push", link: "/guides/commit-and-push" },
{ text: "Use The AI Panel", link: "/guides/use-ai-panel" },
{ text: "Use The Command Palette", link: "/guides/use-command-palette" },
],
},
@@ -82,8 +79,6 @@ export default defineConfig({
{ text: "Frontmatter Fields", link: "/reference/frontmatter-fields" },
{ text: "View Filters", link: "/reference/view-filters" },
{ text: "Keyboard Shortcuts", link: "/reference/keyboard-shortcuts" },
{ text: "Release Channels", link: "/reference/release-channels" },
{ text: "Contribute", link: "/reference/contribute" },
{ text: "Docs Maintenance", link: "/reference/docs-maintenance" },
],
},
@@ -93,7 +88,6 @@ export default defineConfig({
{ text: "Vault Not Loading", link: "/troubleshooting/vault-not-loading" },
{ text: "Git Authentication", link: "/troubleshooting/git-auth" },
{ text: "AI Agent Not Found", link: "/troubleshooting/ai-agent-not-found" },
{ text: "Model Provider Connection", link: "/troubleshooting/model-provider-connection" },
{ text: "Sync Conflicts", link: "/troubleshooting/sync-conflicts" },
],
},

View File

@@ -54,9 +54,9 @@ const featureSections: FeatureSection[] = [
{
icon: "pen",
label: "Editor",
title: "Writes richly, saves as Markdown",
title: "Writes like Notion, saves as Markdown",
description:
"Block-based editing with slash commands, wikilinks, raw Markdown, whiteboards, media previews, table navigation, and note width controls. Everything durable stays in vault files.",
"Block-based editing with slash commands, wikilinks, and drag-and-drop images. Everything writes clean Markdown to disk. Keyboard-first by design.",
cards: [
{
title: "Rich block editor",
@@ -103,9 +103,9 @@ const featureSections: FeatureSection[] = [
{
icon: "sparkle",
label: "AI",
title: "Local agents and direct models",
title: "Born for Claude Code",
description:
"Use CLI coding agents such as Claude Code, Codex, OpenCode, Pi, and Gemini when you want tool-backed editing. Use local or API model providers for chat over note context without vault-write tools.",
"Tolaria exposes a full MCP server to make CLI tools work easily with your notes out of the box. And with the Git integration, you are always in control of changes and history.",
cards: [
{
title: "Sidebar with custom sections",
@@ -132,7 +132,7 @@ const docsLinks: DocsLink[] = [
{
icon: "workflow",
title: "Follow workflows",
text: "Capture notes, organize the inbox, use wikilinks, create types, push changes, configure AI, and navigate long notes.",
text: "Capture notes, organize the inbox, use wikilinks, create types, push changes, and use the AI panel.",
link: "/guides/capture-a-note",
},
{
@@ -176,18 +176,18 @@ const testimonials = [
<section class="landing-container hero-section">
<h1>A second brain for the AI era. Free forever.</h1>
<p class="hero-lede">
Organize your notes as Markdown files, with native relationships, Git,
local agents, and direct AI model providers.
Organize your notes as Markdown files. With native relationships, Git,
and Claude Code integration
</p>
<div class="hero-actions">
<a
class="landing-button primary"
:href="route('/download/')"
href="https://github.com/refactoringhq/tolaria/releases/latest/download/Tolaria.app.tar.gz"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 3v11m0 0 4-4m-4 4-4-4M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
</svg>
Download Tolaria
Download for macOS
</a>
<a class="landing-button secondary" href="https://github.com/refactoringhq/tolaria">
<svg viewBox="0 0 24 24" aria-hidden="true">
@@ -208,20 +208,7 @@ const testimonials = [
class="screenshot-field"
:style="{ backgroundImage: `url(${asset('les-saintes.jpg')})` }"
>
<div class="screenshot-frame">
<img
class="screenshot-image light"
:src="asset('tolaria-screenshot.png')"
alt="Tolaria app in light mode"
draggable="false"
/>
<img
class="screenshot-image dark"
:src="asset('tolaria-screenshot-dark.png')"
alt="Tolaria app in dark mode"
draggable="false"
/>
</div>
<img :src="asset('tolaria-full-layout.png')" alt="Tolaria app screenshot" />
</div>
</section>
@@ -414,26 +401,24 @@ const testimonials = [
<style scoped>
.tolaria-landing {
--landing-bg: var(--tolaria-bg);
--landing-surface: var(--tolaria-surface);
--landing-bg: #faf9f5;
--landing-surface: #ffffff;
--landing-dark: #1a1a18;
--landing-text: var(--tolaria-text);
--landing-muted: var(--tolaria-text-secondary);
--landing-tertiary: var(--tolaria-text-muted);
--landing-border: var(--tolaria-border);
--landing-subtle: var(--tolaria-surface-muted);
--landing-text: #1a1a18;
--landing-muted: #6b6b60;
--landing-tertiary: #9b9b90;
--landing-border: #e5e5e0;
--landing-subtle: #eeeeea;
--landing-blue: #155dff;
--landing-blue-hover: #4a5ad6;
--landing-accent: var(--landing-blue);
--landing-blue-soft: var(--tolaria-blue-soft);
--landing-blue-border: color-mix(in srgb, var(--landing-accent) 20%, transparent);
--landing-page-width: 1280px;
--landing-blue-soft: rgba(20, 91, 255, 0.05);
--landing-blue-border: rgba(21, 93, 255, 0.2);
color: var(--landing-text);
background: var(--landing-bg);
}
.landing-container {
width: min(100%, var(--landing-page-width));
width: min(100%, 1440px);
margin: 0 auto;
padding-right: 20px;
padding-left: 20px;
@@ -538,25 +523,11 @@ const testimonials = [
background-size: cover;
}
.screenshot-frame {
position: relative;
.screenshot-field img {
width: min(100%, 1160px);
margin: 0 auto;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 12px 48px -4px rgba(0, 0, 0, 0.3);
user-select: none;
}
.screenshot-image {
display: block;
width: 100%;
height: auto;
pointer-events: none;
}
.screenshot-image.dark {
display: none;
}
.feature-section {
@@ -583,8 +554,8 @@ const testimonials = [
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
color: var(--landing-accent);
background: var(--landing-blue-soft);
color: #1a4fcc;
background: #e8eeff;
font-size: 12px;
font-weight: 650;
line-height: 1.2;
@@ -649,7 +620,7 @@ const testimonials = [
.feature-card-title p {
margin: 0;
color: var(--landing-accent);
color: var(--landing-blue);
font-size: 13px;
font-weight: 650;
line-height: 1.3;
@@ -719,8 +690,8 @@ const testimonials = [
height: 38px;
margin-bottom: 24px;
border-radius: 8px;
color: var(--landing-accent);
background: var(--landing-blue-soft);
color: var(--landing-blue);
background: #e8eeff;
}
.docs-icon svg {
@@ -801,7 +772,7 @@ const testimonials = [
.author-heading p {
margin: 4px 0 0;
color: var(--landing-accent);
color: var(--landing-blue);
font-size: 16px;
font-weight: 650;
}
@@ -947,28 +918,16 @@ const testimonials = [
}
.hero-section h1 {
max-width: 960px;
font-size: 36px;
max-width: none;
font-size: 42px;
}
.hero-lede {
max-width: 820px;
font-size: 24px;
font-size: 30px;
}
.hero-note {
font-size: 14px;
}
.landing-button {
min-height: 42px;
padding: 10px 20px;
font-size: 14px;
}
.landing-button svg {
width: 18px;
height: 18px;
font-size: 16px;
}
.screenshot-field {
@@ -1100,19 +1059,3 @@ const testimonials = [
}
}
</style>
<style>
.dark .tolaria-landing {
--landing-accent: #9bbeff;
--landing-blue-soft: rgba(120, 164, 255, 0.24);
--landing-blue-border: rgba(120, 164, 255, 0.38);
}
.dark .tolaria-landing .screenshot-image.light {
display: none;
}
.dark .tolaria-landing .screenshot-image.dark {
display: block;
}
</style>

View File

@@ -1,212 +1,28 @@
<script setup lang="ts">
import DefaultTheme from "vitepress/theme";
import { onBeforeUnmount, onMounted, ref, watchEffect } from "vue";
import { onBeforeUnmount, onMounted } from "vue";
import { useData } from "vitepress";
const { frontmatter } = useData();
const fallbackGithubStars = "9,946";
const githubStars = ref(fallbackGithubStars);
const githubStarsCacheKey = "tolaria:github-stars";
const githubStarsCacheTtlMs = 60 * 60 * 1000;
const githubRepoApiUrl = "https://api.github.com/repos/refactoringhq/tolaria";
type GithubStarsCache = {
stars: number;
savedAt: number;
};
const formatGithubStars = (stars: number) =>
new Intl.NumberFormat("en-US").format(stars);
const scrollClass = "tolaria-scrolled";
const landingPageClass = "tolaria-landing-page";
const updateScrollClass = () => {
document.documentElement.classList.toggle(scrollClass, window.scrollY > 8);
};
const readCachedGithubStars = (): GithubStarsCache | null => {
try {
const rawCache = window.localStorage.getItem(githubStarsCacheKey);
if (!rawCache) {
return null;
}
const parsedCache = JSON.parse(rawCache) as Partial<GithubStarsCache>;
if (
typeof parsedCache.stars !== "number" ||
typeof parsedCache.savedAt !== "number" ||
!Number.isFinite(parsedCache.stars) ||
!Number.isFinite(parsedCache.savedAt)
) {
return null;
}
return {
stars: parsedCache.stars,
savedAt: parsedCache.savedAt,
};
} catch {
return null;
}
};
const updateGithubStars = async () => {
const cachedStars = readCachedGithubStars();
if (cachedStars) {
githubStars.value = formatGithubStars(cachedStars.stars);
if (Date.now() - cachedStars.savedAt < githubStarsCacheTtlMs) {
return;
}
}
try {
const response = await fetch(githubRepoApiUrl, {
headers: { Accept: "application/vnd.github+json" },
});
if (!response.ok) {
return;
}
const repo = (await response.json()) as { stargazers_count?: unknown };
if (
typeof repo.stargazers_count !== "number" ||
!Number.isFinite(repo.stargazers_count)
) {
return;
}
window.localStorage.setItem(
githubStarsCacheKey,
JSON.stringify({
stars: repo.stargazers_count,
savedAt: Date.now(),
} satisfies GithubStarsCache),
);
githubStars.value = formatGithubStars(repo.stargazers_count);
} catch {
// Keep the cached or bundled fallback count.
}
};
watchEffect(() => {
if (typeof document === "undefined") {
return;
}
document.documentElement.classList.toggle(
landingPageClass,
Boolean(frontmatter.value.landing),
);
});
onMounted(() => {
updateScrollClass();
void updateGithubStars();
window.addEventListener("scroll", updateScrollClass, { passive: true });
});
onBeforeUnmount(() => {
window.removeEventListener("scroll", updateScrollClass);
document.documentElement.classList.remove(scrollClass);
document.documentElement.classList.remove(landingPageClass);
});
</script>
<template>
<div :class="{ 'tolaria-landing-shell': frontmatter.landing }">
<DefaultTheme.Layout>
<template #nav-bar-content-after>
<a
class="github-star-widget"
href="https://github.com/refactoringhq/tolaria"
target="_blank"
rel="noreferrer"
:aria-label="`${githubStars} GitHub stars`"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 .5C5.65.5.5 5.65.5 12c0 5.09 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.54-3.88-1.54-.52-1.33-1.28-1.68-1.28-1.68-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.55-.29-5.23-1.28-5.23-5.68 0-1.25.45-2.28 1.19-3.08-.12-.29-.52-1.46.11-3.04 0 0 .97-.31 3.17 1.18A11 11 0 0 1 12 5.53c.98 0 1.97.13 2.89.39 2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.04.74.8 1.19 1.83 1.19 3.08 0 4.41-2.69 5.38-5.25 5.67.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.56A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z"
/>
</svg>
<span>Star</span>
<strong>{{ githubStars }}</strong>
</a>
</template>
</DefaultTheme.Layout>
<DefaultTheme.Layout />
</div>
</template>
<style scoped>
.github-star-widget {
display: inline-flex;
align-items: center;
gap: 7px;
height: 34px;
margin-left: 8px;
padding: 0 10px;
border: 1px solid var(--vp-c-border);
border-radius: 7px;
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
font-size: 13px;
font-weight: 700;
line-height: 1;
text-decoration: none;
transition:
border-color 160ms ease,
background-color 160ms ease,
color 160ms ease;
}
.github-star-widget:hover {
color: var(--vp-c-brand-1);
border-color: color-mix(in srgb, var(--vp-c-brand-1) 38%, var(--vp-c-border));
}
.github-star-widget svg {
width: 18px;
height: 18px;
fill: currentColor;
}
.github-star-widget strong {
padding-left: 7px;
border-left: 1px solid var(--vp-c-border);
font-weight: 800;
}
@media (min-width: 1280px) {
.github-star-widget {
order: 1;
}
:global(.VPNavBar .appearance) {
order: 2;
}
}
@media (max-width: 767px) {
.github-star-widget {
height: 32px;
margin-left: 4px;
padding: 0 7px;
font-size: 12px;
}
.github-star-widget svg {
width: 17px;
height: 17px;
}
.github-star-widget span {
display: none;
}
.github-star-widget strong {
padding-left: 0;
border-left: 0;
}
}
</style>

View File

@@ -43,16 +43,6 @@
}
.dark {
--tolaria-bg: #1f1e1b;
--tolaria-surface: #23221f;
--tolaria-surface-muted: #191814;
--tolaria-text: #e6e1d8;
--tolaria-text-secondary: #b8b1a6;
--tolaria-text-muted: #7f776d;
--tolaria-border: #34322d;
--tolaria-blue: #78a4ff;
--tolaria-blue-hover: #9bbeff;
--tolaria-blue-soft: rgba(120, 164, 255, 0.16);
--vp-c-bg: #1f1e1b;
--vp-c-bg-alt: #191814;
--vp-c-bg-elv: #23221f;
@@ -70,18 +60,6 @@
--vp-code-color: #d8d1c6;
}
html,
body,
#app {
background: var(--vp-c-bg);
}
html.tolaria-landing-page,
html.tolaria-landing-page body,
html.tolaria-landing-page #app {
background: var(--tolaria-bg);
}
.VPNavBarTitle .logo {
width: auto;
height: 28px;
@@ -97,46 +75,17 @@ html.tolaria-landing-page #app {
.tolaria-landing-shell {
--vp-c-bg: var(--tolaria-bg);
--vp-nav-bg-color: var(--tolaria-bg);
}
.tolaria-landing-shell .VPNav,
.tolaria-landing-shell .VPNavBar,
.tolaria-landing-shell .VPNavBar:not(.has-sidebar):not(.home.top),
.tolaria-landing-shell .VPNavBar:not(.home.top) .content-body,
.tolaria-landing-shell .VPNavBar .content-body {
background: var(--tolaria-bg);
background-color: var(--tolaria-bg);
background: color-mix(in srgb, var(--tolaria-bg) 94%, transparent);
backdrop-filter: blur(14px);
}
.tolaria-landing-shell .VPNavBar .wrapper,
.tolaria-landing-shell .VPNavBar .container {
width: min(100%, 1280px);
max-width: 1280px;
margin-right: auto;
margin-left: auto;
}
.tolaria-landing-shell .VPNavBar .wrapper {
padding-right: 20px;
padding-left: 20px;
}
.tolaria-landing-shell .landing-container {
width: min(100%, 1280px);
}
@media (min-width: 768px) {
.tolaria-landing-shell .VPNavBar .wrapper {
padding-right: 40px;
padding-left: 40px;
}
}
.tolaria-landing-shell .VPNavBar .divider,
.tolaria-landing-shell .VPNavBar .divider-line {
display: none;
background-color: transparent;
}
@@ -145,25 +94,10 @@ html.tolaria-landing-page #app {
transition: opacity 160ms ease;
}
.VPNavBar {
position: relative;
z-index: calc(var(--vp-z-index-nav) + 2);
}
.tolaria-landing-shell .VPLocalNav {
display: none;
}
.VPNavScreen {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
bottom: auto !important;
height: calc(100vh - var(--vp-nav-height) - var(--vp-layout-top-height, 0px)) !important;
z-index: calc(var(--vp-z-index-nav) + 1);
background: var(--vp-c-bg);
}
.tolaria-scrolled .tolaria-landing-shell .VPNav {
box-shadow: 0 10px 24px rgba(26, 26, 24, 0.06);
}
@@ -176,11 +110,6 @@ html.tolaria-landing-page #app {
border-radius: 999px;
}
.tolaria-landing-shell .DocSearch-Button {
border: 1px solid var(--tolaria-border);
background: var(--tolaria-surface);
}
.vp-doc h1,
.vp-doc h2,
.vp-doc h3 {
@@ -221,10 +150,12 @@ html.tolaria-landing-page #app {
margin: 0;
}
@media (min-width: 960px) {
.tolaria-landing-shell .VPContent {
padding-top: var(--vp-nav-height);
}
.tolaria-landing-shell .VPPage {
padding-top: var(--vp-nav-height);
}
.tolaria-landing-shell .VPDoc {
padding-top: var(--vp-nav-height);
}
.tolaria-landing-shell .vp-doc > div {

View File

@@ -1,32 +1,20 @@
# AI
Tolaria has two AI paths: coding agents that can use tools to inspect and edit a vault, and direct model targets that answer in chat mode from note context.
Tolaria is designed for local AI agents that can work with files and tools rather than a hidden cloud-only notes API.
## Coding Agents
## Agent Panel
The AI panel can stream supported local CLI agents through Tolaria's normalized event layer. Current targets include Claude Code, Codex, OpenCode, Pi, and Gemini CLI when they are installed on the machine.
The AI panel streams messages from supported local CLI agents. It shows reasoning, tool activity, and file changes in the app.
Coding agents can run in:
## MCP Server
- **Vault Safe** mode, limited to file, search, and edit tools.
- **Power User** mode, which can allow local shell commands scoped to the active vault for agents that support shell access.
## Direct Models
Direct model targets run in chat mode. They receive the active note, linked context, and conversation history, but they do not receive vault-write tools or shell access.
Supported provider shapes include:
- Local models through Ollama or LM Studio.
- Hosted providers such as OpenAI, Anthropic, Gemini, and OpenRouter.
- Custom OpenAI-compatible endpoints.
## External MCP Setup
Tolaria exposes an MCP server for external tools. The setup flow can write Tolaria's MCP entry into Claude Code, Gemini CLI, Cursor, and a generic MCP config path, and it can also copy the exact JSON snippet for manual setup.
MCP setup is explicit. Closing the dialog leaves third-party config files untouched.
Tolaria exposes an MCP server so tools such as Claude Code and Codex can search, read, and edit vault notes through explicit tools.
## Why Git Matters For AI
AI-generated changes should be inspectable. Git gives you diffs, history, rollback, and a clear boundary between suggestions and committed work.
## Current Direction
Claude Code is the primary integration. Codex and other CLI agents are supported through the shared agent architecture as the app evolves.

View File

@@ -1,23 +0,0 @@
# Editor
Tolaria offers a rich editor for daily writing and a raw Markdown mode for exact file control. Both modes write back to the same Markdown file.
## Rich Editing
The rich editor supports blocks, slash commands, wikilinks, tables, code blocks, images, Mermaid diagrams, LaTeX-style math, and markdown-backed whiteboards.
Use it when you want to write and reorganize quickly without thinking about Markdown syntax.
## Raw Mode
Raw mode shows the Markdown source directly. Use it when you need to edit YAML frontmatter, repair unusual Markdown, or make an exact text change.
Toggle raw mode with `Cmd+\` on macOS or `Ctrl+\` on Windows and Linux.
## Table Of Contents
The table of contents panel builds an outline from headings in the current note. It is useful for long notes, procedures, research files, and generated documents. Toggle it with `Cmd+Shift+T` on macOS or `Ctrl+Shift+T` on Windows and Linux.
## Width
Notes can use normal or wide editor width. Set the default in Settings, or override an individual note from the editor toolbar.

View File

@@ -1,34 +0,0 @@
# Files And Media
Tolaria starts with Markdown notes, but a vault can also contain images, PDFs, media files, whiteboards, and other local files.
## Mermaid Diagrams
Use Mermaid code blocks when a note needs a diagram that should stay plain text and versionable.
````md
```mermaid
flowchart LR
Idea --> Draft --> Review --> Publish
```
````
Tolaria renders Mermaid diagrams in the editor while keeping the source in Markdown.
## Attachments
Images pasted into the editor are saved into the vault as normal files. They remain portable and can be opened by other tools.
## Previews
Tolaria can preview common image files, PDFs, and supported media files in the app. Files without an in-app preview can still be opened in the default system app.
Settings control whether PDFs, images, and unsupported files appear in All Notes. Folder browsing still shows files in their folders.
## Whiteboards
Whiteboards use tldraw in the editor, but their durable representation stays in Markdown. That keeps them inside the vault and versioned by Git with the rest of your notes.
## Git Boundary
If generated or local-only files are ignored by Git, Tolaria can hide them from notes, search, quick open, and folders. Use this when build artifacts or private local files should not behave like vault content.

View File

@@ -1,25 +1,16 @@
# Git
Git is Tolaria's recommended history and sync layer. Tolaria can work with plain Markdown folders, and Git unlocks local history, recovery, remote backup, and multi-device workflows when you want them.
Tolaria acts as a lightweight Git client for your vault. You can review changes, commit, pull, push, and inspect history without leaving the app.
Git is Tolaria's history and sync layer. It keeps the model local-first while still supporting remote backup and multi-device workflows.
## What Tolaria Uses Git For
- Whole-vault commit history.
- Current diff for the vault.
- Local commit history.
- Diff views.
- Per-note history.
- Current diff for an individual note.
- Pull and push.
- Conflict detection and resolution.
- Remote connection for local-only vaults.
## History And Diffs
Each note can show its own history and current diff, so you can understand how that file changed over time or what is unsaved relative to Git.
Tolaria also shows a history of the whole vault. Use it when you want to review broader changes across multiple notes before committing or syncing.
## Local Commits
You can commit changes inside Tolaria without leaving the app. This gives you useful restore points even before a remote is configured.
@@ -27,3 +18,4 @@ You can commit changes inside Tolaria without leaving the app. This gives you us
## Remotes
Connect a compatible Git remote when you want sync or backup. Tolaria relies on your system Git authentication, so GitHub CLI, SSH keys, credential helpers, and existing Git configuration can continue to work.

View File

@@ -6,8 +6,6 @@ The Inbox is for notes that have been captured but not yet organized.
Fast capture should not require perfect structure. The Inbox gives you a place to put incomplete notes, then process them later.
The Inbox workflow is optional. Turn it off in Settings > Workflow if you prefer every note to appear organized by default.
## Organizing Inbox Notes
When reviewing the Inbox:
@@ -21,3 +19,4 @@ When reviewing the Inbox:
## Healthy Inbox Habit
Keep the Inbox small enough that it can be reviewed in one focused pass. Tolaria works best when capture is fast and organization is deliberate.

View File

@@ -19,16 +19,13 @@ Draft the public Tolaria docs and keep them close to code changes.
## Titles
The first H1 is the note title. Tolaria uses that title wherever the note is displayed: note lists, search results, wikilink suggestions, relationship pickers, tabs, and window titles.
The title is separate from the filename. The filename stays visible in the breadcrumb so you can see the file on disk, and you can rename it independently when needed.
Use the breadcrumb action to rename the file to match the title. New untitled notes can also auto-rename from the first H1 the first time they get a real title. Turn this behavior off in Settings > Vault Content > Titles & Filenames if you prefer filenames to stay unchanged until you rename them manually.
The first H1 is the main title. Older notes can still use a `title:` frontmatter fallback, but new notes should rely on the H1.
## Body Links
Use `[[wikilinks]]` to connect notes from the body. Tolaria shows autocomplete suggestions while you type, and links can resolve by filename or title.
Use `[[wikilinks]]` to connect notes from the body. Tolaria can resolve links by filename, title, and aliases.
## Frontmatter
Use frontmatter for structured fields such as type, status, date, URL, and relationships. Keep free-form thinking in the body.

View File

@@ -2,9 +2,7 @@
Properties are frontmatter fields that Tolaria can display, filter, and edit.
## Suggested Properties
Suggested properties are the fields Tolaria knows how to create quickly from the Properties panel. When a suggested property is missing, the panel shows a shortcut to add it with the right editor.
## Common Properties
| Field | Purpose |
| --- | --- |
@@ -12,15 +10,16 @@ Suggested properties are the fields Tolaria knows how to create quickly from the
| `status` | Tracks lifecycle state such as Active, Done, or Blocked. |
| `url` | Stores a canonical external link. |
| `date` | Represents a single date. |
| `start_date`, `end_date` | Represents a date range. |
| `aliases` | Gives a note alternative names for wikilink resolution. |
## System Properties
Fields that start with `_` are system properties. They remain in plain text but are hidden from normal property editing.
Examples include `_icon`, `_color`, `_order`, `_sidebar_label`, `_width`, and `_pinned_properties` on type documents or notes.
Examples include `_icon`, `_color`, `_order`, and `_pinned_properties` on type documents.
## Property Editing
The Properties panel is the safest place to edit structured properties. Toggle it with `Cmd+Shift+I` on macOS or `Ctrl+Shift+I` on Windows and Linux.
The Inspector is the safest place to edit structured properties. Use raw Markdown mode when you need direct control over YAML.
Date fields use Tolaria's picker, relationship fields can use wikilinks, and raw Markdown mode is available when you need direct control over YAML.

View File

@@ -4,29 +4,24 @@ Relationships make a vault feel like a graph instead of a pile of documents.
## Relationship Fields
Any frontmatter field containing wikilinks can become a relationship. Relationship fields can point to one note or to an array of notes.
Any frontmatter field containing wikilinks can become a relationship.
```yaml
belongs_to:
- "[[product-work]]"
related_to:
- "[[documentation]]"
- "[[editor-research]]"
blocked_by:
- "[[release-process]]"
- "[[sync-conflicts]]"
```
Tolaria supports default relationship fields out of the box: `belongs_to`, `has`, and `related_to`. It also detects custom relationship fields dynamically when they contain wikilinks.
Default relationships have automatically computed inverses. If a note says it `belongs_to` a project, the project can show that note under its inverse `has` relationship without you writing the reverse link by hand. `related_to` works as a lateral relationship in both directions.
These outgoing and inverse relationships appear in the Properties panel and in Neighborhood mode, where the note list becomes a graph view around the selected note.
Tolaria does not need a hardcoded list of relationship names. It detects relationship fields dynamically.
## Body Links Versus Relationship Fields
Use body links when the relationship appears naturally in writing. Use frontmatter relationships when the connection is important enough to show in navigation, filters, Neighborhood mode, or the Properties panel.
Use body links when the relationship appears naturally in writing. Use frontmatter relationships when the connection is important enough to show in navigation, filters, or the Inspector.
## Backlinks
Tolaria can show incoming links and inverse relationships, making it easier to navigate from a note to the rest of its context.

View File

@@ -12,23 +12,17 @@ type: Project
Tolaria does not infer type from folder location. Moving a file into another folder does not change its type.
## Prefer Types Over Folders
Types are the preferred way to group notes in Tolaria. Folders are supported for existing vaults and fallback organization, but Tolaria is built around types and relationships because they carry stronger meaning than file paths.
Use types for semantic groups such as Projects, People, Topics, Procedures, Events, and Essays. Use relationships to connect notes across those groups. This gives Tolaria better structure for navigation, filtering, properties, templates, and future automation than folder location alone.
## Type Documents
Type documents are Markdown notes with `type: Type` in frontmatter. They describe how a type should appear and what new notes of that type should start with.
Type documents live in the `type/` folder and describe how a type should appear.
```yaml
---
type: Type
_icon: folder
_color: blue
_sidebar_label: Projects
_order: 10
icon: folder
color: blue
sidebar_label: Projects
sort: modified:desc
---
# Project
@@ -38,12 +32,7 @@ _order: 10
- Sidebar grouping.
- Type icon and color.
- Sidebar order and label.
- Default sort.
- Pinned properties.
- New-note templates.
## New Note Defaults
Type documents can define empty properties and relationships. When you create a new note of that type, Tolaria shows placeholders for those fields so you can fill them in from the Properties panel.
If a type document gives a property a value, that value becomes the default for new notes of that type. For example, a Project type can define `status: Active` so every new project starts active until you change it.

View File

@@ -8,7 +8,7 @@ A vault is the folder Tolaria reads and writes. The filesystem is the source of
- YAML frontmatter provides structure.
- Attachments are normal files inside the vault.
- Type definitions and saved views are also files.
- Git can track history and support remote sync.
- Git tracks history and supports remote sync.
## Why Local Files Matter
@@ -16,12 +16,6 @@ Local files keep your notes inspectable. You can open them in another editor, se
Tolaria should never become the only way to read your data.
## Git Is A Capability
A plain folder of Markdown files can open as a vault. Git-backed vaults unlock history, changes, commits, pull, push, conflict handling, and remote setup.
If a folder is not a Git repository, Tolaria can initialize Git when you explicitly ask it to. It avoids initializing broad personal folders such as Desktop, Documents, or Downloads unless they are clearly dedicated vault folders.
## App State Versus Vault State
Vault-level information should travel with the vault. Machine-specific preferences stay with the app installation.
@@ -32,4 +26,4 @@ Vault-level information should travel with the vault. Machine-specific preferenc
| Saved views | Window size |
| Pinned properties | Recent vault list |
| Relationship conventions | Local cache |
| Vault AI guidance files | AI target selection |

View File

@@ -2,17 +2,15 @@
Download Tolaria from the latest stable release.
## Latest Stable
## macOS
[Open the installer download page](https://refactoringhq.github.io/tolaria/download/)
[Download the latest stable build](https://refactoringhq.github.io/tolaria/download/)
The download page reads the latest stable release metadata and points you to the current macOS, Windows, or Linux artifact.
macOS is the primary supported platform.
## Other Builds
## Linux And Windows
- [Browse all GitHub releases](https://github.com/refactoringhq/tolaria/releases)
- [Read release history](/releases/)
- [Learn release channels](/reference/release-channels)
Linux and Windows builds are best effort for now. Check the [latest GitHub release](https://github.com/refactoringhq/tolaria/releases/latest) for available artifacts.
## Verify The Version

View File

@@ -14,12 +14,7 @@ Custom views are saved filters for recurring questions.
Saved views live as files in the vault. They describe filters, sorting, and visible columns using structured data.
## Filters
Custom views can use nested conditions, similar to Notion or Airtable filter groups. Combine `all` and `any` logic when a view needs to answer a more precise question than a single field filter can express.
Date filters support dynamic natural-language values such as `today`, `yesterday`, or `one week ago`. Use these for views that should keep moving over time, such as recent work, stale follow-ups, or upcoming events.
## Design The Question First
Before creating a view, write the question it answers. A good view is not "all fields with all filters"; it is a focused lens.

View File

@@ -4,10 +4,11 @@ Use capture when you need to get an idea into the vault before you know where it
## Steps
1. Press `Cmd+N` on macOS or `Ctrl+N` on Windows and Linux.
2. Write a clear H1.
3. Add the rough content.
4. Leave structure for later if you are still thinking.
1. Open the command palette with `Cmd+K` or `Ctrl+K`.
2. Run `New Note`.
3. Write a clear H1.
4. Add the rough content.
5. Leave structure for later if you are still thinking.
## Capture Well
@@ -15,4 +16,5 @@ Prefer a useful title over a perfect taxonomy. You can add type, status, and rel
## When To Add Structure Immediately
Add structure while capturing when the note's type or relationships are already obvious. Otherwise, capture the idea first and organize it later.
Add structure while capturing if the note is obviously a Project, Person, Event, or Procedure and the context is already known.

View File

@@ -1,23 +1,19 @@
# Manage Git Manually Or With AutoGit
# Commit And Push
Tolaria can act as a lightweight Git client for a Git-enabled vault. You can manage commits and pushes yourself, or enable AutoGit to create conservative checkpoints after editing pauses or when the app is no longer active.
Tolaria lets you commit and push vault changes from inside the app.
## Manual Git
## Commit
1. Open the Git or changes surface.
2. Review changed files.
3. Write a short commit message.
4. Commit locally.
5. Push when a remote is configured.
If the remote has changed, pull first and resolve any conflicts. If the vault has no remote, manual commits still give you local history, diffs, and rollback.
## Push
## AutoGit
AutoGit is available in Settings for Git-enabled vaults. When enabled, Tolaria automatically commits and pushes saved local changes after an idle pause or after the app becomes inactive.
Use AutoGit when you want the safety of regular checkpoints without interrupting capture or editing. You can still inspect each note's current diff, review note history, and browse the whole-vault history before making larger manual commits.
Push after committing when a remote is configured. If the remote has changed, pull first and resolve any conflicts.
## Use Small Commits
Small commits make it easier to understand what changed, roll back safely, and review AI-generated edits.

View File

@@ -1,25 +0,0 @@
# Configure AI Models
Use model providers when you want chat over note context without giving an agent vault-write tools.
## Local Models
Local model targets are for tools such as Ollama and LM Studio. They usually need a base URL and model ID, and they usually do not need an API key.
## API Models
API model targets are for hosted providers such as OpenAI, Anthropic, Gemini, OpenRouter, or another OpenAI-compatible endpoint.
Tolaria does not store provider API keys in vault settings. Choose one of the supported key paths:
- Save the key locally on this device.
- Read the key from an environment variable.
- Use no key for local providers that do not require one.
## Test The Connection
After adding a provider, use the test action in Settings. A successful test means Tolaria reached the endpoint and the model replied.
## Select The Target
Once configured, choose the model from the AI target selector or set it as the default AI target in Settings.

View File

@@ -18,6 +18,6 @@ Make sure the remote repository exists and your system Git can authenticate to i
- SSH keys.
- GitHub CLI authentication.
- Existing Git credential helpers.
- macOS Keychain credentials for HTTPS remotes on macOS.
If authentication fails, see [Git Authentication](/troubleshooting/git-auth).

View File

@@ -4,19 +4,18 @@ Create a type when several notes share the same role in your system.
## Steps
1. Run `New Type` from the command palette, or click `+` in the Types header in the sidebar.
2. Give the type a clear name.
3. Add optional icon, color, sidebar order, sidebar label, pinned properties, suggested fields, default values, or a new-note template.
You can also right-click a type in the sidebar to change its icon and color.
1. Create a note in the `type/` folder.
2. Set `type: Type` in frontmatter.
3. Give the document a clear H1.
4. Add optional icon, color, sort, and sidebar label.
```yaml
---
type: Type
_icon: briefcase
_color: blue
_sidebar_label: Projects
_order: 10
icon: briefcase
color: blue
sidebar_label: Projects
sort: modified:desc
---
# Project
@@ -26,8 +25,3 @@ _order: 10
A type should represent a recurring category, not a one-off label. If you only need a temporary grouping, use a saved view or property instead.
## Templates
Type documents can include a Markdown template for new notes of that type. Keep templates small and useful: a heading, a few expected fields, and the first checklist are usually enough.
Type documents can also define fields for new notes. Empty properties and relationships become placeholders in new notes of that type. Properties with values become defaults for new notes of that type.

View File

@@ -1,26 +0,0 @@
# Manage Display Preferences
Display preferences live in local app settings unless a setting is intentionally stored in the note or vault.
## Theme
Choose Light, Dark, or System in Settings. System follows the operating system appearance at runtime.
You can also switch theme mode from the command palette.
## Note Width
Set the default rich-editor width in Settings:
- **Normal** for focused writing.
- **Wide** for tables, diagrams, dense notes, and generated documents.
An individual note can override the default width from the editor toolbar. That override is stored as `_width` in the note frontmatter.
## Sidebar Labels
Tolaria can pluralize type names in the sidebar. Turn this off in Settings if your type names should be shown exactly as written, or use `_sidebar_label` on a type document for an explicit label.
## Vault Content
Settings also control whether Gitignored files and non-Markdown file categories are visible in the app. Use these controls to keep generated or local-only files out of regular note workflows.

View File

@@ -2,12 +2,6 @@
Inbox review turns quick captures into usable knowledge.
## Remove A Note From Inbox
When a note is organized enough, mark it as organized. Use `Cmd+E` on macOS or `Ctrl+E` on Windows and Linux, or click the organize action in the breadcrumb bar.
That action is what removes the note from Inbox. If auto-advance is enabled in Settings > Workflow, Tolaria opens the next Inbox item immediately after you mark the current note organized.
## Review Checklist
- Rename unclear notes.
@@ -23,9 +17,10 @@ A note is organized when you can answer:
- What kind of thing is this?
- What is it connected to?
- What is this useful for?
- What will I do with it?
- What should happen next?
- Where would I expect to find it later?
## Avoid Over-Structuring
Do not add fields just because they exist. Add the structure that will help future navigation, review, or automation.

View File

@@ -1,30 +1,10 @@
# Use The AI
# Use The AI Panel
Tolaria gives you two ways to ask for AI help: open the AI panel for an ongoing conversation, or prompt directly from the editor with `Cmd+K` followed by a space.
The AI panel connects Tolaria to local CLI agents.
## Choose How To Prompt
## Before Using It
- **AI panel** is best for longer conversations, agent work, and requests that need visible back-and-forth.
- **Inline prompt** is best when you are already writing. Press `Cmd+K`, type a space, then write the prompt you want the AI to handle from the current note context.
## Choose A Target
Open Settings and choose the default AI target:
- **Coding agent** for tool-backed vault editing through Claude Code, Codex, OpenCode, Pi, or Gemini CLI.
- **Local model** for Ollama or LM Studio chat over note context.
- **API model** for OpenAI, Anthropic, Gemini, OpenRouter, or an OpenAI-compatible endpoint.
If a coding agent is missing, install it and reopen Tolaria or switch to another target.
## Permission Mode
Coding agents support per-vault permission modes:
- **Vault Safe** keeps agents limited to file, search, and edit tools.
- **Power User** can allow shell commands for agents that support them.
Direct model targets always stay in chat mode. They can use note context, but they cannot edit vault files through tools.
Install a supported agent such as Claude Code and make sure it is available from your shell. Tolaria detects common install locations, but explicit shell availability is still the simplest setup.
## Good Requests
@@ -36,3 +16,4 @@ Direct model targets always stay in chat mode. They can use note context, but th
## Review Changes
AI edits are file edits. Review them with Tolaria's diff and Git history before committing.

View File

@@ -16,11 +16,9 @@ Open it with:
- Add Remote.
- Open Getting Started Vault.
- Toggle Raw Mode.
- Toggle Table of Contents.
- Toggle AI Panel.
- Use Light, Dark, or System theme.
- Open in New Window.
## Keyboard-First Workflow
Use the palette when you know what you want to do but do not want to hunt through panels. It is also the best place to discover commands as the app grows.

View File

@@ -1,25 +0,0 @@
# Use Media Previews
Media previews let you inspect vault files without leaving Tolaria.
## Open A File
Select an image, PDF, media file, or unsupported file from a folder or file list. Tolaria opens supported files in the app and offers an external-open action for files that should use the system default app.
## All Notes Visibility
Open Settings to choose whether non-Markdown files appear in All Notes:
- PDFs.
- Images.
- Unsupported files.
Folder browsing still shows files in their folders even when a category is hidden from All Notes.
## Attachments
When you paste or drop an image into a note, Tolaria copies it into the vault and references the copied file from Markdown.
## Troubleshooting
If a preview does not render, open the file in the default app to confirm the file is valid, then check whether the file is inside the active vault and not blocked by operating-system permissions.

View File

@@ -1,23 +0,0 @@
# Use The Table Of Contents
The table of contents panel helps you navigate long notes by heading.
## Open It
Use the editor toolbar, the command palette, or the shortcut:
- `Cmd+Shift+T` on macOS.
- `Ctrl+Shift+T` on Windows and Linux.
## How It Works
Tolaria builds the outline from the current note's headings. The panel updates as the note changes and can jump to sections in the editor.
## Good Uses
- Long procedures.
- Meeting notes with many sections.
- Research notes.
- Generated documents that need review.
If a note has no useful headings, add clear H2 and H3 sections rather than relying on a long uninterrupted document.

View File

@@ -21,4 +21,5 @@ related_to:
## Keep Links Stable
Prefer clear note titles and filenames. Tolaria's wikilink autocomplete helps you pick the right target while you type.
Prefer clear note titles and filenames. Use aliases when people or projects are known by multiple names.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 927 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 KiB

View File

@@ -1,23 +0,0 @@
# Contribute
Tolaria is free and open source, and any kind of help is useful. Pick the path that matches what you want to do.
## Sponsor Or Support
The best way to support Tolaria is to subscribe to [Refactoring](https://refactoring.fm/), Luca's newsletter and community about running good teams and shipping software with AI.
## Feature Requests
Use the [product board](https://tolaria.canny.io/) for feature ideas. Search first, upvote existing ideas, and create a new post when the request is genuinely new.
## Discussions
Use [GitHub Discussions](https://github.com/refactoringhq/tolaria/discussions) for questions, conversations, show and tell, and broader community context.
## Contribute Code
Small, focused pull requests are welcome. Check the product board first so you build the right thing, then open a PR on [GitHub](https://github.com/refactoringhq/tolaria/pulls). The [contributing guide](https://github.com/refactoringhq/tolaria/blob/main/CONTRIBUTING.md) explains the local workflow.
## Report A Bug
Use [GitHub Issues](https://github.com/refactoringhq/tolaria/issues) for bugs. Include what happened, what you expected, and clear reproduction steps. If you are reporting from inside Tolaria, use the Contribute panel to copy sanitized diagnostics and attach them to the issue.

Some files were not shown because too many files have changed in this diff Show More