Compare commits

...

934 Commits

Author SHA1 Message Date
lucaronin
bf3caab862 fix: recover blocked native folder picker 2026-06-07 15:00:51 +02:00
lucaronin
2556907f36 fix: launch Pi CLI from Windows shims 2026-06-07 12:04:55 +02:00
lucaronin
8e3f8fbb77 fix: bound ai agent startup status probe 2026-06-07 05:46:41 +02:00
lucaronin
501df0e265 refactor: share rich editor input transforms 2026-06-07 03:25:09 +02:00
lucaronin
48eac1e40d fix: recover paragraph editor index errors 2026-06-07 02:49:04 +02:00
lucaronin
3d28e21a5a fix: keep palette keyboard selection stable 2026-06-06 14:49:43 +02:00
lucaronin
191066a3bf feat: copy note git URLs 2026-06-06 14:24:51 +02:00
lucaronin
3a88724d8f test: update stable signing bypass guard 2026-06-06 12:06:17 +02:00
lucaronin
65ea3f37fc ci: allow v2026-06-06 stable without windows signing 2026-06-06 11:42:57 +02:00
lucaronin
0e81f20e31 docs: add release notes for v2026-06-06 2026-06-06 11:32:51 +02:00
lucaronin
3c8edd8346 fix: let OpenAI API create notes 2026-06-06 10:42:33 +02:00
lucaronin
80ee94da73 fix: localize highlight toolbar copy 2026-06-06 10:35:21 +02:00
lucaronin
008e8f3471 fix: support rich-editor markdown highlights 2026-06-06 10:15:44 +02:00
lucaronin
6aaaca8e24 fix: sync mcp server lockfile 2026-06-06 09:31:38 +02:00
lucaronin
c5dc4a9300 fix: handle symlinked gitignored vault paths 2026-06-06 08:21:49 +02:00
lucaronin
25c7ba0926 test: cover stack-based null append recovery 2026-06-06 04:24:52 +02:00
lucaronin
2b1f4395b3 fix: recover null editor transform errors 2026-06-06 03:57:21 +02:00
lucaronin
e3ef3f6073 test: stabilize wikilink ime composition test 2026-06-06 03:50:51 +02:00
lucaronin
8d47b8d7d2 fix: correct Chinese file manager labels 2026-06-06 03:45:14 +02:00
lucaronin
8fc366c8c0 fix: scope math block selection highlight 2026-06-06 03:23:04 +02:00
lucaronin
42d7a3ac44 refactor: share mcp tool orchestration 2026-06-06 02:27:19 +02:00
lucaronin
3e349fa017 fix: render markdown highlight syntax 2026-06-06 01:34:06 +02:00
lucaronin
8138925bb0 fix: localize custom menus and feedback 2026-06-06 00:48:10 +02:00
lucaronin
cc4203ab1c fix: expose mermaid raw edit action 2026-06-05 23:32:01 +02:00
lucaronin
401087a8ef fix: recover plain table row index errors 2026-06-05 22:58:28 +02:00
lucaronin
0053a36b6e fix: guard title focus without block id 2026-06-05 22:36:26 +02:00
lucaronin
762ac4ff48 fix: guard native drag drop payloads 2026-06-05 22:11:49 +02:00
lucaronin
4716003ca1 fix: refresh sync history surfaces 2026-06-05 21:43:53 +02:00
lucaronin
af9cc118e7 fix: inherit shell ai provider env 2026-06-05 20:44:28 +02:00
lucaronin
95bdbc90c7 feat: add whiteboard fullscreen workspace 2026-06-05 20:01:55 +02:00
lucaronin
12a229e604 fix: align inline math with text baseline 2026-06-05 19:21:22 +02:00
lucaronin
cc52a52216 fix: recognize raw editor backslash shortcut 2026-06-05 18:49:14 +02:00
lucaronin
057c73376c fix: preserve rich editor copy html 2026-06-05 18:16:16 +02:00
lucaronin
a194763847 fix: restore editor focus after active refresh 2026-06-05 17:20:43 +02:00
lucaronin
6945de0060 fix: tolerate stale Pi agent config entries 2026-06-05 16:45:59 +02:00
lucaronin
4e81ab8aa3 fix: add MCP create note tool 2026-06-05 16:27:18 +02:00
lucaronin
1309cf322e fix: respect configured Git author identity 2026-06-05 15:55:28 +02:00
lucaronin
c9467711ad fix: keep view filters focused across workspaces 2026-06-05 15:22:13 +02:00
lucaronin
82b2ff2ac4 ci: allow unsigned Windows stable build for v2026-06-01 2026-06-01 16:25:07 +02:00
lucaronin
34a22c485d docs: add release notes for v2026-06-01 2026-06-01 16:13:26 +02:00
Tolaria
c12aa4f4fd fix: keep math source selection readable 2026-06-01 15:42:44 +02:00
Luca Rossi
6c979addb3 Delete docs/LARGE-VAULT-LOADING-QA.md 2026-06-01 12:45:46 +02:00
Tolaria
7fa284f0d0 fix: edit display math as block property 2026-06-01 10:05:11 +02:00
Tolaria
74891e0283 fix: export notes directly to PDF 2026-05-31 15:11:29 +02:00
lucaronin
f003b38518 fix: avoid Windows npm cmd shim launches 2026-05-31 12:13:18 +02:00
lucaronin
b84d6579da fix: refresh clean active notes after external edits 2026-05-30 23:04:02 +02:00
lucaronin
4f52f3e803 refactor: reduce view filter test duplication 2026-05-30 21:39:09 +02:00
lucaronin
83cd7bc20d refactor: improve ai workspace code health 2026-05-30 21:10:41 +02:00
lucaronin
0cdf758f7d fix: localize ai workspace copy 2026-05-30 04:19:51 +02:00
lucaronin
cba038766c refactor: share ai workspace cross-window store 2026-05-30 03:49:40 +02:00
lucaronin
944ab9705a refactor: extract app shell side effects 2026-05-30 03:23:29 +02:00
lucaronin
1a08102188 refactor: extract app orchestration hooks 2026-05-30 02:43:28 +02:00
lucaronin
1633f10752 fix: swallow stale tauri listener cleanup 2026-05-29 21:12:23 +02:00
lucaronin
c09f1b9c93 test: stabilize ai workspace title persistence 2026-05-29 20:46:35 +02:00
lucaronin
8cee321ebe feat: add common code block grammars 2026-05-29 20:35:53 +02:00
lucaronin
f79cdac570 fix: require signing for stable windows releases 2026-05-29 19:43:48 +02:00
lucaronin
10aed871da feat: export notes as pdf 2026-05-29 19:37:38 +02:00
lucaronin
30cde097d2 ci: allow unsigned Windows stable build 2026-05-29 19:04:15 +02:00
lucaronin
d4e66443bd docs: add release notes for v2026-05-29 2026-05-29 18:36:36 +02:00
lucaronin
1d5d573138 feat: refine AI workspace side panel 2026-05-29 18:00:32 +02:00
lucaronin
ed8a0b155b fix: recover block type render mismatch 2026-05-29 17:44:24 +02:00
lucaronin
8b937012f9 fix: restore editable math source 2026-05-29 17:16:26 +02:00
lucaronin
6a80d01c34 fix: create unmatched quick open notes safely 2026-05-29 16:34:05 +02:00
lucaronin
ef55891cc2 fix: preserve katex square root radicals 2026-05-29 11:50:09 +02:00
lucaronin
9a2c025d07 fix: recover table row render errors 2026-05-29 11:20:45 +02:00
lucaronin
9be7049bfd docs: streamline agent instructions 2026-05-29 11:02:13 +02:00
Luca Rossi
ff569bfba4 updated README 2026-05-29 10:54:15 +02:00
lucaronin
34fda19b62 docs: add ADR for request-scoped AI stream events (guard — from commit 78892c4) 2026-05-29 08:01:49 +02:00
lucaronin
b69cf8dd5b refactor: consolidate git remote helpers 2026-05-29 05:18:52 +02:00
lucaronin
0234314945 test: guard note-list body search against stale results 2026-05-29 04:06:09 +02:00
lucaronin
6bf70eb635 refactor: consolidate note-list body search 2026-05-29 03:19:59 +02:00
lucaronin
78892c4637 feat: add side AI workspace 2026-05-29 00:58:40 +02:00
lucaronin
6910347bf3 fix: preserve edited vault guidance status 2026-05-28 22:00:29 +02:00
lucaronin
181ff6de8c fix: search note list body content 2026-05-28 21:19:55 +02:00
lucaronin
07c47e68f9 fix: preserve spaced filenames in pending changes 2026-05-28 20:02:21 +02:00
lucaronin
05a6e61f11 fix: extend favorite row hover state 2026-05-28 15:56:38 +02:00
lucaronin
7de2726f27 fix: resolve packaged macos mcp server path 2026-05-28 11:18:11 +02:00
lucaronin
a45e44d0f3 test: align release workflow assertions 2026-05-28 10:30:42 +02:00
lucaronin
14b1668ebe fix: unblock alpha release without windows cert 2026-05-28 10:18:45 +02:00
lucaronin
c5049e4332 docs: add ADR for reusable release artifact workflow (guard - from commit c6e570d) 2026-05-28 08:01:42 +02:00
lucaronin
c6e570d2e3 refactor: deduplicate release artifact workflows 2026-05-28 01:51:08 +02:00
lucaronin
e93676bbb7 test: format git no remote regression 2026-05-28 00:59:55 +02:00
lucaronin
952e632622 test: stabilize add remote smoke 2026-05-28 00:53:25 +02:00
lucaronin
641dc2c885 fix: merge local-only git remote PR #739 2026-05-28 00:42:44 +02:00
lucaronin
78f8469c98 perf: merge AI agent startup discovery PR #751 2026-05-27 23:59:59 +02:00
lucaronin
1ec5f7c745 fix: complete Indonesian locale keys 2026-05-27 23:20:15 +02:00
lucaronin
f616fe449b feat: merge Indonesian translation PR #757 2026-05-27 23:14:50 +02:00
lucaronin
8ad23f72e1 fix: recover table-cell transform errors 2026-05-27 19:50:16 +02:00
lucaronin
41b531e899 fix: avoid inactive AI chat context work 2026-05-27 19:23:06 +02:00
lucaronin
03a667661e fix: guard whiteboard permission rejections 2026-05-27 18:56:36 +02:00
lucaronin
908994c253 fix: repair malformed nested editor blocks 2026-05-27 18:32:18 +02:00
lucaronin
af83d270fd fix: coalesce overlapping editor saves 2026-05-27 17:37:16 +02:00
lucaronin
24f4ee3272 fix: harden tldraw text measurement guard 2026-05-27 16:42:12 +02:00
lucaronin
bbb30d6b91 refactor: consolidate anchored dropdown positioning 2026-05-27 16:08:24 +02:00
lucaronin
ba3f324a52 fix: sign Windows release installers 2026-05-27 15:27:34 +02:00
github-actions[bot]
5240aa3b4e Merge branch 'main' into feat/add-indonesia-locale 2026-05-27 13:04:29 +00:00
lucaronin
f80e571c78 feat: add vault item deep links 2026-05-27 14:51:32 +02:00
github-actions[bot]
df5c2a46e6 Merge branch 'main' into feat/add-indonesia-locale 2026-05-27 10:53:16 +00:00
lucaronin
5090ed31bb fix: stabilize native search navigation 2026-05-27 12:42:04 +02:00
github-actions[bot]
0bad22eb57 Merge branch 'main' into feat/add-indonesia-locale 2026-05-27 09:39:26 +00:00
lucaronin
05fc93677c fix: stabilize wikilink ime flushing 2026-05-27 11:26:24 +02:00
lucaronin
bdb9fabfd0 fix: stabilize global search keyboard navigation 2026-05-27 11:13:45 +02:00
lucaronin
4296977fe3 test: cover folder note creation event bridge 2026-05-27 04:05:26 +02:00
github-actions[bot]
bb5296ba3d Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 23:39:16 +00:00
lucaronin
cfc62b1ce2 test: document folder parent creation 2026-05-27 01:25:34 +02:00
github-actions[bot]
29bfde6a20 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 23:18:02 +00:00
lucaronin
759af948b4 fix: polish ai workspace follow-up 2026-05-27 01:03:31 +02:00
github-actions[bot]
5fb1389d69 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 17:51:07 +00:00
lucaronin
f5475f7773 fix: complete action history undo redo 2026-05-26 19:41:25 +02:00
github-actions[bot]
1d93da3ade Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 17:29:26 +00:00
lucaronin
29e5dd8f7b fix: stabilize AI workspace QA 2026-05-26 19:15:05 +02:00
github-actions[bot]
18c6d18474 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 15:02:15 +00:00
lucaronin
8828516d43 feat: rework AI workspace window 2026-05-26 16:52:38 +02:00
github-actions[bot]
8df1f3043d Merge branch 'main' into pr-726 2026-05-26 13:35:13 +00:00
github-actions[bot]
95b89e331d Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 13:35:09 +00:00
lucaronin
2382305576 feat: support action history undo redo 2026-05-26 14:56:39 +02:00
lucaronin
b9f90b0657 fix: use cached startup loads for note windows 2026-05-26 12:58:08 +02:00
Aris Ripandi
0f5ac35f67 fix: add missing locales keys for Indonesian translation 2026-05-26 17:21:07 +07:00
Aris Ripandi
6424509bb9 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 17:10:28 +07:00
Aris Ripandi
7614db62f2 fix: increase sidebar width to avoid truncated label 2026-05-26 17:06:15 +07:00
github-actions[bot]
42dc2f726f Merge branch 'main' into pr-726 2026-05-26 10:05:58 +00:00
lucaronin
56c6cb56d6 feat: create notes inside selected folders 2026-05-26 11:56:28 +02:00
github-actions[bot]
37536e1974 Merge branch 'main' into pr-726 2026-05-26 09:53:49 +00:00
Aris Ripandi
9234ffcac2 Merge branch 'refactoringhq:main' into feat/add-indonesia-locale 2026-05-26 16:49:37 +07:00
Aris Ripandi
198b7c993f feat: add Indonesia translation 2026-05-26 16:47:45 +07:00
lucaronin
ea54560e11 fix: simplify local agent setup surfaces 2026-05-26 11:42:22 +02:00
lucaronin
fbb51a0b4c fix: resolve codacy high severity findings 2026-05-26 10:52:53 +02:00
github-actions[bot]
c8eba73863 Merge branch 'main' into pr-726 2026-05-26 08:42:14 +00:00
lucaronin
ae7ee4064f refactor: keep merged PRs above code health gate 2026-05-26 10:26:29 +02:00
lucaronin
e3bfeba163 Merge remote-tracking branch 'refs/remotes/pr/745' 2026-05-26 10:24:56 +02:00
lucaronin
8c8a2bccfe Merge remote-tracking branch 'refs/remotes/pr/740' 2026-05-26 10:24:52 +02:00
lucaronin
42065d9843 Merge remote-tracking branch 'refs/remotes/pr/733' 2026-05-26 10:24:49 +02:00
lucaronin
6080500df0 Merge remote-tracking branch 'refs/remotes/pr/738' 2026-05-26 10:24:46 +02:00
Elias Stiffel
fe1ff59879 perf: defer and parallelize AI agent CLI discovery at startup
get_ai_agents_status used to probe all six agent CLIs (claude_code,
codex, opencode, pi, gemini, kiro) sequentially during cold start. For
each agent missing from PATH it would fall through to three login-shell
spawns (`<shell> -lc 'command -v <agent>'`), each evaluating the user's
full zsh init. On a machine with no agents installed this added ~5 s to
cold start.

The result is not on the first-paint render path. It feeds the status
bar AI badge, the AI tab in settings, and the AI agents onboarding
prompt — all of which are gated behind aiFeaturesEnabled and the
onboarding flow, so they happily render nothing while the probe is
pending.

Three additive changes:

* useAiAgentsStatus now takes an `enabled` option. When false (AI
  features off, or running in a detached note window) the probe never
  fires. App.tsx passes `aiFeaturesEnabled && !noteWindowParams`.
* When enabled, the invoke call is deferred via requestIdleCallback
  (with setTimeout(0) fallback because WKWebView lacks the API) so it
  lands after first paint instead of during it.
* Rust get_ai_agents_status is now async and fans the six check_cli()
  calls out under tokio::task::spawn_blocking + tokio::join!. Per-agent
  check_cli() signatures stay sync, so run_agent_stream callers are
  unaffected and the per-agent unit tests need no changes.

Cold-start measurements on this machine (no agents on PATH, dev mode):

  Baseline (main, AI on):       cascade settles at t+15.5 s
  Patched, AI features off:                       t+5.0 s
  Patched, AI features on:                        t+6.0 s,
                                  AI status arrives ~1.6 s after first
                                  paint, fully off the critical path.

Fixes: https://github.com/refactoringhq/tolaria/issues/726

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:35:15 +02:00
github-actions[bot]
fe939763cf Merge branch 'main' into pr-728 2026-05-25 15:18:46 +00:00
github-actions[bot]
e15aa76826 Merge branch 'main' into pr-736 2026-05-25 15:18:44 +00:00
github-actions[bot]
ebe85faf53 Merge branch 'main' into pr-735 2026-05-25 15:18:39 +00:00
github-actions[bot]
d0ce5ae165 Merge branch 'main' into main 2026-05-25 15:18:35 +00:00
lucaronin
780e33bff2 fix: recover rich editor keydown join errors 2026-05-25 17:09:11 +02:00
github-actions[bot]
443d34b8b2 Merge branch 'main' into pr-728 2026-05-25 10:22:31 +00:00
github-actions[bot]
77fa49d5f5 Merge branch 'main' into pr-736 2026-05-25 10:22:29 +00:00
github-actions[bot]
a2094b57b7 Merge branch 'main' into pr-735 2026-05-25 10:22:25 +00:00
github-actions[bot]
59d272cf24 Merge branch 'main' into main 2026-05-25 10:22:21 +00:00
lucaronin
afc2ead14c fix: restore full app note windows 2026-05-25 12:07:25 +02:00
github-actions[bot]
80fb3dc6c9 Merge branch 'main' into pr-728 2026-05-25 09:58:00 +00:00
github-actions[bot]
181906cb17 Merge branch 'main' into pr-736 2026-05-25 09:57:58 +00:00
github-actions[bot]
1b00eaf461 Merge branch 'main' into pr-735 2026-05-25 09:57:54 +00:00
github-actions[bot]
9ad5ab822a Merge branch 'main' into main 2026-05-25 09:57:49 +00:00
lucaronin
c255c91a0b fix: keep sort dropdown within viewport 2026-05-25 11:45:34 +02:00
github-actions[bot]
384aa22fbd Merge branch 'main' into pr-728 2026-05-25 09:24:11 +00:00
github-actions[bot]
093d331719 Merge branch 'main' into pr-736 2026-05-25 09:24:09 +00:00
github-actions[bot]
f9c5e70916 Merge branch 'main' into pr-735 2026-05-25 09:24:05 +00:00
github-actions[bot]
d08a08195c Merge branch 'main' into main 2026-05-25 09:23:59 +00:00
lucaronin
2a3589181e fix: dedupe multi-vault auto sync fetches 2026-05-25 11:09:49 +02:00
github-actions[bot]
fb02644358 Merge branch 'main' into pr-728 2026-05-25 08:56:32 +00:00
github-actions[bot]
7c2e7a8990 Merge branch 'main' into pr-736 2026-05-25 08:56:30 +00:00
github-actions[bot]
e33f1b3106 Merge branch 'main' into pr-735 2026-05-25 08:56:26 +00:00
github-actions[bot]
815827b6e2 Merge branch 'main' into main 2026-05-25 08:56:21 +00:00
lucaronin
deb6998c97 refactor: consolidate type definition lookup 2026-05-25 10:41:24 +02:00
github-actions[bot]
1427035623 Merge branch 'main' into pr-728 2026-05-25 08:03:29 +00:00
github-actions[bot]
b7301568b8 Merge branch 'main' into pr-736 2026-05-25 08:03:26 +00:00
github-actions[bot]
0bf921dad1 Merge branch 'main' into pr-735 2026-05-25 08:03:21 +00:00
github-actions[bot]
563030982b Merge branch 'main' into main 2026-05-25 08:03:15 +00:00
lucaronin
41121ebf70 docs: add release notes for v2026-05-25 2026-05-25 10:02:26 +02:00
mehmet turac
7516dfe48a fix: treat name-only git remotes as local-only 2026-05-24 22:14:05 +03:00
mehmet turac
8764b6f78e fix: respect Kiro default agent fallback 2026-05-24 21:00:37 +03:00
github-actions[bot]
094f59eb7b Merge branch 'main' into pr-728 2026-05-24 12:54:58 +00:00
github-actions[bot]
62e1ab0e26 Merge branch 'main' into pr-736 2026-05-24 12:54:56 +00:00
github-actions[bot]
0135ce3f10 Merge branch 'main' into pr-735 2026-05-24 12:54:52 +00:00
lucaronin
c752a52e28 docs: update native QA guidance 2026-05-24 14:54:26 +02:00
github-actions[bot]
ede82fc7a2 Merge branch 'main' into pr-728 2026-05-24 12:28:24 +00:00
github-actions[bot]
8c2432339a Merge branch 'main' into pr-736 2026-05-24 12:28:21 +00:00
github-actions[bot]
fbfd0f1cf3 Merge branch 'main' into pr-735 2026-05-24 12:28:18 +00:00
lucaronin
2fb14f4c35 fix: disambiguate sidebar type visibility by vault 2026-05-24 14:18:17 +02:00
github-actions[bot]
51e32dc832 Merge branch 'main' into pr-728 2026-05-24 12:11:53 +00:00
github-actions[bot]
a541c1b916 Merge branch 'main' into pr-736 2026-05-24 12:11:51 +00:00
github-actions[bot]
411c1947ae Merge branch 'main' into pr-735 2026-05-24 12:11:47 +00:00
lucaronin
5bebd1c458 fix: restore light app icon 2026-05-24 13:52:16 +02:00
github-actions[bot]
e2e9e81ee9 Merge branch 'main' into pr-728 2026-05-24 11:49:09 +00:00
github-actions[bot]
11c7e35519 Merge branch 'main' into pr-736 2026-05-24 11:49:07 +00:00
github-actions[bot]
b9bff9223e Merge branch 'main' into pr-735 2026-05-24 11:49:04 +00:00
lucaronin
d204e95460 fix: expose desktop menu in window chrome 2026-05-24 13:33:14 +02:00
github-actions[bot]
764c899b63 Merge branch 'main' into pr-728 2026-05-24 11:26:39 +00:00
github-actions[bot]
a3a9738034 Merge branch 'main' into pr-736 2026-05-24 11:26:36 +00:00
github-actions[bot]
ad28aed309 Merge branch 'main' into pr-735 2026-05-24 11:26:33 +00:00
lucaronin
b5d462b3cd fix: sync active git vaults together 2026-05-24 13:11:29 +02:00
github-actions[bot]
35e50b75fe Merge branch 'main' into pr-728 2026-05-24 10:54:12 +00:00
github-actions[bot]
018a0aff4f Merge branch 'main' into pr-736 2026-05-24 10:54:10 +00:00
github-actions[bot]
0ffacb3a83 Merge branch 'main' into pr-734 2026-05-24 10:54:08 +00:00
github-actions[bot]
46f82d201d Merge branch 'main' into pr-735 2026-05-24 10:54:06 +00:00
lucaronin
c77d2af537 fix: ignore canceled external URL opens 2026-05-24 12:44:51 +02:00
github-actions[bot]
e662730fbc Merge branch 'main' into pr-728 2026-05-24 10:39:32 +00:00
github-actions[bot]
dc19b2d002 Merge branch 'main' into pr-736 2026-05-24 10:39:30 +00:00
github-actions[bot]
bb68ba78b6 Merge branch 'main' into pr-734 2026-05-24 10:39:28 +00:00
github-actions[bot]
4b375932f4 Merge branch 'main' into pr-735 2026-05-24 10:39:25 +00:00
lucaronin
7d6ba934c6 feat: switch app icon with theme 2026-05-24 12:28:29 +02:00
github-actions[bot]
aa4de6f155 Merge branch 'main' into pr-728 2026-05-24 10:18:53 +00:00
github-actions[bot]
7b62cd5e13 Merge branch 'main' into pr-736 2026-05-24 10:18:51 +00:00
github-actions[bot]
86e414d192 Merge branch 'main' into pr-734 2026-05-24 10:18:49 +00:00
github-actions[bot]
69bf1ca27b Merge branch 'main' into pr-735 2026-05-24 10:18:47 +00:00
lucaronin
9ca05c1665 fix: stack narrow status bar footer 2026-05-24 11:04:18 +02:00
github-actions[bot]
6fd0638d14 Merge branch 'main' into pr-728 2026-05-24 09:01:13 +00:00
github-actions[bot]
c62fb2227e Merge branch 'main' into pr-736 2026-05-24 09:01:10 +00:00
github-actions[bot]
29774c4f46 Merge branch 'main' into pr-734 2026-05-24 09:01:07 +00:00
github-actions[bot]
437c15a280 Merge branch 'main' into pr-735 2026-05-24 09:01:04 +00:00
lucaronin
194cebdfe0 fix: clamp selection toolbar to viewport 2026-05-24 10:48:14 +02:00
github-actions[bot]
d7e1de65b6 Merge branch 'main' into pr-728 2026-05-24 08:44:23 +00:00
github-actions[bot]
38b336357f Merge branch 'main' into pr-736 2026-05-24 08:44:21 +00:00
github-actions[bot]
7f7272e09b Merge branch 'main' into pr-734 2026-05-24 08:44:19 +00:00
github-actions[bot]
8458a337bd Merge branch 'main' into pr-735 2026-05-24 08:44:16 +00:00
lucaronin
d8e0524613 test: cover immediate new note editing 2026-05-24 10:30:16 +02:00
github-actions[bot]
72e10d3b41 Merge branch 'main' into pr-728 2026-05-24 08:22:27 +00:00
github-actions[bot]
80e685cc33 Merge branch 'main' into pr-736 2026-05-24 08:22:25 +00:00
github-actions[bot]
d87d15982f Merge branch 'main' into pr-734 2026-05-24 08:22:23 +00:00
github-actions[bot]
b8b5515a7d Merge branch 'main' into pr-735 2026-05-24 08:22:21 +00:00
lucaronin
e4afa7cfa5 fix: ratchet codescene thresholds 2026-05-24 10:08:24 +02:00
Elias Stiffel
42c3b93886 refactor: group applyMountedChange callbacks under a single field
Code review on pr-735 flagged `applyMountedChange` for exceeding the
8-parameter limit. The three effect callbacks (`onSetDefaultWorkspace`,
`onSwitchVault`, `onUpdateWorkspaceIdentity`) are now grouped under a
single `callbacks` field. The function's destructure surface drops
from 8 to 6 fields, and the semantic split between "state + change"
and "effects" is now explicit in the type.

No behaviour change; the six existing tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:45:01 +02:00
Elias Stiffel
da3e9b9865 fix: switch active vault when unmounting it via the vault switcher
Part of the fix for #735 — top-level folder creation invisible in
multi-workspace mode. The primary route, where the create-folder flow
ignored the sidebar selection and always wrote to the legacy
`resolvedPath`, is fixed in #728 (selection-aware routing in
`handleCreateFolder` and `FolderTree`). This commit fixes the
complementary state-machine bug: the vault switcher could leave
`vaultPath` pointing at an unmounted vault, so any consumer that still
reads `vaultPath` would operate on a vault the user no longer sees.

The vault switcher tracks two related paths:
- `defaultWorkspacePath` — the workspace the user is currently "on"
  (used as the destination for new notes, the graph default, etc.).
- `vaultPath` — the active vault the switcher tracks; read by the file
  watcher, folder/view reloaders, and the active-vault fallback in the
  folder loader.

Unchecking a vault in the switcher previously re-routed only
`defaultWorkspacePath` to another mounted vault, leaving `vaultPath`
pointing at the just-unmounted vault. The two paths drifted apart and
every downstream consumer that read `vaultPath` operated on stale
state — for example the folder loader's active-vault fallback (when
enabled) would silently re-introduce the unmounted vault into the
sidebar, and the file watcher would keep listening to it.

`applyMountedChange` now also calls `onSwitchVault(nextPath)` when the
unmounted vault is the active one, mirroring the existing reroute for
`defaultWorkspacePath`. After the change `vaultPath` and mounted state
stay consistent for every consumer.

`applyMountedChange` is extracted from `VaultMenu.tsx` to a sibling
helper module (`vaultMenuMountedChange.ts`) so it can be unit-tested
directly without tripping `react-refresh/only-export-components`.

Tests:
- New `VaultMenu.applyMountedChange.test.ts` covers the four reroute
  permutations (default+active, active only, default only, neither),
  the bail-out when no alternative mounted vault exists, and the
  remount no-op.

Fixes: https://github.com/refactoringhq/tolaria/issues/735
Related: https://github.com/refactoringhq/tolaria/pull/728

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:08:57 +02:00
lucaronin
c6faf005b5 fix: prefer Windows OpenCode cmd shims 2026-05-24 00:06:53 +02:00
github-actions[bot]
5374e66a80 Merge branch 'main' into pr-728 2026-05-23 21:49:29 +00:00
github-actions[bot]
3efa44df6d Merge branch 'main' into pr-736 2026-05-23 21:49:27 +00:00
github-actions[bot]
f1c2470a6e Merge branch 'main' into pr-734 2026-05-23 21:49:25 +00:00
lucaronin
2e9f07bab4 fix: use custom chrome on Windows 2026-05-23 23:36:22 +02:00
lucaronin
39159b9b1e fix: detect installed Codex on Windows 2026-05-23 23:25:38 +02:00
github-actions[bot]
1be0c4b32f Merge branch 'main' into pr-728 2026-05-23 21:01:34 +00:00
github-actions[bot]
a9f38fb372 Merge branch 'main' into pr-736 2026-05-23 21:01:32 +00:00
github-actions[bot]
2a15ecb3e3 Merge branch 'main' into pr-734 2026-05-23 21:01:30 +00:00
lucaronin
770fdee881 fix: preserve AI session when panel closes 2026-05-23 22:39:02 +02:00
github-actions[bot]
205f3641e3 Merge branch 'main' into pr-728 2026-05-23 20:30:14 +00:00
github-actions[bot]
56e5c98a84 Merge branch 'main' into pr-736 2026-05-23 20:30:11 +00:00
github-actions[bot]
73766ca471 Merge branch 'main' into pr-734 2026-05-23 20:30:09 +00:00
lucaronin
0979c0664c fix: derive AI agent targets from registry 2026-05-23 22:09:39 +02:00
github-actions[bot]
bcbd9d3c5a Merge branch 'main' into pr-728 2026-05-23 20:08:08 +00:00
github-actions[bot]
69000f205a Merge branch 'main' into pr-736 2026-05-23 20:08:06 +00:00
github-actions[bot]
d8e280b9ee Merge branch 'main' into pr-734 2026-05-23 20:08:04 +00:00
lucaronin
ce237039c9 fix: preserve editor focus during active note refresh 2026-05-23 21:53:21 +02:00
github-actions[bot]
6a3d730f0b Merge branch 'main' into pr-728 2026-05-23 19:46:31 +00:00
github-actions[bot]
c1cd894516 Merge branch 'main' into pr-736 2026-05-23 19:46:28 +00:00
github-actions[bot]
2d04335648 Merge branch 'main' into pr-734 2026-05-23 19:46:26 +00:00
lucaronin
1c8207eeb5 refactor: split vault loader workspace tests 2026-05-23 21:26:03 +02:00
github-actions[bot]
fa9855fbf3 Merge branch 'main' into pr-728 2026-05-23 19:06:37 +00:00
github-actions[bot]
d03c780180 Merge branch 'main' into pr-736 2026-05-23 19:06:35 +00:00
github-actions[bot]
c903eb1e79 Merge branch 'main' into pr-734 2026-05-23 19:06:32 +00:00
lucaronin
35d258c0ba fix: keep active vault folder roots visible 2026-05-23 21:01:36 +02:00
github-actions[bot]
720e2a9f60 Merge branch 'main' into pr-728 2026-05-23 18:18:00 +00:00
github-actions[bot]
b8b07a0310 Merge branch 'main' into pr-736 2026-05-23 18:17:57 +00:00
github-actions[bot]
f2bbcef7f0 Merge branch 'main' into pr-734 2026-05-23 18:17:55 +00:00
lucaronin
2df076f834 fix: bound code-block clipboard copies 2026-05-23 19:59:13 +02:00
github-actions[bot]
7e119ee8f0 Merge branch 'main' into pr-728 2026-05-23 17:26:45 +00:00
github-actions[bot]
58f703e0a7 Merge branch 'main' into pr-736 2026-05-23 17:26:43 +00:00
github-actions[bot]
2172719f3e Merge branch 'main' into pr-734 2026-05-23 17:26:40 +00:00
lucaronin
49008eb79c fix: recover empty startup vault reloads 2026-05-23 19:10:10 +02:00
lucaronin
454181456d fix: insert emoji shortcode suggestions 2026-05-23 19:06:07 +02:00
lucaronin
e07dab2310 docs: record app preferences context ADR 2026-05-23 19:06:03 +02:00
github-actions[bot]
6e97f3d4d3 Merge branch 'main' into pr-728 2026-05-23 16:30:09 +00:00
github-actions[bot]
a89416e9a5 Merge branch 'main' into pr-736 2026-05-23 16:30:07 +00:00
github-actions[bot]
5b275629be Merge branch 'main' into pr-734 2026-05-23 16:30:05 +00:00
Elias Stiffel
4a45616b16 Merge remote-tracking branch 'origin/pr-728' into pr-728 2026-05-23 18:17:22 +02:00
lucaronin
ae529f64cd fix: persist property-only frontmatter edits 2026-05-23 18:12:58 +02:00
Elias Stiffel
d337e2204a fix(lint): use optional chain in FolderTree create handler 2026-05-23 18:10:34 +02:00
github-actions[bot]
7110fef6f8 Merge branch 'main' into pr-728 2026-05-23 16:00:39 +00:00
github-actions[bot]
ec286e98d2 Merge branch 'main' into pr-736 2026-05-23 16:00:36 +00:00
github-actions[bot]
66b1a2b301 Merge branch 'main' into pr-734 2026-05-23 16:00:34 +00:00
lucaronin
2a3ecf7270 fix: recover invalid editor block joins 2026-05-23 17:49:00 +02:00
Elias Stiffel
3403c56394 test: align desktop_remote_commands_report_no_remote with has_remote fix
The test's name says "report_no_remote" but its push assertion still
expected the pre-fix behavior ("error"). After 6d97335d made has_remote
URL-aware, git_push on a vault with no URL-bearing remote short-circuits
with "no_remote" — matching the test name and the pull assertion above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:47:58 +02:00
Elias Stiffel
2b368dc8f0 fix(git): list_remotes ignores name-only inherited remote sections
Sibling to the has_remote fix on this branch (6d97335d). Plain `git remote`
lists remote names defined in any config layer, so a [remote "origin"]
section inherited from ~/.gitconfig (e.g. with `prune = true`) made
list_remotes return ["origin"] for fresh local vaults. That broke
git_add_remote (returned "already_configured") and silently neutered
disconnect_all_remotes.

Reuse remote::remote_line_has_url to parse `git remote -v` output and
keep only remotes with a non-empty URL column. Dedupe by name across
the (fetch)/(push) line pair.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:46:54 +02:00
Elias Stiffel
6d97335d68 fix: treat name-only remote sections as no remote
`has_remote()` ran bare `git remote`, which lists every remote name from
any config layer — so a `[remote "origin"]` section inherited from the
user's `~/.gitconfig` (e.g. with only `prune = true`) made every vault
look remote-backed. `git_pull` then ran `git pull --no-rebase`, hit
"no tracking information", and the status bar surfaced "Sync failed".

Parse `git remote -v` and require at least one line with a non-empty URL
column. Mirror the guard on `git_push` so direct callers return
`no_remote` instead of issuing a doomed push. Handle `no_remote`
explicitly in `useAutoSync.performPull` / `pullAndPush` / `handlePushResult`
so the frontend short-circuits the push step and keeps a neutral status.

Fixes: https://github.com/refactoringhq/tolaria/issues/734

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:19:34 +02:00
github-actions[bot]
3042d6180f Merge branch 'main' into pr-728 2026-05-23 15:09:57 +00:00
lucaronin
12646d4e98 fix: recover stale editor block references 2026-05-23 16:53:43 +02:00
github-actions[bot]
972d13e2cc Merge branch 'main' into pr-728 2026-05-23 14:28:31 +00:00
github-actions[bot]
86e88a3d43 Merge branch 'main' into pr-736 2026-05-23 14:28:29 +00:00
lucaronin
f0fb7b7418 fix: recover invalid insertion depth transforms 2026-05-23 16:11:20 +02:00
github-actions[bot]
ba5464cc24 Merge branch 'main' into pr-728 2026-05-23 13:45:10 +00:00
github-actions[bot]
ec20c02784 Merge branch 'main' into pr-736 2026-05-23 13:45:08 +00:00
lucaronin
5bb3fe0550 fix: recover stale table editor transforms 2026-05-23 15:28:35 +02:00
github-actions[bot]
b4610ab9be Merge branch 'main' into pr-728 2026-05-23 12:57:07 +00:00
github-actions[bot]
7c231b08d7 Merge branch 'main' into pr-736 2026-05-23 12:57:04 +00:00
lucaronin
7709ad1002 fix: recover missing note frontmatter writes 2026-05-23 14:41:11 +02:00
lucaronin
948b1501bf fix: route sync to selected git repository 2026-05-23 14:07:56 +02:00
Elias Stiffel
5dd3b98fec fix: avoid duplicate H1 when template begins with one
`buildNoteBody` unconditionally prepended an empty `# ` placeholder
whenever `initialEmptyHeading` was true, which collided with templates
whose first line is already an H1 (e.g. a Weekly Notes type whose
template starts with `# Woche 2026.21`). The placeholder is now
suppressed when the template's first non-whitespace line is `# `.

Fixes: https://github.com/refactoringhq/tolaria/issues/736

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:35:37 +02:00
Elias Stiffel
f0e32cfd5f fix: nest new folders under the selected parent
The Create Folder action ignored the sidebar selection and always
wrote to the vault root. Now it threads the selected folder as
parent_path to the backend, and expands the parent in the tree so
the new child is visible without re-clicking.

Fixes: https://github.com/refactoringhq/tolaria/issues/728

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:03:47 +02:00
lucaronin
806df2c586 feat: create notes from quick open 2026-05-21 19:03:13 +02:00
Luca Rossi
3bed1f9235 Merge pull request #717 from mvanhorn/fix/710-numeric-property-rendered-as-ipv4
fix: do not treat pure-numeric property values as bare-domain URLs (#710)
2026-05-21 16:20:10 +02:00
Luca Rossi
07248fb868 Merge pull request #711 from mvanhorn/fix/705-mcp-node-discovery-linuxbrew-fallback
fix: include linuxbrew node path in MCP runtime fallbacks (#705)
2026-05-21 16:19:46 +02:00
github-actions[bot]
8f948d4c41 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-21 14:19:12 +00:00
github-actions[bot]
cfb8e0aa4f Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-21 14:19:08 +00:00
lucaronin
fa9de47729 Merge remote-tracking branch 'refs/remotes/origin/pr-711' 2026-05-21 16:04:56 +02:00
lucaronin
fe84ed91ac Merge remote-tracking branch 'refs/remotes/origin/pr-716' 2026-05-21 16:00:58 +02:00
lucaronin
b5e0c9d890 Merge remote-tracking branch 'refs/remotes/origin/pr-717' 2026-05-21 15:56:29 +02:00
github-actions[bot]
5d9deb3028 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 13:30:12 +00:00
lucaronin
600ddccff4 docs: align stable release notes filename 2026-05-21 15:29:37 +02:00
github-actions[bot]
e3e7981d8a Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 13:25:11 +00:00
lucaronin
bf1feb204b docs: add stable release notes 2026-05-21 15:24:35 +02:00
github-actions[bot]
2f4d52e53a Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 12:08:49 +00:00
lucaronin
2c9b897430 docs: add Portent templates page 2026-05-21 13:57:23 +02:00
github-actions[bot]
e1486959bb Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 10:13:52 +00:00
lucaronin
74dac301d3 fix: speed up note window startup 2026-05-21 12:01:14 +02:00
github-actions[bot]
2011acf1e2 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 08:33:24 +00:00
github-actions[bot]
50c46f9924 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 08:22:15 +00:00
lucaronin
ecfc76af59 fix: restore note window opening 2026-05-21 10:19:49 +02:00
lucaronin
b2a94b482e chore: tighten eslint pipeline 2026-05-21 10:08:15 +02:00
github-actions[bot]
425393b4a2 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 08:03:06 +00:00
lucaronin
ac196bec58 feat: expand note list context menu
Co-authored-by: Brian Lee <engbrianlee@gmail.com>
2026-05-21 09:49:38 +02:00
github-actions[bot]
4e3dfaad55 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 21:32:53 +00:00
lucaronin
d5b2773436 fix: satisfy release project build 2026-05-20 23:19:52 +02:00
github-actions[bot]
695c8423f5 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 18:02:43 +00:00
lucaronin
c7c53cafa2 feat: add note item context menu 2026-05-20 19:50:42 +02:00
github-actions[bot]
6c29a01a69 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 17:42:41 +00:00
github-actions[bot]
7a72df1c2f Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 17:42:38 +00:00
lucaronin
5f87debecb fix: support go code block highlighting 2026-05-20 19:30:32 +02:00
github-actions[bot]
57c61a1ddd Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 17:27:45 +00:00
github-actions[bot]
08abcd5eff Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 17:27:42 +00:00
github-actions[bot]
bb47cf875d Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 17:27:39 +00:00
lucaronin
0a36915903 fix: detect shell-managed pi installs 2026-05-20 19:14:14 +02:00
github-actions[bot]
df1e2913c0 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 17:09:24 +00:00
github-actions[bot]
ecf4df2d11 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 17:09:21 +00:00
github-actions[bot]
54595ab174 Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 17:09:18 +00:00
lucaronin
73742495b0 fix: unzoom fullscreen mermaid diagrams 2026-05-20 18:57:08 +02:00
github-actions[bot]
b93e25ddb1 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 16:51:53 +00:00
github-actions[bot]
5c5da8a670 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 16:51:50 +00:00
github-actions[bot]
3a7ce6bb5f Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 16:51:46 +00:00
lucaronin
ae6bd8ec73 fix: resolve non-markdown wikilink paths 2026-05-20 18:39:44 +02:00
github-actions[bot]
c7f957775e Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 16:36:03 +00:00
github-actions[bot]
9ea4c367bd Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 16:36:00 +00:00
github-actions[bot]
55c0153692 Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 16:35:57 +00:00
lucaronin
67007e75ff fix: refresh active note after external edits 2026-05-20 18:21:16 +02:00
Matt Van Horn
57c6a4a2a4 fix: do not treat pure-numeric property values as bare-domain URLs
Fixes #710

A number property like "Year: 2026" rendered as "0.0.7.234" in the
note list column because the WHATWG URL parser canonicalizes integer-only
hosts to IPv4 form: new URL('https://2026').hostname === '0.0.7.234'.
formatChipLabel called isUrlValue('2026'), which returned true via the
bare-domain branch of normalizeExternalUrl, and then asked the URL parser
for the hostname.

Add a single guard in normalizeExternalUrl: when the input contains no '.',
reject it before the bare-domain candidate is constructed. The
parseHttpUrl(trimmed) branch above already handles inputs with an explicit
scheme (https://...), so the guard only narrows the bare-domain path -
the path that produced the bug. Existing cases ('example.com', 'localhost')
remain unaffected.

Two new vitest cases cover the regression ('2026' and '0') plus a parity
case for https://example.com to make sure the scheme branch is untouched.
2026-05-19 23:30:59 -07:00
Matt Van Horn
663a7d9a3f fix: prefer .cmd shim over POSIX wrapper for claude/codex on Windows
Fixes #695

`where claude` on Windows returns both the npm-generated POSIX shell
wrapper (extensionless) and the Windows batch shim (`claude.cmd`)
side-by-side. The existing `path_from_successful_output` helpers in
`claude_cli.rs` and `codex_cli.rs` picked the first existing line,
which gave the POSIX wrapper - Windows tried to load it as a PE32,
`CreateProcessW` returned ERROR_BAD_EXE_FORMAT (193), and the AI chat
panel surfaced "%1 is not a valid Win32 application."

Mirror the fix that landed for Gemini in `gemini_discovery.rs`: when
`cfg!(windows)`, filter candidates through the existing shared utility
`cli_agent_runtime::has_windows_cli_extension` so .bat/.cmd/.com/.exe
win over extensionless POSIX scripts. Non-Windows behavior is unchanged.

Added a regression test in each file matching the
`windows_path_lookup_prefers_cmd_shim_over_extensionless_npm_script`
pattern from gemini_discovery.rs.
2026-05-19 23:28:16 -07:00
Matt Van Horn
0abf7b492a fix: include linuxbrew node path in MCP runtime fallbacks
Fixes #705

GUI launches of the AppImage on Linux do not inherit the shell PATH, so
`find_on_path` and `find_in_user_shell` cannot reach Homebrew-on-Linux's
Node install. Add the canonical Linuxbrew paths to `fallback_node_paths`
and to `node_binary_candidates_for_home`; `.is_file()` filtering keeps
machines without Linuxbrew unaffected.
2026-05-19 23:23:58 -07:00
lucaronin
6cf77f9b94 fix: restore autogit multi-vault pushes 2026-05-19 15:46:29 +02:00
lucaronin
2c1fe9f308 fix: reduce retained editor memory 2026-05-19 15:34:46 +02:00
lucaronin
efd3cd4f27 Merge pull request #572 from oksusucha/feat/add-kiro-cli-agent
feat: add Kiro CLI as AI agent

Co-authored-by: oksusucha <sol4877@gmail.com>
2026-05-18 13:55:43 +02:00
lucaronin
3429474fc5 fix: finish pr 412 693 integration 2026-05-18 13:22:09 +02:00
lucaronin
c6ce5f421b fix: save new views in default workspace 2026-05-18 13:17:11 +02:00
lucaronin
7c307e8baf Merge pull request #693 from kzadorozhny/feat/support-bun-runtime
feat: support Bun as MCP server runtime
2026-05-18 13:09:06 +02:00
lucaronin
9d447741f3 Merge pull request #412 from kossoy/main
fix: vault watcher reload loop on iCloud .git symlink vaults
2026-05-18 13:09:00 +02:00
github-actions[bot]
e9079df63c Merge branch 'main' into feat/support-bun-runtime 2026-05-18 10:31:34 +00:00
lucaronin
48fb6710e1 Merge pull request #583 from alroniks/main
feat: add Belarusian basic and latin locales
2026-05-18 12:18:41 +02:00
github-actions[bot]
ae72a43a43 Merge branch 'main' into feat/support-bun-runtime 2026-05-18 09:17:41 +00:00
lucaronin
49935db529 docs: add release notes for v2026-05-18 2026-05-18 11:17:08 +02:00
github-actions[bot]
cd4d3dece2 Merge branch 'main' into feat/support-bun-runtime 2026-05-18 08:43:27 +00:00
lucaronin
0ca0f4a6cf fix: point update banner to release notes 2026-05-18 10:33:50 +02:00
Konstantin Zadorozhny
c2a0988368 docs: note Bun runtime alongside Node for MCP server
Mention `find_mcp_runtime` (Node.js 18+ preferred, Bun 1+ fallback) wherever
the docs previously said Tolaria "verifies Node.js" before writing MCP config,
update the mcp-server directory description, and broaden the AppImage subprocess
sanitisation note to cover both runtimes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:46:59 -07:00
Konstantin Zadorozhny
ab123a1428 feat: support Bun as MCP server runtime
Tolaria's MCP server is pure ESM with only standard `node:fs|path|http|child_process`
imports and pure-JS dependencies, so Bun can execute it identically to Node.
Until now `find_node()` was the single entry point for spawning the WebSocket
bridge and writing external AI tool config — users with Bun but no Node would
hit "node not found in PATH or common install locations" and lose access to MCP
tools entirely.

Introduce `find_mcp_runtime()` which returns the first verifying runtime,
preferring Node 18+ when present and falling back to Bun 1+. The generic
PATH and login-shell lookup helpers (`find_on_path`, `find_in_user_shell`,
`lookup_command`, `lookup_paths`) are now parameterised by command name so
both runtimes share the same machinery. Bun candidates cover `~/.bun/bin/bun`,
mise/asdf/proto shims, Homebrew, and `%USERPROFILE%\.bun\bin\bun.exe` on
Windows. The Codex CLI and Windows .cmd-shim resolution paths keep using the
strict `find_node()` since they specifically need Node.

`spawn_ws_bridge_with_paths`, `mcp_config_snippet`, and `register_mcp` now
resolve through `find_mcp_runtime` so the runtime that gets registered into
`~/.claude.json`, `~/.gemini/settings.json`, `~/.cursor/mcp.json`, etc.
matches the one actually present on the machine.

Locale copy updated from "nodeRequirement" to "runtimeRequirement" across
all 15 catalogs to reflect "Node.js 18+ or Bun 1+".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:42:31 -07:00
lucaronin
c1a5042fb9 test: cover image toolbar hover access 2026-05-17 20:06:16 +02:00
lucaronin
9d331ffda8 fix: respect rtl direction in editor blocks 2026-05-17 19:46:18 +02:00
lucaronin
41992506f1 fix: align raw editor line numbers 2026-05-17 19:34:18 +02:00
lucaronin
761f232647 fix: address clawpatch findings 2026-05-17 19:31:12 +02:00
lucaronin
c09b25d3c5 fix: restore release build type safety 2026-05-17 16:34:09 +02:00
lucaronin
4061d5e88e fix: remove built-in note body templates 2026-05-17 16:31:20 +02:00
lucaronin
0983e4e288 test: cover attachment preservation during tab switch 2026-05-17 04:09:47 +02:00
lucaronin
247c3eefb9 fix: repair rich editor invalid list content 2026-05-17 03:06:49 +02:00
lucaronin
28e2072f9b fix: stabilize mermaid diagram reload clicks 2026-05-17 00:24:17 +02:00
lucaronin
7adecaea60 fix: preserve embedded attachment paths 2026-05-16 19:25:48 +02:00
lucaronin
2616f87185 fix: prevent editor file drop navigation 2026-05-16 18:54:19 +02:00
lucaronin
d81c696742 fix: resolve relative dates in view filters 2026-05-16 18:34:41 +02:00
lucaronin
678cc8f7ba fix: preserve aliased wikilinks in markdown tables 2026-05-16 17:58:44 +02:00
lucaronin
35176d6eff feat: expose rpm download option 2026-05-16 13:44:11 +02:00
lucaronin
42130e116d feat: add note-list search clear action 2026-05-16 13:13:16 +02:00
lucaronin
1040d95eec feat: add granular git controls 2026-05-16 12:28:56 +02:00
lucaronin
480876558a fix: preserve new note edits during empty heading swap 2026-05-16 11:34:59 +02:00
lucaronin
ce04a49bae fix: keep default workspace views mounted 2026-05-15 11:33:55 +02:00
lucaronin
3695b6f3fe fix: mark MCP tools approval-safe 2026-05-15 11:19:07 +02:00
lucaronin
7e50da7816 fix: match scalar array properties in views 2026-05-15 08:33:17 +02:00
lucaronin
17a938f8cf docs: add/update ADR for AppImage media preview fallback (guard — from commit 9b6a43c) 2026-05-15 08:03:35 +02:00
lucaronin
3d2efcedeb fix: bound parsed note-list preload warmups 2026-05-15 03:16:38 +02:00
lucaronin
c7b7b97022 fix: suppress app-owned frontmatter reloads 2026-05-15 02:42:51 +02:00
lucaronin
0c8be62831 refactor: consolidate vault ordering 2026-05-15 02:29:39 +02:00
lucaronin
9b28f60be0 chore: ratchet codescene thresholds 2026-05-15 01:36:10 +02:00
lucaronin
9b6a43cec7 fix: guard linux appimage media previews 2026-05-15 01:22:56 +02:00
lucaronin
384c752c33 docs: add release notes for v2026-05-14 2026-05-14 18:12:17 +02:00
lucaronin
e844cfa0bc feat: support vault reordering 2026-05-14 17:30:13 +02:00
lucaronin
7174061b2a fix: warm parsed blocks for large notes 2026-05-14 17:20:49 +02:00
lucaronin
cf03c01b75 fix: use stock appimage packaging in release 2026-05-14 17:04:36 +02:00
lucaronin
8b83b2c691 fix: preserve appimage plugin name 2026-05-14 16:44:46 +02:00
lucaronin
d445c72edb fix: extract appimage tools in linux release 2026-05-14 16:33:56 +02:00
lucaronin
9a8aacad40 fix: install fuse for linux appimage builds 2026-05-14 16:27:21 +02:00
lucaronin
416131aa8f fix: restore TypeScript build 2026-05-14 16:09:54 +02:00
lucaronin
8b1d2bbed8 fix: support relationship equality filters 2026-05-14 16:01:34 +02:00
github-actions[bot]
9a7a31c47b Merge branch 'main' into fix/view_filters 2026-05-14 13:27:51 +00:00
lucaronin
62e62790c1 feat: stabilize external mcp appimage registration
Co-authored-by: Domenico Lupinetti <domenico@translated.net>
2026-05-14 15:22:47 +02:00
lucaronin
3b2f675264 fix: create notes in selected vault 2026-05-14 15:12:56 +02:00
github-actions[bot]
9706ee8ec2 Merge branch 'main' into fix/view_filters 2026-05-14 11:57:37 +00:00
lucaronin
c9a4b273d6 refactor: simplify file attachment serialization 2026-05-14 13:43:14 +02:00
github-actions[bot]
8fe02f8c26 Merge branch 'main' into fix/view_filters 2026-05-14 11:38:23 +00:00
lucaronin
18f91af77f fix: preserve file attachment raw toggles 2026-05-14 13:29:33 +02:00
github-actions[bot]
1c047079f0 Merge branch 'main' into fix/view_filters 2026-05-14 11:16:48 +00:00
lucaronin
a8273ad7a9 feat: make external mcp vault resolution dynamic
Port the useful dynamic-vault MCP direction from Domenico Lupinetti's proposal into the current mounted-workspace model.

Durable external MCP registration is now vault-neutral, Node MCP resolves active mounted vaults from explicit env or Tolaria vaults.json, and vault context includes root AGENTS.md instructions per active vault.

Based-on: https://github.com/refactoringhq/tolaria/pull/603

Co-authored-by: Domenico Lupinetti <domenico@translated.net>
2026-05-14 13:06:43 +02:00
github-actions[bot]
133b2601a2 Merge branch 'main' into fix/view_filters 2026-05-14 11:06:39 +00:00
lucaronin
21d4577079 fix: apply webkit safeguards on linux wayland 2026-05-14 12:55:09 +02:00
github-actions[bot]
c19003cf39 Merge branch 'main' into fix/view_filters 2026-05-14 10:42:50 +00:00
lucaronin
d99da92b43 fix: preserve windows fullscreen navigation 2026-05-14 12:32:42 +02:00
github-actions[bot]
8b9c4592f1 Merge branch 'main' into fix/view_filters 2026-05-14 10:23:26 +00:00
lucaronin
832feb7df1 fix: standardize icons on phosphor 2026-05-14 12:14:16 +02:00
github-actions[bot]
506e8d0eac Merge branch 'main' into fix/view_filters 2026-05-14 10:11:02 +00:00
lucaronin
7cd346bde0 fix: prevent duplicate vault folder pickers 2026-05-14 11:59:20 +02:00
Andrius Miasnikovas
47f5da1798 Fix equals and not equals filters not working in views 2026-05-14 11:08:48 +03:00
lucaronin
ff4939fb29 fix: recover procedure editor schema errors 2026-05-14 09:55:20 +02:00
lucaronin
3fa1d34c32 fix: stabilize table handle controls after reload 2026-05-14 09:22:44 +02:00
lucaronin
2ef4d95bb5 fix: retarget externally moved inbox notes 2026-05-14 08:15:49 +02:00
lucaronin
ce715ed5aa docs: add/update ADR for entry-scoped note windows (guard — from commit 6bb9c7f) 2026-05-14 08:05:28 +02:00
lucaronin
95dab219b8 fix: preserve cjk code block copy text 2026-05-14 07:48:04 +02:00
lucaronin
2fcea0624d test: stabilize type-derived smoke setup 2026-05-14 06:57:25 +02:00
lucaronin
7f8aad0686 fix: clean up invalid mermaid renders 2026-05-14 06:43:50 +02:00
lucaronin
2eae7a0899 fix: preserve focused editor after pulls 2026-05-14 05:53:37 +02:00
lucaronin
6bb9c7fbc9 fix: avoid note-window vault scans 2026-05-14 05:05:33 +02:00
lucaronin
9d9681345d ci: keep linux check off push lane 2026-05-14 04:05:05 +02:00
lucaronin
201aefca05 fix: bundle fcitx gtk module in appimage 2026-05-13 16:44:38 +02:00
lucaronin
41f0e00cfb fix: support symlinked appimage launches 2026-05-13 16:06:57 +02:00
lucaronin
9be428aad3 fix: sanitize appimage mcp node launches 2026-05-13 15:21:36 +02:00
lucaronin
b2d23a8559 fix: dedupe calendar stable releases 2026-05-13 14:47:26 +02:00
lucaronin
d6ad2ff836 test: isolate pi agent config tests 2026-05-13 14:32:04 +02:00
lucaronin
6ac8f822ff test: stabilize autogit smoke setup 2026-05-13 14:18:03 +02:00
lucaronin
b0c8eff6c7 fix: handle autogit author failures 2026-05-13 14:12:23 +02:00
lucaronin
ae7a468310 test: guard reload editing loop 2026-05-13 13:24:41 +02:00
lucaronin
84a993b948 test: guard fresh-note enter selection 2026-05-13 13:03:36 +02:00
lucaronin
2a5ebcf73d fix: prevent whiteboard dialog crash 2026-05-13 12:42:37 +02:00
lucaronin
09978cf8e5 fix: preserve ampersand in native commit menu 2026-05-13 11:52:06 +02:00
lucaronin
0e981228e2 fix: point release history link to public site 2026-05-13 11:41:36 +02:00
lucaronin
b77ebd2a02 docs: add release notes for v2026-05-12 2026-05-13 11:33:02 +02:00
lucaronin
2e414b0867 fix: restore production build 2026-05-13 10:39:34 +02:00
lucaronin
00fec78b16 fix: stabilize editing and mounted vault updates 2026-05-13 10:29:22 +02:00
lucaronin
e774fcd824 fix: load vault index in note windows 2026-05-13 09:34:11 +02:00
lucaronin
043cdec141 fix: allow default vault notes to move workspaces 2026-05-13 09:18:54 +02:00
lucaronin
11b93638d6 docs: add/update ADR for rich/raw transition ownership (guard — from commit 5020771) 2026-05-13 08:03:41 +02:00
lucaronin
713c2a9dbb fix: set git author before app commits 2026-05-13 07:59:34 +02:00
lucaronin
553ed4de07 fix: recover stale rich-editor transforms 2026-05-13 07:33:37 +02:00
lucaronin
83cf82294c test: preserve raw exit edits during tab state sync 2026-05-13 04:06:34 +02:00
lucaronin
502077170d refactor: consolidate editor mode handoff 2026-05-13 02:18:19 +02:00
lucaronin
16a836b59c fix: register native mac window menu 2026-05-12 23:06:03 +02:00
lucaronin
68374109f4 feat: add ai visibility setting 2026-05-12 22:30:24 +02:00
lucaronin
8a3b6b57f8 fix: restore ai panel view menu 2026-05-12 21:07:40 +02:00
lucaronin
376069d124 fix: preserve pi agent config 2026-05-12 20:18:00 +02:00
lucaronin
1d563a8978 fix: scope saved views to source vault 2026-05-12 19:33:53 +02:00
lucaronin
5215754ffa feat: improve editor code blocks 2026-05-12 18:21:26 +02:00
lucaronin
c0b0a02ce6 fix: show commit dialog opening feedback 2026-05-12 16:28:58 +02:00
lucaronin
dfb363e768 fix: keep mounted workspace notes during vault switch 2026-05-12 16:11:31 +02:00
lucaronin
e82fc964d6 fix: avoid editor stalls during git status refresh 2026-05-12 15:01:41 +02:00
lucaronin
3f029b914c fix: hide startup boot diagnostics 2026-05-12 12:15:09 +02:00
lucaronin
96e8857a77 fix: recover startup vault loading 2026-05-12 11:37:14 +02:00
lucaronin
442b5f0256 fix: normalize renamed save paths 2026-05-12 11:13:41 +02:00
lucaronin
90cfb06e74 fix: narrow manual commit flow config 2026-05-12 10:30:42 +02:00
lucaronin
992c042328 fix: dispatch stable release docs deploy 2026-05-12 10:26:48 +02:00
lucaronin
91ca97bcd4 docs: document multi-vault usage 2026-05-12 10:13:16 +02:00
lucaronin
d3624edf9f refactor: consolidate repository git state 2026-05-12 08:59:30 +02:00
lucaronin
aa942362e9 fix: stabilize editor mode round trips 2026-05-12 08:15:03 +02:00
lucaronin
f2ec12aff6 fix: insert emoji shortcode suggestions 2026-05-12 07:51:10 +02:00
lucaronin
c392b84f3d fix: refresh mounted workspace startup marker 2026-05-12 07:13:01 +02:00
lucaronin
79d3a924f0 refactor: consolidate date display preference flow 2026-05-12 06:57:42 +02:00
lucaronin
b33d1060ef refactor: split vault view helpers 2026-05-12 05:24:51 +02:00
lucaronin
6c90865542 refactor: extract vault workspace entry helpers 2026-05-12 04:57:44 +02:00
lucaronin
956619237e test: preserve editor draft when tab closes 2026-05-12 04:30:41 +02:00
lucaronin
66dfc26bc9 refactor: extract git file workflows 2026-05-12 04:04:31 +02:00
lucaronin
7709255fb3 refactor: extract app orchestration hooks 2026-05-12 03:38:54 +02:00
lucaronin
a3fdaee7ad refactor: extract inbox organize advance 2026-05-12 02:27:53 +02:00
lucaronin
e84bd1a8cd fix: keep note opening responsive 2026-05-11 19:58:01 +02:00
lucaronin
8ff4cfde4f fix: type selected UI language preference 2026-05-11 18:41:25 +02:00
lucaronin
b2a6111a65 refactor: extract app vault setup state 2026-05-11 18:21:12 +02:00
lucaronin
487884bcaf fix: support nested mock Tauri args 2026-05-11 18:21:12 +02:00
lucaronin
07edfac400 feat: support mounted vault workspaces 2026-05-11 18:21:12 +02:00
lucaronin
5c05347690 fix: recover editor block ids before remount 2026-05-11 17:47:15 +02:00
lucaronin
f9fe3d833d fix: restore previous list after neighborhood toggle 2026-05-11 15:47:51 +02:00
lucaronin
7a4d027f12 feat: add collapsed sidebar reopen button 2026-05-11 14:36:20 +02:00
lucaronin
5a14848cf8 fix: tolerate malformed MCP frontmatter 2026-05-11 14:07:23 +02:00
lucaronin
0715f51b02 feat: add date display setting 2026-05-11 13:46:18 +02:00
lucaronin
e727afdd81 fix: pipe oversized claude prompts on windows 2026-05-11 12:30:44 +02:00
lucaronin
6c21aa1087 fix: avoid claude cmd shim on windows 2026-05-11 11:23:39 +02:00
lucaronin
21c68a1138 test: include save-before-switch smoke in core lane 2026-05-11 04:10:16 +02:00
lucaronin
8bee3b51cb fix: launch opencode cmd shims on windows 2026-05-10 11:18:56 +02:00
lucaronin
b4b49eaad2 fix: toggle active neighborhood back to notes 2026-05-10 10:56:57 +02:00
lucaronin
f8515fb191 fix: resolve note-relative markdown images 2026-05-10 10:30:31 +02:00
lucaronin
a2a6feea52 fix: support current Claude and Gemini CLI behavior 2026-05-10 09:58:54 +02:00
lucaronin
b9d417ec1d test: protect Cmd+N note creation persistence 2026-05-10 04:07:41 +02:00
lucaronin
6076332b8b fix: preserve list markers on fresh paste 2026-05-09 18:39:41 +02:00
lucaronin
fb08dd0c71 ci: avoid duplicate frontend typecheck in build gate 2026-05-09 04:05:50 +02:00
lucaronin
064a8879e9 fix: resolve windows mcp server install path 2026-05-08 23:13:41 +02:00
lucaronin
eb731bbaad fix: guard inspector property mutations 2026-05-08 17:37:04 +02:00
lucaronin
852c153d24 fix: avoid math placeholders for currency prose 2026-05-08 17:07:36 +02:00
lucaronin
2b345e9f76 fix: guard inline link clicks during reload 2026-05-08 16:46:22 +02:00
lucaronin
e8e30611fb test: cover rich editor media reload crash 2026-05-08 16:13:52 +02:00
lucaronin
6b71faa6e2 fix: publish docs at custom domain root 2026-05-08 15:51:10 +02:00
lucaronin
dd32f14d3a fix: clear stale editor selection on reload 2026-05-08 15:06:50 +02:00
lucaronin
5a75548652 fix: guard ai markdown highlighting on legacy webkit 2026-05-08 14:59:44 +02:00
lucaronin
81ba8f80d5 fix: tolerate stale file block clicks 2026-05-08 14:19:52 +02:00
lucaronin
ee0d4fc470 fix: allow delete menu clicks in drag regions 2026-05-08 13:44:19 +02:00
lucaronin
f7e9b66988 fix: keep fullscreen mermaid dark themed 2026-05-08 13:24:16 +02:00
lucaronin
91e02f0719 fix: sync whiteboard theme with app mode 2026-05-08 13:04:24 +02:00
lucaronin
4fd0a265f4 fix: tolerate missing note titles in list search 2026-05-08 12:33:04 +02:00
lucaronin
5a2642ba81 fix: allow multimedia embed url input 2026-05-08 12:06:28 +02:00
lucaronin
4870d2a05f fix: keep new note title rename current 2026-05-08 12:06:28 +02:00
lucaronin
6b7de99f78 fix: deploy release pages via github pages 2026-05-08 11:54:48 +02:00
lucaronin
901560467f fix: harden alpha updater metadata lookup 2026-05-08 11:34:27 +02:00
lucaronin
707c164b1c test: harden telemetry onboarding wait 2026-05-08 11:05:46 +02:00
lucaronin
5f00bae2cf fix: preserve slash menu mouse selection 2026-05-08 11:01:44 +02:00
lucaronin
cef3e6a805 fix: allow tldraw context menus in tauri 2026-05-08 10:06:25 +02:00
lucaronin
5e978e315e fix: apply rustfmt to workspace boundary 2026-05-08 01:12:39 +02:00
lucaronin
5e7d29f620 fix: satisfy production workspace build 2026-05-08 01:12:39 +02:00
lucaronin
ef3aed64e3 feat: unify mounted workspace graph 2026-05-08 01:12:39 +02:00
lucaronin
7bda58bbf6 fix: clear codacy focus and history highs 2026-05-07 22:59:58 +02:00
lucaronin
b8d1681aae fix: constrain mock vault api fetches 2026-05-07 22:40:33 +02:00
lucaronin
a29ce24280 fix: clear codacy regex and xss highs 2026-05-07 22:22:29 +02:00
lucaronin
27f1092251 feat: add math slash command 2026-05-07 21:30:19 +02:00
lucaronin
f89c5e8f7d fix: harden mock vault api requests 2026-05-07 21:06:06 +02:00
lucaronin
5965d5f23f fix: clear codacy object injection highs 2026-05-07 20:54:32 +02:00
lucaronin
8d35fbcba3 fix: clear codacy high severity batches 2026-05-07 19:52:14 +02:00
lucaronin
f5866a2376 fix: resolve codacy high severity findings 2026-05-07 18:43:59 +02:00
github-actions[bot]
c571b06293 Merge branch 'main' into main 2026-05-07 16:00:03 +00:00
Luca Rossi
b9ce903991 Updated readme 2026-05-07 17:59:31 +02:00
Ivan Klimchuk
14bf0dc32f fix: resolve pnpm patchedDependencies routing for tests 2026-05-07 18:44:28 +03:00
Ivan Klimchuk
238b4df55f feat: add Belarusian basic and latin locales 2026-05-07 18:44:27 +03:00
lucaronin
3e01c87f64 test: cover note history edit stability 2026-05-07 17:39:35 +02:00
Luca Rossi
f816c9a175 Added Codacy badge 2026-05-07 16:40:49 +02:00
lucaronin
1714b13583 fix: avoid mermaid html label clipping 2026-05-07 16:29:11 +02:00
lucaronin
fb55c57536 fix: show reference titles in breadcrumb 2026-05-07 15:48:29 +02:00
lucaronin
99e509f553 fix: keep whiteboard dialogs usable 2026-05-07 15:27:41 +02:00
lucaronin
09b8a28e39 fix: restore generated release history page 2026-05-07 15:00:28 +02:00
Luca Rossi
e0766dc678 Updated release notes 2026-05-07 14:53:47 +02:00
lucaronin
eed307f3d5 docs: add release notes for v2026-05-07 2026-05-07 14:41:37 +02:00
lucaronin
6ab5c2aa46 fix: keep saved view property sorts visible 2026-05-07 14:04:44 +02:00
lucaronin
9113f44789 test: stabilize type-derived quick open smoke 2026-05-07 13:47:18 +02:00
lucaronin
45643c59a8 fix: improve dark wikilink suggestion highlight 2026-05-07 13:38:29 +02:00
lucaronin
6d6b19f894 fix: use canonical download URL 2026-05-07 13:02:51 +02:00
lucaronin
ea4830ac30 fix: hard navigate to download page 2026-05-07 12:57:25 +02:00
lucaronin
cd3cff733b ci: preserve download assets on docs deploy 2026-05-07 12:38:26 +02:00
lucaronin
a441a6bd6a docs: harden release readiness instructions 2026-05-07 11:46:56 +02:00
lucaronin
e24ea96436 docs: adjust landing hero spacing 2026-05-07 11:24:59 +02:00
lucaronin
496a0373ff fix: resolve linux package mcp server path 2026-05-07 10:58:09 +02:00
lucaronin
8f8b9ada47 fix: launch gemini windows shims 2026-05-07 10:42:56 +02:00
lucaronin
efd3ff3132 ci: keep site updates out of app releases 2026-05-07 10:35:00 +02:00
oksusucha
881f860504 refactor: extract shared binary discovery into cli_agent_runtime
Move common shell lookup logic (PATH check, login shell probe,
first_existing_path) into cli_agent_runtime as find_cli_binary()
and check_cli_availability(). Rewrite kiro_discovery to use these
shared utilities, reducing duplication from 100 lines to ~20.

Other discovery modules (pi, gemini, opencode) can adopt the same
pattern in a follow-up.
2026-05-07 17:15:27 +09:00
oksusucha
596d2b93bb refactor: reduce kiro_cli complexity by extracting helper functions
Split run_agent_stream into focused helpers:
- spawn_kiro_process: command setup and spawn
- write_prompt_async: threaded stdin write
- generate_session_id: unique ID generation
- stream_stdout: line-by-line output processing
- collect_stderr: stderr collection

Reduces per-function cyclomatic complexity from ~18 to ~6.
2026-05-07 17:13:16 +09:00
oksusucha
16ce8b105c feat: add Kiro CLI as AI agent (rebuilt on upstream architecture)
- Add kiro_discovery.rs: binary detection (PATH, login shell, known paths)
- Add kiro_cli.rs: streaming via stdin, ANSI stripping, MCP config merge
- Register Kiro in ai_agents.rs enum, status, and dispatcher
- Add 'kiro' to settings normalization
- Add Kiro to frontend agent definitions and helpers
- Update test for agent cycling

Kiro uses plain text streaming (not JSON) so it has its own stream
handler rather than using cli_agent_runtime::run_ai_agent_json_stream.
MCP config merges into existing .kiro/settings/mcp.json to preserve
user-configured servers. Prompt is passed via stdin to avoid E2BIG.
2026-05-07 17:08:44 +09:00
lucaronin
abc6f7d9b1 docs: deploy public site with GitHub Pages 2026-05-07 09:29:47 +02:00
lucaronin
a59582c067 fix: normalize agent docs paths for windows 2026-05-07 09:08:11 +02:00
lucaronin
afc9face71 docs: add/update ADR for shared attachment path normalization (guard — from commit c8cc9b1) 2026-05-07 08:07:20 +02:00
lucaronin
7bbff355f3 fix: normalize note open entries 2026-05-07 06:41:50 +02:00
lucaronin
8707c9e4be fix: recover stale BlockNote render ids 2026-05-07 06:22:39 +02:00
lucaronin
a92651e048 fix(whiteboard): guard missing tldraw text measurements 2026-05-07 04:13:46 +02:00
lucaronin
c8cc9b1bf1 refactor: consolidate vault attachment paths 2026-05-07 03:05:10 +02:00
lucaronin
8b39eb92d6 fix(ai): handle command backspace in composer 2026-05-07 02:05:50 +02:00
lucaronin
5a02b9240a fix(editor): open PDF attachments from notes 2026-05-07 00:55:07 +02:00
lucaronin
d211d89ab8 fix: clear stale editor state on vault switch 2026-05-07 00:25:15 +02:00
lucaronin
5c3a49d2ca fix: handle shorthand type sort fields 2026-05-06 23:19:50 +02:00
lucaronin
1eeda047c2 feat: bundle docs for Tolaria agents 2026-05-06 23:08:42 +02:00
lucaronin
55dcdd4c87 fix: allow local model network access on macos 2026-05-06 18:26:57 +02:00
Luca Rossi
366d240cc4 Updated release notes 2026-05-06 17:07:45 +02:00
lucaronin
e19db5cab7 feat: add table of contents shortcut 2026-05-06 16:45:18 +02:00
lucaronin
74eecfda02 docs: add release notes for v2026-05-06 2026-05-06 16:22:11 +02:00
lucaronin
d093bd44bc fix(git): use macos keychain credentials for remotes 2026-05-06 16:09:33 +02:00
lucaronin
2e7c5cb433 fix: restore native image attachments 2026-05-06 14:47:43 +02:00
lucaronin
41ee40d427 fix(type-actions): handle type filename collisions 2026-05-06 14:30:03 +02:00
lucaronin
5bf59a9d2f fix(note-list): keep status dot steady while typing 2026-05-06 13:42:04 +02:00
lucaronin
ee24982c72 fix(editor): repair malformed numbered list blocks 2026-05-06 13:15:40 +02:00
lucaronin
755a743e93 fix(download): keep public installer page visible 2026-05-06 12:46:12 +02:00
lucaronin
35be37d256 fix(onboarding): keep ai setup dialog scrollable 2026-05-06 12:22:35 +02:00
lucaronin
9c0d0a8566 chore: ratchet codescene threshold 2026-05-06 12:04:45 +02:00
lucaronin
0294bb6fc0 fix(ai): preserve composer focus after replies 2026-05-06 12:04:45 +02:00
lucaronin
dee1c8b1e8 fix: refine frontmatter date picker 2026-05-06 11:52:15 +02:00
lucaronin
e22b4047ed refactor(test): improve app test harness health 2026-05-06 11:17:46 +02:00
lucaronin
691f5eec7c fix: expand editor whitespace selection hit area 2026-05-06 10:49:06 +02:00
lucaronin
7cf2a077ed test: cover editor flush when returning to switched note 2026-05-06 04:08:58 +02:00
lucaronin
b40c1051af feat(properties): speed up distant date edits 2026-05-05 20:44:27 +02:00
lucaronin
7a274d3a83 fix(editor): guard stale whiteboard block writes 2026-05-05 19:54:09 +02:00
lucaronin
634975e505 fix(vault): coalesce background file activity 2026-05-05 19:23:12 +02:00
lucaronin
88f5d04b8c fix(editor): preserve exact copied text 2026-05-05 18:18:56 +02:00
lucaronin
59408bbebd fix: raise CI Node heap for release builds 2026-05-05 17:36:29 +02:00
lucaronin
bc97ade133 fix(editor): support whitespace drag selection 2026-05-05 16:51:56 +02:00
lucaronin
eb6b58eb36 fix: refine breadcrumb overflow actions 2026-05-05 15:46:18 +02:00
lucaronin
520b2c9799 feat(settings): follow system theme 2026-05-05 15:00:24 +02:00
lucaronin
d5ecd567d0 fix(editor): guard stale wikilink click targets 2026-05-05 13:43:48 +02:00
lucaronin
f88b359bb5 fix(editor): render wikilinks inside tables 2026-05-05 13:02:56 +02:00
lucaronin
67ada08e05 fix(editor): ignore stale history diff responses 2026-05-05 12:29:46 +02:00
lucaronin
6ce04eead6 fix(ai): avoid Codex Windows batch shims 2026-05-05 11:45:56 +02:00
lucaronin
b8e3d8193c fix(editor): guard stale table handle state 2026-05-05 11:20:43 +02:00
lucaronin
9c09b5ad78 fix(editor): guard composing enter in list items 2026-05-05 10:15:59 +02:00
lucaronin
a6caa2b069 fix(linux): enable fcitx input for AppImage Wayland 2026-05-05 09:54:39 +02:00
lucaronin
0f231048cf fix(vault): retry transient access denied saves 2026-05-05 09:32:08 +02:00
lucaronin
8a3d00e8c6 docs: add/update ADR for media previews and vault refresh policy (guard — from commit d347cda, 92c0b89) 2026-05-05 09:32:08 +02:00
lucaronin
bfd175224a ci: split frontend and rust quality lanes 2026-05-05 04:06:28 +02:00
lucaronin
0df9c1d707 fix(types): normalize built-in notes type creation 2026-05-05 03:41:41 +02:00
lucaronin
ccfa148014 fix(nav): guard stale entity backlink metadata 2026-05-05 03:19:51 +02:00
lucaronin
dda4943425 test(editor): cover mermaid property refresh 2026-05-05 03:00:47 +02:00
lucaronin
be87c3864c fix(ai): remount composer after native input 2026-05-05 02:43:49 +02:00
lucaronin
982d6476d7 fix(editor): guard stale suggestion query cleanup 2026-05-05 02:19:49 +02:00
lucaronin
92c0b895dc fix(editor): preserve focus during vault refresh 2026-05-05 02:00:28 +02:00
lucaronin
75a8444b41 fix(window): preserve windows properties geometry 2026-05-05 01:43:13 +02:00
lucaronin
d347cda9e3 fix(preview): allow native media playback 2026-05-05 01:08:33 +02:00
lucaronin
f5adbf9cf7 feat(settings): add note display preferences 2026-05-04 22:07:27 +02:00
lucaronin
19059fbf81 fix(frontmatter): preserve crlf inbox organization 2026-05-04 20:35:01 +02:00
lucaronin
1694e3ad07 fix(editor): route clipboard images to attachments 2026-05-04 19:12:14 +02:00
lucaronin
e46613b82b fix: stabilize tldraw whiteboard assets 2026-05-04 18:34:07 +02:00
김덕환
38ea51627d fix(i18n): correct Korean locale mistranslations (#524)
- Inbox: '받은 편지함' → 'Inbox' (이메일 용어 오역. 다른 언어 전부 Inbox 유지)
- Note: '메모' → '노트' (앱 전체 용어 불일치 수정)
- Vault: '보관소/보관함' → 'Vault' (다른 언어 전부 Vault 유지)
- Views: '조회수' → '뷰' (YouTube 조회수 뉘앙스 제거)
- Guidance: '가이드ANCE' → '가이던스' (오타 수정)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-04 18:05:19 +02:00
lucaronin
e5fa40b826 docs: add release notes for v2026-05-04 2026-05-04 16:52:41 +02:00
lucaronin
ff4a78f964 feat: load readable release notes dynamically 2026-05-04 15:07:09 +02:00
lucaronin
9973d8d4e6 fix: bundle tldraw whiteboard assets 2026-05-04 14:57:09 +02:00
lucaronin
e7e33c479d fix: make AI target selection exclusive 2026-05-04 13:30:42 +02:00
lucaronin
69b9da6da6 feat: add readable stable release notes 2026-05-04 13:01:06 +02:00
lucaronin
71ef8325a0 feat: add lazy table of contents panel 2026-05-04 12:48:18 +02:00
lucaronin
582d1d25d6 fix: stabilize embedded diagrams 2026-05-04 11:28:53 +02:00
lucaronin
ebf4545d46 fix: show type-derived instance property placeholders 2026-05-04 10:55:04 +02:00
lucaronin
3c483ba0e6 feat(editor): add table of contents panel 2026-05-04 06:08:09 +02:00
lucaronin
71629763ee chore: ignore local codacy runtime 2026-05-04 05:25:43 +02:00
lucaronin
417e37f1d1 refactor(paths): share note path identity 2026-05-04 04:59:50 +02:00
lucaronin
a3e3192b66 refactor(editor): share durable block codecs 2026-05-04 04:09:14 +02:00
lucaronin
86c503c1f9 refactor(ai): share model provider catalog 2026-05-04 02:59:20 +02:00
lucaronin
21a47b4a77 fix(editor): avoid modern array copy in rich export 2026-05-04 02:07:32 +02:00
lucaronin
64d961bd98 fix(editor): type ime cancel listener 2026-05-04 01:18:28 +02:00
lucaronin
1b218f5250 fix(editor): keep toolbar hidden through ime settle 2026-05-04 01:02:07 +02:00
lucaronin
dfb9f98b7a fix(types): block same-path type collisions 2026-05-03 23:44:24 +02:00
lucaronin
6a033cd2db fix(status): keep git popup above panels 2026-05-03 20:25:50 +02:00
lucaronin
b872b6148c fix(menu): route windows menu actions 2026-05-03 20:00:37 +02:00
lucaronin
c7b6832cb1 fix: harden stale note rename and open flows 2026-05-03 19:51:15 +02:00
lucaronin
05a9aacd1e fix(git): preserve unicode path output 2026-05-03 19:24:27 +02:00
lucaronin
701ebe3f07 fix: hide toolbar during ime composition 2026-05-03 18:36:40 +02:00
lucaronin
f49b8f60dc fix: guard fresh-note heading paste 2026-05-03 18:27:26 +02:00
lucaronin
39615f49f3 fix: guard appimage colrv1 emoji font startup 2026-05-03 18:06:42 +02:00
lucaronin
3d7a0ee936 fix: guard stale checklist toggles 2026-05-03 17:42:04 +02:00
lucaronin
9faee02e04 test: observe hardened autosave requests 2026-05-03 17:25:31 +02:00
lucaronin
dfff3a848b fix: resolve codacy critical security findings 2026-05-03 17:25:31 +02:00
lucaronin
aaf0336733 feat: add direct AI model providers 2026-05-03 16:23:15 +02:00
lucaronin
5b906cb3a4 fix(inspector): show blank frontmatter properties 2026-05-03 15:05:37 +02:00
choi woo-nyong
0a40f12a6a fix: ko-KR 사이드바 Views 그룹 번역을 "조회수"에서 "보기"로 바로잡아 (#514)
sidebar.group.views 키가 "조회수"로 오역되어 있어 기능과 무관한 표현이었다

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-03 12:53:51 +02:00
lucaronin
033759598a fix: improve editor block controls 2026-05-03 12:28:00 +02:00
lucaronin
871b62fbd3 feat: add markdown whiteboards 2026-05-03 12:17:20 +02:00
lucaronin
aa310b25df fix(ai): expand agent vault paths before spawn 2026-05-03 11:57:15 +02:00
lucaronin
dbb12e96fe feat(editor): add code block copy action 2026-05-03 11:34:56 +02:00
lucaronin
c795f118d0 fix(ui): preserve tooltip positioning under zoom 2026-05-03 11:01:49 +02:00
lucaronin
d96d6efd57 fix: support automatic rtl editor direction 2026-05-03 10:51:45 +02:00
lucaronin
67623d848b fix(layout): keep resize handles above panes 2026-05-03 10:24:16 +02:00
lucaronin
45d72297b3 test: cover image asset URLs on tab swap 2026-05-03 04:14:02 +02:00
lucaronin
4f86b97e8d fix(ai): cap agent composer height 2026-05-02 20:41:08 +02:00
lucaronin
b6dd6189a2 fix(linux): locate appimage mcp resources 2026-05-02 20:08:11 +02:00
lucaronin
ba07bafc97 fix: expose codex mcp tools reliably 2026-05-02 19:47:33 +02:00
lucaronin
6d86770abf fix: compact large ai agent context 2026-05-02 19:24:52 +02:00
lucaronin
e23ba0ae27 fix: normalize windows wikilink targets 2026-05-02 19:01:00 +02:00
lucaronin
7a97f24a0e fix: detect codex windows npm shims 2026-05-02 18:41:38 +02:00
lucaronin
20f34ab9d1 fix: preserve ai panel delete shortcuts 2026-05-02 18:12:37 +02:00
lucaronin
19b31cc96a fix: recover when active vault disappears 2026-05-02 17:53:13 +02:00
lucaronin
e5ed863d5d fix: refine sidebar folder rows 2026-05-02 17:21:58 +02:00
lucaronin
4f7f5a31e4 fix(linux): choose matching wayland preload 2026-05-02 17:01:57 +02:00
lucaronin
439e2b7f66 fix: prefetch tauri nsis tools in release ci 2026-05-02 16:45:59 +02:00
lucaronin
048d27243d fix: refine slash command menu 2026-05-02 16:39:43 +02:00
lucaronin
58e129cdbc fix: stabilize editor block dragging 2026-05-02 12:18:59 +02:00
lucaronin
713b486750 fix: render release history tabs as pills 2026-05-02 11:25:39 +02:00
lucaronin
7d66e2f8e1 fix: guard table handle add selection 2026-05-02 11:16:09 +02:00
lucaronin
cd6847d59e feat: refine titlebar navigation controls 2026-05-02 10:56:36 +02:00
lucaronin
74c9841ad7 fix(ui): refine icon picker appearance 2026-05-02 10:16:24 +02:00
lucaronin
ba41854f0e fix: ignore malformed reload entries 2026-05-02 10:00:50 +02:00
lucaronin
6532ee125a fix(vault): prevent gitignored folder scan freezes 2026-05-02 09:28:51 +02:00
lucaronin
c9698b853e fix(notes): serialize immediate note creation writes 2026-05-02 03:56:48 +02:00
lucaronin
db16f2ff5e fix(views): ignore stale saved-view deletes 2026-05-02 03:25:02 +02:00
lucaronin
1e5d83d4a3 refactor(commands): share app command manifest 2026-05-02 02:57:45 +02:00
lucaronin
062de2656e refactor(frontmatter): share canonical key rules 2026-05-02 02:16:13 +02:00
lucaronin
65d14ebf6f fix(editor): theme code blocks in light mode 2026-05-02 01:47:42 +02:00
lucaronin
0b5149ba8d fix(ai): preserve gui path for cli agents 2026-05-02 01:26:34 +02:00
lucaronin
c23a3c1caf fix: add sidebar section bottom spacing 2026-05-02 01:00:06 +02:00
lucaronin
44e2870697 fix(editor): preserve pasted markdown raw toggles 2026-05-02 00:50:21 +02:00
lucaronin
802b6b77d9 fix: align macOS header controls 2026-05-02 00:35:14 +02:00
lucaronin
bb09fcfd6f fix: repair blocknote core patch 2026-05-01 23:45:38 +02:00
lucaronin
c8ac12f3dc feat: cache note content and parsed editor blocks 2026-05-01 23:14:36 +02:00
lucaronin
fbfda2f15c fix(types): render css type colors 2026-05-01 21:02:42 +02:00
lucaronin
c4c7737e83 fix(types): guard root type filename collisions 2026-05-01 20:33:07 +02:00
lucaronin
c24c60b433 fix(editor): guard stale block side menu actions 2026-05-01 19:58:35 +02:00
lucaronin
21532de941 fix: remove legacy startup loading skeleton 2026-05-01 19:11:45 +02:00
lucaronin
f92efd72df fix: keep note windows editable 2026-05-01 18:36:48 +02:00
lucaronin
6e41328f21 fix: tolerate colliding frontmatter properties 2026-05-01 18:09:48 +02:00
lucaronin
5b2b4cd569 fix: recognize type frontmatter case-insensitively 2026-05-01 17:45:14 +02:00
Jarek Krochmalski
494aeb9533 feat: add Polish (pl-PL) translation (#475)
Add complete Polish locale covering all 525 translation keys:
- New pl-PL.json catalog with full Polish translations
- Register pl-PL in APP_LOCALES and LOCALE_DEFINITIONS in i18n.ts
- Add locale.plPL label to en.json and all existing locale catalogs
- Add pl-PL target to lara.yaml for Lara CLI sync
- Add "polish" to language settings search keywords

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 15:02:18 +02:00
lucaronin
8272e68934 fix: ignore stale rich editor beforeinput views 2026-05-01 13:33:39 +02:00
lucaronin
7ba20c1e83 fix: guard stale table handle drags 2026-05-01 12:55:23 +02:00
lucaronin
3384b9acd0 fix: make editor left block handle draggable 2026-05-01 12:15:14 +02:00
lucaronin
8b36ff6b00 fix: avoid active editor remount on unrelated vault refresh 2026-05-01 11:40:14 +02:00
lucaronin
acb35cb5df docs: add/update ADR for Tauri frontend readiness watchdog (guard — from commit 1c010dd) 2026-05-01 08:03:02 +02:00
lucaronin
a8bda0a11b fix: allow linux window controls 2026-05-01 07:01:58 +02:00
lucaronin
4acbe17e1e refactor: consolidate sidebar item interactions 2026-05-01 06:34:25 +02:00
lucaronin
09759d204c test: cover traditional chinese wikilinks 2026-05-01 06:11:09 +02:00
lucaronin
7a01ef6952 fix: recover formula note parse failures 2026-05-01 05:44:22 +02:00
lucaronin
1c010ddc22 fix: recover inert macos webview startup 2026-05-01 04:58:44 +02:00
lucaronin
0df5c7882d ci: bound vitest coverage parallelism 2026-05-01 04:32:51 +02:00
lucaronin
2d138f1564 ci: bound frontend coverage parallelism 2026-05-01 04:19:39 +02:00
lucaronin
c9ee81c5dc test: cover locale-stable note subtitles 2026-05-01 04:06:02 +02:00
lucaronin
04fa74e8d8 fix: keep mcp setup dialog scrollable 2026-05-01 03:47:13 +02:00
lucaronin
b42ed35596 refactor: consolidate blocknote stale guards 2026-05-01 03:18:19 +02:00
lucaronin
75e1a82ff0 test: cover windows type creation 2026-05-01 02:42:20 +02:00
lucaronin
2af2156526 test: cover windows typed note creation 2026-05-01 02:24:07 +02:00
lucaronin
568ada8992 fix: sanitize appimage git subprocess env 2026-05-01 02:01:49 +02:00
lucaronin
0987946cff fix: surface codex final replies 2026-05-01 01:29:46 +02:00
lucaronin
81b03f05b6 chore: ratchet codescene thresholds 2026-05-01 00:59:32 +02:00
lucaronin
ee71a00c6f feat: add paste without formatting command 2026-05-01 00:59:32 +02:00
lucaronin
adcaa8a387 fix: align ai agent permission modes 2026-04-30 21:18:15 +02:00
lucaronin
4a03f1675e fix: parse crlf multibyte headings safely 2026-04-30 20:18:31 +02:00
lucaronin
50c304fce5 fix: reduce autosave pressure while typing 2026-04-30 19:50:44 +02:00
lucaronin
aa4d4fd12e fix: revert sidebar navigation arrows 2026-04-30 19:33:04 +02:00
lucaronin
b31f0b57da fix: keep macos startup responsive 2026-04-30 18:58:48 +02:00
lucaronin
d3a577b488 fix: copy mcp config through native clipboard 2026-04-30 18:22:14 +02:00
lucaronin
fbb0927401 fix: preserve mermaid svg styles under tauri csp 2026-04-30 17:48:19 +02:00
lucaronin
687d6403a3 fix: remove rename view menu action 2026-04-30 17:28:19 +02:00
lucaronin
1738dd2405 fix: persist claude power user shell access 2026-04-30 16:45:10 +02:00
lucaronin
8c286a4856 fix: stabilize linux appimage webkit rendering 2026-04-30 16:17:37 +02:00
lucaronin
0331b40ae2 fix: persist layout panel widths 2026-04-30 15:53:32 +02:00
lucaronin
d53c3fa7e5 fix: detect linuxbrew ai agent binaries 2026-04-30 15:01:35 +02:00
lucaronin
3c14d60be7 fix: stream gemini cli replies 2026-04-30 14:16:27 +02:00
lucaronin
765a16a4e8 fix: ignore stale BlockNote side menu blocks 2026-04-30 14:02:48 +02:00
lucaronin
886ded9575 fix: canonicalize simplified chinese ui language 2026-04-30 13:34:46 +02:00
lucaronin
831f6eb72b fix: guard uninitialized BlockNote suggestion updates 2026-04-30 13:16:55 +02:00
lucaronin
45efb08b23 fix: persist supported ui language settings 2026-04-30 12:58:26 +02:00
lucaronin
fdc7d8fd84 fix: guard note suggestions during reload 2026-04-30 12:38:23 +02:00
lucaronin
690ace2d1f fix: resolve linux mcp server resources 2026-04-30 12:22:29 +02:00
lucaronin
a267a00716 test: cover native menu event dispatch 2026-04-30 11:57:23 +02:00
lucaronin
536cab3491 fix: align view customization with type actions 2026-04-30 11:39:25 +02:00
lucaronin
e49af5855b fix: ignore stale vault refreshes after note switch 2026-04-30 10:54:16 +02:00
lucaronin
9012cd901b test: include multibyte search smoke in pre-push 2026-04-30 10:37:39 +02:00
lucaronin
0d6e3853f2 fix: make search snippets unicode-safe 2026-04-30 10:23:54 +02:00
lucaronin
8754b1bc77 docs: add ADRs for vault-root navigation and safe analytics events (guard — from commits ec83e9b, 18b4f9d) 2026-04-30 08:03:04 +02:00
lucaronin
ec5751711f feat: improve type sidebar actions 2026-04-30 07:48:34 +02:00
lucaronin
0995c90cc0 fix: show pointer cursor on interactive controls 2026-04-30 07:14:05 +02:00
lucaronin
dede400296 fix: surface desktop menu update feedback 2026-04-30 06:46:45 +02:00
lucaronin
65ff44eb50 feat: open inline images in lightbox 2026-04-30 06:24:38 +02:00
lucaronin
2c06c8dcc2 fix: disable unsafe shiki highlighting on old webkit 2026-04-30 06:06:36 +02:00
lucaronin
bc56eec35e fix: block frontmatter writes for attachments 2026-04-30 05:37:58 +02:00
lucaronin
388d389ba4 fix: disable unsafe editor link opens 2026-04-30 05:21:42 +02:00
lucaronin
a1a4cb50d0 feat: add sidebar navigation arrows 2026-04-30 04:52:28 +02:00
lucaronin
a7ed9adaae fix: ignore stale code block controls 2026-04-30 04:33:19 +02:00
lucaronin
f8f08f4160 feat: reuse type appearance controls for views 2026-04-30 04:14:47 +02:00
lucaronin
c0afae2296 fix: repair blocknote patch metadata 2026-04-30 03:47:26 +02:00
lucaronin
176f983dc9 fix: render opencode text parts after tools 2026-04-30 03:37:31 +02:00
lucaronin
01a87ed478 fix: ignore stale suggestion menu close 2026-04-30 03:13:15 +02:00
lucaronin
18b4f9d078 feat: retrofit safe product analytics events 2026-04-30 02:58:36 +02:00
lucaronin
fd3afc2493 feat: configure all notes file visibility 2026-04-30 02:17:49 +02:00
lucaronin
0ad207e318 fix: reject unsafe editor serialization 2026-04-30 01:47:21 +02:00
lucaronin
2e40945c2d fix: guard blocknote popover references 2026-04-30 01:20:12 +02:00
lucaronin
54d53b882f fix: dedupe native menu state sync 2026-04-30 00:58:07 +02:00
lucaronin
848d718373 fix: repair empty editor block swaps 2026-04-30 00:42:46 +02:00
lucaronin
ec83e9b839 feat: show vault root in folder tree 2026-04-30 00:18:33 +02:00
lucaronin
ce859aa0e7 fix: stabilize inbox organize navigation 2026-04-29 23:40:28 +02:00
lucaronin
1f4889fe66 fix: keep vault loading shell visible 2026-04-29 23:12:33 +02:00
lucaronin
2a2ea1b8ef fix: uncap wide editor content 2026-04-29 22:41:05 +02:00
lucaronin
b2a50d9870 fix: remove wide note max width 2026-04-29 22:24:54 +02:00
d 🔹
cca72a9d6a fix: pass 'en-US' to word-count toLocaleString in note subtitles (#438)
Subtitle helpers used the system locale to format word counts, while
adjacent date formatting in the same file already pins 'en-US'. On
locales like de-DE this produced '1.200 words' alongside English copy
('words', 'Created') and English-formatted dates, and broke
`noteListHelpers extra coverage` plus two SearchPanel cases on any
machine with a non-comma thousands separator.

Pass 'en-US' explicitly in both formatSubtitle and formatSearchSubtitle
so the number separator matches the surrounding English text and the
existing date convention in the file.

Closes #429

Co-authored-by: voidborne-d <voidborne@agentmail.to>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 22:08:56 +02:00
Alexandre ETEOCLE
ef0e85b053 ci: add rpm bundle to linux release workflows
Adds RPM generation, validation, intermediate artifact upload, and final GitHub Release publish patterns for Linux alpha and stable workflows.
2026-04-29 22:04:34 +02:00
github-actions[bot]
e7d74a7880 Merge branch 'main' into main 2026-04-29 19:58:07 +00:00
lucaronin
2624d32b50 fix: keep large vault startup responsive 2026-04-29 21:47:29 +02:00
github-actions[bot]
8cb5d0fd88 Merge branch 'main' into main 2026-04-29 19:42:54 +00:00
Luca Rossi
9c42da5745 Revise README for platform support and clarity
Updated README to reflect support for macOS, Windows, and Linux. Enhanced clarity in download instructions and removed outdated Gemini CLI setup section.
2026-04-29 21:42:21 +02:00
github-actions[bot]
cebcd17db0 Merge branch 'main' into main 2026-04-29 19:40:29 +00:00
Muescha
454acb5ded doc: README add installation via Homebrew (#338)
Updated installation instructions via Homebrew ( https://formulae.brew.sh/cask/tolaria ) and added a section for getting started with Tolaria.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:40:01 +02:00
github-actions[bot]
4276c1c3ac Merge branch 'main' into main 2026-04-29 19:39:54 +00:00
lucaronin
2a691f68db fix: keep vault asset previews available after switches 2026-04-29 21:33:20 +02:00
lucaronin
c4de154410 test: stabilize wikilink smoke insertion 2026-04-29 21:29:17 +02:00
lucaronin
670aae9f2e fix: create type documents at vault root 2026-04-29 21:29:17 +02:00
github-actions[bot]
fa3cf3f7f3 Merge branch 'main' into main 2026-04-29 19:27:18 +00:00
hhungxun
f3b4e5e61d fix: preload Wayland client for Linux AppImage (#405)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:26:50 +02:00
github-actions[bot]
8d761751e7 Merge branch 'main' into main 2026-04-29 19:21:47 +00:00
Khanh Thanh
4c4f01af43 Add Vietnamese language support and update locale files (#421)
- Added 'vi' (Vietnamese) to the list of supported app locales in i18n.ts.
- Updated locale definitions to include Vietnamese translations for various keys.
- Modified existing locale files (de-DE, en, es-419, es-ES, fr-FR, it-IT, ja-JP, ko-KR, pt-BR, pt-PT, ru-RU, zh-CN) to include Vietnamese translations for Vietnamese and ensure consistency.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:21:24 +02:00
github-actions[bot]
eabab20453 Merge branch 'main' into main 2026-04-29 19:18:49 +00:00
rubinette
79d7eb9795 feat: add comprehensive Traditional Chinese (zh-TW) locale support (#419)
- Add Traditional Chinese (zh-TW) locale support throughout the app
- Add and register a comprehensive zh-TW translation file
- Update tests to cover normalization and handling of zh-TW and its aliases
- Refine locale registry to include zh-TW definition and improve language keyword handling
- Extend locale-related keywords and display options in en.json for zh-TW locale

Signed-off-by: rubinette <rubinette@hey.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:18:17 +02:00
github-actions[bot]
fdd6a64c72 Merge branch 'main' into main 2026-04-29 18:55:44 +00:00
lucaronin
d1ed826714 fix: suppress watcher refresh after new note writes 2026-04-29 20:45:07 +02:00
github-actions[bot]
2db83d0810 Merge branch 'main' into main 2026-04-29 18:23:25 +00:00
lucaronin
2e8286d2b3 fix: normalize Windows verbatim note paths 2026-04-29 20:10:43 +02:00
github-actions[bot]
cdb303d98c Merge branch 'main' into main 2026-04-29 18:05:55 +00:00
lucaronin
cb9ebaadad fix: keep manual selection after inbox organize 2026-04-29 19:53:33 +02:00
github-actions[bot]
fe2a35b09e Merge branch 'main' into main 2026-04-29 17:40:28 +00:00
lucaronin
c295c9f2b5 feat: add note width modes 2026-04-29 19:26:24 +02:00
github-actions[bot]
816145ea0f Merge branch 'main' into main 2026-04-29 16:32:08 +00:00
lucaronin
a4b11089c0 fix: debounce rich editor serialization 2026-04-29 18:19:35 +02:00
github-actions[bot]
5d3661995c Merge branch 'main' into main 2026-04-29 15:25:20 +00:00
lucaronin
6f29542528 fix: render mermaid diagrams from notes 2026-04-29 17:14:28 +02:00
github-actions[bot]
c06b673e62 Merge branch 'main' into main 2026-04-29 12:34:15 +00:00
lucaronin
dcd0d73848 fix: open vault files externally 2026-04-29 14:25:15 +02:00
github-actions[bot]
45e7416267 Merge branch 'main' into main 2026-04-29 12:24:43 +00:00
lucaronin
0e8c8fb61a docs: add product analytics workflow 2026-04-29 14:24:12 +02:00
github-actions[bot]
743b259fb5 Merge branch 'main' into main 2026-04-29 11:59:15 +00:00
lucaronin
42bdd559ef fix: move mcp config copy to settings 2026-04-29 13:51:28 +02:00
github-actions[bot]
154e6f68be Merge branch 'main' into main 2026-04-29 11:46:39 +00:00
lucaronin
3a6c97272d fix: allow pdf asset previews in csp 2026-04-29 13:32:29 +02:00
github-actions[bot]
053404afee Merge branch 'main' into main 2026-04-29 11:29:05 +00:00
lucaronin
f53a3746d3 fix: localize recent UI copy 2026-04-29 13:25:22 +02:00
lucaronin
23145a05f5 feat: preview pdf files in app 2026-04-29 13:14:44 +02:00
github-actions[bot]
459eae1334 Merge branch 'main' into main 2026-04-29 10:22:51 +00:00
lucaronin
5b86b0b50d fix: validate active note cache refresh 2026-04-29 12:09:58 +02:00
lucaronin
dc1b1783b6 docs: add localization developer workflow 2026-04-29 12:09:58 +02:00
lucaronin
3dc520abea feat: add safe note open cache 2026-04-29 12:09:58 +02:00
github-actions[bot]
22b366d6f9 Merge branch 'main' into main 2026-04-29 10:00:02 +00:00
lucaronin
792b3009dd fix: keep saved view reorder UI native 2026-04-29 11:51:22 +02:00
github-actions[bot]
9540a4e9dc Merge branch 'main' into main 2026-04-29 09:28:10 +00:00
lucaronin
6ee7f219f5 feat: add gemini cli agent support 2026-04-29 11:16:52 +02:00
github-actions[bot]
de1cad448b Merge branch 'main' into main 2026-04-29 08:54:27 +00:00
lucaronin
3db1ed9dc5 fix: split ai permission tooltips 2026-04-29 10:44:20 +02:00
github-actions[bot]
1133b8ee6f Merge branch 'main' into main 2026-04-29 08:42:58 +00:00
lucaronin
7a7ef5d9ca fix: improve ai permission toggle contrast 2026-04-29 10:35:19 +02:00
github-actions[bot]
ae4dbefe83 Merge branch 'main' into main 2026-04-29 08:31:25 +00:00
Luca Rossi
4c42cf4d52 Updated release download link 2026-04-29 10:30:53 +02:00
github-actions[bot]
2f1ffeffcb Merge branch 'main' into main 2026-04-29 08:17:58 +00:00
lucaronin
405b711e16 revert: restore editor side menu ordering 2026-04-29 10:06:03 +02:00
github-actions[bot]
a3a89ef483 Merge branch 'main' into main 2026-04-29 06:19:49 +00:00
lucaronin
83705e26f5 docs: add ADRs for gitignored visibility and saved view ordering (guard — from commits d2c15b8, fd92ff5) 2026-04-29 08:03:17 +02:00
github-actions[bot]
6c533b7055 Merge branch 'main' into main 2026-04-29 05:56:55 +00:00
lucaronin
434927160c fix: stabilize radix overlay presence 2026-04-29 07:45:02 +02:00
github-actions[bot]
d54467cbfe Merge branch 'main' into main 2026-04-29 05:09:38 +00:00
lucaronin
ec2e04a464 test: wait for date picker smoke note load 2026-04-29 06:55:07 +02:00
github-actions[bot]
359103b0d9 Merge branch 'main' into main 2026-04-29 04:50:54 +00:00
lucaronin
a17ec689a3 fix: tolerate stale vault watcher listeners 2026-04-29 06:36:39 +02:00
github-actions[bot]
2302594c5c Merge branch 'main' into main 2026-04-29 04:21:53 +00:00
lucaronin
eda4d380e0 fix: let disk decide type file conflicts 2026-04-29 06:09:31 +02:00
github-actions[bot]
e226804947 Merge branch 'main' into main 2026-04-29 03:41:19 +00:00
lucaronin
f9b155ba6e fix: repair malformed editor block entries 2026-04-29 05:27:39 +02:00
github-actions[bot]
165697f678 Merge branch 'main' into main 2026-04-29 03:17:43 +00:00
lucaronin
4a62008b1f fix: clamp stale raw editor selections 2026-04-29 05:05:15 +02:00
github-actions[bot]
a6a8146975 Merge branch 'main' into main 2026-04-29 03:00:21 +00:00
lucaronin
a08cee7965 refactor: share cli agent runtime scaffold 2026-04-29 04:47:16 +02:00
github-actions[bot]
ec03ffc7a9 Merge branch 'main' into main 2026-04-29 02:47:15 +00:00
lucaronin
a7c480af62 fix: normalize stale vault entry metadata 2026-04-29 04:33:18 +02:00
github-actions[bot]
beeecb1845 Merge branch 'main' into main 2026-04-29 02:11:15 +00:00
lucaronin
f4790d5763 fix: detect Windows npm ai agent shims 2026-04-29 04:01:53 +02:00
lucaronin
ed52a9e800 fix: preserve frontmatter date picker days 2026-04-29 03:56:44 +02:00
lucaronin
2fcd808f09 fix: detect Windows npm ai agent shims 2026-04-29 03:53:54 +02:00
github-actions[bot]
f7a54593ac Merge branch 'main' into main 2026-04-29 01:43:08 +00:00
lucaronin
9d15286f7d fix: stop orphaned mcp server processes 2026-04-29 03:29:16 +02:00
github-actions[bot]
6209b0baa5 Merge branch 'main' into main 2026-04-29 01:16:17 +00:00
lucaronin
537bd0f3ec fix: normalize Windows vault image paths 2026-04-29 03:02:57 +02:00
github-actions[bot]
8739605572 Merge branch 'main' into main 2026-04-29 00:48:11 +00:00
github-actions[bot]
f18a309469 Merge branch 'main' into main 2026-04-29 00:36:54 +00:00
lucaronin
648241604f feat: add theme mode command actions 2026-04-29 02:35:52 +02:00
lucaronin
c6f3d9d945 fix: create type files in type folder 2026-04-29 02:24:01 +02:00
github-actions[bot]
aa1d54f4c9 Merge branch 'main' into main 2026-04-28 23:54:31 +00:00
lucaronin
65e54f108a fix: render saved view colors 2026-04-29 01:40:16 +02:00
github-actions[bot]
f816be5bbe Merge branch 'main' into main 2026-04-28 23:28:37 +00:00
lucaronin
58f8a774b5 chore: ratchet codescene thresholds 2026-04-29 01:26:33 +02:00
lucaronin
0bf280d3ec fix: satisfy ai permission mode clippy 2026-04-29 01:26:33 +02:00
lucaronin
d2d4746d5c feat: add vault ai agent permission modes 2026-04-29 01:26:33 +02:00
github-actions[bot]
101423c977 Merge branch 'main' into main 2026-04-28 22:27:31 +00:00
lucaronin
91db7c0098 fix: pause saves without active vault 2026-04-29 00:19:47 +02:00
github-actions[bot]
e66c81d896 Merge branch 'main' into main 2026-04-28 22:04:32 +00:00
lucaronin
87908d982f fix: allow guidance open path 2026-04-28 23:56:08 +02:00
github-actions[bot]
e399d4c1fe Merge branch 'main' into main 2026-04-28 21:50:18 +00:00
lucaronin
40544fc551 fix: detect nvm ai agent binaries 2026-04-28 23:39:48 +02:00
github-actions[bot]
63c5c5e6b1 Merge branch 'main' into main 2026-04-28 21:39:27 +00:00
lucaronin
f34fdb0289 fix: guard stale suggestion menu actions 2026-04-28 23:29:09 +02:00
github-actions[bot]
d39fa1aa9d Merge branch 'main' into main 2026-04-28 21:09:09 +00:00
lucaronin
e601a42f41 fix: surface ai agent final replies 2026-04-28 23:00:06 +02:00
lucaronin
de627c2478 fix: normalize missing vault metadata 2026-04-28 22:52:07 +02:00
Oleg Kossoy
52aae9ed96 fix: tolerate non-JS pnpm binary in coverage runner
`run-vitest-coverage.mjs` reused `process.env.npm_execpath` with
`process.execPath` (node) for a fast path that skips the pnpm wrapper.
That assumes `npm_execpath` is a JS file, but standalone pnpm installs
ship a native Mach-O / ELF binary, so node tries to load the binary as
a CJS module and crashes with `SyntaxError: Invalid or unexpected
token`, taking the pre-push hook down with it.

Only take the fast path when the path actually looks like a JS module
(`.js`/`.mjs`/`.cjs`); otherwise fall back to invoking `pnpm` from
PATH, which is what the script already does when `npm_execpath` is
unset.
2026-04-28 23:49:40 +03:00
Oleg Kossoy
f72b7d3361 fix: ignore .git symlink targets in vault watcher
Vaults stored on iCloud / Dropbox commonly use a `.git -> .git.nosync`
symlink so the cloud sync engine ignores git internals. Background
`git fetch` calls (auto-pull, remote-status, gitstatusd) wrote to the
real `.git.nosync/` directory, which the watcher's `.git` component
filter did not match — every fetch fired a `vault-changed` event and
triggered `reloadVault`, flickering the "Reload vault" status badge
multiple times per second.

Resolve the real git directory at watcher start (symlink, regular dir,
or `gitdir:` pointer for worktrees/submodules) and reject any path
inside it. Also filter `.gitstatus.*` cache dirs and `*.icloud`
placeholders, which are noisy on the same setups.
2026-04-28 23:49:40 +03:00
lucaronin
46d9aaed48 fix: dedupe editor toolbar state closes 2026-04-28 21:57:26 +02:00
lucaronin
6b96b5caf6 fix: reset editor selection before note swaps 2026-04-28 21:31:54 +02:00
lucaronin
595c48ca59 fix: apply settings theme changes immediately 2026-04-28 21:16:30 +02:00
lucaronin
fd92ff5e9b feat: allow manual saved view ordering 2026-04-28 20:41:07 +02:00
lucaronin
a39b346a75 fix: detect mise node for mcp setup 2026-04-28 20:28:56 +02:00
lucaronin
deac10caa2 fix: detect nvm pi installs 2026-04-28 19:54:39 +02:00
lucaronin
0e1e0ff47a fix: preserve ai composer line breaks 2026-04-28 19:32:08 +02:00
lucaronin
a645edbc19 fix: repair malformed editor block ids 2026-04-28 18:56:36 +02:00
lucaronin
48f1fb0b50 feat: add gemini cli external ai setup 2026-04-28 18:30:14 +02:00
lucaronin
1aede09834 fix: preserve wikilinks inside markdown tables 2026-04-28 18:00:15 +02:00
lucaronin
d51b76f992 fix: restrict sentry releases to stable builds 2026-04-28 17:17:23 +02:00
lucaronin
8e66fb4c19 fix: preserve table math markdown 2026-04-28 16:47:52 +02:00
lucaronin
a2843a0cf3 fix: persist immediate note creation before open 2026-04-28 15:01:32 +02:00
lucaronin
38fb8a7370 feat: copy mcp config from app 2026-04-28 14:03:10 +02:00
lucaronin
be5a8bb300 fix: make editor left handle draggable 2026-04-28 12:31:13 +02:00
lucaronin
ce040c75fd test: cover table hover stability 2026-04-28 10:46:06 +02:00
lucaronin
ced89a42bd fix: render mermaid diagrams with lightbox 2026-04-28 10:32:37 +02:00
lucaronin
20a960551f fix: update fallback vault git email 2026-04-28 10:08:18 +02:00
lucaronin
2e331315e8 fix: refine Italian locale labels 2026-04-28 09:43:44 +02:00
lucaronin
d2c15b8469 feat: hide gitignored vault content 2026-04-28 09:07:21 +02:00
lucaronin
2fe6c5b580 fix: respect selected ai agent in right panel 2026-04-28 07:36:22 +02:00
lucaronin
2a224f78f8 refactor: simplify note mutation flushing 2026-04-28 06:25:39 +02:00
lucaronin
b85d54bb5d fix: preserve ai agent final result text 2026-04-28 06:17:33 +02:00
lucaronin
9c05e17d37 fix: sanitize custom view filenames 2026-04-28 05:39:41 +02:00
lucaronin
cf6ad71fff ci: de-duplicate coverage-backed test lane 2026-04-28 04:53:39 +02:00
lucaronin
db82aee172 refactor: consolidate ai agent session flow 2026-04-28 04:20:40 +02:00
lucaronin
c72d833624 feat: add pi ai agent support 2026-04-28 03:41:04 +02:00
lucaronin
a3ca78fced fix: guard git init parent folders 2026-04-28 02:38:32 +02:00
lucaronin
e912ab9a9f fix: use selected vault for mcp bridge 2026-04-28 02:26:14 +02:00
lucaronin
49717dc671 feat: add opencode ai agent option 2026-04-28 01:26:21 +02:00
lucaronin
d9a0a04ddb chore: ratchet codescene thresholds 2026-04-28 01:21:59 +02:00
lucaronin
0561fe68b1 fix: restore editor block handle reordering 2026-04-28 01:09:47 +02:00
lucaronin
2f078fab11 feat: watch active vault filesystem changes 2026-04-27 23:54:50 +02:00
lucaronin
98fde6571a feat: render mermaid note diagrams 2026-04-27 22:14:23 +02:00
lucaronin
a5c4b8fa20 fix: hide ai agent subprocess windows 2026-04-27 21:21:00 +02:00
lucaronin
1301653b81 fix: avoid tooltip dropdown popper nesting 2026-04-27 20:58:31 +02:00
lucaronin
ee90b05159 fix: guard inline input types 2026-04-27 20:36:59 +02:00
lucaronin
b6c11b1468 fix: close claude print-mode stdin 2026-04-27 19:25:53 +02:00
lucaronin
2fbdafa6f2 fix: set git identity before remote connect 2026-04-27 18:59:23 +02:00
lucaronin
39f44b86aa fix: copy fenced code blocks exactly 2026-04-27 18:31:51 +02:00
lucaronin
cb3274cdbc fix: harden windows mcp setup 2026-04-27 17:51:25 +02:00
lucaronin
76d8bba332 fix: remove hidden Claude permission bypass 2026-04-27 16:46:32 +02:00
lucaronin
86853cf337 fix: allow Codex to edit active vault 2026-04-27 16:07:05 +02:00
lucaronin
9a292d2476 fix: map releases to Sentry 2026-04-27 15:27:31 +02:00
lucaronin
4a22b02810 test: harden locale catalog validation 2026-04-27 14:48:49 +02:00
github-actions[bot]
e4270cda3b Merge branch 'main' into main 2026-04-27 12:45:03 +00:00
lucaronin
9cb0de37a5 fix: parse crlf frontmatter 2026-04-27 14:31:51 +02:00
github-actions[bot]
2a794cb373 Merge branch 'main' into main 2026-04-27 11:37:50 +00:00
lucaronin
324af6b271 fix: normalize folder filtering across path separators 2026-04-27 13:27:20 +02:00
Disheng Qiu
926db0eeb5 feat: add lara-powered app localization 2026-04-27 13:06:59 +02:00
lucaronin
b1a97834c9 fix: keep raw editor full width 2026-04-27 11:54:15 +02:00
lucaronin
da13cb38ac fix: satisfy editor find command contract 2026-04-27 10:57:58 +02:00
lucaronin
dbf8ba5f40 feat: add editor find and replace 2026-04-27 10:57:58 +02:00
lucaronin
3e8a6e5d7c fix: render inline math while typing 2026-04-27 10:44:35 +02:00
lucaronin
8141644729 fix: revert note width modes 2026-04-27 10:13:55 +02:00
lucaronin
0d88c75718 Merge branches 'main', 'main', 'main' and 'main' of https://github.com/refactoringhq/tolaria 2026-04-27 09:42:36 +02:00
lucaronin
f8721f2a1b feat: add note width modes 2026-04-27 05:34:11 +02:00
lucaronin
fb39c6679a fix: handle invalid Windows save paths 2026-04-27 04:04:04 +02:00
lucaronin
f1bed131bf fix: refresh type state after type changes 2026-04-27 03:25:24 +02:00
lucaronin
257cf6ed02 fix: keep theme tooltip inside window 2026-04-27 02:20:28 +02:00
lucaronin
908daafaa1 feat: add basic file actions 2026-04-27 01:45:16 +02:00
lucaronin
d15437face test: cover consecutive IME composer input 2026-04-27 01:05:34 +02:00
lucaronin
30281f879d chore: ratchet codescene thresholds 2026-04-27 00:49:39 +02:00
lucaronin
4ef6edfb10 feat: preview vault images in app 2026-04-27 00:36:24 +02:00
lucaronin
918419e371 fix: respect macos control text bindings 2026-04-26 23:22:53 +02:00
lucaronin
2704002973 fix: remove note drag and drop flows 2026-04-26 22:51:08 +02:00
lucaronin
9265b8f117 fix: stabilize AI composer text editing 2026-04-26 22:11:54 +02:00
lucaronin
943fa854e1 fix: satisfy localization build 2026-04-26 19:39:56 +02:00
lucaronin
a2f8ba72a2 fix: complete app localization pass 2026-04-26 19:37:21 +02:00
lucaronin
1d546dde3d fix: allow note drops to insert wikilinks 2026-04-26 18:15:22 +02:00
lucaronin
63877e61ec fix: store window state in logical points 2026-04-26 17:54:41 +02:00
lucaronin
8896f1d451 fix: restore saved window size after launch 2026-04-26 17:54:41 +02:00
lucaronin
f523099854 feat: add vault loading skeleton 2026-04-26 17:35:09 +02:00
lucaronin
15f38f096f feat: support non-git vaults 2026-04-26 17:08:14 +02:00
lucaronin
cdd2ddec6c feat: persist main window placement 2026-04-26 15:45:34 +02:00
lucaronin
292d3739df fix: prevent editor handling note wikilink drops 2026-04-26 14:37:11 +02:00
lucaronin
1136c1948e fix: clarify macos release artifact names 2026-04-26 14:29:47 +02:00
lucaronin
8406042db1 fix: support native folder drops in AI inputs 2026-04-26 13:59:39 +02:00
lucaronin
f07cbbe0eb test: reflect cloned getting started vault in smoke 2026-04-26 12:28:01 +02:00
lucaronin
768da9f955 fix: prevent bottom bar wrap at narrow widths 2026-04-26 12:12:05 +02:00
lucaronin
f58e9d8930 fix: allow editor runtime styles under CSP 2026-04-26 11:31:41 +02:00
lucaronin
133242eff5 fix: keep compact status bar on one row 2026-04-26 10:33:16 +02:00
lucaronin
aebd2bea4a fix: refresh language settings draft 2026-04-26 09:46:11 +02:00
lucaronin
60238fbe78 fix: persist native language settings 2026-04-26 09:17:28 +02:00
lucaronin
5336236e79 test: cover native command wrappers 2026-04-26 08:39:56 +02:00
lucaronin
f9719f11b5 feat: add app localization foundation 2026-04-26 08:18:47 +02:00
lucaronin
e4083ded1a fix: detect claude cli on windows 2026-04-26 07:34:27 +02:00
lucaronin
65a89f102a fix: detect codex from macos shell paths 2026-04-26 06:48:35 +02:00
lucaronin
5b34ec2980 feat: publish intel mac builds 2026-04-26 06:07:33 +02:00
lucaronin
e579979829 fix: preserve composed ai input 2026-04-26 05:24:05 +02:00
1246 changed files with 154887 additions and 18887 deletions

17
.codacy.yaml Normal file
View File

@@ -0,0 +1,17 @@
---
exclude_paths:
- "coverage/**"
- "dist/**"
- "e2e/**"
- "node_modules/**"
- "scripts/**"
- "src/test/**"
- "src-tauri/gen/**"
- "src-tauri/resources/agent-docs/**"
- "src-tauri/target/**"
- "target/**"
- "test-results/**"
- "tests/**"
- "**/*.test.ts"
- "**/*.test.tsx"
- "vite.config.ts"

View File

@@ -1,2 +1,2 @@
HOTSPOT_THRESHOLD=9.98
AVERAGE_THRESHOLD=9.87
HOTSPOT_THRESHOLD=10.0
AVERAGE_THRESHOLD=9.95

View File

@@ -12,3 +12,7 @@ VITE_SENTRY_DSN=
# PostHog (https://posthog.com → Project → Settings → Project API Key)
VITE_POSTHOG_KEY=
VITE_POSTHOG_HOST=https://eu.i.posthog.com
# Lara CLI (https://github.com/translated/lara-cli)
LARA_ACCESS_KEY_ID=
LARA_ACCESS_KEY_SECRET=

View File

@@ -0,0 +1,115 @@
param(
[string]$ConfigPath = "src-tauri/tauri.windows-signing.conf.json"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Read-FirstEnv {
param([string[]]$Names)
foreach ($Name in $Names) {
$Value = [Environment]::GetEnvironmentVariable($Name)
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value.Trim()
}
}
throw "Set one of these environment variables: $($Names -join ', ')"
}
function Read-OptionalEnv {
param(
[string[]]$Names,
[string]$DefaultValue
)
foreach ($Name in $Names) {
$Value = [Environment]::GetEnvironmentVariable($Name)
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value.Trim()
}
}
return $DefaultValue
}
function Normalize-Thumbprint {
param([string]$Thumbprint)
return ($Thumbprint -replace "\s", "").ToUpperInvariant()
}
function Convert-CertificateSecretToBytes {
param([string]$CertificateSecret)
$Base64Lines = $CertificateSecret -split "\r?\n" |
Where-Object { $_ -notmatch "^-+BEGIN " -and $_ -notmatch "^-+END " }
$CertificateBase64 = ($Base64Lines -join "") -replace "\s", ""
try {
return [Convert]::FromBase64String($CertificateBase64)
} catch {
throw "Windows code-signing certificate must be base64-encoded PFX data."
}
}
$CertificateSecret = Read-FirstEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE", "WINDOWS_CERTIFICATE")
$CertificatePassword = Read-FirstEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD", "WINDOWS_CERTIFICATE_PASSWORD")
$ConfiguredThumbprint = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT", "WINDOWS_CERTIFICATE_THUMBPRINT") ""
$DigestAlgorithm = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_DIGEST_ALGORITHM") "sha256"
$TimestampUrl = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_TIMESTAMP_URL", "WINDOWS_TIMESTAMP_URL") "http://timestamp.digicert.com"
$TempRoot = Join-Path ([IO.Path]::GetTempPath()) "tolaria-windows-signing"
if (-not [string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
$TempRoot = Join-Path $env:RUNNER_TEMP "tolaria-windows-signing"
}
New-Item -ItemType Directory -Force -Path $TempRoot | Out-Null
$PfxPath = Join-Path $TempRoot "certificate.pfx"
[IO.File]::WriteAllBytes($PfxPath, (Convert-CertificateSecretToBytes $CertificateSecret))
$SecurePassword = ConvertTo-SecureString -String $CertificatePassword -Force -AsPlainText
$ImportedCertificates = @(Import-PfxCertificate -FilePath $PfxPath -CertStoreLocation Cert:\CurrentUser\My -Password $SecurePassword)
Remove-Item -Force -ErrorAction SilentlyContinue $PfxPath
$ImportedCertificate = $ImportedCertificates | Where-Object { $_.HasPrivateKey } | Select-Object -First 1
if ($null -eq $ImportedCertificate) {
throw "The imported Windows code-signing certificate does not include a private key."
}
if ([string]::IsNullOrWhiteSpace($ConfiguredThumbprint)) {
$CertificateThumbprint = Normalize-Thumbprint $ImportedCertificate.Thumbprint
} else {
$CertificateThumbprint = Normalize-Thumbprint $ConfiguredThumbprint
}
$StoreCertificate = Get-ChildItem Cert:\CurrentUser\My |
Where-Object { (Normalize-Thumbprint $_.Thumbprint) -eq $CertificateThumbprint } |
Select-Object -First 1
if ($null -eq $StoreCertificate) {
throw "The requested Windows code-signing certificate thumbprint was not found in Cert:\CurrentUser\My."
}
$Config = @{
bundle = @{
windows = @{
certificateThumbprint = $CertificateThumbprint
digestAlgorithm = $DigestAlgorithm
timestampUrl = $TimestampUrl
}
}
}
$ResolvedConfigPath = Resolve-Path -Path (Split-Path -Parent $ConfigPath) -ErrorAction SilentlyContinue
if ($null -eq $ResolvedConfigPath) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $ConfigPath) | Out-Null
}
$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding utf8NoBOM
if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
"WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT=$CertificateThumbprint" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
}
Write-Host "Prepared Windows Authenticode signing config at $ConfigPath."

137
.github/scripts/prefetch-tauri-nsis.ps1 vendored Normal file
View File

@@ -0,0 +1,137 @@
$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,10 +14,12 @@ 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:
test:
name: Tests & Quality Checks
frontend-quality:
name: Frontend Tests & Quality Checks
runs-on: macos-15
steps:
@@ -26,7 +28,7 @@ jobs:
fetch-depth: 0 # Full history for CodeScene
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
@@ -36,69 +38,42 @@ 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
- name: Vite build check
run: pnpm build
# TypeScript is checked explicitly above; run Vite directly here to avoid
# paying for the package build script's duplicate `tsc -b` pass.
run: pnpm exec vite build
# ── 1. Tests ──────────────────────────────────────────────────────────
- name: Run frontend tests
run: pnpm test
- name: Docs build check
run: pnpm docs:build
# ── 1. Coverage-backed tests ──────────────────────────────────────────
# The coverage command runs the canonical frontend test suite.
- name: Bundle MCP server resources (required by Tauri build)
run: node scripts/bundle-mcp-server.mjs
- name: Run Rust tests
run: cargo test --manifest-path=src-tauri/Cargo.toml
# ── 2. Coverage (enforced — fails build if thresholds not met) ────────
- name: Frontend coverage (≥70% lines/functions/branches/statements)
# ── 2. Tests + coverage (enforced — fails build if thresholds not met) ─
- name: Frontend tests + coverage (≥70% lines/functions/branches/statements)
run: pnpm test:coverage
# Thresholds configured in vite.config.ts — exits non-zero if coverage drops
- name: Rust 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
- name: Upload frontend coverage to Codecov
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe
with:
use_oidc: true
fail_ci_if_error: true
disable_search: true
files: ./coverage/lcov.info,./coverage/rust.lcov
files: ./coverage/lcov.info
flags: frontend
verbose: true
# OIDC avoids long-lived CODECOV_TOKEN secrets.
@@ -162,6 +137,56 @@ 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@29eef336d9b2848a0b548edc03f92a220660cdb8
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@e5de28abeb52d916c5e5875d54b21a9e738b61ec
- 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@75cd11691c0faa626561e295848008c8a7dddffe
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
@@ -170,6 +195,11 @@ jobs:
linux-build:
name: Linux build verification
# Keep the normal push CI lane under the 10-minute target. The release
# workflows already perform the full Linux/AppImage build after main
# pushes, so this slower compatibility check stays available for PRs and
# manual diagnostics without blocking every direct push.
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-22.04
steps:
@@ -184,13 +214,14 @@ jobs:
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
libfuse2 \
librsvg2-dev \
patchelf \
build-essential \
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
@@ -201,7 +232,7 @@ jobs:
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
components: clippy

104
.github/workflows/deploy-docs.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
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@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: latest
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build docs and download pages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pnpm docs:build
DIST="site/.vitepress/dist"
mkdir -p "$DIST/alpha" "$DIST/stable" "$DIST/download" "$DIST/releases" "$DIST/stable/download"
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > "$DIST/releases.json"
STABLE_TAG="$(gh release list --repo ${{ github.repository }} --limit 100 --json tagName,isDraft,isPrerelease --jq '[.[] | select(.isDraft == false and .isPrerelease == false)][0].tagName // ""')"
if [ -n "$STABLE_TAG" ]; then
gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output "$DIST/stable/latest.json" || echo '{}' > "$DIST/stable/latest.json"
else
echo '{}' > "$DIST/stable/latest.json"
fi
ALPHA_TAG="$(gh release list --repo ${{ github.repository }} --limit 100 --json tagName,isDraft,isPrerelease --jq '[.[] | select(.isDraft == false and .isPrerelease == true)][0].tagName // ""')"
if [ -n "$ALPHA_TAG" ]; then
gh release download --repo ${{ github.repository }} "$ALPHA_TAG" --pattern "alpha-latest.json" --output "$DIST/alpha/latest.json" || echo '{}' > "$DIST/alpha/latest.json"
else
echo '{}' > "$DIST/alpha/latest.json"
fi
bun scripts/build-release-download-page.ts --latest-json "$DIST/stable/latest.json" --releases-json "$DIST/releases.json" --output-file "$DIST/download/index.html"
bun scripts/build-release-history-page.ts --releases-json "$DIST/releases.json" --output-file "$DIST/releases/index.html"
cp "$DIST/download/index.html" "$DIST/stable/download/index.html"
cp "$DIST/alpha/latest.json" "$DIST/latest.json"
cp "$DIST/alpha/latest.json" "$DIST/latest-canary.json"
- 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

@@ -0,0 +1,567 @@
name: Release build artifacts
on:
workflow_call:
inputs:
version:
required: true
type: string
macos_bundles:
required: false
type: string
default: ""
upload_macos_dmg:
required: true
type: boolean
require_windows_authenticode:
required: false
type: boolean
default: true
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
jobs:
build:
name: Build (${{ matrix.arch }})
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
- arch: x86_64
target: x86_64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/${{ matrix.target }}/release/bundle
- name: Set version
run: |
VERSION="${{ inputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Validate telemetry env
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
python3 <<'PY'
import os
import re
import sys
from urllib.parse import urlparse
DISALLOWED_PLACEHOLDERS = {
"",
"-",
"_",
"false",
"true",
"null",
"undefined",
"none",
"disabled",
}
def normalize(name: str) -> str:
value = os.getenv(name, "").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1].strip()
return value
def normalize_http_like(value: str) -> str:
if "://" in value:
return value
return f"https://{value}"
def normalize_hostname(hostname: str) -> str:
normalized = hostname.strip().rstrip('.').lower()
if normalized.startswith('[') and normalized.endswith(']'):
normalized = normalized[1:-1]
return normalized
def is_ip_address(hostname: str) -> bool:
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
def is_allowed_hostname(hostname: str) -> bool:
normalized = normalize_hostname(hostname)
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
return False
if normalized == 'localhost':
return True
return '.' in normalized or is_ip_address(normalized)
def is_http_url(value: str) -> bool:
parsed = urlparse(normalize_http_like(value))
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
values = {
name: normalize(name)
for name in (
"VITE_SENTRY_DSN",
"SENTRY_DSN",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
)
}
errors = []
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
value = values[name]
if value.lower() in DISALLOWED_PLACEHOLDERS:
errors.append(f"{name} must be set to a real value, not a placeholder")
elif not is_http_url(value):
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
if errors:
print("Telemetry env validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)
print("Telemetry env validation passed.")
PY
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_RELEASE: ${{ inputs.version }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
MACOS_BUNDLES="${{ inputs.macos_bundles }}"
if [ -n "$MACOS_BUNDLES" ]; then
pnpm tauri build --target ${{ matrix.target }} --bundles "$MACOS_BUNDLES"
else
pnpm tauri build --target ${{ matrix.target }}
fi
- name: Upload .dmg
if: ${{ inputs.upload_macos_dmg }}
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
build-linux:
name: Build (linux-x86_64)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
fcitx5-frontend-gtk3 \
libfuse2 \
librsvg2-dev \
curl \
wget \
patchelf \
build-essential \
file \
rpm
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Set version
run: |
VERSION="${{ inputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Build Tauri app (Linux bundles)
env:
APPIMAGE_EXTRACT_AND_RUN: 1
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_RELEASE: ${{ inputs.version }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
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
- name: Validate Linux bundles
run: |
shopt -s nullglob
appimages=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
)
installers=(
"${appimages[@]}"
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
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#appimages[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage bundle."
exit 1
fi
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage, deb or rpm bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Linux bundles
uses: actions/upload-artifact@v4
with:
name: linux-x86_64-bundles
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
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
if-no-files-found: error
retention-days: 1
build-windows:
name: Build (windows-x86_64)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
src-tauri\target
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
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
- name: Clear cached Windows bundle artifacts
shell: pwsh
run: |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
- name: Set version
shell: pwsh
run: |
$version = "${{ inputs.version }}"
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
$tauri.version = $version
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
- name: Validate Windows release env
id: windows-signing
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }}
WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
REQUIRE_WINDOWS_AUTHENTICODE: ${{ inputs.require_windows_authenticode }}
run: |
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required to build signed Windows updater artifacts."
exit 1
fi
done
has_certificate=false
has_password=false
if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE" ] || [ -n "$WINDOWS_CERTIFICATE" ]; then
has_certificate=true
fi
if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD" ] || [ -n "$WINDOWS_CERTIFICATE_PASSWORD" ]; then
has_password=true
fi
if [ "$has_certificate" != "$has_password" ]; then
echo "::error::Windows Authenticode signing is partially configured. Set both certificate and password secrets, or remove both for unsigned alpha Windows artifacts."
exit 1
fi
if [ "$has_certificate" = "true" ]; then
echo "authenticode_available=true" >> "$GITHUB_OUTPUT"
elif [ "$REQUIRE_WINDOWS_AUTHENTICODE" = "true" ]; then
echo "::error::WINDOWS_CODE_SIGNING_CERTIFICATE or WINDOWS_CERTIFICATE is required to Authenticode-sign Windows installers."
exit 1
else
echo "::warning::Windows Authenticode certificate secrets are not configured. Building alpha Windows artifacts without Authenticode signatures; Tauri updater signatures are still required."
echo "authenticode_available=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare Windows Authenticode signing
if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }}
shell: pwsh
env:
WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }}
WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }}
WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT }}
WINDOWS_CODE_SIGNING_TIMESTAMP_URL: ${{ secrets.WINDOWS_CODE_SIGNING_TIMESTAMP_URL }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }}
run: ./.github/scripts/configure-windows-authenticode.ps1
- name: Build Tauri app (Windows bundles)
shell: pwsh
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_RELEASE: ${{ inputs.version }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
if ("${{ steps.windows-signing.outputs.authenticode_available }}" -eq "true") {
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --config src-tauri/tauri.windows-signing.conf.json
} else {
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
}
- name: Validate Windows Authenticode signatures
if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }}
shell: pwsh
run: |
$expectedThumbprint = $env:WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT
if ([string]::IsNullOrWhiteSpace($expectedThumbprint)) {
throw "WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT was not exported by the signing setup step."
}
$expectedThumbprint = ($expectedThumbprint -replace "\s", "").ToUpperInvariant()
$paths = @()
$paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "*.exe" -File -ErrorAction SilentlyContinue
$paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis" -Filter "*.exe" -File -ErrorAction SilentlyContinue
$paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi" -Filter "*.msi" -File -ErrorAction SilentlyContinue
$paths = @($paths | Sort-Object FullName -Unique)
if ($paths.Count -eq 0) {
throw "No Windows executable or installer artifacts found to verify."
}
foreach ($path in $paths) {
$signature = Get-AuthenticodeSignature -FilePath $path.FullName
if ($signature.Status -ne "Valid") {
throw "Invalid Authenticode signature for $($path.FullName): $($signature.Status)"
}
if ($null -eq $signature.SignerCertificate) {
throw "Missing signer certificate for $($path.FullName)."
}
$actualThumbprint = ($signature.SignerCertificate.Thumbprint -replace "\s", "").ToUpperInvariant()
if ($actualThumbprint -ne $expectedThumbprint) {
throw "Unexpected signer thumbprint for $($path.FullName): $actualThumbprint"
}
Write-Host "Authenticode signature OK: $($path.FullName)"
}
- name: Validate Windows bundles
shell: bash
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
)
signatures=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Windows build produced no installable NSIS or MSI bundle."
exit 1
fi
for installer in "${installers[@]}"; do
if [[ "$(basename "$installer")" != *"${{ inputs.version }}"* ]]; then
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
exit 1
fi
done
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Windows build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Windows bundles
uses: actions/upload-artifact@v4
with:
name: windows-x86_64-bundles
path: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
if-no-files-found: error
retention-days: 1

View File

@@ -4,10 +4,7 @@ on:
push:
tags:
- 'stable-v*'
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
- 'v20*'
concurrency:
group: release-stable-${{ github.ref }}
@@ -34,14 +31,24 @@ jobs:
from datetime import date
tag = os.environ["GITHUB_REF_NAME"]
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}")
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}")
date(*map(int, match.groups()))
print(f"version={version}")
print(f"display_version={version}")
print(f"display_version={display_version}")
print(f"tag={tag}")
PY
@@ -49,446 +56,32 @@ jobs:
DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
echo "### Stable version: \`$DISPLAY_VERSION\`" >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Phase 2: Build release bundles in parallel
# ─────────────────────────────────────────────────────────────
build:
name: Build (${{ matrix.arch }})
# -------------------------------------------------------------
# Phase 2: Build shared release artifacts
# -------------------------------------------------------------
build-artifacts:
name: Build release artifacts
needs: version
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
steps:
- 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 Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/${{ matrix.target }}/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Validate telemetry env
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
python3 <<'PY'
import os
import re
import sys
from urllib.parse import urlparse
DISALLOWED_PLACEHOLDERS = {
"",
"-",
"_",
"false",
"true",
"null",
"undefined",
"none",
"disabled",
}
def normalize(name: str) -> str:
value = os.getenv(name, "").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1].strip()
return value
def normalize_http_like(value: str) -> str:
if "://" in value:
return value
return f"https://{value}"
def normalize_hostname(hostname: str) -> str:
normalized = hostname.strip().rstrip('.').lower()
if normalized.startswith('[') and normalized.endswith(']'):
normalized = normalized[1:-1]
return normalized
def is_ip_address(hostname: str) -> bool:
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
def is_allowed_hostname(hostname: str) -> bool:
normalized = normalize_hostname(hostname)
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
return False
if normalized == 'localhost':
return True
return '.' in normalized or is_ip_address(normalized)
def is_http_url(value: str) -> bool:
parsed = urlparse(normalize_http_like(value))
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
values = {
name: normalize(name)
for name in (
"VITE_SENTRY_DSN",
"SENTRY_DSN",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
)
}
errors = []
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
value = values[name]
if value.lower() in DISALLOWED_PLACEHOLDERS:
errors.append(f"{name} must be set to a real value, not a placeholder")
elif not is_http_url(value):
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
if errors:
print("Telemetry env validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)
print("Telemetry env validation passed.")
PY
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target ${{ matrix.target }}
- name: Upload .dmg
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
build-linux:
name: Build (linux-x86_64)
needs: version
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
curl \
wget \
patchelf \
build-essential \
file
- 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 Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Build Tauri app (Linux bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
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,appimage
- name: Validate Linux bundles
run: |
shopt -s nullglob
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
)
signatures=(
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.sig
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 or deb bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Linux bundles
uses: actions/upload-artifact@v4
with:
name: linux-x86_64-bundles
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/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
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
if-no-files-found: error
retention-days: 1
build-windows:
name: Build (windows-x86_64)
needs: version
runs-on: windows-latest
steps:
- 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 Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
src-tauri\target
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached Windows bundle artifacts
shell: pwsh
run: |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
- name: Set version
shell: pwsh
run: |
$version = "${{ needs.version.outputs.version }}"
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
$tauri.version = $version
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
- name: Validate Windows release env
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: |
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required to build signed Windows updater artifacts."
exit 1
fi
done
- name: Build Tauri app (Windows bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
- name: Validate Windows bundles
shell: bash
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
)
signatures=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Windows build produced no installable NSIS or MSI bundle."
exit 1
fi
for installer in "${installers[@]}"; do
if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
exit 1
fi
done
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Windows build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Windows bundles
uses: actions/upload-artifact@v4
with:
name: windows-x86_64-bundles
path: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
if-no-files-found: error
retention-days: 1
uses: ./.github/workflows/release-build-artifacts.yml
with:
version: ${{ needs.version.outputs.version }}
macos_bundles: ""
upload_macos_dmg: true
# One-time stable promotion exceptions while Windows certificate secrets
# are still being provisioned. Future stable tags must keep Authenticode.
require_windows_authenticode: ${{ !contains(fromJson('["v2026-06-01","v2026-06-06"]'), needs.version.outputs.tag) }}
secrets: inherit
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release (stable)
needs: [version, build, build-linux, build-windows]
needs: [version, build-artifacts]
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
steps:
- uses: actions/checkout@v4
with:
@@ -497,26 +90,81 @@ jobs:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Normalize macOS release artifact names
run: |
normalize_macos_artifacts() {
local arch="$1"
local normalized_updater="$2"
local normalized_dmg="$3"
local updater_dir="updater-${arch}"
local updater_file
updater_file=$(find "$updater_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
if [ -z "$updater_file" ]; then
echo "::error::Missing macOS updater artifact in ${updater_dir}" >&2
return 1
fi
local sig_file="${updater_file}.sig"
if [ ! -f "$sig_file" ]; then
echo "::error::Missing macOS updater signature for ${updater_file}" >&2
return 1
fi
local normalized_sig="${normalized_updater}.sig"
if [ "$updater_file" != "$normalized_updater" ]; then
mv "$updater_file" "$normalized_updater"
fi
if [ "$sig_file" != "$normalized_sig" ]; then
mv "$sig_file" "$normalized_sig"
fi
local dmg_dir="dmg-${arch}"
local dmg_file
dmg_file=$(find "$dmg_dir" -maxdepth 1 -name "*.dmg" -print -quit)
if [ -z "$dmg_file" ]; then
echo "::error::Missing macOS DMG artifact in ${dmg_dir}" >&2
return 1
fi
if [ "$dmg_file" != "$normalized_dmg" ]; then
mv "$dmg_file" "$normalized_dmg"
fi
}
normalize_macos_artifacts aarch64 \
"updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" \
"dmg-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.dmg"
normalize_macos_artifacts x86_64 \
"updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" \
"dmg-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.dmg"
- name: Generate release notes
run: |
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)
NOTES_FILE="release-notes/${{ needs.version.outputs.tag }}.md"
if [ -f "$NOTES_FILE" ]; then
cat "$NOTES_FILE" > release_notes.md
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}")
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
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\`**"
echo ""
echo "**Includes macOS (Apple Silicon), Windows x64, and Linux x64 bundles**"
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: |
@@ -545,6 +193,12 @@ jobs:
ARM_TARBALL=$(basename "$ARM_UPDATER_FILE")
ARM_DMG=$(basename "$(find_required "dmg-aarch64/*.dmg")")
INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
INTEL_SIG=$(cat "$INTEL_SIG_FILE")
INTEL_TARBALL=$(basename "$INTEL_UPDATER_FILE")
INTEL_DMG=$(basename "$(find_required "dmg-x86_64/*.dmg")")
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
@@ -568,6 +222,11 @@ jobs:
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}",
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_DMG}"
},
"darwin-x86_64": {
"signature": "${INTEL_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_TARBALL}",
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_DMG}"
},
"linux-x86_64": {
"signature": "${LINUX_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
@@ -584,7 +243,7 @@ jobs:
echo "stable-latest.json:"; cat stable-latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Tolaria ${{ needs.version.outputs.display_version }}
@@ -595,14 +254,19 @@ jobs:
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
dmg-x86_64/*.dmg
updater-x86_64/*.app.tar.gz
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
@@ -622,46 +286,18 @@ jobs:
stable-latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages
# Phase 4: Trigger the main-branch GitHub Pages deployment
# ─────────────────────────────────────────────────────────────
pages:
name: Update release history page
name: Update docs and release pages
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: github-pages
cancel-in-progress: false
actions: write
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build release history page
- name: Dispatch docs deployment from main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site/alpha _site/stable
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#*/}"
curl -fsSL "${PAGES_URL}/alpha/latest.json" -o _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html
mkdir -p _site/download
cp _site/stable/download/index.html _site/download/index.html
cp _site/alpha/latest.json _site/latest.json
cp _site/alpha/latest.json _site/latest-canary.json
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
commit_message: "Update release history for ${{ needs.version.outputs.tag }}"
gh workflow run deploy-docs.yml --repo ${{ github.repository }} --ref main
echo "Triggered deploy-docs.yml on main after publishing ${{ needs.version.outputs.tag }}."

View File

@@ -4,10 +4,11 @@ on:
push:
branches:
- main
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
paths-ignore:
- ".husky/**"
- ".github/workflows/deploy-docs.yml"
- ".github/workflows/release.yml"
- "site/**"
concurrency:
group: release-alpha-${{ github.ref }}
@@ -69,10 +70,18 @@ jobs:
else:
today = datetime.now(timezone.utc).date()
stable_date = None
stable_pattern = re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$")
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})$"),
)
for stable_tag in lines(["git", "tag", "--list", "stable-v*", "--sort=-version:refname"]):
match = stable_pattern.fullmatch(stable_tag)
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)
if not match:
continue
@@ -107,442 +116,31 @@ jobs:
DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
echo "### Alpha version: \`$DISPLAY_VERSION\` (\`$VERSION\`)" >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Phase 2: Build each architecture in parallel
# tauri build handles signing automatically via env vars
# ─────────────────────────────────────────────────────────────
build:
name: Build (${{ matrix.arch }})
# -------------------------------------------------------------
# Phase 2: Build shared release artifacts
# -------------------------------------------------------------
build-artifacts:
name: Build release artifacts
needs: version
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
uses: ./.github/workflows/release-build-artifacts.yml
with:
version: ${{ needs.version.outputs.version }}
macos_bundles: app
upload_macos_dmg: false
require_windows_authenticode: false
secrets: inherit
- 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 Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/${{ matrix.target }}/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Validate telemetry env
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
python3 <<'PY'
import os
import re
import sys
from urllib.parse import urlparse
DISALLOWED_PLACEHOLDERS = {
"",
"-",
"_",
"false",
"true",
"null",
"undefined",
"none",
"disabled",
}
def normalize(name: str) -> str:
value = os.getenv(name, "").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1].strip()
return value
def normalize_http_like(value: str) -> str:
if "://" in value:
return value
return f"https://{value}"
def normalize_hostname(hostname: str) -> str:
normalized = hostname.strip().rstrip('.').lower()
if normalized.startswith('[') and normalized.endswith(']'):
normalized = normalized[1:-1]
return normalized
def is_ip_address(hostname: str) -> bool:
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
def is_allowed_hostname(hostname: str) -> bool:
normalized = normalize_hostname(hostname)
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
return False
if normalized == 'localhost':
return True
return '.' in normalized or is_ip_address(normalized)
def is_http_url(value: str) -> bool:
parsed = urlparse(normalize_http_like(value))
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
values = {
name: normalize(name)
for name in (
"VITE_SENTRY_DSN",
"SENTRY_DSN",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
)
}
errors = []
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
value = values[name]
if value.lower() in DISALLOWED_PLACEHOLDERS:
errors.append(f"{name} must be set to a real value, not a placeholder")
elif not is_http_url(value):
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
if errors:
print("Telemetry env validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)
print("Telemetry env validation passed.")
PY
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
# Alpha releases only need the notarized app bundle and updater tarball.
# Skipping DMG packaging avoids fragile bundle_dmg.sh failures on macOS runners.
pnpm tauri build --target ${{ matrix.target }} --bundles app
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
build-linux:
name: Build (linux-x86_64)
needs: version
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
curl \
wget \
patchelf \
build-essential \
file
- 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 Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Build Tauri app (Linux bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
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,appimage
- name: Validate Linux bundles
run: |
shopt -s nullglob
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
)
signatures=(
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.sig
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 or deb bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Linux bundles
uses: actions/upload-artifact@v4
with:
name: linux-x86_64-bundles
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/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
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
if-no-files-found: error
retention-days: 1
build-windows:
name: Build (windows-x86_64)
needs: version
runs-on: windows-latest
steps:
- 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 Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
src-tauri\target
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached Windows bundle artifacts
shell: pwsh
run: |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
- name: Set version
shell: pwsh
run: |
$version = "${{ needs.version.outputs.version }}"
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
$tauri.version = $version
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
- name: Validate Windows release env
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: |
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required to build signed Windows updater artifacts."
exit 1
fi
done
- name: Build Tauri app (Windows bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
- name: Validate Windows bundles
shell: bash
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
)
signatures=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Windows build produced no installable NSIS or MSI bundle."
exit 1
fi
for installer in "${installers[@]}"; do
if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
exit 1
fi
done
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Windows build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Windows bundles
uses: actions/upload-artifact@v4
with:
name: windows-x86_64-bundles
path: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
if-no-files-found: error
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release
# No lipo/re-signing — use the per-arch artifacts directly
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release (alpha)
needs: [version, build, build-linux, build-windows]
needs: [version, build-artifacts]
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
steps:
- uses: actions/checkout@v4
with:
@@ -551,6 +149,37 @@ jobs:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Normalize macOS updater artifact names
run: |
normalize_updater() {
local arch="$1"
local normalized_updater="$2"
local artifact_dir="updater-${arch}"
local updater_file
updater_file=$(find "$artifact_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
if [ -z "$updater_file" ]; then
echo "::error::Missing macOS updater artifact in ${artifact_dir}" >&2
return 1
fi
local sig_file="${updater_file}.sig"
if [ ! -f "$sig_file" ]; then
echo "::error::Missing macOS updater signature for ${updater_file}" >&2
return 1
fi
local normalized_sig="${normalized_updater}.sig"
if [ "$updater_file" != "$normalized_updater" ]; then
mv "$updater_file" "$normalized_updater"
fi
if [ "$sig_file" != "$normalized_sig" ]; then
mv "$sig_file" "$normalized_sig"
fi
}
normalize_updater aarch64 "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz"
normalize_updater x86_64 "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz"
- name: Generate release notes
run: |
PREV_TAG=$(python3 <<'PY'
@@ -587,7 +216,7 @@ jobs:
echo "---"
echo "**Alpha build — updated on every push to \`main\`**"
echo ""
echo "**Includes macOS (Apple Silicon), Linux x64, and Windows x64 bundles**"
echo "**Includes macOS (Apple Silicon and Intel), Linux x64, and Windows x64 bundles**"
echo ""
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
@@ -616,6 +245,11 @@ jobs:
ARM_SIG=$(cat "$ARM_SIG_FILE")
ARM_UPDATER=$(basename "$ARM_UPDATER_FILE")
INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
INTEL_SIG=$(cat "$INTEL_SIG_FILE")
INTEL_UPDATER=$(basename "$INTEL_UPDATER_FILE")
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
@@ -639,6 +273,11 @@ jobs:
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}"
},
"darwin-x86_64": {
"signature": "${INTEL_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}"
},
"linux-x86_64": {
"signature": "${LINUX_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
@@ -655,7 +294,7 @@ jobs:
echo "alpha-latest.json:"; cat alpha-latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Tolaria ${{ needs.version.outputs.display_version }}
@@ -665,14 +304,18 @@ jobs:
files: |
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
updater-x86_64/*.app.tar.gz
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
@@ -692,46 +335,77 @@ jobs:
alpha-latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages with release history
# Phase 4: Update GitHub Pages with docs, release history, and download assets
# ─────────────────────────────────────────────────────────────
pages:
name: Update release history page
name: Update docs and release pages
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: latest
- name: Build release history page
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build docs and release pages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site/alpha _site/stable
VITEPRESS_BASE="/" pnpm docs:build
mkdir -p _site/alpha _site/stable _site/release-notes
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#*/}"
STABLE_TAG=$(gh release list --repo ${{ github.repository }} --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // ""')
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
curl -fsSL "${PAGES_URL}/stable/latest.json" -o _site/stable/latest.json || echo '{}' > _site/stable/latest.json
if [ -n "$STABLE_TAG" ]; then
gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json
else
echo '{}' > _site/stable/latest.json
fi
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/releases/index.html
mkdir -p _site/download
cp _site/stable/download/index.html _site/download/index.html
cp _site/alpha/latest.json _site/latest.json
cp _site/alpha/latest.json _site/latest-canary.json
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
commit_message: "Update release history for ${{ needs.version.outputs.tag }}"
path: ./_site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

9
.gitignore vendored
View File

@@ -10,6 +10,9 @@ lerna-debug.log*
node_modules
dist
dist-ssr
site/.vitepress/cache/
site/.vitepress/dist/
_site/
*.local
# Editor directories and files
@@ -42,7 +45,7 @@ final_selection.py
src-tauri/target
# Generated mcp-server bundle (built by scripts/bundle-mcp-server.mjs)
src-tauri/resources/
src-tauri/resources/mcp-server/
# Python cache
__pycache__/
@@ -70,5 +73,9 @@ CODE-HEALTH-REPORT.md
*.key.pub
# Local environment variables (never commit)
.env
.env.local
.env.*.local
# Local Codacy CLI runtime/config generated by the MCP server
.codacy/

View File

@@ -40,8 +40,36 @@ 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=$(git diff --cached --name-only | grep -E '\.(ts|tsx)$' || true)
STAGED_TS=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx)$' || true)
if [ -n "$STAGED_TS" ]; then
echo " → lint + tsc..."
pnpm lint --quiet

View File

@@ -28,6 +28,23 @@
# ─────────────────────────────────────────────────────────────────────────
set -e
ensure_cargo_tooling() {
if command -v cargo >/dev/null 2>&1; then
return 0
fi
if [ -s "$HOME/.cargo/env" ]; then
# shellcheck disable=SC1091
. "$HOME/.cargo/env"
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "❌ cargo must be available before pushing"
echo " Install Rust via https://rustup.rs or ensure ~/.cargo/bin is in PATH."
exit 1
fi
}
ensure_node_tooling() {
if command -v node >/dev/null 2>&1 && command -v pnpm >/dev/null 2>&1; then
return 0
@@ -85,6 +102,7 @@ else
fi
require_main_push
ensure_node_tooling
ensure_cargo_tooling
START_TIME=$(date +%s)
@@ -95,47 +113,83 @@ 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
# ── 0. TypeScript + Vite build ──────────────────────────────────────────
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. Frontend lint ───────────────────────────────────────────────────
echo ""
echo "📦 [0/5] TypeScript + Vite build..."
pnpm exec tsc --noEmit
echo "🔎 [0/6] Frontend lint..."
pnpm lint
echo " ✅ Lint OK"
# ── 1. TypeScript + Vite build ──────────────────────────────────────────
echo ""
echo "📦 [1/6] TypeScript + Vite build..."
pnpm build
echo " ✅ Build OK"
# ── 1. Frontend coverage (≥70%) — includes all unit tests ───────────────
# ── 2. Frontend coverage (≥70%) — includes all unit tests ───────────────
echo ""
echo "📊 [1/5] Frontend tests + coverage (≥70%)..."
echo "📊 [2/6] Frontend tests + coverage (≥70%)..."
pnpm test:coverage --silent
echo " ✅ Frontend coverage OK"
# ── 2. Rust lint (clippy + fmt) — fast, run before coverage ─────────────
# ── 3. Rust lint (clippy + fmt) — fast, run before coverage ─────────────
echo ""
if [ "$RUST_CHANGED" = true ]; then
echo "🔧 [2/5] Clippy + rustfmt..."
echo "🔧 [3/6] Clippy + rustfmt..."
cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings
cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
echo " ✅ Rust lint OK"
else
echo "⏭️ [2/5] Rust lint — skipped (no src-tauri/ changes)"
echo "⏭️ [3/6] Rust lint — skipped (no src-tauri/ changes)"
fi
# ── 3. Rust coverage (≥85% lines) ──────────────────────────────────────
# ── 4. Rust coverage (≥85% lines) ──────────────────────────────────────
echo ""
if [ "$RUST_CHANGED" = true ]; then
LLVM_COV_FLAGS="--no-clean"
if [ "${LAPUTA_FULL_COVERAGE:-0}" = "1" ]; then
LLVM_COV_FLAGS=""
echo "🦀 [3/5] Rust coverage — FULL (LAPUTA_FULL_COVERAGE=1)..."
echo "🦀 [4/6] Rust coverage — FULL (LAPUTA_FULL_COVERAGE=1)..."
else
echo "🦀 [3/5] Rust coverage (≥85%, incremental)..."
echo "🦀 [4/6] Rust coverage (≥85%, incremental)..."
fi
# Unset GIT_DIR so git tests create isolated repos without inheriting hook context
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
@@ -148,24 +202,24 @@ if [ "$RUST_CHANGED" = true ]; then
-- --test-threads=1
echo " ✅ Rust coverage OK"
else
echo "⏭️ [3/5] Rust coverage — skipped (no src-tauri/ changes)"
echo "⏭️ [4/6] Rust coverage — skipped (no src-tauri/ changes)"
fi
# ── 4. Playwright core smoke lane (if any exist) ──────────────────────
# ── 5. Playwright core smoke lane (if any exist) ──────────────────────
echo ""
SMOKE_FILES=$(find tests/smoke tests/integration -name '*.spec.ts' 2>/dev/null | head -1)
if [ -n "$SMOKE_FILES" ]; then
echo "🎭 [4/5] Playwright core smoke tests..."
echo "🎭 [5/6] Playwright core smoke tests..."
if ! pnpm playwright:smoke; then
echo " ❌ Core smoke tests FAILED"
exit 1
fi
echo " ✅ Core smoke tests OK"
else
echo "⏭️ [4/5] Playwright core smoke tests — skipped (no tests/**/*.spec.ts)"
echo "⏭️ [5/6] Playwright core smoke tests — skipped (no tests/**/*.spec.ts)"
fi
# ── 5. CodeScene code health gate (ratchet) ──────────────────────────────
# ── 6. CodeScene code health gate (ratchet) ──────────────────────────────
# Thresholds live in .codescene-thresholds and only ever go UP (ratchet).
# If remote scores improved, the hook updates the file and stops so the new
# floor is committed with normal verified hooks before the next push.
@@ -181,7 +235,7 @@ if [ -f "$THRESHOLDS_FILE" ]; then
fi
echo ""
echo "🏥 [5/5] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..."
echo "🏥 [6/6] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..."
if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then
echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping"
else

10
.semgrepignore Normal file
View File

@@ -0,0 +1,10 @@
tests/
e2e/
node_modules/
dist/
coverage/
test-results/
src-tauri/target/
target/
src-tauri/gen/apple/assets/mcp-server/index.js
src-tauri/gen/apple/assets/mcp-server/ws-bridge.js

158
AGENTS.md
View File

@@ -1,30 +1,88 @@
# AGENTS.md — Tolaria App
> Quick links: [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
## 1. Development Process
---
## 1. Task Workflow
### 1a. Pick up a task
Run `/laputa-next-task` — fetches next task (To Rework first, then Open), moves to In Progress, returns full description.
### Start working on a task
**Before writing a single line of code:** run `mcp__codescene__code_health_score` to check the current codebase health against `.codescene-thresholds`. If the score is already below the threshold, **stop and refactor first** — find the worst files with the MCP, improve them, commit, then start the task. Never start feature work on a codebase that is already below the gate.
- Read task description and all comments fully
- For To Rework: the ❌ QA failed comment tells you exactly what to fix
- Check `docs/adr/` for relevant architecture decisions before structural choices
- Add a comment: `🚀 Starting work on this task. [Brief description of approach]`
- Check `docs/ARCHITECTURE.md` and `docs/ABSTRACTIONS.md` for relevant structural information
- For UI tasks: study app visual language and components first. Prioritize reusing existing components, assets, and variables over recreating them.
- If working on a Todoist task, add a comment: `🚀 Starting work on this task. [Brief description of approach]`
### 1b. Implement
### Commits & pushes
- Work on `main` branch — **no branches, no PRs, ever**. Pre-commit and pre-push block work from any other branch.
- Push directly to `main` — no PRs, no branches. Pre-push blocks non-`main` pushes.
- 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
- Pre-push hook runs full check suite (build + tests + core Playwright smoke + CodeScene)
- **A task is NOT done until `git push origin main` succeeds.** If the hook blocks: read the error, fix it (clippy, tests, CodeScene, build), commit the fix, push again. **⛔ NEVER use --no-verify**
### 1c. When done
### TDD (mandatory)
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes.
**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`.
**Release rule:** CodeScene is a before/after gate, not just a final score. Every task must record the starting CodeScene state before edits and the final state after edits. If touched code gets worse, refactor before committing.
**⛔ NEVER edit `.codescene-thresholds` to lower the values.** If the gate blocks you, improve the code — do not lower the bar.
**CodeScene access order:** use CodeScene MCP tools if available. If MCP is unavailable, use the installed `cs` CLI for file-level review/delta work, and use the CodeScene API (`CODESCENE_PAT` + `CODESCENE_PROJECT_ID`) for project-wide Hotspot/Average threshold checks from `.codescene-thresholds`.
**Before editing any existing code file:** capture its current file-level CodeScene score. After your edits, re-run the same file-level review and verify the score is higher. If the file already starts at `10.0`, it must remain `10.0`.
**New files:** every new **scorable code file** must reach CodeScene score `10.0` before commit. If CodeScene reports `null` / "no scorable code" for a new file, it must still have zero CodeScene findings/warnings.
**Before every commit:** run CodeScene file-level review on every touched or newly created code file and verify the rule above. **Boy Scout Rule:** every file you touch must leave with a higher score, unless it was already `10.0`, in which case it must stay `10.0`.
**If CodeScene gate blocks your push:** use `mcp__codescene__code_health_score` to find the worst file, refactor it, commit, push again. Do NOT stop or wait for laputa-refactor — that is a background loop, not a substitute for fixing your own regressions.
### Security scan with Codacy (mandatory)
Use Codacy as a security and static-analysis gate before a task is considered releasable.
- Prefer the Codacy MCP inside Codex to inspect repository/file issues for every touched code file.
- If MCP is unavailable, use the local CLI wrapper, e.g. `.codacy/cli.sh analyze <path> --format sarif`; choose the relevant tool when useful (`eslint`, `opengrep`, `trivy`, `lizard`).
- **Always fix Critical and High severity findings introduced by your change.** Do not move the task to In Review with new Critical/High Codacy issues.
- Review Medium findings. Fix them when they are real defects or security-sensitive; otherwise explain why they are acceptable in the completion comment.
- Never silence a Codacy rule just to pass the scan. Prefer small code changes that remove the finding.
### Check suite (runs on every push)
```bash
pnpm lint && npx tsc --noEmit && pnpm test && pnpm test:coverage # frontend ≥70%
cargo test && cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
```
Coverage is a release gate, not a vanity metric:
- Frontend coverage must stay ≥70%.
- Rust line coverage must stay ≥85%.
- For bug fixes, add a regression test when practical.
- For new behavior, add targeted coverage close to the changed code; do not rely only on broad E2E coverage.
### UI and native QA
**Phase 1 — Playwright (only for core user flows):**
@@ -45,55 +103,26 @@ bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh laputa
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.
Use computer-use/browser-control style interaction for native UI QA when available: click, hover, drag, select, scroll, and type the way a real user would with the mouse and trackpad. For every UI feature, test the primary mouse-driven path first, then verify any relevant keyboard shortcut or keyboard-first workflow still works. Tolaria is still a keyboard-first app, but QA must not assume users only interact by keyboard.
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
Use `osascript` for app focus, keyboard shortcuts, and keyboard-specific checks. **⚠️ WKWebView:** `osascript keystroke` can be blocked inside editor content — use computer use for native editor interaction when possible, and rely on Playwright for deterministic text-input coverage. Write result as Todoist comment (✅ or ❌).
Then run `/laputa-done <task_id>` → moves to In Review, notifies Brian, self-dispatches next task.
### Release-readiness checklist
---
Before pushing or moving a task to In Review, verify the release gates and add a **completion comment** to the Todoist task. The comment must include:
## 2. Development Process
### Commits & pushes
- Push directly to `main` — no PRs, no branches. Pre-push blocks non-`main` pushes.
- Pre-push hook runs full check suite (build + tests + core Playwright smoke + CodeScene)
- **A task is NOT done until `git push origin main` succeeds.** If the hook blocks: read the error, fix it (clippy, tests, CodeScene, build), commit the fix, push again. **⛔ NEVER use --no-verify**
### TDD (mandatory)
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes.
**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.
### 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`.
**⛔ NEVER edit `.codescene-thresholds` to lower the values.** If the gate blocks you, improve the code — do not lower the bar.
**CodeScene access order:** use CodeScene MCP tools if available. If MCP is unavailable, use the installed `cs` CLI for file-level review/delta work, and use the CodeScene API (`CODESCENE_PAT` + `CODESCENE_PROJECT_ID`) for project-wide Hotspot/Average threshold checks from `.codescene-thresholds`.
**Before editing any existing code file:** capture its current file-level CodeScene score. After your edits, re-run the same file-level review and verify the score is higher. If the file already starts at `10.0`, it must remain `10.0`.
**New files:** every new **scorable code file** must reach CodeScene score `10.0` before commit. If CodeScene reports `null` / "no scorable code" for a new file, it must still have zero CodeScene findings/warnings.
**Before every commit:** run CodeScene file-level review on every touched or newly created code file and verify the rule above. **Boy Scout Rule:** every file you touch must leave with a higher score, unless it was already `10.0`, in which case it must stay `10.0`.
**If CodeScene gate blocks your push:** use `mcp__codescene__code_health_score` to find the worst file, refactor it, commit, push again. Do NOT stop or wait for laputa-refactor — that is a background loop, not a substitute for fixing your own regressions.
### Check suite (runs on every push)
```bash
pnpm lint && npx tsc --noEmit && pnpm test && pnpm test:coverage # frontend ≥70%
cargo test && cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
```
- What was implemented (a few lines covering logic and UX/UI).
- QA: what was tested and how (Playwright / native screenshot / osascript).
- Tests/coverage: commands run and final coverage result.
- CodeScene: before/after touched-file checks plus final Hotspot and Average scores after push; final scores must pass `.codescene-thresholds`.
- Coverage commands passed (`pnpm test:coverage` and `cargo llvm-cov ... --fail-under-lines 85`) or the change is docs-only.
- Codacy: MCP/CLI scan summary; confirm no new Critical/High findings.
- Localization: any user-facing copy lives in `src/lib/locales/en.json`, `pnpm l10n:translate` was run, and `pnpm l10n:validate` passes. If no copy changed, say “Localization: no UI copy changes”.
- PostHog: meaningful new user actions/events are instrumented with safe metadata; noisy/minor changes explicitly say “PostHog: no event needed because …”.
- 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".
- Demo vault dirt checked: `git status --short -- demo-vault demo-vault-v2` is empty unless fixture changes are intentional.
### ADRs & docs
@@ -103,7 +132,7 @@ After any Tauri command, new component/hook, data model change, or new integrati
---
## 3. Product Rules
## 2. Product Rules
### Demo vault hygiene (`demo-vault/`, `demo-vault-v2/`)
@@ -122,10 +151,6 @@ 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:
@@ -147,7 +172,7 @@ Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task
---
## 4. Reference
## 3. Reference
### macOS / Tauri gotchas
@@ -155,10 +180,11 @@ Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task
- 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 laputa
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh Tolaria
bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/tolaria-qa/scripts/shortcut.sh "command" "s"
```

View File

@@ -1,3 +1,8 @@
---
type: Note
_organized: true
---
@AGENTS.md
This file is a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`.
This file is only a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`.

8
GEMINI.md Normal file
View File

@@ -0,0 +1,8 @@
---
type: Note
_organized: true
---
@AGENTS.md
This file is only a Gemini CLI compatibility shim. Keep shared agent instructions in `AGENTS.md`.

View File

@@ -1,8 +1,8 @@
![Latest stable](https://img.shields.io/github/v/release/refactoringhq/tolaria?display_name=tag) [![CI](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [![Build](https://github.com/refactoringhq/tolaria/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/refactoringhq/tolaria/actions/workflows/release.yml) [![Codecov](https://codecov.io/gh/refactoringhq/tolaria/graph/badge.svg?branch=main)](https://codecov.io/gh/refactoringhq/tolaria) [![CodeScene Hotspot Code Health](https://codescene.io/projects/76865/status-badges/hotspot-code-health)](https://codescene.io/projects/76865)
![Latest stable](https://img.shields.io/github/v/release/refactoringhq/tolaria?display_name=tag) [![CI](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [![Codecov](https://codecov.io/gh/refactoringhq/tolaria/graph/badge.svg?branch=main)](https://codecov.io/gh/refactoringhq/tolaria) [![CodeScene Hotspot Code Health](https://codescene.io/projects/76865/status-badges/hotspot-code-health)](https://codescene.io/projects/76865)
# 💧 Tolaria
Tolaria is a desktop app for Mac and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases:
Tolaria is a desktop app for macOS, Windows, 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,16 +27,30 @@ 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 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.
- 🪄**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.
- ⌨️ **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. Windows installers are Authenticode-signed; company-managed devices may still require IT approval of the Tolaria publisher before first install.
## Getting started
Download the [latest release here](https://github.com/refactoringhq/tolaria/releases/latest/download/Tolaria.app.tar.gz).
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).
## Open source and local setup
Tolaria is open source and built with Tauri, React, and TypeScript. If you want to run or contribute to the app locally, here is [how to get started](https://github.com/refactoringhq/tolaria/blob/main/docs/GETTING-STARTED.md). You can also find the gist below 👇

View File

@@ -16,7 +16,7 @@ We currently support security fixes for:
## Reporting a vulnerability
Please email **luca@refactoring.club** with the subject line **`[Tolaria Security]`**.
Please use GitHub's private vulnerability reporting flow for this repository.
Include as much of the following as you can:

38
biome.json Normal file
View File

@@ -0,0 +1,38 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"files": {
"includes": [
"**",
"!src-tauri/gen/**",
"!target/**",
"!dist/**",
"!node_modules/**"
]
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"overrides": [
{
"includes": ["site/**/*.vue"],
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "off",
"noUnusedVariables": "off",
"useHookAtTopLevel": "off"
}
}
}
}
],
"linter": {
"rules": {
"correctness": {
"useQwikValidLexicalScope": "off"
}
}
}
}

View File

@@ -17,5 +17,5 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
"iconLibrary": "phosphor"
}

View File

@@ -49,7 +49,13 @@
"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

@@ -0,0 +1,17 @@
---
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 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.
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.
The full set of design principles is documented in [ARCHITECTURE.md](./ARCHITECTURE.md#design-principles).
@@ -47,6 +47,7 @@ _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.
@@ -57,6 +58,19 @@ The frontmatter parser (Rust: `vault/mod.rs`, TS: `utils/frontmatter.ts`) must f
All data lives in markdown files with YAML frontmatter. There is no database — the filesystem is the source of truth.
### Vault Git Capability
Git is a per-vault capability, not a prerequisite for the document model. A vault can be:
| State | Meaning | UI behavior |
|---|---|---|
| Git-backed | The vault path contains a Git repository | History, changes, commits, sync, conflict resolution, remotes, AutoGit, and auto-sync are available according to remote/config state |
| Non-git | The vault path is a plain folder | Markdown scanning, editing, search, and navigation work; Git-dependent status-bar controls and command-palette entries are replaced by `Git disabled` + `Initialize Git for Current Vault` |
Plain folders become Git-backed only when the user explicitly runs Git initialization from the setup dialog, status bar, or command palette. The setup dialog supports "not now" for a one-time dismissal and "never for this vault" for a local per-vault opt-out from future automatic prompts. Features that depend on Git must check both the vault capability and the installation-local `git_enabled` setting instead of assuming every vault has `.git` or that Git chrome is globally visible.
Git initialization is intentionally scoped to dedicated vault folders. When the current non-git folder looks like a broad personal root such as Documents, Desktop, or Downloads and does not already carry Tolaria-managed vault markers, `init_git_repo` refuses to run Git and asks the user to select or create a dedicated subfolder instead.
### VaultEntry
The core data type representing a single note, defined in Rust (`src-tauri/src/vault/mod.rs`) and TypeScript (`src/types.ts`).
@@ -74,14 +88,16 @@ classDiagram
+Record~string,string[]~ relationships
+String[] outgoingLinks
+String? status
+String? noteWidth
+Number? modifiedAt
+Number? createdAt
+Number wordCount
+String? snippet
+Boolean archived
+WorkspaceIdentity? workspace
+Boolean trashed ⚠ legacy
+Number? trashedAt ⚠ legacy
+Record~string,string~ properties
+Record~string,VaultPropertyValue~ properties
}
class TypeDocument {
@@ -122,19 +138,79 @@ 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)
fileSize: number
wordCount: number | null // Body word count (excludes frontmatter)
snippet: string | null // First 200 chars of body
workspace?: WorkspaceIdentity // Mounted-workspace provenance for cross-vault graph entries
archived: boolean // Archived flag
trashed: boolean // Kept for backward compatibility (Trash system removed — delete is permanent)
trashedAt: number | null // Kept for backward compatibility (Trash system removed)
properties: Record<string, string> // Scalar frontmatter fields (custom properties)
properties: Record<string, VaultPropertyValue> // Scalar and scalar-array custom properties
fileKind?: 'markdown' | 'text' | 'binary' // Controls editor/raw/preview behavior
}
```
### WorkspaceIdentity
Mounted workspace provenance is renderer-owned metadata attached to `VaultEntry.workspace` when entries are loaded through the registered workspace set. It is not parsed from note frontmatter and is not written into vault files.
```typescript
interface WorkspaceIdentity {
id: string
label: string
alias: string // Stable prefix used in cross-workspace wikilinks
path: string // Absolute workspace root
shortLabel: string // Compact note-list badge text
color: string | null
icon: string | null
mounted: boolean
available: boolean
defaultForNewNotes: boolean
}
```
The status-bar workspace manager edits installation-local identity and mount state. The alias is the durable user-facing namespace for cross-workspace links such as `[[team/projects/alpha]]`; labels and colors are display affordances only. The default workspace controls where new notes and Type files are created; it is not a claim that only one vault is active. When multiple workspaces are enabled, every mounted available workspace participates in the graph and the active Git repository set.
Git-facing renderer code must pass an explicit repository path instead of assuming a single active vault. Changes and Pulse/history display one selected repository at a time, manual commit selects one target repository, and AutoGit checkpoints iterate every active repository. Diff, file history, note saves, and discarded changes resolve the repository from the note's workspace provenance or from the selected Git surface. Manual Sync refreshes vault-derived sidebar state and bumps a shared Git history refresh key after successful pulls, including `up_to_date` pulls, while automatic up-to-date checks avoid that heavier reload path.
`useGitFileWorkflows` is the renderer abstraction for note-scoped Git file actions. It translates active tabs, visible entries, and modified-file surfaces into the correct repository path for diff/history commands, deleted-note previews, queued editor diff requests, and discard refresh behavior.
### Tolaria Deep Links
Deep links identify existing vault items with `tolaria://<vault-slug>/<relative-path-with-extension>`. The slug is derived from the registered workspace alias, then label, then path basename; generated links append a stable short hash when two vaults share the same base slug. A manually typed ambiguous base slug is rejected instead of choosing the wrong vault.
The relative path is encoded per segment, preserving `/` as the separator while allowing spaces, Unicode, and reserved characters inside filenames. Decoding rejects `.`, `..`, encoded slashes, backslashes, empty segments, and any resolved path outside the target vault root. Links keep the file extension so Markdown, text, media, PDFs, and other vault files can all route through the same `VaultEntry` lookup.
Deep links are navigation-only. Opening one can focus Tolaria, switch to a registered vault, reload the index once, and open an existing item; it never creates missing files, imports external files, or silently falls back to another vault. v1 links are path-based, so renaming or moving a file changes the canonical link. macOS and Windows are the verified v1 desktop targets; Linux registration is best-effort until package-level QA covers the supported desktop environments.
### File kinds and binary previews
`VaultEntry.fileKind` comes from the Rust vault scanner and intentionally stays coarse-grained:
| `fileKind` | Source files | UI behavior |
|---|---|---|
| `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 |
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. On Linux AppImage builds, `should_use_external_media_preview` can disable in-webview audio/video rendering so the same file blocks show filename/external-open fallback controls instead of triggering unstable WebKitGTK media playback. 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.
Markdown note PDF export is not a stored file-kind transformation. `src/utils/notePdfExport.ts` temporarily marks the current webview for print-only rendering, asks for a `.pdf` filesystem destination only when the native capability reports direct save support, and invokes Tauri's native `WKWebView` PDF export command on macOS. Windows/Linux Tauri builds and browser mode keep print-dialog fallback behavior. `src/components/useEditorPdfExport.ts` ensures the rich rendered note is active before export, so frontmatter is ignored and the PDF reflects the current rendered editor DOM while leaving the vault file unchanged.
### 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.
### Entity Types (isA / type)
Entity type is stored in the `type:` frontmatter field (e.g. `type: Quarter`). The legacy field name `Is A:` is still accepted as an alias for backwards compatibility but new notes use `type:`. The `VaultEntry.isA` property in TypeScript/Rust holds the resolved value.
@@ -149,20 +225,23 @@ 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. 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.
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.
A `flatten_vault` migration command is available to move existing notes from type-based subfolders to the vault root.
### Types as Files
Each entity type can have a corresponding **type document** in the `type/` folder (e.g., `type/project.md`, `type/person.md`). Type documents:
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:
- 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
@@ -171,15 +250,17 @@ Each entity type can have a corresponding **type document** in the `type/` folde
| Property | Type | Description |
|----------|------|-------------|
| `icon` | string | Type icon as a Phosphor name (kebab-case, e.g., "cooking-pot") |
| `color` | string | Accent color: red, purple, blue, green, yellow, orange |
| `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)` |
| `order` | number | Sidebar display order (lower = higher priority) |
| `sidebar_label` | string | Custom label overriding auto-pluralization |
| `template` | string | Markdown template for new notes of this type |
| `sort` | string | Default sort: "modified:desc", "title:asc", "property:Priority:asc" |
| `sort` | string | Default sort: "modified:desc", "title:asc", "property:Priority:asc"; bare custom-property form such as "Priority:asc" is accepted and normalized in the UI |
| `view` | string | Default view mode: "all", "editor-list", "editor-only" |
| `visible` | bool | Whether type appears in sidebar (default: true) |
**Type relationship**: When any entry has an `isA` value (e.g., "Project"), the Rust backend automatically adds a `"Type"` entry to its `relationships` map pointing to `[[type/project]]`. This makes the type navigable from the Inspector panel.
**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/scalar-array 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.
**UI behavior**:
- Clicking a section group header pins the type document at the top of the NoteList if it exists
@@ -211,6 +292,8 @@ Supported value types (defined in `src-tauri/src/frontmatter/yaml.rs` as `Frontm
- **List**: Multi-line ` - item` or inline `[item1, item2]`
- **Null**: `owner:` (empty value)
Custom frontmatter fields with scalar values are exposed through `VaultEntry.properties`. Custom fields with scalar arrays are also exposed there, unless any array value contains a wikilink; wikilink-bearing fields belong to `VaultEntry.relationships`. Single-item scalar arrays continue to normalize to their scalar value for compatibility, while multi-item scalar arrays remain arrays so saved view filters can match exact elements.
### Custom Relationships
The Rust parser scans all frontmatter keys for fields containing `[[wikilinks]]`. Any non-standard field with wikilink values is captured in the `relationships` HashMap:
@@ -241,7 +324,9 @@ 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.
- **Untitled drafts** start as `untitled-*.md` and are auto-renamed on save once the note gains an H1.
### Title Surface (UI)
@@ -251,6 +336,7 @@ The BlockNote body is the only title editing surface:
- The first H1 is the canonical display title.
- There is no separate title row above the editor, even when a note has no H1.
- Notes without an H1 show the editor body and placeholder only.
- Legacy no-H1 notes whose display title differs from the filename show that title as read-only breadcrumb context beside the editable filename, so referenced notes remain identifiable without raw mode.
- Filename changes are explicit breadcrumb actions, not a dedicated title-input side effect.
### Sidebar Selection
@@ -263,19 +349,34 @@ type SidebarFilter = 'all' | 'archived' | 'changes' | 'pulse'
type SidebarSelection =
| { kind: 'filter'; filter: SidebarFilter }
| { kind: 'sectionGroup'; type: string } // e.g. type: 'Project'
| { kind: 'folder'; path: string }
| { kind: 'folder'; path: string; rootPath?: 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.
- `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. Inline folder creation carries an optional `FolderCreationParent` (`path` plus `rootPath`) through `App` to the `create_vault_folder` command, so new folders land under the selected folder or selected mounted vault root while preserving the active-vault path boundary. 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.
- `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 `sort` value accepts built-in sort forms such as `"modified:desc"` and custom-property forms such as `"property:Priority:asc"` or bare `"Priority:asc"`; the renderer keeps configured custom-property sorts visible even when the current result set has no populated values for that property. Filter conditions on scalar-array custom properties, such as `tags: [blues, chicago]`, evaluate `contains`, `any_of`, and related set operators against exact array elements rather than substrings. 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.
In a mounted-workspace graph, each loaded `ViewFile` carries optional renderer-owned `rootPath` and `workspace` provenance. `SidebarSelection.kind === 'view'` can include that `rootPath`, and view identity is `(rootPath, filename)` rather than filename alone. This lets two vaults both expose `views/focus.yml` without colliding in sidebar selection, note-list filtering, counts, sort/column persistence, edit, or delete flows. A saved View with `rootPath` filters only entries from its own workspace and persists changes through `save_view_cmd` / `delete_view_cmd` against that source vault.
`useAppViewActions()` owns the renderer-side saved View lifecycle: choosing the target workspace, preserving mounted-view identity, saving/deleting YAML definitions, reloading affected vault state, and exposing the available note-list fields for the create/edit dialog. `App.tsx` wires those callbacks into `Sidebar`, `NoteList`, `CreateViewDialog`, and command surfaces without duplicating the persistence rules.
`useMcpSetupDialogController()` owns MCP setup dialog state, busy actions, and manual config callbacks so `App.tsx` only passes the controller into settings/status surfaces. `useAiWorkspaceWindowBridgeEvents()` owns native AI-workspace event subscriptions and listener cleanup for popped-out workspace windows.
`createCrossWindowPersistedStore()` is the shared renderer primitive for AI workspace state that must stay synchronized across the main window and popped-out workspace windows. It owns localStorage reads/writes, BroadcastChannel publishing, storage-event synchronization, and external-store subscribers; domain modules such as `aiWorkspaceSessionStore` and `aiWorkspaceWindowSharedContext` provide sanitizers and mutations around that shell.
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.
@@ -289,6 +390,14 @@ type SidebarSelection =
- 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, custom 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, 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 custom desktop titlebar menu; OS-native menu items such as Undo, Copy/Paste, Services, Quit, and Window controls remain local to the native menu implementation.
`useActionHistory` is the renderer-owned stack for reversible app-level actions. It records note-state actions only after persistence succeeds, replays one undo/redo at a time, and reveals the affected note before applying the reversal so editor pending-content flushes stay path-correct. Text editors and text inputs keep their native undo/redo history; app-level Undo/Redo shortcuts are handled only when focus is outside text-editing surfaces.
## File System Integration
### Vault Scanning (Rust)
@@ -296,24 +405,33 @@ type SidebarSelection =
`vault::scan_vault(path)` in `src-tauri/src/vault/mod.rs`:
1. Validates the path exists and is a directory
2. Scans root-level `.md` files (non-recursive)
3. Recursively scans protected folders: `type/`, legacy `config/`, `attachments/`
4. Files in non-protected subfolders are **not indexed** (flat vault enforcement)
5. For each `.md` file, calls `parse_md_file()`:
2. Recursively scans non-hidden files while skipping hidden directories such as `.git/`
3. For each `.md` file, calls `parse_md_file()`:
- Reads content with `fs::read_to_string()`
- Parses frontmatter with `gray_matter::Matter::<YAML>`
- Extracts title from first `#` heading
- Reads entity type from `type:` frontmatter field (`Is A:` accepted as legacy alias); type is never inferred from folder
- Parses dates as ISO 8601 to Unix timestamps
- Extracts relationships, outgoing links, custom properties, word count, snippet
4. For recognized non-markdown text and binary files, emits a minimal `VaultEntry` with `fileKind`
5. Sorts by `modified_at` descending
6. Skips unparseable files with a warning log
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.
6. Sorts by `modified_at` descending
7. 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 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.
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. Image attachment commands refresh 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. 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.
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 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 the active mounted workspace set in `VAULT_PATHS`, 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 accept explicit `VAULT_PATH`/`VAULT_PATHS` for app-owned or legacy launches; durable external registrations omit vault env and resolve the current mounted workspace set from Tolaria's `vaults.json` at tool-call time. `mcp-server/tool-service.js` owns the shared tool semantics for active-vault resolution, cross-vault lookup/search, note-creation defaults, vault listing, and UI action intents; `mcp-server/index.js` and `mcp-server/ws-bridge.js` remain transport adapters around that service. Manual MCP config export uses the same packaged `mcp-server/` resolver as registration and app-managed AI agents, including Windows executable-adjacent installs under `%LOCALAPPDATA%\Tolaria`, so the copied snippet stays durable across active-workspace changes without writing third-party config files. Vault context checks each active workspace root for `AGENTS.md` and returns those instructions alongside note counts, folders, and recent notes. 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.
### Vault Caching
@@ -347,7 +465,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).
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.
### Data Types
@@ -398,29 +516,38 @@ interface PulseCommit {
| `history.rs` | File history | `git log` — last 20 commits per file |
| `status.rs` | Modified files | `git status --porcelain` — filtered to `.md` |
| `status.rs` | File diff | `git diff`, fallback to `--cached`, then synthetic for untracked |
| `commit.rs` | Commit | `git add -A && git commit -m "..."`; broken signing helpers trigger one unsigned retry for the same app-managed commit |
| `file_url.rs` | File URL | Builds a copyable remote URL from the primary remote, current branch, and vault-relative path without exposing remote credentials |
| `commit.rs` | Commit | Ensures a local author fallback when needed, then runs `git add -A && git commit -m "..."`; broken signing helpers trigger one unsigned retry for the same app-managed commit |
| `remote.rs` | Pull / Push | `git pull --rebase` / `git push` |
| `connect.rs` | Add remote | Adds `origin`, fetches it, validates history compatibility, and only starts tracking when the remote is safe |
| `conflict.rs` | Conflict resolution | Detect conflicts, resolve with ours/theirs/manual |
| `conflict.rs` | Conflict resolution | Detect conflicts, resolve with ours/theirs/manual, and ensure a local author fallback before commit/rebase continuation |
| `pulse.rs` | Activity feed | `git log` with `--name-status` for file changes |
### Auto-Sync
`useAutoSync` hook handles automatic git sync:
`useAutoSync` hook handles automatic git sync across every active Git repository:
- 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 after a successful pull update so the editor and note list stay aligned
- Pulls the active repository set concurrently on launch, focus, interval, and manual sync
- Budgets automatic launch/focus/interval pulls per repository with a short cooldown so focus or low interval settings do not repeat network Git work immediately after a recent sync; manual sync bypasses this budget
- Refreshes aggregate remote status after a pull, and avoids a separate startup status fetch when the initial pull will already refresh it
- Pushes the active repository set during divergence recovery
- Awaits the post-pull vault refreshes so toasts land after note-list state is fresh
- Reopens the clean active tab from disk only when the pull changed that active note, then restores editor focus if the editor owned focus before the remount
- Detects merge conflicts → opens `ConflictResolverModal`
- Tracks remote status (branch, ahead/behind via `git_remote_status`)
- Tracks aggregate remote status (ahead/behind via `git_remote_status`)
- Handles push rejection (divergence) → sets `pull_required` status
- `pullAndPush()`: pulls then auto-pushes for divergence recovery
- `pullAndPush()`: pulls then auto-pushes each active repository for divergence recovery
- `ConflictNoteBanner`: inline banner in editor for conflicted notes (Keep mine / Keep theirs)
`useGitRemoteStatus` is the commit-time companion to `useAutoSync`:
- Re-checks `git_remote_status` when the Commit dialog opens and right before submit
### 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 when the changed-path list includes that note, and closes the active tab if the file disappeared. Editor focus does not block the clean active note from converging to disk when its own file changed externally; if the active editor owned focus before that remount, the app requests editor focus again after the fresh tab is mounted. 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.
`useGitRepositories` is the commit-time companion to `useAutoSync`:
- Owns repository picker validation plus `get_modified_files` and `git_remote_status` loading for active Git repositories
- Re-checks the selected repository when the Commit dialog opens and right before submit
- Converts `hasRemote: false` into a local-only commit path
- Keeps the normal push path unchanged for vaults that do have a remote
- Keeps the normal push path unchanged for repositories that do have a remote
`AddRemoteModal` is the explicit recovery path for those local-only vaults:
- Opens from the `No remote` status-bar chip and the command palette
@@ -442,7 +569,7 @@ interface PulseCommit {
- **No remote indicator**: Neutral chip in the bottom bar when `GitRemoteStatus.hasRemote === false`
- **Pulse view**: Activity feed when Pulse filter is selected
- **Pull command**: Cmd+K → "Pull from Remote", also in Vault menu
- **Git status popup**: Click sync badge → shows branch, ahead/behind, Pull button
- **Git status popup**: Click sync badge → shows aggregate ahead/behind and a Pull button for the active repository set
- **Conflict banner**: Inline banner in editor with Keep mine / Keep theirs for conflicted notes
## BlockNote Customization
@@ -470,8 +597,9 @@ 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 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.
- Missing common grammars live in `src/utils/codeBlockLanguageCatalog.ts` and are registered lazily from direct `@shikijs/langs` imports by `src/components/codeBlockOptions.ts`; known aliases such as `ps1` and `vb` normalize to canonical picker values during Markdown import.
- 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.
### Markdown Math
@@ -479,43 +607,80 @@ Defined in `src/utils/mathMarkdown.ts`, `src/components/editorSchema.tsx`, and s
- `$...$` becomes a `mathInline` schema node and line-owned `$$...$$` / multiline `$$` blocks become `mathBlock` nodes.
- The rich editor renders both node types through KaTeX with `throwOnError: false`, so malformed formulas keep their source visible instead of breaking the note.
- Double-clicking rendered display math edits the math block's `latex` property in-place; Markdown delimiters remain owned by serialization. Inline math can still be reopened as source text for direct editing.
- `serializeMathAwareBlocks()` converts math nodes back to Markdown delimiters before save, raw-mode entry, and editor-position snapshots.
- Raw CodeMirror mode always shows the plain Markdown source, so imported technical notes stay editable outside Tolaria.
### 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`:
- 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.
- The tldraw runtime receives Tolaria's resolved light/dark mode as its user color scheme, so embedded whiteboards follow the app appearance and update while mounted.
- Embedded whiteboards expose a session-only full-window workspace that reuses the same tldraw store and Markdown snapshot; expanding or closing it does not persist camera, tool, or size state.
- 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.
### 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.
- `editorRichCopy.ts` owns rich-editor copy serialization for external apps. Normal selections use BlockNote's external clipboard HTML so tables, lists, checklists, and inline marks paste as rich content outside Tolaria, while `SingleEditorView` still normalizes `text/plain` and keeps fenced code-block selections on raw code text.
- 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.
- 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.
- `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.
- `richEditorInputTransform.ts` is the shared execution shell for rich-editor Markdown `beforeinput` transforms. It reads the live ProseMirror view, skips IME composition, resets state when a stale view is detected, dispatches transform transactions, prevents native input only after successful dispatch, and reports recoverable editor-transform errors through the same telemetry path. Arrow ligatures, inline math conversion, and `==highlight==` keep their syntax-specific matching in their feature files and are composed for the main editor by `richEditorInputTransformExtension.ts`.
- `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.
- `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.
- `tauriEventCleanup.ts` owns safe Tauri event unlisten cleanup. Hooks and stream utilities route listener teardown through it so stale or duplicate native listener removals cannot surface as unhandled promise rejections during fast remounts, window teardown, or stream completion.
- `useTauriDragDropEvent()` owns the shared Tauri window drag/drop subscription 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.
### Markdown-to-BlockNote Pipeline
```mermaid
flowchart LR
A["📄 Raw markdown\n(from disk)"] --> B["splitFrontmatter()\n→ yaml + body"]
B --> C["preProcessWikilinks(body)\n[[target]]token"]
C --> D["preProcessMathMarkdown(body)\n$...$ / $$...$$ → tokens"]
D --> E["tryParseMarkdownToBlocks()\n→ BlockNote block tree"]
E --> F["injectWikilinks + injectMathInBlocks\n tokens → schema nodes"]
F --> G["editor.replaceBlocks()\n→ rendered editor"]
B --> C["preProcessDurableEditorMarkdown(body)\nmermaid/tldraw fences + file links → tokens"]
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"]
G --> H["editor.replaceBlocks()\n→ rendered editor"]
style A fill:#f8f9fa,stroke:#6c757d,color:#000
style G fill:#d4edda,stroke:#28a745,color:#000
style H fill:#d4edda,stroke:#28a745,color:#000
```
> Wikilink placeholder tokens use `\u2039` and `\u203A`; math placeholder tokens use ASCII sentinels with URI-encoded LaTeX payloads.
> Wikilink placeholder tokens use `\u2039` and `\u203A`; math, Mermaid, tldraw, and standalone file-attachment link placeholders 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 + serializeMathAwareBlocks()\nschema nodes → Markdown source"]
B --> C["restoreWikilinks + serializeDurableEditorBlocks()\nschema nodes → Markdown source"]
C --> D["prepend frontmatter yaml"]
D --> E["invoke('save_note_content')\n→ disk write"]
@@ -523,6 +688,10 @@ 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. `src/utils/richEditorMarkdown.ts` is the shared BlockNote-to-Markdown owner for autosave/tab-swap and raw-mode entry, so wikilink restoration, durable schema-node serialization, frontmatter preservation, file-attachment block round-tripping, and portable attachment paths cannot drift between editor modes. 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:
@@ -530,13 +699,22 @@ Two navigation mechanisms:
1. **Click handler**: DOM event listener on `.editor__blocknote-container` catches clicks on `.wikilink` elements → `onNavigateWikilink(target)`.
2. **Suggestion menu**: Typing `[[` triggers `SuggestionMenuController` with filtered vault entries.
Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts`) uses multi-pass matching with global priority: filename stem (strongest) → alias exact title humanized title (kebab-case words). No path-based matching — flat vault uses title/filename only. Legacy path-style targets like `[[person/alice]]` are supported by extracting the last segment.
Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts`) uses multi-pass matching with global priority: path suffix for path-style targets, filename stem, alias, exact title, then humanized title (kebab-case -> words). In a mounted-workspace graph, unprefixed links prefer the source note's workspace, while links prefixed by a known workspace alias resolve inside that workspace (`[[team/projects/alpha]]`). Cross-workspace canonical link insertion prefixes the target alias only when source and target workspaces differ; same-workspace links stay vault-relative.
### Raw Editor Mode
Toggle via Cmd+K → "Raw Editor" or breadcrumb bar button. Uses CodeMirror 6 (`useCodeMirror` hook) to edit the raw markdown + frontmatter directly. Changes saved via the same `save_note_content` command.
`useRawModeWithFlush` owns the rich/raw transition model: pending raw-exit content and raw-mode overrides move together as one content transition, while cursor/scroll restoration moves through one restore-transition ref consumed by `useEditorModePositionSync`. The raw editor should not carry independent pending-content or pending-position refs outside that handoff.
While the user types, `useEditorSaveWithLinks` derives a transient `VaultEntry` patch from parseable frontmatter so the Inspector, relationship chips, and note-list-visible metadata stay in sync with the raw editor before the next vault reload. Temporarily invalid or half-typed frontmatter is ignored until it becomes parseable again, which avoids clobbering the last known good derived state.
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:
@@ -544,14 +722,28 @@ 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 (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.
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.
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**: 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
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`
## Localization
App UI strings are resolved through `src/lib/i18n.ts`, with flat JSON catalogs in `src/lib/locales/*.json` (see [ADR-0087](adr/0087-json-catalogs-and-lara-cli-localization.md)):
- `AppLocale`: canonical locale tags such as `'en'`, `'zh-CN'`, `'fr-FR'`, `'es-419'`
- `UiLanguagePreference`: `'system' | AppLocale`; persisted settings serialize `system` as `null`
- `resolveEffectiveLocale()`: maps an explicit preference or system/browser language list to the effective supported locale, including legacy aliases
- `translate()` / `createTranslator()`: resolve keys with English fallback and simple `{name}` interpolation
- `scripts/validate-locales.mjs`: asserts every checked-in locale catalog matches the English keyset and stays flat-string-only
`App.tsx` owns the effective locale and passes it to localized app chrome through props. Settings and command-palette language commands call back into `saveSettings`, so UI language changes update the current session without touching vault content or reopening the vault.
## Inspector Abstraction
@@ -560,10 +752,13 @@ 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.
- **Anchored dropdowns**: Fixed-position property menus and note-list sort menus use `src/components/anchoredDropdown.ts` for anchor measurement, viewport clamping, scroll/resize repositioning, and optional max-height calculations. Property-specific filtering and keyboard navigation stay in `propertyDropdownUtils.ts`.
- **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.
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.
3. **BacklinksPanel**: Scans `allContent` for notes that reference the current note via `[[title]]` or `[[path]]`.
@@ -573,7 +768,7 @@ The Inspector panel (`src/components/Inspector.tsx`) is composed of sub-panels:
### Search
Keyword-based search scans all vault `.md` files using `walkdir`:
Keyword-based search scans all vault `.md` files using `walkdir` and applies the same Gitignored-content visibility filter as vault loading:
```typescript
interface SearchResult {
@@ -591,6 +786,8 @@ interface SearchResult {
- Click result to open note in editor
- Shows relevance score and snippet
The NoteList header search keeps its local title/snippet/property filtering for immediate scoped results, then augments the match set with `search_vault` hits from the visible workspace roots using the command's frontmatter-excluding search option. React stores only matching paths so body-only matches appear in the current list scope without a second content-read pass or rendering private matched text in note rows.
No indexing step required — search runs directly against the filesystem.
## Vault Management
@@ -601,24 +798,46 @@ No indexing step required — search runs directly against the filesystem.
- Persists vault list to `~/.config/com.tolaria.app/vaults.json` (reads legacy `com.laputa.app` on upgrade)
- Switching closes all tabs and resets sidebar
- Supports adding, removing, hiding/restoring vaults
- Persists workspace aliases, colors, mount state, and the default new-note destination for the unified graph
- Default vault: public Getting Started starter vault cloned on demand
Mounted workspaces are loaded together by `useVaultLoader` for note-list, quick-open, keyword search, wikilink navigation, and saved View discovery. Workspace switching remains a focus operation for per-vault capabilities (Git status, folders, AutoGit, watchers, and repair commands), not a graph isolation boundary.
### Vault Config
Per-vault settings stored locally and scoped by vault path:
- Managed by `useVaultConfig` hook and `vaultConfigStore`
- 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
- Settings: zoom, view mode, editor mode, tag colors, status colors, property display modes, Inbox/All Notes note-list column overrides, explicit organization workflow toggle, Git setup prompt preference, 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
- 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; user-authored custom `AGENTS.md` / `CLAUDE.md` files are surfaced as custom and left untouched
- `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
- Editing a usable `AGENTS.md`, including changing its frontmatter `type`, makes the file custom rather than broken; broken is reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs
- 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
### Action History
`useActionHistory` owns renderer-scoped app undo/redo state. It stores explicit action entries with labels plus undo/redo callbacks, suppresses nested recording during replay, and exposes the top labels to command-palette commands.
- Frontmatter mutations record history only after the write succeeds and only for non-silent user actions.
- Entry state toggles such as archive, favorite, and organized record explicit before/after replay callbacks after persistence succeeds.
- Text inputs, contenteditable surfaces, and editor-owned text history keep native undo/redo first; app-level history runs only when focus is outside text editing.
- Irreversible destructive actions stay outside the stack and continue to use confirmation/destructive affordances.
### Getting Started / Onboarding
`useOnboarding` hook detects first launch:
@@ -635,6 +854,9 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte
`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 checks Claude Code, Codex, OpenCode, Pi, Gemini, and Kiro by treating 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
- App-managed Claude Code runs preserve the same user-managed Anthropic/provider env behavior by forwarding selected exported variables from the app process or the user's zsh/bash startup files without persisting those secrets
- The shared `useAiAgentsStatus` hook defers that command until after the first render, skips it when AI features are disabled or the current window cannot render AI status surfaces, and falls back to missing-agent statuses if the native probe does not return promptly so first-launch onboarding keeps a recovery path
- Persists dismissal locally once the user continues
### Remote Git Operations
@@ -642,6 +864,10 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte
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 and MCP runtime subprocess (Node.js or Bun) removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` before spawning, so helpers like `git-remote-https` and the system MCP runtime bind against the host library stack instead of Tolaria's bundled WebKit/AppImage libraries
- On native Linux Wayland launches and Linux AppImage launches, startup environment safeguards set `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` unless the user already provided either variable, keeping WebKitGTK rendering crashes out of the app startup path while leaving native X11 launches unchanged.
- On Linux AppImage launches, release packaging bundles the GTK3 fcitx immodule into the AppImage and startup environment safeguards write a cache-local `GTK_IM_MODULE_FILE` that points GTK at the mounted module whenever fcitx is configured. If the user has not explicitly chosen a GTK IM module, Tolaria also sets `GTK_IM_MODULE=fcitx`, allowing WebKitGTK editor input to reach fcitx5 on both Wayland and X11 fallback launches without relying on host GTK module cache paths.
- `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
@@ -651,6 +877,13 @@ Tolaria delegates remote auth to the user's system git setup:
App-level settings persisted at `~/.config/com.tolaria.app/settings.json` (reads legacy `com.laputa.app` on upgrade):
```typescript
interface AiWorkspaceConversationSetting {
archived: boolean | null
id: string
target_id: string | null
title: string
}
interface Settings {
auto_pull_interval_minutes: number | null
autogit_enabled: boolean | null
@@ -661,12 +894,25 @@ 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' | null
default_ai_agent: 'claude_code' | 'codex' | null
theme_mode: 'light' | 'dark' | 'system' | null
ui_language: AppLocale | null
date_display_format: 'us' | 'european' | 'friendly' | 'iso' | null
note_width_mode: 'normal' | 'wide' | null
sidebar_type_pluralization_enabled: boolean | null // null = default true
ai_features_enabled: boolean | null // null = default true
git_enabled: boolean | null // null = default true
default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | 'gemini' | 'kiro' | null
default_ai_target: string | null // "agent:codex" or "model:<provider>/<model>"
ai_model_providers: AiModelProvider[] | null
ai_workspace_conversations: AiWorkspaceConversationSetting[] | 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. `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. 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; 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. `date_display_format` is installation-local and controls rendered dates in note rows, property chips/cells, note info, table-of-contents metadata, and search result subtitles; `AppPreferencesProvider` owns the UI-level value so rendering surfaces can consume it without prop forwarding, while date picker text input remains ISO for predictable manual entry and storage. `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. `ai_features_enabled` is installation-local and defaults to `true`; when false, Tolaria hides AI panel controls, status bar AI indicators, command-palette AI mode, and missing-agent prompts while leaving Settings as the re-enable path. `git_enabled` is also installation-local and defaults to `true`; when false, Tolaria hides Git status-bar entries and command-palette actions, disables AutoGit controls, and avoids background Git refresh/sync work while leaving Settings as the re-enable path. `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; env-backed keys can come from the app process or exported zsh/bash startup values on Unix. Direct OpenAI-compatible model streams receive the active vault root and may execute Tolaria's native create-only `create_note` tool, but they do not receive shell access or general file-write tools. `ai_workspace_conversations` stores installation-local AI chat sidebar metadata only: conversation ids, titles, archive state, and explicit target overrides. It does not store vault content, prompts, transcripts, or model credentials. 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.
## Telemetry
@@ -678,9 +924,17 @@ 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` / `VITE_POSTHOG_KEY` env vars.
- **`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/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. `reinit_sentry()` for runtime toggle.
- **`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.
- **AI feature visibility** — `ai_features_visibility_changed` records only whether installation-level AI surfaces were enabled or hidden.
- **All Notes visibility** — `all_notes_visibility_changed` records only the toggled category and enabled state.
### Tauri Commands
- **`reinit_telemetry`** — Re-reads settings and toggles Rust Sentry on/off. Called from frontend when user changes crash reporting setting.
@@ -690,7 +944,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 available/downloading/ready states, and delegates install work to Rust.
- **`useUpdater(releaseChannel)`** — Channel-aware updater state machine. Checks the selected feed, surfaces checking/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
@@ -698,7 +952,7 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
- **`src/lib/appUpdater.ts`** — Thin wrapper around the Tauri updater commands. Keeps the React hook free of endpoint-selection details.
### Rust
- **`src-tauri/src/app_updater.rs`** — Chooses the correct update endpoint (`alpha/latest.json` or `stable/latest.json`) and adapts Tauri updater results into frontend-friendly payloads.
- **`src-tauri/src/app_updater.rs`** — Chooses the correct update endpoint and adapts Tauri updater results into frontend-friendly payloads. Stable uses the public `stable/latest.json` feed. Alpha first resolves the newest non-draft `alpha-vYYYY.M.D-alpha.NNNN` GitHub Release asset named `alpha-latest.json`, then falls back to the public `alpha/latest.json` feed if the release lookup is unavailable.
- **`src-tauri/src/commands/version.rs`** — Formats app build/version labels for the status bar, including calendar alpha labels and legacy release compatibility.
### Tauri Commands
@@ -706,6 +960,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` and refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed.
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon artifacts, Windows x64 installers/updater bundles, and Linux x86_64 `.deb` / AppImage artifacts.
- **`.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. The Windows job builds NSIS with Tauri updater signatures and uses Authenticode signing plus `Get-AuthenticodeSignature` verification when CI certificate secrets are configured; stable remains the strict channel for mandatory Authenticode enforcement. The Linux job uses Tauri's stock linuxdeploy AppImage output plugin and validates that installer and updater-signature artifacts exist before upload. The docs/release Pages job reads the stable manifest from the latest stable release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so the public updater JSON changes as part of the release workflow. Changes to the shared artifact workflow are not ignored by the alpha trigger, so release-pipeline fixes produce a fresh alpha run. 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, Authenticode-signed Windows x64 installers plus Tauri-signed updater bundles, Linux x86_64 `.deb` / `.rpm` / AppImage artifacts, and a static public download page that starts selected non-Windows installers without replacing the page with a blank download navigation. Windows visitors see an explicit signed-installer action and managed-device approval guidance instead of an automatic download. Linux visitors default to the AppImage target while the page exposes RPM as a manual Linux package option when the stable release includes one. The Linux job uses the same stock Tauri/linuxdeploy AppImage packaging and artifact validation as alpha releases. The Pages job reads the alpha manifest from the latest alpha release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so stable and alpha manifests stay fresh. 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.
- **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed.

View File

@@ -24,6 +24,13 @@ 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 |
| N/A | Whether this installation shows Git features |
| Per-vault All Notes note-list column overrides | All Notes PDF/image/unsupported file visibility |
| N/A | Per-vault Git setup prompt opt-out |
| Type `_sidebar_label` overrides | Whether this installation auto-pluralizes type labels |
| N/A | Registered workspace labels, aliases, mount state, and default new-note destination |
| 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.
@@ -31,7 +38,13 @@ 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: `date_display_format: "friendly"` (installation-specific date rendering preference)
- ✅ 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
@@ -78,9 +91,40 @@ 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. The three functions in `useEntryActions` (`handleCustomizeType`, `handleRenameSection`, `handleToggleTypeVisibility`) follow this rule — disk write first, then state update.
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.
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.
#### 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 clean active-editor content refresh under the ADR-0135 unsaved-edit rules. When that clean active-editor remount starts from a focused editor, the main app dispatches a post-replacement editor focus request so typing can continue in the refreshed tab. `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).
#### Mounted Workspaces
The registered vault list can act as a mounted-workspace set. `useVaultSwitcher` persists each workspace's installation-local identity (`label`, stable `alias`, color, mount flag) and the default destination for newly created notes in `~/.config/com.tolaria.app/vaults.json`. `useVaultLoader` scans every available mounted workspace and annotates each `VaultEntry` with provenance before React consumes the combined graph. The default workspace is the write target for new notes and Type documents; it is not the only active vault when multiple workspaces are enabled.
Vault item deep links use the registered vault list as their resolver namespace. `src/utils/deepLinks.ts` builds `tolaria://<vault-slug>/<relative-path-with-extension>` URLs from workspace aliases, labels, and paths, appending a short stable hash when generated slugs would collide. `useDeepLinks` validates incoming links, switches vaults when required, reloads the vault index once for recently changed files, and opens the matching `VaultEntry` through the normal note-selection path.
Saved Views participate in that mounted graph as source-scoped chrome. `useVaultLoader` loads view definitions from every mounted vault, annotates each `ViewFile` with its owning `rootPath` and workspace identity, and keeps sidebar selection/persistence keyed by `(rootPath, filename)` so same-named view files from different vaults stay independent.
Git surfaces resolve repository paths explicitly. `useGitRepositories` derives the active repository set from the mounted available workspaces, keeps separate selected repositories for Changes, Pulse/history, and manual commits, and exposes the combined modified-file count for status/commands. AutoGit checkpoints iterate that repository set, while manual commit, history, diff, and discard operations use the selected surface or the note's workspace provenance.
Renderer git file workflows stay behind `useGitFileWorkflows`. The hook resolves per-note repository paths, queues editor diff requests, opens Pulse history entries including deleted-file previews, and keeps discard/reload handling close to the selected Git surface while `App.tsx` only wires the resulting callbacks into `NoteList`, `PulseView`, and `Editor`.
Cross-workspace note reads and writes keep the disk-first invariant. When an absolute note path is saved or read without an explicit `vaultPath`, the Tauri boundary resolves the deepest registered vault root that contains the path and validates against that root before touching disk. This lets an editor tab opened from a mounted workspace save back to its source repository while preserving the same path-escape protections as active-vault operations.
#### 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
@@ -90,15 +134,20 @@ flowchart LR
| Frontend | React + TypeScript | React 19, TS 5.9 |
| Editor | BlockNote | 0.46.2 |
| Code block highlighting | @blocknote/code-block | 0.46.2 |
| Additional code grammars | @shikijs/langs | 3.23.0 |
| 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 | - |
| Icons | Phosphor Icons + Lucide | - |
| Icons | Phosphor Icons | - |
| Build | Vite | 7.3.1 |
| Backend language | Rust (edition 2021) | 1.77.2 |
| Frontmatter parsing | gray_matter | 0.2 |
| AI (agent panel) | CLI agent adapters (Claude Code + Codex) | - |
| Filesystem watcher | notify | 6.1 |
| AI (workspace) | CLI agent adapters (Claude Code + Codex + OpenCode + Pi + Gemini + Kiro) plus configured local/API model targets | - |
| 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 |
| Tests | Vitest (unit), Playwright (E2E/smoke), cargo test (Rust) | - |
| Package manager | pnpm | - |
@@ -114,14 +163,16 @@ flowchart TD
SB["Sidebar\n(navigation + filters + types)"]
NL["NoteList / PulseView\n(filtered list / activity)"]
ED["Editor\n(BlockNote + diff + raw)"]
IN["Inspector\n(metadata + relationships)"]
AIP["AiPanel\n(selected CLI agent + tools)"]
IN["Right Panel\n(Inspector + TOC)"]
AIW["AiWorkspace\n(docked or native window)"]
AIP["AiPanel\n(transcript + composer)"]
SP["SearchPanel\n(keyword search)"]
ST["StatusBar\n(vault picker + sync + version)"]
CP["CommandPalette\n(Cmd+K launcher)"]
App --> WS & SB & NL & ED & SP & ST & CP
ED --> IN & AIP
App --> WS & SB & NL & ED & AIW & SP & ST & CP
ED --> IN
AIW --> AIP
end
subgraph RB["Rust Backend"]
@@ -131,11 +182,11 @@ flowchart TD
GIT["git/\n(commit, sync, clone)"]
SETTINGS["settings.rs"]
SEARCH["search.rs"]
CLI["ai_agents.rs\n+ claude_cli.rs"]
CLI["ai_agents.rs\n+ CLI adapters"]
end
subgraph EXT["External Services"]
CCLI["Claude CLI / Codex CLI\n(agent subprocesses)"]
CCLI["Claude / Codex / OpenCode / Pi / Gemini / Kiro 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.)"]
@@ -157,12 +208,12 @@ flowchart TD
```
┌────────┬─────────────┬─────────────────────────┬────────────┐
│Sidebar │ Note List │ Editor │ Inspector
│Sidebar │ Note List │ Editor │ Right Panel
│(250px) │ (300px) │ (flex-1) │ (280px) │
│ │ OR │ │ OR │
│ All │ Pulse View │ [Breadcrumb Bar] │ AI Chat
│ All │ Pulse View │ [Breadcrumb Bar] │ TOC
│ Changes│ │ │ OR │
│ Pulse │ [Search] │ # My Note │ AI Agent
│ Pulse │ [Search] │ # My Note │ Properties
│ Inbox │ [Sort/Filt] │ │ │
│ │ │ │ Context │
│Projects│ Note 1 │ Content here... │ Messages │
@@ -176,21 +227,33 @@ flowchart TD
└──────────────────────────────────────────────────────────────┘
```
- **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, 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. 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, 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. Decomposed into `Editor` (orchestrator), `EditorContent`, `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).
- **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 for single-vault lists: 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. In multiple-vault mode, saved View rows are keyed by source vault plus filename so duplicate filenames do not collide, and edits/deletes route to the owning vault. 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. Folder creation sends the selected folder's vault-relative path and mounted root to `create_vault_folder`, so a new folder is created under the focused parent instead of defaulting to the active vault root. 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. In mounted multi-vault graphs, duplicate type names still render as one sidebar section, but the visibility picker becomes a workspace matrix and writes visibility to the specific vault's Type document, so hidden type definitions suppress only notes of that type from the same workspace.
- **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. Inbox organization auto-advance is coordinated by `useInboxOrganizeAdvance`, which only opens the next visible Inbox note when the organized note is still the active requested tab after the write finishes. 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 filename controls, read-only legacy display-title context when a no-H1 note's title differs from its filename, 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` plus lazy direct `@shikijs/langs` registrations for missing common grammars. 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. Linux AppImage builds ask the native runtime whether audio/video should fall back to external-open controls before mounting webview media elements. 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. Rich-editor Markdown input transforms for arrows, inline math, and `==highlight==` share one capture-phase `beforeinput` execution path in `src/components/richEditorInputTransform.ts` and are composed by `src/components/richEditorInputTransformExtension.ts`. Navigation history (Cmd+[/]) replaces tabs.
Rich-editor copy uses BlockNote's external HTML serializer for selected note content so tables, lists, checklists, and inline formatting paste richly into other apps, with Tolaria keeping fenced-code selections as raw code text and normalized plain text on the clipboard.
Note PDF export stays renderer-owned for layout: `useEditorPdfExport` exits diff/raw views, applies a print-only stylesheet to the rendered note root, and checks the native PDF capability before choosing a platform path. On macOS, the renderer asks for a filesystem PDF destination before the Tauri `export_current_webview_pdf` command saves the current `WKWebView` print operation directly; on Windows/Linux Tauri builds and in browser mode, the same export action falls back to the native/browser print dialog. The export reuses rendered BlockNote output so frontmatter is omitted, while math, images, Mermaid diagrams, tldraw blocks, code, tables, and links degrade through their existing DOM rather than a second Markdown-to-PDF renderer, and the source Markdown is never modified. Markdown notes expose the same export action from Cmd+K, the native Note menu, the breadcrumb overflow menu, and each Markdown row's note-list context menu.
- **Right side panels** (200-500px or hidden): Properties and Table of Contents 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. The breadcrumb bar toggles Table of Contents and Properties actions. 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).
Panels are separated by `ResizeHandle` components that support drag-to-resize.
- **AI workspace** (docked panel or native window): `AiWorkspace` owns the multi-chat orchestration, sidebar tabs, installed-only target picker, permission-mode picker, and dock/pop-out controls. Header/guidance chrome lives in `AiWorkspaceChrome`, edge-resize handles in `AiWorkspaceResizeHandles`, conversation metadata/settings persistence in `aiWorkspaceConversations`, and sizing/class/style helpers in `aiWorkspaceSizing`. The status-bar AI affordance opens this workspace instead of changing the default target inline. Docked workspace mode renders as a compact bounded desktop tool inside the main app; users resize the anchored panel from its left/top edges and resize the chat-list sidebar separately from the transcript area. Pop-out mode opens a dedicated undecorated transparent Tauri webview window labeled `ai-workspace` and boots the lightweight `AiWorkspaceWindowApp` route instead of the full vault shell. The chat header and sidebar header are draggable in native-window mode; closing the pop-out only closes that window, while the dock control emits a dock request back to the main window before closing the pop-out. Chat sessions reuse `AiPanelView` for transcript/composer rendering with the old panel header disabled; target and permission controls live in the composer toolbar so there is one workspace header per active chat. AI workspace cross-window localStorage, BroadcastChannel, storage-event, and subscriber-set plumbing is owned by `createCrossWindowPersistedStore`; domain stores keep only sanitization, mutation, and native persistence behavior.
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.
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`.
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.
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 and skips min-size mutation while fullscreen or maximized so navigation/sidebar interactions do not unfullscreen or reposition the main window. 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. AI-agent CLI availability probing is also off the first-paint path: the renderer defers `get_ai_agents_status` until an idle/timeout tick, skips it for disabled AI surfaces and secondary windows, falls back to missing-agent onboarding state if the status IPC does not settle promptly, and the Rust command fans per-agent CLI checks across Tokio's blocking pool with per-agent timeouts. 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.
Desktop startup registers `tauri-plugin-deep-link` and `tauri-plugin-single-instance` before setup so `tolaria://` links can focus the existing main window and deliver URL events to the renderer. `tauri.conf.json` declares the `tolaria` scheme for bundled desktop builds; Windows and Linux also run `register_all()` as a runtime repair path, while macOS relies on bundle registration.
Linux and Windows use custom React-rendered window chrome instead of the native Tauri menu bar. `setup_custom_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 is macOS-only so Services/Hide/Quit and the reserved `WINDOW_SUBMENU_ID` keep behaving like normal NSApp menu items, while cross-platform custom items such as Check for Updates emit Tolaria command IDs with visible updater feedback from the renderer menu.
On Linux, `run()` applies WebKitGTK startup safeguards before Tauri creates the webview. Native Wayland launches and AppImage launches inject `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, covering compositor-specific WebKit crashes without changing native X11 launches. AppImage launches keep the additional AppImage-only safeguards: on Wayland sessions Tolaria 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. Runtime startup writes a mount-path-specific `GTK_IM_MODULE_FILE` cache when fcitx is configured via `GTK_IM_MODULE=fcitx` or common fcitx environment hints; release packaging currently uses Tauri's stock linuxdeploy AppImage output plugin instead of Tolaria's experimental output-plugin shim. If the user has not already chosen `GTK_IM_MODULE`, Tolaria sets `GTK_IM_MODULE=fcitx` before WebKit starts. 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 WebViews from blanking or crashing 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`, and the fontconfig guard avoids known WebKit crashes in COLRv1 emoji font rendering while leaving other emoji fonts available.
## Multi-Window (Note Windows)
Notes can be opened in separate Tauri windows for focused editing. Secondary windows show only the editor panel (no sidebar, no note list).
Notes can be opened in separate Tauri windows for focused editing. Secondary windows boot the same `App` shell and load the same active workspace graph as the main window, but they start in the editor-only view mode with side panels collapsed.
The AI workspace can also open in a separate Tauri window through `openAiWorkspaceWindow()`. That window uses `?window=ai-workspace` to boot the lightweight `AiWorkspaceWindowApp` route, receives the active vault context through URL params, opts out of main-window size constraints and startup AI onboarding, and redocks by emitting `tolaria:ai-workspace-dock-requested` to the main window. Its webview is transparent so the rounded workspace shell defines the visible floating-window corners across desktop platforms.
**Triggers:**
- `Cmd+Shift+Click` on any note in the note list or sidebar
@@ -200,9 +263,8 @@ Notes can be opened in separate Tauri windows for focused editing. Secondary win
**Architecture:**
- `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 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
- `main.tsx` always mounts `App`; `App` checks `isNoteWindow()` at startup, keeps normal vault/workspace loading active, and `useNoteWindowLifecycle` opens the requested note after the app graph is ready
- 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
- 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
@@ -212,12 +274,15 @@ 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` + `aiAgents.ts`) — streaming UI with 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`; Codex runs through `codex exec --json` with the CLI's normal approval / sandbox defaults
4. **MCP Integration** — Claude receives the generated MCP config file path, while Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides
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 permission-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. Kiro runs through `kiro-cli chat --no-interactive --trust-all-tools`, streams line-oriented stdout, drains stderr concurrently, and writes prompt content through stdin to avoid OS argument length limits. Codex, OpenCode, Pi, Gemini, and Kiro all launch from the active vault cwd with transient MCP config. Pi seeds its transient agent directory from the user's Pi agent directory before merging Tolaria MCP, so app-managed runs keep standalone Pi provider/auth settings. 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` after copying and merging the user's Pi agent config, Gemini receives it through a temporary settings file pointed at by `GEMINI_CLI_SYSTEM_SETTINGS_PATH`, and Kiro receives it through `.kiro/settings/mcp.json` in the active vault.
Claude Code availability intentionally does not depend only on the desktop app's inherited `PATH`. The detector checks 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, and Homebrew paths so first-run onboarding works on fresh macOS installs.
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. Claude Code launches also copy a narrow set of exported provider/auth environment variables from the app process or the user's zsh/bash startup files, including Anthropic API/base URL values, so company proxy and API-key setups that work in Terminal also work when Tolaria is opened from Finder or Dock. Windows npm `.cmd` shims are not spawned directly; the shared CLI runtime resolves them to their quoted Node script or native executable target first so prompt arguments do not hit batch-file argument validation.
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.
#### Agent Event Flow
@@ -231,12 +296,12 @@ sequenceDiagram
U->>FE: sendMessage(text, references)
FE->>FE: buildContextSnapshot(activeNote, linkedNotes, openTabs)
FE->>R: invoke('stream_ai_agent', {agent, message, systemPrompt, vaultPath})
R->>R: pick adapter for claude_code or codex
FE->>R: invoke('stream_ai_agent', {agent, message, systemPrompt, vaultPath, permissionMode})
R->>R: pick adapter for claude_code, codex, opencode, pi, gemini, or kiro
R->>C: spawn agent with MCP-enabled config
loop Normalized stream
C-->>R: Claude NDJSON or Codex JSONL events
C-->>R: Claude NDJSON, Codex JSONL, OpenCode JSON, Pi JSON, Gemini JSONL, or Kiro text events
R-->>FE: emit("ai-agent-stream", event)
alt TextDelta
FE->>FE: accumulate response (revealed on Done)
@@ -258,7 +323,7 @@ sequenceDiagram
#### File Operation Detection
When the agent writes or edits vault files, `useCliAiAgent` detects this from normalized tool inputs and calls `onFileCreated` or `onFileModified` callbacks to trigger vault reload.
When the agent writes or edits vault files, `aiAgentFileOperations.ts` detects this from normalized tool inputs and calls `onFileCreated` or `onFileModified` callbacks to trigger vault reload. Unrecognized write-like operations fall back to a full vault refresh.
### Context Building
@@ -266,38 +331,46 @@ The agent panel (`ai-context.ts`) builds a structured JSON snapshot from the act
```json
{
"activeNote": { "path", "title", "type", "frontmatter", "content" },
"linkedNotes": [{ "path", "title", "content" }],
"openTabs": [{ "title", "snippet" }],
"vaultMetadata": { "noteTypes", "stats", "filter" },
"references": [{ "title", "path", "type" }]
"activeNote": { "path", "title", "type", "frontmatter", "body", "wordCount", "bodyTruncated?" },
"openTabs": [{ "path", "title", "type", "frontmatter" }],
"noteList": [{ "path", "title", "type" }],
"vault": { "types", "totalNotes" },
"referencedNotes": [{ "title", "path", "type" }]
}
```
Token budget: 60% of 180k context limit (~108k tokens max). Active note gets priority, then linked notes, then truncation.
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 shell access. OpenAI-compatible direct targets can use Tolaria's narrow native `create_note` tool when an active vault is loaded; the tool calls the same create-only, active-vault-bounded note write command as the UI and emits tool events so the renderer refreshes and opens the created note. 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. Env-backed provider keys are resolved from the app process first, then from exported values in the user's zsh/bash startup files on Unix so GUI-launched sessions can still use shell-managed secrets. Local endpoints can omit authentication.
### 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. Tolaria does not store model-provider API keys in app settings.
Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login or user-managed Anthropic/provider environment variables; 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. App-managed Pi sessions copy that local Pi agent config into a per-run temporary directory before adding Tolaria MCP, so Tolaria does not overwrite global Pi files and does not drop a working standalone Pi setup.
## MCP Server
The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistants (Claude Code, Cursor, or any MCP-compatible client).
The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistants (Claude Code, Gemini CLI, Cursor, or any MCP-compatible client).
The stdio entrypoint and desktop WebSocket bridge share `mcp-server/tool-service.js` for mounted-vault resolution, note lookup/search, note creation defaults, vault listing, and UI action intents; `index.js` and `ws-bridge.js` only adapt those semantics to their transport-specific request and response shapes.
### Tool Surface (14 tools)
### Tool Surface
| Tool | Params | Description |
|------|--------|-------------|
| `open_note` | `path` | Open and read a note by relative path |
| `read_note` | `path` | Read note content (alias for `open_note`) |
| `create_note` | `path, title, [type]` | Create new note with title and optional type frontmatter |
| `create_note` | `path, content, [title], [type], [vaultPath]` | Create a new markdown note inside an active vault without overwriting existing files |
| `search_notes` | `query, [limit]` | Search notes by title or content substring |
| `list_vaults` | — | List active mounted vaults and whether each has root `AGENTS.md` instructions |
| `append_to_note` | `path, text` | Append text to end of existing note |
| `edit_note_frontmatter` | `path, patch` | Merge key-value patch into YAML frontmatter |
| `delete_note` | `path` | Delete a note file from the vault |
| `link_notes` | `source_path, property, target_title` | Add a target to an array property in frontmatter |
| `list_notes` | `[type_filter], [sort]` | List all notes, optionally filtered by type |
| `vault_context` | — | Get vault summary: entity types + 20 recent notes + configFiles |
| `vault_context` / `get_vault_context` | `[vaultPath]` | Get mounted-vault summary: entity types, folders, recent notes, and root `AGENTS.md` instructions |
| `ui_open_note` | `path` | Open a note in the Tolaria UI editor |
| `ui_open_tab` | `path` | Open a note in a new UI tab |
| `ui_highlight` | `element, [path]` | Highlight a UI element (editor, tab, properties, notelist) |
@@ -314,29 +387,34 @@ 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)
- `~/.config/opencode/opencode.json` (OpenCode, using its `mcp` config key)
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. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`).
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/OpenCode settings), uses `upsert` semantics, and can be reversed by removing Tolaria's entry again. Tolaria resolves an MCP runtime (Node.js 18+ preferred, Bun 1+ as fallback) before writing config so external clients are not left pointing at a missing binary, writes a vault-neutral `type: "stdio"` entry for standard MCP clients, writes OpenCode's vault-neutral `type: "local"` entry, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. Durable external MCP processes resolve active workspaces at tool-call time: explicit `VAULT_PATH`/`VAULT_PATHS` env still wins for app-owned and legacy launches, otherwise the MCP server reads Tolaria's `vaults.json`, uses `active_vault` first, and includes every workspace not marked `mounted: false`. Vault context checks each active workspace root for `AGENTS.md` and includes those instructions in the returned context. 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. Linux AppImage startup extracts the bundled `mcp-server/` to `~/.local/share/tolaria/mcp-server/` with a `.tolaria-version` marker, so durable external registrations use a stable path instead of the changing AppImage mount point. The `useMcpStatus` hook tracks whether Tolaria's durable MCP entry is 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 runtime process instead of keeping it alive in the background.
### Architecture
```mermaid
flowchart TD
subgraph MCP["MCP Server (Node.js) — spawned by Tauri on startup"]
subgraph MCP["MCP Server (Node.js or Bun) — mounted-workspace scoped"]
IDX["index.js"]
SVC["tool-service.js\n(vault resolution, note operations,\nUI action intents)"]
VAULT["vault.js\n(findMarkdownFiles, readNote, createNote,\nsearchNotes, appendToNote, editNoteFrontmatter,\ndeleteNote, linkNotes, listNotes, vaultContext)"]
WSB["ws-bridge.js"]
IDX -->|"stdio transport"| STDIO["Claude Code / Cursor"]
IDX --> VAULT
IDX --> WSB
IDX -->|"stdio transport"| STDIO["Claude Code / Cursor / Gemini / OpenCode"]
IDX --> SVC
SVC --> VAULT
IDX -.->|"UI action WebSocket client"| WSB
WSB --> SVC
WSB -->|"port 9710 — tool bridge"| AI["AI Clients\n(Claude Code, external)"]
WSB -->|"port 9711 — UI bridge"| FE["Frontend\n(useAiActivity)"]
end
TAURI["Tauri (mcp.rs)"] -->|"spawn on startup"| MCP
UI["Status bar / Command Palette"] -->|"explicit setup or disconnect"| CFG["~/.claude.json\n~/.claude/mcp.json\n~/.cursor/mcp.json\n~/.config/mcp/mcp.json"]
TAURI["Tauri bridge lifecycle"] -->|"start/stop/restart with VAULT_PATHS"| MCP
UI["Status bar / Command Palette"] -->|"explicit setup or disconnect"| CFG["~/.claude.json\n~/.claude/mcp.json\n~/.cursor/mcp.json\n~/.config/mcp/mcp.json\n~/.config/opencode/opencode.json"]
```
### WebSocket Bridge
@@ -362,12 +440,15 @@ flowchart LR
| Function | Purpose |
|----------|---------|
| `spawn_ws_bridge(vault_path)` | Spawns `ws-bridge.js` as child process with VAULT_PATH env |
| `register_mcp(vault_path)` | Writes Tolaria entry to Claude Code, Cursor, and generic MCP configs on explicit user request |
| `remove_mcp()` | Removes Tolaria's MCP entry from Claude Code, Cursor, and generic MCP configs |
| `spawn_ws_bridge(vault_path)` | Spawns `ws-bridge.js` as child process with `VAULT_PATH`/`VAULT_PATHS` env |
| `sync_mcp_bridge_vault(vault_path?)` | Starts, restarts, or stops the desktop WebSocket bridge as the selected vault changes |
| `extract_mcp_server_to_stable_dir(app_version)` | On Linux AppImage launches, copies bundled MCP files to `~/.local/share/tolaria/mcp-server/` with version-gated replacement so external clients can keep a stable `index.js` path |
| `register_mcp(vault_path)` | Resolves an MCP runtime (Node.js 18+ preferred, Bun 1+ fallback), resolves the packaged or stable extracted `mcp-server/`, and writes Tolaria's vault-neutral entry to Claude Code, Gemini CLI, Cursor, OpenCode, and generic MCP configs on user request |
| `mcp_config_snippet(vault_path)` | Builds the exact vault-neutral `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, OpenCode, 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 killed on app exit via `RunEvent::Exit` handler. The same desktop layer now keeps the Tauri asset protocol scoped to the active vault instead of every filesystem path.
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.
## Search
@@ -380,13 +461,15 @@ Search is keyword-based, using `walkdir` to scan all `.md` files in the vault di
The `search_vault` Tauri command runs the scan in a blocking Tokio task and returns results sorted by relevance score.
The note-list search field combines client-side scoped filtering with that same command: title, snippet, and visible-property matches resolve immediately, while backend body-content hits use `search_vault` with frontmatter excluded before adding matching paths for the currently visible workspace roots without displaying matched body text in the note row.
## Vault Cache System
The vault cache (`src-tauri/src/vault/cache.rs`) accelerates vault scanning using git-based incremental updates.
### 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 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.
`~/.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: v14 (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.
@@ -408,11 +491,19 @@ flowchart TD
## Styling
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.
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.
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 `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.
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.
## Localization
Tolaria's app chrome uses an app-owned localization runtime in `src/lib/i18n.ts`, backed by flat JSON catalogs in `src/lib/locales/` and Lara CLI synchronization through `lara.yaml` (see [ADR-0087](adr/0087-json-catalogs-and-lara-cli-localization.md)). `en.json` is the canonical source catalog, locale files are one file per locale, and English remains the fallback for any missing locale file or key. The installation-local `ui_language` setting stores an explicit locale when the user chooses one; `null` means "follow the system language when Tolaria supports it, otherwise English." Legacy stored values such as `zh-Hans` are normalized to canonical locale codes like `zh-CN`.
`App.tsx` derives the effective locale from settings and browser/system language hints, then passes it down to localized surfaces. Settings exposes a keyboard-accessible shadcn `Select`, and the command palette includes actions to open language settings or switch directly to a supported language.
`App.tsx` also resolves the installation-local date display format from `settings.date_display_format` and publishes it through `AppPreferencesProvider` in `src/hooks/useAppPreferences.ts`. Note rows, note-list property chips, inspector property cells, note info, table-of-contents metadata, and search result subtitles read that shared preference and render dates through `src/utils/dateDisplay.ts` so the visible style stays consistent. Date picker text entry remains ISO (`YYYY-MM-DD`) to preserve predictable manual input and frontmatter storage.
## Vault Management
@@ -441,34 +532,41 @@ Per-vault UI settings stored locally per vault path (currently in browser/Tauri
- `inbox.noteListProperties`: Optional Inbox-only property chip override for the note list
- `allNotes.noteListProperties`: Optional All Notes-only property chip override for the note list
- `inbox.explicitOrganization`: When `false`, hide Inbox and the organized toggle so the vault behaves like a plain note collection
- `git_setup_preference`: `"never"` when the user has opted out of future automatic Git setup prompts for that vault
### Getting Started Vault
On first launch, `useOnboarding` checks if the default vault exists. If not, it shows `WelcomeScreen` with three options:
- **Create a new vault** → creates an empty git repo in a folder the user chooses
- **Open an existing folder** → system file picker
- **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
When an opened folder is not yet a git repo, `init_git_repo` runs `git init`, ensures Tolaria's default `.gitignore`, stages the vault, and writes the initial `Initial vault setup` commit. 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.
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.
Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code and Codex are installed, offers per-agent install links when they are missing, and stores local dismissal so the prompt does not repeat on every launch.
When an opened folder is not yet a git repo, Tolaria can show a Git setup dialog with Initialize, Not now, and Never for this vault actions. The Never choice stores a local per-vault `git_setup_preference` so the automatic dialog does not return for that vault, while manual initialization remains reachable from Git commands when global Git features are enabled. Markdown scanning, note browsing, note editing, and search continue normally in plain folders. Git-dependent surfaces (history, changes, commit, sync, conflict resolution, remotes, AutoGit, and auto-sync) stay unavailable until the user explicitly initializes Git.
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, remote-connection, manual/automatic, and conflict-resolution commits, Tolaria ensures Git can resolve an author identity without overriding one the user configured: it heals the legacy repo-local `vault@tolaria.md` email earlier versions wrote, respects identities resolvable from local, global, or system scope, skips the legacy email wherever it resolves, and only when nothing resolves writes a repo-local `Tolaria <vault@tolaria.default>` fallback. 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, Gemini, and Kiro 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.
`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. 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. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. Once a user edits a usable `AGENTS.md`, including changing its frontmatter `type`, the status command treats it as custom guidance instead of broken; repair remains reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs. 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.
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.
**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. `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
4. Linux AppImage builds strip AppImage loader variables from system-git and MCP Node subprocesses before spawning them, keeping `git-remote-https` and system `node` on the host 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
**Auth model:**
- SSH keys, Git Credential Manager, macOS Keychain helpers, `gh auth`, and other git helpers all work without app-specific setup
@@ -499,9 +597,9 @@ sequenceDiagram
participant MCP as MCP Server
participant U as User
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->>T: apply Linux WebKit env safeguards<br/>(Wayland/AppImage)
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->>A: App mounts
A->>A: useOnboarding — vault exists?
@@ -509,10 +607,16 @@ sequenceDiagram
A-->>U: WelcomeScreen
else Vault found
A->>VL: useVaultLoader fires
VL->>T: invoke('reload_vault') → sync active vault asset scope + scan_vault_cached()
VL->>T: invoke('reload_vault') → allow requested vault roots in 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
@@ -520,9 +624,10 @@ 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(blocks)
A->>A: injectWikilinks + injectDurableEditorMarkdownBlocks(blocks)
A-->>U: Editor renders note
```
@@ -549,8 +654,10 @@ flowchart TD
RV --> TAB{"clean active tab?"}
TAB -->|Yes| RT["replace active tab\nwith fresh disk content"]
TAB -->|No| DONE["idle"]
RT --> DONE
PC -->|Up to date| DONE["idle"]
RT --> RF["restore editor focus\nwhen previously focused"]
RF --> DONE
PC -->|Manual up to date| RV
PC -->|Auto up to date| DONE["idle"]
MAN["Manual commit\n(CommitDialog)"] --> RS["useGitRemoteStatus\n(commit-time check)"]
RS --> RCHK["invoke('git_remote_status')"]
@@ -571,11 +678,17 @@ flowchart TD
STATUS["Click sync badge"] --> POPUP["GitStatusPopup\n(branch, ahead/behind)"]
```
`useGitRemoteStatus` re-checks `git_remote_status` when the commit dialog opens and again right before submit. If `hasRemote` is false, Tolaria keeps the flow local-only: the status bar shows a neutral `No remote` chip, the dialog copy switches from "Commit & Push" to "Commit", and no `git_push` call is attempted.
Manual Sync forces a visible-state refresh even when `git_pull` reports `up_to_date`, because the working tree may have already changed through another process while the app still holds stale vault and History state. Updated pulls refresh the vault index, folders, saved views, clean active-editor content, and Git history surfaces; manual up-to-date pulls refresh the vault/sidebar surfaces with unknown changed files and bump the History refresh key without showing a "Pulled 0 updates" toast. Automatic mount/focus/interval up-to-date checks stay cheap and do not reload the vault.
The same local-only state enables the explicit Add Remote flow. `AddRemoteModal` is reachable from the `No remote` chip and the command palette. The backend `git_add_remote` command adds `origin`, fetches it, refuses incompatible histories, and only enables tracking after a safe push or fast-forward-compatible check succeeds.
`useGitRemoteStatus` re-checks `git_remote_status` for the default repository, and `useCommitFlow` can resolve remote status for an explicit selected repository when the commit dialog opens and again right before submit. If `hasRemote` is false, Tolaria keeps that repository's flow local-only: the status bar shows a neutral `No remote` chip for the default repository, the dialog copy switches from "Commit & Push" to "Commit", and no `git_push` call is attempted.
`useCommitFlow` also exposes `runAutomaticCheckpoint()`, a dialog-free commit path shared by AutoGit and the bottom-bar Commit button. `useAutoGit` watches the last editor activity plus app focus/visibility state, and when the vault is git-backed, all saves are flushed, and no unsaved edits remain, it triggers the same deterministic `Updated N note(s)` / `Updated N file(s)` commit message path after the configured idle or inactive thresholds. The bottom-bar quick action reuses that checkpoint flow after forcing a save first, so manual quick commits and scheduled AutoGit commits stay aligned on message generation and push behavior.
If the current vault is not a Git repository, Tolaria treats Git as unavailable instead of degraded. With global Git features enabled, the status bar replaces changes, commit, sync, remote, conflict, and history controls with a `Git disabled` warning that reopens Git setup unless the user has chosen not to be prompted automatically for that vault. Command registration follows the same state: only `Initialize Git for Current Vault` is available in the Git group, while pull, commit, changes, conflict, and remote commands are hidden. `useAutoSync` is disabled for non-git vaults so the app does not run background Git commands against plain folders.
The installation-local `git_enabled` setting is a broader visibility switch. When it is `false`, Tolaria hides Git status-bar entries and Git command-palette actions completely, disables AutoGit controls in Settings, and prevents background Git refresh/sync work even for repositories that are otherwise Git-backed. Settings remains the re-enable path.
The same local-only state enables the explicit Add Remote flow. `AddRemoteModal` is reachable from the `No remote` chip and the command palette. The backend `git_add_remote` command ensures the local author identity, adds `origin`, fetches it, refuses incompatible histories, and only enables tracking after a safe push or fast-forward-compatible check succeeds.
`useCommitFlow` also exposes `runAutomaticCheckpoint()`, a dialog-free commit path shared by AutoGit and the bottom-bar Commit button. `useAutoGit` watches the last editor activity plus app focus/visibility state, and when the default vault is git-backed, all saves are flushed, and no unsaved edits remain, it triggers the deterministic `Updated N note(s)` / `Updated N file(s)` commit message path after the configured idle or inactive thresholds. In multiple-workspace mode, that checkpoint reads, commits, and pushes every active repository independently; one failed or rejected repository does not prevent the remaining repositories from being attempted. The manual commit dialog remains single-repository and requires the user to choose the target repository when more than one is active.
#### Sync States
@@ -597,11 +710,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` |
| `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` shim), 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`, and optional `GEMINI.md` shims), 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
@@ -610,10 +724,12 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|--------|---------|
| `vault/` | Vault scanning, caching, parsing, rename, image, migration |
| `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 |
| `ai_agents.rs` | Shared CLI-agent detection, stream normalization, and adapter dispatch |
| `claude_cli.rs` | Claude Code subprocess spawning + NDJSON stream parsing |
| `git/` | Git operations and shared system-git helpers (`command.rs`, `remote_config.rs`, `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 |
| `pi_cli.rs`, `pi_config.rs`, `pi_discovery.rs`, `pi_events.rs` | Pi subprocess launch, user-config-seeded 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) |
| `settings.rs` | App settings persistence |
@@ -627,24 +743,27 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| Command | Description |
|---------|-------------|
| `list_vault` | Scan vault (cached) → `Vec<VaultEntry>` |
| `list_vault` | Scan vault (cached), then apply Gitignored-content visibility`Vec<VaultEntry>` |
| `get_note_content` | Read note file content |
| `save_note_content` | Write note content to disk |
| `delete_note` | Permanently delete note from disk (with confirm dialog) |
| `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` | Sync the active vault asset scope, invalidate cache, and full rescan from filesystem → `Vec<VaultEntry>` |
| `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_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` and the `CLAUDE.md` shim are managed, missing, broken, or custom |
| `get_vault_ai_guidance_status` | Report whether `AGENTS.md`, `CLAUDE.md`, and optional `GEMINI.md` guidance are managed, missing, broken, or custom |
| `restore_vault_ai_guidance` | Restore any missing/broken Tolaria-managed guidance files without overwriting custom ones |
### Frontmatter
@@ -663,6 +782,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `git_pull` | Pull from remote |
| `git_push` | Push to remote |
| `git_remote_status` | Get branch name + ahead/behind counts |
| `git_file_url` | Build a remote-backed browser/git URL for a vault file |
| `git_add_remote` | Connect a local-only vault to a compatible remote and start tracking it |
| `git_resolve_conflict` | Resolve a merge conflict |
| `git_commit_conflict_resolution` | Commit conflict resolution |
@@ -695,13 +815,17 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| Command | Description |
|---------|-------------|
| `stream_claude_chat` | Claude CLI chat mode (streaming) |
| `stream_claude_agent` | Claude CLI agent mode (streaming + tools) |
| `check_claude_cli` | Check if Claude CLI is available |
| `get_ai_agents_status` | Check Claude Code + Codex availability |
| `stream_ai_agent` | Stream the selected CLI agent through the normalized 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 |
| `get_ai_agents_status` | Check Claude Code + Codex + OpenCode + Pi + Gemini + Kiro 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, Gemini, or Kiro through the normalized agent event layer |
| `register_mcp_tools` | Register vault-neutral MCP in Claude/Gemini/Cursor/OpenCode/generic config |
| `remove_mcp_tools` | Remove Tolaria's MCP entry from Claude/Gemini/Cursor/OpenCode/generic config |
| `check_mcp_status` | Check whether Tolaria's durable MCP entry is registered in Claude/Gemini/Cursor/OpenCode/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 |
| `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.
@@ -717,8 +841,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 refresh the active vault asset scope |
| `copy_image_to_vault` | Copy image file to `attachments/` and refresh the active vault asset scope |
| `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 |
| `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 |
@@ -750,22 +874,34 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
| `App.tsx` | `selection`, panel widths, dialog visibility, toast, view mode | UI state |
| `useVaultLoader` | `entries`, `allContent`, `modifiedFiles` | Vault data |
| `useNoteActions` | `tabs`, `activeTabPath` | Composes `useNoteCreation` + `useNoteRename` + `frontmatterOps` |
| `useNoteWindowLifecycle` | note-window open/title side effects | Opens `tauri://` note windows without full vault scans and keeps the native title current |
| `useStartupScreenState` | startup visibility booleans | Keeps onboarding, telemetry-consent, missing-vault, and initial indexing decisions out of `App.tsx` |
| `useAppWindowControls` | view mode, panel visibility, command refs, zoom/build labels | Keeps main-window sizing and editor command ref plumbing out of `App.tsx` |
| `useAppViewActions` | saved-view/type creation and saved-view mutation callbacks | Keeps saved-view persistence and Type auto-creation orchestration out of `App.tsx` |
| `useAiWorkspacePublishedContext` | AI workspace note-list snapshot and BroadcastChannel context publishing | Keeps AI workspace context derivation close to its cross-window publication side effect |
| `useMcpSetupDialogController` | MCP setup dialog state/actions | Keeps MCP status, manual config, and connect/disconnect dialog flow out of `App.tsx` |
| `useAiWorkspaceWindowBridgeEvents` | native AI workspace event subscriptions | Owns Tauri listener setup/cleanup for popped-out workspace window bridge events |
| `useGitFileWorkflows` | git diff/history/discard callbacks | Resolves note-scoped repository paths and owns deleted-file preview and queued diff side effects |
| `useVaultRenameDetection` | detected rename banner state | Detects external Git renames on focus and owns the wikilink update callback |
| `useNoteCreation` | — | Note/type creation with optimistic persistence |
| `useNoteRename` | — | Note renaming and folder moves with wikilink update |
| `useNoteRetargeting` | — | Shared note retargeting logic for drag/drop and command-palette actions |
| `useDeepLinks` | Deep-link listener and copy actions | Resolves `tolaria://` links into known vault navigation and clipboard URLs |
| `useTauriDragDropEvent` | — | Shared native window drag/drop event subscription and cleanup |
| `useNativePathDrop` | — | Tauri-native file/folder path drops for AI and command text inputs |
| `frontmatterOps` | — (pure functions) | Frontmatter CRUD: key→VaultEntry mapping, mock/Tauri dispatch |
| `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 |
| `useCliAiAgent` | `messages`, `status`, tool actions | Selected AI agent conversation backed by the shared session pipeline and vault permission mode |
| `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, auto-sync interval, AutoGit thresholds, default AI agent) | 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 |
| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, date display format, auto-sync interval, Git visibility, AutoGit thresholds, default AI agent, Gitignored-content visibility, All Notes file visibility) | Persistent settings |
| `useVaultConfig` | Per-vault UI preferences, Git setup prompt preference, AI permission mode | Vault-specific config |
| `appCommandDispatcher` | Manifest-backed 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`.
@@ -777,21 +913,27 @@ Data flows unidirectionally: `App` passes data and callbacks as props to child c
| Cmd+P / Cmd+O | Open quick open palette |
| Cmd+N | Create new note |
| 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 |
| Cmd+[ / Cmd+] | Navigate back / forward |
| `[[` in editor | Open wikilink suggestion menu |
Selection-dependent actions are wired through the command palette and the native menus. For example, a deleted file opened from Changes view becomes a read-only diff preview, and that state enables the "Restore Deleted Note" menu/command while normal note mutation actions stay disabled. Folder selection follows the same pattern: when `selection.kind === 'folder'`, the command palette exposes "Rename Folder" and "Delete Folder", and the sidebar row can launch the same flows directly through inline rename or the folder context menu. Active notes now follow the same shared-action model for retargeting: Cmd+K can open "Change Note Type…" and "Move Note to Folder…", and the sidebar drop targets call the same hook-backed implementations instead of maintaining separate mutation paths.
Selection-dependent actions are wired through the command palette and the native menus. For example, a deleted file opened from Changes view becomes a read-only diff preview, and that state enables the "Restore Deleted Note" menu/command while normal note mutation actions stay disabled. Folder selection follows the same pattern: when `selection.kind === 'folder'`, the command palette exposes "Reveal Folder in Finder", "Copy Folder Path", "Rename Folder", and "Delete Folder", and the sidebar row can launch the same flows directly through inline rename or the folder context menu. Active files also expose "Reveal in Finder" and "Copy File Path" through the command palette; non-Markdown file tabs additionally expose "Open in Default App", matching the `FilePreview` header controls. Markdown notes expose "Export note as PDF" from Cmd+K, the native Note menu, the breadcrumb overflow menu, and the note-list context menu, all routed through the same editor export hook. Remote-backed notes expose "Copy git URL" from the breadcrumb overflow and note-list context menu; the renderer gates the action per note workspace via `git_remote_status`, then asks `git_file_url` to build the copied URL from the primary remote, current branch, and vault-relative path. Active notes now follow the same shared-action model for retargeting: Cmd+K can open "Change Note Type…" and "Move Note to Folder…", and the sidebar drop targets call the same hook-backed implementations instead of maintaining separate mutation paths.
Shortcut routing is explicit:
- `appCommandCatalog.ts` is the shared shortcut manifest for command IDs, modifier rules, and deterministic QA metadata
- `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
- `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` and `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same command IDs for native menu clicks, accelerators, and custom titlebar menu actions
- 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 the custom titlebar `LinuxMenuButton` emit the same manifest-derived 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()`
@@ -810,17 +952,24 @@ push to main
and a GitHub-sorted tag alpha-vYYYY.M.D-alpha.NNNN
→ use today's UTC date unless the latest stable-vYYYY.M.D tag already uses today
→ if stable already uses today, advance alpha to the next calendar day so semver still increases
→ build job:
→ build-artifacts job, via `.github/workflows/release-build-artifacts.yml`:
→ pnpm install, stamp version, pnpm build, tauri build --target aarch64-apple-darwin --bundles app
upload signed .app.tar.gz + .sig updater artifacts
→ build-windows job:
→ pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis
pnpm install, stamp version, pnpm build, tauri build --target x86_64-apple-darwin --bundles app
→ upload signed Apple Silicon and Intel .app.tar.gz + .sig updater artifacts named Tolaria_<version>_macOS_Silicon and Tolaria_<version>_macOS_Intel
→ pnpm install, stamp version
→ tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
→ verify Linux installer and updater-signature artifacts exist
→ upload .deb, .rpm, .AppImage, and signed Linux updater bundles
→ pnpm install, stamp version, import the Windows code-signing certificate when configured
→ tauri build --target x86_64-pc-windows-msvc --bundles nsis, using the Authenticode signing config when certificate secrets exist
→ verify the Windows app executable and installer Authenticode signatures with Get-AuthenticodeSignature when Authenticode is configured
→ upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles
→ release job:
→ generate alpha-latest.json
→ 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 static HTML release history page
→ build VitePress public docs into the GitHub Pages root
→ build static HTML release history page at /releases/
→ publish alpha/latest.json
→ refresh latest.json + latest-canary.json as compatibility aliases to alpha
→ preserve stable/latest.json
@@ -832,25 +981,32 @@ Stable promotions trigger `.github/workflows/release-stable.yml`:
```
push stable-vYYYY.M.D tag
→ version job: validate YYYY.M.D from the tag
→ build job:
→ build-artifacts job, via `.github/workflows/release-build-artifacts.yml`:
→ pnpm install, stamp version, pnpm build, tauri build --target aarch64-apple-darwin
upload signed .app.tar.gz + .sig and .dmg artifacts
→ build-linux job:
→ pnpm install, stamp version, tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
upload .deb, .AppImage, and signed Linux updater bundles
→ build-windows job:
pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis
pnpm install, stamp version, pnpm build, tauri build --target x86_64-apple-darwin
→ upload signed Apple Silicon and Intel .app.tar.gz + .sig and .dmg artifacts named Tolaria_<version>_macOS_Silicon and Tolaria_<version>_macOS_Intel
→ pnpm install, stamp version
tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
→ verify Linux installer and updater-signature artifacts exist
upload .deb, .rpm, .AppImage, and signed Linux updater bundles
→ pnpm install, stamp version, import the Windows code-signing certificate
→ tauri build --target x86_64-pc-windows-msvc --bundles nsis with Authenticode signing config
→ verify the Windows app executable and installer Authenticode signatures with Get-AuthenticodeSignature
→ upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles
→ release job:
→ generate stable-latest.json with macOS, Linux, and Windows updater URLs plus platform-specific manual download URLs
→ 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 redirect URLs for the latest stable platform installer
→ publish stable/download/ and download/ as permanent download pages that keep the browser page visible while the platform installer starts, default Linux visitors to AppImage, require an explicit Windows installer click with managed-device signing guidance, and expose RPM as a manual Linux option when the stable release includes one
→ preserve alpha/latest.json
→ deploy to gh-pages
```
Linux AppImage release jobs use Tauri's stock linuxdeploy AppImage output plugin. `scripts/appimage-launcher-tools.mjs` remains available for local experiments with symlink-safe AppRun patching and fcitx module bundling, but release packaging does not pre-seed that shim because linuxdeploy currently exits before sealing the AppImage when the shim replaces the stock output plugin in Tauri's tools cache.
### Versioning
- Stable promotions use git tags in the form `stable-vYYYY.M.D` and stamp the technical version `YYYY.M.D`.
@@ -887,7 +1043,7 @@ sequenceDiagram
App->>User: TelemetryConsentDialog
alt Accept
User->>Settings: telemetry_consent=true, anonymous_id=UUID
Settings->>Sentry: init(DSN, anonymous_id)
Settings->>Sentry: init(DSN, release, anonymous_id)
Settings->>PostHog: init(key, anonymous_id)
else Decline
User->>Settings: telemetry_consent=false
@@ -905,10 +1061,12 @@ 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.
- **Settings:** `telemetry_consent`, `crash_reporting_enabled`, `analytics_enabled`, `anonymous_id` in `Settings` struct
- **Consent:** `TelemetryConsentDialog` shown when `telemetry_consent === null`
@@ -963,7 +1121,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)
- CLI AI agent streaming (Claude, Codex, OpenCode, Pi, Gemini, Kiro)
- MCP registration and status
- Menu state updates

View File

@@ -29,6 +29,32 @@ 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 Linux builds automatically disable unstable WebKitGTK rendering paths on Wayland and AppImage launches. AppImage launches also retry startup with an architecture-matching system Wayland client library when they detect this class of AppImage + Wayland environment. If you are running an older build, use this workaround:
```bash
WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/usr/lib64/libwayland-client.so.0 ./Tolaria*.AppImage
```
If your distribution stores the 64-bit library elsewhere, use that path instead, for example `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`. On 64-bit Fedora, avoid `/usr/lib/libwayland-client.so.0`; that path can point at a 32-bit library and be ignored by the loader with a wrong ELF class warning.
### Linux AppImage packaging checks
Linux release CI currently uses Tauri's stock linuxdeploy AppImage output plugin:
```bash
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
```
Release validation verifies that the Linux job produced an AppImage, at least one installer bundle, and updater signature artifacts. Windows release jobs import the CI code-signing certificate, build NSIS with a generated Tauri Authenticode signing config, and verify the app executable plus installer signatures before upload. The experimental AppImage output-plugin shim in `scripts/appimage-launcher-tools.mjs` is retained for local investigation, but it is not wired into release packaging because linuxdeploy currently exits before sealing the AppImage when the shim is pre-seeded in Tauri's tools cache.
## Quick Start
```bash
@@ -43,9 +69,14 @@ pnpm dev
pnpm tauri dev
# Run tests
pnpm test # Vitest unit tests
cargo test # Rust tests (from src-tauri/)
pnpm playwright:smoke # Curated Playwright core smoke lane (~5 min)
pnpm test # Vitest unit tests
cargo test # Rust tests (from src-tauri/)
# Or, run Rust tests from root project directory
cargo test --manifest-path src-tauri/Cargo.toml
# E2E tests
pnpm playwright:smoke # Curated Playwright core smoke lane (~5 min)
pnpm playwright:regression # Full Playwright regression suite
```
@@ -53,20 +84,30 @@ 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` and `node`. Before Tolaria spawns those Git or MCP Node subprocesses, it removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` so HTTPS clone helpers and MCP tooling use the host library stack instead of bundled AppImage libraries.
## Multiple Vaults At The Same Time
The `settings.multi_workspace_enabled` flag turns the registered vault list into a unified graph. When enabled, `useVaultLoader` loads every available mounted vault, annotates entries with workspace provenance, and lets note lists, quick open, keyword search, backlinks, and wikilink navigation span those vaults.
The selected/default vault remains the write target for new notes and Type documents when `defaultWorkspacePath` points at an available mounted vault. Git status, changes, AutoGit checkpointing, and sync operate across the active mounted repository set, while history, diff, repair, and file operations still resolve explicit repository roots from the selected surface or entry provenance. Saved Views are listed from every mounted vault with source-vault identity, so duplicate view filenames remain separate and edits persist back to the view's owning vault.
The bottom-left `VaultMenu` exposes quick include/exclude controls and a `Manage vaults` entry. The Vaults settings section owns the full identity controls: display name, short label, read-only alias, accent color, removal, and default destination for new notes.
## Directory Structure
```
tolaria/
├── src/ # React frontend
│ ├── main.tsx # Entry point (renders <App />)
│ ├── App.tsx # Root component — orchestrates layout + state
│ ├── App.tsx # Root component — wires layout + state hooks
│ ├── App.css # App shell layout styles
│ ├── types.ts # Shared TS types (VaultEntry, Settings, etc.)
│ ├── mock-tauri.ts # Mock Tauri layer for browser testing
│ ├── theme.json # Editor typography theme configuration
│ ├── index.css # Semantic app theme variables + Tailwind setup
│ │
│ ├── components/ # UI components (~98 files)
│ ├── components/ # UI components (~100 files)
│ │ ├── Sidebar.tsx # Left panel: filters + type groups
│ │ ├── SidebarParts.tsx # Sidebar subcomponents
│ │ ├── NoteList.tsx # Second panel: filtered note list
@@ -79,7 +120,10 @@ 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)
│ │ ├── AiWorkspace.tsx # Multi-chat AI workspace orchestration (docked or native window)
│ │ ├── AiWorkspaceChrome.tsx # AI workspace header and vault-guidance chrome
│ │ ├── AiWorkspaceResizeHandles.tsx # AI workspace edge resize handles
│ │ ├── AiPanel.tsx # AI transcript/composer surface (selected target + per-vault permission mode)
│ │ ├── AiMessage.tsx # Agent message display
│ │ ├── AiActionCard.tsx # Agent tool action cards
│ │ ├── AiAgentsOnboardingPrompt.tsx # First-launch AI agent installer prompt
@@ -89,7 +133,7 @@ tolaria/
│ │ ├── CommandPalette.tsx # Cmd+K command launcher
│ │ ├── BreadcrumbBar.tsx # Breadcrumb + word count + actions
│ │ ├── WelcomeScreen.tsx # Onboarding screen
│ │ ├── LinuxTitlebar.tsx # Linux-only custom window chrome + controls
│ │ ├── LinuxTitlebar.tsx # Linux/Windows custom window chrome + controls
│ │ ├── LinuxMenuButton.tsx # Linux titlebar menu mirroring app commands
│ │ ├── CloneVaultModal.tsx # Clone a vault from any git URL
│ │ ├── AddRemoteModal.tsx # Connect a local-only vault to a remote later
@@ -106,16 +150,16 @@ tolaria/
│ │ └── ui/ # shadcn/ui primitives
│ │ ├── button.tsx, dialog.tsx, input.tsx, ...
│ │
│ ├── hooks/ # Custom React hooks (~87 files)
│ ├── hooks/ # Custom React hooks (~90 files)
│ │ ├── useVaultLoader.ts # Loads vault entries + content
│ │ ├── useVaultSwitcher.ts # Multi-vault management
│ │ ├── useVaultConfig.ts # Per-vault UI settings
│ │ ├── useNoteActions.ts # Composes creation + rename + frontmatter
│ │ ├── useNoteCreation.ts # Note/type creation
│ │ ├── useNoteRename.ts # Note renaming + wikilink updates
│ │ ├── useAiAgent.ts # Legacy Claude-specific stream helpers reused by the shared agent hook
│ │ ├── useCliAiAgent.ts # Selected AI agent state + normalized tool tracking
│ │ ├── useAiAgentsStatus.ts # Claude/Codex availability polling
│ │ ├── useCliAiAgent.ts # Selected AI agent state + normalized session pipeline
│ │ ├── aiAgentPermissionMode.ts # Safe/Power User mode normalization + labels
│ │ ├── useAiAgentsStatus.ts # Claude/Codex/OpenCode/Pi/Gemini/Kiro availability polling
│ │ ├── useAiAgentPreferences.ts # Default-agent persistence + cycling
│ │ ├── useAiActivity.ts # MCP UI bridge listener
│ │ ├── useAutoSync.ts # Auto git pull/push
@@ -141,6 +185,7 @@ 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
@@ -157,6 +202,8 @@ tolaria/
│ ├── lib/
│ │ ├── aiAgents.ts # Shared agent registry + status helpers
│ │ ├── appUpdater.ts # Frontend wrapper around channel-aware updater commands
│ │ ├── i18n.ts # App-owned localization runtime and locale resolution
│ │ ├── locales/ # JSON locale catalogs (English source + translated locales)
│ │ ├── releaseChannel.ts # Alpha/stable normalization helpers
│ │ └── utils.ts # Tailwind merge + cn() helper
│ │
@@ -183,22 +230,26 @@ tolaria/
│ │ ├── frontmatter/ # Frontmatter module
│ │ │ ├── mod.rs, yaml.rs, ops.rs
│ │ ├── git/ # Git module
│ │ │ ├── mod.rs, commit.rs, status.rs, history.rs, clone.rs, connect.rs
│ │ │ ├── conflict.rs, remote.rs, pulse.rs
│ │ │ ├── mod.rs, command.rs, remote_config.rs, commit.rs, status.rs
│ │ │ ├── history.rs, clone.rs, connect.rs, conflict.rs, remote.rs, pulse.rs
│ │ ├── telemetry.rs # Sentry init + path scrubber
│ │ ├── search.rs # Keyword search (walkdir-based)
│ │ ├── ai_agents.rs # Shared CLI-agent detection + stream adapters
│ │ ├── claude_cli.rs # Claude CLI subprocess management
│ │ ├── ai_agents.rs # CLI-agent request normalization + adapter dispatch
│ │ ├── cli_agent_runtime.rs # Shared CLI-agent runtime process/prompt/MCP helpers
│ │ ├── claude_cli.rs # Claude CLI adapter
│ │ ├── codex_cli.rs # Codex CLI adapter
│ │ ├── pi_cli.rs # Pi CLI adapter
│ │ ├── kiro_cli.rs # Kiro CLI adapter
│ │ ├── mcp.rs # MCP server lifecycle + explicit config registration/removal
│ │ ├── app_updater.rs # Alpha/stable updater endpoint selection
│ │ ├── app_updater.rs # Alpha/stable updater metadata resolution
│ │ ├── settings.rs # App settings persistence
│ │ ├── vault_config.rs # Per-vault UI config
│ │ ├── vault_list.rs # Vault list persistence
│ │ └── menu.rs # Native macOS menu bar
│ └── icons/ # App icons
├── mcp-server/ # MCP bridge (Node.js)
│ ├── index.js # MCP server entry (stdio, 14 tools)
├── mcp-server/ # MCP bridge (Node.js or Bun)
│ ├── index.js # MCP server entry (stdio tools)
│ ├── vault.js # Vault file operations
│ ├── ws-bridge.js # WebSocket bridge (ports 9710, 9711)
│ ├── test.js # MCP server tests
@@ -211,6 +262,7 @@ tolaria/
├── scripts/ # Build/utility scripts
├── package.json # Frontend dependencies + scripts
├── lara.yaml # Lara CLI locale sync configuration
├── vite.config.ts # Vite bundler config
├── tsconfig.json # TypeScript config
├── playwright.config.ts # Full Playwright regression config
@@ -233,7 +285,7 @@ tolaria/
| File | Why it matters |
|------|---------------|
| `src/App.tsx` | Root component. Shows the 4-panel layout, state flow, and how all features connect. |
| `src/App.tsx` | Root component. Shows the 4-panel layout, state flow, and how orchestration hooks connect. |
| `src/types.ts` | All shared TypeScript types. Read this first to understand the data model. |
| `src-tauri/src/commands/` | Tauri command handlers (split into modules). This is the frontend-backend API surface. |
| `src-tauri/src/lib.rs` | Tauri setup, command registration, startup tasks, WebSocket bridge lifecycle. |
@@ -246,6 +298,10 @@ tolaria/
| `src/hooks/useNoteActions.ts` | Orchestrates note operations: composes `useNoteCreation`, `useNoteRename`, frontmatter CRUD, and wikilink navigation. |
| `src/hooks/useVaultSwitcher.ts` | Multi-vault management, vault switching, and persisting cloned vaults in the switcher list. |
| `src/hooks/useGettingStartedClone.ts` | Shared "Clone Getting Started Vault" action for the status bar and command palette. |
| `src/hooks/useNoteWindowLifecycle.ts` | Note-window URL opening, asset-scope sync, and window-title updates. |
| `src/hooks/useVaultRenameDetection.ts` | Focus-triggered Git rename detection and wikilink update action wiring. |
| `src/hooks/useStartupScreenState.ts` | Startup-screen and vault-content loading visibility decisions. |
| `src/hooks/useGitFileWorkflows.ts` | Git diff/history/discard wiring and deleted-note preview workflow. |
| `src/components/AddRemoteModal.tsx` | Modal UI for connecting a local-only vault to a compatible remote. |
| `src/mock-tauri.ts` | Mock data for browser testing. Shows the shape of all Tauri responses. |
@@ -258,9 +314,10 @@ 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` | Shared CLI-agent availability checks, safe-default Codex adapter, and stream normalization. |
| `src-tauri/src/claude_cli.rs` | Claude CLI subprocess spawning + NDJSON stream parsing. |
| `src-tauri/src/app_updater.rs` | Desktop updater bridge — selects alpha/stable manifests and streams install progress. |
| `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`, `src-tauri/src/kiro_cli.rs` | Per-agent command, config, discovery, and event adapters. |
| `src-tauri/src/app_updater.rs` | Desktop updater bridge — resolves alpha/stable manifests and streams install progress. |
### Editor
@@ -278,8 +335,17 @@ tolaria/
| File | Why it matters |
|------|---------------|
| `src/components/AiPanel.tsx` | AI agent panel — selected CLI agent with tool execution, reasoning, and actions. |
| `src/hooks/useCliAiAgent.ts` | Agent state: messages, streaming, tool tracking, file detection. |
| `src/components/AiWorkspace.tsx` | Multi-chat AI workspace orchestration — chat sessions, sidebar tabs, target/permission controls, and dock/pop-out wiring. |
| `src/components/AiWorkspaceChrome.tsx` | Header and vault-guidance chrome shared by docked and popped-out AI workspace modes. |
| `src/components/AiWorkspaceResizeHandles.tsx` | Edge resize affordances for docked/side AI workspace layouts. |
| `src/components/aiWorkspaceConversations.ts` | Conversation metadata state, settings persistence, default title generation, and target resolution. |
| `src/components/aiWorkspaceSizing.ts` | AI workspace sizing, localStorage persistence, class names, and layout style helpers. |
| `src/components/AiPanel.tsx` | Reusable AI transcript/composer surface — selected target with tool execution, reasoning, actions, and per-vault permission mode. |
| `src/utils/openAiWorkspaceWindow.ts` | Native Tauri AI workspace window creation, focus, and dock-back traffic-light handling. |
| `src/hooks/useCliAiAgent.ts` | Thin React owner for the selected CLI agent session state. |
| `src/lib/aiAgentSession.ts` | Single message/session lifecycle for prompt normalization, history, streaming, and reset behavior. |
| `src/lib/aiAgentPermissionMode.ts` | Safe/Power User mode normalization, display labels, and local transcript marker text. |
| `src/lib/aiAgentFileOperations.ts` | Detects agent-created or modified vault files from normalized tool inputs. |
| `src/lib/aiAgents.ts` | Supported agent definitions, status normalization, and default-agent helpers. |
| `src/utils/ai-context.ts` | Context snapshot builder for AI conversations. |
@@ -287,19 +353,19 @@ tolaria/
| File | Why it matters |
|------|---------------|
| `src/index.css` | Semantic CSS custom properties for app-owned light/dark themes. |
| `src/index.css` | Semantic CSS custom properties for app-owned light/dark themes; System mode resolves to one of these at runtime. |
| `src/theme.json` | Editor-specific typography theme (fonts, headings, lists, code blocks). |
### Settings & Config
| File | Why it matters |
|------|---------------|
| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, auto-sync interval, default AI agent). |
| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, UI language, date display format, Git visibility, auto-sync interval, default note width, sidebar type pluralization, default AI agent). |
| `src/lib/releaseChannel.ts` | Normalizes persisted updater-channel values (`stable` default, optional `alpha`). |
| `src/lib/appUpdater.ts` | Frontend wrapper for channel-aware updater commands. |
| `src/hooks/useMainWindowSizeConstraints.ts` | Derives the main-window minimum width from the visible panes and asks Tauri to grow back to fit wider layouts. |
| `src/hooks/useVaultConfig.ts` | Per-vault local UI preferences (zoom, view mode, colors, Inbox columns, explicit organization workflow). |
| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, sync interval, 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, Git setup prompt preference, AI permission mode). |
| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, Git visibility, sync interval, UI language, content display preferences, default AI agent, and the vault-level explicit organization toggle. |
| `src/hooks/useUpdater.ts` | In-app updates using the selected alpha/stable feed. |
## Architecture Patterns
@@ -335,10 +401,12 @@ 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. 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.
`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. Settings commands can update installation-local preferences directly when they reuse an existing settings path, such as the Light/Dark/System theme-mode actions writing `settings.theme_mode`. Shortcut combos live in `appCommandCatalog.ts`; real keypresses always flow through `useAppKeyboard`, native menu clicks emit the same command IDs through `useMenuEvents`, and `appCommandDispatcher.ts` suppresses the duplicate native/renderer echo from a single shortcut. Plain-text paste follows this same path: the command owns `Cmd+Shift+V`, the menu and palette expose the same action, and `plainTextPaste.ts` resolves the active rich/raw editor target or focused text control before reading clipboard text. On macOS, any browser-reserved chord that WKWebView swallows before that path must also be added to the narrow `tauri-plugin-prevent-default` registration in `src-tauri/src/lib.rs`. On Linux and Windows, `LinuxTitlebar.tsx` and `LinuxMenuButton.tsx` reuse the same command IDs through `trigger_menu_command` because those builds use Tolaria's custom chrome instead of the native desktop menu bar. The same shortcut manifest also declares the deterministic QA mode for each shortcut-capable command.
Commands whose availability depends on the current note or Git state must also flow through `update_menu_state` so the native menu stays in sync with the command palette. The deleted-note restore action in Changes view is the reference example: the row opens a deleted diff preview, the command palette exposes "Restore Deleted Note", and the Note menu enables the same action only while that preview is active.
Current-note find/replace is a surface-aware command: editor focus enables "Find in Note" / "Replace in Note" and routes Cmd+F into raw CodeMirror mode; note-list focus enables existing note-list search instead. When adding another focus-dependent command, mirror this pattern with an availability event consumed by `useMenuEvents.ts` and `update_menu_state`.
For automated shortcut QA, use the explicit proof path from `appCommandCatalog.ts`:
- `window.__laputaTest.triggerShortcutCommand()` for deterministic renderer shortcut-event coverage
@@ -378,7 +446,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
4. Call it from the frontend via `invoke()` in the appropriate hook or utility, keeping native-only permission work behind the Tauri command boundary
5. Add a mock handler in `mock-tauri.ts`
### Add a new component
@@ -390,7 +458,7 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
### Add a new entity type
1. Create a type document: `type/mytype.md` with `type: Type` frontmatter (icon, color, order, etc.)
1. Create a type document at the vault root: `mytype.md` with `type: Type` frontmatter (icon, color, order, etc.)
2. The sidebar section groups are auto-generated from type documents — no code change needed if `visible: true`
3. Update `CreateNoteDialog.tsx` type options if users should be able to create it from the dialog
4. Notes of this type are created at the vault root with `type: MyType` in frontmatter — no dedicated folder needed
@@ -412,5 +480,16 @@ 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. **Claude CLI arguments**: Edit `src-tauri/src/claude_cli.rs` (`run_agent_stream()`)
5. **Shared agent adapters / Codex args**: Edit `src-tauri/src/ai_agents.rs` (keep Codex on the normal approval/sandbox path unless you are intentionally designing an advanced mode)
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_*`, `kiro_*`). Keep Codex Safe on `read-only` + `untrusted` and Codex Power User on active-vault `workspace-write` + `never`, keep Pi, Gemini, and Kiro on transient MCP config, and do not use dangerous permission bypasses unless an ADR explicitly designs a new mode. Pi's transient agent directory must be seeded from the user's existing Pi agent directory before Tolaria MCP is merged so standalone provider/auth setup keeps working. Gemini Power User intentionally uses Gemini's `yolo` mode per ADR-0103. Kiro receives prompt content over stdin and writes Tolaria MCP config into `.kiro/settings/mcp.json` in the active vault.
7. **Availability probing**: Edit `src/hooks/useAiAgentsStatus.ts` and `src-tauri/src/ai_agents.rs` for AI-agent install/status detection. Keep renderer probing deferred until after first paint, skip it when AI features or AI surfaces are unavailable, and keep backend per-agent CLI checks parallel so missing tools do not serialize shell startup cost.
### Work with external MCP setup
1. **Backend registration/status/snippets**: Edit `src-tauri/src/mcp.rs` and its `src-tauri/src/mcp/` helpers; registration and manual config generation must resolve an MCP runtime via `find_mcp_runtime` (Node.js 18+ preferred, Bun 1+ fallback) first, resolve the packaged `mcp-server/` for macOS, Windows executable-adjacent installs such as `%LOCALAPPDATA%\Tolaria`, Linux package roots (`/usr/local/Tolaria`, `/usr/local/lib/tolaria`, `/usr/lib/tolaria`, `/usr/lib/tolaria/resources`), and AppImage installs, and use a vault-neutral entry with `WS_UI_PORT=9711`. Linux AppImage startup must extract `mcp-server/` to `~/.local/share/tolaria/mcp-server/` before durable registration uses that stable path. App-owned bridge launches still pass `VAULT_PATH`/`VAULT_PATHS`; durable external registrations rely on the MCP server reading `vaults.json` at tool-call time.
2. **Setup dialog copy/actions**: Edit `src/components/McpSetupDialog.tsx` and `src/hooks/useMcpStatus.ts`; users should see the runtime prerequisite (Node.js 18+ or Bun 1+), the exact generated manual config, and a copy action 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. **OpenCode compatibility**: Keep `~/.config/opencode/opencode.json` in durable registration. OpenCode uses the top-level `mcp` key, `command` as an array, `environment` for env vars, `type: "local"`, and `enabled: true`; it must remain vault-neutral like the standard `mcpServers` entry.
6. **Process lifecycle and vault guidance**: Stdio MCP servers in `mcp-server/index.js` must exit when their external client closes stdin, and the desktop-owned `ws-bridge.js` child must be stopped on vault deselection, vault switch, and app exit. MCP context must include root `AGENTS.md` instructions for every active mounted workspace when those files exist.

47
docs/PUBLIC-DOCS-PLAN.md Normal file
View File

@@ -0,0 +1,47 @@
# Public Docs Plan
This document records the phase 1 information architecture for public Tolaria documentation. The public docs source lives in `site/`; the existing `docs/` directory remains contributor, architecture, and agent context.
## Audiences
| Audience | Needs | Primary location |
|---|---|---|
| New users | Install, first launch, understand the app layout, clone the starter vault | `site/start/` |
| Active users | Learn concrete workflows such as organizing, Git sync, custom views, and AI | `site/guides/` |
| Power users | Understand file layout, frontmatter, filters, release channels, shortcuts, and platform support | `site/reference/` |
| Contributors and agents | Architecture, abstractions, ADRs, development workflow | `docs/`, `AGENTS.md` |
## Hosting Shape
The GitHub Pages output should reserve the root for public docs and mount release assets underneath it:
```text
/ public docs home
/releases/ release history
/download/ latest stable download redirect
/stable/latest.json
/alpha/latest.json
/latest.json compatibility alias for alpha latest
/latest-canary.json compatibility alias for alpha latest
```
## Current Coverage
The phase 1 site now covers post-branch features added after the original April docs snapshot:
- Windows and Linux release artifacts.
- Stable and Alpha updater channels.
- Direct AI model providers and local/API model setup.
- Claude Code, Codex, OpenCode, Pi, and 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
Public docs impact:
- updated: <pages>
- not needed because: <reason>
```

View File

@@ -2,8 +2,9 @@
type: ADR
id: "0026"
title: "Props-down callbacks-up (no global state management)"
status: active
status: superseded
date: 2026-02-15
superseded_by: "0115"
---
## Context

View File

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

View File

@@ -0,0 +1,38 @@
---
type: ADR
id: "0083"
title: "Dual-architecture macOS release artifacts"
status: active
date: 2026-04-26
supersedes: "0080"
---
## Context
ADR-0080 made Tolaria's desktop release pipeline cross-platform, but the macOS leg still shipped only Apple Silicon artifacts. That left Intel Mac users without a compatible build in both the alpha feed and stable releases, even though Tauri and Rust can produce `x86_64-apple-darwin` bundles from the same release workflow.
The updater manifest also needs to distinguish macOS CPU architectures. A generic `darwin` or macOS-only entry would make it too easy for an Intel installation to see an Apple Silicon updater bundle, and browser user agents cannot reliably tell Apple Silicon Macs apart from Intel Macs.
## Decision
**Tolaria publishes macOS release artifacts for both Apple Silicon (`darwin-aarch64`) and Intel (`darwin-x86_64`) in every alpha and stable release.**
- Alpha and stable workflows build the macOS matrix for `aarch64-apple-darwin` and `x86_64-apple-darwin`.
- Alpha manifests include signed updater tarballs for `darwin-aarch64` and `darwin-x86_64`.
- Stable manifests include both macOS updater tarballs and both manual DMG downloads, alongside the existing Windows x64 and Linux x86_64 entries.
- Release jobs normalize macOS artifact filenames with the architecture suffix before publishing so GitHub release assets stay unambiguous.
- The stable download page exposes separate Apple Silicon and Intel Mac links. When both Mac links exist, a generic macOS browser is not auto-redirected because user-agent architecture detection is unreliable.
- The cross-platform filename portability decisions from ADR-0080 remain in force.
## Options considered
- **Publish separate Apple Silicon and Intel Mac artifacts** (chosen): gives each updater client an architecture-specific manifest key and gives users explicit manual download links. Cons: doubles the macOS release matrix and signing/notarization surface.
- **Publish a universal macOS binary**: gives users one download, but requires lipo/re-sign/notarize coordination and reintroduces the artifact-combining complexity the release pipeline intentionally avoided.
- **Keep Apple Silicon-only macOS releases**: keeps CI cheaper, but leaves Intel Mac users unsupported and makes the release artifacts inconsistent with Tolaria's desktop support goals.
## Consequences
- macOS release jobs now run one matrix entry per CPU architecture.
- Release manifest consumers must treat `darwin-aarch64` and `darwin-x86_64` as distinct platform keys.
- Stable manual downloads show two Mac choices instead of pretending browser detection can select the right CPU architecture.
- Future macOS release changes must validate both updater and manual-download artifacts for both architectures.

View File

@@ -0,0 +1,35 @@
---
type: ADR
id: "0084"
title: "App-owned localization foundation"
status: active
date: 2026-04-26
---
## Context
Tolaria was effectively English-only. Users requested a general i18n foundation and Chinese-language support. We need a path that lets the UI adopt additional locales without pushing UI-language preferences into vault files or making every partially translated string a runtime failure.
## Decision
Tolaria owns a dependency-free frontend localization layer in `src/lib/i18n.ts`.
- English is the canonical fallback locale.
- Simplified Chinese (`zh-Hans`) is the first additional locale.
- `ui_language` is an installation-local app setting in `~/.config/com.tolaria.app/settings.json`; `null` means "follow system language when supported, otherwise English".
- Missing translation keys fall back to English.
- App-level chrome receives locale through props from `App.tsx`, following the existing props-down/callbacks-up architecture instead of introducing global React context.
- Language switching is exposed in Settings and through command-palette actions.
## Alternatives considered
- **Add an i18n dependency now**: useful long term, but unnecessary for the first locale and would add framework surface before we know Tolaria's locale workflow.
- **Store language in the vault**: rejected because UI language is an installation preference, not content structure.
- **Translate ad hoc strings inline**: rejected because it would make fallback behavior inconsistent and future locales expensive.
## Consequences
- New UI strings should be added to `src/lib/i18n.ts` first and rendered through `translate()` / `createTranslator()` where the surface already receives locale.
- Partially translated locales remain usable because English is the fallback for missing keys.
- Locale choice changes UI chrome immediately after settings save or command-palette language commands without reopening the vault.
- Larger feature surfaces can migrate to the shared localization module incrementally.

View File

@@ -0,0 +1,39 @@
---
type: ADR
id: "0085"
title: "Non-git vaults open with explicit later Git initialization"
status: active
date: 2026-04-26
supersedes: "0034"
---
## Context
ADR-0034 made Git a hard prerequisite for opening a vault because Git-backed cache, history, change, and sync flows failed invisibly when users opened plain Markdown folders. That protected Git features, but it blocked the common adoption path of opening an existing folder from Obsidian, iCloud, Dropbox, or a manually maintained notes directory.
Tolaria now needs the opposite default: browsing and editing Markdown should work immediately, while Git remains an explicit capability users can enable when they want history, sync, commits, or collaboration.
## Decision
**Open existing Markdown folders even when they are not Git repositories.** A non-git vault is a supported state, not an error state. On open, Tolaria asks whether to initialize Git; if the user dismisses the prompt, the app keeps working and the status bar permanently shows a `Git disabled` warning. Clicking that warning, or running `Initialize Git for Current Vault` from the command palette, reopens the setup action.
While a vault is not Git-backed:
- Git history, change, commit, sync, conflict, and remote actions are hidden or disabled.
- Background auto-sync and AutoGit checkpoints do not run.
- Markdown scanning, note browsing, note editing, search, and non-Git vault features continue normally.
`init_git_repo` remains the single backend command for enabling Git later. It creates the repository, writes Tolaria's default `.gitignore`, stages the vault, and creates the unsigned setup commit.
## Options considered
- **Option A (chosen): Supported non-git mode with explicit later initialization.** Best adoption path; keeps Git capabilities visible without blocking the basic notes workflow.
- **Option B: Keep the ADR-0034 blocking modal.** Prevents Git feature ambiguity, but rejects valid plain-folder workflows.
- **Option C: Auto-initialize Git when opening a plain folder.** Low friction, but surprising for users who do not want Tolaria to mutate folder metadata.
## Consequences
- Existing Git-backed vaults keep the same history, commit, sync, and remote behavior.
- UI surfaces must treat Git capability as stateful per vault, not as an app-wide invariant.
- Tests need to cover both Git-backed and non-git vaults in browser mocks and native QA.
- Future Git-dependent features must check the current vault's Git state before registering commands or running background work.

View File

@@ -0,0 +1,32 @@
---
type: ADR
id: "0086"
title: "In-app image previews for binary vault files"
status: active
date: 2026-04-26
supersedes: "0041"
---
## Context
ADR-0041 made the vault scanner index all visible files and introduced `fileKind` as `"markdown"`, `"text"`, or `"binary"`. Binary files were deliberately shown as inert entries until Tolaria had a dedicated preview model.
That made image references visible in folder views, but opening an image still felt outside the normal Tolaria workflow. Users need to inspect screenshots, diagrams, and other image assets while keeping their place in the vault.
## Decision
**Tolaria previews supported image files in the editor pane while keeping them as ordinary binary `VaultEntry` files.**
- The scanner keeps the existing `fileKind: "binary"` representation. Image previewability is inferred in the renderer from the file extension, not by introducing a proprietary image document type.
- Opening a binary entry creates the same single active-tab state used for notes, but with empty content and no `get_note_content` text read.
- `FilePreview` renders supported image extensions through Tauri's asset protocol (`convertFileSrc`) so the original file remains on disk.
- Broken images and unsupported binary files render an explicit fallback state with an intentional "Open in default app" action instead of launching another app automatically.
- Note-list rows use an image indicator for previewable image binaries. Unsupported binary rows remain muted and non-clickable in the normal list surface.
- The preview surface is keyboard focusable and `Escape` returns focus to the note list, matching the app's keyboard-first navigation model.
## Consequences
- The existing `VaultEntry` model and cache version do not need to change.
- Supported image files can participate in normal selection/navigation context without being converted into Markdown notes.
- Unsupported/broken binary files have a clear in-app state when reached through navigation paths that can select them.
- Any future PDF, audio, or video preview should extend the same file-preview renderer rather than adding new vault-owned document representations.

View File

@@ -0,0 +1,39 @@
---
type: ADR
id: "0087"
title: "JSON locale catalogs with Lara CLI synchronization"
status: active
date: 2026-04-27
supersedes:
- "0084"
---
## Context
ADR-0084 established an app-owned localization layer in `src/lib/i18n.ts` with English fallback and hand-maintained TypeScript dictionaries. That was enough for the first localized UI surface, but it does not scale well to a broader locale matrix or machine-assisted translation workflows.
We now want Tolaria to support a wider set of locales and to automate translation updates with Lara CLI while keeping the runtime dependency-light and preserving the existing English fallback behavior.
## Decision
Tolaria will keep its app-owned runtime localization layer, but the translation source-of-truth moves to flat JSON catalogs in `src/lib/locales/`.
- `src/lib/locales/en.json` is the canonical source catalog.
- Additional locale files use one JSON file per locale code (for example `zh-CN.json`, `fr-FR.json`).
- `src/lib/i18n.ts` keeps fallback, interpolation, locale resolution, and props-down locale wiring, but it now loads locale catalogs from JSON files instead of TypeScript objects.
- Lara CLI configuration lives in `lara.yaml`, and translation runs happen through repo scripts (`pnpm l10n:translate`, `pnpm l10n:translate:force`).
- `scripts/validate-locales.mjs` verifies that every locale catalog present in the repo matches the English keyset and only contains flat string values.
- Legacy stored preferences such as `zh-Hans` are normalized to the canonical `zh-CN` locale.
## Alternatives considered
- **Keep TypeScript dictionaries and point Lara at `.ts` files**: possible, but JSON is the more standard interchange format for translation tooling and keeps diffs simpler for translators and reviewers.
- **Adopt a full frontend i18n framework now**: rejected because Tolaria already has working locale propagation and fallback behavior, and the immediate need is better content management plus translation automation.
- **Store translated strings outside the app repo**: rejected because Tolaria's chrome localization should stay versioned with the app code that consumes it.
## Consequences
- Translators and automation tools now work against plain JSON catalogs instead of editing source code.
- The runtime keeps English fallback behavior, so a missing locale file or missing key does not break app chrome.
- Locale additions become a data/config change first: add the locale metadata, run Lara, review JSON output, then ship.
- Localization work now has a dedicated validation step that can run in CI or before commit.

View File

@@ -0,0 +1,40 @@
---
type: ADR
id: "0088"
title: "Markdown-durable Mermaid diagrams in notes"
status: active
date: 2026-04-27
---
## Context
Tolaria notes are plain Markdown files, while the rich editor uses BlockNote and raw mode uses CodeMirror. Users need fenced `mermaid` blocks to render as diagrams in the note surface without changing the canonical file format or hiding the source from raw editing.
BlockNote can parse fenced code blocks, but a generic highlighted code block does not provide diagram rendering. Rendering Mermaid directly from the Markdown fence also has to preserve the original fence source when notes are saved, copied through raw mode, closed, and reopened.
## Decision
**Tolaria will support Mermaid diagrams through a Markdown placeholder round-trip owned by the editor pipeline and rendered with the `mermaid` package.**
The implementation:
- Converts fenced `mermaid` blocks to temporary placeholders before BlockNote parses Markdown.
- Replaces placeholders with a `mermaidBlock` schema block that stores both the original fenced source and the diagram body.
- Renders the block through Mermaid in the rich editor.
- Serializes `mermaidBlock` nodes back to their stored fenced Markdown before save, raw-mode entry, and editor-position snapshots.
- Shows the original source as an inline fallback when Mermaid cannot render a diagram.
## Options considered
- **Tolaria-owned placeholder round-trip with Mermaid rendering** (chosen): matches the existing wikilink and math architecture, keeps Markdown as the source of truth, and gives Tolaria explicit control over serialization.
- **Render all `mermaid` code blocks by overriding the generic code-block renderer**: smaller surface, but it couples diagram behavior to the code-highlighting package and makes exact source preservation harder.
- **Raw-mode-only Mermaid support**: preserves source but fails the enhanced note reading experience users expect.
- **Store parsed diagram metadata outside the Markdown body**: enables richer future editing, but violates the files-first model.
## Consequences
- `src/utils/mermaidMarkdown.ts` is the canonical parser/serializer bridge for note diagrams.
- Rich mode renders diagrams as schema-backed blocks; raw mode remains the direct source editor.
- Invalid Mermaid source remains visible instead of breaking the editor surface.
- `mermaid` is now a runtime dependency and should be upgraded deliberately with rendering regression coverage.
- Future diagram controls, such as copy source or expand, can attach to the same `mermaidBlock` without changing storage.

View File

@@ -0,0 +1,36 @@
---
type: ADR
id: "0089"
title: "Active vault filesystem watcher"
status: active
date: 2026-04-27
---
## Context
Tolaria treats the filesystem as the source of truth, but before this decision the running app only noticed external file changes after a manual Reload Vault, a Git pull, or an AI-agent-specific refresh callback. Edits from another editor, terminal commands, another Tolaria window, or a non-pull Git operation could leave React state and the editor surface stale.
ADR-0071 already defines the safe reconciliation policy for external vault mutations: reload vault-derived state, protect unsaved local edits, and reopen the clean active note from disk when needed. Filesystem watching needed to reuse that policy instead of adding another ad hoc reload path.
## Decision
**Tolaria watches the active desktop vault with a native filesystem watcher and routes external change batches through the shared external-refresh reconciler.**
The desktop backend exposes `start_vault_watcher` and `stop_vault_watcher` commands backed by Rust `notify`. It watches the active vault recursively, ignores known non-content churn such as `.git/`, `node_modules/`, temp files, and `.tolaria-rename-txn`, then emits `vault-changed` events with the active vault path and changed paths.
The renderer owns batching and reconciliation. `useVaultWatcher` starts the backend watcher for the active main-window vault, debounces native events into one refresh, filters out recent app-owned saves, and calls `refreshPulledVaultState()`. Manual Reload Vault still uses `reload_vault` directly, but now exposes visible reload feedback.
## Options considered
- **Native active-vault watcher plus shared reconciliation** (chosen): keeps external changes visible without polling and preserves ADR-0071 behavior for clean and unsaved tabs. Cons: adds one native dependency and a long-lived watcher state.
- **Frontend-only polling**: simpler backend surface, but wastes work on idle vaults and still needs careful active-note reconciliation.
- **Direct `reloadVault()` on every native event**: easy to implement, but bypasses clean-tab reopen handling and can clobber the user experience around unsaved edits.
- **Watch every configured vault**: could pre-warm state, but burns resources for inactive vaults and complicates event ownership across windows.
## Consequences
- External writes converge automatically into the visible vault state after a short debounce.
- Active clean notes are refreshed through the same path as pull and AI-agent updates; unsaved local edits remain protected.
- Tolaria app-owned saves are suppressed briefly so autosave does not trigger a full external refresh loop.
- The status bar can show reload progress for manual and automatic refreshes.
- The watcher is a desktop-only integration; mobile builds keep no-op command stubs until a mobile-specific filesystem strategy exists.

View File

@@ -0,0 +1,35 @@
---
type: ADR
id: "0090"
title: "Pi CLI agent adapter"
status: active
date: 2026-04-28
---
## Context
Tolaria already supports Claude Code, Codex, and OpenCode as local CLI agents in the AI panel. The next provider request is Pi Coding Agent support with the same first-class availability, settings, status, streaming, and MCP vault access.
Pi exposes non-interactive JSON events through `pi --mode json`, but Pi core intentionally does not include built-in MCP. Its supported MCP path is the `pi-mcp-adapter` extension, which reads MCP server definitions from Pi-compatible config files.
## Decision
Tolaria adds Pi as a supported CLI agent id (`pi`) and launches app-managed Pi sessions through a dedicated adapter module.
The adapter runs `pi --mode json --no-session` from the active vault cwd, closes stdin, and points `PI_CODING_AGENT_DIR` at a temporary directory containing Tolaria's `mcp.json`. That config loads the Tolaria MCP server through `pi-mcp-adapter`, pins `VAULT_PATH` to the selected vault, sets `WS_UI_PORT=9711`, uses lazy server lifecycle, and exposes the small Tolaria tool set directly.
Pi availability follows the existing desktop pattern: check the inherited `PATH`, the user's login shell, and common local/toolchain install locations. Pi authentication remains owned by the Pi CLI; Tolaria only surfaces setup errors and does not store provider API keys.
## Options Considered
- **Transient Pi adapter config** (chosen): gives app-launched Pi sessions Tolaria MCP access without mutating user or vault config files. Cons: requires the Pi MCP adapter extension path to be available to Pi.
- **Write `.mcp.json` into the active vault**: simple for Pi discovery, but creates project files as a side effect of a chat session and can dirty user vaults.
- **Rely on global `~/.pi/agent/mcp.json`**: matches Pi documentation, but silently retargets or depends on user-global state and conflicts with Tolaria's explicit integration boundary.
- **Skip MCP for Pi**: easier to implement, but creates a weaker agent than the existing providers and violates the requirement for first-class MCP support.
## Consequences
- The AI panel, settings, command palette, status bar, onboarding, and stream normalization now treat Pi as a first-class supported agent.
- App-launched Pi sessions can use Tolaria vault MCP tools while remaining scoped to the selected vault.
- Pi support stays isolated in Pi-specific modules instead of expanding the shared `ai_agents.rs` hotspot.
- Users still need Pi itself, a configured Pi model/provider, and the Pi MCP adapter extension path available to the Pi CLI.

View File

@@ -0,0 +1,35 @@
---
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

@@ -0,0 +1,39 @@
---
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

@@ -0,0 +1,35 @@
---
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

@@ -0,0 +1,37 @@
---
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

@@ -0,0 +1,35 @@
---
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

@@ -0,0 +1,37 @@
---
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

@@ -0,0 +1,42 @@
---
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

@@ -0,0 +1,30 @@
---
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

@@ -0,0 +1,37 @@
---
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

@@ -0,0 +1,35 @@
---
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

@@ -0,0 +1,35 @@
---
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

@@ -0,0 +1,29 @@
---
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

@@ -0,0 +1,32 @@
---
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

@@ -0,0 +1,48 @@
---
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

@@ -0,0 +1,39 @@
---
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

@@ -0,0 +1,31 @@
---
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

@@ -0,0 +1,43 @@
---
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

@@ -0,0 +1,32 @@
---
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

@@ -0,0 +1,30 @@
---
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

@@ -0,0 +1,25 @@
# 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

@@ -0,0 +1,32 @@
---
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

@@ -0,0 +1,44 @@
---
type: ADR
id: "0110"
title: "In-app media and PDF previews for binary vault files"
status: superseded
date: 2026-05-05
supersedes: "0098"
superseded_by: "0121"
---
## 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

@@ -0,0 +1,49 @@
---
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

@@ -0,0 +1,41 @@
---
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

@@ -0,0 +1,43 @@
---
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

@@ -0,0 +1,40 @@
---
type: ADR
id: "0114"
title: "Mounted workspaces unified graph"
status: active
date: 2026-05-07
---
## Context
Tolaria users can already register multiple vaults, but switching vaults historically replaced the active graph. That model breaks down when separate Git repositories represent different workspaces that still need to reference each other: search, quick-open, wikilink navigation, and note lists should see one graph, while Git status, folders, saved views, and sync controls remain scoped to the repository currently in focus.
The app also needs a stable way to disambiguate same-named notes across repositories without writing machine-specific paths into Markdown. A full storage migration or database-backed graph would conflict with Tolaria's filesystem-first model and make separate Git histories harder to reason about.
## Decision
**Tolaria treats the registered vault list as an installation-local mounted-workspace set and annotates loaded entries with workspace provenance.**
Specifically:
1. `vaults.json` persists workspace identity (`label`, stable `alias`, color, mount flag) and the default workspace path for newly created notes.
2. `useVaultLoader` loads entries from every available mounted workspace and attaches `WorkspaceIdentity` to each `VaultEntry` before React consumes the combined graph.
3. Active-vault switching remains the focus control for Git, folder tree, saved views, watchers, repair, and other per-repository operations.
4. Wikilinks stay Markdown-first. Same-workspace links remain vault-relative; cross-workspace canonical links are prefixed with the target workspace alias.
5. Note reads and writes for absolute paths can resolve the deepest registered vault root at the Tauri boundary when no explicit `vaultPath` is supplied, preserving path-containment validation across mounted workspaces.
## Alternatives considered
- **Mounted workspace provenance on `VaultEntry` with alias-prefixed links** (chosen): preserves filesystem/Git independence while letting UI graph surfaces operate across repositories.
- **Merge separate repositories into one vault**: avoids cross-root resolution, but forces users to collapse unrelated Git histories and permissions into one repo.
- **Persist absolute paths in wikilinks**: disambiguates locally, but makes notes non-portable and leaks machine paths into user data.
- **Store a global graph database**: could make cross-workspace queries faster, but violates the cache-is-disposable rule and adds a new source of truth.
## Consequences
- Search, quick-open, note lists, and wikilink navigation can operate across mounted workspaces.
- UI surfaces that show ambiguous note names should display compact workspace provenance only when more than one workspace is present.
- New notes and Type files are created in the configured default workspace, falling back to the active workspace if the default is unavailable or unmounted.
- Backend command boundaries must continue validating every disk operation against a registered root; mounted workspaces do not loosen filesystem access.
- Future per-workspace features should distinguish graph-wide behavior from active-repository behavior before adding state or commands.

View File

@@ -0,0 +1,28 @@
---
type: ADR
id: "0115"
title: "Scoped React Context for shared UI preferences"
status: active
date: 2026-05-12
---
## Context
Laputa has relied on props-down callbacks-up state flow since ADR-0026 because most renderer state is orchestrated in `App.tsx` and the component tree stays understandable. Today's `date_display_format` refactor exposed a narrow exception: the same installation-local rendering preference now needs to reach note rows, property chips and cells, inspector surfaces, table-of-contents metadata, search subtitles, and date-editing controls across multiple branches of the tree. Continuing to thread that value through intermediate components would add noisy prop plumbing to components that do not conceptually own the preference.
## Decision
**Use a scoped React context for shared UI preferences that are read in many renderer leaves but still sourced from `App.tsx`. `AppPreferencesProvider` publishes the current installation-local preference values, and leaf components consume them through focused hooks such as `useDateDisplayFormat`; writes still flow through the existing settings/update path rather than through context mutations.**
## Alternatives considered
- **Scoped app-preferences context** (chosen): removes prop forwarding for cross-cutting rendering preferences while keeping the source of truth in `App.tsx` and avoiding a general-purpose global store.
- **Continue prop drilling from `App.tsx`**: preserves the old rule literally, but keeps widening component signatures and couples intermediate components to preferences they do not use.
- **Adopt a broader global state/store solution**: centralizes access, but introduces more indirection and policy surface than this renderer-only preference case needs.
## Consequences
Leaf components can read shared formatting preferences directly, so `date_display_format` stays consistent across note-list, inspector, search, and metadata surfaces without forwarding props through unrelated layers.
This narrows ADR-0026's blanket "no Context for data" rule. The replacement rule is: mutable application/domain state still lives in `App.tsx` plus focused hooks, while React context is allowed only for tightly scoped, cross-cutting UI preferences whose canonical value still originates from that same top-level state.
Future additions to `AppPreferencesProvider` should stay small, renderer-local, and read-focused. If Laputa starts moving writable domain state, async workflows, or large derived objects into context, that needs a new ADR rather than quietly expanding this pattern.

View File

@@ -0,0 +1,44 @@
---
type: ADR
id: "0116"
title: "Rich/raw transition and serialization ownership"
status: active
date: 2026-05-13
---
## Context
Tolaria already relies on BlockNote as the rich editor and on a Markdown-first save path, but the rich/raw boundary had started to split that contract across multiple local helpers. Autosave and tab-swap logic serialized rich-editor content in one place, raw-mode entry rebuilt Markdown in another, and raw-mode toggles carried pending content and pending cursor/scroll restore state through separate refs.
That fragmentation created two drift risks in one of the most correctness-sensitive parts of the app:
- rich-editor Markdown output could diverge across autosave, tab switches, and raw-mode entry, especially around wikilink restoration, durable schema-node serialization, frontmatter preservation, and portable attachment paths
- raw/rich mode switches could desynchronize pending content from pending position restoration, making stale transition state harder to reason about and harder to harden
Tolaria's editor contract already prioritizes no crashes, no stale overwrites, and minimal per-keystroke work. The rich/raw boundary needs the same single-owner discipline.
## Decision
**Tolaria centralizes BlockNote-to-Markdown serialization for editor flows in one shared owner and treats raw/rich mode handoff as explicit transition state with a single owner per concern.**
Specifically:
1. `src/utils/richEditorMarkdown.ts` is the canonical owner for rich-editor body/document serialization used by autosave, tab-swap, and raw-mode entry.
2. Raw-mode content handoff is modeled as one content transition object, so pending raw-exit content and raw-mode overrides move together.
3. Cursor/scroll restoration across rich/raw toggles is modeled as one restore-transition ref consumed by the editor-mode position sync path.
4. Editor surfaces should not keep independent ad hoc pending-content or pending-position refs outside those shared owners.
## Options considered
- **Shared serialization owner plus explicit transition owners** (chosen): keeps the Markdown contract and mode-switch lifecycle consistent across editor flows, while still allowing debounced work and focused testing.
- **Per-flow local serializers and pending refs**: simpler inside each hook, but lets autosave, tab-swap, and raw-mode entry drift apart over time.
- **Separate raw-mode-specific serialization and restore logic**: would preserve local autonomy, but duplicates correctness-sensitive rules at the exact boundary where stale state bugs are hardest to diagnose.
- **Always rebuild all content/position state synchronously on every toggle**: reduces retained transition state, but increases work at toggle time and does not solve ownership drift in shared serialization logic.
## Consequences
- Autosave, tab-switch flushing, and raw-mode entry now share one Markdown serialization contract.
- Wikilink restoration, durable editor-node serialization, frontmatter preservation, and portable attachment-path rewriting have one place to evolve.
- Rich/raw mode toggles become easier to reason about because content transition state and restore transition state each have a single owner.
- Future editor features that need rich-editor Markdown output or mode-transition bookkeeping should extend these shared owners rather than introducing local one-off refs or serializers.
- Re-evaluation is warranted if Tolaria adopts a different editor runtime or if rich/raw mode stops being a first-class bidirectional workflow.

View File

@@ -0,0 +1,24 @@
# ADR-0117: Bundle the fcitx GTK3 frontend in Linux AppImages
## Status
Accepted
## Context
Linux AppImages run WebKitGTK through the GTK3 input-method stack. Users with fcitx5 can export `GTK_IM_MODULE=fcitx`, `QT_IM_MODULE=fcitx`, and `XMODIFIERS=@im=fcitx`, but the AppImage still cannot load the host GTK immodule reliably because the GTK module cache and library paths are isolated from the mounted AppDir.
The previous AppImage startup fallback set `GTK_IM_MODULE=fcitx` when fcitx was detected, but it did not make the `im-fcitx5.so` module available inside the AppImage. That left Chinese/Japanese/Korean input dependent on host paths that GTK may not search from a sealed AppImage.
## Decision
Linux release jobs install `fcitx5-frontend-gtk3` and the AppImage output-plugin shim copies the GTK3 fcitx immodule plus its client library into the AppDir before the AppImage is sealed. At runtime, AppImage startup writes a mount-path-specific `GTK_IM_MODULE_FILE` cache that points GTK at the bundled module whenever fcitx is configured explicitly or through common fcitx environment hints.
The sealed AppImage validation step extracts every produced AppImage and fails the release if the symlink-safe AppRun resolver, the bundled fcitx immodule, or the fcitx client library is missing.
## Consequences
- fcitx5 input works in AppImage launches without relying on the host GTK immodule cache path.
- X11 fallback launches with explicit `GTK_IM_MODULE=fcitx` use the same bundled module path as Wayland launches.
- Linux AppImage builds now depend on the distro package that provides the GTK3 fcitx frontend.
- If the Ubuntu package path changes, the AppImage validation step fails before publishing a broken bundle.

View File

@@ -0,0 +1,35 @@
---
type: ADR
id: "0118"
title: "Entry-scoped note windows without vault index scans"
status: active
date: 2026-05-14
---
## Context
ADR-0031 kept secondary note windows on the full `App` shell so they would inherit the same editor capabilities as the primary window. That decision also accepted a full vault load per secondary window as the simpler trade-off.
In practice, repeated note-window opens were paying that full-vault scan cost even when the window only needed one known note path. The startup path loaded the vault index and passed related entries into the editor even though note-window mode renders a single-note surface. That extra work increased window-open cost and made every secondary window depend on repository-wide entry hydration for a workflow that is intentionally scoped to one note.
Tolaria still wants note windows to reuse the main App architecture rather than reviving a separate `NoteWindow` shell. The missing decision was how far the shared vault loader should go when the window contract is already narrowed to a single entry.
## Decision
**Secondary note windows continue to render the full `App` shell, but they no longer load the full vault index during startup.** In note-window mode, Tolaria skips the shared vault-entry scan, reloads only the requested note entry, and scopes editor entry props to that active note instead of passing repository-wide visible entries.
This keeps the architectural benefit of ADR-0031 (one window architecture, one editor surface) while changing the data-loading contract for secondary windows from vault-scoped to entry-scoped.
## Alternatives considered
- **Full `App` shell with entry-scoped loading** (chosen): preserves feature parity in the shared shell while removing unnecessary full-vault scans for a one-note window. Trade-off: note windows should not assume vault-index-derived context is available by default.
- **Full `App` shell with full vault scan**: simplest continuation of ADR-0031, but repeats avoidable repository-wide I/O every time a note window opens.
- **Dedicated `NoteWindow` shell**: could be lighter still, but reintroduces the architectural drift and duplicated feature work that ADR-0031 intentionally removed.
## Consequences
- Opening a secondary note window no longer requires `list_vault`/full entry hydration before the editor can render the requested note.
- Repeated note-window opens avoid redundant vault scans and stay aligned with the product contract that these windows are single-note work surfaces.
- Features inside note-window mode must treat vault-index-derived data as opt-in; they cannot assume related entries are already loaded just because the full `App` shell is mounted.
- ADR-0031 remains directionally valid for shared window architecture, but its original "full vault load per secondary window" trade-off is no longer the operating model.
- Re-evaluate if note windows later need immediate repository-wide browsing context, or if future profiling shows the remaining single-entry reload path is still too expensive.

View File

@@ -0,0 +1,35 @@
# 0119. Vault-Neutral MCP Registration With Mounted Workspace Guidance
Status: active
Date: 2026-05-14
## Context
Tolaria used to register external MCP clients with a durable `VAULT_PATH` environment variable. That made the copied config easy to inspect, but it also meant Claude Code, Cursor, Gemini CLI, and generic MCP clients stayed pinned to whichever vault was active at setup time.
Domenico Lupinetti's dynamic-vault MCP proposal in PR #603 pointed in the right direction: MCP clients should follow Tolaria's current workspace state instead of requiring users to reconnect after each vault change. The current app model has also moved from one selected vault toward mounted workspaces, so the MCP server needs to operate on every active mounted vault and load local agent guidance from each vault.
## Decision
Durable external MCP registration is vault-neutral. Tolaria still writes an explicit stdio MCP entry, but that entry contains only the Node command, `mcp-server/index.js`, and `WS_UI_PORT=9711`. It no longer writes `VAULT_PATH`.
The Node MCP entrypoints resolve vaults at tool-call time:
- Explicit `VAULT_PATH` and `VAULT_PATHS` environment variables continue to win for app-owned bridge launches and legacy/manual launches.
- When those env vars are absent, the MCP server reads Tolaria's `vaults.json`.
- `active_vault` is returned first.
- Every workspace in `vaults[]` is included unless it is explicitly marked `mounted: false`.
- Paths are deduplicated and blank paths are ignored.
Vault context now checks each active mounted workspace root for `AGENTS.md` and returns those instructions with the vault summary. The MCP server also exposes `list_vaults` so agents can discover the active workspace set and whether each vault has root guidance.
We are not adding a session-local `switch_vault` tool. A switch tool would create a second source of truth inside the MCP process, while Tolaria already owns mounted workspace state.
## Consequences
External MCP config survives vault switches and mounted-workspace changes without reconnecting.
Agents can work across all active mounted vaults and receive the per-vault `AGENTS.md` instructions needed to respect local rules.
Manual users can still override the resolved workspace set with `VAULT_PATH` or `VAULT_PATHS` when they intentionally want a static or scripted MCP session.

View File

@@ -0,0 +1,37 @@
# 0120. Stable AppImage MCP Server Path With OpenCode Registration
Status: active
Date: 2026-05-14
## Context
Domenico Lupinetti's PR #600 identified two gaps in durable external MCP setup:
- Linux AppImage launches expose bundled resources through a mount path that can change between app starts, so external clients can keep a stale `mcp-server/index.js` path.
- OpenCode uses `~/.config/opencode/opencode.json` with a different MCP schema from Claude Code, Cursor, Gemini, and generic `mcpServers` clients.
ADR-0119 made durable MCP registration vault-neutral, so PR #600 could not be merged directly: its registered entries still carried `VAULT_PATH`. The stable-path and OpenCode work is still valid, but it has to preserve the current mounted-workspace resolution model.
## Decision
On Linux AppImage startup, Tolaria extracts the bundled `mcp-server/` directory to `~/.local/share/tolaria/mcp-server/`. The extracted directory is version-gated by a `.tolaria-version` marker. Extraction runs on first launch or after an app version change, uses a staging directory plus rename, and uses a process lock so concurrent app launches do not write the stable directory at the same time.
Durable external registration prefers the stable extracted server directory when it is ready. Otherwise it falls back to the packaged resource resolver.
OpenCode is added to durable MCP registration and removal. Tolaria writes an OpenCode-specific entry under the top-level `mcp` key using:
- `type: "local"`
- `command: [node, index.js]`
- `enabled: true`
- `environment.WS_UI_PORT = "9711"`
OpenCode registration remains vault-neutral. It does not write `VAULT_PATH`; the Node MCP server resolves active mounted workspaces from Tolaria state per ADR-0119.
## Consequences
Linux AppImage users can register external MCP clients once and keep a valid `index.js` path across restarts and updates.
OpenCode participates in the same connect, disconnect, and status flow as Claude Code, Cursor, Gemini, and generic MCP clients while preserving its own config schema.
The stable path fixes the packaging lifecycle without reintroducing static vault pinning.

View File

@@ -0,0 +1,34 @@
---
type: ADR
id: "0121"
title: "AppImage external fallback for audio and video previews"
status: active
date: 2026-05-15
supersedes: "0110"
---
## Context
ADR-0110 standardized in-app previews for image, audio, video, and PDF vault files through the shared `FilePreview` surface and Tauri asset URLs. In practice, Linux AppImage builds run audio and video playback through WebKitGTK, and that runtime has proven unstable enough that mounting the same in-webview media controls is not a reliable default for packaged Linux releases.
Tolaria still needs one binary-preview model across platforms: previewability should remain renderer-inferred from filename extensions, binary files should remain ordinary vault entries, and external-open actions must continue to re-enter the active-vault command boundary before the OS opens a file.
## Decision
**Tolaria keeps in-app image and PDF previews everywhere, but Linux AppImage builds fall back to external-open controls for audio and video instead of mounting in-webview media playback.**
- `FilePreview` remains the single renderer-owned surface for supported binary vault files.
- The preview policy is runtime-owned: the renderer asks the native runtime whether external media fallback is required before rendering audio or video elements.
- Linux AppImage builds return `true` for that runtime check and suppress in-webview audio/video previews; other targets keep the existing native HTML media controls.
- Editor-embedded BlockNote audio/video blocks follow the same runtime gate so binary preview behavior stays consistent between note bodies and file previews.
## Alternatives considered
- **Runtime-gated external fallback on Linux AppImage** (chosen): keeps one preview architecture while containing a platform-specific runtime instability. Cons: AppImage users lose inline playback for audio/video.
- **Keep in-app audio/video previews on every platform**: preserves feature parity, but continues shipping a known unstable playback path on AppImage.
- **Disable all binary previews on Linux**: simpler policy, but unnecessarily removes stable image/PDF previews and weakens the file-first editor experience.
## Consequences
Tolaria now treats audio/video preview as a runtime capability decision rather than a universal guarantee of the binary preview system. Linux AppImage users see explicit external-open fallback controls for audio and video, while other platforms keep the richer in-app playback path.
This keeps the filesystem-first binary model, scoped asset access, and active-vault validation boundary intact without introducing persisted media types or a separate media subsystem. Re-evaluate this decision if AppImage media playback becomes stable enough to restore inline playback without special handling, or if other packaged runtimes need their own preview capability gates.

View File

@@ -0,0 +1,33 @@
# 0122. Scalar Array Frontmatter Properties
Status: active
Date: 2026-05-15
## Context
Saved Views filter against `VaultEntry.properties` in the renderer and in the Rust view evaluator. Before this decision, Tolaria preserved custom scalar frontmatter values as properties but dropped multi-element non-wikilink arrays during a full vault scan. That made a view such as `tags / contains / blues` unstable: optimistic renderer state could see a changed array for a while, but reload, view switch, or restart rebuilt the entry without the array-backed property.
Relationship arrays already have separate semantics because wikilink-bearing fields are stored in `VaultEntry.relationships`. Plain scalar arrays need to stay queryable as custom properties without being treated as relationship fields.
## Decision
**Tolaria preserves custom scalar-array frontmatter fields in `VaultEntry.properties`, while wikilink-bearing arrays remain relationships.** Single-item scalar arrays continue to normalize to a scalar value for compatibility; multi-item scalar arrays remain arrays.
Saved View filters evaluate scalar-array properties with set semantics. `contains` and `any_of` match exact case-insensitive elements, not substrings inside an element. Scalar properties keep their existing case-insensitive text matching behavior.
The vault cache version is bumped so existing caches that dropped array properties are rebuilt from disk.
## Options considered
- **Option A (chosen): Preserve scalar arrays as properties** - keeps YAML frontmatter expressive, fixes reload/restart behavior, and avoids hardcoded fields such as `tags`. The cost is widening `VaultEntry.properties` from scalar-only to scalar-or-array.
- **Option B: Flatten arrays to comma-delimited strings** - keeps the old property type, but cannot distinguish exact elements from substrings and makes filters ambiguous.
- **Option C: Treat every array as a relationship** - reuses existing relationship matching, but non-wikilink values such as tags are not graph edges and should not appear as relationships.
## Consequences
Views can filter custom scalar arrays consistently across save, reload, view switches, and app restart.
Property chips and sorting must tolerate property arrays. The note-list chip resolver already expands array values; custom-property sorting falls back to string comparison for arrays.
Any future custom-property logic must handle `VaultPropertyValue` rather than assuming every property is a scalar.

View File

@@ -0,0 +1,32 @@
---
type: ADR
id: "0123"
title: "Full vault graph for secondary note windows"
status: active
date: 2026-05-25
supersedes: "0118"
---
## Context
ADR-0118 made secondary note windows entry-scoped to avoid repeated full-vault scans. That reduced startup work, but it also removed the vault-index context that normal Tolaria capabilities depend on: properties, view actions, quick open/search, workspace-aware navigation, and other command surfaces no longer behaved like the main window.
The product expectation is that opening a note in a separate window creates another capable Tolaria window, not a reduced editor shell. Performance remains important, but capability parity is the stronger contract.
## Decision
**Secondary note windows load the same active vault/workspace graph as a normal Tolaria window.** They still start in editor-only view mode and auto-open the requested note from the URL parameters, but the app keeps the shared vault loader, mounted-workspace filtering, watcher scope, editor entry list, and workspace-aware note actions enabled.
`main.tsx` always mounts `App`; note-window mode is handled inside `App` through `getNoteWindowParams()` and `useNoteWindowLifecycle`.
## Alternatives considered
- **Entry-scoped note windows**: faster startup, but loses app-level features that require repository-wide context. Rejected because it breaks the secondary-window product contract.
- **Full app with full active graph** (chosen): keeps one window architecture and restores feature parity. Trade-off: each secondary window performs its own vault load.
- **Shared state from the main window over IPC**: could provide parity without duplicate scans, but adds synchronization complexity and failure modes. Deferred until profiling proves the duplicate load is a real bottleneck.
## Consequences
- Secondary windows can use normal Tolaria capabilities such as Properties, view actions, quick open/search, wikilink navigation, and workspace-aware note operations.
- Opening several note windows can repeat vault-loading work. This is acceptable for now because correctness and parity are more important than avoiding the scan.
- ADR-0118 is superseded. If secondary-window startup becomes too slow, optimize with shared state or incremental loading without removing app capabilities.

View File

@@ -0,0 +1,27 @@
---
type: ADR
id: "0124"
title: "Cached secondary note window startup"
status: active
date: 2026-05-26
supersedes: "0123"
---
## Context
ADR-0123 restored secondary note windows to the normal `App` path so they retain the full vault/workspace graph required by Properties, quick open/search, wikilinks, and workspace-aware note actions.
That parity is still the product contract, but forcing a fresh Tauri `reload_vault` during every secondary-window mount invalidates the backend cache. Opening several note windows can therefore repeat expensive full-vault scans even when the main window has already warmed the cache.
## Decision
**Secondary note windows keep the full vault graph, but their initial vault load uses the cached/incremental `list_vault` path instead of the forced `reload_vault` path.**
Normal main-window startup continues to force a fresh initial reload. Explicit refresh paths, watcher-driven refreshes, and user-initiated reloads still use reload commands where they need disk freshness.
## Consequences
- Secondary note windows remain capable full app windows rather than reduced editor shells.
- Repeated note-window opens can reuse the backend vault cache instead of invalidating it on every startup.
- First open after a cold cache still scans the vault, then warms the shared backend cache for later windows.
- If the cached scan path is stale, the existing backend cache update logic remains responsible for incremental freshness.

View File

@@ -0,0 +1,29 @@
---
id: "0126"
title: "Renderer action history for app-level undo and redo"
status: "active"
date: "2026-05-26"
supersedes:
- "0106"
---
# ADR-0126: Renderer action history for app-level undo and redo
## Context
Tolaria already lets native text surfaces keep their own undo stacks, but app-level state changes such as frontmatter edits, archive toggles, favorite toggles, and organization toggles did not share a consistent undo/redo model. Routing all Undo and Redo through the native menu items left these app actions one-way while also making command-palette discoverability inconsistent.
## Decision
Introduce a renderer-owned `useActionHistory` stack for app-level actions. Supported actions record explicit undo and redo callbacks only after the write succeeds, clear redo after new user actions, and suppress nested recordings while a history entry is replaying.
The Edit menu and command manifest now route Undo and Redo to renderer commands. Focused text-editing controls still receive native text history first through `document.execCommand('undo' | 'redo')`, so editor/input undo behavior remains separate from the app action stack.
Destructive actions that are not safely reversible remain outside this stack and continue to rely on confirmation/destructive UX instead of pretending to be undoable.
## Consequences
- App-level history is scoped to the active renderer session and is not persisted across launches.
- Undo/redo labels can be surfaced in the command palette because the top stack entries expose labels.
- Menu accelerators and keyboard shortcuts use the shared command manifest instead of Tauri's native Undo/Redo menu builders.
- ADR-0106 remains valid for the broader menu ownership model, but its native Undo/Redo exception is superseded by this renderer action-history route.

View File

@@ -0,0 +1,32 @@
---
type: ADR
id: "0127"
title: "Native AI workspace window"
status: active
date: 2026-05-26
---
## Context
The AI panel used to behave like another right-side editor panel. That kept the agent UI inside the main Tolaria window even when the user undocked it, so the "floating" surface could not be moved to another macOS space or placed beside Tolaria as a real window.
The AI surface also needed to support multiple chat sessions, per-chat target selection, and a single header that does not duplicate the old panel title and permission controls.
## Decision
**The AI surface is a renderer-owned `AiWorkspace` that can run either docked in the main app or in a dedicated native Tauri webview window labeled `ai-workspace`.**
The docked and native-window modes share the same React workspace component. The native window boots the normal `App` path with `?window=ai-workspace`, skips main-window size constraints, and uses macOS overlay traffic lights. Close and minimize requests from that window emit a dock request back to the main window before destroying the pop-out window.
## Options considered
- **Native Tauri window** (chosen): gives macOS users real window movement, traffic lights, and normal desktop window management; requires route/window-mode plumbing and explicit dock events.
- **CSS floating panel inside the main window**: simple and preserves component state in one renderer, but it cannot leave the main window bounds and fails the expected macOS behavior.
- **Separate full AI app shell**: isolates the workspace, but would duplicate vault loading and settings flows more than necessary.
## Consequences
- The status-bar AI affordance opens the workspace; target selection now belongs in the workspace header.
- `AiWorkspace` owns multi-chat sidebar state and filters target choices to installed local agents plus configured local/API model providers.
- The old `AiPanel` remains the reusable transcript/composer surface, but its header and prompt/focus effects can be disabled when mounted inside workspace sessions.
- Pop-out/dock currently transfers the workspace at the window level; future persistence can promote active conversations into a shared store if users need exact in-flight chat reparenting across renderer instances.

View File

@@ -0,0 +1,35 @@
---
type: ADR
id: "0128"
title: "Lightweight AI workspace window"
status: active
date: 2026-05-26
supersedes: "0127"
---
## Context
ADR-0127 moved the AI workspace into a native Tauri window, but the first version booted the full `App` shell and used macOS overlay traffic lights. That made pop-out feel slow, duplicated startup work, and left the chat route dependent on main-window vault loading before agent turns could run.
The AI workspace also needs installation-local chat metadata so user-facing chat titles and archive state survive dock/pop-out transitions without writing to a vault.
## Decision
**The AI workspace pop-out uses a lightweight renderer route backed by app settings metadata.**
`openAiWorkspaceWindow()` opens the `ai-workspace` Tauri webview with `?window=ai-workspace` plus active vault context in URL params. `App` routes that window directly to `AiWorkspaceWindowApp`, which loads settings, AI agent status, and vault guidance without mounting the full vault/editor shell. The window is undecorated and transparent, and it relies on `AiWorkspace` headers for drag regions plus separate close and dock controls. Close only closes the pop-out; dock emits the main-window dock request before closing the pop-out.
`settings.ai_workspace_conversations` stores only chat sidebar metadata: conversation id, title, archive state, and explicit target override. Prompt text, transcripts, note content, model credentials, and vault-local configuration stay out of app settings.
## Options considered
- **Lightweight AI route** (chosen): keeps pop-out startup focused on AI state and passes explicit vault context to the agent controller.
- **Full `App` route**: preserves maximum feature parity by default, but repeats vault/editor startup work and delays a window that should contain only the AI workspace.
- **Vault-stored chat metadata**: would travel with a vault, but chat labels and archive state are installation UI preferences rather than vault content.
## Consequences
- Pop-out startup avoids full note graph loading and should be close to instant after the Tauri webview is created.
- The dedicated AI window has no native traffic lights; users close or redock it through separate workspace header controls, and the rounded workspace shell defines the visible floating-window corners.
- Chat titles, archived state, and target overrides persist at the installation level in `settings.json`.
- Future transcript persistence must use a separate storage decision; `ai_workspace_conversations` is intentionally metadata-only.

View File

@@ -0,0 +1,45 @@
---
type: ADR
id: "0129"
title: "Tolaria vault item deep links"
status: active
date: 2026-05-27
---
# ADR-0129: Tolaria vault item deep links
## Context
Users need durable links they can paste into calendars, task managers, chats, and other apps to return to a Tolaria vault item. The link needs to identify a registered vault, preserve the file extension so non-Markdown files can be opened, and fail clearly when the vault or item is unavailable. Links must not create or import files implicitly.
Mounted workspaces make vault naming non-trivial. A readable slug is useful, but two vaults can share a label, alias, or folder name. URL parsing also cannot rely only on the browser URL implementation because dot-segment normalization can hide path traversal attempts before validation runs.
## Decision
Tolaria deep links use this shape:
```text
tolaria://<vault-slug>/<relative-path-with-extension>
```
The vault slug is generated from the registered workspace alias, then label, then path basename. When two vaults would share the same base slug, generated links append a stable short hash derived from the normalized vault path. A handwritten ambiguous base slug is rejected instead of picking an arbitrary vault.
The path component is encoded per segment with `encodeURIComponent`, so spaces, Unicode, and reserved characters are preserved while `/` remains the path separator. Parsing rejects empty path segments, `.`, `..`, decoded separators inside a segment, unsafe Windows separators, and resolved paths outside the selected vault root.
`src/utils/deepLinks.ts` owns URL building, parsing, and vault resolution. `src/hooks/useDeepLinks.ts` owns renderer integration: it receives Tauri deep-link events, validates them against the registered vault list, switches vaults when needed, reloads once if the target file is not in the current index yet, opens existing Markdown/text/binary entries, reports localized errors, and emits safe PostHog outcomes. Deep links are navigation-only; they never create missing files, import external content, or infer a fallback vault.
The desktop shell registers the `tolaria` scheme through `tauri-plugin-deep-link` and keeps second launches focused through `tauri-plugin-single-instance`. Windows and Linux also call runtime `register_all()` as a repair step. macOS uses bundle scheme registration. Linux runtime registration is best-effort and is not part of the verified v1 support target.
Copy surfaces are shared actions:
- Breadcrumb overflow: `Copy note deeplink`
- Command palette: `Copy deep link to current item`
- File preview header: copy action for non-Markdown vault files
## Consequences
Path-based links are understandable and support every vault file kind, but a file rename changes the old link. A future stable item-id layer could supersede this URL shape while preserving path links as a readable fallback.
Collision handling keeps generated links deterministic without exposing full local paths. Ambiguous handwritten slugs fail clearly, which is safer than opening the wrong vault.
Renderer-owned resolution keeps the navigation logic close to mounted-workspace state and note selection. Native plugins stay responsible only for scheme registration, event delivery, and focusing the existing app instance.

View File

@@ -0,0 +1,36 @@
---
type: ADR
id: "0130"
title: "Windows Authenticode signing for release installers"
status: active
date: 2026-05-27
---
## Context
Tolaria's Windows release job already produced Tauri updater signatures, but those signatures are not the Windows trust signal used by SmartScreen, Smart App Control, Defender, or WDAC policies when a user downloads and runs an installer from the browser. A managed Windows 11 user reported that the stable NSIS installer was blocked by Windows Security with no bypass option.
Microsoft's current guidance is that unsigned public installers can be fully blocked by enterprise policy, while signed installers at least carry a publisher identity and can build reputation across releases. Store distribution would provide the strongest SmartScreen outcome, but Tolaria does not currently publish a Microsoft Store package.
## Decision
**Tolaria release CI must Authenticode-sign Windows app executables and installers before publishing them.**
- Alpha and stable Windows release jobs import a CI-provided code-signing certificate from GitHub secrets.
- The workflow generates a temporary Tauri config that sets `bundle.windows.certificateThumbprint`, `digestAlgorithm`, and `timestampUrl`, then passes that config to `pnpm tauri build`.
- The Windows job verifies the produced app executable and installer artifacts with `Get-AuthenticodeSignature` and fails before upload if any signature is missing, invalid, or signed by an unexpected certificate.
- The public stable download page requires an explicit Windows installer click and tells managed-device users that IT may need to approve the Tolaria publisher before first install.
## Options considered
- **CI-enforced Authenticode signing** (chosen): gives Windows users and enterprise admins a real publisher identity, lets certificate reputation transfer across releases, and blocks accidental publication of unsigned installers. Cons: release jobs now depend on code-signing secrets and a valid certificate.
- **Documentation-only SmartScreen warning**: cheaper, but it leaves managed-device users with no supported path when policy removes the bypass option.
- **Microsoft Store distribution only**: strongest SmartScreen behavior, but it requires a separate packaging, submission, and release-management path that Tolaria does not yet own.
- **Portable ZIP fallback**: still downloads executable content from the browser and can remain subject to SmartScreen, Mark-of-the-Web, Smart App Control, or WDAC policy.
## Consequences
- Windows release failures caused by missing or expired code-signing credentials are intentional release blockers.
- Tauri updater signatures remain required for in-app updates, but they are treated as separate from Windows Authenticode trust.
- Enterprise-managed Windows installs can be documented around a stable Tolaria publisher identity instead of asking users to disable security policy.
- A future Microsoft Store/MSIX distribution path can supersede or supplement this policy if Tolaria decides to support Store-managed installs.

View File

@@ -0,0 +1,29 @@
---
type: ADR
id: "0131"
title: "Reusable release artifact build workflow"
status: active
date: 2026-05-28
---
## Context
Tolaria's alpha and stable release workflows both need to build the same platform artifact set: dual-architecture macOS updater bundles, optional stable macOS DMGs, Linux bundles, and signed Windows installers/updater bundles. Keeping those build jobs copied into both release workflows made platform fixes and validation changes easy to apply in one channel while accidentally leaving the other channel behind.
The release workflows still differ in how they compute versions, create releases, and publish alpha vs. stable metadata, but the artifact build contract is shared.
## Decision
**Tolaria centralizes release artifact production in `.github/workflows/release-build-artifacts.yml`, invoked by alpha and stable release workflows through `workflow_call`.** Channel-specific workflows own versioning and publishing; the shared workflow owns platform build, signing, validation, and artifact upload behavior.
## Alternatives considered
- **Reusable artifact workflow** (chosen): keeps alpha and stable artifact behavior aligned while preserving separate channel-specific release orchestration. Cons: release behavior is split across one caller workflow and one called workflow, so debugging requires following both files.
- **Keep duplicated jobs in each release workflow**: makes each workflow self-contained, but every platform build fix must be applied twice and drift is likely.
- **Merge alpha and stable releases into one workflow**: reduces workflow count, but couples different trigger/version/publishing semantics and makes the release pipeline harder to reason about.
## Consequences
Alpha and stable releases now share one platform artifact contract, including macOS, Linux, and Windows validation. Changes to signing, cache keys, bundle validation, or platform matrices should happen in the reusable artifact workflow unless they are genuinely channel-specific.
The architecture documentation should describe the release pipeline as channel orchestration plus shared artifact production, not as independent duplicated build job sets.

View File

@@ -0,0 +1,25 @@
---
type: ADR
id: "0132"
title: "Alpha Authenticode soft gate"
status: active
date: 2026-05-28
amends: "0130"
---
## Context
ADR 0130 made Windows Authenticode signing mandatory for release installers. That is still the right requirement for stable promotions, but the repository does not yet have the Windows code-signing certificate secrets needed by CI. Because alpha releases run on every push to `main`, requiring those secrets there broke the continuous alpha channel before the certificate provisioning work was complete.
## Decision
**Alpha Windows artifacts keep building when Authenticode certificate secrets are absent; stable Windows artifacts still require Authenticode signing.**
- The shared release artifact workflow accepts `require_windows_authenticode`.
- Alpha passes `false`, emits a workflow warning when certificate secrets are absent, and still requires Tauri updater signatures.
- Stable passes `true` and fails before building Windows artifacts unless certificate and password secrets are configured.
- When certificate secrets are present, both channels use the generated Tauri Authenticode config and verify Windows executable/installer signatures before upload.
## Consequences
The alpha updater channel remains live while Windows certificate provisioning is underway. Stable releases continue to enforce the Windows trust policy from ADR 0130 before public promotion. Once the certificate secrets are configured, alpha builds automatically regain Authenticode signing without another workflow change.

View File

@@ -0,0 +1,27 @@
---
type: ADR
id: "0133"
title: "Request-scoped AI stream event channels"
status: active
date: 2026-05-29
---
## Context
AI agent streams and direct model streams used shared Tauri event names (`ai-agent-stream` and `ai-model-stream`). That worked while only one stream of each kind was active, but the side AI workspace and pop-out window can make concurrent or rapidly reused AI sessions more likely. Shared channels risk routing deltas, tool events, or completion events to the wrong renderer listener.
## Decision
**Each native AI stream uses a request-scoped Tauri event channel generated by the renderer and validated by the backend.**
The renderer creates a unique event name with the stream's stable base prefix, listens on that channel, and passes it to the `stream_ai_agent` or `stream_ai_model` command as `event_name`. The Rust command accepts only scoped names that match the expected base prefix and safe character set; invalid or missing names fall back to the legacy shared channel.
## Alternatives considered
- **Request-scoped renderer channel** (chosen): isolates simultaneous streams without changing the stream event payload shape, while preserving backward-compatible command defaults.
- **Keep shared static event names**: simpler, but concurrent agent/model sessions can cross-deliver stream events between workspaces or chats.
- **Backend-generated channel names**: centralizes validation, but requires a setup handshake before the renderer can subscribe and complicates the current fire-and-stream command flow.
## Consequences
Concurrent AI streams can run without contaminating each other's renderer callbacks. The backend keeps a narrow validation boundary for externally supplied event names. Any future AI streaming command should follow the same base-prefix plus scoped-suffix convention instead of adding another process-wide static channel.

View File

@@ -0,0 +1,27 @@
---
type: ADR
id: "0134"
title: "Direct Shiki language registrations for code blocks"
status: active
date: 2026-05-29
---
## Context
Tolaria uses `@blocknote/code-block` for rich-editor fenced-code highlighting. The bundled BlockNote highlighter covers the common web and systems languages already in the editor menu, but it does not include several common Shiki grammars such as PowerShell, VBScript, Dart, Dockerfile, Terraform/HCL, and TOML. Users still expect imported fences like `powershell`, `ps1`, `vb`, and `vbscript` to highlight, show a valid language picker state, and serialize back to a stable Markdown fence.
## Decision
**Tolaria keeps BlockNote's code-block integration and adds direct, lazy `@shikijs/langs` registrations for missing common languages and aliases.**
## Options considered
- **Keep only the BlockNote bundle**: simplest, but leaves PowerShell/VBScript and other common fences unsupported.
- **Register selected `@shikijs/langs` grammars lazily** (chosen): preserves BlockNote's schema and parser path while adding only the extra grammars users need.
- **Replace BlockNote's highlighter with a full custom Shiki bundle**: more control, but a larger structural change than the current requirement needs.
## Consequences
`src/components/codeBlockOptions.ts` remains the owner of the BlockNote highlighter configuration, but extra grammar modules are now imported directly from `@shikijs/langs` only when a matching fence or picker value needs highlighting. `src/utils/codeBlockLanguageCatalog.ts` owns the supported extra language labels and aliases, and `src/utils/codeBlockLanguage.ts` normalizes known imported aliases such as `ps1` and `vb` to the canonical picker language.
The language menu grows, but unsupported aliases still fail safely by staying as plain explicit fence names. If Tolaria later needs a generated language bundle, export-time highlighting, or a substantially smaller menu, this ADR should be superseded by a custom Shiki packaging decision.

View File

@@ -0,0 +1,44 @@
---
type: ADR
id: "0135"
title: "Clean active notes refresh immediately after external edits"
status: active
date: 2026-05-30
supersedes: "0111"
---
## Context
ADR-0111 made external vault refreshes path-aware and preserved focused editor mounts so unrelated watcher events would not disrupt cursor state. That avoided needless remount churn, but it also meant a clean active note edited by Codex or another external process could remain visibly stale while the editor stayed focused. Because no later safe-remount trigger was guaranteed, users could see the old content until a full app restart.
Tolaria's filesystem-first model requires clean in-memory editor state to converge to the file on disk during the current session. Unsaved local editor buffers still need protection, but editor focus alone is not enough reason to keep showing stale content when the changed-path batch identifies the active file.
## Decision
**External vault refreshes now remount a clean active note immediately when the external changed-path batch includes that note, regardless of editor focus.**
The shared `refreshPulledVaultState()` path 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 the active file disappeared, close the tab instead of leaving a stale editor behind.
6. If the changed-path batch includes the clean active file, close and reopen the active tab from disk even when focus is inside the rich or raw editor.
7. Unknown or unrelated change batches refresh vault-derived state without remounting the active editor.
Git pulls, AI-agent refresh callbacks, and filesystem-watcher batches continue to converge through this single reconciliation helper instead of adding separate reload policies.
## Alternatives considered
- **Immediate clean active-note remount** (chosen): restores filesystem convergence for Codex and other external note edits while preserving unsaved local edits. Cons: a focused clean editor can lose cursor state when its own file changes externally.
- **Keep focused-editor preservation from ADR-0111**: avoids cursor disruption, but can leave the active editor stale indefinitely.
- **Defer active-note reload until blur**: reduces focus disruption, but adds another pending-refresh state machine and still allows the active editor to show stale disk content for an unbounded editing session.
## Consequences
- External edits to the currently open clean note become visible without restarting Tolaria.
- Unsaved local content remains authoritative and is not replaced by watcher, pull, or agent refreshes.
- The changed-path batch remains part of the external-refresh contract; callers should pass specific file paths whenever available.
- Unrelated watcher events still avoid active-editor remounts, so broad vault churn does not disturb the editor unless the active file itself changed.
- ADR-0111 is superseded by this stronger filesystem-convergence rule.

View File

@@ -0,0 +1,31 @@
# ADR-0136: macOS Webview PDF Export
## Status
Accepted
## Context
The first note PDF export implementation reused the native webview print command. On macOS that opens the full printer dialog, which is not the product behavior expected from "Export note as PDF"; users should choose a filesystem destination and get a PDF directly.
Tolaria already renders the exportable note in the live BlockNote DOM and applies print-only CSS so math, Mermaid, images, code blocks, tables, links, and custom blocks follow the same rendering path users see in the editor. Introducing a second Markdown-to-PDF renderer would duplicate that rendering logic and create drift.
## Decision
Use the existing Tauri webview and WebKit's own print operation to save the current webview directly to a chosen PDF path. The renderer remains responsible for export preparation:
- exit raw/diff modes
- apply the PDF export body class
- ask the user for a `.pdf` destination
- invoke the native `export_current_webview_pdf` command
The native command uses direct `objc2`, `objc2-app-kit`, `objc2-foundation`, and `objc2-web-kit` dependencies on macOS only. These crates are already part of Tauri's platform stack; declaring them directly lets Tolaria ask `WKWebView` for a WebKit-aware `NSPrintOperation` without adding a separate PDF rendering engine.
Windows, Linux, and browser mode keep print-dialog fallback behavior because they do not have the macOS WebKit/AppKit direct PDF save path yet. The renderer checks the native capability before opening a filesystem save dialog, so unsupported platforms do not ask for a destination that cannot be used.
## Consequences
- The macOS path opens a save-file dialog, not the printer dialog.
- The exported PDF keeps using the rendered note DOM, so frontmatter stays excluded by the existing rich-editor body extraction.
- The feature depends on macOS WebKit/AppKit behavior for direct PDF output. Other desktop platforms use the existing native print dialog until they get a platform-specific direct PDF path.
- The new direct dependencies must stay target-scoped to macOS so Linux and Windows builds do not compile AppKit crates.

View File

@@ -0,0 +1,58 @@
---
type: ADR
id: "0137"
title: "Shared rich-editor input transforms"
status: active
date: 2026-06-07
---
## Context
Tolaria has several Markdown-style conveniences in the rich BlockNote editor:
typed arrows become ligatures, completed inline math becomes a math node, and
completed `==highlight==` syntax becomes the durable highlight mark.
These features were added incrementally as separate `beforeinput` extensions.
Each extension repeated the same lifecycle work: reading the live ProseMirror
view, skipping IME composition, guarding stale views, dispatching transactions,
preventing native input only after a successful transform, and recovering known
BlockNote/ProseMirror transform failures. The syntax matchers differed, but the
execution shell was parallel enough that each new Markdown affordance risked a
slightly different edge-case policy.
## Decision
**Tolaria routes rich-editor Markdown input transforms through one shared
`beforeinput` execution path.**
`src/components/richEditorInputTransform.ts` owns the common lifecycle,
dispatch, and recoverable-error behavior. Feature files such as
`arrowLigaturesExtension.ts`, `mathInputExtension.ts`, and
`markdownHighlightInputExtension.ts` expose small transform objects that only
decide whether the current input event should produce a transaction.
`src/components/richEditorInputTransformExtension.ts` composes the Markdown
transform set used by the main editor and hidden editor probe.
## Options Considered
- **Shared transform primitive with feature-owned matchers** (chosen): removes
duplicate listener, dispatch, composition, and recovery code while keeping each
syntax rule local and testable.
- **One monolithic Markdown input extension**: reduces listener count, but mixes
unrelated syntax rules in one file and makes each future input affordance
harder to test independently.
- **Keep one extension per feature**: preserves local ownership, but keeps the
repeated edge-case shell and invites divergence as more transforms are added.
## Consequences
- `Editor` mounts one Markdown input-transform extension rather than separate
arrow, math, and highlight `beforeinput` listeners.
- Feature-specific files remain responsible for their syntax matching and
transaction construction only.
- Recoverable transform errors use the same telemetry event and fallback policy
across Markdown input transforms.
- New rich-editor Markdown input affordances should plug into the shared
transform primitive instead of adding another capture-phase `beforeinput`
extension.

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 | active |
| [0015](0015-auto-save-with-debounce.md) | Auto-save with 500ms debounce | superseded → [0102](0102-low-end-safe-autosave-idle-window.md) |
| [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) |
@@ -81,7 +81,7 @@ proposed → active → superseded
| [0023](0023-repair-vault-auto-bootstrap.md) | Repair Vault auto-bootstrap pattern | active |
| [0024](0024-cache-outside-vault.md) | Vault cache stored outside vault directory | active |
| [0025](0025-type-field-canonical.md) | type: as canonical field (replacing Is A:) | active |
| [0026](0026-props-down-no-global-state.md) | Props-down callbacks-up (no global state) | active |
| [0026](0026-props-down-no-global-state.md) | Props-down callbacks-up (no global state) | superseded → [0115](0115-scoped-react-context-for-shared-ui-preferences.md) |
| [0027](0027-dual-ai-architecture.md) | Dual AI architecture (API chat + CLI agent) | superseded |
| [0028](0028-cli-agent-only-no-api-key.md) | CLI agent only — no direct Anthropic API key | active |
| [0029](0029-domain-command-builder-pattern.md) | Domain command builder pattern for useCommandRegistry | active |
@@ -89,7 +89,7 @@ proposed → active → superseded
| [0031](0031-full-app-for-note-windows.md) | Full App instance for secondary note windows | active |
| [0032](0032-status-bar-for-git-actions.md) | Git actions (Changes, Pulse, Commit) in status bar, not sidebar | active |
| [0033](0033-subfolder-scanning-and-folder-tree.md) | Subfolder scanning and folder tree navigation | active |
| [0034](0034-git-repo-required-for-vault.md) | Git repo required — blocking modal enforces vault prerequisite | active |
| [0034](0034-git-repo-required-for-vault.md) | Git repo required — blocking modal enforces vault prerequisite | superseded → [0085](0085-non-git-vault-support.md) |
| [0035](0035-path-suffix-wikilink-resolution.md) | Path-suffix wikilink resolution for subfolder vaults | active |
| [0036](0036-external-rename-detection-via-git-diff.md) | External rename detection via git diff on focus regain | active |
| [0037](0037-codemirror-language-markdown-highlighting.md) | Language-based markdown syntax highlighting in raw editor | active |
@@ -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 | 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) |
| [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 |
@@ -135,6 +135,56 @@ proposed → active → superseded
| [0077](0077-concurrent-safe-vault-cache-replacement.md) | Concurrent-safe vault cache replacement | active |
| [0078](0078-scoped-unsigned-fallback-for-app-managed-git-commits.md) | Scoped unsigned fallback for app-managed git commits | active |
| [0079](0079-linux-window-chrome-and-menu-reuse.md) | Linux window chrome and menu reuse | active |
| [0080](0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md) | Cross-platform desktop release artifacts and portable vault names | active |
| [0080](0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md) | Cross-platform desktop release artifacts and portable vault names | superseded → [0083](0083-dual-architecture-macos-release-artifacts.md) |
| [0081](0081-internal-light-dark-theme-runtime.md) | Internal light and dark theme runtime | active |
| [0082](0082-markdown-durable-math-notes.md) | Markdown-durable math in notes | active |
| [0083](0083-dual-architecture-macos-release-artifacts.md) | Dual-architecture macOS release artifacts | active |
| [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 | superseded → [0121](0121-appimage-external-fallback-for-audio-and-video-previews.md) |
| [0111](0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md) | Path-aware external vault refresh with focused-editor preservation | superseded → [0135](0135-clean-active-note-refresh-after-external-edit.md) |
| [0112](0112-system-theme-mode.md) | System theme mode | active |
| [0113](0113-shared-renderer-attachment-path-normalization.md) | Shared renderer attachment path normalization | active |
| [0114](0114-mounted-workspaces-unified-graph.md) | Mounted workspaces unified graph | active |
| [0115](0115-scoped-react-context-for-shared-ui-preferences.md) | Scoped React Context for shared UI preferences | active |
| [0116](0116-rich-raw-transition-and-serialization-ownership.md) | Rich/raw transition and serialization ownership | active |
| [0118](0118-entry-scoped-note-windows-without-vault-index-scans.md) | Entry-scoped note windows without vault index scans | superseded -> [0123](0123-full-vault-graph-for-secondary-note-windows.md) |
| [0119](0119-vault-neutral-mcp-registration-with-mounted-workspace-guidance.md) | Vault-neutral MCP registration with mounted workspace guidance | active |
| [0120](0120-stable-appimage-mcp-server-path-with-opencode-registration.md) | Stable AppImage MCP server path with OpenCode registration | active |
| [0121](0121-appimage-external-fallback-for-audio-and-video-previews.md) | AppImage external fallback for audio and video previews | active |
| [0122](0122-scalar-array-frontmatter-properties.md) | Scalar array frontmatter properties | active |
| [0123](0123-full-vault-graph-for-secondary-note-windows.md) | Full vault graph for secondary note windows | superseded -> [0124](0124-cached-secondary-note-window-startup.md) |
| [0124](0124-cached-secondary-note-window-startup.md) | Cached secondary note window startup | active |
| [0126](0126-renderer-action-history.md) | Renderer action history for app-level undo and redo | active |
| [0127](0127-native-ai-workspace-window.md) | Native AI workspace window | superseded -> [0128](0128-lightweight-ai-workspace-window.md) |
| [0128](0128-lightweight-ai-workspace-window.md) | Lightweight AI workspace window | active |
| [0129](0129-tolaria-vault-item-deep-links.md) | Tolaria vault item deep links | active |
| [0130](0130-windows-authenticode-release-signing.md) | Windows Authenticode signing for release installers | active |
| [0131](0131-reusable-release-artifact-build-workflow.md) | Reusable release artifact build workflow | active |
| [0132](0132-alpha-authenticode-soft-gate.md) | Alpha Authenticode soft gate | active |
| [0133](0133-request-scoped-ai-stream-events.md) | Request-scoped AI stream event channels | active |
| [0134](0134-direct-shiki-language-registrations.md) | Direct Shiki language registrations for code blocks | active |
| [0135](0135-clean-active-note-refresh-after-external-edit.md) | Clean active notes refresh immediately after external edits | active |
| [0137](0137-shared-rich-editor-input-transforms.md) | Shared rich-editor input transforms | active |

View File

@@ -224,7 +224,7 @@ test('full create note flow', async ({ page }) => {
// Count should increase
const countAfter = await page.locator('.note-list__count').textContent()
expect(parseInt(countAfter!)).toBe(parseInt(countBefore!) + 1)
expect(parseInt(countAfter!, 10)).toBe(parseInt(countBefore!, 10) + 1)
// Note should be opened in editor
await expect(page.locator('.editor__tab--active')).toHaveText(/E2E Test Note/)

View File

@@ -32,7 +32,7 @@ test('visual verify: editor theme + list indentation', async ({ page }) => {
window.getComputedStyle(el).paddingLeft
)
console.log(`Level-0 padding-left: ${paddingL0}`)
expect(parseInt(paddingL0)).toBe(40)
expect(parseInt(paddingL0, 10)).toBe(40)
// Bullet widgets and checkboxes are rendered
const bulletCount = await page.locator('.cm-live-bullet').count()

View File

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

View File

@@ -7,6 +7,36 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet" />
<script>
(function () {
function isResizeObserverLoopMessage(message) {
return typeof message === 'string'
&& (
message.indexOf('ResizeObserver loop completed with undelivered notifications') !== -1
|| message.indexOf('ResizeObserver loop limit exceeded') !== -1
);
}
window.addEventListener('error', function (event) {
if (isResizeObserverLoopMessage(event.message)) {
event.preventDefault();
}
});
window.addEventListener('unhandledrejection', function (event) {
var reason = event.reason && (event.reason.stack || event.reason.message || String(event.reason));
if (isResizeObserverLoopMessage(reason)) {
event.preventDefault();
}
});
try {
if (new URLSearchParams(window.location.search).get('window') === 'ai-workspace') {
document.documentElement.classList.add('ai-workspace-native-window');
}
} catch {
// Leave the normal app chrome untouched if URL parsing is unavailable.
}
})();
</script>
<style>
:root {
color-scheme: light;
@@ -22,6 +52,33 @@
background: inherit;
color: inherit;
}
:root.ai-workspace-native-window,
:root.ai-workspace-native-window body {
background: transparent;
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
:root.ai-workspace-native-window body {
box-sizing: border-box;
padding: 20px;
}
:root.ai-workspace-native-window #root {
background: #FFFFFF;
border: 1px solid #E9E9E7;
border-radius: 12px;
box-sizing: border-box;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12), 0 1px 3px rgba(15, 23, 42, 0.10);
height: 100%;
overflow: hidden;
width: 100%;
}
:root.ai-workspace-native-window[data-theme="dark"] #root {
background: #1F1E1B;
border-color: #34322D;
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.30), 0 1px 4px rgba(0, 0, 0, 0.22);
}
</style>
<title>Tolaria</title>
</head>
@@ -30,26 +87,83 @@
(function () {
var key = 'tolaria-theme';
var legacyKey = 'laputa-theme';
var systemDarkQuery = '(prefers-color-scheme: dark)';
function normalizeTheme(value) {
return value === 'dark' || value === 'light' ? value : null;
return value === 'dark' || value === 'light' || value === 'system' ? value : null;
}
function prefersDarkTheme() {
try {
return typeof window.matchMedia === 'function' && window.matchMedia(systemDarkQuery).matches;
} catch {
return false;
}
}
function resolveTheme(value) {
var mode = normalizeTheme(value) || 'light';
return mode === 'system' ? (prefersDarkTheme() ? 'dark' : 'light') : mode;
}
function applyTheme(value) {
var mode = normalizeTheme(value) || 'light';
var mode = resolveTheme(value);
document.documentElement.setAttribute('data-theme', mode);
document.documentElement.classList.toggle('dark', mode === 'dark');
}
var mode;
try {
var mode = normalizeTheme(localStorage.getItem(key));
mode = normalizeTheme(localStorage.getItem(key));
if (mode === null) {
mode = normalizeTheme(localStorage.getItem(legacyKey));
if (mode !== null) localStorage.setItem(key, mode);
}
applyTheme(mode);
} catch (err) {
} catch {
applyTheme('light');
}
})();
</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 {
return true;
}
}
function markReloadAttempted() {
try {
sessionStorage.setItem(reloadAttemptKey, '1');
return true;
} catch {
return false;
}
}
function clearReloadAttempt() {
try {
sessionStorage.removeItem(reloadAttemptKey);
} catch {
// 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>

810
lara.lock Normal file
View File

@@ -0,0 +1,810 @@
version: 1.1.0
files:
e6ab9bf36c30b9d72bc97897e0d89a16:
command.noMatches: a2a97d96e196148e04c51c47d985e327
command.palettePlaceholder: 6d6b61b4c08ed382ad7ba406a9ba395d
command.footerNavigate: e3808db59d29bcfc91532d6539c4f9ae
command.footerSelect: 6d73339906ed42e20f6be91bf7f5f4a9
command.footerClose: dcf77460e548fe215ef28583f092a496
command.footerSend: 6b19fc9694fd8b7b3f1aafaca83db864
command.aiMode: 7197e3c5f9afd24022377176611f35cb
command.openSettings: 638c05f4f159a403e04aebe94b46a2a8
command.openSettings.keywords: ab6b5e03395f4db955ed9cbfd4de33b7
command.openLanguageSettings: e5a425aa6222fab3df66c0e1e0ca4183
command.openLanguageSettings.keywords: 6a910000cbf23eac3c7078e518cae66a
command.useSystemLanguage: b7f0315c47d4a59faa2a49571fcf1fca
command.openH1Setting: 05b4f6edc0e98b29670e8ee902cdb9bf
command.toggleGitignoredFilesVisibility: 0a2d3ea4e8ff8b00a801183caeb97a03
command.contribute: 9887a4451812854f0f1b6f669a874307
command.checkUpdates: f77f38f684c8b974dd4a56bb321cfd5f
menu.application: bc16b1fbe2e90ace4991cafc5149955c
menu.file: 0b27918290ff5323bea1e3b78a9cf04e
menu.edit: 7dce122004969d56ae2e0245cb754d35
menu.view: 4351cfebe4b61d8aa5efa1d020710005
menu.go: 5f075ae3e1f9d0382bb8c4632991f96f
menu.note: 3b0649c72650c313a357338dcdfb64ec
menu.vault: 5b70a213f150f01f0776fa9481ef2ddf
menu.window: c89686a387d2b12b3c729ce35a0bcb5b
menu.file.quickOpen: d79cba7ca4badcccc081bdcba22abd32
menu.file.quickOpenCmdO: 2f1c2a963833b85008562cd615515234
menu.file.quickOpenCtrlO: 8aaba3ce15c61798ce80bda311fc1e2d
menu.file.save: c9cc8cce247e49bae79f15173ce97354
menu.edit.pasteWithoutFormatting: 0219dac5b3b9c457aa65eb19fdd3d22c
menu.edit.findInVault: 7dad10e092a9fe1bfbb4a96c377bc417
menu.edit.toggleNoteListSearch: f6e0cd9529f571609033958a55f18d78
menu.view.allPanels: d33bf99e0756e1d5008cb78902e38ca9
menu.view.zoomIn: f5ca4abce85e2dddb0342d0bae3a7270
menu.view.zoomOut: 30850b501f98539b1aabaa29fabe41ef
menu.view.actualSize: a5dac79ddad1cbdb8b60c5b347ee047e
menu.view.commandPalette: c0a2436cd8b2dc5085c869e4a052572f
menu.go.allNotes: cd76cf851b08a9d2feafaf255bc1f4d5
menu.go.archived: 7d69b3cb4cada18ae61811304f8fbcb5
menu.go.changes: c112bb3542e98308d12d5ecb10a67abc
menu.go.inbox: 3882d32c66e7e768145ecd8f104b0c08
menu.note.toggleOrganized: 003f5648f585407c894e85543a5fda47
menu.note.toggleTableOfContents: c464111f97490e65699fe18970fc00e1
menu.vault.addRemote: 4d9d0f784920d6ef7f9f3b33266dd00e
feedback.title: 96ab5025e38aef43fdb73c9830795264
feedback.description: e900af4efb9167033d4ecc8a02cfe463
feedback.sponsor.title: 2f6a1db54812f51924c3324a178125ed
feedback.sponsor.description: 6517e0c5d4e4801814a6034dd54862b6
feedback.sponsor.cta: a2c71ec076796ab6af0ba0ea882303fc
feedback.sponsor.linkLabel: 7fbe24a8f025219f4d165aeb75352125
feedback.featureRequests.title: 73ea5953d68b2c20a82bb94240f6fe17
feedback.featureRequests.description: 9583575bdef16e12927b0a5acfa84307
feedback.featureRequests.cta: c19ef0158349da249920b0d9f67d7d48
feedback.featureRequests.linkLabel: d125f38d1dc8dbdc7dae3f1778df3a3a
feedback.discussions.title: 560f23c77d499c21038c0b4487f03cb2
feedback.discussions.description: 2e4da4c5f54c7af3b8db9f3d93ed63c5
feedback.discussions.cta: 61ccb6a90ec8037f2926c48c0a90caa3
feedback.discussions.linkLabel: 4f85e3343c5e21d2b11812b2baa8ed83
feedback.contributeCode.title: 8f93fd72b63620711cc118403b85133e
feedback.contributeCode.description: 53230626356951578842a8cd7233d1f1
feedback.contributeCode.cta: c73dd5e7d432cd18c109b333e0b55299
feedback.contributeCode.linkLabel: bfe9dd9816bb39f82fc003de794a075d
feedback.contributingGuide.cta: 6cc1fbcbbc69a70a70a2020c979d8e22
feedback.contributingGuide.linkLabel: b1310938ecab4c502a8b3af2ca1ce188
feedback.reportBug.title: 3406147c3ce729ce17ed76d45c2896f9
feedback.reportBug.description: 6da172412f774e022b0f32ba17305491
feedback.reportBug.cta: 1f5aa905b40956ff5ea8b4cc5ea29315
feedback.reportBug.linkLabel: ffaa2eaa0f015cf037d2b5fe8af6b813
feedback.linkFallback.title: c5396ec50cec23a09977180747737264
feedback.linkFallback.description: 91d1e18130765119606f095ac473d9cd
feedback.copyDiagnostics: dedf5388697bba1fd53173b04823761a
feedback.diagnosticsCopied: 9c7f3f37df9c000fc67c1a6ee35f40e5
feedback.diagnosticsCopiedSentence: 6f8ebe492dbb95827ccf7536965dc672
feedback.clipboardUnavailable: 8e25a424420687ed8e0dc2b1be8c7406
command.group.navigation: 846495f9ceed11accf8879f555936a7d
command.group.note: 3b0649c72650c313a357338dcdfb64ec
command.group.git: 0bcc70105ad279503e31fe7b3f47b665
command.group.view: 4351cfebe4b61d8aa5efa1d020710005
command.group.settings: f4f70727dc34561dfde1a3c529b6205c
command.navigation.searchNotes: d8002e888293774162e43b263658cbb5
command.navigation.goAllNotes: 7030dcbbb85b6f01d3c8b2fdcb6a5cff
command.navigation.goArchived: b620b8cb8fef8a287b986ff83a992a2a
command.navigation.goChanges: c6dd094ce55f8a765b68cef40a1c6bd9
command.navigation.goHistory: 39b86b661267053a240ffe89b9eca847
command.navigation.goBack: 4f2f5e1d6e2a13469ad431474a68ab82
command.navigation.goForward: d13c318438f67d749cfc0904e3fafcda
command.navigation.goInbox: 522af71c26bc7e3ce1dae6204c2fbe9f
command.navigation.renameFolder: a3d3b0f787c52a984af61085c577bff2
command.navigation.deleteFolder: 991e322cd8225c369a963bb60b0a9688
command.navigation.showOpenNotes: bd1905c340d0518187865f4a6694e14d
command.navigation.showArchivedNotes: 241399b908884adfe8608a3ab827ca74
command.navigation.listType: 4f70c27c6826a37543c8ada561397c22
command.note.newNote: 23c4edda8b815f750782f01870d27271
command.note.newNoteInCurrentFolder: 167b765903c70ab1b645908ff8a899ab
command.note.newType: adce5108f18945cc502a06c02445e32d
command.note.newTypedNote: 1493eda772c2179cb6247169d00723b2
command.note.saveNote: 2a309cda46e95b00d2a5a8afb3cc0047
command.note.undo: 1cdc076b28f70afac5fcedadf99fa119
command.note.undoAction: 83a1f43c314533161109d8d4daf032f2
command.note.redo: 5afeaba074ef570dc720caaa855d49f6
command.note.redoAction: 7556c9dd6845991933c56006bf7ffbac
command.note.pastePlainText: 09e8075dbd91e11878f8f4a138df760c
command.note.findInNote: f72f8f93d8e6119d5d08b60eb3a7b3bc
command.note.replaceInNote: b008e2e20c5e4cd202f4386271d6bed1
command.note.deleteNote: 56f727a2ee11d15159f1aa6373314cb6
command.note.archiveNote: 6043509fda6dd7f6f167e4108033f8e8
command.note.unarchiveNote: 6937fb694d00b11d579c21cd4bf103a2
command.note.addFavorite: 782d6b28dce4e5d0b2ac92142c6624ba
command.note.removeFavorite: 4f9b5fa08f2ef86fdc5d0ceefc271981
command.note.markOrganized: 505ad63357f533fb39e4276a2bfb9496
command.note.markUnorganized: 1c62f1bffcb793c63d729096d2794ced
command.note.restoreDeleted: d704f8283ca322a9b3713d2ccf9a6cd4
command.note.setIcon: 99b81d2663266c53e45e4d9c62c87a29
command.note.removeIcon: 4bccbad66025e506b37b4218498b299c
command.note.changeType: 4a54ffd47ed1f65ee97f34bfe8c3de14
command.note.moveToFolder: 3a0f26c42b9168ad839960d134065905
command.note.copyDeepLink: 8de1795f59af124594dc4decf77909a1
command.note.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0
command.note.openNewWindow: 6f67b2857093fa0fd45b1122dff4865d
command.git.initialize: 54a57cdc7105db6fb22dca661ce01ad6
command.git.commitPush: 8cb5faa4019716f43dd69c41496b1d46
command.git.addRemote: 7eef951b3f909340a68dc9811be83e9e
command.git.pull: 3988d61f4a4546381f61a4eaf61315b4
command.git.pullRepository: 931ad25176586fd4332dd671574d3185
command.git.resolveConflicts: 871ac36968cfca3d2297a89b28b25dee
command.git.viewChanges: b2699edf69d8e1031afce2b2f94e5c8d
git.repository.select: 33fcf2b3ec4686d9cd06051c726d0ba2
git.toast.autoGitFailed: 3e5d7e183a78e1f54c87ad0dba8a302b
git.toast.commitFailed: bb50986d29c30042bed1446f8adaa544
git.toast.missingAuthor: 0d4e7a7d6b6a3f883146dd86c9cdca09
command.view.editorOnly: 8355e056b32086b9190d639be290dda8
command.view.editorNoteList: b9604b1609eb6f581cd9570dca72d3f0
command.view.fullLayout: 1cebdf839eee2b1713828731aaa3b507
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
command.settings.createEmptyVault: d7c9673bb6c62a2b8aef009546ce8b23
command.settings.openVault: c40e74822da9417fdf52eb4a37bb23f2
command.settings.removeVault: 390e2b2031cde0dd18381efee0923bea
command.settings.restoreGettingStarted: b3358dc1c7b873b2a02ac6f05473fe4d
command.settings.manageExternalAi: c36b9bd171f11a18a1e624365d2b57ac
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
command.ai.switchDefault: 42724de254240d598fbcc40fdb5b18a3
command.ai.switchDefaultWithAgent: d405f7914bb1cd86172f108f3c33d953
settings.title: f4f70727dc34561dfde1a3c529b6205c
settings.close: 7812b3644f897c5d3efb0dd63440e751
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.workspaces.title: 10075fec14fcc45a489d02bedf972e97
settings.workspaces.enable: f418b797fc4d13ca6b59333326ac3246
settings.workspaces.enableDescription: 1487bd26586b773c77c01746399cc4cd
settings.workspaces.name: 49ee3087348e8d44e1feda1917443987
settings.workspaces.label: b021df6aac4654c454f46c77646e745f
settings.workspaces.slug: 0c908588520b3ef787bce443fc2b507c
settings.workspaces.nameAria: a94a5bbfbc6bbcff81961501be487703
settings.workspaces.labelAria: f6a0693020980910eaa8b168b30b33c3
settings.workspaces.slugAria: 1d9e314005db037b4a83a220a45f3d77
settings.workspaces.nameTooltip: 5c12e3507377eadae5c2340462dda553
settings.workspaces.labelTooltip: b1b87a2affcd504e54bf97100e3d9c34
settings.workspaces.slugTooltip: daa690f5855e35d1cfa9dbffe1870d40
settings.workspaces.color: cb5feb1b7314637725a2e73bdc9f7295
settings.workspaces.moveUpAria: 2ade43872ef9432d14af83056304c6d7
settings.workspaces.moveDownAria: ed1ef4a310797ac84894a224bf4c77a6
settings.workspaces.removeAria: a5d2ceeeb76bfcb99015fc780ced07c2
settings.appearance.title: a1c58e94227389415de133efdf78ea6e
settings.appearance.description: ca7b75a4c10ab8a540707e3a96cd3592
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
settings.language.system: 41e2252344aa441e925589ddcb762e6d
settings.language.summary: edc83f1f48ce29bc80bf2f2f90e24997
settings.autogit.title: 0bcc70105ad279503e31fe7b3f47b665
settings.git.enable: c40503e1b0f483eecf3aff2fd01665c4
settings.git.enableDescription: a96a43dc4dbf1490d665eef437807b07
settings.autogit.description.enabled: e65bec70ca4d7921be56f84d10836018
settings.autogit.description.disabled: 1143ef6ee00614816785e3c6fc299d51
settings.autogit.description.gitDisabled: 55a1dd2d7f797062cde1374930221649
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: f15c1cae7882448b3fb0404682e17e61
settings.vaultContent.description: a53487bf1c7898a1459dc1510e216f7d
settings.dateDisplay.default: 915e8ca934abe6625e17695ee7ae266a
settings.dateDisplay.defaultDescription: 502ed7ff7b9c0fe5971e8cb0576e91f6
settings.dateDisplay.us: abe1c573145c4419ba8bb158cf0068d6
settings.dateDisplay.european: 0b4f509278b2919973c5d1ae906c162b
settings.dateDisplay.friendly: 81e56440280f6dfaef0eb503ca301578
settings.dateDisplay.iso: 89b4ef461b67eb141eadd0fc6caa8d8d
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: 768c138c3432fb159ac58c5a4471e786
settings.aiFeatures.enable: af47a6b2bd32a7b7309bac7bef34f026
settings.aiFeatures.enableDescription: 7d3916f7f9e1bf8b8797f50f4533b9da
settings.aiAgents.default: 6af573559fd05d8c35bc57b41cc78e24
settings.aiAgents.defaultTarget: 5acfc74fd97a6dd2d67d15c66de5eed5
settings.aiAgents.agentGroup: 11eaa7f817e3f24d54f3cf3025be49b1
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: 0de6536215f76c44351ba4d809804c7a
settings.aiAgents.installedDescription: 27c8ed48439ac4a613409f5303fbca84
settings.aiAgents.noVersion: 89831adb7114946e916b9c31d57a1e33
settings.aiProviders.title: 63eb84d8510f9f086d282ec7d47adaa1
settings.aiProviders.description: d29dfa440713d15de311d29a64a27767
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.description: 33d53059be620b58e38b8e5c5ab26b27
settings.privacy.crashReporting: b4efc5b61526566d431e99242f5c21b4
settings.privacy.crashReportingDescription: e9d7c7efce44adc08d0460be1aad5c42
settings.privacy.analytics: 29d55bb222ea76e1a17396375dfab228
settings.privacy.analyticsDescription: a0f8b61d555d6d3fd279de2f47aaca76
settings.footerShortcut: 7dc840d6b0ef9f422a663ba41975871e
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: cea2547385aeeb68fa494bb3b09b98f5
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
ai.message.regenerate: e464a5330788a9f79d06c96989f840cb
ai.message.copy: 53fa249ce3d02c8a686b657b31d02ee7
ai.message.fork: f5aace5e5b5e7b0954651a2da44b6755
ai.message.reasoning: 87a222577e9c7b7a0739d92337f4fe46
ai.message.toolUse: d6093cfe0c95f805875e5724eecd20aa
ai.workspace.title: 375099f0c804e924c1dc275370710d92
ai.workspace.newChat: a917baaf1484ec5dc85f19ab8fc4f4e9
ai.workspace.collapseSidebar: 9ecc763686b7c61547281f3e05ffd06f
ai.workspace.expandSidebar: 281b619f3da2ba27e604302c3c00db56
ai.workspace.close: 184649ab030e9a609488608221c28d8c
ai.workspace.expandPanel: edbe17b531457367c1412e4b6349c194
ai.workspace.restorePanel: 64eb6176c678c6b395cfc93bac02a16f
ai.workspace.restoreGuidance: 2bd339d85ee3b33e513359ce781b60cc
ai.workspace.closeChat: 6c5be35688a466297db40c3a65ac9805
ai.workspace.popOut: 85ec25ec056b3082651e6e3fcfa57416
ai.workspace.dock: 72e81ed23007ec6fb9820b9abec353c4
ai.workspace.settings: 2c178b1d7a60a3c2b37ca4221d322899
ai.workspace.archive: a02bc089273a8c460ea3d2cd2c257b59
ai.workspace.restore: 98ee8b364870e27dd48aca3240801abf
ai.workspace.showArchived: ae2bcb4cb837cc09a5767153efe4bf39
ai.workspace.hideArchived: e7c72757d5bc7330d876eeb1b6ce9c53
ai.workspace.noActiveChats: f0cdbf09f23eb9a2efbb5977bcc39c1a
ai.workspace.noArchivedChats: 68410efd12fc0308ce1ed2225000d4d5
ai.workspace.chatTitle: 189013cfac88018c68ec02e8edc2379b
ai.workspace.renameChat: 0feba25e166f55afc51c4ebb33d8d757
ai.workspace.status.archived: 7d69b3cb4cada18ae61811304f8fbcb5
ai.workspace.status.idle: e599161956d626eda4cb0a5ffb85271c
ai.workspace.status.running: 5bda814c4aedb126839228f1a3d92f09
ai.workspace.status.done: f92965e2c8a7afb3c1b9a5c09a263636
ai.workspace.status.error: 902b0d55fddef6f8d651fe1035b7d4bd
ai.workspace.permissionMode: 5a7983f759c9e15ce53d7326f0abad71
ai.workspace.guidanceWarning: 00d95da9bc82394785728dec587b026e
ai.workspace.targetLabel: df5dd38c32585fe0c71c955b3a31f83c
ai.workspace.targetLocalAgents: 5a6a65e960093bfc37b4cc81c4a9f773
ai.workspace.targetLocalModels: c7c20443d8aaf04271df9d713f5c02ce
ai.workspace.targetApiModels: 66537b9e5200ebbbfbd091144d8afbcc
ai.workspace.noTargets: 7c3ed1f10281b41070e5a5cbe77453d7
ai.workspace.target.localAgents: 5a6a65e960093bfc37b4cc81c4a9f773
ai.workspace.target.localModels: c7c20443d8aaf04271df9d713f5c02ce
ai.workspace.target.apiModels: 66537b9e5200ebbbfbd091144d8afbcc
ai.workspace.target.none: 7c3ed1f10281b41070e5a5cbe77453d7
ai.workspace.target.unavailable: 453e6aa38d87b28ccae545967c53004f
ai.workspace.target.installed: 98dd43dfae05b11befe1f140e0ec787a
ai.workspace.target.installedVersion: 8677756d761e4cce240a637805482946
ai.workspace.target.localModel: d34f4b997ce78da5aa57d657a5d6fcb5
ai.workspace.target.apiModel: 589a257cbd265eaa7de93de8b4084dff
window.minimize: d27532d90ecd513e97ab811c0f34dbfd
window.maximize: 9369ba148ee259473fc1fb4939b6c2e8
window.restore: 2bd339d85ee3b33e513359ce781b60cc
window.close: d3d2e617335f08df83599665eef8a418
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.runtimeRequirement: 1d7a8c99b2bc875b3601fa57860432fb
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
sidebar.nav.archive: e727b00944f81e1d0a95c12886ac4641
sidebar.group.favorites: 953bf4d5e8a1807c15dec4e255384b1d
sidebar.group.views: 8f56a72923a6ca8ff53462533d89e567
sidebar.group.types: 6b20155008db90cf7589c07baa7334e9
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
sidebar.action.createFolder: 5dc1e97c14fbe72d8b566ce29c39f010
sidebar.action.renameFolder: db76034f8218cda07dadb746a5643019
sidebar.action.deleteFolder: 2a2f15300f6f7c81d69860ac1796b517
sidebar.action.createNodeInFolderMenu: 857a479229a4ab0bc62bbfb621d3180a
sidebar.action.revealFolderMenu: 76f4ed52da9937ae432dcf5a108fabb4
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
sidebar.folder.collapse: e9f8c6da708219313d72d99cc3998388
sidebar.section.name: c3642037e309db278bbbfe0590114c02
sidebar.section.showInSidebar: e38d073926d6fb3dfc4d4347708e3cf5
sidebar.section.toggle: 2992ffbc76e607a2dc0e99781f101637
sidebar.disabled.comingSoon: 81151a1669ebd695e837f304a0d8ec79
noteList.title.archive: e727b00944f81e1d0a95c12886ac4641
noteList.title.changes: c112bb3542e98308d12d5ecb10a67abc
noteList.title.inbox: 3882d32c66e7e768145ecd8f104b0c08
noteList.title.history: 16d2b386b2034b9488996466aaae0b57
noteList.title.view: 4351cfebe4b61d8aa5efa1d020710005
noteList.title.notes: f4c6f851b00d5518bf888815de279aba
noteList.searchPlaceholder: 4857cd2689a0a40eb4a77cdc745c518e
noteList.searchAction: 5012120fc480c67686dd22ee2f9493cd
noteList.clearSearch: 9983381c21069a3d6e4432e0aaea0079
noteList.quickOpenCreate: a4e964a3bc5b25e7ae87163624731e05
noteList.createNote: f1484bc40bd3fe3430c13fb89e2ce1af
noteList.empty.changesError: 4fca500c4b70f61f7d9413c57c34bdc4
noteList.empty.noChanges: ad82ac153532f5625760c29c176c5d5f
noteList.empty.noArchived: 75fa9e2aaa902f65d433be856a2e8331
noteList.empty.noMatching: 22838e3b8d3d174d33d7986ab6bdd693
noteList.empty.allOrganized: ea8fbb54d21c03fae1469635e7043b9b
noteList.empty.noNotes: 910bd677fdd52f3e6edb4abc1d03ade8
noteList.empty.noMatchingItems: 25cd76c4bbd00b682a45687705b41a14
noteList.empty.noRelatedItems: 67eecd103b56ef0e56fd892d379b5a5e
noteList.sort.modified: 35e0c8c0b180c95d4e122e55ed62cc64
noteList.sort.created: 0eceeb45861f9585dd7a97a3e36f85c6
noteList.sort.title: b78a3223503896721cca1303f776159b
noteList.sort.status: ec53a8c4f07baed5d8825072c89799be
noteList.sort.by: 5cd12b67b005056a94f06368a7645b8e
noteList.sort.menu: 62bfadcf958eb6ad238e6c71e312a0ce
noteList.sort.ascending: cf3fb1ff52ea1eed3347ac5401ee7f0c
noteList.sort.descending: e3cf5ac19407b1a62c6fccaff675a53b
noteList.filter.open: c3bf447eabe632720a3aa1a7ce401274
noteList.filter.archived: 7d69b3cb4cada18ae61811304f8fbcb5
noteList.filter.week: d2ce009594dcc60befa6a4e6cbeb71fc
noteList.filter.month: 7cbb885aa1164b390a0bc050a64e1812
noteList.filter.all: b1c94ca2fbc3e78fc30069c8d0f01680
noteList.properties.customizeColumns: be39bb193d872e11bb21388add6eca23
noteList.properties.customizeAllColumns: 0d382ece9a9d08bb9ddf10ed2c88d726
noteList.properties.customizeInboxColumns: e7cf53b914ce1079801eeee603875660
noteList.properties.customizeViewColumns: a5612e5d2aa2e86941d536a85b27c8b4
noteList.properties.showInNoteList: f22a57c5fc14ff477000cc66d837c344
noteList.properties.searchPlaceholder: 9c7569eb531ff041b2b7cf4de1e5a619
noteList.properties.searchLabel: de1bc9695d5e79a75296abf74dc25056
noteList.properties.noMatches: 1627ecc53b9e97c94b2b6b1b93a58721
noteList.properties.reorder: c0955c7efaa1ce90d449a23cbc52b553
noteList.changes.restoreNote: d6e99303c0e0b7b2abce06fe9214788b
noteList.changes.discardChanges: 98313f623bb6f464b9a154eca0b99bf3
noteList.changes.restoreDescription: 45e13380b0538e6cbff330f99c260c0a
noteList.changes.discardDescription: cd953f51a81e10f8c90135e0106a45d6
noteList.changes.thisFile: 976b976e66879a470635bf0f660e81fc
noteList.changes.restore: 2bd339d85ee3b33e513359ce781b60cc
noteList.changes.discard: d94b42030b9785fd754d5c1754961269
noteList.changes.cancel: ea4788705e6873b424c65e91c2846b19
editor.empty.selectNote: a71ec7c80aefe49c59f6aba033bf453e
editor.empty.shortcuts: 1133fdf3ecef6a09dd9e2a185e1bd4ec
editor.raw.label: 7a9754c15264b0604aa31dfea32321ea
editor.find.findLabel: 4cfa6c981549e990fe2344e4c805405e
editor.find.findPlaceholder: 4cfa6c981549e990fe2344e4c805405e
editor.find.replaceLabel: 0ebe6df8a3ac338e0512acc741823fdb
editor.find.replacePlaceholder: 0ebe6df8a3ac338e0512acc741823fdb
editor.find.matchCount: 29d3cd243c6850f5fbac283e79d6d6a7
editor.find.noMatches: 3b470c1f6a10f58c8a8cb6b904577786
editor.find.invalidRegex: 9704b2ccc6158864810a313702019d68
editor.find.regexMustMatchText: 267728c2e7f2889b39847ee9b9b38891
editor.find.previousMatch: afb1b7e6bbdfaa97114e1b519a817f13
editor.find.nextMatch: 7f4d218b4f566ea7ff69ab8d912ba7b9
editor.find.showReplace: 82c8fdb73dfe8baa1759a36c6c0fb282
editor.find.hideReplace: a85baf9735f9acf26aa15b70f2ee55fb
editor.find.regex: 7486f19f279f95357c84706dd09ce9da
editor.find.matchCase: ba945280b8802ee1a7a50140e2fe94e2
editor.find.close: b040fbda901d2135cad53e54e0127071
editor.find.replace: 0ebe6df8a3ac338e0512acc741823fdb
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.copyNoteDeepLink: 0fff274a7c47e7c12457c0b7412e86ce
editor.toolbar.copyNoteGitUrl: 04a732b98f7585e3c3b5474688e0baac
editor.toolbar.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0
editor.toolbar.moreActions: 89e19353176b94068dec4c768c25e70b
editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e
editor.formatting.highlight: 0b90582f4589d84be89f5b847d4d1ed1
editor.formatting.highlightTooltip: c27173fea9ec1718030c5fa8440b2c78
editor.whiteboard.enterFullscreen: 606bf766125ecff4b33ca98122222c72
editor.whiteboard.exitFullscreen: afd0a0414d79d047f00319921e4d4944
editor.exportPdf.unavailable: 2fce9834984172b64b66767485d815e9
editor.exportPdf.failed: bf3fa78ff5725f5cbde8979b67ec2e2a
filePreview.copyDeepLink: b75833669968adbef634495636e3fe50
deepLinks.copied: 6ada57681192daf3b4afa07ce7a30575
deepLinks.error.ambiguousVault: 4365c6d5cf8c2c1165259b32d24a6274
deepLinks.error.copyFailed: 57b74ecce2d00b4c327d15830f79410d
deepLinks.error.invalidScheme: 58eaa7ac701a0508cbae90c5d3837ce4
deepLinks.error.malformedUrl: 4f85323322ef0a1d627df621edb749c7
deepLinks.error.missingFile: 79a0c73176508c58b4d785fabaf7d8cc
deepLinks.error.missingPath: a6bb1c92f1a38476ff603a2d3ff81223
deepLinks.error.missingVault: e2936e1f599c7ed229efb5d4a122fc4b
deepLinks.error.openFailed: 97901feafe25688fcc3fc4dcfaf82619
deepLinks.error.outsideVault: fda2fd5675b596df7ba0e0b1a11ec47d
deepLinks.error.unavailableVault: 911dfaf8e855bd8cadefe7df9a98a7e8
deepLinks.error.unknownVault: b4940569e884833f50466177dab1f62f
deepLinks.error.unsafePath: e09caab43be8c2372bfc94e947ea34e6
noteGitUrls.copied: 6832bf85abaa03a6304cd1cce073d747
noteGitUrls.error.copyFailed: 1952ee267f473efe8e28ca90659f75f6
noteGitUrls.error.unavailable: ed08c134bba26350fd28f780c6f1b55b
editor.slash.math: a49950aa047c2292e989e368a97a3aae
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
editor.banner.archived: 7d69b3cb4cada18ae61811304f8fbcb5
editor.banner.unarchive: 9fd02d2eb5ce348a74372eea202a0aec
editor.banner.conflict: 384fc5a4b31c662b1e3426a7ef56441e
editor.banner.keepMine: 544a2f2e8e09f93919f10e1920d1b69e
editor.banner.keepMineTooltip: 9988cb86b051dd728e2d2f852f0283e4
editor.banner.keepTheirs: 46131020af44cc6ec4bfa0a8ff39e044
editor.banner.keepTheirsTooltip: 9e5b845c4459747d6ab0df6438ccfac4
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
inspector.empty.invalidProperties: 2ee2429c1ffbabcda9f57f91fb6c0b9d
inspector.empty.fixInEditor: b86d4934630d68bc5333c0feb25e5f3f
inspector.properties.addProperty: 9820ade036bf1bdf80c0b7da24a4ce0a
inspector.properties.deleteProperty: f1d0ab48c8596108e949dfc90e13050b
inspector.properties.none: 6adf97f83acf6453d4a6a4b1070f3754
inspector.properties.workspace: 5b70a213f150f01f0776fa9481ef2ddf
inspector.properties.missingType: 9179080e7bcc72408f3de7cb944ff400
inspector.properties.missingTypeAria: 582c2c46117cff0534d13e7c8a45a3ba
inspector.properties.searchWorkspaces: ebd1ca0ae74f6d6f5b32f18db699be11
inspector.properties.noMatchingWorkspaces: a4339dbce88712e787c0615a961636f6
inspector.properties.searchTypes: 84c8d94846183a79444bf36667e8d7ea
inspector.properties.noMatchingTypes: 9b64c31214171ba3b2d591743990b840
inspector.properties.yes: 93cba07454f06a4a960172bbd6e2a435
inspector.properties.no: bafd7322c6e97d25b6299b5d6fe8920b
inspector.properties.pickDate: e8e91fbba934dc8adfd7e433f5193911
inspector.properties.valuePlaceholder: 689202409e48743b914713f96d93947c
inspector.properties.propertyName: 7e9982311d69d66d38809e5d85377a08
inspector.relationship.add: ec211f7c20af43e742bf2570c3cb84f9
inspector.relationship.addRelationship: d2b0cb45dcdbb00ee70db397c36f73ad
inspector.relationship.name: 7ed3fccc071d6939eff211b1a6fd9696
inspector.relationship.noteTitle: f72ea00f174785710f0369b23c53963c
inspector.relationship.createAndOpen: 55bb53fea743c584c4024e7439c55bfe
inspector.info.title: 4059b0251f66a18cb56f544728796875
inspector.info.modified: 35e0c8c0b180c95d4e122e55ed62cc64
inspector.info.created: 0eceeb45861f9585dd7a97a3e36f85c6
inspector.info.words: 6f15b8d4b7287d60a8ea3d1c5cbadc84
inspector.info.size: 6f6cb72d544962fa333e2e34ce64f719
update.available: 992477975168b876be8e77ce2975e390
update.checking: a07813cc05571f23ce8dd7039583d7da
update.releaseNotes: 5dd03e8d039863e563e049be198c3fd3
update.updateNow: 99b1054c0f320be9f109c877a5efd0b2
update.dismiss: c8a59e7135a20b362f9c768b09454fdb
update.downloading: 2c7fdcafb08f831420f661810f826549
update.readyRestart: 4d7487b02d955a44d599189ec383e469
update.restartNow: 2d9c2140c53daf05855033aaceeaa8fd
status.update.check: eb1b4bb3667dd670210c7822badc2f1d
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
status.build.unknown: d250349dd489bdfeb0cb0750b8b3ff89
status.vault.switch: 8a2ad262643c556ff0cfa99497aaa529
status.vault.default: 5b70a213f150f01f0776fa9481ef2ddf
status.vault.createEmpty: f37c03f236fbf1516222360a936249e8
status.vault.openLocal: a367344e0429a12a5cc9a6cecbbfcde5
status.vault.cloneGit: a7d0aef7c6237a36e54fd5a26e9c23fe
status.vault.cloneGettingStarted: 9953a11a477b441976a626a9acf5d7df
status.vault.availableHeader: 2be36e986c7859a439c30bc5696c15a9
status.vault.manageWorkspaces: ecce6ef1f37e975762b1413d8bc7d21d
status.vault.includeWorkspace: fe8ee397206a29f2d91eea7b95e9c112
status.vault.notFound: 3119b7fa94c96209bca571b7c7ed0b7e
status.vault.reloading: 893da50ef3a9e01d8a99ff5d3ceb55e9
status.vault.reloadingTooltip: 4a884bd7d113797ad16f905f70742da8
status.vault.remove: 7f3b4f76df23626170940dbc55c70728
status.vault.removeConfirmTitle: 713713c6d8c4eb8b75cf12ebaca44f96
status.vault.removeConfirmMessage: 854744291c28ea9e18bed1110b0737d9
status.vault.removeConfirmAction: 8b064b07e2f4f1e63d1d5087aead2648
workspace.manager.title: ecce6ef1f37e975762b1413d8bc7d21d
workspace.manager.description: d0de6e81a4268a52f756f6073f938fc3
workspace.manager.default: 7a1920d61156abc05a60135aefe8bc67
workspace.manager.makeDefault: 581e972562013d920e9b4d364265829e
workspace.manager.label: ce0aa8f9a9fad0faf99b6185fbc09a1f
workspace.manager.alias: e89ff14f985995ab3af3bd8af66d019c
workspace.manager.mounted: 38ca22b706f01504d11883bc1984d45b
status.remote.noneConfigured: 18a4edd4e8d862ccb343937f45585fb1
status.remote.inSync: a56f571b4df55d88fb06e61e343b3c91
status.remote.aheadTitle: 1516f4b92671b83c17441b4cddf25a7a
status.remote.behindTitle: 55ee20469bc46b7c61e7515fbfdc0b5d
status.remote.ahead: 681557ace3a84e15060ffca1623b75e6
status.remote.behind: 1825bb12c857a71861280e490e29debd
status.remote.add: f292f8ef85219f0acde6b66eb9c9c0f4
status.remote.none: ed3f2ebe0d847584fd62fc9e0db2df33
status.remote.noneDescription: f47636010c21c88a6f81cc41a962b26c
status.sync.syncing: b21888f3f83c224b3451901da243d00a
status.sync.conflict: f1d4ac54357cc0932f385d56814ba7e4
status.sync.failed: 85b08b086888f8ee2680ddd3398f3574
status.sync.pullRequired: 1ea27a2d3048b078a18665b24e8dcd91
status.sync.notSynced: d18d6927f7ffe96f72e18b4da12e306b
status.sync.justNow: 7b9275ee2786ef410555041b92b36bb3
status.sync.minutesAgo: b3ac76d1e2595531940035a8e2e2d13c
status.sync.resolveConflicts: b9f44cc5ee01c964f004f16e3a2833ca
status.sync.inProgress: 278d2bf6768809fbf6e97e7bb319a7c0
status.sync.pullAndPush: e07c084a9e734a54bd079d5327a924e7
status.sync.retry: ca43e18d4f930e8a3e6d403f31d30a39
status.sync.now: c03fafa3a653240e6104348e1293bb88
status.sync.synced: 5befab0dde764b6dd8b24a34dc30afa7
status.sync.conflicts: d35cef9595a935771f5e8158f387227b
status.sync.error: 902b0d55fddef6f8d651fe1035b7d4bd
status.sync.status: 76590d5f9708692e3735891bdca40903
status.sync.pull: 718f59718640c6506b3721fbc8bf3a4d
status.conflict.count: 46293d20ed2071fb30762b71fdaa5894
status.offline.title: 3bf45a7003646cc4f963810b0b7ac0f5
status.offline.label: 8d9da4bc0e49a50e09ac9f7e56789d39
status.changes.view: 6d60d24d8f475ddaef94bbbc58514b5a
status.changes.label: c112bb3542e98308d12d5ecb10a67abc
status.commit.local: 6474fe7e2ed525df3da3eb447943bfcd
status.commit.push: a4aaf10ef20cf17a982bb5b6dad198e0
status.commit.label: 59d5b10c3a447f036d85cb5ce524c96c
status.commit.openOnGitHub: 445ad18680a573c2aab062c3273ae16a
status.git.disabledTooltip: 6981fe5b4993e4b3299c774b185ecd70
status.git.disabled: 7f822b0ee91b8922b0c41723da867bd1
status.history.onlyGit: 7da5397c33ada1b73a81c5a449cbab16
status.history.open: 802ec1fd97592612c19ebef7bebbb5c2
status.history.label: 16d2b386b2034b9488996466aaae0b57
status.mcp.notConnected: 7e8850f4a564088ced4f592996a39309
status.mcp.unknown: 051ff29ff900592f75974d0ef554c2eb
status.claude.missing: 89021856e6152467f54ed33faed55e2f
status.claude.label: 38c66b824d5769e42c3866005296525b
status.claude.install: 1f4688bc2a2acc268811c9edcec810a1
status.ai.noAgents: 0876a176a6297e79366d0a2935851f81
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
status.ai.unavailable: 0d3e6e3afef545710aa24ed2ea4990af
status.ai.install: 349838fb1d851d3e2014b9fe39203275
status.ai.installAgent: 78ac3395d977f8b86ca9a02f781f4af8
status.ai.vaultGuidance: 7bb4644efe6ae7cec9993c8bd85f1519
status.ai.restoreGuidance: 23331fecc692d11d85cf24838ed273f2
status.ai.openOptions: d387b1ab67586c7a2d83db8900a4dd8e
status.ai.openWorkspace: 178247429f30251ce21265570b612e9f
status.ai.modelTargets: 8f3ceb96e088f8bdaecc236778401ac9
status.ai.localChat: 215a24e3d38fb8b68deea6dad0142326
status.ai.apiChat: 6dffd5b9997885efe701631cea5c470d
pulse.title: 16d2b386b2034b9488996466aaae0b57
pulse.today: 1dd1c5fb7f25cd41b291d43a89e3aefd
pulse.yesterday: ebfe9ce86e6e9fb953aa7a25b59c1956
pulse.commitCount: 17dc5f11868c946f55ff8164d318856d
pulse.commitSingular: fffca4d67ea0a788813031b8bbc3b329
pulse.commitPlural: a90814b38bbdfe717205f6d24183f6a7
pulse.openOnGitHub: ddab0146d46f585f2ab36072c92a0048
pulse.expandFiles: 53c567ce9f999dd937fc954c39853e81
pulse.collapseFiles: 0ae74096d72faf840cd7d35635aa0cf9
pulse.noActivity: cfbd8245c849f5f00b495d61ea14dfdf
pulse.emptyDescription: b5ee4968b62908444166182fe8e593c1
pulse.retry: 6327b4e59f58137083214a1fec358855
pulse.loadingActivity: 26bf6a6bc2fd8148cd8753542a0ddf86
pulse.loading: 5f02f05c744a0f875aebb8625ae1486f
pulse.loadError: 63c17996fe219e6bd33b27d2b7ce0c96
command.switchLanguage: 60f1a6760c7d1da0b1f7f6d0529608ee
locale.itIT: 4be8e06d27bca7e1828f2fa9a49ca985
locale.frFR: ad225f707802ba118c22987186dd38e8
locale.deDE: 86bc3115eb4e9873ac96904a4a68e19e
locale.ruRU: deba6920e70615401385fe1fb5a379ec
locale.esES: cdbe021be1976335ab583a845edf7ed6
locale.ptBR: 26d5352ead4a731f3d11eb2e85d0e297
locale.ptPT: 71bfc0d79772120b52c1c0782450ff84
locale.es419: edbf64ac382b82a54795fb409beb54be
locale.zhCN: 1e81fc32fea0e9f3a978661e4b5e484d
locale.zhTW: d0bb8c23f6e4f8c8037b6774ba912036
locale.jaJP: f32ced6a9ba164c4b3c047fd1d7c882e
locale.koKR: d0bdb3cde477d82e766da05ebda50ccb
locale.vi: 7b80fae85640c16cdb0261bef0c27636
locale.plPL: c730389bc8d99e59c867766babdd48b5
locale.beBY: 0a6de4460095f398e8a45ed512a7538f
locale.beLatn: 9c454dccccd6aaac0121a45b923dc5ad
locale.idID: cc1c9b57607b4439578b68074a5e2d80

35
lara.yaml Normal file
View File

@@ -0,0 +1,35 @@
version: "1.0.0"
project:
instruction: >
Tolaria is a desktop knowledge-management app. Keep product names, CLI names,
markdown wikilinks, frontmatter keys, file paths, and placeholders like
{agent}, {zoom}, {language}, {name}, {label}, {file}, and {count} unchanged.
locales:
source: en
target:
- it-IT
- fr-FR
- de-DE
- ru-RU
- es-ES
- pt-BR
- pt-PT
- es-419
- zh-CN
- zh-TW
- ja-JP
- ko-KR
- vi
- pl-PL
- be-BY
- id-ID
files:
json:
include:
- "src/lib/locales/[locale].json"
exclude: []
lockedKeys: []
ignoredKeys: []

View File

@@ -0,0 +1,23 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { vaultContext } from './vault.js'
export async function readAgentInstructions(vaultPath) {
const instructionsPath = path.join(vaultPath, 'AGENTS.md')
try {
return {
path: instructionsPath,
content: await readFile(instructionsPath, 'utf8'),
}
} catch (error) {
if (error?.code === 'ENOENT') return null
throw error
}
}
export async function vaultContextWithInstructions(vaultPath) {
return {
...(await vaultContext(vaultPath)),
agentInstructions: await readAgentInstructions(vaultPath),
}
}

View File

@@ -2,13 +2,13 @@
/**
* Tolaria MCP Server — lightweight vault tools for AI agents.
*
* The agent has full shell access (bash, read, write, edit).
* These MCP tools provide Tolaria-specific capabilities that
* native tools cannot replace:
* These MCP tools provide Tolaria-specific capabilities alongside each
* app-managed agent's own Safe / Power User permission profile:
*
* - search_notes: full-text search across vault notes
* - get_vault_context: vault structure overview (types, note count, folders)
* - get_note: parsed frontmatter + content (convenience over raw cat)
* - create_note: create a new markdown note without overwriting existing files
* - open_note: signal Tolaria UI to open a note as a tab
* - highlight_editor: visually highlight a UI element (editor, tab, etc.)
* - refresh_vault: trigger vault rescan so new/modified files appear
@@ -20,46 +20,102 @@ import {
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import WebSocket from 'ws'
import { searchNotes, getNote, vaultContext } from './vault.js'
import { createMcpToolService } from './tool-service.js'
const VAULT_PATH = process.env.VAULT_PATH || process.env.HOME + '/Laputa'
const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10)
const WS_UI_URL = `ws://localhost:${WS_UI_PORT}`
const LOCAL_READ_ONLY_TOOL_ANNOTATIONS = Object.freeze({
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
})
const LOCAL_CREATE_TOOL_ANNOTATIONS = Object.freeze({
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
})
// 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', () => {
uiSocket = ws
if (shutdownStarted) {
closeUiSocket()
return
}
console.error(`[mcp] Connected to UI bridge at ${WS_UI_URL}`)
})
ws.on('close', () => {
uiSocket = null
setTimeout(connectUiBridge, RECONNECT_INTERVAL_MS)
if (uiSocket === ws) uiSocket = null
scheduleUiReconnect()
})
ws.on('error', () => {
// Silent — bridge may not be running yet, will retry
})
} catch {
setTimeout(connectUiBridge, RECONNECT_INTERVAL_MS)
scheduleUiReconnect()
}
}
connectUiBridge()
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?.()
}
function broadcastUiAction(action, payload) {
if (!uiSocket || uiSocket.readyState !== WebSocket.OPEN) return
uiSocket.send(JSON.stringify({ type: 'ui_action', action, ...payload }))
}
const toolService = createMcpToolService({ emitUiAction: broadcastUiAction })
const TOOLS = [
{
name: 'search_notes',
description: 'Full-text search across vault notes by title or content. Returns matching paths, titles, and snippets.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
@@ -71,16 +127,50 @@ const TOOLS = [
},
{
name: 'get_vault_context',
description: 'Get vault orientation: entity types, total note count, top-level folders, and 20 most recently modified notes.',
inputSchema: { type: 'object', properties: {} },
description: 'Get vault orientation for the active Tolaria vaults: entity types, AGENTS.md instructions, note count, folders, and recent notes.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
vaultPath: { type: 'string', description: 'Optional target vault root. Omit to inspect all active vaults.' },
},
},
},
{
name: 'list_vaults',
description: 'List the current active Tolaria vaults available to MCP tools, including whether each vault has AGENTS.md instructions.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_note',
description: 'Read a note with parsed YAML frontmatter and markdown content. Returns {path, frontmatter, content}.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative path to the note (e.g. "project/my-project.md")' },
vaultPath: { type: 'string', description: 'Optional target vault root when multiple vaults are active.' },
},
required: ['path'],
},
},
{
name: 'create_note',
description: 'Create a new markdown note inside an active Tolaria vault. Does not overwrite existing files. Use content for the full markdown including YAML frontmatter and H1.',
annotations: LOCAL_CREATE_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative path inside the vault, or an absolute path inside an active vault. Must end in .md.' },
content: { type: 'string', description: 'Full markdown note content, including YAML frontmatter when needed.' },
title: { type: 'string', description: 'Optional title used only when content is omitted.' },
type: { type: 'string', description: 'Optional note type used only when content is omitted.' },
is_a: { type: 'string', description: 'Legacy alias for type, used only when content is omitted.' },
vaultPath: { type: 'string', description: 'Optional target vault root when multiple vaults are active.' },
},
required: ['path'],
},
@@ -88,10 +178,12 @@ const TOOLS = [
{
name: 'open_note',
description: 'Open a note in the Tolaria UI as a new tab. Use after creating or editing a note so the user can see it.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative path to the note' },
vaultPath: { type: 'string', description: 'Optional target vault root when opening a note outside the default vault.' },
},
required: ['path'],
},
@@ -99,6 +191,7 @@ const TOOLS = [
{
name: 'highlight_editor',
description: 'Visually highlight a UI element in Tolaria (editor, tab, properties panel, or note list). The highlight auto-clears after a short delay.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
@@ -111,60 +204,83 @@ const TOOLS = [
{
name: 'refresh_vault',
description: 'Trigger a vault rescan so new or modified files appear immediately in the Tolaria note list.',
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Optional specific note path that changed' },
vaultPath: { type: 'string', description: 'Optional target vault root when refreshing a note outside the default vault.' },
},
},
},
]
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 results = await toolService.searchNotes(args)
const text = results.length === 0
? 'No matching notes found.'
: results.map(r => `**${r.title}** (${r.path})\n${r.snippet}`).join('\n\n')
: results.map(r => `**${r.title}** (${r.vaultLabel} / ${r.path})\n${r.snippet}`).join('\n\n')
return { content: [{ type: 'text', text }] }
}
async function handleVaultContext() {
const ctx = await vaultContext(VAULT_PATH)
async function handleVaultContext(args = {}) {
const ctx = await toolService.vaultContext(args)
return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }] }
}
async function handleListVaults() {
return { content: [{ type: 'text', text: JSON.stringify(await toolService.listVaults(), null, 2) }] }
}
async function handleGetNote(args) {
const note = await getNote(VAULT_PATH, args.path)
const note = await toolService.readNote(args)
return { content: [{ type: 'text', text: JSON.stringify(note, null, 2) }] }
}
async function handleCreateNote(args = {}) {
const note = await toolService.createNote(args)
return {
content: [{
type: 'text',
text: JSON.stringify(note, null, 2),
}],
}
}
function handleOpenNote(args) {
// Refresh vault first so the new/modified note appears in the note list,
// then signal the UI to open it in a tab.
broadcastUiAction('vault_changed', { path: args.path })
broadcastUiAction('open_tab', { path: args.path })
return { content: [{ type: 'text', text: `Opening ${args.path} in Tolaria` }] }
const { targetPath } = toolService.openNoteAsTab(args)
return { content: [{ type: 'text', text: `Opening ${targetPath} in Tolaria` }] }
}
function handleHighlightEditor(args) {
broadcastUiAction('highlight', { element: args.element, path: args.path })
toolService.highlightEditor(args)
return { content: [{ type: 'text', text: `Highlighting ${args.element}` }] }
}
function handleRefreshVault(args) {
broadcastUiAction('vault_changed', { path: args?.path })
toolService.refreshVault(args)
return { content: [{ type: 'text', text: 'Vault refresh triggered' }] }
}
const TOOL_HANDLERS = new Map([
['search_notes', handleSearchNotes],
['get_vault_context', handleVaultContext],
['list_vaults', handleListVaults],
['get_note', handleGetNote],
['create_note', handleCreateNote],
['open_note', handleOpenNote],
['highlight_editor', handleHighlightEditor],
['refresh_vault', handleRefreshVault],
])
function callToolHandler(name, args) {
const handler = TOOL_HANDLERS.get(name)
if (!handler) throw new Error(`Unknown tool: ${name}`)
return handler(args)
}
// --- Server setup ---
const server = new Server(
@@ -178,12 +294,8 @@ 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 handler(args)
return await callToolHandler(name, args)
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
@@ -192,10 +304,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
})
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
console.error(`Tolaria MCP server running (vault: ${VAULT_PATH})`)
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))
}
main().catch(console.error)
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 (vaults resolved per call)')
}
main().catch((error) => {
console.error(error)
void shutdown(1)
})

View File

@@ -10,13 +10,13 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"gray-matter": "^4.0.3",
"ws": "^8.19.0"
"ws": "^8.20.1"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.9",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
"version": "1.19.13",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -431,12 +431,12 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"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==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
@@ -467,9 +467,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",
@@ -619,9 +619,9 @@
}
},
"node_modules/hono": {
"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==",
"version": "4.12.21",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz",
"integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -670,9 +670,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz",
"integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -882,9 +882,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
"integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -914,9 +914,9 @@
}
},
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -1227,9 +1227,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View File

@@ -6,11 +6,20 @@
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node --test test.js"
"test": "node --test test.js tool-service.test.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"gray-matter": "^4.0.3",
"ws": "^8.19.0"
"ws": "^8.20.1"
},
"overrides": {
"@hono/node-server": "1.19.13",
"express-rate-limit": "8.2.2",
"fast-uri": "3.1.2",
"hono": "4.12.21",
"qs": "6.15.2",
"ip-address": "10.1.1",
"path-to-regexp": "8.4.0"
}
}

View File

@@ -1,23 +1,34 @@
import { describe, it, before, after } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
import { spawn } from 'node:child_process'
import {
findMarkdownFiles, getNote, searchNotes, vaultContext,
access, mkdtemp, mkdir, open, readFile, rm, writeFile,
} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { clearTimeout, setTimeout } from 'node:timers'
import { fileURLToPath } from 'node:url'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import {
createNote, findMarkdownFiles, getNote, searchNotes, vaultContext,
} from './vault.js'
import { requireVaultPath, requireVaultPaths } from './vault-path.js'
import { vaultContextWithInstructions } from './agent-instructions.js'
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 fs.mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-test-'))
tmpDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-test-'))
await fs.mkdir(path.join(tmpDir, 'project'), { recursive: true })
await fs.mkdir(path.join(tmpDir, 'note'), { recursive: true })
await mkdir(path.join(tmpDir, 'project'), { recursive: true })
await mkdir(path.join(tmpDir, 'note'), { recursive: true })
await fs.writeFile(path.join(tmpDir, 'project', 'test-project.md'), `---
await writeTextFile(path.join(tmpDir, 'project', 'test-project.md'), `---
title: Test Project
is_a: Project
status: Active
@@ -28,7 +39,7 @@ status: Active
This is a test project for the MCP server.
`)
await fs.writeFile(path.join(tmpDir, 'note', 'daily-log.md'), `---
await writeTextFile(path.join(tmpDir, 'note', 'daily-log.md'), `---
title: Daily Log
is_a: Note
---
@@ -38,7 +49,18 @@ is_a: Note
Today I worked on the MCP server implementation.
`)
await fs.writeFile(path.join(tmpDir, 'project', 'second-project.md'), `---
await writeTextFile(path.join(tmpDir, 'note', 'hashtag-tags.md'), `---
title: Hashtag Tags
type: Note
tags: [#abc, def, ghi]
---
# Hashtag Tags
This note has AI-generated hashtag-style YAML tags.
`)
await writeTextFile(path.join(tmpDir, 'project', 'second-project.md'), `---
title: Second Project
type: Project
status: Draft
@@ -53,16 +75,17 @@ Another project for testing list and context.
})
after(async () => {
await fs.rm(tmpDir, { recursive: true, force: true })
await rm(tmpDir, { recursive: true, force: true })
})
describe('findMarkdownFiles', () => {
it('should find all .md files recursively', async () => {
const files = await findMarkdownFiles(tmpDir)
assert.equal(files.length, 3)
assert.equal(files.length, 4)
assert.ok(files.some(f => f.endsWith('test-project.md')))
assert.ok(files.some(f => f.endsWith('daily-log.md')))
assert.ok(files.some(f => f.endsWith('second-project.md')))
assert.ok(files.some(f => f.endsWith('hashtag-tags.md')))
})
})
@@ -75,6 +98,15 @@ describe('getNote', () => {
assert.ok(note.content.includes('test project for the MCP server'))
})
it('should tolerate hashtag-style tags in malformed YAML frontmatter', async () => {
const note = await getNote(tmpDir, 'note/hashtag-tags.md')
assert.equal(note.path, 'note/hashtag-tags.md')
assert.equal(note.frontmatter.title, 'Hashtag Tags')
assert.equal(note.frontmatter.type, 'Note')
assert.deepEqual(note.frontmatter.tags, ['#abc', 'def', 'ghi'])
assert.ok(note.content.includes('has AI-generated hashtag-style YAML tags'))
})
it('should throw for missing notes', async () => {
await assert.rejects(
() => getNote(tmpDir, 'nonexistent.md'),
@@ -94,6 +126,74 @@ describe('getNote', () => {
})
})
describe('createNote', () => {
it('creates a new markdown note inside the vault', async () => {
const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-'))
const content = `---
type: Note
---
# MCP Created
`
try {
const note = await createNote(vaultDir, 'note/mcp-created.md', content)
assert.equal(note.path, 'note/mcp-created.md')
assert.equal(await readFile(path.join(vaultDir, note.path), 'utf-8'), content)
} finally {
await rm(vaultDir, { recursive: true, force: true })
}
})
it('does not overwrite an existing note', async () => {
const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-existing-'))
const notePath = path.join(vaultDir, 'existing.md')
await writeFile(notePath, '# Existing\n', 'utf-8')
try {
await assert.rejects(
() => createNote(vaultDir, 'existing.md', '# Replacement\n'),
{ code: 'EEXIST' },
)
assert.equal(await readFile(notePath, 'utf-8'), '# Existing\n')
} finally {
await rm(vaultDir, { recursive: true, force: true })
}
})
it('rejects absolute paths outside the vault', async () => {
const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-vault-'))
const outsideDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-outside-'))
try {
await assert.rejects(
() => createNote(vaultDir, path.join(outsideDir, 'outside.md'), '# Outside\n'),
{ message: ACTIVE_VAULT_ERROR },
)
} finally {
await rm(vaultDir, { recursive: true, force: true })
await rm(outsideDir, { recursive: true, force: true })
}
})
it('rejects outside paths before creating missing parent folders', async () => {
const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-vault-'))
const outsideDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-outside-'))
const outsideParent = path.join(outsideDir, 'missing-parent')
try {
await assert.rejects(
() => createNote(vaultDir, path.join(outsideParent, 'outside.md'), '# Outside\n'),
{ message: ACTIVE_VAULT_ERROR },
)
await assert.rejects(() => access(outsideParent), { code: 'ENOENT' })
} finally {
await rm(vaultDir, { recursive: true, force: true })
await rm(outsideDir, { recursive: true, force: true })
}
})
})
describe('searchNotes', () => {
it('should find notes matching title', async () => {
const results = await searchNotes(tmpDir, 'Test Project')
@@ -131,6 +231,14 @@ describe('vaultContext', () => {
assert.ok(ctx.types.includes('Note'))
})
it('should include notes with hashtag-style tags in malformed YAML frontmatter', async () => {
const ctx = await vaultContext(tmpDir)
const note = ctx.recentNotes.find(entry => entry.path === 'note/hashtag-tags.md')
assert.ok(note)
assert.equal(note.title, 'Hashtag Tags')
assert.equal(note.type, 'Note')
})
it('should cap recent notes at 20', async () => {
const ctx = await vaultContext(tmpDir)
assert.ok(ctx.recentNotes.length <= 20)
@@ -152,7 +260,27 @@ describe('vaultContext', () => {
it('should report correct note count', async () => {
const ctx = await vaultContext(tmpDir)
assert.equal(ctx.noteCount, 3)
assert.equal(ctx.noteCount, 4)
})
it('includes root AGENTS.md instructions when present', async () => {
const agentsPath = path.join(tmpDir, 'AGENTS.md')
await writeFile(agentsPath, '# Vault Rules\n\nUse this vault carefully.\n', 'utf-8')
try {
const ctx = await vaultContextWithInstructions(tmpDir)
assert.deepEqual(ctx.agentInstructions, {
path: agentsPath,
content: '# Vault Rules\n\nUse this vault carefully.\n',
})
} finally {
await rm(agentsPath, { force: true })
}
})
it('reports null agent instructions when AGENTS.md is absent', async () => {
const ctx = await vaultContextWithInstructions(tmpDir)
assert.equal(ctx.agentInstructions, null)
})
})
@@ -191,17 +319,229 @@ describe('evaluateBridgeRequest', () => {
})
})
describe('requireVaultPath', () => {
it('returns the explicit configured vault path', () => {
assert.equal(
requireVaultPath({ VAULT_PATH: '/tmp/Selected Vault' }),
'/tmp/Selected Vault',
)
})
it('rejects missing vault paths instead of falling back to ~/Laputa', async () => {
const configDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-empty-config-'))
assert.throws(
() => requireVaultPaths({}, { configDir }),
/VAULT_PATH is required/,
)
await rm(configDir, { recursive: true, force: true })
})
it('returns all configured active vault paths with the primary vault first', () => {
assert.deepEqual(
requireVaultPaths({
VAULT_PATH: '/tmp/Default Vault',
VAULT_PATHS: JSON.stringify(['/tmp/Default Vault', '/tmp/Second Vault']),
}),
['/tmp/Default Vault', '/tmp/Second Vault'],
)
})
it('loads active mounted vault paths from Tolaria config when env is vault-neutral', async () => {
const configDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-config-'))
const primaryVault = path.join(configDir, 'Primary Vault')
const secondaryVault = path.join(configDir, 'Secondary Vault')
const hiddenVault = path.join(configDir, 'Hidden Vault')
const configPath = path.join(configDir, 'com.tolaria.app', 'vaults.json')
await mkdir(path.dirname(configPath), { recursive: true })
await writeFile(configPath, JSON.stringify({
active_vault: primaryVault,
vaults: [
{ label: 'Secondary', path: secondaryVault, mounted: true },
{ label: 'Hidden', path: hiddenVault, mounted: false },
{ label: 'Primary', path: primaryVault, mounted: true },
],
}), 'utf-8')
try {
assert.deepEqual(
requireVaultPaths({}, { configDir }),
[primaryVault, secondaryVault],
)
} finally {
await rm(configDir, { recursive: true, force: true })
}
})
})
describe('stdio process lifecycle', () => {
it('advertises local vault tools as approval-safe for MCP clients', async () => {
const { client, stderr } = await connectMcpClient()
try {
const { tools } = await client.listTools()
const toolsByName = new Map(tools.map(tool => [tool.name, tool]))
const safeReadTools = [
'search_notes',
'get_vault_context',
'list_vaults',
'get_note',
'open_note',
'highlight_editor',
'refresh_vault',
]
for (const name of safeReadTools) {
const tool = toolsByName.get(name)
assert.ok(tool, `Missing MCP tool: ${name}`)
assert.equal(tool.annotations?.readOnlyHint, true, `${name} should not require destructive approval`)
assert.equal(tool.annotations?.destructiveHint, false, `${name} should not be treated as destructive`)
assert.equal(tool.annotations?.openWorldHint, false, `${name} should stay scoped to local active vaults`)
}
const createTool = toolsByName.get('create_note')
assert.ok(createTool, 'Missing MCP tool: create_note')
assert.equal(createTool.annotations?.readOnlyHint, false)
assert.equal(createTool.annotations?.destructiveHint, false)
assert.equal(createTool.annotations?.openWorldHint, false)
} finally {
await closeMcpClient(client, stderr)
}
})
it('creates a note through the MCP create_note tool', async () => {
const { client, stderr } = await connectMcpClient()
const relativePath = 'note/mcp-tool-created.md'
const absolutePath = path.join(tmpDir, relativePath)
const content = `---
type: Note
---
# MCP Tool Created
`
try {
await rm(absolutePath, { force: true })
const result = await client.callTool({
name: 'create_note',
arguments: { path: relativePath, content },
})
assert.equal(await readFile(absolutePath, 'utf-8'), content)
assert.match(JSON.stringify(result.content), /mcp-tool-created\.md/)
} finally {
await rm(absolutePath, { force: true })
await closeMcpClient(client, stderr)
}
})
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 connectMcpClient() {
const transport = new StdioClientTransport({
command: process.execPath,
args: ['index.js'],
cwd: MCP_SERVER_DIR,
env: { ...process.env, VAULT_PATH: tmpDir, WS_UI_PORT: '65534' },
stderr: 'pipe',
})
const stderr = collectTransportStderr(transport)
const client = new Client(
{ name: 'tolaria-mcp-test-client', version: '0.0.0' },
{ capabilities: {} },
)
await client.connect(transport)
return { client, stderr }
}
function collectTransportStderr(transport) {
const chunks = []
transport.stderr?.setEncoding('utf8')
transport.stderr?.on('data', chunk => {
chunks.push(chunk)
})
return () => chunks.join('')
}
async function closeMcpClient(client, stderr) {
try {
await client.close()
} catch (error) {
assert.fail(`Failed to close MCP test client: ${error.message}\n${stderr()}`)
}
}
async function assertRejectsOutsideVault(prefix, resolveNotePath) {
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
const outsideDir = await mkdtemp(path.join(os.tmpdir(), prefix))
const outsideNote = path.join(outsideDir, 'outside.md')
try {
await fs.writeFile(outsideNote, '# Outside\n')
await writeTextFile(outsideNote, '# Outside\n')
await assert.rejects(
() => getNote(tmpDir, resolveNotePath(outsideNote)),
{ message: ACTIVE_VAULT_ERROR },
)
} finally {
await fs.rm(outsideDir, { recursive: true, force: true })
await 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)
}
})
}

213
mcp-server/tool-service.js Normal file
View File

@@ -0,0 +1,213 @@
import path from 'node:path'
import {
createNote as createVaultNote,
getNote,
searchNotes as searchVaultNotes,
} from './vault.js'
import { requireVaultPaths } from './vault-path.js'
import { readAgentInstructions, vaultContextWithInstructions } from './agent-instructions.js'
export function createMcpToolService({
resolveVaultPaths = () => requireVaultPaths(),
emitUiAction = () => {},
} = {}) {
function activeVaultPaths() {
return resolveVaultPaths()
}
function requestedVaultPath(args = {}) {
const requested = typeof args.vaultPath === 'string' ? args.vaultPath.trim() : ''
if (!requested) return null
if (!activeVaultPaths().includes(requested)) {
throw new Error(`Vault is not active in Tolaria: ${requested}`)
}
return requested
}
function resolveUiPath(args = {}) {
const notePath = typeof args.path === 'string' ? args.path : ''
if (path.isAbsolute(notePath)) return notePath
const roots = activeVaultPaths()
const vaultPath = requestedVaultPath(args) ?? (roots.length === 1 ? roots[0] : '')
return vaultPath ? path.join(vaultPath, notePath) : notePath
}
async function readNote(args = {}) {
return getNoteFromActiveVaults(notePathArg(args), requestedVaultPath(args))
}
async function searchNotes(args = {}) {
const requestedLimit = Number.isFinite(args.limit) && args.limit > 0 ? args.limit : 10
const results = []
for (const vaultPath of activeVaultPaths()) {
const vaultResults = await searchVaultNotes(vaultPath, args.query, requestedLimit)
results.push(...vaultResults.map((result) => withVaultMetadata(result, vaultPath)))
if (results.length >= requestedLimit) break
}
return results.slice(0, requestedLimit)
}
async function vaultContext(args = {}) {
const targetVaultPath = requestedVaultPath(args)
const roots = activeVaultPaths()
if (targetVaultPath) return vaultContextWithInstructions(targetVaultPath)
if (roots.length === 1) return vaultContextWithInstructions(roots[0])
return {
vaults: await Promise.all(roots.map(vaultContextWithInstructions)),
}
}
async function listVaults() {
const vaults = await Promise.all(activeVaultPaths().map(async (vaultPath) => {
const agentInstructions = await readAgentInstructions(vaultPath)
return {
path: vaultPath,
label: vaultLabel(vaultPath),
agentInstructionsPath: agentInstructions?.path ?? null,
hasAgentInstructions: agentInstructions !== null,
}
}))
return { vaults }
}
async function createNote(args = {}) {
const vaultPath = writableVaultPath(args)
const notePath = writableNotePath(args, vaultPath)
const note = await createVaultNote(vaultPath, notePath, createNoteContent(args))
const targetPath = resolveUiPath({ ...args, path: note.path, vaultPath })
emitUiAction('vault_changed', { path: targetPath })
emitUiAction('open_tab', { path: targetPath })
return { path: note.path, absolutePath: note.absolutePath, vaultPath }
}
function openNoteAsTab(args = {}) {
const targetPath = resolveUiPath(args)
emitUiAction('vault_changed', { path: targetPath })
emitUiAction('open_tab', { path: targetPath })
return { targetPath }
}
function openNoteInEditor(args = {}) {
const targetPath = resolveUiPath(args)
emitUiAction('vault_changed', { path: targetPath })
emitUiAction('open_note', { path: targetPath })
return { targetPath }
}
function highlightEditor(args = {}) {
emitUiAction('highlight', { element: args.element, path: args.path })
}
function setFilter(args = {}) {
emitUiAction('set_filter', { filterType: args.type })
}
function refreshVault(args = {}) {
emitUiAction('vault_changed', { path: resolveUiPath(args) })
}
async function getNoteFromActiveVaults(notePath, vaultPath = null) {
const candidates = vaultPath ? [vaultPath] : activeVaultPaths()
const matches = []
const errors = []
for (const candidate of candidates) {
try {
matches.push(withVaultMetadata(await getNote(candidate, notePath), candidate))
} catch (error) {
errors.push(error)
}
}
if (matches.length === 1) return matches[0]
if (matches.length > 1) {
throw new Error(`Note path is ambiguous across active vaults. Pass vaultPath for ${notePath}.`)
}
throw errors[0] ?? new Error(`Note not found: ${notePath}`)
}
function writableVaultPath(args = {}) {
const requested = requestedVaultPath(args)
if (requested) return requested
const roots = activeVaultPaths()
const notePath = notePathArg(args)
if (path.isAbsolute(notePath)) {
const root = roots.find(vaultPath => isInsideVaultRoot(vaultPath, notePath))
if (root) return root
}
if (roots.length === 1) return roots[0]
throw new Error(`Note path is ambiguous across active vaults. Pass vaultPath for ${notePath}.`)
}
return {
activeVaultPaths,
createNote,
highlightEditor,
listVaults,
openNoteAsTab,
openNoteInEditor,
readNote,
refreshVault,
requestedVaultPath,
resolveUiPath,
searchNotes,
setFilter,
vaultContext,
}
}
function writableNotePath(args, vaultPath) {
const notePath = notePathArg(args)
if (!path.isAbsolute(notePath) || !isInsideVaultRoot(vaultPath, notePath)) return notePath
return path.relative(vaultPath, notePath)
}
function withVaultMetadata(note, vaultPath) {
return {
...note,
vaultPath,
vaultLabel: vaultLabel(vaultPath),
}
}
function vaultLabel(vaultPath) {
return path.basename(vaultPath) || vaultPath
}
function isInsideVaultRoot(vaultPath, notePath) {
const relative = path.relative(vaultPath, notePath)
return Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative)
}
function notePathArg(args = {}) {
const notePath = typeof args.path === 'string' ? args.path.trim() : ''
if (!notePath) throw new Error('Note path is required')
return notePath
}
function yamlScalar(value) {
return JSON.stringify(value)
}
function fallbackCreateNoteContent(args = {}) {
const title = typeof args.title === 'string' && args.title.trim()
? args.title.trim()
: path.basename(notePathArg(args), '.md')
const type = typeof args.type === 'string' && args.type.trim()
? args.type.trim()
: typeof args.is_a === 'string' && args.is_a.trim()
? args.is_a.trim()
: 'Note'
return `---\ntype: ${yamlScalar(type)}\n---\n\n# ${title}\n`
}
function createNoteContent(args = {}) {
return typeof args.content === 'string' && args.content.trim()
? args.content
: fallbackCreateNoteContent(args)
}

View File

@@ -0,0 +1,156 @@
import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { createMcpToolService } from './tool-service.js'
let tmpDir
let firstVault
let secondVault
beforeEach(async () => {
tmpDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-service-'))
firstVault = path.join(tmpDir, 'First Vault')
secondVault = path.join(tmpDir, 'Second Vault')
await seedVault(firstVault, {
'note/shared.md': noteFixture('Shared Note', 'Shared content from the first vault.'),
'note/alpha.md': noteFixture('Alpha Project', 'Project planning in the first vault.'),
})
await seedVault(secondVault, {
'AGENTS.md': '# Second Vault Rules\n',
'note/shared.md': noteFixture('Shared Note', 'Shared content from the second vault.'),
'note/beta.md': noteFixture('Beta Project', 'Project planning in the second vault.'),
})
})
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true })
})
describe('createMcpToolService', () => {
it('requires vaultPath when reading an ambiguous note path', async () => {
const service = makeService()
await assert.rejects(
() => service.readNote({ path: 'note/shared.md' }),
/Note path is ambiguous across active vaults/,
)
const note = await service.readNote({
path: 'note/shared.md',
vaultPath: secondVault,
})
assert.equal(note.vaultPath, secondVault)
assert.equal(note.vaultLabel, 'Second Vault')
assert.match(note.content, /second vault/)
})
it('creates notes with fallback markdown and emits refresh and tab actions', async () => {
const emittedActions = []
const service = makeService({ emittedActions })
const absolutePath = path.join(secondVault, 'note/created.md')
const note = await service.createNote({
path: absolutePath,
title: 'Created From MCP',
type: 'Project',
})
assert.equal(note.path, 'note/created.md')
assert.equal(note.vaultPath, secondVault)
assert.equal(path.basename(note.absolutePath), 'created.md')
assert.equal(
await readFile(note.absolutePath, 'utf-8'),
'---\ntype: "Project"\n---\n\n# Created From MCP\n',
)
assert.deepEqual(emittedActions, [
{ action: 'vault_changed', payload: { path: absolutePath } },
{ action: 'open_tab', payload: { path: absolutePath } },
])
})
it('searches active vaults with consistent vault metadata', async () => {
const service = makeService()
const results = await service.searchNotes({ query: 'Project', limit: 2 })
assert.equal(results.length, 2)
assert.deepEqual(
results.map(({ path: notePath, vaultPath, vaultLabel }) => ({
notePath,
vaultPath,
vaultLabel,
})),
[
{ notePath: 'note/alpha.md', vaultPath: firstVault, vaultLabel: 'First Vault' },
{ notePath: 'note/beta.md', vaultPath: secondVault, vaultLabel: 'Second Vault' },
],
)
})
it('lists active vaults with agent-instruction metadata', async () => {
const service = makeService()
assert.deepEqual(await service.listVaults(), {
vaults: [
{
path: firstVault,
label: 'First Vault',
agentInstructionsPath: null,
hasAgentInstructions: false,
},
{
path: secondVault,
label: 'Second Vault',
agentInstructionsPath: path.join(secondVault, 'AGENTS.md'),
hasAgentInstructions: true,
},
],
})
})
it('emits transport-neutral UI intents for note opening and filters', () => {
const emittedActions = []
const service = makeService({ emittedActions })
service.openNoteAsTab({ path: 'note/beta.md', vaultPath: secondVault })
service.openNoteInEditor({ path: 'note/beta.md', vaultPath: secondVault })
service.highlightEditor({ element: 'editor', path: 'note/beta.md' })
service.setFilter({ type: 'Project' })
service.refreshVault({ path: 'note/beta.md', vaultPath: secondVault })
assert.deepEqual(emittedActions, [
{ action: 'vault_changed', payload: { path: path.join(secondVault, 'note/beta.md') } },
{ action: 'open_tab', payload: { path: path.join(secondVault, 'note/beta.md') } },
{ action: 'vault_changed', payload: { path: path.join(secondVault, 'note/beta.md') } },
{ action: 'open_note', payload: { path: path.join(secondVault, 'note/beta.md') } },
{ action: 'highlight', payload: { element: 'editor', path: 'note/beta.md' } },
{ action: 'set_filter', payload: { filterType: 'Project' } },
{ action: 'vault_changed', payload: { path: path.join(secondVault, 'note/beta.md') } },
])
})
})
function makeService({ emittedActions = [] } = {}) {
return createMcpToolService({
resolveVaultPaths: () => [firstVault, secondVault],
emitUiAction: (action, payload) => {
emittedActions.push({ action, payload })
},
})
}
async function seedVault(vaultPath, files) {
for (const [relativePath, content] of Object.entries(files)) {
const filePath = path.join(vaultPath, relativePath)
await mkdir(path.dirname(filePath), { recursive: true })
await writeFile(filePath, content, 'utf-8')
}
}
function noteFixture(title, body) {
return `---\ntitle: ${JSON.stringify(title)}\ntype: Note\n---\n\n# ${title}\n\n${body}\n`
}

88
mcp-server/vault-path.js Normal file
View File

@@ -0,0 +1,88 @@
import { existsSync, readFileSync } from 'node:fs'
import { homedir, platform } from 'node:os'
import { join } from 'node:path'
const APP_CONFIG_DIR = 'com.tolaria.app'
const LEGACY_APP_CONFIG_DIR = 'com.laputa.app'
function parseVaultPathList(rawValue) {
if (!rawValue?.trim()) return []
try {
const parsed = JSON.parse(rawValue)
if (Array.isArray(parsed)) return parsed.filter(value => typeof value === 'string')
} catch {
// Older clients only set VAULT_PATH; keep VAULT_PATHS strict JSON so paths
// with platform separators are never split incorrectly.
}
return []
}
function uniqueVaultPaths(paths) {
const seen = new Set()
const unique = []
for (const path of paths) {
const trimmed = path.trim()
if (!trimmed || seen.has(trimmed)) continue
seen.add(trimmed)
unique.push(trimmed)
}
return unique
}
function appConfigBaseDir(env = process.env) {
if (platform() === 'darwin') return join(homedir(), 'Library', 'Application Support')
if (platform() === 'win32') return env.APPDATA || join(homedir(), 'AppData', 'Roaming')
return env.XDG_CONFIG_HOME || join(homedir(), '.config')
}
export function vaultsJsonPath({ configDir = appConfigBaseDir() } = {}) {
const preferred = join(configDir, APP_CONFIG_DIR, 'vaults.json')
if (existsSync(preferred)) return preferred
const legacy = join(configDir, LEGACY_APP_CONFIG_DIR, 'vaults.json')
return existsSync(legacy) ? legacy : preferred
}
function pushUniquePath(paths, value) {
const path = typeof value === 'string' ? value.trim() : ''
if (!path || paths.includes(path)) return
paths.push(path)
}
function activeVaultPathsFromList(list) {
const paths = []
pushUniquePath(paths, list?.active_vault)
for (const vault of list?.vaults ?? []) {
if (vault?.mounted === false) continue
pushUniquePath(paths, vault?.path)
}
return paths
}
export function configuredVaultPaths({ configDir } = {}) {
const filePath = vaultsJsonPath({ configDir })
if (!existsSync(filePath)) return []
return activeVaultPathsFromList(JSON.parse(readFileSync(filePath, 'utf-8')))
}
export function requireVaultPaths(env = process.env, options = {}) {
const vaultPaths = uniqueVaultPaths([
env.VAULT_PATH?.trim() ?? '',
...parseVaultPathList(env.VAULT_PATHS),
])
if (vaultPaths.length === 0) {
const configuredPaths = configuredVaultPaths(options)
if (configuredPaths.length > 0) return configuredPaths
throw new Error('VAULT_PATH is required. Open a vault in Tolaria before starting MCP tools.')
}
return vaultPaths
}
export function requireVaultPath(env = process.env, options = {}) {
return requireVaultPaths(env, options)[0]
}

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