@neuralis/brain-core 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +26 -35
  2. package/app/README.md +1 -1
  3. package/app/cards/DiffView.tsx +9 -7
  4. package/app/cards/README.md +1 -1
  5. package/app/cards/diffLine.tsx +2 -4
  6. package/app/filesystem/README.md +4 -4
  7. package/app/filesystem/api/README.md +3 -3
  8. package/app/filesystem/api/index.ts +137 -123
  9. package/app/filesystem/domain/types.ts +36 -5
  10. package/app/filesystem/hooks/README.md +5 -5
  11. package/app/filesystem/hooks/useDiscoverableSources.ts +4 -2
  12. package/app/filesystem/hooks/useFilesystemSSE.ts +31 -23
  13. package/app/filesystem/hooks/useGitChangesInventory.ts +4 -2
  14. package/app/filesystem/host/README.md +3 -2
  15. package/app/filesystem/host/installFilesystemHostComponents.tsx +22 -9
  16. package/app/filesystem/store/README.md +11 -7
  17. package/app/filesystem/store/filesystemStore.ts +454 -162
  18. package/app/filesystem/store/gitSlice.ts +6 -6
  19. package/app/filesystem/store/listSlice.ts +10 -6
  20. package/app/filesystem/store/searchSlice.ts +27 -4
  21. package/app/filesystem/ui/FileEditor.tsx +48 -35
  22. package/app/filesystem/ui/FileTree.tsx +10 -9
  23. package/app/filesystem/ui/FileTreeSearch.tsx +29 -2
  24. package/app/filesystem/ui/GitCommitChangesView.tsx +5 -3
  25. package/app/filesystem/ui/GitCommitGraph.tsx +6 -5
  26. package/app/filesystem/ui/GitControlPanel.tsx +14 -13
  27. package/app/filesystem/ui/GitDiffView.tsx +2 -1
  28. package/app/filesystem/ui/GitFileDiff.tsx +7 -6
  29. package/app/filesystem/ui/PendingChangesPanel.tsx +88 -28
  30. package/app/filesystem/ui/PendingReviewDialog.tsx +52 -24
  31. package/app/filesystem/ui/README.md +8 -7
  32. package/app/filesystem/ui/ReembedDialog.tsx +22 -9
  33. package/app/filesystem/ui/SourceDeleteConfirm.tsx +1 -1
  34. package/app/filesystem/ui/SourcesPanel.tsx +135 -42
  35. package/app/filesystem/ui/VectorHealthPanel.tsx +20 -4
  36. package/app/filesystem/ui/pendingChangesModel.ts +9 -5
  37. package/dist/app/chunks/{astro-LRAFGXNX.js → astro-B2J4AS2D.js} +1 -1
  38. package/dist/app/chunks/bash-KY6SAQTH.js +1 -0
  39. package/dist/app/chunks/c-OBOPYVZ7.js +1 -0
  40. package/dist/app/chunks/{chunk-5H7ZCOD3.js → chunk-2X3GNKJ3.js} +1 -1
  41. package/dist/app/chunks/{chunk-XIFGTNRM.js → chunk-JPJIVRFI.js} +1 -1
  42. package/dist/app/chunks/{chunk-RH3QY5LF.js → chunk-LEFIFWND.js} +1 -1
  43. package/dist/app/chunks/{chunk-G4RZ3UNX.js → chunk-N6RXX3PH.js} +1 -1
  44. package/dist/app/chunks/{chunk-QXXJ73P5.js → chunk-PZAW4HPB.js} +1 -1
  45. package/dist/app/chunks/{chunk-3QLE7Y4L.js → chunk-ZBYWWTO6.js} +1 -1
  46. package/dist/app/chunks/cpp-TVWVOTEQ.js +1 -0
  47. package/dist/app/chunks/css-MANZBYA4.js +1 -0
  48. package/dist/app/chunks/{elixir-M36XROA6.js → elixir-S725Z7EH.js} +1 -1
  49. package/dist/app/chunks/{elm-7BG6QAIK.js → elm-5WUUSTNB.js} +1 -1
  50. package/dist/app/chunks/graphql-UGOPR3WU.js +1 -0
  51. package/dist/app/chunks/html-F6FWTBSG.js +1 -0
  52. package/dist/app/chunks/java-ANIVV5H5.js +1 -0
  53. package/dist/app/chunks/javascript-IH374YWG.js +1 -0
  54. package/dist/app/chunks/json-WGGVVXSB.js +1 -0
  55. package/dist/app/chunks/jsx-OTXJQD32.js +1 -0
  56. package/dist/app/chunks/{latex-ZWK7SXB6.js → latex-QCZNITGN.js} +1 -1
  57. package/dist/app/chunks/lua-4CRZ64PU.js +1 -0
  58. package/dist/app/chunks/{nginx-2L6VRV3A.js → nginx-V4CYS7WR.js} +1 -1
  59. package/dist/app/chunks/{perl-PBUAXXK6.js → perl-BHTX7F5B.js} +1 -1
  60. package/dist/app/chunks/{php-QJX57PA6.js → php-AYGJ5WYZ.js} +1 -1
  61. package/dist/app/chunks/r-4YFCFA4B.js +1 -0
  62. package/dist/app/chunks/{ruby-RLOWMVPO.js → ruby-42NMJWAL.js} +1 -1
  63. package/dist/app/chunks/{scss-B5CAI3HR.js → scss-XD7VU2CU.js} +1 -1
  64. package/dist/app/chunks/sql-B55MH6JJ.js +1 -0
  65. package/dist/app/chunks/{svelte-GT3D4C44.js → svelte-WR3QPIJU.js} +1 -1
  66. package/dist/app/chunks/tsx-S73GAE2L.js +1 -0
  67. package/dist/app/chunks/typescript-NEWOAB5Z.js +1 -0
  68. package/dist/app/chunks/{vue-JOW62MHD.js → vue-CCDS4VZK.js} +1 -1
  69. package/dist/app/chunks/xml-UXKAGLDR.js +1 -0
  70. package/dist/app/chunks/yaml-UMWKDTK7.js +1 -0
  71. package/dist/app/host.js +46 -46
  72. package/dist/scripts/embeddingLockV3.d.ts +30 -0
  73. package/dist/scripts/embeddingLockV3.d.ts.map +1 -0
  74. package/dist/scripts/embeddingLockV3.js +100 -0
  75. package/dist/scripts/embeddingLockV3.js.map +1 -0
  76. package/dist/scripts/vectorProofV1.d.ts +90 -0
  77. package/dist/scripts/vectorProofV1.d.ts.map +1 -0
  78. package/dist/scripts/vectorProofV1.js +220 -0
  79. package/dist/scripts/vectorProofV1.js.map +1 -0
  80. package/dist/src/config/env.d.ts +21 -0
  81. package/dist/src/config/env.d.ts.map +1 -1
  82. package/dist/src/config/env.js +55 -0
  83. package/dist/src/config/env.js.map +1 -1
  84. package/dist/src/connectors/AppSourceConfigStore.d.ts +12 -2
  85. package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
  86. package/dist/src/connectors/AppSourceConfigStore.js +48 -40
  87. package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
  88. package/dist/src/connectors/LocalDiskConnector.d.ts +11 -14
  89. package/dist/src/connectors/LocalDiskConnector.d.ts.map +1 -1
  90. package/dist/src/connectors/LocalDiskConnector.js +87 -107
  91. package/dist/src/connectors/LocalDiskConnector.js.map +1 -1
  92. package/dist/src/connectors/sourceConfigMutation.d.ts +11 -1
  93. package/dist/src/connectors/sourceConfigMutation.d.ts.map +1 -1
  94. package/dist/src/connectors/sourceConfigMutation.js +36 -3
  95. package/dist/src/connectors/sourceConfigMutation.js.map +1 -1
  96. package/dist/src/domain/chunker.js +8 -3
  97. package/dist/src/domain/chunker.js.map +1 -1
  98. package/dist/src/domain/healthTypes.d.ts +11 -0
  99. package/dist/src/domain/healthTypes.d.ts.map +1 -1
  100. package/dist/src/domain/mime.d.ts +1 -0
  101. package/dist/src/domain/mime.d.ts.map +1 -1
  102. package/dist/src/domain/mime.js +48 -0
  103. package/dist/src/domain/mime.js.map +1 -1
  104. package/dist/src/domain/pendingChange.d.ts +7 -0
  105. package/dist/src/domain/pendingChange.d.ts.map +1 -1
  106. package/dist/src/domain/pendingChange.js +11 -0
  107. package/dist/src/domain/pendingChange.js.map +1 -1
  108. package/dist/src/domain/sourceConfig.d.ts +7 -14
  109. package/dist/src/domain/sourceConfig.d.ts.map +1 -1
  110. package/dist/src/domain/sourceConfig.js +1 -13
  111. package/dist/src/domain/sourceConfig.js.map +1 -1
  112. package/dist/src/domain/sourcePackages.d.ts +11 -2
  113. package/dist/src/domain/sourcePackages.d.ts.map +1 -1
  114. package/dist/src/domain/syncTrigger.d.ts +2 -3
  115. package/dist/src/domain/syncTrigger.d.ts.map +1 -1
  116. package/dist/src/domain/syncTrigger.js +2 -3
  117. package/dist/src/domain/syncTrigger.js.map +1 -1
  118. package/dist/src/domain/syncTypes.d.ts +21 -4
  119. package/dist/src/domain/syncTypes.d.ts.map +1 -1
  120. package/dist/src/domain/syncTypes.js.map +1 -1
  121. package/dist/src/domain/text.d.ts +3 -0
  122. package/dist/src/domain/text.d.ts.map +1 -1
  123. package/dist/src/domain/text.js +5 -0
  124. package/dist/src/domain/text.js.map +1 -1
  125. package/dist/src/domain/types.d.ts +9 -0
  126. package/dist/src/domain/types.d.ts.map +1 -1
  127. package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
  128. package/dist/src/embeddings/clients/GeminiEmbedder.js +11 -5
  129. package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
  130. package/dist/src/embeddings/clients/OllamaEmbedder.d.ts.map +1 -1
  131. package/dist/src/embeddings/clients/OllamaEmbedder.js +13 -6
  132. package/dist/src/embeddings/clients/OllamaEmbedder.js.map +1 -1
  133. package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
  134. package/dist/src/embeddings/clients/OpenAIEmbedder.js +12 -9
  135. package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
  136. package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
  137. package/dist/src/embeddings/clients/QwenEmbedder.js +12 -5
  138. package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
  139. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
  140. package/dist/src/embeddings/clients/VoyageEmbedder.js +12 -5
  141. package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
  142. package/dist/src/embeddings/factory.d.ts +2 -0
  143. package/dist/src/embeddings/factory.d.ts.map +1 -1
  144. package/dist/src/embeddings/factory.js +23 -3
  145. package/dist/src/embeddings/factory.js.map +1 -1
  146. package/dist/src/embeddings/lock.d.ts +22 -11
  147. package/dist/src/embeddings/lock.d.ts.map +1 -1
  148. package/dist/src/embeddings/lock.js +47 -29
  149. package/dist/src/embeddings/lock.js.map +1 -1
  150. package/dist/src/embeddings/retry.d.ts +5 -0
  151. package/dist/src/embeddings/retry.d.ts.map +1 -1
  152. package/dist/src/embeddings/retry.js +35 -0
  153. package/dist/src/embeddings/retry.js.map +1 -1
  154. package/dist/src/events/pending.d.ts +56 -0
  155. package/dist/src/events/pending.d.ts.map +1 -0
  156. package/dist/src/events/pending.js +108 -0
  157. package/dist/src/events/pending.js.map +1 -0
  158. package/dist/src/events/source.d.ts +60 -14
  159. package/dist/src/events/source.d.ts.map +1 -1
  160. package/dist/src/events/source.js +149 -31
  161. package/dist/src/events/source.js.map +1 -1
  162. package/dist/src/events/vector.d.ts +33 -0
  163. package/dist/src/events/vector.d.ts.map +1 -0
  164. package/dist/src/events/vector.js +57 -0
  165. package/dist/src/events/vector.js.map +1 -0
  166. package/dist/src/git/GitRunner.d.ts.map +1 -1
  167. package/dist/src/git/GitRunner.js +11 -8
  168. package/dist/src/git/GitRunner.js.map +1 -1
  169. package/dist/src/git/sourceGitControl.d.ts.map +1 -1
  170. package/dist/src/git/sourceGitControl.js +2 -1
  171. package/dist/src/git/sourceGitControl.js.map +1 -1
  172. package/dist/src/index.d.ts +5 -4
  173. package/dist/src/index.d.ts.map +1 -1
  174. package/dist/src/index.js +4 -3
  175. package/dist/src/index.js.map +1 -1
  176. package/dist/src/lifecycle.d.ts +20 -4
  177. package/dist/src/lifecycle.d.ts.map +1 -1
  178. package/dist/src/lifecycle.js +5 -1
  179. package/dist/src/lifecycle.js.map +1 -1
  180. package/dist/src/routes/health.js +6 -1
  181. package/dist/src/routes/health.js.map +1 -1
  182. package/dist/src/routes/pending.d.ts.map +1 -1
  183. package/dist/src/routes/pending.js +2 -1
  184. package/dist/src/routes/pending.js.map +1 -1
  185. package/dist/src/routes/sources.d.ts.map +1 -1
  186. package/dist/src/routes/sources.js +60 -11
  187. package/dist/src/routes/sources.js.map +1 -1
  188. package/dist/src/services/BridgeService.d.ts +2 -2
  189. package/dist/src/services/BridgeService.d.ts.map +1 -1
  190. package/dist/src/services/BridgeService.js +11 -2
  191. package/dist/src/services/BridgeService.js.map +1 -1
  192. package/dist/src/services/FilesystemService.d.ts +3 -1
  193. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  194. package/dist/src/services/FilesystemService.js +3 -1
  195. package/dist/src/services/FilesystemService.js.map +1 -1
  196. package/dist/src/services/FilesystemUiService.js +1 -1
  197. package/dist/src/services/FilesystemUiService.js.map +1 -1
  198. package/dist/src/services/SyncScheduler.d.ts.map +1 -1
  199. package/dist/src/services/SyncScheduler.js +0 -14
  200. package/dist/src/services/SyncScheduler.js.map +1 -1
  201. package/dist/src/services/VectorHealthService.d.ts.map +1 -1
  202. package/dist/src/services/VectorHealthService.js +2 -46
  203. package/dist/src/services/VectorHealthService.js.map +1 -1
  204. package/dist/src/services/baselineRepair.d.ts +2 -2
  205. package/dist/src/services/baselineRepair.js +2 -2
  206. package/dist/src/services/declaredSources.d.ts +8 -1
  207. package/dist/src/services/declaredSources.d.ts.map +1 -1
  208. package/dist/src/services/declaredSources.js +19 -19
  209. package/dist/src/services/declaredSources.js.map +1 -1
  210. package/dist/src/store/dataFormats.d.ts +14 -3
  211. package/dist/src/store/dataFormats.d.ts.map +1 -1
  212. package/dist/src/store/dataFormats.js +15 -5
  213. package/dist/src/store/dataFormats.js.map +1 -1
  214. package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
  215. package/dist/src/sync/DeltaSyncRunner.js +78 -243
  216. package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
  217. package/dist/src/sync/SyncJobQueue.d.ts +33 -4
  218. package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
  219. package/dist/src/sync/SyncJobQueue.js +179 -19
  220. package/dist/src/sync/SyncJobQueue.js.map +1 -1
  221. package/dist/src/sync/moveDetector.d.ts +2 -1
  222. package/dist/src/sync/moveDetector.d.ts.map +1 -1
  223. package/dist/src/sync/moveDetector.js +2 -1
  224. package/dist/src/sync/moveDetector.js.map +1 -1
  225. package/dist/src/tools/fs_search.d.ts.map +1 -1
  226. package/dist/src/tools/fs_search.js +8 -0
  227. package/dist/src/tools/fs_search.js.map +1 -1
  228. package/dist/src/tools/fs_write.js +3 -1
  229. package/dist/src/tools/fs_write.js.map +1 -1
  230. package/dist/src/usecases/chunkUpsert.d.ts +4 -0
  231. package/dist/src/usecases/chunkUpsert.d.ts.map +1 -1
  232. package/dist/src/usecases/chunkUpsert.js +103 -60
  233. package/dist/src/usecases/chunkUpsert.js.map +1 -1
  234. package/dist/src/usecases/embeddingBacklog.d.ts +31 -0
  235. package/dist/src/usecases/embeddingBacklog.d.ts.map +1 -0
  236. package/dist/src/usecases/embeddingBacklog.js +244 -0
  237. package/dist/src/usecases/embeddingBacklog.js.map +1 -0
  238. package/dist/src/usecases/listSourcePackages.js +17 -1
  239. package/dist/src/usecases/listSourcePackages.js.map +1 -1
  240. package/dist/src/usecases/mediaLane.d.ts +11 -4
  241. package/dist/src/usecases/mediaLane.d.ts.map +1 -1
  242. package/dist/src/usecases/mediaLane.js +67 -30
  243. package/dist/src/usecases/mediaLane.js.map +1 -1
  244. package/dist/src/usecases/pendingReview.d.ts.map +1 -1
  245. package/dist/src/usecases/pendingReview.js +24 -7
  246. package/dist/src/usecases/pendingReview.js.map +1 -1
  247. package/dist/src/usecases/purgeExcludedPoints.d.ts +2 -0
  248. package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
  249. package/dist/src/usecases/purgeExcludedPoints.js +3 -2
  250. package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
  251. package/dist/src/usecases/readFiles.d.ts.map +1 -1
  252. package/dist/src/usecases/readFiles.js +33 -6
  253. package/dist/src/usecases/readFiles.js.map +1 -1
  254. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +31 -0
  255. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -0
  256. package/dist/src/usecases/reconcileSourcePackageRoots.js +114 -0
  257. package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -0
  258. package/dist/src/usecases/searchFiles.d.ts.map +1 -1
  259. package/dist/src/usecases/searchFiles.js +62 -64
  260. package/dist/src/usecases/searchFiles.js.map +1 -1
  261. package/dist/src/usecases/syncIndexedFile.d.ts +42 -0
  262. package/dist/src/usecases/syncIndexedFile.d.ts.map +1 -0
  263. package/dist/src/usecases/syncIndexedFile.js +242 -0
  264. package/dist/src/usecases/syncIndexedFile.js.map +1 -0
  265. package/dist/src/usecases/syncPointWrites.d.ts +5 -0
  266. package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
  267. package/dist/src/usecases/syncPointWrites.js +32 -5
  268. package/dist/src/usecases/syncPointWrites.js.map +1 -1
  269. package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
  270. package/dist/src/usecases/uploadBinary.js +36 -49
  271. package/dist/src/usecases/uploadBinary.js.map +1 -1
  272. package/dist/src/usecases/vector/endpointGuard.js +2 -1
  273. package/dist/src/usecases/vector/endpointGuard.js.map +1 -1
  274. package/dist/src/usecases/vector/estimate.d.ts +7 -4
  275. package/dist/src/usecases/vector/estimate.d.ts.map +1 -1
  276. package/dist/src/usecases/vector/estimate.js +8 -5
  277. package/dist/src/usecases/vector/estimate.js.map +1 -1
  278. package/dist/src/usecases/vector/rebuildGeneration.d.ts +29 -5
  279. package/dist/src/usecases/vector/rebuildGeneration.d.ts.map +1 -1
  280. package/dist/src/usecases/vector/rebuildGeneration.js +224 -82
  281. package/dist/src/usecases/vector/rebuildGeneration.js.map +1 -1
  282. package/dist/src/usecases/vectorMaintenance.d.ts +46 -4
  283. package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
  284. package/dist/src/usecases/vectorMaintenance.js +190 -11
  285. package/dist/src/usecases/vectorMaintenance.js.map +1 -1
  286. package/dist/src/usecases/write/commit.d.ts +6 -8
  287. package/dist/src/usecases/write/commit.d.ts.map +1 -1
  288. package/dist/src/usecases/write/commit.js +96 -112
  289. package/dist/src/usecases/write/commit.js.map +1 -1
  290. package/dist/src/usecases/write/content.js +1 -2
  291. package/dist/src/usecases/write/content.js.map +1 -1
  292. package/dist/src/usecases/write/ports.d.ts +1 -0
  293. package/dist/src/usecases/write/ports.d.ts.map +1 -1
  294. package/dist/src/vector/FilesystemVectorIndex.d.ts +34 -3
  295. package/dist/src/vector/FilesystemVectorIndex.d.ts.map +1 -1
  296. package/dist/src/vector/FilesystemVectorIndex.js +254 -20
  297. package/dist/src/vector/FilesystemVectorIndex.js.map +1 -1
  298. package/dist/src/vector/InMemoryVectorIndex.d.ts +9 -2
  299. package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
  300. package/dist/src/vector/InMemoryVectorIndex.js +25 -6
  301. package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
  302. package/dist/src/vector/QdrantVectorIndex.d.ts +35 -9
  303. package/dist/src/vector/QdrantVectorIndex.d.ts.map +1 -1
  304. package/dist/src/vector/QdrantVectorIndex.js +209 -90
  305. package/dist/src/vector/QdrantVectorIndex.js.map +1 -1
  306. package/dist/src/vector/qdrantFilter.d.ts +3 -1
  307. package/dist/src/vector/qdrantFilter.d.ts.map +1 -1
  308. package/dist/src/vector/qdrantFilter.js +2 -0
  309. package/dist/src/vector/qdrantFilter.js.map +1 -1
  310. package/dist/src/vector/types.d.ts +81 -0
  311. package/dist/src/vector/types.d.ts.map +1 -1
  312. package/dist/src/vector/types.js +2 -0
  313. package/dist/src/vector/types.js.map +1 -1
  314. package/dist/src/wiring.d.ts +6 -0
  315. package/dist/src/wiring.d.ts.map +1 -1
  316. package/dist/src/wiring.js +62 -3
  317. package/dist/src/wiring.js.map +1 -1
  318. package/dist/tools/fs_search.json +2 -2
  319. package/docs/drive-overview.md +2 -2
  320. package/instructions/brain-model.md +1 -1
  321. package/package.json +1000 -11
  322. package/rules/brain-rules.md +1 -1
  323. package/skills/connect-business-data/SKILL.md +6 -4
  324. package/skills/create-package/SKILL.md +14 -12
  325. package/skills/create-package/references/config-and-credentials.md +3 -3
  326. package/skills/create-package/references/connectors-and-uri-policies.md +5 -3
  327. package/skills/create-package/references/manifest-and-discovery.md +5 -4
  328. package/skills/create-package/references/runtime-and-build.md +9 -9
  329. package/skills/create-package/references/skills-and-context.md +3 -3
  330. package/skills/create-package/references/surfaces-and-design.md +9 -6
  331. package/skills/create-package/references/team-and-workflows.md +2 -2
  332. package/skills/create-package/references/test-and-ship.md +15 -15
  333. package/skills/create-package/references/tools-and-schemas.md +1 -1
  334. package/skills/create-package/scripts/check-package.sh +50 -4
  335. package/skills/create-package/scripts/verify-loaded.sh +16 -16
  336. package/skills/manage-files/SKILL.md +2 -2
  337. package/skills/manage-sources/SKILL.md +16 -15
  338. package/skills/manage-sources/scripts/reembed.sh +9 -3
  339. package/skills/sync/SKILL.md +4 -4
  340. package/tools/fs_search.json +2 -2
  341. package/app/filesystem/hooks/useContainerWidth.ts +0 -39
  342. package/dist/app/chunks/bash-M45GHPBQ.js +0 -1
  343. package/dist/app/chunks/c-X3REOAWD.js +0 -1
  344. package/dist/app/chunks/cpp-VOWI743W.js +0 -1
  345. package/dist/app/chunks/css-2ZVPHE25.js +0 -1
  346. package/dist/app/chunks/graphql-7V2NP54C.js +0 -1
  347. package/dist/app/chunks/html-UF3LFLNA.js +0 -1
  348. package/dist/app/chunks/java-PNTAU4L3.js +0 -1
  349. package/dist/app/chunks/javascript-4JJTQGEL.js +0 -1
  350. package/dist/app/chunks/json-VLQ6D3JH.js +0 -1
  351. package/dist/app/chunks/jsx-KQKSLAAH.js +0 -1
  352. package/dist/app/chunks/lua-UFPYUJ2O.js +0 -1
  353. package/dist/app/chunks/r-QEZB45SR.js +0 -1
  354. package/dist/app/chunks/sql-DTZHNUZP.js +0 -1
  355. package/dist/app/chunks/tsx-WPR2WRZD.js +0 -1
  356. package/dist/app/chunks/typescript-LWO362OA.js +0 -1
  357. package/dist/app/chunks/xml-A5H72RTS.js +0 -1
  358. package/dist/app/chunks/yaml-MXHSIJCI.js +0 -1
  359. package/dist/src/services/purgePendingNodes.d.ts +0 -32
  360. package/dist/src/services/purgePendingNodes.d.ts.map +0 -1
  361. package/dist/src/services/purgePendingNodes.js +0 -61
  362. package/dist/src/services/purgePendingNodes.js.map +0 -1
  363. /package/dist/app/chunks/{chunk-737Y24HS.js → chunk-3JASFUPF.js} +0 -0
  364. /package/dist/app/chunks/{chunk-DKD2L57N.js → chunk-6EYLOQ4F.js} +0 -0
  365. /package/dist/app/chunks/{chunk-KYH5EHHY.js → chunk-7YRZ5LOQ.js} +0 -0
  366. /package/dist/app/chunks/{chunk-6TC3DVWV.js → chunk-C7LU4RID.js} +0 -0
  367. /package/dist/app/chunks/{chunk-TY33BMKR.js → chunk-CD52YDZP.js} +0 -0
  368. /package/dist/app/chunks/{chunk-OLEIKJ3X.js → chunk-DCJKTCJ5.js} +0 -0
  369. /package/dist/app/chunks/{chunk-QLBZ4JR2.js → chunk-EXIU2KBV.js} +0 -0
  370. /package/dist/app/chunks/{chunk-SFUNBBEA.js → chunk-FDP4LO4P.js} +0 -0
  371. /package/dist/app/chunks/{chunk-BGRVFATT.js → chunk-HNETUS7U.js} +0 -0
  372. /package/dist/app/chunks/{chunk-4QNMHAIR.js → chunk-LBFZJEDR.js} +0 -0
  373. /package/dist/app/chunks/{chunk-TRRUHDPN.js → chunk-ULRXL5VM.js} +0 -0
  374. /package/dist/app/chunks/{chunk-S6B4WWIF.js → chunk-XJK47SMJ.js} +0 -0
  375. /package/dist/app/chunks/{chunk-DZ5C5FKI.js → chunk-YYAAIAU2.js} +0 -0
  376. /package/dist/app/chunks/{clojure-OC6KPSJO.js → clojure-4QCN4GHG.js} +0 -0
  377. /package/dist/app/chunks/{core-KGE7TQJ3.js → core-ZHTX3P2Q.js} +0 -0
  378. /package/dist/app/chunks/{csharp-E7VIFITL.js → csharp-HQWR4N52.js} +0 -0
  379. /package/dist/app/chunks/{dark-plus-BOSCZXE4.js → dark-plus-GYPKKZES.js} +0 -0
  380. /package/dist/app/chunks/{dart-6Y3X27SZ.js → dart-FO4TTVO2.js} +0 -0
  381. /package/dist/app/chunks/{diff-TR2NPA64.js → diff-SHANV4YC.js} +0 -0
  382. /package/dist/app/chunks/{dist-T32U2VVR.js → dist-WSDF34DM.js} +0 -0
  383. /package/dist/app/chunks/{docker-N33HOW54.js → docker-VX55HTDS.js} +0 -0
  384. /package/dist/app/chunks/{go-6DGG5UTP.js → go-5UCPEC6S.js} +0 -0
  385. /package/dist/app/chunks/{haskell-KK36PMMM.js → haskell-25PJ3OSG.js} +0 -0
  386. /package/dist/app/chunks/{ini-VHVQ5TPB.js → ini-BJIVD5MK.js} +0 -0
  387. /package/dist/app/chunks/{kotlin-44WS3JHE.js → kotlin-MNR6XOEQ.js} +0 -0
  388. /package/dist/app/chunks/{less-5WZ3KLAK.js → less-KA5JURX6.js} +0 -0
  389. /package/dist/app/chunks/{light-plus-HGGBC43G.js → light-plus-QXH2UUWZ.js} +0 -0
  390. /package/dist/app/chunks/{make-X7RXMVW7.js → make-LPD5KOCO.js} +0 -0
  391. /package/dist/app/chunks/{markdown-46IKUAIB.js → markdown-LMWF6MQR.js} +0 -0
  392. /package/dist/app/chunks/{mdx-DGLHDAO7.js → mdx-WPMNN5BS.js} +0 -0
  393. /package/dist/app/chunks/{nix-Q6FYQZLX.js → nix-QGWLUUMZ.js} +0 -0
  394. /package/dist/app/chunks/{powershell-L4KJJ6DK.js → powershell-7GSBBEFF.js} +0 -0
  395. /package/dist/app/chunks/{prisma-IKS4LVH2.js → prisma-FPMPQISO.js} +0 -0
  396. /package/dist/app/chunks/{proto-AYCDOTJO.js → proto-7KDX5Z2U.js} +0 -0
  397. /package/dist/app/chunks/{python-KXMCSAFQ.js → python-CFG4FTQW.js} +0 -0
  398. /package/dist/app/chunks/{rust-J3BC3P6V.js → rust-X7A37WU3.js} +0 -0
  399. /package/dist/app/chunks/{scala-AYIIHBJK.js → scala-OYHJSUG2.js} +0 -0
  400. /package/dist/app/chunks/{swift-QPHHCQD3.js → swift-PE7VNYW5.js} +0 -0
  401. /package/dist/app/chunks/{toml-UV7JK7K7.js → toml-ETQRDILP.js} +0 -0
@@ -1,7 +1,10 @@
1
- import { create } from 'zustand';
1
+ import { createContext, createElement, useContext, useLayoutEffect, useMemo, type ReactNode } from 'react';
2
+ import { useStore } from 'zustand';
2
3
  import { persist, createJSONStorage } from 'zustand/middleware';
4
+ import { createStore, type StoreApi as ZustandStoreApi } from 'zustand/vanilla';
3
5
  import type { ClientStateResetScope } from '@neuralis/package-system/contracts';
4
- import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, SidebarView, WorkspacePanelId } from '../domain/types';
6
+ import { createPackageApi, type PackageApi } from '@neuralis/package-system/client';
7
+ import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, PersistedFilesWindow, SidebarView, WorkspacePanelId } from '../domain/types';
5
8
  export type { SidebarView, WorkspacePanelId } from '../domain/types';
6
9
  import { countPendingChanges, readFiles, uploadFile, writeFile, writeRefusalMessage } from '../api';
7
10
  import {
@@ -42,44 +45,6 @@ import { gitSliceInitial, createGitSlice, type GitSliceState, type GitSliceActio
42
45
  */
43
46
  export const FILES_LRU_HARD_CAP = 50_000;
44
47
 
45
- /**
46
- * Bumped by `fenceInFlightWriters` — the ONE fence BOTH reset paths run before
47
- * their wipe: `resetClientState` (the dock broom) and `loadList`'s agent/project
48
- * switch. Every async writer that materialises a node or content AFTER an await
49
- * (the `openFile` read, a reconcile pass's `set`, the optimistic folder in
50
- * `createNode`, the editor tails `saveFileInPlace` / `markFileDeleted`) captures
51
- * it BEFORE the await and writes only when it still matches — a read, listing
52
- * or mutation answer that resolves after the reset must not put a tab, its
53
- * folder chain, its content or its parent's dirty flag into a store that now
54
- * belongs to another context. Module-level like the generation map: the reset
55
- * is a store-wide event. Until 2026-09-04 only the broom fenced; the switch
56
- * wiped without fencing, so a read a tab click had started under the previous
57
- * agent could land its file — and `activeFile` — under the new one.
58
- *
59
- * Writers that need NO epoch (documented, not oversight): `moveFileToFolder` /
60
- * `renameNode` / `pasteNodes` / `deleteNodes` / `upload*` are no-ops on an empty
61
- * store (`replaceFilePath`, `prunePrefix`, `patchFileNode`, the `openFiles.map`
62
- * all find nothing) or set a hint flag; a late `loadList` answer writes
63
- * `brain://` + `sources`, which the remounted widget re-requests anyway; late
64
- * `runSearch` / `loadGitStatus` / `loadGitBranches` answers are overwritten on
65
- * the next use; `ensureFolderLoaded`'s superseded and catch branches write
66
- * `hydrationState[uri]` only (`'idle'` / `'error'` for a folder row that is gone);
67
- * the SSE handlers are synchronous and die with the widget; `applyNav` /
68
- * `autoExpandToFile` write synchronously before any await.
69
- */
70
- let resetEpoch = 0;
71
- /** The newest `refreshPendingCount` call — an older answer never overwrites a newer one. */
72
- let pendingCountSeq = 0;
73
- /**
74
- * Tabs whose rehydrate read is in flight — a second activation must not issue a
75
- * second read. Keyed by uri, so the fence CLEARS it: the same uri under another
76
- * context is another file, and a stale key would suppress the new context's read
77
- * for good (the old answer is fenced out, the baseline never arrives).
78
- */
79
- const rehydrateInFlight = new Set<string>();
80
-
81
- const liveFolderLoadInFlight = new Map<string, Promise<void>>();
82
-
83
48
  /**
84
49
  * Per-source reconcile job. The guard used to DROP a request that arrived while a
85
50
  * reconcile was running, which made every mutation tail unsafe: the drop silently
@@ -89,29 +54,78 @@ const liveFolderLoadInFlight = new Map<string, Promise<void>>();
89
54
  * and the caller awaits the whole chain.
90
55
  */
91
56
  type ReconcileJob = { rerun: boolean; wide: boolean; extra: Set<string>; promise: Promise<void> };
92
- const reconcileJobs = new Map<string, ReconcileJob>();
93
57
 
94
58
  /** Bound on coalesced reruns so a hot event stream can't spin the loop forever. */
95
59
  const MAX_RECONCILE_PASSES = 3;
96
60
 
97
61
  /**
98
- * Monotonic list-generation token PER FOLDER (not per source). Two listings of the
99
- * SAME folder can be in flight at once — `reconcileSourceFolders` lists its targets
100
- * concurrently and `ensureFolderLoaded` can overlap it — so a slow response must not
101
- * overwrite a newer one. Every list issue bumps its folder's counter; a response
102
- * whose token is no longer current is discarded. A per-SOURCE token would not catch
103
- * this: both listings share the source.
62
+ * The asynchronous fences of ONE Files store — one set per window, created with
63
+ * it (`createFilesystemStore`). Two windows never share a fence: a reset or a
64
+ * context switch is that window's event, so it fences only that window's
65
+ * in-flight work, and a sibling window's listing can never supersede this one's.
66
+ *
67
+ * - `resetEpoch` — bumped by `fenceInFlightWriters`, the ONE fence BOTH reset
68
+ * paths run before their wipe: `resetClientState` (the dock broom) and
69
+ * `loadList`'s context switch. Every async writer that materialises a node or
70
+ * content AFTER an await (the `openFile` read, a reconcile pass's `set`, the
71
+ * optimistic folder in `createNode`, the editor tails `saveFileInPlace` /
72
+ * `markFileDeleted`) captures it BEFORE the await and writes only when it still
73
+ * matches — a read, listing or mutation answer that resolves after the reset
74
+ * must not put a tab, its folder chain, its content or its parent's dirty flag
75
+ * into a store that now belongs to another context. Writers that need NO epoch
76
+ * (documented, not oversight): `moveFileToFolder` / `renameNode` / `pasteNodes`
77
+ * / `deleteNodes` / `upload*` are no-ops on an empty store (`replaceFilePath`,
78
+ * `prunePrefix`, `patchFileNode`, the `openFiles.map` all find nothing) or set
79
+ * a hint flag; a late `loadList` answer writes `brain://` + `sources`, which the
80
+ * remounted widget re-requests anyway; late `runSearch` / `loadGitStatus` /
81
+ * `loadGitBranches` answers are overwritten on the next use;
82
+ * `ensureFolderLoaded`'s superseded and catch branches write
83
+ * `hydrationState[uri]` only; the SSE handlers are synchronous and die with the
84
+ * widget; `applyNav` / `autoExpandToFile` write synchronously before any await.
85
+ * - `pendingCountSeq` — the newest `refreshPendingCount` call; an older answer
86
+ * never overwrites a newer one.
87
+ * - `rehydrateInFlight` — tabs whose rehydrate read is in flight, so a second
88
+ * activation issues no second read. Keyed by uri, so the fence CLEARS it: the
89
+ * same uri under another context is another file, and a stale key would
90
+ * suppress the new context's read for good.
91
+ * - `liveFolderLoadInFlight` — the folder-listing dedup map.
92
+ * - `reconcileJobs` — the per-source coalescer (`ReconcileJob`).
93
+ * - `folderGeneration` — a monotonic list-generation token PER FOLDER (not per
94
+ * source). Two listings of the SAME folder can be in flight at once —
95
+ * `reconcileSourceFolders` lists its targets concurrently and
96
+ * `ensureFolderLoaded` can overlap it — so a slow response must not overwrite a
97
+ * newer one. Every list issue bumps its folder's counter; a response whose
98
+ * token is no longer current is discarded. A per-SOURCE token would not catch
99
+ * this: both listings share the source.
104
100
  */
105
- const folderGeneration = new Map<string, number>();
101
+ type StoreFences = {
102
+ resetEpoch: number;
103
+ pendingCountSeq: number;
104
+ rehydrateInFlight: Set<string>;
105
+ liveFolderLoadInFlight: Map<string, Promise<void>>;
106
+ reconcileJobs: Map<string, ReconcileJob>;
107
+ folderGeneration: Map<string, number>;
108
+ };
106
109
 
107
- function nextFolderGeneration(folderKey: string): number {
108
- const gen = (folderGeneration.get(folderKey) ?? 0) + 1;
109
- folderGeneration.set(folderKey, gen);
110
+ function createStoreFences(): StoreFences {
111
+ return {
112
+ resetEpoch: 0,
113
+ pendingCountSeq: 0,
114
+ rehydrateInFlight: new Set<string>(),
115
+ liveFolderLoadInFlight: new Map<string, Promise<void>>(),
116
+ reconcileJobs: new Map<string, ReconcileJob>(),
117
+ folderGeneration: new Map<string, number>(),
118
+ };
119
+ }
120
+
121
+ function nextFolderGeneration(fences: StoreFences, folderKey: string): number {
122
+ const gen = (fences.folderGeneration.get(folderKey) ?? 0) + 1;
123
+ fences.folderGeneration.set(folderKey, gen);
110
124
  return gen;
111
125
  }
112
126
 
113
- function isCurrentFolderGeneration(folderKey: string, gen: number): boolean {
114
- return folderGeneration.get(folderKey) === gen;
127
+ function isCurrentFolderGeneration(fences: StoreFences, folderKey: string, gen: number): boolean {
128
+ return fences.folderGeneration.get(folderKey) === gen;
115
129
  }
116
130
 
117
131
  /** `pendingReviewPaths` transitions the shared mapper observed, applied by the caller. */
@@ -280,8 +294,8 @@ function withPendingDelta(
280
294
  return out;
281
295
  }
282
296
 
283
- /** Minimal store handle the module-level reconcile engine needs. */
284
- type StoreApi = { get: () => any; set: (fn: (s: any) => any) => void };
297
+ /** The store handle the module-level reconcile engine needs: state, its window's fences and request client. */
298
+ type StoreApi = { get: () => any; set: (fn: (s: any) => any) => void; fences: StoreFences; client: PackageApi };
285
299
 
286
300
  /**
287
301
  * How many folder listings one reconcile pass has in flight. Each costs a server
@@ -399,7 +413,7 @@ async function runReconcilePass(
399
413
  // Captured at pass START: the per-folder generation guard sits in the worker,
400
414
  // so a listing set that landed BEFORE a broom would still be written by the
401
415
  // `set()` below unless the pass itself is fenced.
402
- const epoch = resetEpoch;
416
+ const epoch = api.fences.resetEpoch;
403
417
  const state0 = api.get();
404
418
  const lf = state0.loadedFolders || {};
405
419
  const targetSet = new Set<string>();
@@ -442,12 +456,12 @@ async function runReconcilePass(
442
456
  // walk plus an index pass. A tree-wide Refresh across several sources multiplies
443
457
  // that again.
444
458
  const listed = await mapWithLimit(targets, RECONCILE_LIST_CONCURRENCY, async (folderKey) => {
445
- const gen = nextFolderGeneration(folderKey);
459
+ const gen = nextFolderGeneration(api.fences, folderKey);
446
460
  const issuedAt = Date.now();
447
461
  try {
448
- const res = await resolveList(agentId, projectId ?? undefined, { uri: folderKey, recursive: false });
462
+ const res = await resolveList(agentId, projectId ?? undefined, { uri: folderKey, recursive: false }, api.client);
449
463
  // A newer listing of the SAME folder superseded this one → drop it whole.
450
- if (!isCurrentFolderGeneration(folderKey, gen)) return { folderKey, res: null, issuedAt, ok: false as const };
464
+ if (!isCurrentFolderGeneration(api.fences, folderKey, gen)) return { folderKey, res: null, issuedAt, ok: false as const };
451
465
  return { folderKey, res, issuedAt, ok: true as const };
452
466
  } catch {
453
467
  return { folderKey, res: null, issuedAt, ok: false as const };
@@ -455,7 +469,7 @@ async function runReconcilePass(
455
469
  });
456
470
 
457
471
  api.set((state: any) => {
458
- if (epoch !== resetEpoch) return state; // the broom emptied the store mid-pass → inert
472
+ if (epoch !== api.fences.resetEpoch) return state; // the broom emptied the store mid-pass → inert
459
473
  const next = { ...state.files };
460
474
  const ts = Date.now();
461
475
  const pendingDelta: ListedPendingDelta = { add: new Set(), remove: new Set() };
@@ -612,7 +626,7 @@ async function runSourceReconcile(
612
626
  const { agentId, projectId, source, targets } = params;
613
627
  const wide = params.wide !== false;
614
628
  if (!agentId || !source) return;
615
- const existing = reconcileJobs.get(source);
629
+ const existing = api.fences.reconcileJobs.get(source);
616
630
  if (existing) {
617
631
  existing.rerun = true;
618
632
  if (wide) existing.wide = true;
@@ -621,7 +635,7 @@ async function runSourceReconcile(
621
635
  return;
622
636
  }
623
637
  const job: ReconcileJob = { rerun: false, wide, extra: new Set(targets ?? []), promise: Promise.resolve() };
624
- reconcileJobs.set(source, job);
638
+ api.fences.reconcileJobs.set(source, job);
625
639
  job.promise = (async () => {
626
640
  try {
627
641
  for (let pass = 0; pass < MAX_RECONCILE_PASSES; pass++) {
@@ -634,7 +648,7 @@ async function runSourceReconcile(
634
648
  if (!job.rerun) break;
635
649
  }
636
650
  } finally {
637
- reconcileJobs.delete(source);
651
+ api.fences.reconcileJobs.delete(source);
638
652
  }
639
653
  })();
640
654
  await job.promise;
@@ -756,7 +770,7 @@ function evictColdSubtrees(
756
770
  }
757
771
 
758
772
  type FilesystemNavRef =
759
- | { kind: 'filesystem'; action: string; uri?: string; query?: string; filter?: PendingPanelFilter }
773
+ | { kind: 'filesystem'; action: string; uri?: string; query?: string; filter?: PendingPanelFilter; source?: string }
760
774
  | undefined;
761
775
 
762
776
 
@@ -837,9 +851,11 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
837
851
  refreshTree: (params: { agentId: string; projectId: string | null }) => Promise<void>;
838
852
  /**
839
853
  * The dock broom's reach into this store (host → kernel port
840
- * `onClientStateReset` → here). Matches the scope against the ACTIVE
841
- * `(activeProjectId, activeAgentId)` pair (widget level: only the
842
- * `filesystem` widget type); a non-matching scope is a no-op. A match applies
854
+ * `onClientStateReset` → `resetFilesClientState` → here). Matches the scope
855
+ * against the ACTIVE `(activeProjectId, activeAgentId)` pair; the `instance`
856
+ * and `widget` levels also need the `filesystem` widget type AND this store's
857
+ * own `windowId` — one window's clean or close never empties a sibling
858
+ * window, of the same agent included. A non-matching scope is a no-op. A match applies
843
859
  * the ONE `contextResetPatch` (the same wipe an agent/project switch does),
844
860
  * clears the persisted tab strip (`tabMeta` included — the allowlist is written
845
861
  * EMPTY, so a persisted `activeFile` cannot re-run the read/list race on the
@@ -856,6 +872,18 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
856
872
  * through `get()` — never a second copy.
857
873
  */
858
874
  fenceInFlightWriters: () => void;
875
+ /**
876
+ * The host's id of the Files window this store belongs to (`null` for a store
877
+ * no window owns). Identity like `activeAgentId`: no reset touches it, and it
878
+ * is never an authority source — it only addresses an `instance`/`widget` reset.
879
+ */
880
+ windowId: string | null;
881
+ /**
882
+ * The request client every API call of this store goes through — created with
883
+ * the store over the window's project and BOUND agent, so a request's headers
884
+ * never come from another mounted window (`api/index.ts`).
885
+ */
886
+ client: PackageApi;
859
887
  /** Dismiss the structural-operation banner (`organizeError`). */
860
888
  clearOrganizeError: () => void;
861
889
  /**
@@ -897,6 +925,14 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
897
925
  */
898
926
  pendingSourceSuggestion: DiscoverableSourceSuggestion | null;
899
927
  setPendingSourceSuggestion: (suggestion: DiscoverableSourceSuggestion | null) => void;
928
+ /**
929
+ * Is the Sources panel's Vector Health section open? In the STORE so a
930
+ * notification's `open-vector-health` nav can open it while the panel is not
931
+ * mounted; context data (a `contextResetPatch` key), never persisted. The
932
+ * section still mounts its panel only while open (no CSS-only collapse).
933
+ */
934
+ vectorHealthOpen: boolean;
935
+ setVectorHealthOpen: (open: boolean) => void;
900
936
  setWorkspacePanelOpen: (panel: WorkspacePanelId, open: boolean) => void;
901
937
  setActiveWorkspaceTab: (tab: 'file' | 'git-diff' | 'git-commit' | WorkspacePanelId) => void;
902
938
  diffViewMode: DiffViewMode;
@@ -1070,7 +1106,11 @@ function readStoredSidebarWidth(): number {
1070
1106
  }
1071
1107
  }
1072
1108
 
1073
- /** The localStorage payload (v2): prefs, the active triple, the context projections. */
1109
+ /**
1110
+ * The localStorage payload (v2): prefs, the active triple, the per-agent context
1111
+ * projections, and — OPTIONAL, absent until a window writes one — the per-window
1112
+ * projections keyed by the host's window id (`PersistedFilesWindow`).
1113
+ */
1074
1114
  type PersistedFilesShape = {
1075
1115
  previewEditorsEnabled: boolean;
1076
1116
  diffViewMode: DiffViewMode;
@@ -1078,9 +1118,39 @@ type PersistedFilesShape = {
1078
1118
  activeAgentId: string | null;
1079
1119
  activeProjectId: string | null;
1080
1120
  contexts: Record<string, PersistedFilesContext>;
1121
+ windows?: Record<string, PersistedFilesWindow>;
1122
+ };
1123
+
1124
+ /** What a new store starts from: its window, its request client, the preferences and the seeded view. */
1125
+ export type FilesystemStoreInit = {
1126
+ /** The request client every API call of this store goes through (see `FilesystemStore.client`). */
1127
+ client: PackageApi;
1128
+ windowId?: string | null;
1129
+ userId?: string | null;
1130
+ projectId?: string | null;
1131
+ agentId?: string | null;
1132
+ previewEditorsEnabled?: boolean;
1133
+ diffViewMode?: DiffViewMode;
1134
+ /** The window's persisted view — its own projection (a new window has none and starts from the defaults). */
1135
+ projection?: Partial<PersistedFilesContext>;
1081
1136
  };
1082
1137
 
1083
- export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) => ({
1138
+ export type FilesystemStoreApi = ZustandStoreApi<FilesystemStore>;
1139
+
1140
+ /**
1141
+ * ONE Files store per window: a live view that persists nothing itself — the
1142
+ * ONE writer of `neuralis.filesystem.tabs` is `filesPersistence` below, which a
1143
+ * mounted window feeds through `mountFilesWindow`. Every asynchronous fence and
1144
+ * dedup map is created with the store (`StoreFences`, the list/search/git slice
1145
+ * closures), so two windows — of two agents, or of the same agent — never fence,
1146
+ * supersede or dedup each other.
1147
+ */
1148
+ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStoreApi {
1149
+ const fences = createStoreFences();
1150
+ const client = init.client;
1151
+ return createStore<FilesystemStore>()((set, get) => ({
1152
+ windowId: init.windowId ?? null,
1153
+ client,
1084
1154
  files: {},
1085
1155
  folderBadges: {},
1086
1156
  fileContentByPath: {},
@@ -1090,17 +1160,17 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1090
1160
  openFiles: [],
1091
1161
  activeFile: null,
1092
1162
  tabMeta: {},
1093
- activeUserId: null,
1163
+ activeUserId: init.userId ?? null,
1094
1164
  contextStateByKey: {},
1095
- previewEditorsEnabled: false,
1165
+ previewEditorsEnabled: init.previewEditorsEnabled ?? false,
1096
1166
  // Nothing is pre-expanded — under lazy hydration an expanded entry is a FETCH,
1097
1167
  // and every source branch (including `brain://`) starts collapsed.
1098
1168
  expandedFolders: new Set<string>(),
1099
1169
  sidebarOpen: true,
1100
1170
  sidebarWidth: readStoredSidebarWidth(),
1101
1171
  sidebarView: 'tree',
1102
- activeAgentId: null,
1103
- activeProjectId: null,
1172
+ activeAgentId: init.agentId ?? null,
1173
+ activeProjectId: init.projectId ?? null,
1104
1174
  sources: [],
1105
1175
  sourcesLoading: true,
1106
1176
  activeSource: null,
@@ -1110,7 +1180,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1110
1180
  workspacePanels: { upload: false, sources: false, packages: false, pending: false },
1111
1181
  activeWorkspaceTab: 'file',
1112
1182
  pendingSourceSuggestion: null,
1113
- diffViewMode: 'auto',
1183
+ vectorHealthOpen: false,
1184
+ diffViewMode: init.diffViewMode ?? 'auto',
1114
1185
  pendingPanelFilter: {},
1115
1186
  // Nothing is hydrated at boot. `brain://` is EXPANDED by default (below) and the
1116
1187
  // tree's hydration effect lists it — marking it "loaded" here would suppress that
@@ -1180,6 +1251,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1180
1251
  return { workspacePanels: nextPanels, activeWorkspaceTab };
1181
1252
  }),
1182
1253
  setPendingSourceSuggestion: (suggestion) => set(() => ({ pendingSourceSuggestion: suggestion })),
1254
+ setVectorHealthOpen: (open) => set(() => ({ vectorHealthOpen: open })),
1183
1255
  setActiveWorkspaceTab: (tab) => set(() => ({ activeWorkspaceTab: tab })),
1184
1256
  setDiffViewMode: (mode) => set(() => ({ diffViewMode: mode })),
1185
1257
  setPreviewEditorsEnabled: (enabled) => set(() => ({ previewEditorsEnabled: enabled })),
@@ -1259,28 +1331,28 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1259
1331
  // Enumerated once, then hidden — no reconcile listed it off screen, so its rows
1260
1332
  // may be stale. ONE authoritative listing of exactly this folder (absence
1261
1333
  // applies), never a re-list of the whole visible tree.
1262
- await runSourceReconcile({ get, set }, { agentId, projectId, source: sourceFromUri(uri), targets: [uri], wide: false });
1334
+ await runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source: sourceFromUri(uri), targets: [uri], wide: false });
1263
1335
  return;
1264
1336
  }
1265
1337
 
1266
- const existing = liveFolderLoadInFlight.get(uri);
1338
+ const existing = fences.liveFolderLoadInFlight.get(uri);
1267
1339
  if (existing) { await existing; return; }
1268
1340
 
1269
1341
  set((state: any) => ({ hydrationState: { ...(state.hydrationState || {}), [uri]: 'loading' as const } }));
1270
1342
 
1271
1343
  const task = (async () => {
1272
- const gen = nextFolderGeneration(uri);
1344
+ const gen = nextFolderGeneration(fences, uri);
1273
1345
  const issuedAt = Date.now();
1274
1346
  try {
1275
1347
  // recursive:false → single `readdir` on the connector, ~50ms even on
1276
1348
  // million-file subtrees. Replaces the previous full-tree snapshot call.
1277
- const res = await resolveList(agentId, projectId ?? undefined, { uri, recursive: false });
1349
+ const res = await resolveList(agentId, projectId ?? undefined, { uri, recursive: false }, client);
1278
1350
  // Superseded by a newer listing of the same folder (a concurrent reconcile
1279
1351
  // or a second expand) → drop this response instead of overwriting. Restore
1280
1352
  // the fetch state on the way out: returning bare left `loading` pinned
1281
1353
  // forever, and the refresh control is `disabled={loading}` — the one control
1282
1354
  // that could recover would have been disabled by the state it must clear.
1283
- if (!isCurrentFolderGeneration(uri, gen)) {
1355
+ if (!isCurrentFolderGeneration(fences, uri, gen)) {
1284
1356
  set((state: any) => ({
1285
1357
  hydrationState: {
1286
1358
  ...(state.hydrationState || {}),
@@ -1317,8 +1389,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1317
1389
  }
1318
1390
  })();
1319
1391
 
1320
- liveFolderLoadInFlight.set(uri, task);
1321
- try { await task; } finally { liveFolderLoadInFlight.delete(uri); }
1392
+ fences.liveFolderLoadInFlight.set(uri, task);
1393
+ try { await task; } finally { fences.liveFolderLoadInFlight.delete(uri); }
1322
1394
  },
1323
1395
 
1324
1396
  clearOrganizeError: () => set(() => ({ organizeError: null })),
@@ -1335,31 +1407,32 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1335
1407
  }),
1336
1408
 
1337
1409
  async reconcileSourceFolders({ agentId, projectId, source }) {
1338
- await runSourceReconcile({ get, set }, { agentId, projectId, source });
1410
+ await runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source });
1339
1411
  },
1340
1412
 
1341
1413
  fenceInFlightWriters() {
1342
- resetEpoch += 1;
1343
- for (const key of Array.from(folderGeneration.keys())) nextFolderGeneration(key);
1414
+ fences.resetEpoch += 1;
1415
+ for (const key of Array.from(fences.folderGeneration.keys())) nextFolderGeneration(fences, key);
1344
1416
  // The dedup maps only; the in-flight tasks' own `finally` deletes a missing
1345
1417
  // key as a no-op. `reconcileJobs` is deliberately NOT cleared: a running job's
1346
1418
  // `finally` would then delete the NEXT job's entry and two passes would run
1347
1419
  // concurrently — the job runs to completion and the epoch makes it inert.
1348
- liveFolderLoadInFlight.clear();
1349
- rehydrateInFlight.clear();
1420
+ fences.liveFolderLoadInFlight.clear();
1421
+ fences.rehydrateInFlight.clear();
1350
1422
  },
1351
1423
 
1352
1424
  resetClientState(scope) {
1353
1425
  const s = get();
1354
1426
  if (!s.activeProjectId || scope.projectId !== s.activeProjectId) return;
1355
1427
  if (scope.level !== 'project' && scope.agentId !== s.activeAgentId) return;
1356
- if (scope.level === 'widget' && scope.widgetType !== FILESYSTEM_WIDGET_TYPE) return;
1428
+ if ((scope.level === 'widget' || scope.level === 'instance')
1429
+ && (scope.widgetType !== FILESYSTEM_WIDGET_TYPE || scope.instanceId !== s.windowId)) return;
1357
1430
  // Fence FIRST, then wipe: a read/listing/reconcile that resolves after this
1358
1431
  // point finds a stale epoch (or a stale folder generation) and writes nothing.
1359
1432
  get().fenceInFlightWriters();
1360
1433
  // The broom is the ONE writer that drops a stashed context — its own scope
1361
- // only: the widget/agent levels drop this context, the project level every
1362
- // context of the project (this user's). Other projects are not its business.
1434
+ // only: the instance/widget/agent levels drop this context, the project level
1435
+ // every context of the project (this user's). Other projects are not its business.
1363
1436
  const prefix = `${s.activeUserId ?? ''}::${s.activeProjectId}::`;
1364
1437
  const own = contextKey(s.activeUserId, s.activeProjectId, s.activeAgentId ?? '');
1365
1438
  const kept: Record<string, Record<string, unknown>> = {};
@@ -1398,7 +1471,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1398
1471
  if (bySource.size === 0) return;
1399
1472
  await Promise.all(
1400
1473
  Array.from(bySource.entries()).map(([source, targets]) =>
1401
- runSourceReconcile({ get, set }, { agentId, projectId, source, targets }),
1474
+ runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source, targets }),
1402
1475
  ),
1403
1476
  );
1404
1477
  },
@@ -1420,7 +1493,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1420
1493
  if (source) sources.add(source);
1421
1494
  }
1422
1495
  await Promise.all(
1423
- Array.from(sources).map((source) => runSourceReconcile({ get, set }, { agentId, projectId, source })),
1496
+ Array.from(sources).map((source) => runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source })),
1424
1497
  );
1425
1498
  },
1426
1499
 
@@ -1435,8 +1508,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1435
1508
  const agentId = s.activeAgentId;
1436
1509
  if (!uri || !agentId) return;
1437
1510
  if (hasCompletedRead(s, uri)) return;
1438
- if (rehydrateInFlight.has(uri)) return;
1439
- rehydrateInFlight.add(uri);
1511
+ if (fences.rehydrateInFlight.has(uri)) return;
1512
+ fences.rehydrateInFlight.add(uri);
1440
1513
  try {
1441
1514
  // A tab from another agent's source reads through that agent, as the tree
1442
1515
  // open did — never through a SENTINEL (`__system__` is the writer the
@@ -1449,7 +1522,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1449
1522
  // rehydrate is not a user open.
1450
1523
  await s.openFile({ agentId, projectId: s.activeProjectId, uri, readAgentId, revealPane: false });
1451
1524
  } finally {
1452
- rehydrateInFlight.delete(uri);
1525
+ fences.rehydrateInFlight.delete(uri);
1453
1526
  }
1454
1527
  },
1455
1528
 
@@ -1669,8 +1742,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1669
1742
  // The server-side work completes whatever happens to the store; every store
1670
1743
  // write after an await is fenced — a context reset mid-save must not re-key,
1671
1744
  // fill, dirty-flag or re-open anything under the new context.
1672
- const epoch = resetEpoch;
1673
- const fenced = () => epoch !== resetEpoch;
1745
+ const epoch = fences.resetEpoch;
1746
+ const fenced = () => epoch !== fences.resetEpoch;
1674
1747
 
1675
1748
  let effectiveUri = fromUri;
1676
1749
  if (moved) {
@@ -1680,7 +1753,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1680
1753
  id: node.id,
1681
1754
  uri: fromUri,
1682
1755
  move: targetUri,
1683
- });
1756
+ }, client);
1684
1757
  if (!res || res.ok !== true) throw new Error(writeRefusalMessage(res, 'Move failed'));
1685
1758
  effectiveUri = targetUri;
1686
1759
  if (fenced()) return { uri: effectiveUri, moved, contentChanged: false };
@@ -1694,7 +1767,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1694
1767
  id: node.id,
1695
1768
  uri: effectiveUri,
1696
1769
  overwrite: { content },
1697
- });
1770
+ }, client);
1698
1771
  if (!res || res.ok !== true) throw new Error(writeRefusalMessage(res, 'Save failed'));
1699
1772
  if (fenced()) return { uri: effectiveUri, moved, contentChanged };
1700
1773
  // After a move the node lives under its NEW uri (`replaceFilePath` re-keys
@@ -1716,32 +1789,32 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1716
1789
 
1717
1790
  async refreshPendingCount({ agentId, projectId }) {
1718
1791
  if (!agentId) return;
1719
- const mine = ++pendingCountSeq;
1792
+ const mine = ++fences.pendingCountSeq;
1720
1793
  // A failed count keeps the last known number — the next event, focus or reconnect re-asks.
1721
1794
  let count: number;
1722
- try { count = await countPendingChanges({ agentId, ...(projectId ? { projectId } : {}) }); } catch { return; }
1795
+ try { count = await countPendingChanges({ agentId, ...(projectId ? { projectId } : {}) }, client); } catch { return; }
1723
1796
  // No reset-epoch fence: the count is the context's own server answer, so one
1724
1797
  // that resolves after a reset of the SAME context is still right; the context
1725
1798
  // check below is what keeps another context's number out.
1726
- if (mine !== pendingCountSeq) return;
1799
+ if (mine !== fences.pendingCountSeq) return;
1727
1800
  const s = get();
1728
1801
  if (s.activeAgentId !== agentId || s.activeProjectId !== projectId) return;
1729
1802
  set(() => ({ pendingCount: count }));
1730
1803
  },
1731
1804
 
1732
1805
  async markFileDeleted({ agentId, projectId, node }) {
1733
- const epoch = resetEpoch;
1806
+ const epoch = fences.resetEpoch;
1734
1807
  const res = await writeFile({
1735
1808
  agentId,
1736
1809
  ...(projectId ? { projectId } : {}),
1737
1810
  id: node.id,
1738
1811
  uri: node.uri,
1739
1812
  delete: true,
1740
- });
1813
+ }, client);
1741
1814
  if (!res || res.ok !== true) throw new Error(writeRefusalMessage(res, 'Delete failed'));
1742
1815
  // The reconcile flags an unloaded source dirty — not a no-op on a store the
1743
1816
  // reset just handed to another context.
1744
- if (epoch !== resetEpoch) return;
1817
+ if (epoch !== fences.resetEpoch) return;
1745
1818
  await get().reconcileFolders({ agentId, projectId, folders: [folderUri(node.uri)] });
1746
1819
  },
1747
1820
 
@@ -1762,7 +1835,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1762
1835
  id,
1763
1836
  uri: fromUri,
1764
1837
  move: destinationUri,
1765
- });
1838
+ }, client);
1766
1839
  } catch (e) {
1767
1840
  console.error('[Filesystem] move failed', fromUri, e);
1768
1841
  set(() => ({ organizeError: 'Move failed' }));
@@ -1791,7 +1864,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1791
1864
  // of the old subtree would stay in the store forever. Prune by prefix (the
1792
1865
  // pattern `renameNode` already proved) and force-relist the destination.
1793
1866
  if (!retained) {
1794
- prunePrefix({ get, set }, fromUri);
1867
+ prunePrefix({ get, set, fences, client }, fromUri);
1795
1868
  }
1796
1869
  await get().ensureFolderLoaded({ agentId, projectId, uri: toFolderUri, force: true });
1797
1870
  } else {
@@ -1832,7 +1905,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1832
1905
  id,
1833
1906
  uri,
1834
1907
  move: isFolder ? destinationUri : trimmed,
1835
- });
1908
+ }, client);
1836
1909
  } catch (e) {
1837
1910
  console.error('[Filesystem] rename failed', uri, e);
1838
1911
  set(() => ({ organizeError: 'Rename failed' }));
@@ -1851,7 +1924,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1851
1924
  // tombstoned the old one. Prune the stale old subtree from the store and
1852
1925
  // force-relist the parent so the renamed folder surfaces immediately (the
1853
1926
  // non-recursive `refresh` alone would not re-list a nested folder's parent).
1854
- prunePrefix({ get, set }, uri);
1927
+ prunePrefix({ get, set, fences, client }, uri);
1855
1928
  await get().ensureFolderLoaded({ agentId, projectId, uri: parent, force: true });
1856
1929
  } else {
1857
1930
  get().replaceFilePath(uri, destinationUri);
@@ -1863,11 +1936,11 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1863
1936
  const trimmed = (name ?? '').trim();
1864
1937
  if (!agentId || !parentUri || !trimmed) return;
1865
1938
  const targetUri = parentUri.endsWith('://') ? `${parentUri}${trimmed}` : `${parentUri}/${trimmed}`;
1866
- const epoch = resetEpoch;
1939
+ const epoch = fences.resetEpoch;
1867
1940
  try {
1868
1941
  if (kind === 'folder') {
1869
- const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: targetUri, mkdir: true });
1870
- if (res?.ok && epoch === resetEpoch) {
1942
+ const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: targetUri, mkdir: true }, client);
1943
+ if (res?.ok && epoch === fences.resetEpoch) {
1871
1944
  // Optimistic folder node — an EMPTY folder has no vector node, so only a
1872
1945
  // connector-live re-list surfaces it; seed it now so it appears instantly.
1873
1946
  set((state) => {
@@ -1898,7 +1971,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1898
1971
  file: new File([''], trimmed),
1899
1972
  uri: parentUri,
1900
1973
  tags: [],
1901
- });
1974
+ }, client);
1902
1975
  }
1903
1976
  } catch (e) {
1904
1977
  console.error('[Filesystem] create failed', kind, targetUri, e);
@@ -1921,11 +1994,11 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1921
1994
  touched.add(folderUri(src));
1922
1995
  try {
1923
1996
  if (op === 'copy') {
1924
- const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, copy: dest });
1997
+ const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, copy: dest }, client);
1925
1998
  if (res && res.ok === false) rejected.push(base);
1926
1999
  } else {
1927
2000
  // 200-with-`ok:false` is a REJECTED move — collect it, never treat it as done.
1928
- const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, move: dest });
2001
+ const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, move: dest }, client);
1929
2002
  if (res && res.ok === false) rejected.push(base);
1930
2003
  else if (res?.warning === 'moved_with_source_retained') rejected.push(`${base} (original retained)`);
1931
2004
  else if (get().files[src]?.type === 'folder') {
@@ -1934,7 +2007,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1934
2007
  // inspects direct children — so the old prefix has to go here or its
1935
2008
  // interior lingers. Cut/paste reaches this path too, and only the drag
1936
2009
  // path used to handle it.
1937
- prunePrefix({ get, set }, src);
2010
+ prunePrefix({ get, set, fences, client }, src);
1938
2011
  }
1939
2012
  }
1940
2013
  } catch (e) {
@@ -1964,7 +2037,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1964
2037
  id: t.id,
1965
2038
  uri: t.uri,
1966
2039
  delete: true,
1967
- });
2040
+ }, client);
1968
2041
  if (res && res.ok === false) {
1969
2042
  // 200-with-`ok:false`. A FOLDER delete can still have removed N files
1970
2043
  // before failing — `affected`/`failed` carry that partial outcome, and
@@ -1980,7 +2053,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1980
2053
  if (isFolder) {
1981
2054
  // A folder delete removes the whole subtree server-side; prune by prefix so
1982
2055
  // the interior cannot linger (the reconcile only inspects direct children).
1983
- prunePrefix({ get, set }, t.uri);
2056
+ prunePrefix({ get, set, fences, client }, t.uri);
1984
2057
  } else {
1985
2058
  // Optimistic: strike the node (revertible tombstone). The targeted
1986
2059
  // reconcile below converges to server truth — a tracked node KEEPS the
@@ -2064,16 +2137,16 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2064
2137
  (typeof st.fileContentByPath[uri] === 'string' && (st.fileContentByPath[uri]?.length || 0) > 0);
2065
2138
  if (!force && hasContent) return;
2066
2139
 
2067
- const epoch = resetEpoch;
2140
+ const epoch = fences.resetEpoch;
2068
2141
  try {
2069
2142
  const res = await readFiles({
2070
2143
  agentId: readAgentId || agentId,
2071
2144
  ...(projectId ? { projectId } : {}),
2072
2145
  files: [uri],
2073
- });
2146
+ }, client);
2074
2147
  // The broom ran while the read was in flight: the tab is gone, the store is
2075
2148
  // empty, and this answer must not put either back.
2076
- if (epoch !== resetEpoch) return;
2149
+ if (epoch !== fences.resetEpoch) return;
2077
2150
  const files = extractReadFilesPayload(res);
2078
2151
  if (files.length > 0) {
2079
2152
  const f = files[0];
@@ -2183,7 +2256,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2183
2256
  } catch (e) {
2184
2257
  const message = e instanceof Error ? e.message : 'Read failed';
2185
2258
  console.error('[Filesystem] read failed', e);
2186
- if (epoch !== resetEpoch) return;
2259
+ if (epoch !== fences.resetEpoch) return;
2187
2260
  set((state) => ({
2188
2261
  files: {
2189
2262
  ...state.files,
@@ -2217,6 +2290,15 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2217
2290
  get().setWorkspacePanelOpen('pending', true);
2218
2291
  return;
2219
2292
  }
2293
+ // A notification's target: the Sources panel (a source's sync or embedding
2294
+ // outcome), or its Vector Health section (an operator's collection action).
2295
+ // Navigation only — the panel's own listing re-applies every gate.
2296
+ if (action === 'open-sources' || action === 'open-vector-health') {
2297
+ set(() => ({ activeAgentId: agentId, activeProjectId: projectId,
2298
+ ...(action === 'open-vector-health' ? { vectorHealthOpen: true } : {}) }));
2299
+ get().setWorkspacePanelOpen('sources', true);
2300
+ return;
2301
+ }
2220
2302
  if (action === 'list' && uri) {
2221
2303
  set((state) => {
2222
2304
  const root = sourceRootUri(uri);
@@ -2234,7 +2316,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2234
2316
  const targetUri = uri || 'brain://docs';
2235
2317
  set(() => ({ uploading: true }));
2236
2318
  try {
2237
- await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] });
2319
+ await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] }, client);
2238
2320
  await get().reconcileFolders({ agentId, projectId, folders: [targetUri] });
2239
2321
  } finally {
2240
2322
  set(() => ({ uploading: false }));
@@ -2249,25 +2331,43 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2249
2331
  set(() => ({ uploading: true }));
2250
2332
  try {
2251
2333
  for (const file of list) {
2252
- await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] });
2334
+ await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] }, client);
2253
2335
  }
2254
2336
  await get().reconcileFolders({ agentId, projectId, folders: [targetUri] });
2255
2337
  } finally {
2256
2338
  set(() => ({ uploading: false }));
2257
2339
  }
2258
2340
  },
2341
+ // The window's persisted view, lifted into the flat fields — after every
2342
+ // default above, so a seeded tab strip is what the first render shows.
2343
+ ...(init.projection ? sliceFromPersisted(init.projection) : {}),
2344
+ }));
2345
+ }
2346
+
2347
+ /**
2348
+ * The ONE writer of `neuralis.filesystem.tabs` (v2). Window stores persist
2349
+ * nothing; a mounted window feeds its projection here (`mountFilesWindow`), so
2350
+ * two windows writing the same key can never overwrite each other's view — the
2351
+ * whole record is written from this one in-memory owner.
2352
+ *
2353
+ * What it holds: the preferences, the legacy per-agent `contexts` (the record an
2354
+ * older build wrote — never read into a window, pruned by the broom, otherwise
2355
+ * left as it is: never rewritten, re-keyed or converted), and the optional
2356
+ * `windows` map — one `PersistedFilesWindow` per window, carrying the
2357
+ * user/project/agent it was written for. A record without `windows` reads
2358
+ * without one and is not written back until a window really changes.
2359
+ */
2360
+ export const filesPersistence = createStore<PersistedFilesShape>()(persist((): PersistedFilesShape => ({
2361
+ previewEditorsEnabled: false,
2362
+ diffViewMode: 'auto',
2363
+ activeUserId: null,
2364
+ activeAgentId: null,
2365
+ activeProjectId: null,
2366
+ contexts: {},
2259
2367
  }), {
2260
- // Persist the prefs, the active triple and the CONTEXT SPACE as projections
2261
- // (`PersistedFilesContext` per context — tab shape, expanded folders as an
2262
- // array, sidebar view, panels, last-touched folder): the active context's
2263
- // projection is taken off the flat fields on every write, the others off
2264
- // `contextStateByKey`. NEVER the `files`/content dicts or a `Set` as such —
2265
- // `pendingReviewPaths` is derived from `files` and stays in memory.
2266
- // `sidebarWidth` keeps its own hand-persist. On rehydrate (`merge`) the active
2267
- // context's projection lands in the flat fields so `loadList`'s same-context
2268
- // guard keeps them on a page refresh, and every other context waits in the
2269
- // space for its switch-back. v2 (2026-09-04): the flat tab trio became the
2270
- // context space; `migrate` lifts a v1 payload's trio into the active context.
2368
+ // v2 (2026-09-04): the flat tab trio became the context space; `migrate` lifts
2369
+ // a v1 payload's trio into the active context. The optional `windows` map is
2370
+ // additive — no version bump, an absent map stays absent.
2271
2371
  name: 'neuralis.filesystem.tabs',
2272
2372
  version: 2,
2273
2373
  migrate: (persistedState, version) => {
@@ -2303,44 +2403,236 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2303
2403
  merge: (persistedState, currentState) => {
2304
2404
  const p = persistedState as Partial<PersistedFilesShape> | undefined;
2305
2405
  if (!p) return currentState;
2306
- const contexts = p.contexts ?? {};
2307
- const activeKey = p.activeAgentId ? contextKey(p.activeUserId ?? null, p.activeProjectId ?? null, p.activeAgentId) : null;
2308
- const space: Record<string, Record<string, unknown>> = {};
2309
- for (const [key, projection] of Object.entries(contexts)) {
2310
- if (key !== activeKey) space[key] = sliceFromPersisted(projection);
2311
- }
2312
- const active = activeKey && contexts[activeKey] ? sliceFromPersisted(contexts[activeKey]) : {};
2313
2406
  return {
2314
- ...currentState,
2315
2407
  previewEditorsEnabled: p.previewEditorsEnabled ?? currentState.previewEditorsEnabled,
2316
2408
  diffViewMode: p.diffViewMode ?? currentState.diffViewMode,
2317
2409
  activeUserId: p.activeUserId ?? null,
2318
2410
  activeAgentId: p.activeAgentId ?? null,
2319
2411
  activeProjectId: p.activeProjectId ?? null,
2320
- ...active,
2321
- contextStateByKey: space,
2322
- } as FilesystemStore;
2412
+ contexts: p.contexts ?? {},
2413
+ ...(p.windows ? { windows: p.windows } : {}),
2414
+ };
2323
2415
  },
2324
2416
  storage: createJSONStorage(() =>
2325
2417
  typeof window !== 'undefined' && window.localStorage
2326
2418
  ? window.localStorage
2327
2419
  : { getItem: () => null, setItem: () => undefined, removeItem: () => undefined },
2328
2420
  ),
2329
- partialize: (state): PersistedFilesShape => {
2330
- const contexts: Record<string, PersistedFilesContext> = {};
2331
- for (const [key, slice] of Object.entries(state.contextStateByKey)) contexts[key] = persistedContext(slice);
2332
- if (state.activeAgentId) {
2333
- contexts[contextKey(state.activeUserId, state.activeProjectId, state.activeAgentId)] = persistedContext(
2334
- state as unknown as Record<string, unknown>,
2335
- );
2336
- }
2337
- return {
2338
- previewEditorsEnabled: state.previewEditorsEnabled,
2339
- diffViewMode: state.diffViewMode,
2340
- activeUserId: state.activeUserId,
2341
- activeAgentId: state.activeAgentId,
2342
- activeProjectId: state.activeProjectId,
2343
- contexts,
2344
- };
2345
- },
2421
+ // NEVER the `files`/content dicts or a `Set` as such — every projection is a
2422
+ // `persistedContext` allowlist (Sets as arrays). `sidebarWidth` keeps its own
2423
+ // hand-persist.
2424
+ partialize: (state): PersistedFilesShape => ({
2425
+ previewEditorsEnabled: state.previewEditorsEnabled,
2426
+ diffViewMode: state.diffViewMode,
2427
+ activeUserId: state.activeUserId,
2428
+ activeAgentId: state.activeAgentId,
2429
+ activeProjectId: state.activeProjectId,
2430
+ contexts: state.contexts,
2431
+ ...(state.windows ? { windows: state.windows } : {}),
2432
+ }),
2346
2433
  }));
2434
+
2435
+ /** The live identity of one Files window: the host's window id, the signed-in user, the project and the BOUND agent. */
2436
+ export type FilesWindowIdentity = { windowId: string; userId: string; projectId: string; agentId: string };
2437
+
2438
+ function sameWindowIdentity(entry: PersistedFilesWindow, id: FilesWindowIdentity): boolean {
2439
+ return entry.userId === id.userId && entry.projectId === id.projectId && entry.agentId === id.agentId;
2440
+ }
2441
+
2442
+ /** A window projection without its identity fields — the shape `sliceFromPersisted` reads. */
2443
+ function viewOf(entry: PersistedFilesWindow): PersistedFilesContext {
2444
+ const { userId: _u, projectId: _p, agentId: _a, ...view } = entry;
2445
+ return view;
2446
+ }
2447
+
2448
+ /**
2449
+ * A new window store. Its view comes from the window's OWN projection when the
2450
+ * record holds one written for this LIVE identity; a projection written for
2451
+ * another user, project or agent is ignored, and a window with no projection
2452
+ * of its own starts from the defaults. The older per-agent `contexts` entries
2453
+ * are never read into a window (no ID translation from an agent key to a
2454
+ * window id): an upgrade resets the open tabs once.
2455
+ */
2456
+ export function createFilesWindowStore(id: FilesWindowIdentity): FilesystemStoreApi {
2457
+ const record = filesPersistence.getState();
2458
+ const own = record.windows?.[id.windowId];
2459
+ const projection = own && sameWindowIdentity(own, id) ? viewOf(own) : undefined;
2460
+ return createFilesystemStore({
2461
+ client: createPackageApi({ slug: 'brain-core', getProjectId: () => id.projectId, getAgentId: () => id.agentId }),
2462
+ windowId: id.windowId,
2463
+ userId: id.userId,
2464
+ projectId: id.projectId,
2465
+ agentId: id.agentId,
2466
+ previewEditorsEnabled: record.previewEditorsEnabled,
2467
+ diffViewMode: record.diffViewMode,
2468
+ ...(projection ? { projection } : {}),
2469
+ });
2470
+ }
2471
+
2472
+ /** The fields `persistedContext` reads — a store update that changed none of them changed no projection. */
2473
+ const PROJECTION_INPUTS = [
2474
+ 'openFiles', 'activeFile', 'tabMeta', 'expandedFolders',
2475
+ 'sidebarView', 'activeWorkspaceTab', 'workspacePanels', 'activeFolderPath',
2476
+ ] as const;
2477
+
2478
+ /**
2479
+ * Feed ONE window's view into the persistence owner while it is mounted. Per
2480
+ * store update: O(1) — eight reference compares; only an update that touched a
2481
+ * projection input serializes the projection, and only a projection that
2482
+ * differs from the last one written reaches the owner (which serializes the
2483
+ * whole allowlisted record once). Returns the unsubscribe.
2484
+ */
2485
+ function bindWindowPersistence(store: FilesystemStoreApi, id: FilesWindowIdentity): () => void {
2486
+ let last = JSON.stringify(persistedContext(store.getState() as unknown as Record<string, unknown>));
2487
+ return store.subscribe((state, prev) => {
2488
+ if (state.previewEditorsEnabled !== prev.previewEditorsEnabled || state.diffViewMode !== prev.diffViewMode) {
2489
+ const owner = filesPersistence.getState();
2490
+ if (owner.previewEditorsEnabled !== state.previewEditorsEnabled || owner.diffViewMode !== state.diffViewMode) {
2491
+ filesPersistence.setState({ previewEditorsEnabled: state.previewEditorsEnabled, diffViewMode: state.diffViewMode });
2492
+ }
2493
+ }
2494
+ if (!PROJECTION_INPUTS.some((key) => state[key] !== prev[key])) return;
2495
+ // The view belongs to the window's own identity; a store that left it writes nothing.
2496
+ if (state.activeUserId !== id.userId || state.activeProjectId !== id.projectId || state.activeAgentId !== id.agentId) return;
2497
+ const view = persistedContext(state as unknown as Record<string, unknown>);
2498
+ const json = JSON.stringify(view);
2499
+ if (json === last) return;
2500
+ last = json;
2501
+ filesPersistence.setState((owner) => ({
2502
+ windows: {
2503
+ ...(owner.windows ?? {}),
2504
+ [id.windowId]: { userId: id.userId, projectId: id.projectId, agentId: id.agentId, ...view },
2505
+ },
2506
+ }));
2507
+ });
2508
+ }
2509
+
2510
+ type LiveFilesWindow = { identity: FilesWindowIdentity; store: FilesystemStoreApi };
2511
+
2512
+ /** Every mounted Files window — hidden and minimized ones included (they stay mounted). */
2513
+ const liveFilesWindows = new Set<LiveFilesWindow>();
2514
+
2515
+ /** The signed-in user, from the workspace provider — which user's records a broom prunes. */
2516
+ let liveFilesUserId: string | null = null;
2517
+
2518
+ export function setFilesLiveUser(userId: string | null): void {
2519
+ liveFilesUserId = userId;
2520
+ }
2521
+
2522
+ /**
2523
+ * Register a window store while it is mounted: the reset fan-out reaches it and
2524
+ * its view feeds the persistence owner. The disposer drops the subscription and
2525
+ * fences the store's in-flight readers — the persisted view stays (closing or
2526
+ * hiding a window is not a reset).
2527
+ */
2528
+ export function mountFilesWindow(store: FilesystemStoreApi, identity: FilesWindowIdentity): () => void {
2529
+ const entry: LiveFilesWindow = { identity, store };
2530
+ liveFilesWindows.add(entry);
2531
+ const unbind = bindWindowPersistence(store, identity);
2532
+ return () => {
2533
+ unbind();
2534
+ liveFilesWindows.delete(entry);
2535
+ store.getState().fenceInFlightWriters();
2536
+ };
2537
+ }
2538
+
2539
+ function scopeReachesWindow(scope: ClientStateResetScope, id: FilesWindowIdentity): boolean {
2540
+ if (scope.projectId !== id.projectId) return false;
2541
+ if (scope.level === 'project') return true;
2542
+ if (scope.agentId !== id.agentId) return false;
2543
+ return scope.level === 'agent' || scope.instanceId === id.windowId;
2544
+ }
2545
+
2546
+ /**
2547
+ * The persisted half of a broom, applied to the ONE record: `instance` drops that
2548
+ * window's projection; `widget` drops it AND the legacy (user, project, agent)
2549
+ * context — the type-clean meaning that level always had; `agent` / `project`
2550
+ * drop the legacy contexts and the window projections of their scope. Only the
2551
+ * signed-in user's records; another user's, project's or agent's stay.
2552
+ */
2553
+ function resetPersistedFiles(scope: ClientStateResetScope, userId: string | null): void {
2554
+ if ((scope.level === 'instance' || scope.level === 'widget') && scope.widgetType !== FILESYSTEM_WIDGET_TYPE) return;
2555
+ const record = filesPersistence.getState();
2556
+ const dropWindow = (windowId: string, entry: PersistedFilesWindow): boolean => {
2557
+ if (entry.projectId !== scope.projectId) return false;
2558
+ if (scope.level === 'instance' || scope.level === 'widget') return windowId === scope.instanceId && entry.agentId === scope.agentId;
2559
+ if (userId === null || entry.userId !== userId) return false;
2560
+ return scope.level === 'project' || entry.agentId === scope.agentId;
2561
+ };
2562
+ const dropContext = (key: string): boolean => {
2563
+ if (scope.level === 'instance') return false;
2564
+ if (scope.level === 'project') return key.startsWith(`${userId ?? ''}::${scope.projectId}::`);
2565
+ return key === contextKey(userId, scope.projectId, scope.agentId);
2566
+ };
2567
+ let windows = record.windows;
2568
+ for (const [windowId, entry] of Object.entries(record.windows ?? {})) {
2569
+ if (!dropWindow(windowId, entry)) continue;
2570
+ if (windows === record.windows) windows = { ...record.windows };
2571
+ delete windows![windowId];
2572
+ }
2573
+ let contexts = record.contexts;
2574
+ for (const key of Object.keys(record.contexts)) {
2575
+ if (!dropContext(key)) continue;
2576
+ if (contexts === record.contexts) contexts = { ...record.contexts };
2577
+ delete contexts[key];
2578
+ }
2579
+ if (windows === record.windows && contexts === record.contexts) return;
2580
+ filesPersistence.setState({ contexts, ...(windows ? { windows } : {}) });
2581
+ }
2582
+
2583
+ /**
2584
+ * The ONE handler behind the install-level `port.onClientStateReset`
2585
+ * subscription: the matching live window stores first (each wipe re-feeds its
2586
+ * own, now empty, view), then the persisted record — so an `instance` / `widget`
2587
+ * reset leaves that window's projection absent and a sibling's untouched.
2588
+ * O(mounted Files windows) per broom event.
2589
+ */
2590
+ export function resetFilesClientState(scope: ClientStateResetScope): void {
2591
+ for (const live of liveFilesWindows) {
2592
+ if (scopeReachesWindow(scope, live.identity)) live.store.getState().resetClientState(scope);
2593
+ }
2594
+ resetPersistedFiles(scope, liveFilesUserId);
2595
+ }
2596
+
2597
+ /**
2598
+ * The store of the Files window being rendered: created once per LIVE identity
2599
+ * (window, user, project, bound agent) — a different user or project is a
2600
+ * different store, never a re-used one — and mounted for the window's lifetime.
2601
+ * `null` until the identity is known: nothing persisted is read, and nothing
2602
+ * paints, before the live user and project are.
2603
+ */
2604
+ export function useFilesWindowStore(
2605
+ windowId: string | null | undefined,
2606
+ userId: string | null | undefined,
2607
+ projectId: string | null | undefined,
2608
+ agentId: string | null | undefined,
2609
+ ): FilesystemStoreApi | null {
2610
+ const live = useMemo(() => {
2611
+ if (!windowId || !userId || !projectId || !agentId) return null;
2612
+ const identity: FilesWindowIdentity = { windowId, userId, projectId, agentId };
2613
+ return { identity, store: createFilesWindowStore(identity) };
2614
+ }, [windowId, userId, projectId, agentId]);
2615
+ // Layout phase: registered (and its persistence bound) before any child's
2616
+ // passive effect issues the first request; disposed before the next paint.
2617
+ useLayoutEffect(() => (live ? mountFilesWindow(live.store, live.identity) : undefined), [live]);
2618
+ return live?.store ?? null;
2619
+ }
2620
+
2621
+ const FilesystemStoreContext = createContext<FilesystemStoreApi | null>(null);
2622
+
2623
+ /** Puts a window's store under its Files tree; every `useFilesystemStore` below reads THIS window's store. */
2624
+ export function FilesystemStoreProvider({ store, children }: { store: FilesystemStoreApi; children?: ReactNode }) {
2625
+ return createElement(FilesystemStoreContext.Provider, { value: store }, children);
2626
+ }
2627
+
2628
+ /** The window's store itself, for imperative reads/writes outside a selector (event handlers, the SSE hook). */
2629
+ export function useFilesystemStoreApi(): FilesystemStoreApi {
2630
+ const store = useContext(FilesystemStoreContext);
2631
+ if (!store) throw new Error('useFilesystemStore must be used inside a Files window (FilesystemStoreProvider)');
2632
+ return store;
2633
+ }
2634
+
2635
+ /** Select from the store of the Files window this component renders in. */
2636
+ export function useFilesystemStore<T>(selector: (state: FilesystemStore) => T): T {
2637
+ return useStore(useFilesystemStoreApi(), selector);
2638
+ }