Compare commits
586 Commits
codex/mobi
...
alpha-v202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
181ff6de8c | ||
|
|
07c47e68f9 | ||
|
|
05a6e61f11 | ||
|
|
7de2726f27 | ||
|
|
a45e44d0f3 | ||
|
|
14b1668ebe | ||
|
|
c5049e4332 | ||
|
|
c6e570d2e3 | ||
|
|
e93676bbb7 | ||
|
|
952e632622 | ||
|
|
641dc2c885 | ||
|
|
78f8469c98 | ||
|
|
1ec5f7c745 | ||
|
|
f616fe449b | ||
|
|
8ad23f72e1 | ||
|
|
41b531e899 | ||
|
|
03a667661e | ||
|
|
908994c253 | ||
|
|
af83d270fd | ||
|
|
24f4ee3272 | ||
|
|
bbb30d6b91 | ||
|
|
ba3f324a52 | ||
|
|
5240aa3b4e | ||
|
|
f80e571c78 | ||
|
|
df5c2a46e6 | ||
|
|
5090ed31bb | ||
|
|
0bad22eb57 | ||
|
|
05fc93677c | ||
|
|
bdb9fabfd0 | ||
|
|
4296977fe3 | ||
|
|
bb5296ba3d | ||
|
|
cfc62b1ce2 | ||
|
|
29bfde6a20 | ||
|
|
759af948b4 | ||
|
|
5fb1389d69 | ||
|
|
f5475f7773 | ||
|
|
1d93da3ade | ||
|
|
29e5dd8f7b | ||
|
|
18c6d18474 | ||
|
|
8828516d43 | ||
|
|
8df1f3043d | ||
|
|
95b89e331d | ||
|
|
2382305576 | ||
|
|
b9f90b0657 | ||
|
|
0f5ac35f67 | ||
|
|
6424509bb9 | ||
|
|
7614db62f2 | ||
|
|
42dc2f726f | ||
|
|
56c6cb56d6 | ||
|
|
37536e1974 | ||
|
|
9234ffcac2 | ||
|
|
198b7c993f | ||
|
|
ea54560e11 | ||
|
|
fbb51a0b4c | ||
|
|
c8eba73863 | ||
|
|
ae7ee4064f | ||
|
|
e3bfeba163 | ||
|
|
8c8a2bccfe | ||
|
|
42065d9843 | ||
|
|
6080500df0 | ||
|
|
fe1ff59879 | ||
|
|
fe939763cf | ||
|
|
e15aa76826 | ||
|
|
ebe85faf53 | ||
|
|
d0ce5ae165 | ||
|
|
780e33bff2 | ||
|
|
443d34b8b2 | ||
|
|
77fa49d5f5 | ||
|
|
a2094b57b7 | ||
|
|
59d272cf24 | ||
|
|
afc2ead14c | ||
|
|
80fb3dc6c9 | ||
|
|
181906cb17 | ||
|
|
1b00eaf461 | ||
|
|
9ad5ab822a | ||
|
|
c255c91a0b | ||
|
|
384aa22fbd | ||
|
|
093d331719 | ||
|
|
f9c5e70916 | ||
|
|
d08a08195c | ||
|
|
2a3589181e | ||
|
|
fb02644358 | ||
|
|
7c2e7a8990 | ||
|
|
e33f1b3106 | ||
|
|
815827b6e2 | ||
|
|
deb6998c97 | ||
|
|
1427035623 | ||
|
|
b7301568b8 | ||
|
|
0bf921dad1 | ||
|
|
563030982b | ||
|
|
41121ebf70 | ||
|
|
7516dfe48a | ||
|
|
8764b6f78e | ||
|
|
094f59eb7b | ||
|
|
62e1ab0e26 | ||
|
|
0135ce3f10 | ||
|
|
c752a52e28 | ||
|
|
ede82fc7a2 | ||
|
|
8c2432339a | ||
|
|
fbfd0f1cf3 | ||
|
|
2fb14f4c35 | ||
|
|
51e32dc832 | ||
|
|
a541c1b916 | ||
|
|
411c1947ae | ||
|
|
5bebd1c458 | ||
|
|
e2e9e81ee9 | ||
|
|
11c7e35519 | ||
|
|
b9bff9223e | ||
|
|
d204e95460 | ||
|
|
764c899b63 | ||
|
|
a3a9738034 | ||
|
|
ad28aed309 | ||
|
|
b5d462b3cd | ||
|
|
35e50b75fe | ||
|
|
018a0aff4f | ||
|
|
0ffacb3a83 | ||
|
|
46f82d201d | ||
|
|
c77d2af537 | ||
|
|
e662730fbc | ||
|
|
dc19b2d002 | ||
|
|
bb68ba78b6 | ||
|
|
4b375932f4 | ||
|
|
7d6ba934c6 | ||
|
|
aa4de6f155 | ||
|
|
7b62cd5e13 | ||
|
|
86e414d192 | ||
|
|
69bf1ca27b | ||
|
|
9ca05c1665 | ||
|
|
6fd0638d14 | ||
|
|
c62fb2227e | ||
|
|
29774c4f46 | ||
|
|
437c15a280 | ||
|
|
194cebdfe0 | ||
|
|
d7e1de65b6 | ||
|
|
38b336357f | ||
|
|
7f7272e09b | ||
|
|
8458a337bd | ||
|
|
d8e0524613 | ||
|
|
72e10d3b41 | ||
|
|
80e685cc33 | ||
|
|
d87d15982f | ||
|
|
b8b5515a7d | ||
|
|
e4afa7cfa5 | ||
|
|
42c3b93886 | ||
|
|
da3e9b9865 | ||
|
|
c6faf005b5 | ||
|
|
5374e66a80 | ||
|
|
3efa44df6d | ||
|
|
f1c2470a6e | ||
|
|
2e9f07bab4 | ||
|
|
39159b9b1e | ||
|
|
1be0c4b32f | ||
|
|
a9f38fb372 | ||
|
|
2a15ecb3e3 | ||
|
|
770fdee881 | ||
|
|
205f3641e3 | ||
|
|
56e5c98a84 | ||
|
|
73766ca471 | ||
|
|
0979c0664c | ||
|
|
bcbd9d3c5a | ||
|
|
69000f205a | ||
|
|
d8e280b9ee | ||
|
|
ce237039c9 | ||
|
|
6a3d730f0b | ||
|
|
c1cd894516 | ||
|
|
2d04335648 | ||
|
|
1c8207eeb5 | ||
|
|
fa9855fbf3 | ||
|
|
d03c780180 | ||
|
|
c903eb1e79 | ||
|
|
35d258c0ba | ||
|
|
720e2a9f60 | ||
|
|
b8b07a0310 | ||
|
|
f2bbcef7f0 | ||
|
|
2df076f834 | ||
|
|
7e119ee8f0 | ||
|
|
58f703e0a7 | ||
|
|
2172719f3e | ||
|
|
49008eb79c | ||
|
|
454181456d | ||
|
|
e07dab2310 | ||
|
|
6e97f3d4d3 | ||
|
|
a89416e9a5 | ||
|
|
5b275629be | ||
|
|
4a45616b16 | ||
|
|
ae529f64cd | ||
|
|
d337e2204a | ||
|
|
7110fef6f8 | ||
|
|
ec286e98d2 | ||
|
|
66b1a2b301 | ||
|
|
2a3ecf7270 | ||
|
|
3403c56394 | ||
|
|
2b368dc8f0 | ||
|
|
6d97335d68 | ||
|
|
3042d6180f | ||
|
|
12646d4e98 | ||
|
|
972d13e2cc | ||
|
|
86e88a3d43 | ||
|
|
f0fb7b7418 | ||
|
|
ba5464cc24 | ||
|
|
ec20c02784 | ||
|
|
5bb3fe0550 | ||
|
|
b4610ab9be | ||
|
|
7c231b08d7 | ||
|
|
7709ad1002 | ||
|
|
948b1501bf | ||
|
|
5dd3b98fec | ||
|
|
f0e32cfd5f | ||
|
|
806df2c586 | ||
|
|
3bed1f9235 | ||
|
|
07248fb868 | ||
|
|
8f948d4c41 | ||
|
|
cfb8e0aa4f | ||
|
|
fa9de47729 | ||
|
|
fe84ed91ac | ||
|
|
b5e0c9d890 | ||
|
|
5d9deb3028 | ||
|
|
600ddccff4 | ||
|
|
e3e7981d8a | ||
|
|
bf1feb204b | ||
|
|
2f4d52e53a | ||
|
|
2c9b897430 | ||
|
|
e1486959bb | ||
|
|
74dac301d3 | ||
|
|
2011acf1e2 | ||
|
|
50c46f9924 | ||
|
|
ecfc76af59 | ||
|
|
b2a94b482e | ||
|
|
425393b4a2 | ||
|
|
ac196bec58 | ||
|
|
4e3dfaad55 | ||
|
|
d5b2773436 | ||
|
|
695c8423f5 | ||
|
|
c7c53cafa2 | ||
|
|
6c29a01a69 | ||
|
|
7a72df1c2f | ||
|
|
5f87debecb | ||
|
|
57c61a1ddd | ||
|
|
08abcd5eff | ||
|
|
bb47cf875d | ||
|
|
0a36915903 | ||
|
|
df1e2913c0 | ||
|
|
ecf4df2d11 | ||
|
|
54595ab174 | ||
|
|
73742495b0 | ||
|
|
b93e25ddb1 | ||
|
|
5c5da8a670 | ||
|
|
3a7ce6bb5f | ||
|
|
ae6bd8ec73 | ||
|
|
c7f957775e | ||
|
|
9ea4c367bd | ||
|
|
55c0153692 | ||
|
|
67007e75ff | ||
|
|
57c6a4a2a4 | ||
|
|
663a7d9a3f | ||
|
|
0abf7b492a | ||
|
|
6cf77f9b94 | ||
|
|
2c1fe9f308 | ||
|
|
efd3cd4f27 | ||
|
|
3429474fc5 | ||
|
|
c6ce5f421b | ||
|
|
7c307e8baf | ||
|
|
9d447741f3 | ||
|
|
e9079df63c | ||
|
|
48fb6710e1 | ||
|
|
ae72a43a43 | ||
|
|
49935db529 | ||
|
|
cd4d3dece2 | ||
|
|
0ca0f4a6cf | ||
|
|
c2a0988368 | ||
|
|
ab123a1428 | ||
|
|
c1a5042fb9 | ||
|
|
9d331ffda8 | ||
|
|
41992506f1 | ||
|
|
761f232647 | ||
|
|
c09b25d3c5 | ||
|
|
4061d5e88e | ||
|
|
0983e4e288 | ||
|
|
247c3eefb9 | ||
|
|
28e2072f9b | ||
|
|
7adecaea60 | ||
|
|
2616f87185 | ||
|
|
d81c696742 | ||
|
|
678cc8f7ba | ||
|
|
35176d6eff | ||
|
|
42130e116d | ||
|
|
1040d95eec | ||
|
|
480876558a | ||
|
|
ce04a49bae | ||
|
|
3695b6f3fe | ||
|
|
7e50da7816 | ||
|
|
17a938f8cf | ||
|
|
3d2efcedeb | ||
|
|
c7b7b97022 | ||
|
|
0c8be62831 | ||
|
|
9b28f60be0 | ||
|
|
9b6a43cec7 | ||
|
|
384c752c33 | ||
|
|
e844cfa0bc | ||
|
|
7174061b2a | ||
|
|
cf03c01b75 | ||
|
|
8b83b2c691 | ||
|
|
d445c72edb | ||
|
|
9a8aacad40 | ||
|
|
416131aa8f | ||
|
|
8b1d2bbed8 | ||
|
|
9a7a31c47b | ||
|
|
62e62790c1 | ||
|
|
3b2f675264 | ||
|
|
9706ee8ec2 | ||
|
|
c9a4b273d6 | ||
|
|
8fe02f8c26 | ||
|
|
18f91af77f | ||
|
|
1c047079f0 | ||
|
|
a8273ad7a9 | ||
|
|
133b2601a2 | ||
|
|
21d4577079 | ||
|
|
c19003cf39 | ||
|
|
d99da92b43 | ||
|
|
8b9c4592f1 | ||
|
|
832feb7df1 | ||
|
|
506e8d0eac | ||
|
|
7cd346bde0 | ||
|
|
47f5da1798 | ||
|
|
ff4939fb29 | ||
|
|
3fa1d34c32 | ||
|
|
2ef4d95bb5 | ||
|
|
ce715ed5aa | ||
|
|
95dab219b8 | ||
|
|
2fcea0624d | ||
|
|
7f8aad0686 | ||
|
|
2eae7a0899 | ||
|
|
6bb9c7fbc9 | ||
|
|
9d9681345d | ||
|
|
201aefca05 | ||
|
|
41f0e00cfb | ||
|
|
9be428aad3 | ||
|
|
b2d23a8559 | ||
|
|
d6ad2ff836 | ||
|
|
6ac8f822ff | ||
|
|
b0c8eff6c7 | ||
|
|
ae7a468310 | ||
|
|
84a993b948 | ||
|
|
2a5ebcf73d | ||
|
|
09978cf8e5 | ||
|
|
0e981228e2 | ||
|
|
b77ebd2a02 | ||
|
|
2e414b0867 | ||
|
|
00fec78b16 | ||
|
|
e774fcd824 | ||
|
|
043cdec141 | ||
|
|
11b93638d6 | ||
|
|
713c2a9dbb | ||
|
|
553ed4de07 | ||
|
|
83cf82294c | ||
|
|
502077170d | ||
|
|
16a836b59c | ||
|
|
68374109f4 | ||
|
|
8a3b6b57f8 | ||
|
|
376069d124 | ||
|
|
1d563a8978 | ||
|
|
5215754ffa | ||
|
|
c0b0a02ce6 | ||
|
|
dfb363e768 | ||
|
|
e82fc964d6 | ||
|
|
3f029b914c | ||
|
|
96e8857a77 | ||
|
|
442b5f0256 | ||
|
|
90cfb06e74 | ||
|
|
992c042328 | ||
|
|
91ca97bcd4 | ||
|
|
d3624edf9f | ||
|
|
aa942362e9 | ||
|
|
f2ec12aff6 | ||
|
|
c392b84f3d | ||
|
|
79d3a924f0 | ||
|
|
b33d1060ef | ||
|
|
6c90865542 | ||
|
|
956619237e | ||
|
|
66dfc26bc9 | ||
|
|
7709255fb3 | ||
|
|
a3fdaee7ad | ||
|
|
e84bd1a8cd | ||
|
|
8ff4cfde4f | ||
|
|
b2a6111a65 | ||
|
|
487884bcaf | ||
|
|
07edfac400 | ||
|
|
5c05347690 | ||
|
|
f9fe3d833d | ||
|
|
7a4d027f12 | ||
|
|
5a14848cf8 | ||
|
|
0715f51b02 | ||
|
|
e727afdd81 | ||
|
|
6c21aa1087 | ||
|
|
21c68a1138 | ||
|
|
8bee3b51cb | ||
|
|
b4b49eaad2 | ||
|
|
f8515fb191 | ||
|
|
a2a6feea52 | ||
|
|
b9d417ec1d | ||
|
|
6076332b8b | ||
|
|
fb08dd0c71 | ||
|
|
064a8879e9 | ||
|
|
eb731bbaad | ||
|
|
852c153d24 | ||
|
|
2b345e9f76 | ||
|
|
e8e30611fb | ||
|
|
6b71faa6e2 | ||
|
|
dd32f14d3a | ||
|
|
5a75548652 | ||
|
|
81ba8f80d5 | ||
|
|
ee0d4fc470 | ||
|
|
f7e9b66988 | ||
|
|
91e02f0719 | ||
|
|
4fd0a265f4 | ||
|
|
5a2642ba81 | ||
|
|
4870d2a05f | ||
|
|
6b7de99f78 | ||
|
|
901560467f | ||
|
|
707c164b1c | ||
|
|
5f00bae2cf | ||
|
|
cef3e6a805 | ||
|
|
5e978e315e | ||
|
|
5e7d29f620 | ||
|
|
ef3aed64e3 | ||
|
|
7bda58bbf6 | ||
|
|
b8d1681aae | ||
|
|
a29ce24280 | ||
|
|
27f1092251 | ||
|
|
f89c5e8f7d | ||
|
|
5965d5f23f | ||
|
|
8d35fbcba3 | ||
|
|
f5866a2376 | ||
|
|
c571b06293 | ||
|
|
b9ce903991 | ||
|
|
14bf0dc32f | ||
|
|
238b4df55f | ||
|
|
3e01c87f64 | ||
|
|
f816c9a175 | ||
|
|
1714b13583 | ||
|
|
fb55c57536 | ||
|
|
99e509f553 | ||
|
|
09b8a28e39 | ||
|
|
e0766dc678 | ||
|
|
eed307f3d5 | ||
|
|
6ab5c2aa46 | ||
|
|
9113f44789 | ||
|
|
45643c59a8 | ||
|
|
6d6b19f894 | ||
|
|
ea4830ac30 | ||
|
|
cd3cff733b | ||
|
|
a441a6bd6a | ||
|
|
e24ea96436 | ||
|
|
496a0373ff | ||
|
|
8f8b9ada47 | ||
|
|
efd3ff3132 | ||
|
|
881f860504 | ||
|
|
596d2b93bb | ||
|
|
16ce8b105c | ||
|
|
abc6f7d9b1 | ||
|
|
a59582c067 | ||
|
|
afc9face71 | ||
|
|
7bbff355f3 | ||
|
|
8707c9e4be | ||
|
|
a92651e048 | ||
|
|
c8cc9b1bf1 | ||
|
|
8b39eb92d6 | ||
|
|
5a02b9240a | ||
|
|
d211d89ab8 | ||
|
|
5c3a49d2ca | ||
|
|
1eeda047c2 | ||
|
|
55dcdd4c87 | ||
|
|
366d240cc4 | ||
|
|
e19db5cab7 | ||
|
|
74eecfda02 | ||
|
|
d093bd44bc | ||
|
|
2e7c5cb433 | ||
|
|
41ee40d427 | ||
|
|
5bf59a9d2f | ||
|
|
ee24982c72 | ||
|
|
755a743e93 | ||
|
|
35be37d256 | ||
|
|
9c0d0a8566 | ||
|
|
0294bb6fc0 | ||
|
|
dee1c8b1e8 | ||
|
|
e22b4047ed | ||
|
|
691f5eec7c | ||
|
|
7cf2a077ed | ||
|
|
b40c1051af | ||
|
|
7a274d3a83 | ||
|
|
634975e505 | ||
|
|
88f5d04b8c | ||
|
|
59408bbebd | ||
|
|
bc97ade133 | ||
|
|
eb6b58eb36 | ||
|
|
520b2c9799 | ||
|
|
d5ecd567d0 | ||
|
|
f88b359bb5 | ||
|
|
67ada08e05 | ||
|
|
6ce04eead6 | ||
|
|
b8e3d8193c | ||
|
|
9c09b5ad78 | ||
|
|
a6caa2b069 | ||
|
|
0f231048cf | ||
|
|
8a3d00e8c6 | ||
|
|
bfd175224a | ||
|
|
0df9c1d707 | ||
|
|
ccfa148014 | ||
|
|
dda4943425 | ||
|
|
be87c3864c | ||
|
|
982d6476d7 | ||
|
|
92c0b895dc | ||
|
|
75a8444b41 | ||
|
|
d347cda9e3 | ||
|
|
f5adbf9cf7 | ||
|
|
19059fbf81 | ||
|
|
1694e3ad07 | ||
|
|
e46613b82b | ||
|
|
38ea51627d | ||
|
|
e5fa40b826 | ||
|
|
ff4a78f964 | ||
|
|
9973d8d4e6 | ||
|
|
e7e33c479d | ||
|
|
69b9da6da6 | ||
|
|
71ef8325a0 | ||
|
|
582d1d25d6 | ||
|
|
ebf4545d46 | ||
|
|
3c483ba0e6 | ||
|
|
71629763ee | ||
|
|
417e37f1d1 | ||
|
|
a3e3192b66 | ||
|
|
86c503c1f9 | ||
|
|
21a47b4a77 | ||
|
|
64d961bd98 | ||
|
|
1b218f5250 | ||
|
|
dfb9f98b7a | ||
|
|
6a033cd2db | ||
|
|
b872b6148c | ||
|
|
e7d74a7880 | ||
|
|
8cb5d0fd88 | ||
|
|
cebcd17db0 | ||
|
|
4276c1c3ac | ||
|
|
fa3cf3f7f3 | ||
|
|
8d761751e7 | ||
|
|
eabab20453 | ||
|
|
fdd6a64c72 | ||
|
|
2db83d0810 | ||
|
|
cdb303d98c | ||
|
|
fe2a35b09e | ||
|
|
816145ea0f | ||
|
|
5d3661995c | ||
|
|
c06b673e62 | ||
|
|
45e7416267 | ||
|
|
743b259fb5 | ||
|
|
154e6f68be | ||
|
|
053404afee | ||
|
|
459eae1334 | ||
|
|
22b366d6f9 | ||
|
|
9540a4e9dc | ||
|
|
de1cad448b | ||
|
|
1133b8ee6f | ||
|
|
ae4dbefe83 | ||
|
|
2f1ffeffcb | ||
|
|
a3a89ef483 | ||
|
|
6c533b7055 | ||
|
|
d54467cbfe | ||
|
|
359103b0d9 | ||
|
|
2302594c5c | ||
|
|
e226804947 | ||
|
|
165697f678 | ||
|
|
a6a8146975 | ||
|
|
ec03ffc7a9 | ||
|
|
beeecb1845 | ||
|
|
f7a54593ac | ||
|
|
6209b0baa5 | ||
|
|
8739605572 | ||
|
|
f18a309469 | ||
|
|
aa1d54f4c9 | ||
|
|
f816be5bbe | ||
|
|
101423c977 | ||
|
|
e66c81d896 | ||
|
|
e399d4c1fe | ||
|
|
63c5c5e6b1 | ||
|
|
d39fa1aa9d | ||
|
|
52aae9ed96 | ||
|
|
f72b7d3361 |
17
.codacy.yaml
Normal file
17
.codacy.yaml
Normal 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"
|
||||
@@ -1,2 +1,2 @@
|
||||
HOTSPOT_THRESHOLD=10.0
|
||||
AVERAGE_THRESHOLD=9.92
|
||||
AVERAGE_THRESHOLD=9.95
|
||||
|
||||
115
.github/scripts/configure-windows-authenticode.ps1
vendored
Normal file
115
.github/scripts/configure-windows-authenticode.ps1
vendored
Normal 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."
|
||||
117
.github/workflows/ci.yml
vendored
117
.github/workflows/ci.yml
vendored
@@ -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,38 +38,25 @@ 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
|
||||
|
||||
- name: Docs build check
|
||||
run: pnpm docs:build
|
||||
|
||||
# ── 1. Coverage-backed tests ──────────────────────────────────────────
|
||||
# The coverage commands run the same frontend and Rust test suites, so keep
|
||||
# them as the canonical test lane instead of running every suite twice.
|
||||
# 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
|
||||
|
||||
@@ -76,25 +65,15 @@ jobs:
|
||||
run: pnpm test:coverage
|
||||
# Thresholds configured in vite.config.ts — exits non-zero if coverage drops
|
||||
|
||||
- name: Rust tests + coverage (≥85% lines)
|
||||
run: |
|
||||
cargo llvm-cov \
|
||||
--manifest-path src-tauri/Cargo.toml \
|
||||
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
|
||||
--lcov \
|
||||
--output-path coverage/rust.lcov \
|
||||
--fail-under-lines 85
|
||||
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
|
||||
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
- 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.
|
||||
|
||||
@@ -158,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
|
||||
|
||||
@@ -166,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:
|
||||
@@ -180,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
|
||||
|
||||
@@ -197,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
104
.github/workflows/deploy-docs.yml
vendored
Normal 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
|
||||
567
.github/workflows/release-build-artifacts.yml
vendored
Normal file
567
.github/workflows/release-build-artifacts.yml
vendored
Normal 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
|
||||
554
.github/workflows/release-stable.yml
vendored
554
.github/workflows/release-stable.yml
vendored
@@ -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,464 +56,30 @@ 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
|
||||
- arch: x86_64
|
||||
target: x86_64-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 }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.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 ${{ 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 \
|
||||
rpm
|
||||
|
||||
- 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 }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.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
|
||||
installers=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||
)
|
||||
signatures=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||
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, 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)
|
||||
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: 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 = "${{ 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 }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.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-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
|
||||
require_windows_authenticode: true
|
||||
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:
|
||||
@@ -565,16 +138,23 @@ jobs:
|
||||
|
||||
- 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\`**"
|
||||
@@ -582,7 +162,7 @@ jobs:
|
||||
echo "**Includes macOS (Apple Silicon and Intel), Windows x64, and Linux x64 bundles**"
|
||||
echo ""
|
||||
echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*"
|
||||
} > release_notes.md
|
||||
} >> release_notes.md
|
||||
|
||||
- name: Build stable-latest.json
|
||||
run: |
|
||||
@@ -661,7 +241,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 }}
|
||||
@@ -704,46 +284,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 }}."
|
||||
|
||||
541
.github/workflows/release.yml
vendored
541
.github/workflows/release.yml
vendored
@@ -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,460 +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
|
||||
- arch: x86_64
|
||||
target: x86_64-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 }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
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 \
|
||||
rpm
|
||||
|
||||
- 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 }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.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
|
||||
installers=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||
)
|
||||
signatures=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||
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, 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)
|
||||
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: 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 = "${{ 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 }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.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-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:
|
||||
@@ -714,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 }}
|
||||
@@ -755,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
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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__/
|
||||
@@ -73,3 +76,6 @@ CODE-HEALTH-REPORT.md
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Local Codacy CLI runtime/config generated by the MCP server
|
||||
.codacy/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
10
.semgrepignore
Normal 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
|
||||
40
AGENTS.md
40
AGENTS.md
@@ -45,15 +45,21 @@ 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.
|
||||
|
||||
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 ❌).
|
||||
|
||||
After both phases pass, add a **completion comment** to the Todoist task. The comment must include:
|
||||
- What was implemented (a few lines covering logic and UX/UI)
|
||||
- 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
|
||||
- Codacy: MCP/CLI scan summary; confirm no new Critical/High findings
|
||||
- Localization: `pnpm l10n:translate` + `pnpm l10n:validate` result, or "no UI copy changes"
|
||||
- PostHog: event name(s) added, or why no event was needed
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
@@ -91,6 +97,8 @@ When adding or changing a meaningful user-facing feature, include the event name
|
||||
|
||||
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`.
|
||||
@@ -103,12 +111,40 @@ Pre-commit and pre-push hooks enforce **Hotspot Code Health** and **Average Code
|
||||
|
||||
**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.
|
||||
|
||||
### Release-readiness checklist
|
||||
|
||||
Before pushing or moving a task to In Review, verify and mention the result in the Todoist completion comment:
|
||||
|
||||
- CodeScene before/after checked for every touched/new scorable file; final Hotspot and Average pass `.codescene-thresholds`.
|
||||
- Coverage commands passed (`pnpm test:coverage` and `cargo llvm-cov ... --fail-under-lines 85`) or the change is docs-only.
|
||||
- Codacy checked via MCP or `.codacy/cli.sh`; all new Critical/High issues fixed.
|
||||
- Localization checked: 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 checked: meaningful new user actions/events are instrumented with safe metadata; noisy/minor changes explicitly say “PostHog: no event needed because …”.
|
||||
- Docs/ADRs checked under the rules below.
|
||||
- Demo vault dirt checked: `git status --short -- demo-vault demo-vault-v2` is empty unless fixture changes are intentional.
|
||||
|
||||
### ADRs & docs
|
||||
|
||||
ADRs live in `docs/adr/`. Create in the same commit as the code. Never edit existing — create a new one that supersedes. Use `/create-adr`. **When:** new dependency, storage strategy, platform target, core abstraction, cross-cutting pattern. **Not for:** bug fixes, styling, refactors.
|
||||
|
||||
@@ -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
8
GEMINI.md
Normal 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`.
|
||||
@@ -1,4 +1,4 @@
|
||||
 [](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [](https://github.com/refactoringhq/tolaria/actions/workflows/release.yml) [](https://codecov.io/gh/refactoringhq/tolaria) [](https://codescene.io/projects/76865)
|
||||
 [](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [](https://codecov.io/gh/refactoringhq/tolaria) [](https://codescene.io/projects/76865)
|
||||
|
||||
# 💧 Tolaria
|
||||
|
||||
@@ -43,12 +43,14 @@ brew install --cask tolaria
|
||||
|
||||
### Download from releases
|
||||
|
||||
Download the [latest release here](https://refactoringhq.github.io/tolaria/download/) for macOS, Windows, or Linux.
|
||||
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
|
||||
|
||||
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 👇
|
||||
|
||||
@@ -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
38
biome.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,5 +17,5 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
"iconLibrary": "phosphor"
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -67,7 +67,7 @@ Git is a per-vault capability, not a prerequisite for the document model. A vaul
|
||||
| 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. Features that depend on Git must check this capability instead of assuming every vault has `.git`.
|
||||
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.
|
||||
|
||||
@@ -94,9 +94,10 @@ classDiagram
|
||||
+Number wordCount
|
||||
+String? snippet
|
||||
+Boolean archived
|
||||
+WorkspaceIdentity? workspace
|
||||
+Boolean trashed ⚠ legacy
|
||||
+Number? trashedAt ⚠ legacy
|
||||
+Record~string,string~ properties
|
||||
+Record~string,VaultPropertyValue~ properties
|
||||
}
|
||||
|
||||
class TypeDocument {
|
||||
@@ -144,14 +145,48 @@ interface VaultEntry {
|
||||
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.
|
||||
|
||||
`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:
|
||||
@@ -160,16 +195,20 @@ interface VaultEntry {
|
||||
|---|---|---|
|
||||
| `markdown` or absent | `.md`, `.markdown` | Full Tolaria note model: frontmatter, BlockNote, raw editor, relationships, title sync |
|
||||
| `text` | UTF-8 editable formats such as `.yml`, `.json`, `.ts`, `.py`, `.sh` | Opens through the raw editor without Markdown note semantics |
|
||||
| `binary` | Images, PDFs, archives, other non-text files | Stays a normal vault file; previewable images and PDFs open in `FilePreview`, unsupported or broken binaries show an explicit fallback |
|
||||
| `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>` and supported PDFs render through the webview's PDF object renderer, both backed by Tauri asset URLs. Runtime asset access is accumulated only for vault roots Tolaria has loaded in the current app session, because Tauri directory forbids cannot be safely reversed after a vault switch. The "open in default app" action re-enters the active-vault command boundary through `open_vault_file_external` before delegating to the native opener. This keeps the filesystem as source of truth and avoids converting assets into proprietary objects.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
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.
|
||||
@@ -200,6 +239,7 @@ Each entity type can have a corresponding **type document**: any markdown note w
|
||||
|
||||
- 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
|
||||
|
||||
@@ -212,12 +252,14 @@ Each entity type can have a corresponding **type document**: any markdown note w
|
||||
| `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 `[[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
|
||||
- Viewing a type document in entity view shows an "Instances" group listing all entries of that type
|
||||
@@ -248,6 +290,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:
|
||||
@@ -278,8 +322,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 the editor keeps the unsaved buffer intact for another attempt.
|
||||
- **Recoverable save failures** (`useEditorSave`, `vault/file.rs`): invalid platform path syntax is reported as a clear retryable save error, while 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)
|
||||
@@ -289,6 +334,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
|
||||
@@ -308,7 +354,7 @@ type SidebarSelection =
|
||||
|
||||
`SidebarSelection.kind === 'folder'` is a first-class navigation target, not just a visual highlight.
|
||||
|
||||
- `FolderTree` keeps the folder interaction surface decomposed into `FolderTreeRow`, `FolderNameInput`, `FolderContextMenu`, and disclosure/context-menu hooks so nested row rendering, inline rename, and right-click actions stay isolated. The UI wraps backend folder nodes in a synthetic vault-root row with `path: ""` and `rootPath` set to the opened vault so root-level files can be listed without turning the vault root into a mutable folder. Non-mutating reveal/copy-path menu items stay callback-driven from `App` so filesystem convenience actions do not leak into folder mutation hooks.
|
||||
- `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.
|
||||
@@ -317,7 +363,9 @@ type SidebarSelection =
|
||||
|
||||
### Saved Views
|
||||
|
||||
Saved Views live as YAML files under `views/`. Their definition includes user-visible fields (`name`, `icon`, `color`), note-list preferences (`sort`, `listPropertiesDisplay`), filters, and an optional top-level `order` number. The `order` value is stored directly in the YAML document, not in Markdown frontmatter, and lower values render earlier in every saved-View list. Views without an explicit order sort after ordered views by filename for stable fallback behavior.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -336,9 +384,11 @@ The renderer uses `viewOrdering` helpers to convert drag or command-palette move
|
||||
|
||||
## Command Surface
|
||||
|
||||
`src/shared/appCommandManifest.json` is the cross-runtime source for stable app command IDs, menu structure, display labels, accelerators, deterministic shortcut QA metadata, and native menu enablement groups. The renderer imports it through `src/hooks/appCommandCatalog.ts`, which derives `APP_COMMAND_IDS`, shortcut lookup maps, Linux titlebar menu sections, native-menu command membership, and test helpers. Tauri includes the same JSON in `src-tauri/src/menu.rs` and uses it to build custom menu items, emit overridden menu item IDs such as the quick-open alias as their primary command IDs, and toggle state-dependent menu items from manifest groups.
|
||||
`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 Linux fallback menu; OS-native menu items such as Undo, Copy/Paste, Services, Quit, and Window controls remain local to the native menu implementation.
|
||||
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
|
||||
|
||||
@@ -369,9 +419,11 @@ A `vault_health_check` command detects stray files in non-protected subfolders a
|
||||
|
||||
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 that vault's canonical path, or stops it when there is no selected vault. App exit uses the same child cleanup path and waits for the bridge process after killing it. MCP Node entrypoints require `VAULT_PATH` and fail clearly instead of falling back to `~/Laputa`. Manual MCP config export uses the same generated stdio entry as registration, so the copied snippet remains scoped to the active vault without writing third-party config files. Desktop snippet copy goes through the native `copy_text_to_clipboard` command, while browser/mock mode keeps using the Web Clipboard API. External-client stdio MCP processes also exit when stdin closes; their UI-bridge reconnect timers and WebSocket are canceled during shutdown so disconnected clients do not leave extra Node processes behind.
|
||||
The local MCP WebSocket bridge follows the same active-vault boundary. `useVaultSwitcher` calls `sync_mcp_bridge_vault` after the persisted selection loads and after each vault switch; the desktop command starts/restarts the bridge with 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. 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
|
||||
|
||||
@@ -456,33 +508,37 @@ 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 |
|
||||
| `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
|
||||
- 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, so unrelated updates do not remount the editor
|
||||
- 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)
|
||||
|
||||
### External Vault Refresh
|
||||
|
||||
External vault mutations are any disk writes Tolaria did not just perform through its own save path: Git pulls, AI-agent writes, filesystem watcher events, and edits from another app. These changes must route through `refreshPulledVaultState()` rather than calling `reloadVault()` in isolation. The shared refresh abstraction reloads entries, folders, and saved views together, preserves unsaved active-editor content, reopens a clean active note only when the changed-path list includes that note, and closes the active tab if the file disappeared. Unknown or unrelated watcher updates refresh vault-derived state without remounting the active editor. `useVaultWatcher` supplies changed filesystem paths to this abstraction after debouncing and after filtering recent app-owned saves.
|
||||
External vault mutations are any disk writes Tolaria did not just perform through its own save path: Git pulls, AI-agent writes, filesystem watcher events, and edits from another app. These changes must route through `refreshPulledVaultState()` rather than calling `reloadVault()` in isolation. The shared refresh abstraction reloads entries, folders, and saved views together, preserves unsaved active-editor content, reopens a clean active note only when the changed-path list includes that note, and closes the active tab if the file disappeared. Unknown or unrelated watcher updates refresh vault-derived state without remounting the active editor. `useVaultWatcher` supplies changed filesystem paths to this abstraction after debouncing and after filtering recent app-owned saves. Overlapping entry reloads and modified-file polls are coalesced with a single trailing rerun so watcher and sync bursts do not stack native vault scans or Git status processes.
|
||||
|
||||
`useGitRemoteStatus` is the commit-time companion to `useAutoSync`:
|
||||
- Re-checks `git_remote_status` when the Commit dialog opens and right before submit
|
||||
`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
|
||||
@@ -504,7 +560,7 @@ External vault mutations are any disk writes Tolaria did not just perform throug
|
||||
- **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
|
||||
@@ -546,21 +602,24 @@ Defined in `src/utils/mathMarkdown.ts`, `src/components/editorSchema.tsx`, and s
|
||||
|
||||
### Mermaid Diagrams
|
||||
|
||||
Defined in `src/utils/mermaidMarkdown.ts`, `src/components/MermaidDiagram.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`:
|
||||
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.
|
||||
- `serializeMermaidAwareBlocks()` wraps the math-aware serializer so math, wikilinks, and diagrams share the same Markdown-first save path.
|
||||
- `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/tldrawMarkdown.ts`, `src/components/TldrawWhiteboard.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`:
|
||||
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.
|
||||
- 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
|
||||
@@ -568,13 +627,16 @@ Defined in `src/utils/tldrawMarkdown.ts`, `src/components/TldrawWhiteboard.tsx`,
|
||||
Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tolariaEditorFormattingConfig.ts`:
|
||||
|
||||
- `SingleEditorView` disables BlockNote's default formatting toolbar, `/` menu, and side menu, then mounts Tolaria-owned controllers so the visible formatting surface matches Tolaria's markdown round-trip guarantees.
|
||||
- `SingleEditorView` owns a whitespace mouse-selection bridge around BlockNote and its rich-editor scroll area: drag starts that land outside the editable text DOM are remapped through the ProseMirror view with clamped coordinates, while drags below the rendered document fall back to the document end. Drags that begin inside BlockNote's contenteditable surface, toolbars, side menu, dialogs, or non-primary mouse buttons stay on BlockNote/native handling.
|
||||
- The formatting toolbar only exposes inline controls that persist through `blocksToMarkdownLossy()` in Tolaria's save pipeline: bold, italic, strike, nesting, and link creation. Controls that BlockNote can render temporarily but Tolaria cannot faithfully persist, such as underline, color, alignment, and the block-type dropdown, are hidden instead of appearing to work and later disappearing.
|
||||
- Tolaria's formatting-toolbar controller also keeps file/image actions mounted across the tiny hover gap between an image block and the floating toolbar, and while the toolbar itself is hovered, so image controls remain usable instead of collapsing mid-interaction.
|
||||
- `useEditorComposing` tracks `compositionstart` and `compositionend` events that target the BlockNote editor and closes the floating formatting toolbar while IME text is being composed, keeping CJK candidate windows unobstructed without changing normal selection toolbar behavior.
|
||||
- `useEditorComposing` tracks editor-owned IME composition events and closes the floating formatting toolbar during composition plus a short post-composition settle window, keeping CJK candidate windows unobstructed without changing normal selection toolbar behavior.
|
||||
- `createImeCompositionKeyGuardExtension()` intercepts composing `Enter` keydown events before BlockNote's list shortcuts see them, so Korean/Japanese/Chinese IMEs can commit text at the start of list items without Tolaria splitting the current bullet. It stops editor shortcut propagation only; it does not prevent the browser/IME default composition action.
|
||||
- `useImageLightbox` listens for `dblclick` on the rich-editor container and opens `ImageLightbox` only when the event target resolves to a viewable BlockNote image. The target resolver handles media wrappers, ignores image captions/resize controls, missing sources, and tiny tracking-style images, preserving BlockNote's ordinary single-click image selection path.
|
||||
- The `/` slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, list blocks, Mermaid diagrams, and whiteboards. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model.
|
||||
- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface. Tolaria renders the add-block button outside the drag handle so the handle stays next to the block content. The side menu aligns itself to the first rendered text line for the hovered block, so H1/H2 typography, line-height, wrapping, and theme changes do not need per-heading offsets. Block reordering uses a Tolaria-owned pointer gesture and direct BlockNote block moves instead of HTML5 `DataTransfer`, keeping it independent from Tauri's native file-drop system. Block-handle actions re-resolve the current live BlockNote block before mutating or dragging, so note reloads and sync churn cannot leave controls acting on stale block references.
|
||||
- BlockNote's table row/column handles are patched so stale or missing hovered-table state cancels the drag and hides handles instead of throwing. Add/remove row and column actions also validate the table position and cell indexes before resolving a ProseMirror `CellSelection`, so reloads or menu lag cannot turn stale handles into invalid table-selection positions. Checklist checkbox handlers also re-resolve the live block before updating `checked`, making delayed clicks after note reloads a no-op instead of a stale block mutation. Browser and native table regressions should exercise row and column dragging plus add-menu actions because the state is tracked per orientation.
|
||||
- `SingleEditorView` wraps the BlockNote surface in a narrow render-recovery boundary for BlockNote's transient `Block doesn't have id` node-view failure. The boundary retries the BlockNote view once, records `editor_render_recovered`, and marks the recovered error so the React root handler does not send that handled case back to Sentry. Other render errors still propagate through the normal root error path.
|
||||
- `useNoteWikilinkDrop()` is the shared editor-drop abstraction for dragging note rows into either editor mode. It reads the existing note-retargeting drag payload, resolves the vault-relative stem, and inserts a canonical `[[wikilink]]` without hijacking unrelated plain-text drags.
|
||||
- `plainTextPaste.ts` is the shared plain-text paste target registry. Rich BlockNote and raw CodeMirror surfaces register focused insertion targets, while ordinary focused text controls use DOM selection replacement, so the `Cmd+Shift+V` command can preserve caret/selection behavior without each surface inventing its own clipboard reader.
|
||||
- `useTauriDragDropEvent()` owns the shared Tauri window drag/drop subscription and duplicate-unlisten cleanup used by native drop features.
|
||||
@@ -585,26 +647,25 @@ Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tola
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["📄 Raw markdown\n(from disk)"] --> B["splitFrontmatter()\n→ yaml + body"]
|
||||
B --> C["preProcessTldrawMarkdown(body)\ntldraw fence → token"]
|
||||
C --> D["preProcessMermaidMarkdown(body)\nmermaid fence → token"]
|
||||
D --> E["preProcessWikilinks(body)\n[[target]] → ‹token›"]
|
||||
E --> F["preProcessMathMarkdown(body)\n$...$ / $$...$$ → tokens"]
|
||||
F --> G["tryParseMarkdownToBlocks()\n→ BlockNote block tree"]
|
||||
G --> H["injectWikilinks + injectMathInBlocks + injectMermaidInBlocks + injectTldrawInBlocks\n tokens → schema nodes"]
|
||||
H --> I["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 I fill:#d4edda,stroke:#28a745,color:#000
|
||||
style H fill:#d4edda,stroke:#28a745,color:#000
|
||||
```
|
||||
|
||||
> Wikilink placeholder tokens use `\u2039` and `\u203A`; math, Mermaid, and tldraw placeholder tokens use ASCII sentinels with URI-encoded payloads.
|
||||
> Wikilink placeholder tokens use `\u2039` and `\u203A`; math, 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 + serializeMermaidAwareBlocks()\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"]
|
||||
|
||||
@@ -612,7 +673,7 @@ flowchart LR
|
||||
style E fill:#d4edda,stroke:#28a745,color:#000
|
||||
```
|
||||
|
||||
Rich-editor change events are coalesced before this serialization runs. `useEditorTabSwap` keeps the latest BlockNote state in the editor, schedules one Markdown serialization for a short idle window, and exposes an explicit flush hook for save, note switch, raw-mode entry, and destructive note actions. This keeps long notes from paying full-document Markdown serialization on every keystroke while preserving the disk-first save path.
|
||||
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.
|
||||
|
||||
@@ -623,11 +684,12 @@ 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.
|
||||
@@ -649,12 +711,12 @@ Typed ASCII arrow sequences are normalized consistently in both editor modes:
|
||||
|
||||
## 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
|
||||
4. **Theme mode commands**: Command-palette actions for light and dark mode call the same `saveSettings` path as the Settings panel and persist only `settings.theme_mode`
|
||||
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
|
||||
|
||||
@@ -675,11 +737,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]]`.
|
||||
|
||||
@@ -707,6 +771,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` full-content hits from the visible workspace roots. It verifies backend hits against Markdown body text with frontmatter excluded, and stores only matching paths so body-only matches appear in the current list scope without rendering private matched text in note rows.
|
||||
|
||||
No indexing step required — search runs directly against the filesystem.
|
||||
|
||||
## Vault Management
|
||||
@@ -717,13 +783,16 @@ 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, tag colors, status colors, property display modes, Inbox/All Notes note-list column overrides, explicit organization workflow toggle, AI agent permission mode (`safe` / `power_user`)
|
||||
- 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`)
|
||||
|
||||
@@ -739,6 +808,20 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte
|
||||
- `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
|
||||
- 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:
|
||||
@@ -755,7 +838,8 @@ 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 treats the app process path, login-shell path, and supported local/toolchain/app install locations, including nvm-managed Node installs plus Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources
|
||||
- Uses `get_ai_agents_status`, whose backend 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
|
||||
- The shared `useAiAgentsStatus` hook defers that command until after the first render and skips it when AI features are disabled or the current window cannot render AI status surfaces
|
||||
- Persists dismissal locally once the user continues
|
||||
|
||||
### Remote Git Operations
|
||||
@@ -763,7 +847,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 Linux AppImage launches, every system-git command removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` before spawning `git`, so helpers like `git-remote-https` bind against the host git/library stack instead of Tolaria's bundled WebKit/AppImage libraries
|
||||
- On 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
|
||||
@@ -773,6 +860,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
|
||||
@@ -783,12 +877,17 @@ 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
|
||||
theme_mode: 'light' | 'dark' | 'system' | null
|
||||
ui_language: AppLocale | null
|
||||
date_display_format: 'us' | 'european' | 'friendly' | 'iso' | null
|
||||
note_width_mode: 'normal' | 'wide' | null
|
||||
default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | 'gemini' | 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
|
||||
@@ -796,7 +895,7 @@ interface Settings {
|
||||
}
|
||||
```
|
||||
|
||||
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 actions both update that same value. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `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. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation.
|
||||
Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; 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. `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
|
||||
|
||||
@@ -817,6 +916,7 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
|
||||
- **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
|
||||
@@ -835,7 +935,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
|
||||
@@ -843,6 +943,6 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
|
||||
- **`download_and_install_app_update`** — Channel-aware download/install with streamed progress events.
|
||||
|
||||
### CI/CD
|
||||
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. macOS release assets use `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds.
|
||||
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, and Linux x86_64 `.deb` / AppImage artifacts. Stable macOS DMG/updater assets use the same `Tolaria_<version>_macOS_Silicon` and `Tolaria_<version>_macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release.
|
||||
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. 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.
|
||||
|
||||
@@ -26,7 +26,11 @@ When deciding where to persist a piece of data, ask: **"Would the user want this
|
||||
| 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.
|
||||
@@ -38,6 +42,8 @@ Examples:
|
||||
- ✅ 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
|
||||
@@ -85,7 +91,7 @@ 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.
|
||||
@@ -100,6 +106,20 @@ Vault opening is allowed to render the main app shell while the full entry scan
|
||||
|
||||
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.
|
||||
@@ -119,12 +139,12 @@ The note list opportunistically preloads visible and adjacent markdown/text entr
|
||||
| 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 |
|
||||
| Filesystem watcher | notify | 6.1 |
|
||||
| AI (agent panel) | CLI agent adapters (Claude Code + Codex + OpenCode + Pi + Gemini) | - |
|
||||
| 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 |
|
||||
@@ -142,14 +162,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"]
|
||||
@@ -163,7 +185,7 @@ flowchart TD
|
||||
end
|
||||
|
||||
subgraph EXT["External Services"]
|
||||
CCLI["Claude / Codex / OpenCode / Pi / Gemini 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.)"]
|
||||
@@ -185,12 +207,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 │
|
||||
@@ -204,25 +226,31 @@ flowchart TD
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Sidebar** (220-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types: pointer users can drag the existing view row, double-click to rename it, or right-click for edit/rename/appearance/delete actions, while keyboard users can use the row context key for the same menu and command-palette move actions for ordering. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root.
|
||||
- **Note List / Pulse View** (220-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Folder-backed lists also show non-Markdown files: previewable image and PDF binaries get file indicators and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
|
||||
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count and rich-editor width toggle, BlockNote rich text editor with wikilink support, Markdown-compatible inline/display math rendering, first-class Mermaid diagram blocks, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block`. Can toggle to diff view (modified files), raw CodeMirror view, or a wide 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 and PDF files render through `FilePreview` as ordinary vault files using Tauri asset URLs; 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`, `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`. 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. Navigation history (Cmd+[/]) replaces tabs.
|
||||
- **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).
|
||||
|
||||
- **AI workspace** (docked panel or native window): `AiWorkspace` owns the multi-chat sidebar, installed-only target picker, permission-mode picker, and dock/pop-out controls. 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.
|
||||
|
||||
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`.
|
||||
|
||||
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 also grows the current window back out when a wider pane combination is restored, while note windows skip this path and keep their dedicated 800×700 initial sizing.
|
||||
The main Tauri window 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. 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.
|
||||
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, and the Rust command fans per-agent CLI checks across Tokio's blocking pool. The HTML bootstrap also installs a Tauri-only one-shot watchdog: React reports readiness from an effect after the root commits, and if that readiness signal never arrives the WebView reloads once instead of leaving macOS users in an inert rendered shell.
|
||||
|
||||
Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that the desktop native menus use. The native app menu keeps macOS-only Services/Hide entries off Windows and Linux, while cross-platform custom items such as Check for Updates emit Tolaria command IDs and show visible updater feedback.
|
||||
When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, and on Wayland sessions it re-execs once with the first architecture-matching system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The candidate order prefers Fedora-style `lib64` and Debian-style `x86_64-linux-gnu` paths before generic `/usr/lib`, and the ELF header is checked so a 64-bit Tolaria process does not retry with a 32-bit Wayland client library. The same AppImage path checks whether `fc-match` resolves the default emoji font to `Noto-COLRv1.ttf`; when the user has not provided `FONTCONFIG_FILE` or `FONTCONFIG_PATH`, Tolaria writes a cache-local fontconfig file that rejects only that matched font file and exports it before WebKit starts. The rendering overrides keep AppImage WebViews from blanking after accelerated compositing/DMA-BUF failures, the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER`, and the fontconfig guard avoids known WebKit crashes in COLRv1 emoji font rendering while leaving other emoji fonts available.
|
||||
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
|
||||
@@ -232,8 +260,7 @@ 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
|
||||
- `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
|
||||
@@ -244,14 +271,16 @@ Notes can be opened in separate Tauri windows for focused editing. Secondary win
|
||||
|
||||
Full agent mode — spawns the selected local CLI agent as a subprocess with tool access and MCP vault integration.
|
||||
|
||||
1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts` + `aiTargets.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, default-target selection, and the per-vault Safe / Power User permission mode shown in the panel header for coding agents
|
||||
1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts` + `aiTargets.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, default-target selection, bundled-docs prompt injection, and the per-vault Safe / Power User permission mode shown in the panel header for coding agents
|
||||
2. **Backend orchestration** (`ai_agents.rs`) — normalizes agent availability, streaming, and the request permission mode before dispatching to per-agent adapters
|
||||
3. **Shared runtime scaffold** (`cli_agent_runtime.rs`) — owns the common request shape, prompt wrapping, JSON-line subprocess lifecycle, normalized error/done handling, version probing, and Tolaria MCP server path resolution used by app-managed CLI agents
|
||||
4. **Agent adapters** — Shared prompts are mode-aware on every turn, including turns with note context snapshots: Vault Safe tells agents not to use or advertise shell, while Power User tells shell-capable agents to keep local commands scoped to the active vault. Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, and a scoped tool list: Safe enables file/search/edit tools only, while Power User adds Bash to the available tools and pre-approves Bash with `--allowedTools` without using dangerous bypass flags. Codex runtime specifics live in `codex_cli.rs`; Safe runs `codex --sandbox read-only --ask-for-approval untrusted exec --json`, while Power User runs `codex --sandbox workspace-write --ask-for-approval never exec --json` so shell execution stays enabled across repeated turns. OpenCode runs through `opencode run --format json` with transient permissions: Safe denies bash and external directories, while Power User allows bash but still denies external directories. Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`; both modes currently share the same transient MCP config and the prompt does not promise shell for Pi Power User. Gemini runs through `gemini --output-format stream-json --prompt` so assistant message chunks, tool calls, and final errors are mapped from the CLI event stream instead of relying on a buffered `response` field. Gemini Safe uses `auto_edit` plus `tools.exclude=["run_shell_command"]`; Power User intentionally uses `yolo` against a trusted transient Tolaria MCP entry. Codex, OpenCode, Pi, and Gemini all launch from the active vault cwd with closed stdin and transient MCP config. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags.
|
||||
5. **MCP Integration** — Claude receives the generated MCP config file path, Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides using Tolaria's resolved Node path plus `VAULT_PATH` and `WS_UI_PORT`, OpenCode receives it through `OPENCODE_CONFIG_CONTENT`, Pi receives it through a temporary `PI_CODING_AGENT_DIR/mcp.json` consumed by `pi-mcp-adapter`, and Gemini receives it through a temporary settings file pointed at by `GEMINI_CLI_SYSTEM_SETTINGS_PATH`
|
||||
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.
|
||||
|
||||
CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, nvm-managed Node installs, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. App-managed CLI spawns also expand the active vault path before using it as the subprocess working directory, then extend the child process `PATH` with the resolved binary directory plus those common toolchain directories, which lets GUI-launched macOS sessions run Homebrew/npm shims and their `node`-backed MCP subprocesses even when Finder/Dock did not inherit a terminal shell path.
|
||||
|
||||
CLI-agent system prompts also include a local Tolaria docs orientation when the bundled docs resource is present. `scripts/build-agent-docs.mjs` generates `src-tauri/resources/agent-docs/` from the public VitePress Markdown sources, including `index.md`, `AGENTS.md`, per-section bundles, `all.md`, `search-index.json`, and generated per-page files. Tauri bundles that folder as `agent-docs/`; `get_agent_docs_path` resolves the installed resource path, with a repository fallback for development, and `getAgentDocsPath()` caches it before each agent run. Agents are instructed to read the active vault's `AGENTS.md` for local conventions and search the bundled docs for Tolaria product behavior.
|
||||
|
||||
#### Agent Event Flow
|
||||
|
||||
```mermaid
|
||||
@@ -265,11 +294,11 @@ sequenceDiagram
|
||||
U->>FE: sendMessage(text, references)
|
||||
FE->>FE: buildContextSnapshot(activeNote, linkedNotes, openTabs)
|
||||
FE->>R: invoke('stream_ai_agent', {agent, message, systemPrompt, vaultPath, permissionMode})
|
||||
R->>R: pick adapter for claude_code, codex, opencode, or pi
|
||||
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, Codex JSONL, OpenCode JSON, Pi JSON, or Gemini 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)
|
||||
@@ -311,19 +340,19 @@ Large active notes are compacted into a head/tail body snapshot before they ente
|
||||
|
||||
### 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. Direct model targets run in Chat mode: they receive the same note-context snapshot and conversation history, but they do not receive vault-write tools or shell access. The backend `stream_ai_model` command supports OpenAI-compatible chat completions and Anthropic Messages-compatible calls, including Ollama, LM Studio, OpenRouter, OpenAI, Anthropic, Gemini, and custom compatible endpoints.
|
||||
Tolaria also supports direct model targets for local servers and API providers. These targets are stored as app-level provider metadata and can be selected in Settings or the status bar alongside coding agents. `src/shared/aiModelProviderCatalog.json` is the shared source for provider defaults, local/API grouping, API-key environment placeholders, and runtime fallback base URLs; the renderer imports it through `aiTargets.ts`, and Tauri includes the same JSON in `ai_models.rs`. Direct model targets run in Chat mode: they receive the same note-context snapshot and conversation history, but they do not receive vault-write tools or shell access. The backend `stream_ai_model` command supports OpenAI-compatible chat completions and Anthropic Messages-compatible calls, including Ollama, LM Studio, OpenRouter, OpenAI, Anthropic, Gemini, and custom compatible endpoints.
|
||||
|
||||
Provider secrets are not written to `settings.json`. Hosted API targets can use Tolaria's local app-data secrets file (`ai-provider-secrets.json`, outside vaults/worktrees and owner-only on Unix) or reference an environment variable name. Local endpoints can omit authentication.
|
||||
|
||||
### Authentication
|
||||
|
||||
Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login; Codex surfaces a friendly prompt to run `codex login` when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings; direct provider secrets stay in local app data or user-managed environment variables.
|
||||
Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login; Codex surfaces a friendly prompt to run `codex login` when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings; 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, Gemini CLI, Cursor, or any MCP-compatible client).
|
||||
|
||||
### Tool Surface (14 tools)
|
||||
### Tool Surface
|
||||
|
||||
| Tool | Params | Description |
|
||||
|------|--------|-------------|
|
||||
@@ -331,12 +360,13 @@ The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistan
|
||||
| `read_note` | `path` | Read note content (alias for `open_note`) |
|
||||
| `create_note` | `path, title, [type]` | Create new note with title and optional type frontmatter |
|
||||
| `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) |
|
||||
@@ -356,27 +386,28 @@ Tolaria can register itself as an MCP server in:
|
||||
- `~/.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 and Gemini settings), uses `upsert` semantics, and can be reversed by removing Tolaria's entry again. Tolaria verifies Node.js is available before writing config, writes an explicit `type: "stdio"` entry, pins `VAULT_PATH` to the active vault, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. The same generated entry is exposed as a manual JSON snippet in the MCP setup dialog and through the AI panel copy action, giving users a transparent fallback for MCP-compatible tools Tolaria does not auto-configure. In the desktop app, `useMcpStatus` copies that snippet through the native `copy_text_to_clipboard` command instead of the Web Clipboard API so macOS WKWebView permission policy cannot block setup. Packaged builds resolve `mcp-server/` from the installed resource directory next to the executable before falling back to macOS `Resources`, Linux package roots such as `/usr/local/Tolaria`, `/usr/lib/tolaria`, and `/usr/lib/tolaria/resources`, and AppImage paths. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`) and owns connect, disconnect, exact-snippet load, and copy-to-clipboard actions. Gemini CLI still owns its own install and sign-in; Tolaria writes the durable external MCP entry only on explicit setup, while app-managed Gemini sessions use transient settings and optional vault guidance. The desktop WebSocket bridge is started only when a persisted active vault exists and is resynced from React state on vault changes; no selected vault stops the bridge instead of falling back to `~/Laputa`. Stdio MCP server processes are owned by the external client that launched them: when that client closes stdin, Tolaria cancels UI-bridge reconnect timers, closes any UI WebSocket, and exits the Node process instead of keeping it alive in the background.
|
||||
That setup is user-initiated through the status bar / command palette flow, not a startup side effect. Registration is non-destructive (additive, preserves other servers 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) — selected-vault scoped"]
|
||||
subgraph MCP["MCP Server (Node.js or Bun) — mounted-workspace scoped"]
|
||||
IDX["index.js"]
|
||||
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 -->|"stdio transport"| STDIO["Claude Code / Cursor / Gemini / OpenCode"]
|
||||
IDX --> VAULT
|
||||
IDX --> WSB
|
||||
WSB -->|"port 9710 — tool bridge"| AI["AI Clients\n(Claude Code, external)"]
|
||||
WSB -->|"port 9711 — UI bridge"| FE["Frontend\n(useAiActivity)"]
|
||||
end
|
||||
|
||||
TAURI["Tauri bridge lifecycle"] -->|"start/stop/restart with active VAULT_PATH"| 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
|
||||
@@ -402,11 +433,12 @@ flowchart LR
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `spawn_ws_bridge(vault_path)` | Spawns `ws-bridge.js` as child process with VAULT_PATH env |
|
||||
| `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 |
|
||||
| `register_mcp(vault_path)` | Verifies Node.js, resolves the packaged `mcp-server/`, and writes Tolaria's explicit stdio entry to Claude Code, Gemini CLI, Cursor, and generic MCP configs on user request |
|
||||
| `mcp_config_snippet(vault_path)` | Builds the exact `mcpServers.tolaria` JSON users can copy into any compatible client without writing third-party config files |
|
||||
| `remove_mcp()` | Removes Tolaria's MCP entry from Claude Code, Gemini CLI, Cursor, and generic MCP configs |
|
||||
| `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 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.
|
||||
@@ -422,13 +454,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 full-content hits are verified against Markdown body text, excluding frontmatter, 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.
|
||||
|
||||
@@ -450,11 +484,11 @@ 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. Settings and command-palette theme actions both write the same installation-local `settings.theme_mode` value.
|
||||
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
|
||||
|
||||
@@ -462,6 +496,8 @@ Tolaria's app chrome uses an app-owned localization runtime in `src/lib/i18n.ts`
|
||||
|
||||
`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
|
||||
|
||||
### Vault List
|
||||
@@ -489,6 +525,7 @@ 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
|
||||
|
||||
@@ -499,11 +536,11 @@ On first launch, `useOnboarding` checks if the default vault exists. If not, it
|
||||
|
||||
If the selected vault disappears after startup, `useVaultLoader` re-checks `check_vault_exists` when reloads or vault-derived surfaces fail. A confirmed missing path clears cached entries, folders, views, modified-file state, and prefetched note content, then `App` reuses the `vault-missing` `WelcomeScreen` state so note and view actions cannot keep targeting the stale active vault.
|
||||
|
||||
When an opened folder is not yet a git repo, Tolaria shows a dismissible Git setup dialog and a persistent `Git disabled` status-bar warning. Markdown scanning, note browsing, note editing, and search continue normally. Git-dependent surfaces (history, changes, commit, sync, conflict resolution, remotes, AutoGit, and auto-sync) stay unavailable until the user explicitly initializes Git from the dialog, the status-bar warning, or the `Initialize Git for Current Vault` command-palette action.
|
||||
When 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 and remote-connection commits, Tolaria ensures the vault has local `user.name` / `user.email` values, falling back to `Tolaria <vault@tolaria.md>` when the vault has no local Git identity yet. That app-managed setup commit explicitly disables commit signing for the single command so inherited global or local `commit.gpgsign` preferences cannot strand onboarding when GPG is missing or misconfigured. Later `git_commit` calls honor the user's signing configuration first, then retry the same app-managed commit once with `commit.gpgsign=false` only when Git reports a signing-helper failure, so working GPG/SSH signing setups continue to sign while broken GPG setups do not create repeated opaque commit failures.
|
||||
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 the vault has local `user.name` / `user.email` values, falling back to `Tolaria <vault@tolaria.md>` when the vault has no local Git identity yet. That app-managed setup commit explicitly disables commit signing for the single command so inherited global or local `commit.gpgsign` preferences cannot strand onboarding when GPG is missing or misconfigured. Later `git_commit` calls honor the user's signing configuration first, then retry the same app-managed commit once with `commit.gpgsign=false` only when Git reports a signing-helper failure, so working GPG/SSH signing setups continue to sign while broken GPG setups do not create repeated opaque commit failures.
|
||||
|
||||
Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code, Codex, OpenCode, Pi, and Gemini CLI are installed, offers per-agent install links when they are missing, and stores local dismissal so the prompt does not repeat on every launch.
|
||||
Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code, Codex, OpenCode, 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.
|
||||
|
||||
@@ -513,15 +550,16 @@ After the clone completes, Tolaria removes every configured git remote from the
|
||||
|
||||
### 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. Linux AppImage builds strip AppImage loader variables from system-git subprocesses before spawning `git`, keeping `git-remote-https` on the host git/library stack
|
||||
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. Clone commands disable interactive terminal / askpass prompts and surface the git failure back to the UI instead of freezing the app waiting for input
|
||||
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
|
||||
@@ -552,7 +590,7 @@ sequenceDiagram
|
||||
participant MCP as MCP Server
|
||||
participant U as User
|
||||
|
||||
T->>T: apply Linux AppImage WebKit env/preload safeguards<br/>(AppImage only)
|
||||
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
|
||||
@@ -579,9 +617,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
|
||||
```
|
||||
|
||||
@@ -630,13 +669,15 @@ 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.
|
||||
`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.
|
||||
|
||||
If the current vault is not a Git repository, Tolaria treats Git as disabled instead of degraded. The status bar replaces changes, commit, sync, remote, conflict, and history controls with a `Git disabled` warning that reopens Git setup. 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.
|
||||
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 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.
|
||||
`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
|
||||
|
||||
@@ -677,7 +718,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|
||||
| `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, transient MCP adapter config, discovery, and JSON stream parsing |
|
||||
| `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 |
|
||||
@@ -763,11 +804,12 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|
||||
|---------|-------------|
|
||||
| `stream_claude_chat` | Claude CLI chat mode (streaming) |
|
||||
| `check_claude_cli` | Check if Claude CLI is available |
|
||||
| `get_ai_agents_status` | Check Claude Code + Codex + OpenCode + Pi + Gemini availability |
|
||||
| `stream_ai_agent` | Stream Claude Code, Codex, OpenCode, Pi, or Gemini through the normalized agent event layer |
|
||||
| `register_mcp_tools` | Register MCP in Claude/Gemini/Cursor/generic config for the active vault |
|
||||
| `remove_mcp_tools` | Remove Tolaria's MCP entry from Claude/Gemini/Cursor/generic config |
|
||||
| `check_mcp_status` | Check whether the active vault is explicitly registered in Claude/Gemini/Cursor/generic config |
|
||||
| `get_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 |
|
||||
@@ -820,9 +862,14 @@ 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` |
|
||||
| `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 |
|
||||
@@ -835,8 +882,8 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
|
||||
| `useCommitFlow` | Commit dialog state, shared manual/automatic checkpoint runner | Git commit/push orchestration |
|
||||
| `useGitRemoteStatus` | `remoteStatus`, `refreshRemoteStatus()` | On-demand remote detection for commit UI |
|
||||
| `useUnifiedSearch` | Query, results, loading state | Keyword search |
|
||||
| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, AutoGit thresholds, default AI agent, Gitignored-content visibility, All Notes file visibility) | Persistent settings |
|
||||
| `useVaultConfig` | Per-vault UI preferences, AI permission mode | Vault-specific config |
|
||||
| `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`.
|
||||
@@ -869,7 +916,7 @@ Shortcut routing is explicit:
|
||||
- macOS browser-reserved chords such as `Cmd+O`, `Cmd+F`, and `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path
|
||||
- `Cmd+Shift+V` uses the same command path for "Paste without Formatting"; `plainTextPaste.ts` reads text through the native clipboard command in Tauri and inserts it through the active rich/raw editor target or the focused browser text control
|
||||
- `Cmd+F` is surface-aware: editor focus opens current-note find/replace in raw CodeMirror, note-list focus preserves note-list search, and native menu enablement follows focus availability events so only one `Cmd+F` menu item is active
|
||||
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same manifest-derived command IDs for native menu clicks, accelerators, and custom titlebar menu actions
|
||||
- `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()`
|
||||
@@ -888,18 +935,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
|
||||
→ 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
|
||||
→ build-windows job:
|
||||
→ pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis
|
||||
→ 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 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
|
||||
@@ -911,26 +964,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
|
||||
→ 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
|
||||
→ 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
|
||||
→ 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 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`.
|
||||
@@ -1045,7 +1104,7 @@ Desktop-only modules gated at the crate level:
|
||||
Desktop-only features gated at the function level in `commands/`:
|
||||
- Git operations (commit, pull, push, status, history, diff, conflicts)
|
||||
- Clone-by-URL via system git (`clone_repo`)
|
||||
- CLI AI agent streaming (Claude, Codex, OpenCode, Pi, Gemini)
|
||||
- CLI AI agent streaming (Claude, Codex, OpenCode, Pi, Gemini, Kiro)
|
||||
- MCP registration and status
|
||||
- Menu state updates
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ On some Wayland systems, the Linux AppImage may fail to launch with:
|
||||
Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...
|
||||
```
|
||||
|
||||
Recent Tolaria AppImages automatically disable unstable WebKitGTK AppImage rendering paths and retry startup with an architecture-matching system Wayland client library when they detect this class of AppImage + Wayland environment. If you are running an older build, use this workaround:
|
||||
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
|
||||
@@ -45,6 +45,16 @@ WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/u
|
||||
|
||||
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
|
||||
@@ -59,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
|
||||
```
|
||||
|
||||
@@ -69,7 +84,15 @@ pnpm playwright:regression # Full Playwright regression suite
|
||||
|
||||
`create_getting_started_vault` clones the public starter repo and then removes every git remote from the new local copy. That means Getting Started vaults open local-only by default. Users connect a compatible remote later through the bottom-bar `No remote` chip or the command palette, both of which feed the same `AddRemoteModal` and `git_add_remote` backend flow.
|
||||
|
||||
Linux AppImage builds still use the user's system `git`. Before Tolaria spawns that `git` process, it removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` so HTTPS clone helpers use the host git libraries instead of bundled AppImage libraries.
|
||||
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
|
||||
|
||||
@@ -77,7 +100,7 @@ Linux AppImage builds still use the user's system `git`. Before Tolaria spawns t
|
||||
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
|
||||
@@ -97,7 +120,8 @@ tolaria/
|
||||
│ │ ├── RawEditorView.tsx # CodeMirror raw editor
|
||||
│ │ ├── Inspector.tsx # Fourth panel: metadata + relationships
|
||||
│ │ ├── DynamicPropertiesPanel.tsx # Editable frontmatter properties
|
||||
│ │ ├── AiPanel.tsx # AI agent panel (selected CLI agent + per-vault permission mode)
|
||||
│ │ ├── AiWorkspace.tsx # Multi-chat AI workspace (docked or native window)
|
||||
│ │ ├── 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
|
||||
@@ -107,7 +131,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
|
||||
@@ -124,7 +148,7 @@ tolaria/
|
||||
│ │ └── ui/ # shadcn/ui primitives
|
||||
│ │ ├── button.tsx, dialog.tsx, input.tsx, ...
|
||||
│ │
|
||||
│ ├── hooks/ # Custom React hooks (~86 files)
|
||||
│ ├── hooks/ # Custom React hooks (~90 files)
|
||||
│ │ ├── useVaultLoader.ts # Loads vault entries + content
|
||||
│ │ ├── useVaultSwitcher.ts # Multi-vault management
|
||||
│ │ ├── useVaultConfig.ts # Per-vault UI settings
|
||||
@@ -133,7 +157,7 @@ tolaria/
|
||||
│ │ ├── useNoteRename.ts # Note renaming + wikilink updates
|
||||
│ │ ├── useCliAiAgent.ts # Selected AI agent state + normalized session pipeline
|
||||
│ │ ├── aiAgentPermissionMode.ts # Safe/Power User mode normalization + labels
|
||||
│ │ ├── useAiAgentsStatus.ts # Claude/Codex/OpenCode/Pi/Gemini availability polling
|
||||
│ │ ├── useAiAgentsStatus.ts # Claude/Codex/OpenCode/Pi/Gemini/Kiro availability polling
|
||||
│ │ ├── useAiAgentPreferences.ts # Default-agent persistence + cycling
|
||||
│ │ ├── useAiActivity.ts # MCP UI bridge listener
|
||||
│ │ ├── useAutoSync.ts # Auto git pull/push
|
||||
@@ -213,15 +237,16 @@ tolaria/
|
||||
│ │ ├── 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)
|
||||
├── mcp-server/ # MCP bridge (Node.js or Bun)
|
||||
│ ├── index.js # MCP server entry (stdio, 14 tools)
|
||||
│ ├── vault.js # Vault file operations
|
||||
│ ├── ws-bridge.js # WebSocket bridge (ports 9710, 9711)
|
||||
@@ -258,7 +283,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. |
|
||||
@@ -271,6 +296,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. |
|
||||
|
||||
@@ -285,8 +314,8 @@ tolaria/
|
||||
| `src-tauri/src/search.rs` | Keyword search — scans vault files with walkdir. |
|
||||
| `src-tauri/src/ai_agents.rs` | CLI-agent request normalization, availability aggregation, adapter dispatch, and Claude event mapping. |
|
||||
| `src-tauri/src/cli_agent_runtime.rs` | Shared CLI-agent request shape, prompt wrapping, JSON subprocess lifecycle, version probing, and MCP path helpers. |
|
||||
| `src-tauri/src/claude_cli.rs`, `src-tauri/src/codex_cli.rs`, `src-tauri/src/opencode_cli.rs`, `src-tauri/src/pi_cli.rs`, `src-tauri/src/gemini_cli.rs` | Per-agent command, config, discovery, and JSON event adapters. |
|
||||
| `src-tauri/src/app_updater.rs` | Desktop updater bridge — selects alpha/stable manifests and streams install progress. |
|
||||
| `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
|
||||
|
||||
@@ -304,7 +333,9 @@ tolaria/
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/components/AiPanel.tsx` | AI agent panel — selected CLI agent with tool execution, reasoning, actions, and per-vault permission mode. |
|
||||
| `src/components/AiWorkspace.tsx` | Multi-chat AI workspace — sidebar chats, installed-only target picker, permission picker, and dock/pop-out controls. |
|
||||
| `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. |
|
||||
@@ -316,19 +347,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, UI language, 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, AI permission mode). |
|
||||
| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, sync interval, UI language, default AI agent, and the vault-level explicit organization toggle. |
|
||||
| `src/hooks/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
|
||||
@@ -364,7 +395,7 @@ type SidebarSelection =
|
||||
|
||||
### Command Registry
|
||||
|
||||
`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. Settings commands can update installation-local preferences directly when they reuse an existing settings path, such as the light/dark 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, `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.
|
||||
|
||||
@@ -445,12 +476,14 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
|
||||
3. **Tool action display**: Edit `src/components/AiActionCard.tsx`
|
||||
4. **Permission-mode UI and request plumbing**: Edit `src/lib/aiAgentPermissionMode.ts`, `src/components/AiPanel*.tsx`, `src/hooks/useCliAiAgent.ts`, and `src/utils/streamAiAgent.ts`
|
||||
5. **Shared CLI runtime behavior**: Edit `src-tauri/src/cli_agent_runtime.rs` for process lifecycle, prompt wrapping, version probing, and common Tolaria MCP path handling.
|
||||
6. **Agent-specific arguments/events**: Edit the per-agent adapter modules (`claude_cli.rs`, `codex_cli.rs`, `opencode_*`, `pi_*`, `gemini_*`). Keep Codex Safe on `read-only` + `untrusted` and Codex Power User on active-vault `workspace-write` + `never`, keep Pi and Gemini on transient MCP config, and do not use dangerous permission bypasses unless an ADR explicitly designs a new mode. Gemini Power User intentionally uses Gemini's `yolo` mode per ADR-0103.
|
||||
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`; registration and manual config generation must verify Node.js first, resolve the packaged `mcp-server/` for macOS, Windows, Linux package roots (`/usr/local/Tolaria`, `/usr/local/lib/tolaria`, `/usr/lib/tolaria`, `/usr/lib/tolaria/resources`), and AppImage installs, and use an explicit stdio entry with `VAULT_PATH` plus `WS_UI_PORT=9711`
|
||||
2. **Setup dialog copy/actions**: Edit `src/components/McpSetupDialog.tsx` and `src/hooks/useMcpStatus.ts`; users should see the Node.js prerequisite, the exact generated manual config, and a copy action before Tolaria writes third-party config files
|
||||
1. **Backend registration/status/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. **Process lifecycle**: Stdio MCP servers in `mcp-server/index.js` must exit when their external client closes stdin, and the desktop-owned `ws-bridge.js` child must be stopped on vault deselection, vault switch, and app exit
|
||||
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
47
docs/PUBLIC-DOCS-PLAN.md
Normal 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>
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
type: ADR
|
||||
id: "0098"
|
||||
title: "In-app image and PDF previews for binary vault files"
|
||||
status: active
|
||||
status: superseded
|
||||
date: 2026-04-29
|
||||
supersedes: "0086"
|
||||
superseded_by: "0110"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
32
docs/adr/0109-debounced-worker-derived-editor-indexes.md
Normal file
32
docs/adr/0109-debounced-worker-derived-editor-indexes.md
Normal 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.
|
||||
44
docs/adr/0110-in-app-media-and-pdf-file-previews.md
Normal file
44
docs/adr/0110-in-app-media-and-pdf-file-previews.md
Normal 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.
|
||||
@@ -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.
|
||||
41
docs/adr/0112-system-theme-mode.md
Normal file
41
docs/adr/0112-system-theme-mode.md
Normal 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.
|
||||
@@ -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.
|
||||
40
docs/adr/0114-mounted-workspaces-unified-graph.md
Normal file
40
docs/adr/0114-mounted-workspaces-unified-graph.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
24
docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md
Normal file
24
docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
33
docs/adr/0122-scalar-array-frontmatter-properties.md
Normal file
33
docs/adr/0122-scalar-array-frontmatter-properties.md
Normal 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.
|
||||
32
docs/adr/0123-full-vault-graph-for-secondary-note-windows.md
Normal file
32
docs/adr/0123-full-vault-graph-for-secondary-note-windows.md
Normal 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.
|
||||
27
docs/adr/0124-cached-secondary-note-window-startup.md
Normal file
27
docs/adr/0124-cached-secondary-note-window-startup.md
Normal 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.
|
||||
29
docs/adr/0126-renderer-action-history.md
Normal file
29
docs/adr/0126-renderer-action-history.md
Normal 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.
|
||||
32
docs/adr/0127-native-ai-workspace-window.md
Normal file
32
docs/adr/0127-native-ai-workspace-window.md
Normal 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.
|
||||
35
docs/adr/0128-lightweight-ai-workspace-window.md
Normal file
35
docs/adr/0128-lightweight-ai-workspace-window.md
Normal 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.
|
||||
45
docs/adr/0129-tolaria-vault-item-deep-links.md
Normal file
45
docs/adr/0129-tolaria-vault-item-deep-links.md
Normal 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.
|
||||
36
docs/adr/0130-windows-authenticode-release-signing.md
Normal file
36
docs/adr/0130-windows-authenticode-release-signing.md
Normal 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.
|
||||
29
docs/adr/0131-reusable-release-artifact-build-workflow.md
Normal file
29
docs/adr/0131-reusable-release-artifact-build-workflow.md
Normal 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.
|
||||
25
docs/adr/0132-alpha-authenticode-soft-gate.md
Normal file
25
docs/adr/0132-alpha-authenticode-soft-gate.md
Normal 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.
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
@@ -150,7 +150,7 @@ proposed → active → superseded
|
||||
| [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 | 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 |
|
||||
@@ -162,3 +162,25 @@ proposed → active → superseded
|
||||
| [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 | active |
|
||||
| [0112](0112-system-theme-mode.md) | System theme mode | active |
|
||||
| [0113](0113-shared-renderer-attachment-path-normalization.md) | Shared renderer attachment path normalization | active |
|
||||
| [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 |
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: [
|
||||
|
||||
46
index.html
46
index.html
@@ -28,24 +28,56 @@
|
||||
<body>
|
||||
<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();
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
})();
|
||||
@@ -62,7 +94,7 @@
|
||||
function hasReloadAttempted() {
|
||||
try {
|
||||
return sessionStorage.getItem(reloadAttemptKey) === '1';
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -71,7 +103,7 @@
|
||||
try {
|
||||
sessionStorage.setItem(reloadAttemptKey, '1');
|
||||
return true;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +111,7 @@
|
||||
function clearReloadAttempt() {
|
||||
try {
|
||||
sessionStorage.removeItem(reloadAttemptKey);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Storage can be unavailable in hardened WebView/privacy modes.
|
||||
}
|
||||
}
|
||||
|
||||
152
lara.lock
152
lara.lock
@@ -36,9 +36,14 @@ files:
|
||||
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
|
||||
@@ -54,13 +59,19 @@ files:
|
||||
command.note.removeIcon: 4bccbad66025e506b37b4218498b299c
|
||||
command.note.changeType: 4a54ffd47ed1f65ee97f34bfe8c3de14
|
||||
command.note.moveToFolder: 3a0f26c42b9168ad839960d134065905
|
||||
command.note.copyDeepLink: 8de1795f59af124594dc4decf77909a1
|
||||
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
|
||||
@@ -93,6 +104,7 @@ files:
|
||||
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
|
||||
@@ -109,19 +121,39 @@ files:
|
||||
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: e59f720605ec1cfa42b3b97d89d7e883
|
||||
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
|
||||
@@ -132,8 +164,20 @@ files:
|
||||
settings.titles.description: b94aeeca78dd376cc19b9aa019e53f6c
|
||||
settings.titles.autoRename: 5383db8e790ebf5f956d9c59cb4c4f67
|
||||
settings.titles.autoRenameDescription: cc28c28d8817736e658082a2261d9a6e
|
||||
settings.vaultContent.title: 78b883ce9acb9c25e611158a518d9185
|
||||
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
|
||||
@@ -145,10 +189,12 @@ files:
|
||||
settings.allNotesVisibility.unsupported: 257fe04419c9f6543a43e58b1edf9eb5
|
||||
settings.allNotesVisibility.unsupportedDescription: a3b8aa66900c742f001cd4533b3df44a
|
||||
settings.aiAgents.title: 27f08387b26e1ceeb1b60bd9c97e7a47
|
||||
settings.aiAgents.description: 042a8037f1a4f90c76ce31a49fdff59c
|
||||
settings.aiAgents.description: 768c138c3432fb159ac58c5a4471e786
|
||||
settings.aiFeatures.enable: af47a6b2bd32a7b7309bac7bef34f026
|
||||
settings.aiFeatures.enableDescription: 7d3916f7f9e1bf8b8797f50f4533b9da
|
||||
settings.aiAgents.default: 6af573559fd05d8c35bc57b41cc78e24
|
||||
settings.aiAgents.defaultTarget: 5acfc74fd97a6dd2d67d15c66de5eed5
|
||||
settings.aiAgents.agentGroup: 965530cbbec45b1754ed3ece120399f7
|
||||
settings.aiAgents.agentGroup: 11eaa7f817e3f24d54f3cf3025be49b1
|
||||
settings.aiAgents.localGroup: d34f4b997ce78da5aa57d657a5d6fcb5
|
||||
settings.aiAgents.apiGroup: 589a257cbd265eaa7de93de8b4084dff
|
||||
settings.aiAgents.installed: 73329564760013a7824ff9d5d1af91ff
|
||||
@@ -159,11 +205,11 @@ files:
|
||||
settings.aiAgents.apiLocalKey: 119de2f31867b273a134f05bc56b9e57
|
||||
settings.aiAgents.apiEnv: e011db3dff54ed3deae37a9615e31ebf
|
||||
settings.aiAgents.apiNoKey: 5bbccbfcd09b28902bbf5319dd6e6053
|
||||
settings.aiAgents.installedTitle: 5cb9b5ae5ebc66652814dff4584705cc
|
||||
settings.aiAgents.installedDescription: fb9ba9667725fb40fa1e5d58a2c02692
|
||||
settings.aiAgents.installedTitle: 0de6536215f76c44351ba4d809804c7a
|
||||
settings.aiAgents.installedDescription: 27c8ed48439ac4a613409f5303fbca84
|
||||
settings.aiAgents.noVersion: 89831adb7114946e916b9c31d57a1e33
|
||||
settings.aiProviders.title: 63eb84d8510f9f086d282ec7d47adaa1
|
||||
settings.aiProviders.description: 6c86c013eb6fb198b51e29ec47d66464
|
||||
settings.aiProviders.description: d29dfa440713d15de311d29a64a27767
|
||||
settings.aiProviders.localTitle: c7c20443d8aaf04271df9d713f5c02ce
|
||||
settings.aiProviders.localDescription: 7abed40ef2e46741c64ff47e423cca39
|
||||
settings.aiProviders.apiTitle: 66537b9e5200ebbbfbd091144d8afbcc
|
||||
@@ -267,6 +313,43 @@ files:
|
||||
ai.panel.placeholder.missing: 547c492026d115608067988b7cfbb9a9
|
||||
ai.panel.placeholder.ready: 31f5265c2e0432a7ca10d08e5c6f3114
|
||||
ai.panel.send: 747c6a3564774149c989884e0c581d53
|
||||
ai.workspace.title: d5b381672258b0bb54ede1e886895b3e
|
||||
ai.workspace.newChat: a917baaf1484ec5dc85f19ab8fc4f4e9
|
||||
ai.workspace.collapseSidebar: 9ecc763686b7c61547281f3e05ffd06f
|
||||
ai.workspace.expandSidebar: 281b619f3da2ba27e604302c3c00db56
|
||||
ai.workspace.close: 184649ab030e9a609488608221c28d8c
|
||||
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: 2ecae1c7ffb3296a789ff8f3b3ed2922
|
||||
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
|
||||
mcp.setup.manageTitle: d786c7b9ab9b2406258648931bca0e01
|
||||
mcp.setup.setupTitle: b4b373f690c8a0008885b31e78e93a5c
|
||||
mcp.setup.connectedDescription: a2a3be70c0ac9fa7ef112df5252249fa
|
||||
@@ -276,7 +359,7 @@ files:
|
||||
mcp.setup.disconnect: 42ae25231906c83927831e0ef7c317ac
|
||||
mcp.setup.connecting: 182e7eda4e721ad00737460b88701dee
|
||||
mcp.setup.disconnecting: 11de134aefe51cb4a5c545b5a057a871
|
||||
mcp.setup.nodeRequirement: ac58a35cf21d2204fed548c227f77594
|
||||
mcp.setup.runtimeRequirement: 1d7a8c99b2bc875b3601fa57860432fb
|
||||
mcp.setup.writeEntryDescription: 370aa4403d2ffc37f574503bc3cd00e6
|
||||
mcp.setup.clientPathsDescription: 94565852412051c799e78d2f4525ff53
|
||||
mcp.setup.geminiGuidanceDescription: 182cf5b84b2f56cb1cf41dcdb5e093d8
|
||||
@@ -326,6 +409,7 @@ files:
|
||||
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
|
||||
@@ -347,6 +431,8 @@ files:
|
||||
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
|
||||
@@ -416,18 +502,46 @@ files:
|
||||
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.moreActions: 89e19353176b94068dec4c768c25e70b
|
||||
editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e
|
||||
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
|
||||
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
|
||||
@@ -453,8 +567,11 @@ files:
|
||||
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
|
||||
@@ -484,6 +601,8 @@ files:
|
||||
status.zoom.reset: dbf36ab275e5fbd256590e65aa1ca9a3
|
||||
status.feedback.contribute: 96ab5025e38aef43fdb73c9830795264
|
||||
status.feedback.label: 9887a4451812854f0f1b6f669a874307
|
||||
status.docs.open: 2066997d2262d4a9b79de68f255dc4a9
|
||||
status.docs.label: a3907cd461d8739aa3266047bc4b8c0c
|
||||
status.theme.light: 4abf27a209985375949aaa426c7c7f3d
|
||||
status.theme.dark: 541be2a55a52b518b8e510c476c7cc95
|
||||
status.settings.open: bae08226d065231df6f01b08cd681c9b
|
||||
@@ -494,10 +613,23 @@ files:
|
||||
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
|
||||
@@ -557,6 +689,7 @@ files:
|
||||
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
|
||||
@@ -590,3 +723,6 @@ files:
|
||||
locale.koKR: d0bdb3cde477d82e766da05ebda50ccb
|
||||
locale.vi: 7b80fae85640c16cdb0261bef0c27636
|
||||
locale.plPL: c730389bc8d99e59c867766babdd48b5
|
||||
locale.beBY: 0a6de4460095f398e8a45ed512a7538f
|
||||
locale.beLatn: 9c454dccccd6aaac0121a45b923dc5ad
|
||||
locale.idID: cc1c9b57607b4439578b68074a5e2d80
|
||||
|
||||
23
mcp-server/agent-instructions.js
Normal file
23
mcp-server/agent-instructions.js
Normal 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),
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,19 @@ import {
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import WebSocket from 'ws'
|
||||
import { searchNotes, getNote, vaultContext } from './vault.js'
|
||||
import { requireVaultPath } from './vault-path.js'
|
||||
import { searchNotes, getNote } from './vault.js'
|
||||
import { requireVaultPaths } from './vault-path.js'
|
||||
import { readAgentInstructions, vaultContextWithInstructions } from './agent-instructions.js'
|
||||
import path from 'node:path'
|
||||
|
||||
const VAULT_PATH = requireVaultPath()
|
||||
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,
|
||||
})
|
||||
|
||||
// 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).
|
||||
@@ -33,6 +40,10 @@ let reconnectTimer = null
|
||||
let shutdownStarted = false
|
||||
const RECONNECT_INTERVAL_MS = 3000
|
||||
|
||||
function activeVaultPaths() {
|
||||
return requireVaultPaths()
|
||||
}
|
||||
|
||||
function connectUiBridge() {
|
||||
if (shutdownStarted) return
|
||||
|
||||
@@ -102,6 +113,7 @@ 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: {
|
||||
@@ -113,16 +125,33 @@ 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'],
|
||||
},
|
||||
@@ -130,10 +159,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'],
|
||||
},
|
||||
@@ -141,6 +172,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: {
|
||||
@@ -153,39 +185,128 @@ 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.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
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 vaultLabel(vaultPath) {
|
||||
return path.basename(vaultPath) || vaultPath
|
||||
}
|
||||
|
||||
function withVaultMetadata(note, vaultPath) {
|
||||
return {
|
||||
...note,
|
||||
vaultPath,
|
||||
vaultLabel: vaultLabel(vaultPath),
|
||||
}
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
async function searchActiveVaults(query, limit = 10) {
|
||||
const requestedLimit = Number.isFinite(limit) && limit > 0 ? limit : 10
|
||||
const results = []
|
||||
|
||||
for (const vaultPath of activeVaultPaths()) {
|
||||
const vaultResults = await searchNotes(vaultPath, query, requestedLimit)
|
||||
results.push(...vaultResults.map((result) => withVaultMetadata(result, vaultPath)))
|
||||
if (results.length >= requestedLimit) break
|
||||
}
|
||||
|
||||
return results.slice(0, requestedLimit)
|
||||
}
|
||||
|
||||
async function activeVaultContext(targetVaultPath = null) {
|
||||
const roots = activeVaultPaths()
|
||||
if (targetVaultPath) return vaultContextWithInstructions(targetVaultPath)
|
||||
if (roots.length === 1) return vaultContextWithInstructions(roots[0])
|
||||
|
||||
return {
|
||||
vaults: await Promise.all(roots.map(vaultContextWithInstructions)),
|
||||
}
|
||||
}
|
||||
|
||||
function uiPath(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 handleSearchNotes(args) {
|
||||
const results = await searchNotes(VAULT_PATH, args.query, args.limit)
|
||||
const results = await searchActiveVaults(args.query, args.limit)
|
||||
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 activeVaultContext(requestedVaultPath(args))
|
||||
return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }] }
|
||||
}
|
||||
|
||||
async function handleListVaults() {
|
||||
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 { content: [{ type: 'text', text: JSON.stringify({ vaults }, null, 2) }] }
|
||||
}
|
||||
|
||||
async function handleGetNote(args) {
|
||||
const note = await getNote(VAULT_PATH, args.path)
|
||||
const note = await getNoteFromActiveVaults(args.path, requestedVaultPath(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 = uiPath(args)
|
||||
broadcastUiAction('vault_changed', { path: targetPath })
|
||||
broadcastUiAction('open_tab', { path: targetPath })
|
||||
return { content: [{ type: 'text', text: `Opening ${targetPath} in Tolaria` }] }
|
||||
}
|
||||
|
||||
function handleHighlightEditor(args) {
|
||||
@@ -194,27 +315,24 @@ function handleHighlightEditor(args) {
|
||||
}
|
||||
|
||||
function handleRefreshVault(args) {
|
||||
broadcastUiAction('vault_changed', { path: args?.path })
|
||||
broadcastUiAction('vault_changed', { path: uiPath(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],
|
||||
['open_note', handleOpenNote],
|
||||
['highlight_editor', handleHighlightEditor],
|
||||
['refresh_vault', handleRefreshVault],
|
||||
])
|
||||
|
||||
function callToolHandler(name, args) {
|
||||
switch (name) {
|
||||
case 'search_notes':
|
||||
return handleSearchNotes(args)
|
||||
case 'get_vault_context':
|
||||
return handleVaultContext()
|
||||
case 'get_note':
|
||||
return handleGetNote(args)
|
||||
case 'open_note':
|
||||
return handleOpenNote(args)
|
||||
case 'highlight_editor':
|
||||
return handleHighlightEditor(args)
|
||||
case 'refresh_vault':
|
||||
return handleRefreshVault(args)
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`)
|
||||
}
|
||||
const handler = TOOL_HANDLERS.get(name)
|
||||
if (!handler) throw new Error(`Unknown tool: ${name}`)
|
||||
return handler(args)
|
||||
}
|
||||
|
||||
// --- Server setup ---
|
||||
@@ -277,7 +395,7 @@ async function main() {
|
||||
|
||||
connectUiBridge()
|
||||
await server.connect(transport)
|
||||
console.error(`Tolaria MCP server running (vault: ${VAULT_PATH})`)
|
||||
console.error('Tolaria MCP server running (vaults resolved per call)')
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
18
mcp-server/package-lock.json
generated
18
mcp-server/package-lock.json
generated
@@ -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.14",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
|
||||
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
|
||||
"version": "4.12.18",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
|
||||
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -670,9 +670,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.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"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"overrides": {
|
||||
"@hono/node-server": "1.19.13",
|
||||
"express-rate-limit": "8.2.2",
|
||||
"hono": "4.12.14",
|
||||
"fast-uri": "3.1.2",
|
||||
"hono": "4.12.18",
|
||||
"ip-address": "10.1.1",
|
||||
"path-to-regexp": "8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,20 @@ import { describe, it, before, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawn } from 'node:child_process'
|
||||
import {
|
||||
mkdtemp, mkdir, open, rm,
|
||||
mkdtemp, mkdir, open, rm, writeFile,
|
||||
} from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
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 {
|
||||
findMarkdownFiles, getNote, searchNotes, vaultContext,
|
||||
} from './vault.js'
|
||||
import { requireVaultPath } from './vault-path.js'
|
||||
import { requireVaultPath, requireVaultPaths } from './vault-path.js'
|
||||
import { vaultContextWithInstructions } from './agent-instructions.js'
|
||||
import { evaluateBridgeRequest } from './ws-bridge.js'
|
||||
|
||||
let tmpDir
|
||||
@@ -42,6 +47,17 @@ is_a: Note
|
||||
# Daily Log
|
||||
|
||||
Today I worked on the MCP server implementation.
|
||||
`)
|
||||
|
||||
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'), `---
|
||||
@@ -65,10 +81,11 @@ after(async () => {
|
||||
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')))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,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'),
|
||||
@@ -137,6 +163,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)
|
||||
@@ -158,7 +192,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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -205,15 +259,82 @@ describe('requireVaultPath', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects missing vault paths instead of falling back to ~/Laputa', () => {
|
||||
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(
|
||||
() => requireVaultPath({}),
|
||||
() => 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`)
|
||||
}
|
||||
} finally {
|
||||
await closeMcpClient(client, stderr)
|
||||
}
|
||||
})
|
||||
|
||||
it('exits when the MCP client closes stdin', async () => {
|
||||
const child = spawn(process.execPath, ['index.js'], {
|
||||
cwd: MCP_SERVER_DIR,
|
||||
@@ -241,6 +362,41 @@ describe('stdio process lifecycle', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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 mkdtemp(path.join(os.tmpdir(), prefix))
|
||||
const outsideNote = path.join(outsideDir, 'outside.md')
|
||||
|
||||
@@ -1,7 +1,88 @@
|
||||
export function requireVaultPath(env = process.env) {
|
||||
const vaultPath = env.VAULT_PATH?.trim()
|
||||
if (!vaultPath) {
|
||||
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 vaultPath
|
||||
return vaultPaths
|
||||
}
|
||||
|
||||
export function requireVaultPath(env = process.env, options = {}) {
|
||||
return requireVaultPaths(env, options)[0]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function getNote(vaultPath, notePath) {
|
||||
relativePath,
|
||||
} = await resolveVaultNotePath(vaultPath, notePath)
|
||||
const raw = await readUtf8File(noteRealPath)
|
||||
const parsed = matter(raw)
|
||||
const parsed = parseMarkdownNote(raw)
|
||||
return {
|
||||
path: relativePath,
|
||||
frontmatter: parsed.data,
|
||||
@@ -109,7 +109,7 @@ export async function vaultContext(vaultPath) {
|
||||
}
|
||||
|
||||
notesWithMtime.sort((a, b) => b.mtime - a.mtime)
|
||||
const recentNotes = notesWithMtime.slice(0, 20).map(({ mtime: _mtime, ...rest }) => rest)
|
||||
const recentNotes = notesWithMtime.slice(0, 20).map(contextNoteWithoutMtime)
|
||||
|
||||
return {
|
||||
types: [...typesSet].sort(),
|
||||
@@ -160,9 +160,17 @@ function matchesSearchQuery(title, content, query) {
|
||||
return title.toLowerCase().includes(query) || content.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
function contextNoteWithoutMtime(note) {
|
||||
return {
|
||||
path: note.path,
|
||||
title: note.title,
|
||||
type: note.type,
|
||||
}
|
||||
}
|
||||
|
||||
async function readVaultContextNote(vaultPath, filePath) {
|
||||
const raw = await readUtf8File(filePath)
|
||||
const parsed = matter(raw)
|
||||
const parsed = parseMarkdownNote(raw)
|
||||
const rel = path.relative(vaultPath, filePath)
|
||||
const topFolder = extractTopFolder(rel)
|
||||
const stat = await statFile(filePath)
|
||||
@@ -180,6 +188,129 @@ async function readVaultContextNote(vaultPath, filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseMarkdownNote(raw) {
|
||||
try {
|
||||
const parsed = matter(raw)
|
||||
const fallback = parseFrontmatterFallback(raw)
|
||||
return shouldUseFallbackFrontmatter(parsed, fallback) ? fallback : parsed
|
||||
} catch {
|
||||
return parseFrontmatterFallback(raw)
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseFallbackFrontmatter(parsed, fallback) {
|
||||
return Object.keys(parsed.data).length === 0 && Object.keys(fallback.data).length > 0
|
||||
}
|
||||
|
||||
function parseFrontmatterFallback(raw) {
|
||||
const split = splitFrontmatter(raw)
|
||||
if (!split) return { data: {}, content: raw }
|
||||
|
||||
return {
|
||||
data: parseFrontmatterBlock(split.frontmatter),
|
||||
content: split.content,
|
||||
}
|
||||
}
|
||||
|
||||
function splitFrontmatter(raw) {
|
||||
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/)
|
||||
if (!match) return null
|
||||
return { frontmatter: match[1], content: match[2] }
|
||||
}
|
||||
|
||||
function parseFrontmatterBlock(frontmatter) {
|
||||
const data = {}
|
||||
let listKey = null
|
||||
|
||||
for (const line of frontmatter.split(/\r?\n/)) {
|
||||
const item = parseYamlListItem(line)
|
||||
if (listKey && item !== null) {
|
||||
data[listKey].push(parseYamlScalar(item))
|
||||
continue
|
||||
}
|
||||
|
||||
listKey = null
|
||||
const field = parseTopLevelYamlField(line)
|
||||
if (!field) continue
|
||||
|
||||
data[field.key] = field.value ? parseYamlValue(field.value) : []
|
||||
listKey = field.value ? null : field.key
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function parseTopLevelYamlField(line) {
|
||||
if (!line || line.trimStart() !== line || line.trimStart().startsWith('#')) return null
|
||||
|
||||
const separatorIndex = line.indexOf(':')
|
||||
if (separatorIndex <= 0) return null
|
||||
|
||||
return {
|
||||
key: stripMatchingQuotes(line.slice(0, separatorIndex).trim()),
|
||||
value: line.slice(separatorIndex + 1).trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function parseYamlValue(value) {
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
return splitInlineYamlArray(value).map(parseYamlScalar)
|
||||
}
|
||||
return parseYamlScalar(value)
|
||||
}
|
||||
|
||||
function splitInlineYamlArray(value) {
|
||||
const inner = value.slice(1, -1)
|
||||
const items = []
|
||||
let current = ''
|
||||
let quote = null
|
||||
|
||||
for (const char of inner) {
|
||||
if (quote) {
|
||||
current += char
|
||||
if (char === quote) quote = null
|
||||
continue
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char
|
||||
current += char
|
||||
continue
|
||||
}
|
||||
if (char === ',') {
|
||||
items.push(current.trim())
|
||||
current = ''
|
||||
continue
|
||||
}
|
||||
current += char
|
||||
}
|
||||
|
||||
if (current.trim()) items.push(current.trim())
|
||||
return items
|
||||
}
|
||||
|
||||
function parseYamlListItem(line) {
|
||||
const match = line.match(/^\s+-\s*(.*)$/)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
|
||||
function parseYamlScalar(value) {
|
||||
const unquoted = stripMatchingQuotes(value.trim())
|
||||
if (unquoted !== value.trim()) return unquoted
|
||||
|
||||
if (/^(true|yes)$/i.test(unquoted)) return true
|
||||
if (/^(false|no)$/i.test(unquoted)) return false
|
||||
if (/^(null|~)$/i.test(unquoted)) return null
|
||||
if (/^-?\d+(\.\d+)?$/.test(unquoted)) return Number(unquoted)
|
||||
|
||||
return unquoted
|
||||
}
|
||||
|
||||
function stripMatchingQuotes(value) {
|
||||
const first = value[0]
|
||||
const last = value[value.length - 1]
|
||||
return (first === '"' || first === "'") && first === last ? value.slice(1, -1) : value
|
||||
}
|
||||
|
||||
function extractTopFolder(relativePath) {
|
||||
const topFolder = relativePath.split(path.sep)[0]
|
||||
return topFolder === relativePath ? null : `${topFolder}/`
|
||||
|
||||
@@ -22,9 +22,11 @@
|
||||
import { createServer } from 'node:http'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import {
|
||||
getNote, searchNotes, vaultContext,
|
||||
getNote, searchNotes,
|
||||
} from './vault.js'
|
||||
import { requireVaultPath } from './vault-path.js'
|
||||
import { requireVaultPaths } from './vault-path.js'
|
||||
import { readAgentInstructions, vaultContextWithInstructions } from './agent-instructions.js'
|
||||
import path from 'node:path'
|
||||
|
||||
const WS_PORT = parseInt(process.env.WS_PORT || '9710', 10)
|
||||
const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10)
|
||||
@@ -37,12 +39,66 @@ const TRUSTED_UI_ORIGINS = new Set([
|
||||
|
||||
/** @type {WebSocketServer | null} */
|
||||
let uiBridge = null
|
||||
let vaultPath = null
|
||||
const UNKNOWN_TOOL = Symbol('unknown tool')
|
||||
|
||||
function activeVaultPath() {
|
||||
vaultPath ??= requireVaultPath()
|
||||
return vaultPath
|
||||
function activeVaultPaths() {
|
||||
return requireVaultPaths()
|
||||
}
|
||||
|
||||
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 uiPath(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 getNoteFromActiveVaults(notePath, vaultPath = null) {
|
||||
const candidates = vaultPath ? [vaultPath] : activeVaultPaths()
|
||||
const matches = []
|
||||
const errors = []
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
matches.push({ ...(await getNote(candidate, notePath)), vaultPath: 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}`)
|
||||
}
|
||||
|
||||
async function searchActiveVaults(query, limit = 10) {
|
||||
const requestedLimit = Number.isFinite(limit) && limit > 0 ? limit : 10
|
||||
const results = []
|
||||
|
||||
for (const vaultPath of activeVaultPaths()) {
|
||||
const vaultResults = await searchNotes(vaultPath, query, requestedLimit)
|
||||
results.push(...vaultResults.map((result) => ({ ...result, vaultPath })))
|
||||
if (results.length >= requestedLimit) break
|
||||
}
|
||||
|
||||
return results.slice(0, requestedLimit)
|
||||
}
|
||||
|
||||
async function activeVaultContext() {
|
||||
const roots = activeVaultPaths()
|
||||
if (roots.length === 1) return vaultContextWithInstructions(roots[0])
|
||||
return { vaults: await Promise.all(roots.map(vaultContextWithInstructions)) }
|
||||
}
|
||||
|
||||
function broadcastUiAction(action, payload) {
|
||||
@@ -55,19 +111,21 @@ function broadcastUiAction(action, payload) {
|
||||
|
||||
|
||||
async function readNoteTool(args) {
|
||||
const note = await getNote(activeVaultPath(), args.path)
|
||||
const note = await getNoteFromActiveVaults(args.path, requestedVaultPath(args))
|
||||
return { content: note.content, frontmatter: note.frontmatter }
|
||||
}
|
||||
|
||||
function uiOpenNoteTool(args) {
|
||||
broadcastUiAction('vault_changed', { path: args.path })
|
||||
broadcastUiAction('open_note', { path: args.path })
|
||||
const targetPath = uiPath(args)
|
||||
broadcastUiAction('vault_changed', { path: targetPath })
|
||||
broadcastUiAction('open_note', { path: targetPath })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
function uiOpenTabTool(args) {
|
||||
broadcastUiAction('vault_changed', { path: args.path })
|
||||
broadcastUiAction('open_tab', { path: args.path })
|
||||
const targetPath = uiPath(args)
|
||||
broadcastUiAction('vault_changed', { path: targetPath })
|
||||
broadcastUiAction('open_tab', { path: targetPath })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
@@ -82,15 +140,30 @@ function uiSetFilterTool(args) {
|
||||
}
|
||||
|
||||
function refreshVaultTool(args) {
|
||||
broadcastUiAction('vault_changed', { path: args?.path })
|
||||
broadcastUiAction('vault_changed', { path: uiPath(args) })
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
async function listVaultsTool() {
|
||||
return {
|
||||
vaults: await Promise.all(activeVaultPaths().map(async (vaultPath) => {
|
||||
const agentInstructions = await readAgentInstructions(vaultPath)
|
||||
return {
|
||||
path: vaultPath,
|
||||
label: path.basename(vaultPath) || vaultPath,
|
||||
agentInstructionsPath: agentInstructions?.path ?? null,
|
||||
hasAgentInstructions: agentInstructions !== null,
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
const TOOL_EXECUTORS = [
|
||||
['open_note', readNoteTool],
|
||||
['read_note', readNoteTool],
|
||||
['search_notes', (args) => searchNotes(activeVaultPath(), args.query, args.limit)],
|
||||
['vault_context', () => vaultContext(activeVaultPath())],
|
||||
['search_notes', (args) => searchActiveVaults(args.query, args.limit)],
|
||||
['vault_context', () => activeVaultContext()],
|
||||
['list_vaults', () => listVaultsTool()],
|
||||
['ui_open_note', uiOpenNoteTool],
|
||||
['ui_open_tab', uiOpenTabTool],
|
||||
['ui_highlight', highlightTool],
|
||||
@@ -206,7 +279,7 @@ export function startUiBridge(port = WS_UI_PORT) {
|
||||
}
|
||||
|
||||
export function startBridge(port = WS_PORT) {
|
||||
const currentVaultPath = activeVaultPath()
|
||||
const currentVaultPaths = activeVaultPaths()
|
||||
const wss = new WebSocketServer({
|
||||
port,
|
||||
host: LOOPBACK_HOST,
|
||||
@@ -214,7 +287,7 @@ export function startBridge(port = WS_PORT) {
|
||||
})
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.error(`[ws-bridge] Client connected (vault: ${currentVaultPath})`)
|
||||
console.error(`[ws-bridge] Client connected (vaults: ${currentVaultPaths.join(', ')})`)
|
||||
|
||||
ws.on('message', async (raw) => {
|
||||
try {
|
||||
@@ -236,7 +309,7 @@ export function startBridge(port = WS_PORT) {
|
||||
const isMain = process.argv[1]?.endsWith('ws-bridge.js')
|
||||
if (isMain) {
|
||||
try {
|
||||
activeVaultPath()
|
||||
activeVaultPaths()
|
||||
startUiBridge().then(() => startBridge())
|
||||
} catch (err) {
|
||||
console.error(`[ws-bridge] ${err.message}`)
|
||||
|
||||
35
package.json
35
package.json
@@ -7,8 +7,12 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"agent-docs": "node scripts/build-agent-docs.mjs",
|
||||
"bundle-mcp": "node scripts/bundle-mcp-server.mjs",
|
||||
"lint": "eslint .",
|
||||
"docs:dev": "vitepress dev site --host 127.0.0.1",
|
||||
"docs:build": "pnpm agent-docs && vitepress build site",
|
||||
"docs:preview": "vitepress preview site --host 127.0.0.1",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"l10n:translate": "lara-cli translate",
|
||||
"l10n:translate:force": "lara-cli translate --force",
|
||||
"l10n:validate": "node scripts/validate-locales.mjs",
|
||||
@@ -17,7 +21,7 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/autosave-low-end-typing.spec.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/frontmatter-date-picker.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/missing-active-vault-recovery.spec.ts tests/smoke/missing-string-metadata-open-note.spec.ts tests/smoke/multibyte-search-snippet.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/pull-refresh-open-note.spec.ts tests/smoke/vault-loading-skeleton.spec.ts tests/smoke/wikilink-path-fix.spec.ts",
|
||||
"playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/autosave-low-end-typing.spec.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/frontmatter-date-picker.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/note-history-edit-loop.spec.ts tests/smoke/quick-open-create-note.spec.ts tests/smoke/save-before-note-switch.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/missing-active-vault-recovery.spec.ts tests/smoke/missing-string-metadata-open-note.spec.ts tests/smoke/multibyte-search-snippet.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/pull-refresh-open-note.spec.ts tests/smoke/type-derived-properties.spec.ts tests/smoke/vault-loading-skeleton.spec.ts tests/smoke/wikilink-path-fix.spec.ts",
|
||||
"playwright:regression": "playwright test tests/smoke/",
|
||||
"playwright:integration": "playwright test --config playwright.integration.config.ts",
|
||||
"test:coverage": "node scripts/run-vitest-coverage.mjs",
|
||||
@@ -51,16 +55,17 @@
|
||||
"@sentry/react": "^10.47.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@tldraw/assets": "4.5.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "3.4.2",
|
||||
"katex": "^0.16.28",
|
||||
"lucide-react": "^0.564.0",
|
||||
"mermaid": "^11.14.0",
|
||||
"posthog-js": "^1.363.5",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -102,6 +107,7 @@
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.48.0",
|
||||
"vite": "^7.3.2",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitest": "^4.0.18",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
@@ -109,12 +115,31 @@
|
||||
"overrides": {
|
||||
"@hono/node-server": "1.19.13",
|
||||
"express-rate-limit": "8.2.2",
|
||||
"hono": "4.12.14",
|
||||
"hono": "4.12.18",
|
||||
"ip-address": "10.1.1",
|
||||
"mermaid>uuid": "11.1.1",
|
||||
"path-to-regexp": "8.4.0",
|
||||
"fast-uri": "3.1.2",
|
||||
"fast-xml-builder": "1.1.7",
|
||||
"flatted": "3.4.2",
|
||||
"minimatch@3.1.2": "3.1.5",
|
||||
"minimatch@3.1.3": "3.1.5",
|
||||
"minimatch@9.0.5": "9.0.9",
|
||||
"minimatch@9.0.6": "9.0.9",
|
||||
"picomatch": "4.0.4",
|
||||
"postcss": "8.5.10",
|
||||
"protobufjs": "7.5.6",
|
||||
"rollup": "4.59.0"
|
||||
"qs": "6.15.2",
|
||||
"rollup": "4.59.0",
|
||||
"undici": "7.25.0",
|
||||
"@blocknote/core>uuid": "11.1.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@blocknote/core@0.46.2": "patches/@blocknote__core@0.46.2.patch",
|
||||
"@blocknote/react@0.46.2": "patches/@blocknote__react@0.46.2.patch",
|
||||
"@tiptap/extension-link@3.19.0": "patches/@tiptap__extension-link@3.19.0.patch",
|
||||
"prosemirror-tables@1.8.5": "patches/prosemirror-tables@1.8.5.patch",
|
||||
"@blocknote/code-block@0.46.2": "patches/@blocknote__code-block@0.46.2.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
patches/@blocknote__code-block@0.46.2.patch
Normal file
86
patches/@blocknote__code-block@0.46.2.patch
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +1,9 @@
|
||||
diff --git a/dist/blocknote-react.js b/dist/blocknote-react.js
|
||||
index 6271d12d2cd5152e924c5bfe78f3b8904844dbfe..12bb96bb36d445a77d51a18d0de446a3f9f0ea59 100644
|
||||
index d4d36cd..79dd4f0 100644
|
||||
--- a/dist/blocknote-react.js
|
||||
+++ b/dist/blocknote-react.js
|
||||
@@ -155,8 +155,26 @@ var Rt = (e) => {
|
||||
@@ -154,8 +154,26 @@ const co = (e) => {
|
||||
};
|
||||
function so(e) {
|
||||
let t = new DOMRect();
|
||||
- const n = "getBoundingClientRect" in e ? () => e.getBoundingClientRect() : () => e.element.getBoundingClientRect();
|
||||
@@ -17,7 +18,7 @@ index 6271d12d2cd5152e924c5bfe78f3b8904844dbfe..12bb96bb36d445a77d51a18d0de446a3
|
||||
+ t = n();
|
||||
+ return t;
|
||||
+ };
|
||||
}
|
||||
+}
|
||||
+function __bnSafeDomAtPos(e, t) {
|
||||
+ const n = e.prosemirrorView;
|
||||
+ if (!n || n.isDestroyed)
|
||||
@@ -27,11 +28,18 @@ index 6271d12d2cd5152e924c5bfe78f3b8904844dbfe..12bb96bb36d445a77d51a18d0de446a3
|
||||
+ } catch {
|
||||
+ return null;
|
||||
+ }
|
||||
+}
|
||||
}
|
||||
const z = (e) => {
|
||||
var h, b, p;
|
||||
const { refs: t, floatingStyles: n, context: o } = Ze({
|
||||
@@ -216,9 +234,7 @@ const Lt = (e) => {
|
||||
@@ -193,6 +211,7 @@ const z = (e) => {
|
||||
...e.elementProps,
|
||||
style: {
|
||||
display: "flex",
|
||||
+ pointerEvents: c === "close" ? "none" : void 0,
|
||||
...(h = e.elementProps) == null ? void 0 : h.style,
|
||||
zIndex: `calc(var(--bn-ui-base-z-index) + ${((p = (b = e.elementProps) == null ? void 0 : b.style) == null ? void 0 : p.zIndex) || 0})`,
|
||||
...n,
|
||||
@@ -216,9 +235,7 @@ const z = (e) => {
|
||||
const s = Ue(t, c.doc);
|
||||
if (!s)
|
||||
return;
|
||||
@@ -42,7 +50,39 @@ index 6271d12d2cd5152e924c5bfe78f3b8904844dbfe..12bb96bb36d445a77d51a18d0de446a3
|
||||
if (a instanceof Element)
|
||||
return {
|
||||
element: a
|
||||
@@ -3306,14 +3322,15 @@ const pi = (e) => {
|
||||
@@ -2499,7 +2516,14 @@ function Kr(e) {
|
||||
columns: d
|
||||
} = e, m = H(
|
||||
(C) => {
|
||||
- a(), s(), u == null || u(C);
|
||||
+ a();
|
||||
+ try {
|
||||
+ s();
|
||||
+ } catch (x) {
|
||||
+ console.warn("Ignored stale suggestion menu query cleanup:", x);
|
||||
+ return;
|
||||
+ }
|
||||
+ u == null || u(C);
|
||||
},
|
||||
[u, a, s]
|
||||
), { items: g, usedQuery: f, loadingState: h } = Yt(
|
||||
@@ -2734,7 +2758,14 @@ function ti(e) {
|
||||
onItemClick: u
|
||||
} = e, d = H(
|
||||
(p) => {
|
||||
- a(), s(), u == null || u(p);
|
||||
+ a();
|
||||
+ try {
|
||||
+ s();
|
||||
+ } catch (C) {
|
||||
+ console.warn("Ignored stale suggestion menu query cleanup:", C);
|
||||
+ return;
|
||||
+ }
|
||||
+ u == null || u(p);
|
||||
},
|
||||
[u, a, s]
|
||||
), { items: m, usedQuery: g, loadingState: f } = Yt(
|
||||
@@ -3306,14 +3337,15 @@ const ii = (e, t = 0.3) => {
|
||||
);
|
||||
if (!m)
|
||||
return {};
|
||||
@@ -61,7 +101,7 @@ index 6271d12d2cd5152e924c5bfe78f3b8904844dbfe..12bb96bb36d445a77d51a18d0de446a3
|
||||
return p instanceof Element ? (d.cellReference = { element: p }, d.rowReference = {
|
||||
element: f,
|
||||
getBoundingClientRect: () => {
|
||||
@@ -4371,7 +4388,7 @@ const El = (e) => {
|
||||
@@ -4371,7 +4403,7 @@ const zi = (e) => {
|
||||
const a = Ue(t, c.prosemirrorState.doc);
|
||||
if (!a)
|
||||
return;
|
||||
|
||||
1201
pnpm-lock.yaml
generated
1201
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
release-notes/stable-v2026.5.2.md
Normal file
18
release-notes/stable-v2026.5.2.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## New Features
|
||||
|
||||
- 📋 **Paste Without Formatting** — Paste copied text as plain content without bringing unwanted styling into a note.
|
||||
- 🇵🇱 **Polish Language Support** — Use Tolaria with a new Polish interface translation.
|
||||
- 🧭 **Refined Titlebar Navigation** — Navigate with clearer titlebar controls that feel more native on desktop.
|
||||
|
||||
## Improvements
|
||||
|
||||
- ⚡ **Faster Note Loading** — Switch between notes more smoothly by reusing cached note content and parsed editor blocks.
|
||||
- 🗂️ **Cleaner Sidebar and Menus** — Scan folders, sidebar sections, slash commands, icon choices, and release tabs with cleaner spacing and styling.
|
||||
- 🖥️ **Better Cross-Platform Window Controls** — Use macOS and Linux window controls that better match each platform.
|
||||
- ☀️ **Improved Light Mode Editing** — Read code blocks more comfortably when Tolaria is using the light theme.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- This release also improves editor reliability around block dragging, table handles, pasted Markdown, stale side-menu actions, and unrelated vault refreshes.
|
||||
- Vault, type, and frontmatter handling were hardened to prevent freezes, filename collisions, parse failures, and stale saved-view deletes.
|
||||
- Startup, Linux AppImage, release CI, and AI/Codex integration fixes are included in the full commit list.
|
||||
20
release-notes/v2026-05-04.md
Normal file
20
release-notes/v2026-05-04.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## New Features
|
||||
|
||||
- 🤖 **Direct AI Model Providers** — Use OpenAI, Anthropic, Gemini, OpenRouter, Ollama, LM Studio, or a custom OpenAI-compatible endpoint directly from Tolaria.
|
||||
- 🖊️ **Markdown Whiteboards** — Create and edit tldraw-powered whiteboards that live as durable Markdown files in your vault.
|
||||
- 🧭 **Table of Contents Panel** — Navigate long notes with a lazy, outline-style panel generated from the current document headings.
|
||||
- 📄 **Readable Release Notes** — View stable releases through curated notes written for users instead of raw commit lists.
|
||||
|
||||
## Improvements
|
||||
|
||||
- 🧩 **Cleaner AI Settings** — Configure provider secrets, local models, and AI targets through a more focused settings experience.
|
||||
- 💬 **Better Agent Context Handling** — Large AI agent contexts are compacted more safely, and Codex MCP tools are exposed more reliably.
|
||||
- 🧱 **Improved Editor Blocks** — Code blocks can be copied more easily, block controls feel steadier, and rich exports avoid unsupported browser APIs.
|
||||
- 🗂️ **Smarter Note and Type Handling** — Renames, type changes, frontmatter placeholders, and path identity now behave more consistently across vault flows.
|
||||
- 🌍 **Better International Editing** — RTL text direction, IME composition, Korean sidebar copy, and unicode git paths all received focused polish.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- Embedded diagrams, tldraw whiteboard assets, stale checklist toggles, and stale note open or rename flows were hardened to avoid broken editor states.
|
||||
- Vault recovery, missing active vault handling, fresh-note heading paste, and same-path type collisions now fail more gracefully.
|
||||
- Linux AppImage startup, MCP resource discovery, release CI, Codacy security findings, and AI provider runtime paths all received stability fixes.
|
||||
18
release-notes/v2026-05-06.md
Normal file
18
release-notes/v2026-05-06.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## New Features
|
||||
- 🌓 **System Theme Preference** — Let Tolaria follow the operating system light/dark appearance automatically.
|
||||
- ⚡ **Faster Date Editing** — Edit distant frontmatter dates more quickly with improved date controls.
|
||||
- ↔️ **Set default note width** — Change default between normal and wide in the settings
|
||||
- 🗣️ **Set default pluralization** – Enable/disable plurlization of type names in the sidebar, from the settings
|
||||
|
||||
## Improvements
|
||||
- 🖼️ **Richer Media Handling** — Clipboard images, native image attachments, media previews, and tldraw assets are handled more reliably inside vaults.
|
||||
- ✍️ **Better Editor Behavior** — Selection, pasted content, numbered lists, wikilinks in tables, IME composition, table handles, and code-block copy actions were refined.
|
||||
- 🗂️ **Safer Type and Property Flows** — Type filename collisions, built-in note type creation, type-derived placeholders, blank frontmatter properties, and date picker behavior were hardened.
|
||||
- 🧠 **Smoother AI Panel UX** — AI target selection, composer focus, context compaction, direct provider setup, and agent spawning paths are more robust.
|
||||
- 🪟 **Cross-Platform Polish** — Linux AppImage startup/input, Windows menu actions, remote credential handling, and window geometry persistence were improved.
|
||||
|
||||
## Stability and Fixes
|
||||
- Fixed native image attachments in `2e7c5cb433f95d9d10939bcc38453dcc9b4e3ec2`.
|
||||
- Kept the public installer page visible and improved readable release-note loading.
|
||||
- Reduced stale-state bugs around vault refreshes, wikilink targets, history diffs, whiteboard blocks, checklist toggles, note rename/open flows, and suggestion cleanup.
|
||||
- Strengthened release/build reliability with Node heap settings, split quality lanes, CodeScene threshold updates, and broader test coverage.
|
||||
17
release-notes/v2026-05-07.md
Normal file
17
release-notes/v2026-05-07.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## New Features
|
||||
|
||||
- 💻 **New website!** – available at [Tolaria.md](http://tolaria.md/), now includes thorough docs and universal search with cmd+k
|
||||
- 🤖 **Built-in Agent Docs** — Tolaria now bundles product documentation for local AI agents so guidance is available inside the app.
|
||||
|
||||
## Improvements
|
||||
- 📑 **Table of Contents Shortcut** — Open a note outline more quickly when navigating longer notes, via `cmd+shift+t`
|
||||
- 🌐 **More Reliable AI Setup** — Local model connections, Gemini launch paths, Windows shims, and Linux MCP server discovery are handled more consistently.
|
||||
- 🔗 **Better Attachment and Media Paths** — PDF attachments, vault media links, and shared attachment handling now resolve more predictably across editor flows.
|
||||
- 🎨 **Sharper Editor Details** — Wikilink suggestions, saved-view sort indicators, and type-derived quick-open behavior received focused UI polish.
|
||||
|
||||
## Stability and Fixes
|
||||
- Git remotes now use macOS Keychain credentials more reliably.
|
||||
- Switching vaults clears stale editor state more safely, reducing cross-vault confusion.
|
||||
- Editor rendering is more resilient around stale BlockNote IDs, normalized note openings, and missing whiteboard text measurements.
|
||||
- Release automation now keeps documentation-site updates out of app releases and preserves download assets more reliably.
|
||||
- The contributor instructions now make release-readiness checks explicit for CodeScene, coverage, Codacy, localization, analytics, docs, and QA cleanup.
|
||||
21
release-notes/v2026-05-12.md
Normal file
21
release-notes/v2026-05-12.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## New Features
|
||||
|
||||
- 🧮 **Math Blocks from Slash Commands** — Insert math expressions directly from the editor slash menu.
|
||||
- 🗓️ **Flexible Date Display** — Choose how dates appear across note lists, properties, and inspector views.
|
||||
- 🧭 **Mounted Workspaces** — Bring separate vault workspaces into Tolaria while keeping their Git boundaries intact.
|
||||
- 📂 **Collapsed Sidebar Reopen Button** — Reopen the sidebar more easily after switching to a focused writing layout.
|
||||
|
||||
## Improvements
|
||||
|
||||
- 🖼️ **Better Media and Embed Handling** — Note-relative images, multimedia URLs, file blocks, and rich-editor media reloads behave more consistently.
|
||||
- 🤖 **More Reliable Local Agent Launches** — Claude, Gemini, OpenCode, and MCP server setup now handle modern CLI behavior and Windows paths more gracefully.
|
||||
- 🧱 **Smoother Editor Navigation** — Breadcrumb titles, neighborhood toggles, previous-list recovery, note renaming, and editor reloads now preserve more context.
|
||||
- 🌐 **Cleaner Release and Docs Publishing** — Release pages, GitHub Pages deployment, custom-domain docs, and updater metadata were hardened.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- Whiteboard dialogs, mermaid previews, tldraw context menus, and dark-mode rendering are more resilient.
|
||||
- The editor now guards more stale-selection, stale-block, paste, tab-switch, and save-before-switch edge cases.
|
||||
- Security and quality hardening resolved multiple Codacy findings around mock vault APIs, regexes, object injection, focus handling, and history flows.
|
||||
- Test coverage was expanded for note history edits, telemetry onboarding, rich-editor media reloads, Cmd+N persistence, save-before-switch behavior, and editor draft preservation.
|
||||
- Internal app orchestration, vault setup, inbox advance, and Git file workflow code was split into smaller focused pieces without changing the released user model.
|
||||
17
release-notes/v2026-05-14.md
Normal file
17
release-notes/v2026-05-14.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## New Features
|
||||
|
||||
- 🗂️ **Custom Vault Order** — Reorder your vaults so the switcher and workspace controls match the way you actually work.
|
||||
- 🔌 **Smarter External MCP Setup** — Register Tolaria’s external MCP server with dynamic vault resolution, including mounted-workspace guidance for AppImage users.
|
||||
|
||||
## Improvements
|
||||
|
||||
- ⚡ **Faster Large-Note Opens** — Warm parsed editor blocks ahead of note switches so large notes feel more responsive.
|
||||
- 🧭 **Clearer Vault and Status Controls** — Use cleaner status-bar sections, vault menus, and workspace settings rows when managing multiple vaults.
|
||||
- 🐧 **Stronger Linux AppImage Support** — Improve AppImage launch, packaging, FUSE, fcitx input, and MCP subprocess handling across Linux setups.
|
||||
- 🎨 **More Consistent Interface Icons** — Standardize icons on the Phosphor set for a cleaner and more coherent UI.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- This release fixes relationship equality filters, externally moved inbox notes, selected-vault note creation, and duplicate vault folder pickers.
|
||||
- Editor reliability is improved around pulled changes, file attachments, CJK code-block copying, table handles after reload, Mermaid errors, and procedure editor schema recovery.
|
||||
- Whiteboard dialogs, native commit messages, AutoGit author failures, fullscreen navigation, Linux Wayland safeguards, and release history links were hardened.
|
||||
18
release-notes/v2026-05-18.md
Normal file
18
release-notes/v2026-05-18.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## New Features
|
||||
|
||||
- 🔎 **Faster Note-List Search Controls** — Clear note-list searches directly from the list header while keeping keyboard focus and loading feedback predictable.
|
||||
- 🧰 **More Granular Git Controls** — Use clearer Git actions for local commit, pull, push, and sync workflows without collapsing everything into one path.
|
||||
- 🐧 **Linux RPM Download Option** — Stable download pages can expose RPM packages alongside AppImage and other platform installers when release artifacts include them.
|
||||
|
||||
## Improvements
|
||||
|
||||
- 🧭 **Cleaner Update Release Notes Link** — The in-app update banner now opens the public release notes page instead of the old GitHub Pages root.
|
||||
- 📝 **More Reliable Editor Direction and Spacing** — Mixed right-to-left and left-to-right notes resolve text direction per block, and raw-editor line numbers align more cleanly with content.
|
||||
- 🧩 **Better View Filtering for Array Properties** — Saved views now match scalar array properties such as tags more consistently after reloads and across renderer/Rust evaluation.
|
||||
- 🖼️ **Safer Linux AppImage Media Handling** — AppImage builds avoid unstable in-webview audio/video preview paths while preserving stable external-open fallbacks.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- Rich-editor recovery was hardened for invalid list content, embedded attachment paths, file drops, Mermaid reload clicks, aliased wikilinks in Markdown tables, and empty-heading note edits.
|
||||
- Vault and workspace behavior is steadier around mounted default workspaces, app-owned frontmatter reloads, parsed note-list preload warmups, and built-in note body template removal.
|
||||
- Release build type safety, CodeScene thresholds, image-toolbar hover access, and multiple patch-review findings were addressed before promotion.
|
||||
19
release-notes/v2026-05-21.md
Normal file
19
release-notes/v2026-05-21.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## New Features
|
||||
|
||||
- 🧭 **Expanded Note Context Menus** — Right-click notes for faster everyday actions, including opening in a new window, favorites, organization state, Finder reveal, and file path copy.
|
||||
- 🤖 **More Local AI Agent Options** — Use Kiro as a local AI agent, run Tolaria's MCP server through Bun as well as Node, and launch Pi more reliably from shell-managed installs.
|
||||
- 📚 **Portent Knowledge-Base Template** — Tolaria's docs now include Portent as a reference template for structuring durable, agent-friendly knowledge bases.
|
||||
- 🌍 **Belarusian Language Support** — Use Tolaria with new Belarusian interface translations.
|
||||
|
||||
## Improvements
|
||||
|
||||
- ⚡ **Much Faster Note Windows** — Opening a note in a separate window now uses a lighter startup path, avoiding the full main-window load and reducing app-wide stalls.
|
||||
- 🪟 **More Complete Note Actions** — Note-list actions are now available from the context menu as well as existing command paths, making common file and organization tasks easier to reach.
|
||||
- 🧩 **Cleaner CLI Agent Runtime Detection** — Shared binary discovery was consolidated so local agent launches handle shell-managed runtimes more consistently.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- Note windows now open reliably from all entry points and no longer stall the whole application during startup.
|
||||
- Editor reliability is improved around Go code-block highlighting, Mermaid fullscreen zoom, non-Markdown wikilink targets, active-note refresh after external edits, and retained editor memory.
|
||||
- Vault and workspace behavior is steadier around AutoGit multi-vault pushes, new views in the default workspace, and vault watcher Git symlinks.
|
||||
- Release build type safety, pnpm patched dependency handling, CodeScene thresholds, and multiple patch-review findings were addressed before promotion.
|
||||
19
release-notes/v2026-05-25.md
Normal file
19
release-notes/v2026-05-25.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## New Features
|
||||
|
||||
- 🔎 **Create Notes from Quick Open** — Start a new note directly from Quick Open when your search does not already match an existing note.
|
||||
- 🌓 **Theme-Matched App Icons** — Tolaria can now switch its application icon to match the current light or dark appearance.
|
||||
- 🧭 **Multi-Vault Type Visibility** — Sidebar type controls now distinguish vault and workspace context more clearly when several vaults are open.
|
||||
|
||||
## Improvements
|
||||
|
||||
- 🔄 **Smarter Multi-Vault Git Sync** — Sync actions now target the active Git-backed vaults more consistently, including mounted workspaces.
|
||||
- 🪟 **Better Desktop Window Chrome** — Custom window chrome now exposes the desktop menu more reliably and behaves better on Windows.
|
||||
- 🤖 **More Reliable Local Agent Detection** — Codex, Claude, and OpenCode launch detection handles Windows command shims and Homebrew-style runtime paths more consistently.
|
||||
- ✂️ **Safer Code Block Copying** — Large fenced-code copies are bounded more carefully so editor clipboard actions stay responsive.
|
||||
|
||||
## Stability and Fixes
|
||||
|
||||
- Editor recovery was hardened around stale BlockNote references, invalid table transforms, insertion-depth errors, side-menu actions, emoji suggestions, and selection toolbar placement.
|
||||
- Vault and workspace behavior is steadier around empty startup vault reloads, missing frontmatter write paths, active-note refresh focus, and sidebar type visibility across vaults.
|
||||
- Git and AI workflows are more predictable around selected-repository sync, multi-vault autosync, retained AI panel sessions, and agent target selection.
|
||||
- Status-bar layout, external URL cancellation, light icon assets, release CodeScene thresholds, native QA guidance, and new smoke coverage were addressed before promotion.
|
||||
329
scripts/appimage-launcher-tools.mjs
Normal file
329
scripts/appimage-launcher-tools.mjs
Normal file
@@ -0,0 +1,329 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { error as logError, log } from 'node:console'
|
||||
import { existsSync } from 'node:fs'
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rename,
|
||||
rm,
|
||||
writeFile,
|
||||
} from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export const BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE =
|
||||
'this_dir="$(readlink -f "$(dirname "$0")")"'
|
||||
export const FIXED_LINUXDEPLOY_APPRUN_DIR_LINE =
|
||||
'this_dir="$(dirname "$(readlink -f "$0")")"'
|
||||
export const APPIMAGE_PLUGIN_WRAPPER_NAME = 'linuxdeploy-plugin-appimage.AppImage'
|
||||
export const REAL_APPIMAGE_PLUGIN_NAME =
|
||||
'tolaria-real-linuxdeploy-plugin-appimage/linuxdeploy-plugin-appimage.AppImage'
|
||||
export const APPIMAGE_FCITX_GTK3_IM_MODULE_PATH =
|
||||
'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so'
|
||||
export const APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH =
|
||||
'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2'
|
||||
export const DEFAULT_APPIMAGE_PLUGIN_URL =
|
||||
'https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage'
|
||||
|
||||
const WRAPPER_MARKER = 'Tolaria AppImage symlink launcher shim'
|
||||
const REQUIRED_APPIMAGE_PATHS = [
|
||||
'AppRun',
|
||||
APPIMAGE_FCITX_GTK3_IM_MODULE_PATH,
|
||||
APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH,
|
||||
]
|
||||
|
||||
export function tauriToolsCacheDir(env = process.env) {
|
||||
if (env.TOLARIA_TAURI_TOOLS_DIR) {
|
||||
return resolve(env.TOLARIA_TAURI_TOOLS_DIR)
|
||||
}
|
||||
|
||||
if (env.XDG_CACHE_HOME) {
|
||||
return resolve(env.XDG_CACHE_HOME, 'tauri')
|
||||
}
|
||||
|
||||
if (!env.HOME) {
|
||||
throw new Error('HOME or XDG_CACHE_HOME is required to locate the Tauri tools cache')
|
||||
}
|
||||
|
||||
return resolve(env.HOME, '.cache', 'tauri')
|
||||
}
|
||||
|
||||
export function patchAppRunText(text) {
|
||||
if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) {
|
||||
return {
|
||||
changed: true,
|
||||
text: text.replaceAll(
|
||||
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
|
||||
FIXED_LINUXDEPLOY_APPRUN_DIR_LINE,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return { changed: false, text }
|
||||
}
|
||||
|
||||
export function assertSymlinkSafeAppRunText(text, label = 'AppRun') {
|
||||
if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) {
|
||||
throw new Error(`${label} still resolves dirname before following AppRun symlinks`)
|
||||
}
|
||||
|
||||
if (!text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE)) {
|
||||
throw new Error(`${label} is missing the symlink-safe AppRun directory resolver`)
|
||||
}
|
||||
}
|
||||
|
||||
export function appImagePluginWrapperSource({
|
||||
pluginUrl = DEFAULT_APPIMAGE_PLUGIN_URL,
|
||||
} = {}) {
|
||||
return `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ${WRAPPER_MARKER}
|
||||
PLUGIN_URL="\${TOLARIA_APPIMAGE_PLUGIN_URL:-${pluginUrl}}"
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
REAL_PLUGIN="\${TOLARIA_APPIMAGE_REAL_PLUGIN:-"$SCRIPT_DIR/${REAL_APPIMAGE_PLUGIN_NAME}"}"
|
||||
FCITX_GTK3_IM_MODULE="\${TOLARIA_FCITX_GTK3_IM_MODULE:-/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so}"
|
||||
FCITX_LIBRARY_DIR="\${TOLARIA_FCITX_LIBRARY_DIR:-/usr/lib/x86_64-linux-gnu}"
|
||||
|
||||
appdir_from_args() {
|
||||
local previous=""
|
||||
|
||||
for arg in "$@"; do
|
||||
if [ "$previous" = "--appdir" ]; then
|
||||
printf '%s\\n' "$arg"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$arg" in
|
||||
--appdir=*)
|
||||
printf '%s\\n' "\${arg#--appdir=}"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
previous="$arg"
|
||||
done
|
||||
}
|
||||
|
||||
download_real_plugin() {
|
||||
if [ -x "$REAL_PLUGIN" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp_plugin="$REAL_PLUGIN.tmp.$$"
|
||||
rm -f "$tmp_plugin"
|
||||
mkdir -p "$(dirname -- "$REAL_PLUGIN")"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL -o "$tmp_plugin" "$PLUGIN_URL"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q -O "$tmp_plugin" "$PLUGIN_URL"
|
||||
else
|
||||
echo "curl or wget is required to fetch the real linuxdeploy AppImage output plugin" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod +x "$tmp_plugin"
|
||||
mv "$tmp_plugin" "$REAL_PLUGIN"
|
||||
}
|
||||
|
||||
patch_apprun() {
|
||||
local appdir="\${APPDIR:-}"
|
||||
|
||||
if [ -z "$appdir" ]; then
|
||||
appdir="$(appdir_from_args "$@" || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$appdir" ] || [ ! -f "$appdir/AppRun" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
python3 - "$appdir/AppRun" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
broken = 'this_dir="$(readlink -f "$(dirname "$0")")"'
|
||||
fixed = 'this_dir="$(dirname "$(readlink -f "$0")")"'
|
||||
text = path.read_text(encoding="utf-8")
|
||||
|
||||
if broken in text:
|
||||
path.write_text(text.replace(broken, fixed), encoding="utf-8")
|
||||
print(f"Patched linuxdeploy AppRun symlink resolution in {path}", file=sys.stderr)
|
||||
elif fixed in text:
|
||||
pass
|
||||
elif "autogenerated by linuxdeploy" in text and "AppRun.wrapped" in text:
|
||||
raise SystemExit(f"{path} is a linuxdeploy wrapper but does not contain the expected AppRun resolver")
|
||||
PY
|
||||
}
|
||||
|
||||
bundle_fcitx_gtk3_module() {
|
||||
local appdir="\${APPDIR:-}"
|
||||
|
||||
if [ -z "$appdir" ]; then
|
||||
appdir="$(appdir_from_args "$@" || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$appdir" ] || [ ! -d "$appdir" ] || [ ! -f "$FCITX_GTK3_IM_MODULE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local module_dest="$appdir/${APPIMAGE_FCITX_GTK3_IM_MODULE_PATH}"
|
||||
local library_dest_dir="$appdir/usr/lib/x86_64-linux-gnu"
|
||||
|
||||
mkdir -p "$(dirname -- "$module_dest")" "$library_dest_dir"
|
||||
cp -a "$FCITX_GTK3_IM_MODULE" "$module_dest"
|
||||
|
||||
local copied_library=0
|
||||
shopt -s nullglob
|
||||
for lib in "$FCITX_LIBRARY_DIR"/libFcitx5GClient.so* "$FCITX_LIBRARY_DIR"/libFcitx5Utils.so*; do
|
||||
cp -a "$lib" "$library_dest_dir/"
|
||||
copied_library=1
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
if [ "$copied_library" -eq 0 ]; then
|
||||
echo "No fcitx GTK client libraries found in $FCITX_LIBRARY_DIR" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
download_real_plugin
|
||||
patch_apprun "$@"
|
||||
bundle_fcitx_gtk3_module "$@"
|
||||
exec "$REAL_PLUGIN" "$@"
|
||||
`
|
||||
}
|
||||
|
||||
export async function preparePluginWrapper({
|
||||
env = process.env,
|
||||
toolsDir = tauriToolsCacheDir(env),
|
||||
} = {}) {
|
||||
await mkdir(toolsDir, { recursive: true })
|
||||
|
||||
const wrapperPath = join(toolsDir, APPIMAGE_PLUGIN_WRAPPER_NAME)
|
||||
const realPluginPath = join(toolsDir, REAL_APPIMAGE_PLUGIN_NAME)
|
||||
|
||||
if (existsSync(wrapperPath) && !existsSync(realPluginPath)) {
|
||||
const existing = await readFile(wrapperPath, 'utf8').catch(() => '')
|
||||
if (!existing.includes(WRAPPER_MARKER)) {
|
||||
await mkdir(dirname(realPluginPath), { recursive: true })
|
||||
await rename(wrapperPath, realPluginPath)
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(wrapperPath, appImagePluginWrapperSource(), 'utf8')
|
||||
await chmod(wrapperPath, 0o755)
|
||||
|
||||
return { realPluginPath, wrapperPath }
|
||||
}
|
||||
|
||||
export async function validateAppRunFile(path) {
|
||||
const text = await readFile(path, 'utf8')
|
||||
assertSymlinkSafeAppRunText(text, path)
|
||||
}
|
||||
|
||||
function extractAppImagePath(appImage, requiredPath, tempDir) {
|
||||
const result = spawnSync(appImage, ['--appimage-extract', requiredPath], {
|
||||
cwd: tempDir,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
|
||||
if (result.status === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
`Failed to extract ${requiredPath} from ${appImage}`,
|
||||
result.stdout.trim(),
|
||||
result.stderr.trim(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
)
|
||||
}
|
||||
|
||||
function assertAppImagePathsExtracted(appImage, tempDir, requiredPaths) {
|
||||
for (const requiredPath of requiredPaths) {
|
||||
const extractedPath = join(tempDir, 'squashfs-root', requiredPath)
|
||||
if (!existsSync(extractedPath)) {
|
||||
throw new Error(`${appImage} is missing ${requiredPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function validateExtractedAppImage(appImage, tempDir) {
|
||||
for (const requiredPath of REQUIRED_APPIMAGE_PATHS) {
|
||||
extractAppImagePath(appImage, requiredPath, tempDir)
|
||||
}
|
||||
|
||||
await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun'))
|
||||
assertAppImagePathsExtracted(appImage, tempDir, REQUIRED_APPIMAGE_PATHS.slice(1))
|
||||
}
|
||||
|
||||
export async function validateAppImages(paths) {
|
||||
if (paths.length === 0) {
|
||||
throw new Error('At least one AppImage path is required for launcher validation')
|
||||
}
|
||||
|
||||
for (const appImage of paths.map((path) => resolve(path))) {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'tolaria-appimage-'))
|
||||
try {
|
||||
await validateExtractedAppImage(appImage, tempDir)
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function preparePluginCommand() {
|
||||
const { wrapperPath, realPluginPath } = await preparePluginWrapper()
|
||||
log(`Prepared ${wrapperPath}`)
|
||||
log(`Real plugin cache: ${realPluginPath}`)
|
||||
}
|
||||
|
||||
async function validateAppRunFilesCommand(paths) {
|
||||
for (const path of paths) {
|
||||
await validateAppRunFile(path)
|
||||
log(`Validated ${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAppImagesCommand(paths) {
|
||||
await validateAppImages(paths)
|
||||
for (const path of paths) {
|
||||
log(`Validated AppImage launcher in ${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
const COMMANDS = new Map([
|
||||
['prepare-plugin', preparePluginCommand],
|
||||
['validate-apprun-file', validateAppRunFilesCommand],
|
||||
['validate-appimages', validateAppImagesCommand],
|
||||
])
|
||||
|
||||
function usage() {
|
||||
return 'Usage: node scripts/appimage-launcher-tools.mjs prepare-plugin | validate-apprun-file <AppRun...> | validate-appimages <AppImage...>'
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [command, ...args] = process.argv.slice(2)
|
||||
const handler = COMMANDS.get(command)
|
||||
|
||||
if (!handler) {
|
||||
throw new Error(usage())
|
||||
}
|
||||
|
||||
await handler(args)
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
logError(error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
182
scripts/appimage-launcher-tools.test.mjs
Normal file
182
scripts/appimage-launcher-tools.test.mjs
Normal file
@@ -0,0 +1,182 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, realpathSync } from 'node:fs'
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import process from 'node:process'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
|
||||
FIXED_LINUXDEPLOY_APPRUN_DIR_LINE,
|
||||
REAL_APPIMAGE_PLUGIN_NAME,
|
||||
appImagePluginWrapperSource,
|
||||
patchAppRunText,
|
||||
preparePluginWrapper,
|
||||
} from './appimage-launcher-tools.mjs'
|
||||
|
||||
function brokenResolverDir(invokedPath) {
|
||||
return realpathSync(dirname(invokedPath))
|
||||
}
|
||||
|
||||
function fixedResolverDir(invokedPath) {
|
||||
return dirname(realpathSync(invokedPath))
|
||||
}
|
||||
|
||||
test('patches linuxdeploy AppRun wrapper to resolve the invoked path before dirname', () => {
|
||||
const original = [
|
||||
'#! /usr/bin/env bash',
|
||||
'# autogenerated by linuxdeploy',
|
||||
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
|
||||
'exec "$this_dir"/AppRun.wrapped "$@"',
|
||||
].join('\n')
|
||||
|
||||
const patched = patchAppRunText(original)
|
||||
|
||||
assert.equal(patched.changed, true)
|
||||
assert.equal(patched.text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false)
|
||||
assert.equal(patched.text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true)
|
||||
})
|
||||
|
||||
test('fixed resolver follows absolute and relative symlinks before choosing AppDir', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'tolaria-apprun-resolver-'))
|
||||
const appDir = join(root, 'Tolaria.AppDir')
|
||||
const binDir = join(root, 'bin')
|
||||
const relativeDir = join(root, 'relative-bin')
|
||||
const appRun = join(appDir, 'AppRun')
|
||||
|
||||
await mkdir(appDir)
|
||||
await mkdir(binDir)
|
||||
await mkdir(relativeDir)
|
||||
await writeFile(appRun, '#! /usr/bin/env bash\n', 'utf8')
|
||||
|
||||
const absoluteSymlink = join(binDir, 'tolaria')
|
||||
const relativeSymlink = join(relativeDir, 'tolaria')
|
||||
|
||||
await symlink(appRun, absoluteSymlink)
|
||||
await symlink(`../${basename(appDir)}/AppRun`, relativeSymlink)
|
||||
|
||||
assert.equal(brokenResolverDir(absoluteSymlink), realpathSync(binDir))
|
||||
assert.equal(fixedResolverDir(absoluteSymlink), realpathSync(appDir))
|
||||
assert.equal(brokenResolverDir(relativeSymlink), realpathSync(relativeDir))
|
||||
assert.equal(fixedResolverDir(relativeSymlink), realpathSync(appDir))
|
||||
})
|
||||
|
||||
test('plugin wrapper patches AppRun before delegating to the real output plugin', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-plugin-'))
|
||||
const appDir = join(root, 'Tolaria.AppDir')
|
||||
const appRun = join(appDir, 'AppRun')
|
||||
const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage')
|
||||
const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage')
|
||||
const pluginMarker = join(root, 'plugin-ran')
|
||||
|
||||
await mkdir(appDir)
|
||||
await writeFile(
|
||||
appRun,
|
||||
[
|
||||
'#! /usr/bin/env bash',
|
||||
'# autogenerated by linuxdeploy',
|
||||
BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
|
||||
'exec "$this_dir"/AppRun.wrapped "$@"',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
)
|
||||
await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8')
|
||||
await chmod(wrapper, 0o755)
|
||||
await writeFile(
|
||||
realPlugin,
|
||||
`#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`,
|
||||
'utf8',
|
||||
)
|
||||
await chmod(realPlugin, 0o755)
|
||||
|
||||
const result = spawnSync(wrapper, [], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
APPDIR: appDir,
|
||||
TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin,
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(result.status, 0, result.stderr)
|
||||
assert.equal(existsSync(pluginMarker), true)
|
||||
|
||||
const patched = await readFile(appRun, 'utf8')
|
||||
assert.equal(patched.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false)
|
||||
assert.equal(patched.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true)
|
||||
})
|
||||
|
||||
test('plugin wrapper keeps the delegated appimage plugin basename canonical', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-tools-'))
|
||||
const { realPluginPath, wrapperPath } = await preparePluginWrapper({
|
||||
toolsDir: root,
|
||||
})
|
||||
|
||||
assert.equal(basename(wrapperPath), 'linuxdeploy-plugin-appimage.AppImage')
|
||||
assert.equal(
|
||||
realPluginPath,
|
||||
join(root, 'tolaria-real-linuxdeploy-plugin-appimage', 'linuxdeploy-plugin-appimage.AppImage'),
|
||||
)
|
||||
assert.equal(REAL_APPIMAGE_PLUGIN_NAME.endsWith('/linuxdeploy-plugin-appimage.AppImage'), true)
|
||||
|
||||
const wrapper = await readFile(wrapperPath, 'utf8')
|
||||
assert.equal(wrapper.includes('linuxdeploy-plugin-appimage.real.AppImage'), false)
|
||||
})
|
||||
|
||||
test('plugin wrapper bundles fcitx GTK3 input module before sealing AppImage', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-fcitx-'))
|
||||
const appDir = join(root, 'Tolaria.AppDir')
|
||||
const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage')
|
||||
const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage')
|
||||
const pluginMarker = join(root, 'plugin-ran')
|
||||
const hostModule = join(root, 'host', 'im-fcitx5.so')
|
||||
const hostLibraryDir = join(root, 'host-lib')
|
||||
const hostLibrary = join(hostLibraryDir, 'libFcitx5GClient.so.2')
|
||||
|
||||
await mkdir(appDir)
|
||||
await mkdir(dirname(hostModule), { recursive: true })
|
||||
await mkdir(hostLibraryDir)
|
||||
await writeFile(hostModule, 'fake fcitx gtk module', 'utf8')
|
||||
await writeFile(hostLibrary, 'fake fcitx client library', 'utf8')
|
||||
await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8')
|
||||
await chmod(wrapper, 0o755)
|
||||
await writeFile(
|
||||
realPlugin,
|
||||
`#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`,
|
||||
'utf8',
|
||||
)
|
||||
await chmod(realPlugin, 0o755)
|
||||
|
||||
const result = spawnSync(wrapper, [], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
APPDIR: appDir,
|
||||
TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin,
|
||||
TOLARIA_FCITX_GTK3_IM_MODULE: hostModule,
|
||||
TOLARIA_FCITX_LIBRARY_DIR: hostLibraryDir,
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(result.status, 0, result.stderr)
|
||||
assert.equal(existsSync(pluginMarker), true)
|
||||
assert.equal(
|
||||
existsSync(
|
||||
join(
|
||||
appDir,
|
||||
'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so',
|
||||
),
|
||||
),
|
||||
true,
|
||||
)
|
||||
assert.equal(existsSync(join(appDir, 'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2')), true)
|
||||
})
|
||||
197
scripts/build-agent-docs.mjs
Normal file
197
scripts/build-agent-docs.mjs
Normal file
@@ -0,0 +1,197 @@
|
||||
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, '..')
|
||||
const siteRoot = path.join(repoRoot, 'site')
|
||||
const outputRoot = path.join(repoRoot, 'src-tauri', 'resources', 'agent-docs')
|
||||
|
||||
const sectionOrder = ['start', 'concepts', 'guides', 'templates', 'reference', 'troubleshooting', 'download', 'releases']
|
||||
const ignoredDirs = new Set(['.vitepress', 'public', 'node_modules', '.DS_Store'])
|
||||
|
||||
function titleFromSlug(slug) {
|
||||
return slug
|
||||
.replace(/[-_]+/g, ' ')
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
function stripFrontmatter(markdown) {
|
||||
return markdown.replace(/^---\n[\s\S]*?\n---\n/, '')
|
||||
}
|
||||
|
||||
function firstHeading(markdown, fallback) {
|
||||
const match = markdown.match(/^#\s+(.+)$/m)
|
||||
return match?.[1]?.trim() || fallback
|
||||
}
|
||||
|
||||
export function normalizeDocPath(relativePath) {
|
||||
return relativePath.replaceAll(path.win32.sep, '/')
|
||||
}
|
||||
|
||||
export function sectionForFile(relativePath) {
|
||||
const [firstPart] = relativePath.split('/')
|
||||
if (firstPart === 'index.md') return 'home'
|
||||
return firstPart.replace(/\.md$/, '')
|
||||
}
|
||||
|
||||
async function listMarkdownFiles(dir, base = dir) {
|
||||
const entries = await readdir(dir, { withFileTypes: true })
|
||||
const files = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (ignoredDirs.has(entry.name)) continue
|
||||
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await listMarkdownFiles(fullPath, base))
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
files.push(normalizeDocPath(path.relative(base, fullPath)))
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
function sortDocs(files) {
|
||||
return files.sort((a, b) => {
|
||||
const sectionDiff = sectionOrder.indexOf(sectionForFile(a)) - sectionOrder.indexOf(sectionForFile(b))
|
||||
if (sectionDiff !== 0) return sectionDiff
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
}
|
||||
|
||||
function docUrl(relativePath) {
|
||||
const withoutExt = relativePath.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '')
|
||||
return `/${withoutExt}`.replace(/\/$/, '/') || '/'
|
||||
}
|
||||
|
||||
function formatDoc(doc) {
|
||||
return `# ${doc.title}\n\nSource: ${doc.path}\nURL: ${doc.url}\n\n${doc.content}`
|
||||
}
|
||||
|
||||
function groupDocsBySection(docs) {
|
||||
const bySection = new Map()
|
||||
|
||||
for (const doc of docs) {
|
||||
const docsInSection = bySection.get(doc.section) ?? []
|
||||
docsInSection.push(doc)
|
||||
bySection.set(doc.section, docsInSection)
|
||||
}
|
||||
|
||||
return bySection
|
||||
}
|
||||
|
||||
function buildIndex(docs) {
|
||||
const bySection = groupDocsBySection(docs)
|
||||
const lines = [
|
||||
'# Tolaria Agent Docs',
|
||||
'',
|
||||
'These docs are generated from the public Tolaria documentation for local AI agent lookup.',
|
||||
'',
|
||||
'Start here, then use `rg` over this folder for specific Tolaria concepts and workflows.',
|
||||
'',
|
||||
'When users ask how to improve a knowledge base, structure notes, choose types, model relationships, or make a vault easier for humans and agents to use, treat Portent as Tolaria\'s default best-practice model. Read `pages/templates/portent.md` and combine it with the Tolaria concepts for types, relationships, properties, Inbox, archive, and custom views.',
|
||||
'',
|
||||
]
|
||||
|
||||
for (const section of ['home', ...sectionOrder]) {
|
||||
const docsInSection = bySection.get(section)
|
||||
if (!docsInSection?.length) continue
|
||||
|
||||
lines.push(`## ${titleFromSlug(section)}`, '')
|
||||
for (const doc of docsInSection) {
|
||||
lines.push(`- [${doc.title}](pages/${doc.path})`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
lines.push('## Generated Files', '')
|
||||
lines.push('- `all.md`: all public docs concatenated for fast full-context reads.')
|
||||
lines.push('- `search-index.json`: title, heading, section, path, and URL metadata for quick routing.')
|
||||
lines.push('- `<section>.md`: one compact bundle per docs section.')
|
||||
lines.push('- `pages/`: one generated Markdown file per public docs page.')
|
||||
lines.push('')
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildAgentInstructions() {
|
||||
return `# AGENTS.md - Tolaria Docs Bundle
|
||||
|
||||
This folder contains local, generated Tolaria product docs for AI agents.
|
||||
|
||||
Use these docs when a user asks how Tolaria works, when you need product behavior, or before making Tolaria-specific assumptions.
|
||||
|
||||
Portent is the default best-practice model for structuring Tolaria knowledge bases. When a user asks how to improve a knowledge base, make it better organized, choose better types, model relationships, or make the vault easier for humans and agents to use, read \`pages/templates/portent.md\` and combine it with Tolaria's docs for types, relationships, properties, Inbox, archive, and custom views.
|
||||
|
||||
Recommended lookup flow:
|
||||
|
||||
1. Read the active vault's AGENTS.md for vault-specific conventions.
|
||||
2. Read this folder's index.md for the docs map.
|
||||
3. Use \`rg\` over this folder for advanced concepts, workflows, shortcuts, Git, AutoGit, AI, Portent, types, properties, relationships, and troubleshooting.
|
||||
|
||||
Vault-specific AGENTS.md wins for local conventions. These bundled docs win for Tolaria product behavior.
|
||||
`
|
||||
}
|
||||
|
||||
function searchIndexFor(doc) {
|
||||
const headings = [...doc.content.matchAll(/^#{2,3}\s+(.+)$/gm)].map((match) => match[1].trim())
|
||||
return {
|
||||
title: doc.title,
|
||||
path: `pages/${doc.path}`,
|
||||
url: doc.url,
|
||||
section: doc.section,
|
||||
headings,
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = sortDocs(await listMarkdownFiles(siteRoot))
|
||||
const docs = []
|
||||
|
||||
for (const relativePath of files) {
|
||||
const raw = await readFile(path.join(siteRoot, relativePath), 'utf8')
|
||||
const content = stripFrontmatter(raw).trim()
|
||||
const fallbackTitle = titleFromSlug(path.basename(relativePath, '.md'))
|
||||
docs.push({
|
||||
content,
|
||||
path: relativePath,
|
||||
section: sectionForFile(relativePath),
|
||||
title: firstHeading(content, fallbackTitle),
|
||||
url: docUrl(relativePath),
|
||||
})
|
||||
}
|
||||
|
||||
await rm(outputRoot, { force: true, recursive: true })
|
||||
await mkdir(outputRoot, { recursive: true })
|
||||
|
||||
await writeFile(path.join(outputRoot, 'AGENTS.md'), buildAgentInstructions())
|
||||
await writeFile(path.join(outputRoot, 'index.md'), buildIndex(docs))
|
||||
await writeFile(path.join(outputRoot, 'all.md'), docs.map(formatDoc).join('\n\n---\n\n'))
|
||||
await writeFile(path.join(outputRoot, 'search-index.json'), `${JSON.stringify(docs.map(searchIndexFor), null, 2)}\n`)
|
||||
|
||||
for (const doc of docs) {
|
||||
const outputPath = path.join(outputRoot, 'pages', doc.path)
|
||||
await mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, formatDoc(doc))
|
||||
}
|
||||
|
||||
const bySection = groupDocsBySection(docs)
|
||||
for (const [section, docsInSection] of bySection) {
|
||||
await writeFile(
|
||||
path.join(outputRoot, `${section}.md`),
|
||||
docsInSection.map(formatDoc).join('\n\n---\n\n'),
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Generated ${docs.length} agent docs in ${path.relative(repoRoot, outputRoot)}`)
|
||||
}
|
||||
|
||||
const entrypointUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : ''
|
||||
|
||||
if (import.meta.url === entrypointUrl) {
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
11
scripts/build-agent-docs.test.mjs
Normal file
11
scripts/build-agent-docs.test.mjs
Normal file
@@ -0,0 +1,11 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { normalizeDocPath, sectionForFile } from './build-agent-docs.mjs'
|
||||
|
||||
test('normalizes Windows doc paths before section grouping', () => {
|
||||
const docPath = normalizeDocPath('concepts\\ai.md')
|
||||
|
||||
assert.equal(docPath, 'concepts/ai.md')
|
||||
assert.equal(sectionForFile(docPath), 'concepts')
|
||||
})
|
||||
@@ -17,12 +17,16 @@ const hasMaxWorkersOverride = forwardedArgs.some((arg) =>
|
||||
)
|
||||
const maxAttempts = 2
|
||||
|
||||
// Standalone pnpm installs ship a native binary, so npm_execpath points at
|
||||
// a Mach-O/ELF executable. Only reuse process.execPath (node) when the path
|
||||
// is something node can actually load as a module.
|
||||
const packageManagerExec = process.env.npm_execpath
|
||||
const command = packageManagerExec ? process.execPath : 'pnpm'
|
||||
const baseCommandArgs = packageManagerExec
|
||||
const isJsExecpath = packageManagerExec && /\.[mc]?js$/i.test(packageManagerExec)
|
||||
const command = isJsExecpath ? process.execPath : 'pnpm'
|
||||
const baseCommandArgs = isJsExecpath
|
||||
? [packageManagerExec, 'exec', 'vitest', 'run', '--coverage']
|
||||
: ['exec', 'vitest', 'run', '--coverage']
|
||||
const clearCacheCommandArgs = packageManagerExec
|
||||
const clearCacheCommandArgs = isJsExecpath
|
||||
? [packageManagerExec, 'exec', 'vitest', '--clearCache']
|
||||
: ['exec', 'vitest', '--clearCache']
|
||||
|
||||
|
||||
115
site/.vitepress/config.ts
Normal file
115
site/.vitepress/config.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
const base = process.env.VITEPRESS_BASE ?? "/";
|
||||
|
||||
export default defineConfig({
|
||||
title: "Tolaria",
|
||||
description:
|
||||
"Tolaria is a local-first Markdown knowledge base with native relationships, Git history, and AI workflows.",
|
||||
base,
|
||||
ignoreDeadLinks: [/^\/download\/?(?:index)?$/, /^\/releases\/?(?:index)?$/],
|
||||
cleanUrls: true,
|
||||
head: [
|
||||
["link", { rel: "icon", type: "image/png", href: `${base}landing/favicon.png` }],
|
||||
["meta", { property: "og:title", content: "Tolaria" }],
|
||||
[
|
||||
"meta",
|
||||
{
|
||||
property: "og:description",
|
||||
content:
|
||||
"A second brain for the AI era. Free forever, local-first, Markdown-based, Git-ready, and AI-friendly.",
|
||||
},
|
||||
],
|
||||
],
|
||||
themeConfig: {
|
||||
logo: { src: "/landing/tolaria-icon.png", alt: "Tolaria" },
|
||||
nav: [
|
||||
{ text: "Start", link: "/start/install" },
|
||||
{ text: "Concepts", link: "/concepts/vaults" },
|
||||
{ text: "Guides", link: "/guides/capture-a-note" },
|
||||
{ text: "Templates", link: "/templates/portent" },
|
||||
{ text: "Downloads", link: "https://tolaria.md/download/", target: "_self", noIcon: true },
|
||||
],
|
||||
search: {
|
||||
provider: "local",
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
text: "Start Here",
|
||||
items: [
|
||||
{ text: "Install Tolaria", link: "/start/install" },
|
||||
{ text: "First Launch", link: "/start/first-launch" },
|
||||
{ text: "Getting Started Vault", link: "/start/getting-started-vault" },
|
||||
{ text: "Open Or Create A Vault", link: "/start/open-or-create-vault" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Concepts",
|
||||
items: [
|
||||
{ text: "Vaults", link: "/concepts/vaults" },
|
||||
{ text: "Notes", link: "/concepts/notes" },
|
||||
{ text: "Editor", link: "/concepts/editor" },
|
||||
{ text: "Properties", link: "/concepts/properties" },
|
||||
{ text: "Types", link: "/concepts/types" },
|
||||
{ text: "Relationships", link: "/concepts/relationships" },
|
||||
{ text: "Files And Media", link: "/concepts/files-and-media" },
|
||||
{ text: "Inbox", link: "/concepts/inbox" },
|
||||
{ text: "Git", link: "/concepts/git" },
|
||||
{ text: "AI", link: "/concepts/ai" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Guides",
|
||||
items: [
|
||||
{ text: "Capture A Note", link: "/guides/capture-a-note" },
|
||||
{ text: "Organize The Inbox", link: "/guides/organize-inbox" },
|
||||
{ text: "Use Wikilinks", link: "/guides/use-wikilinks" },
|
||||
{ text: "Create Types", link: "/guides/create-types" },
|
||||
{ text: "Build Custom Views", link: "/guides/build-custom-views" },
|
||||
{ text: "Connect A Git Remote", link: "/guides/connect-a-git-remote" },
|
||||
{ text: "Manage Git", link: "/guides/commit-and-push" },
|
||||
{ text: "Use The AI", link: "/guides/use-ai-panel" },
|
||||
{ text: "Configure AI Models", link: "/guides/configure-ai-models" },
|
||||
{ text: "Use The Table Of Contents", link: "/guides/use-table-of-contents" },
|
||||
{ text: "Use Media Previews", link: "/guides/use-media-previews" },
|
||||
{ text: "Manage Display Preferences", link: "/guides/manage-display-preferences" },
|
||||
{ text: "Use The Command Palette", link: "/guides/use-command-palette" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Templates",
|
||||
items: [
|
||||
{ text: "Portent", link: "/templates/portent" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Reference",
|
||||
items: [
|
||||
{ text: "Supported Platforms", link: "/reference/supported-platforms" },
|
||||
{ text: "File Layout", link: "/reference/file-layout" },
|
||||
{ text: "Frontmatter Fields", link: "/reference/frontmatter-fields" },
|
||||
{ text: "View Filters", link: "/reference/view-filters" },
|
||||
{ text: "Keyboard Shortcuts", link: "/reference/keyboard-shortcuts" },
|
||||
{ text: "Release Channels", link: "/reference/release-channels" },
|
||||
{ text: "Contribute", link: "/reference/contribute" },
|
||||
{ text: "Docs Maintenance", link: "/reference/docs-maintenance" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
items: [
|
||||
{ text: "Vault Not Loading", link: "/troubleshooting/vault-not-loading" },
|
||||
{ text: "Git Authentication", link: "/troubleshooting/git-auth" },
|
||||
{ text: "AI Agent Not Found", link: "/troubleshooting/ai-agent-not-found" },
|
||||
{ text: "Model Provider Connection", link: "/troubleshooting/model-provider-connection" },
|
||||
{ text: "Sync Conflicts", link: "/troubleshooting/sync-conflicts" },
|
||||
],
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
message: "Free and open source. Local-first, Git-first, and Markdown-based.",
|
||||
copyright:
|
||||
"Tolaria is AGPL-3.0-or-later. The Tolaria name and logo remain covered by the project trademark policy.",
|
||||
},
|
||||
},
|
||||
});
|
||||
1119
site/.vitepress/theme/LandingHome.vue
Normal file
1119
site/.vitepress/theme/LandingHome.vue
Normal file
File diff suppressed because it is too large
Load Diff
212
site/.vitepress/theme/Layout.vue
Normal file
212
site/.vitepress/theme/Layout.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import DefaultTheme from "vitepress/theme";
|
||||
import { onBeforeUnmount, onMounted, ref, watchEffect } from "vue";
|
||||
import { useData } from "vitepress";
|
||||
|
||||
const { frontmatter } = useData();
|
||||
const fallbackGithubStars = "9,946";
|
||||
const githubStars = ref(fallbackGithubStars);
|
||||
const githubStarsCacheKey = "tolaria:github-stars";
|
||||
const githubStarsCacheTtlMs = 60 * 60 * 1000;
|
||||
const githubRepoApiUrl = "https://api.github.com/repos/refactoringhq/tolaria";
|
||||
|
||||
type GithubStarsCache = {
|
||||
stars: number;
|
||||
savedAt: number;
|
||||
};
|
||||
|
||||
const formatGithubStars = (stars: number) =>
|
||||
new Intl.NumberFormat("en-US").format(stars);
|
||||
|
||||
const scrollClass = "tolaria-scrolled";
|
||||
const landingPageClass = "tolaria-landing-page";
|
||||
const updateScrollClass = () => {
|
||||
document.documentElement.classList.toggle(scrollClass, window.scrollY > 8);
|
||||
};
|
||||
|
||||
const readCachedGithubStars = (): GithubStarsCache | null => {
|
||||
try {
|
||||
const rawCache = window.localStorage.getItem(githubStarsCacheKey);
|
||||
if (!rawCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedCache = JSON.parse(rawCache) as Partial<GithubStarsCache>;
|
||||
if (
|
||||
typeof parsedCache.stars !== "number" ||
|
||||
typeof parsedCache.savedAt !== "number" ||
|
||||
!Number.isFinite(parsedCache.stars) ||
|
||||
!Number.isFinite(parsedCache.savedAt)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
stars: parsedCache.stars,
|
||||
savedAt: parsedCache.savedAt,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const updateGithubStars = async () => {
|
||||
const cachedStars = readCachedGithubStars();
|
||||
if (cachedStars) {
|
||||
githubStars.value = formatGithubStars(cachedStars.stars);
|
||||
if (Date.now() - cachedStars.savedAt < githubStarsCacheTtlMs) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(githubRepoApiUrl, {
|
||||
headers: { Accept: "application/vnd.github+json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = (await response.json()) as { stargazers_count?: unknown };
|
||||
if (
|
||||
typeof repo.stargazers_count !== "number" ||
|
||||
!Number.isFinite(repo.stargazers_count)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
githubStarsCacheKey,
|
||||
JSON.stringify({
|
||||
stars: repo.stargazers_count,
|
||||
savedAt: Date.now(),
|
||||
} satisfies GithubStarsCache),
|
||||
);
|
||||
githubStars.value = formatGithubStars(repo.stargazers_count);
|
||||
} catch {
|
||||
// Keep the cached or bundled fallback count.
|
||||
}
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.classList.toggle(
|
||||
landingPageClass,
|
||||
Boolean(frontmatter.value.landing),
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
updateScrollClass();
|
||||
void updateGithubStars();
|
||||
window.addEventListener("scroll", updateScrollClass, { passive: true });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("scroll", updateScrollClass);
|
||||
document.documentElement.classList.remove(scrollClass);
|
||||
document.documentElement.classList.remove(landingPageClass);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'tolaria-landing-shell': frontmatter.landing }">
|
||||
<DefaultTheme.Layout>
|
||||
<template #nav-bar-content-after>
|
||||
<a
|
||||
class="github-star-widget"
|
||||
href="https://github.com/refactoringhq/tolaria"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:aria-label="`${githubStars} GitHub stars`"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 .5C5.65.5.5 5.65.5 12c0 5.09 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.54-3.88-1.54-.52-1.33-1.28-1.68-1.28-1.68-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.55-.29-5.23-1.28-5.23-5.68 0-1.25.45-2.28 1.19-3.08-.12-.29-.52-1.46.11-3.04 0 0 .97-.31 3.17 1.18A11 11 0 0 1 12 5.53c.98 0 1.97.13 2.89.39 2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.04.74.8 1.19 1.83 1.19 3.08 0 4.41-2.69 5.38-5.25 5.67.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.56A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Star</span>
|
||||
<strong>{{ githubStars }}</strong>
|
||||
</a>
|
||||
</template>
|
||||
</DefaultTheme.Layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.github-star-widget {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 34px;
|
||||
margin-left: 8px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 7px;
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
background-color 160ms ease,
|
||||
color 160ms ease;
|
||||
}
|
||||
|
||||
.github-star-widget:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
border-color: color-mix(in srgb, var(--vp-c-brand-1) 38%, var(--vp-c-border));
|
||||
}
|
||||
|
||||
.github-star-widget svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.github-star-widget strong {
|
||||
padding-left: 7px;
|
||||
border-left: 1px solid var(--vp-c-border);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.github-star-widget {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
:global(.VPNavBar .appearance) {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.github-star-widget {
|
||||
height: 32px;
|
||||
margin-left: 4px;
|
||||
padding: 0 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.github-star-widget svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.github-star-widget span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.github-star-widget strong {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
site/.vitepress/theme/assets/RefactoringSans.otf
Normal file
BIN
site/.vitepress/theme/assets/RefactoringSans.otf
Normal file
Binary file not shown.
12
site/.vitepress/theme/index.ts
Normal file
12
site/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import DefaultTheme from "vitepress/theme";
|
||||
import LandingHome from "./LandingHome.vue";
|
||||
import Layout from "./Layout.vue";
|
||||
import "./styles.css";
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app }) {
|
||||
app.component("LandingHome", LandingHome);
|
||||
},
|
||||
};
|
||||
240
site/.vitepress/theme/styles.css
Normal file
240
site/.vitepress/theme/styles.css
Normal file
@@ -0,0 +1,240 @@
|
||||
@font-face {
|
||||
font-family: "RefactoringSans";
|
||||
src: url("./assets/RefactoringSans.otf") format("opentype");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400 900;
|
||||
}
|
||||
|
||||
:root {
|
||||
--vp-font-family-base:
|
||||
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--vp-font-family-mono: "SF Mono", "Fira Code", ui-monospace, monospace;
|
||||
--tolaria-font-brand:
|
||||
"RefactoringSans", "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--tolaria-bg: #faf9f5;
|
||||
--tolaria-surface: #ffffff;
|
||||
--tolaria-surface-muted: #f7f6f3;
|
||||
--tolaria-text: #1a1a18;
|
||||
--tolaria-text-secondary: #6b6b60;
|
||||
--tolaria-text-muted: #9b9b90;
|
||||
--tolaria-border: #e5e5e0;
|
||||
--tolaria-blue: #155dff;
|
||||
--tolaria-blue-hover: #4a5ad6;
|
||||
--tolaria-blue-soft: #e8eeff;
|
||||
--vp-c-bg: var(--tolaria-surface);
|
||||
--vp-c-bg-alt: var(--tolaria-surface-muted);
|
||||
--vp-c-bg-elv: var(--tolaria-surface);
|
||||
--vp-c-bg-soft: var(--tolaria-surface-muted);
|
||||
--vp-c-text-1: var(--tolaria-text);
|
||||
--vp-c-text-2: var(--tolaria-text-secondary);
|
||||
--vp-c-text-3: var(--tolaria-text-muted);
|
||||
--vp-c-border: var(--tolaria-border);
|
||||
--vp-c-divider: var(--tolaria-border);
|
||||
--vp-c-brand-1: var(--tolaria-blue);
|
||||
--vp-c-brand-2: var(--tolaria-blue-hover);
|
||||
--vp-c-brand-3: var(--tolaria-blue);
|
||||
--vp-c-brand-soft: var(--tolaria-blue-soft);
|
||||
--vp-button-brand-bg: var(--tolaria-blue);
|
||||
--vp-button-brand-hover-bg: var(--tolaria-blue-hover);
|
||||
--vp-button-brand-border: var(--tolaria-blue);
|
||||
--vp-code-bg: #eeeeea;
|
||||
--vp-code-color: var(--tolaria-text-secondary);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--tolaria-bg: #1f1e1b;
|
||||
--tolaria-surface: #23221f;
|
||||
--tolaria-surface-muted: #191814;
|
||||
--tolaria-text: #e6e1d8;
|
||||
--tolaria-text-secondary: #b8b1a6;
|
||||
--tolaria-text-muted: #7f776d;
|
||||
--tolaria-border: #34322d;
|
||||
--tolaria-blue: #78a4ff;
|
||||
--tolaria-blue-hover: #9bbeff;
|
||||
--tolaria-blue-soft: rgba(120, 164, 255, 0.16);
|
||||
--vp-c-bg: #1f1e1b;
|
||||
--vp-c-bg-alt: #191814;
|
||||
--vp-c-bg-elv: #23221f;
|
||||
--vp-c-bg-soft: #23221f;
|
||||
--vp-c-text-1: #e6e1d8;
|
||||
--vp-c-text-2: #b8b1a6;
|
||||
--vp-c-text-3: #7f776d;
|
||||
--vp-c-border: #34322d;
|
||||
--vp-c-divider: #34322d;
|
||||
--vp-c-brand-1: #78a4ff;
|
||||
--vp-c-brand-2: #9bbeff;
|
||||
--vp-c-brand-3: #78a4ff;
|
||||
--vp-c-brand-soft: rgba(120, 164, 255, 0.16);
|
||||
--vp-code-bg: #2d2b27;
|
||||
--vp-code-color: #d8d1c6;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
html.tolaria-landing-page,
|
||||
html.tolaria-landing-page body,
|
||||
html.tolaria-landing-page #app {
|
||||
background: var(--tolaria-bg);
|
||||
}
|
||||
|
||||
.VPNavBarTitle .logo {
|
||||
width: auto;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--tolaria-font-brand);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell {
|
||||
--vp-c-bg: var(--tolaria-bg);
|
||||
--vp-nav-bg-color: var(--tolaria-bg);
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPNav,
|
||||
.tolaria-landing-shell .VPNavBar,
|
||||
.tolaria-landing-shell .VPNavBar:not(.has-sidebar):not(.home.top),
|
||||
.tolaria-landing-shell .VPNavBar:not(.home.top) .content-body,
|
||||
.tolaria-landing-shell .VPNavBar .content-body {
|
||||
background: var(--tolaria-bg);
|
||||
background-color: var(--tolaria-bg);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPNavBar .wrapper,
|
||||
.tolaria-landing-shell .VPNavBar .container {
|
||||
width: min(100%, 1280px);
|
||||
max-width: 1280px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPNavBar .wrapper {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .landing-container {
|
||||
width: min(100%, 1280px);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tolaria-landing-shell .VPNavBar .wrapper {
|
||||
padding-right: 40px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPNavBar .divider,
|
||||
.tolaria-landing-shell .VPNavBar .divider-line {
|
||||
display: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPNavBar .divider-line {
|
||||
opacity: 0;
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
position: relative;
|
||||
z-index: calc(var(--vp-z-index-nav) + 2);
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPLocalNav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPNavScreen {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
bottom: auto !important;
|
||||
height: calc(100vh - var(--vp-nav-height) - var(--vp-layout-top-height, 0px)) !important;
|
||||
z-index: calc(var(--vp-z-index-nav) + 1);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.tolaria-scrolled .tolaria-landing-shell .VPNav {
|
||||
box-shadow: 0 10px 24px rgba(26, 26, 24, 0.06);
|
||||
}
|
||||
|
||||
.tolaria-scrolled .tolaria-landing-shell .VPNavBar .divider-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .DocSearch-Button {
|
||||
border: 1px solid var(--tolaria-border);
|
||||
background: var(--tolaria-surface);
|
||||
}
|
||||
|
||||
.vp-doc h1,
|
||||
.vp-doc h2,
|
||||
.vp-doc h3 {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.vp-doc a,
|
||||
.VPNavBarMenuLink.active,
|
||||
.VPLink.active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vp-doc th {
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.vp-doc td,
|
||||
.vp-doc th {
|
||||
border-color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPContent,
|
||||
.tolaria-landing-shell .VPPage,
|
||||
.tolaria-landing-shell .VPDoc,
|
||||
.tolaria-landing-shell .VPDoc .container,
|
||||
.tolaria-landing-shell .VPDoc .content,
|
||||
.tolaria-landing-shell .VPDoc .content-container,
|
||||
.tolaria-landing-shell .VPDoc .main,
|
||||
.tolaria-landing-shell .vp-doc {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.tolaria-landing-shell .VPContent {
|
||||
padding-top: var(--vp-nav-height);
|
||||
}
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .vp-doc > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .vp-doc a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tolaria-landing-shell .VPFooter {
|
||||
display: none;
|
||||
}
|
||||
32
site/concepts/ai.md
Normal file
32
site/concepts/ai.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# AI
|
||||
|
||||
Tolaria has two AI paths: coding agents that can use tools to inspect and edit a vault, and direct model targets that answer in chat mode from note context.
|
||||
|
||||
## Coding Agents
|
||||
|
||||
The AI panel can stream supported local CLI agents through Tolaria's normalized event layer. Current targets include Claude Code, Codex, OpenCode, Pi, and Gemini CLI when they are installed on the machine.
|
||||
|
||||
Coding agents can run in:
|
||||
|
||||
- **Vault Safe** mode, limited to file, search, and edit tools.
|
||||
- **Power User** mode, which can allow local shell commands scoped to the active vault for agents that support shell access.
|
||||
|
||||
## Direct Models
|
||||
|
||||
Direct model targets run in chat mode. They receive the active note, linked context, and conversation history, but they do not receive vault-write tools or shell access.
|
||||
|
||||
Supported provider shapes include:
|
||||
|
||||
- Local models through Ollama or LM Studio.
|
||||
- Hosted providers such as OpenAI, Anthropic, Gemini, and OpenRouter.
|
||||
- Custom OpenAI-compatible endpoints.
|
||||
|
||||
## External MCP Setup
|
||||
|
||||
Tolaria exposes an MCP server for external tools. The setup flow can write Tolaria's MCP entry into Claude Code, Gemini CLI, Cursor, and a generic MCP config path, and it can also copy the exact JSON snippet for manual setup.
|
||||
|
||||
MCP setup is explicit. Closing the dialog leaves third-party config files untouched.
|
||||
|
||||
## Why Git Matters For AI
|
||||
|
||||
AI-generated changes should be inspectable. Git gives you diffs, history, rollback, and a clear boundary between suggestions and committed work.
|
||||
23
site/concepts/editor.md
Normal file
23
site/concepts/editor.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Editor
|
||||
|
||||
Tolaria offers a rich editor for daily writing and a raw Markdown mode for exact file control. Both modes write back to the same Markdown file.
|
||||
|
||||
## Rich Editing
|
||||
|
||||
The rich editor supports blocks, slash commands, wikilinks, tables, code blocks, images, Mermaid diagrams, LaTeX-style math, and markdown-backed whiteboards.
|
||||
|
||||
Use it when you want to write and reorganize quickly without thinking about Markdown syntax.
|
||||
|
||||
## Raw Mode
|
||||
|
||||
Raw mode shows the Markdown source directly. Use it when you need to edit YAML frontmatter, repair unusual Markdown, or make an exact text change.
|
||||
|
||||
Toggle raw mode with `Cmd+\` on macOS or `Ctrl+\` on Windows and Linux.
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
The table of contents panel builds an outline from headings in the current note. It is useful for long notes, procedures, research files, and generated documents. Toggle it with `Cmd+Shift+T` on macOS or `Ctrl+Shift+T` on Windows and Linux.
|
||||
|
||||
## Width
|
||||
|
||||
Notes can use normal or wide editor width. Set the default in Settings, or override an individual note from the editor toolbar.
|
||||
34
site/concepts/files-and-media.md
Normal file
34
site/concepts/files-and-media.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Files And Media
|
||||
|
||||
Tolaria starts with Markdown notes, but a vault can also contain images, PDFs, media files, whiteboards, and other local files.
|
||||
|
||||
## Mermaid Diagrams
|
||||
|
||||
Use Mermaid code blocks when a note needs a diagram that should stay plain text and versionable.
|
||||
|
||||
````md
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Idea --> Draft --> Review --> Publish
|
||||
```
|
||||
````
|
||||
|
||||
Tolaria renders Mermaid diagrams in the editor while keeping the source in Markdown.
|
||||
|
||||
## Attachments
|
||||
|
||||
Images pasted into the editor are saved into the vault as normal files. They remain portable and can be opened by other tools.
|
||||
|
||||
## Previews
|
||||
|
||||
Tolaria can preview common image files, PDFs, and supported media files in the app. Files without an in-app preview can still be opened in the default system app.
|
||||
|
||||
Settings control whether PDFs, images, and unsupported files appear in All Notes. Folder browsing still shows files in their folders.
|
||||
|
||||
## Whiteboards
|
||||
|
||||
Whiteboards use tldraw in the editor, but their durable representation stays in Markdown. That keeps them inside the vault and versioned by Git with the rest of your notes.
|
||||
|
||||
## Git Boundary
|
||||
|
||||
If generated or local-only files are ignored by Git, Tolaria can hide them from notes, search, quick open, and folders. Use this when build artifacts or private local files should not behave like vault content.
|
||||
29
site/concepts/git.md
Normal file
29
site/concepts/git.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Git
|
||||
|
||||
Git is Tolaria's recommended history and sync layer. Tolaria can work with plain Markdown folders, and Git unlocks local history, recovery, remote backup, and multi-device workflows when you want them.
|
||||
|
||||
Tolaria acts as a lightweight Git client for your vault. You can review changes, commit, pull, push, and inspect history without leaving the app.
|
||||
|
||||
## What Tolaria Uses Git For
|
||||
|
||||
- Whole-vault commit history.
|
||||
- Current diff for the vault.
|
||||
- Per-note history.
|
||||
- Current diff for an individual note.
|
||||
- Pull and push.
|
||||
- Conflict detection and resolution.
|
||||
- Remote connection for local-only vaults.
|
||||
|
||||
## History And Diffs
|
||||
|
||||
Each note can show its own history and current diff, so you can understand how that file changed over time or what is unsaved relative to Git.
|
||||
|
||||
Tolaria also shows a history of the whole vault. Use it when you want to review broader changes across multiple notes before committing or syncing.
|
||||
|
||||
## Local Commits
|
||||
|
||||
You can commit changes inside Tolaria without leaving the app. This gives you useful restore points even before a remote is configured.
|
||||
|
||||
## Remotes
|
||||
|
||||
Connect a compatible Git remote when you want sync or backup. Tolaria relies on your system Git authentication, so GitHub CLI, SSH keys, credential helpers, and existing Git configuration can continue to work.
|
||||
23
site/concepts/inbox.md
Normal file
23
site/concepts/inbox.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Inbox
|
||||
|
||||
The Inbox is for notes that have been captured but not yet organized.
|
||||
|
||||
## Why It Exists
|
||||
|
||||
Fast capture should not require perfect structure. The Inbox gives you a place to put incomplete notes, then process them later.
|
||||
|
||||
The Inbox workflow is optional. Turn it off in Settings > Workflow if you prefer every note to appear organized by default.
|
||||
|
||||
## Organizing Inbox Notes
|
||||
|
||||
When reviewing the Inbox:
|
||||
|
||||
1. Give the note a clear H1.
|
||||
2. Set its `type`.
|
||||
3. Add status, dates, or URL if useful.
|
||||
4. Add relationships with wikilinks or frontmatter fields.
|
||||
5. Move it into a folder only if the folder adds value.
|
||||
|
||||
## Healthy Inbox Habit
|
||||
|
||||
Keep the Inbox small enough that it can be reviewed in one focused pass. Tolaria works best when capture is fast and organization is deliberate.
|
||||
34
site/concepts/notes.md
Normal file
34
site/concepts/notes.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Notes
|
||||
|
||||
A note is a Markdown file with optional YAML frontmatter. Tolaria reads the first H1 as the primary title and keeps the file on disk as the durable representation.
|
||||
|
||||
## Anatomy
|
||||
|
||||
```md
|
||||
---
|
||||
type: Project
|
||||
status: Active
|
||||
belongs_to:
|
||||
- "[[workspace]]"
|
||||
---
|
||||
|
||||
# Launch Documentation
|
||||
|
||||
Draft the public Tolaria docs and keep them close to code changes.
|
||||
```
|
||||
|
||||
## Titles
|
||||
|
||||
The first H1 is the note title. Tolaria uses that title wherever the note is displayed: note lists, search results, wikilink suggestions, relationship pickers, tabs, and window titles.
|
||||
|
||||
The title is separate from the filename. The filename stays visible in the breadcrumb so you can see the file on disk, and you can rename it independently when needed.
|
||||
|
||||
Use the breadcrumb action to rename the file to match the title. New untitled notes can also auto-rename from the first H1 the first time they get a real title. Turn this behavior off in Settings > Vault Content > Titles & Filenames if you prefer filenames to stay unchanged until you rename them manually.
|
||||
|
||||
## Body Links
|
||||
|
||||
Use `[[wikilinks]]` to connect notes from the body. Tolaria shows autocomplete suggestions while you type, and links can resolve by filename or title.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Use frontmatter for structured fields such as type, status, date, URL, and relationships. Keep free-form thinking in the body.
|
||||
26
site/concepts/properties.md
Normal file
26
site/concepts/properties.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Properties
|
||||
|
||||
Properties are frontmatter fields that Tolaria can display, filter, and edit.
|
||||
|
||||
## Suggested Properties
|
||||
|
||||
Suggested properties are the fields Tolaria knows how to create quickly from the Properties panel. When a suggested property is missing, the panel shows a shortcut to add it with the right editor.
|
||||
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| `type` | Groups the note into a type such as Project, Person, or Topic. |
|
||||
| `status` | Tracks lifecycle state such as Active, Done, or Blocked. |
|
||||
| `url` | Stores a canonical external link. |
|
||||
| `date` | Represents a single date. |
|
||||
|
||||
## System Properties
|
||||
|
||||
Fields that start with `_` are system properties. They remain in plain text but are hidden from normal property editing.
|
||||
|
||||
Examples include `_icon`, `_color`, `_order`, `_sidebar_label`, `_width`, and `_pinned_properties` on type documents or notes.
|
||||
|
||||
## Property Editing
|
||||
|
||||
The Properties panel is the safest place to edit structured properties. Toggle it with `Cmd+Shift+I` on macOS or `Ctrl+Shift+I` on Windows and Linux.
|
||||
|
||||
Date fields use Tolaria's picker, relationship fields can use wikilinks, and raw Markdown mode is available when you need direct control over YAML.
|
||||
32
site/concepts/relationships.md
Normal file
32
site/concepts/relationships.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Relationships
|
||||
|
||||
Relationships make a vault feel like a graph instead of a pile of documents.
|
||||
|
||||
## Relationship Fields
|
||||
|
||||
Any frontmatter field containing wikilinks can become a relationship. Relationship fields can point to one note or to an array of notes.
|
||||
|
||||
```yaml
|
||||
belongs_to:
|
||||
- "[[product-work]]"
|
||||
related_to:
|
||||
- "[[documentation]]"
|
||||
- "[[editor-research]]"
|
||||
blocked_by:
|
||||
- "[[release-process]]"
|
||||
- "[[sync-conflicts]]"
|
||||
```
|
||||
|
||||
Tolaria supports default relationship fields out of the box: `belongs_to`, `has`, and `related_to`. It also detects custom relationship fields dynamically when they contain wikilinks.
|
||||
|
||||
Default relationships have automatically computed inverses. If a note says it `belongs_to` a project, the project can show that note under its inverse `has` relationship without you writing the reverse link by hand. `related_to` works as a lateral relationship in both directions.
|
||||
|
||||
These outgoing and inverse relationships appear in the Properties panel and in Neighborhood mode, where the note list becomes a graph view around the selected note.
|
||||
|
||||
## Body Links Versus Relationship Fields
|
||||
|
||||
Use body links when the relationship appears naturally in writing. Use frontmatter relationships when the connection is important enough to show in navigation, filters, Neighborhood mode, or the Properties panel.
|
||||
|
||||
## Backlinks
|
||||
|
||||
Tolaria can show incoming links and inverse relationships, making it easier to navigate from a note to the rest of its context.
|
||||
49
site/concepts/types.md
Normal file
49
site/concepts/types.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Types
|
||||
|
||||
Types describe what kind of thing a note represents: Project, Person, Topic, Procedure, Event, or any category you create.
|
||||
|
||||
## Type Field
|
||||
|
||||
The `type:` field assigns a note to a type.
|
||||
|
||||
```yaml
|
||||
type: Project
|
||||
```
|
||||
|
||||
Tolaria does not infer type from folder location. Moving a file into another folder does not change its type.
|
||||
|
||||
## Prefer Types Over Folders
|
||||
|
||||
Types are the preferred way to group notes in Tolaria. Folders are supported for existing vaults and fallback organization, but Tolaria is built around types and relationships because they carry stronger meaning than file paths.
|
||||
|
||||
Use types for semantic groups such as Projects, People, Topics, Procedures, Events, and Essays. Use relationships to connect notes across those groups. This gives Tolaria better structure for navigation, filtering, properties, templates, and future automation than folder location alone.
|
||||
|
||||
## Type Documents
|
||||
|
||||
Type documents are Markdown notes with `type: Type` in frontmatter. They describe how a type should appear and what new notes of that type should start with.
|
||||
|
||||
```yaml
|
||||
---
|
||||
type: Type
|
||||
_icon: folder
|
||||
_color: blue
|
||||
_sidebar_label: Projects
|
||||
_order: 10
|
||||
---
|
||||
|
||||
# Project
|
||||
```
|
||||
|
||||
## What Types Control
|
||||
|
||||
- Sidebar grouping.
|
||||
- Type icon and color.
|
||||
- Sidebar order and label.
|
||||
- Pinned properties.
|
||||
- New-note templates.
|
||||
|
||||
## New Note Defaults
|
||||
|
||||
Type documents can define empty properties and relationships. When you create a new note of that type, Tolaria shows placeholders for those fields so you can fill them in from the Properties panel.
|
||||
|
||||
If a type document gives a property a value, that value becomes the default for new notes of that type. For example, a Project type can define `status: Active` so every new project starts active until you change it.
|
||||
45
site/concepts/vaults.md
Normal file
45
site/concepts/vaults.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Vaults
|
||||
|
||||
A vault is the folder Tolaria reads and writes. The filesystem is the source of truth; the app state and cache are derived from files.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Notes are Markdown files.
|
||||
- YAML frontmatter provides structure.
|
||||
- Attachments are normal files inside the vault.
|
||||
- Type definitions and saved views are also files.
|
||||
- Git can track history and support remote sync.
|
||||
|
||||
## Why Local Files Matter
|
||||
|
||||
Local files keep your notes inspectable. You can open them in another editor, search with command-line tools, back them up with your own system, and version them with Git.
|
||||
|
||||
Tolaria should never become the only way to read your data.
|
||||
|
||||
## Git Is A Capability
|
||||
|
||||
A plain folder of Markdown files can open as a vault. Git-backed vaults unlock history, changes, commits, pull, push, conflict handling, and remote setup.
|
||||
|
||||
If a folder is not a Git repository, Tolaria can initialize Git when you explicitly ask it to. It avoids initializing broad personal folders such as Desktop, Documents, or Downloads unless they are clearly dedicated vault folders.
|
||||
|
||||
## Multiple Vaults At The Same Time
|
||||
|
||||
Tolaria can load multiple registered vaults into one unified graph. Enable this from `Settings` -> `Vaults` -> `Use multiple vaults at the same time`.
|
||||
|
||||
After the option is enabled, open the bottom-left vault menu to include or exclude vaults from the graph. Included vaults appear together in note lists, search, quick open, backlinks, and wikilink navigation. Each note keeps a compact vault badge when Tolaria needs to disambiguate where it lives.
|
||||
|
||||
The selected vault still matters. Git status, commits, sync, folder navigation, saved views, and vault repair actions stay scoped to the current repository. Use `Manage vaults` from the vault menu or the Vaults settings section to rename vaults, choose colors, and set the default destination for new notes.
|
||||
|
||||
Cross-vault wikilinks use the target vault's stable alias when needed, for example `[[team/projects/alpha]]`. Links inside the same vault stay normal vault-relative links.
|
||||
|
||||
## App State Versus Vault State
|
||||
|
||||
Vault-level information should travel with the vault. Machine-specific preferences stay with the app installation.
|
||||
|
||||
| Vault state | App state |
|
||||
| --- | --- |
|
||||
| Type icons and colors | Editor zoom |
|
||||
| Saved views | Window size |
|
||||
| Pinned properties | Recent vault list |
|
||||
| Relationship conventions | Local cache |
|
||||
| Vault AI guidance files | AI target selection |
|
||||
25
site/guides/build-custom-views.md
Normal file
25
site/guides/build-custom-views.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Build Custom Views
|
||||
|
||||
Custom views are saved filters for recurring questions.
|
||||
|
||||
## Good View Candidates
|
||||
|
||||
- Active projects.
|
||||
- People without a recent follow-up.
|
||||
- Drafts ready for review.
|
||||
- Notes changed this week.
|
||||
- Events in a date range.
|
||||
|
||||
## View Definition
|
||||
|
||||
Saved views live as files in the vault. They describe filters, sorting, and visible columns using structured data.
|
||||
|
||||
## Filters
|
||||
|
||||
Custom views can use nested conditions, similar to Notion or Airtable filter groups. Combine `all` and `any` logic when a view needs to answer a more precise question than a single field filter can express.
|
||||
|
||||
Date filters support dynamic natural-language values such as `today`, `yesterday`, or `one week ago`. Use these for views that should keep moving over time, such as recent work, stale follow-ups, or upcoming events.
|
||||
|
||||
## Design The Question First
|
||||
|
||||
Before creating a view, write the question it answers. A good view is not "all fields with all filters"; it is a focused lens.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user