@neuralis/brain-core 0.3.1 → 0.4.0

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 (371) hide show
  1. package/README.md +88 -1417
  2. package/app/README.md +4 -4
  3. package/app/cards/FilesystemCardShell.tsx +42 -4
  4. package/app/cards/FilesystemChangeCard.tsx +34 -11
  5. package/app/cards/README.md +6 -5
  6. package/app/cards/diffUtils.ts +99 -1
  7. package/app/filesystem/FilesystemHost.tsx +24 -2
  8. package/app/filesystem/README.md +1 -1
  9. package/app/filesystem/api/README.md +3 -3
  10. package/app/filesystem/api/index.ts +38 -14
  11. package/app/filesystem/api/projectPackages.ts +13 -102
  12. package/app/filesystem/domain/types.ts +30 -10
  13. package/app/filesystem/hooks/README.md +4 -4
  14. package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
  15. package/app/filesystem/hooks/useFilesystemSSE.ts +42 -2
  16. package/app/filesystem/host/README.md +6 -4
  17. package/app/filesystem/host/buildFilesystemHostValue.ts +28 -2
  18. package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
  19. package/app/filesystem/lib/sourceKindForm.ts +210 -27
  20. package/app/filesystem/lib/treeVisualState.tsx +98 -4
  21. package/app/filesystem/store/README.md +1 -0
  22. package/app/filesystem/store/filesystemStore.ts +99 -8
  23. package/app/filesystem/store/gitSlice.ts +5 -0
  24. package/app/filesystem/store/helpers.ts +17 -0
  25. package/app/filesystem/store/listSlice.ts +8 -0
  26. package/app/filesystem/ui/CodeEditor.tsx +18 -3
  27. package/app/filesystem/ui/DiscoverDock.tsx +5 -1
  28. package/app/filesystem/ui/FileEditor.tsx +15 -10
  29. package/app/filesystem/ui/FileTree.tsx +75 -24
  30. package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
  31. package/app/filesystem/ui/FolderPicker.tsx +251 -0
  32. package/app/filesystem/ui/GitControlPanel.tsx +7 -2
  33. package/app/filesystem/ui/PackagesPanel.tsx +52 -374
  34. package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
  35. package/app/filesystem/ui/PolicyEditor.tsx +109 -24
  36. package/app/filesystem/ui/README.md +30 -23
  37. package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
  38. package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
  39. package/app/filesystem/ui/SourcesPanel.tsx +502 -342
  40. package/app/filesystem/ui/Toolbar.tsx +1 -2
  41. package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
  42. package/app/shared/filesystem.png +0 -0
  43. package/dist/app/host.js +58 -46
  44. package/dist/scripts/sourceRootPairV1.d.ts +47 -0
  45. package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
  46. package/dist/scripts/sourceRootPairV1.js +147 -0
  47. package/dist/scripts/sourceRootPairV1.js.map +1 -0
  48. package/dist/scripts/sourceToFolderV1.d.ts +56 -0
  49. package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
  50. package/dist/scripts/sourceToFolderV1.js +152 -0
  51. package/dist/scripts/sourceToFolderV1.js.map +1 -0
  52. package/dist/scripts/sourceToHostV1.d.ts +56 -0
  53. package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
  54. package/dist/scripts/sourceToHostV1.js +132 -0
  55. package/dist/scripts/sourceToHostV1.js.map +1 -0
  56. package/dist/src/channels/filesystem.d.ts +1 -1
  57. package/dist/src/channels/filesystem.js +1 -1
  58. package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
  59. package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
  60. package/dist/src/connectors/AppSourceConfigStore.js +46 -6
  61. package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
  62. package/dist/src/connectors/HostConnector.d.ts +201 -0
  63. package/dist/src/connectors/HostConnector.d.ts.map +1 -0
  64. package/dist/src/connectors/HostConnector.js +903 -0
  65. package/dist/src/connectors/HostConnector.js.map +1 -0
  66. package/dist/src/connectors/inProcessTransport.d.ts +131 -0
  67. package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
  68. package/dist/src/connectors/inProcessTransport.js +447 -0
  69. package/dist/src/connectors/inProcessTransport.js.map +1 -0
  70. package/dist/src/domain/access/callerPermissions.d.ts +29 -0
  71. package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
  72. package/dist/src/domain/access/callerPermissions.js +85 -5
  73. package/dist/src/domain/access/callerPermissions.js.map +1 -1
  74. package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
  75. package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
  76. package/dist/src/domain/access/nativeLocalFence.js +171 -0
  77. package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
  78. package/dist/src/domain/access/uriPermission.js +3 -3
  79. package/dist/src/domain/access/uriPermission.js.map +1 -1
  80. package/dist/src/domain/byteRange.d.ts +15 -0
  81. package/dist/src/domain/byteRange.d.ts.map +1 -0
  82. package/dist/src/domain/byteRange.js +30 -0
  83. package/dist/src/domain/byteRange.js.map +1 -0
  84. package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
  85. package/dist/src/domain/defaultPermissions.js +29 -0
  86. package/dist/src/domain/defaultPermissions.js.map +1 -1
  87. package/dist/src/domain/mime.d.ts +11 -19
  88. package/dist/src/domain/mime.d.ts.map +1 -1
  89. package/dist/src/domain/mime.js +13 -102
  90. package/dist/src/domain/mime.js.map +1 -1
  91. package/dist/src/domain/paths.d.ts.map +1 -1
  92. package/dist/src/domain/paths.js +5 -3
  93. package/dist/src/domain/paths.js.map +1 -1
  94. package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
  95. package/dist/src/domain/scopeAccessCtx.js +2 -2
  96. package/dist/src/domain/sourceConfig.d.ts +52 -19
  97. package/dist/src/domain/sourceConfig.d.ts.map +1 -1
  98. package/dist/src/domain/sourceConfig.js +103 -19
  99. package/dist/src/domain/sourceConfig.js.map +1 -1
  100. package/dist/src/domain/sourcePackages.d.ts +8 -8
  101. package/dist/src/domain/sourcePackages.d.ts.map +1 -1
  102. package/dist/src/domain/sourceRootPair.d.ts +88 -0
  103. package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
  104. package/dist/src/domain/sourceRootPair.js +108 -0
  105. package/dist/src/domain/sourceRootPair.js.map +1 -0
  106. package/dist/src/domain/syncTypes.d.ts +8 -1
  107. package/dist/src/domain/syncTypes.d.ts.map +1 -1
  108. package/dist/src/domain/syncTypes.js +1 -0
  109. package/dist/src/domain/syncTypes.js.map +1 -1
  110. package/dist/src/domain/types.d.ts +32 -2
  111. package/dist/src/domain/types.d.ts.map +1 -1
  112. package/dist/src/domain/uri.d.ts.map +1 -1
  113. package/dist/src/domain/uri.js +3 -2
  114. package/dist/src/domain/uri.js.map +1 -1
  115. package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
  116. package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
  117. package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
  118. package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
  119. package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
  120. package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
  121. package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
  122. package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
  123. package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
  124. package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
  125. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
  126. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
  127. package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
  128. package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
  129. package/dist/src/embeddings/factory.d.ts +5 -3
  130. package/dist/src/embeddings/factory.d.ts.map +1 -1
  131. package/dist/src/embeddings/factory.js +7 -3
  132. package/dist/src/embeddings/factory.js.map +1 -1
  133. package/dist/src/embeddings/models.d.ts +1 -1
  134. package/dist/src/embeddings/models.d.ts.map +1 -1
  135. package/dist/src/embeddings/retry.js +1 -1
  136. package/dist/src/embeddings/retry.js.map +1 -1
  137. package/dist/src/embeddings/types.d.ts +6 -0
  138. package/dist/src/embeddings/types.d.ts.map +1 -1
  139. package/dist/src/embeddings/types.js +11 -0
  140. package/dist/src/embeddings/types.js.map +1 -1
  141. package/dist/src/events/pending.d.ts +1 -1
  142. package/dist/src/events/pending.js +1 -1
  143. package/dist/src/git/GitRunner.d.ts +110 -30
  144. package/dist/src/git/GitRunner.d.ts.map +1 -1
  145. package/dist/src/git/GitRunner.js +511 -199
  146. package/dist/src/git/GitRunner.js.map +1 -1
  147. package/dist/src/git/GitWatchRegistry.d.ts +41 -35
  148. package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
  149. package/dist/src/git/GitWatchRegistry.js +61 -79
  150. package/dist/src/git/GitWatchRegistry.js.map +1 -1
  151. package/dist/src/git/gitPlace.d.ts +61 -0
  152. package/dist/src/git/gitPlace.d.ts.map +1 -0
  153. package/dist/src/git/gitPlace.js +74 -0
  154. package/dist/src/git/gitPlace.js.map +1 -0
  155. package/dist/src/git/sourceGitControl.d.ts +33 -21
  156. package/dist/src/git/sourceGitControl.d.ts.map +1 -1
  157. package/dist/src/git/sourceGitControl.js +211 -95
  158. package/dist/src/git/sourceGitControl.js.map +1 -1
  159. package/dist/src/git/sourceGitStatus.js +1 -1
  160. package/dist/src/git/sourceGitStatus.js.map +1 -1
  161. package/dist/src/git/untrackedStats.js +1 -1
  162. package/dist/src/git/untrackedStats.js.map +1 -1
  163. package/dist/src/index.d.ts +2 -1
  164. package/dist/src/index.d.ts.map +1 -1
  165. package/dist/src/index.js +2 -1
  166. package/dist/src/index.js.map +1 -1
  167. package/dist/src/lifecycle.d.ts +9 -12
  168. package/dist/src/lifecycle.d.ts.map +1 -1
  169. package/dist/src/lifecycle.js +2 -1
  170. package/dist/src/lifecycle.js.map +1 -1
  171. package/dist/src/routes/approve.d.ts +1 -1
  172. package/dist/src/routes/approve.js +1 -1
  173. package/dist/src/routes/events.d.ts +1 -1
  174. package/dist/src/routes/events.js +1 -1
  175. package/dist/src/routes/git.d.ts +7 -7
  176. package/dist/src/routes/git.d.ts.map +1 -1
  177. package/dist/src/routes/git.js +201 -72
  178. package/dist/src/routes/git.js.map +1 -1
  179. package/dist/src/routes/health.d.ts +6 -6
  180. package/dist/src/routes/health.js +12 -12
  181. package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
  182. package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
  183. package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
  184. package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
  185. package/dist/src/routes/helpers/uriGuard.js +3 -2
  186. package/dist/src/routes/helpers/uriGuard.js.map +1 -1
  187. package/dist/src/routes/helpers.d.ts +11 -4
  188. package/dist/src/routes/helpers.d.ts.map +1 -1
  189. package/dist/src/routes/helpers.js +13 -6
  190. package/dist/src/routes/helpers.js.map +1 -1
  191. package/dist/src/routes/list.d.ts +1 -1
  192. package/dist/src/routes/list.d.ts.map +1 -1
  193. package/dist/src/routes/list.js +3 -4
  194. package/dist/src/routes/list.js.map +1 -1
  195. package/dist/src/routes/pending.d.ts +4 -4
  196. package/dist/src/routes/pending.js +9 -9
  197. package/dist/src/routes/policies.d.ts +10 -8
  198. package/dist/src/routes/policies.d.ts.map +1 -1
  199. package/dist/src/routes/policies.js +31 -25
  200. package/dist/src/routes/policies.js.map +1 -1
  201. package/dist/src/routes/raw.d.ts +1 -1
  202. package/dist/src/routes/raw.d.ts.map +1 -1
  203. package/dist/src/routes/raw.js +66 -12
  204. package/dist/src/routes/raw.js.map +1 -1
  205. package/dist/src/routes/read.d.ts +1 -1
  206. package/dist/src/routes/read.js +1 -1
  207. package/dist/src/routes/search.d.ts +1 -1
  208. package/dist/src/routes/search.js +1 -1
  209. package/dist/src/routes/sources.d.ts +63 -6
  210. package/dist/src/routes/sources.d.ts.map +1 -1
  211. package/dist/src/routes/sources.js +494 -382
  212. package/dist/src/routes/sources.js.map +1 -1
  213. package/dist/src/routes/upload.d.ts +1 -1
  214. package/dist/src/routes/upload.js +1 -1
  215. package/dist/src/routes/write.d.ts +2 -2
  216. package/dist/src/routes/write.js +2 -2
  217. package/dist/src/services/BridgeService.d.ts +21 -35
  218. package/dist/src/services/BridgeService.d.ts.map +1 -1
  219. package/dist/src/services/BridgeService.js +42 -88
  220. package/dist/src/services/BridgeService.js.map +1 -1
  221. package/dist/src/services/FilesystemService.d.ts +4 -3
  222. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  223. package/dist/src/services/FilesystemService.js +112 -34
  224. package/dist/src/services/FilesystemService.js.map +1 -1
  225. package/dist/src/services/FilesystemUiService.d.ts +4 -1
  226. package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
  227. package/dist/src/services/FilesystemUiService.js +66 -25
  228. package/dist/src/services/FilesystemUiService.js.map +1 -1
  229. package/dist/src/services/SyncScheduler.d.ts +12 -4
  230. package/dist/src/services/SyncScheduler.d.ts.map +1 -1
  231. package/dist/src/services/SyncScheduler.js +37 -8
  232. package/dist/src/services/SyncScheduler.js.map +1 -1
  233. package/dist/src/services/baselineRepair.d.ts +13 -3
  234. package/dist/src/services/baselineRepair.d.ts.map +1 -1
  235. package/dist/src/services/baselineRepair.js +13 -3
  236. package/dist/src/services/baselineRepair.js.map +1 -1
  237. package/dist/src/services/declaredSources.d.ts +132 -43
  238. package/dist/src/services/declaredSources.d.ts.map +1 -1
  239. package/dist/src/services/declaredSources.js +118 -45
  240. package/dist/src/services/declaredSources.js.map +1 -1
  241. package/dist/src/store/dataFormats.d.ts +17 -2
  242. package/dist/src/store/dataFormats.d.ts.map +1 -1
  243. package/dist/src/store/dataFormats.js +18 -3
  244. package/dist/src/store/dataFormats.js.map +1 -1
  245. package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
  246. package/dist/src/sync/DeltaSyncRunner.js +144 -4
  247. package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
  248. package/dist/src/sync/SyncJobQueue.d.ts +7 -2
  249. package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
  250. package/dist/src/sync/SyncJobQueue.js +16 -10
  251. package/dist/src/sync/SyncJobQueue.js.map +1 -1
  252. package/dist/src/sync/SyncWatchService.d.ts +21 -4
  253. package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
  254. package/dist/src/sync/SyncWatchService.js +115 -10
  255. package/dist/src/sync/SyncWatchService.js.map +1 -1
  256. package/dist/src/sync/categoryInference.d.ts +1 -1
  257. package/dist/src/sync/categoryInference.js +1 -1
  258. package/dist/src/tools/execCtx.d.ts +2 -2
  259. package/dist/src/tools/execCtx.js +2 -2
  260. package/dist/src/tools/fs_read.d.ts.map +1 -1
  261. package/dist/src/tools/fs_read.js +5 -0
  262. package/dist/src/tools/fs_read.js.map +1 -1
  263. package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
  264. package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
  265. package/dist/src/usecases/admitSourceRoot.js +85 -0
  266. package/dist/src/usecases/admitSourceRoot.js.map +1 -0
  267. package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
  268. package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
  269. package/dist/src/usecases/browseSourceFolders.js +210 -0
  270. package/dist/src/usecases/browseSourceFolders.js.map +1 -0
  271. package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
  272. package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
  273. package/dist/src/usecases/createDeclaredSources.js +39 -0
  274. package/dist/src/usecases/createDeclaredSources.js.map +1 -1
  275. package/dist/src/usecases/embeddingBacklog.js +1 -1
  276. package/dist/src/usecases/embeddingBacklog.js.map +1 -1
  277. package/dist/src/usecases/list.d.ts.map +1 -1
  278. package/dist/src/usecases/list.js +83 -26
  279. package/dist/src/usecases/list.js.map +1 -1
  280. package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
  281. package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
  282. package/dist/src/usecases/listAttachableDevices.js +10 -0
  283. package/dist/src/usecases/listAttachableDevices.js.map +1 -0
  284. package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
  285. package/dist/src/usecases/listSourcePackages.js +2 -5
  286. package/dist/src/usecases/listSourcePackages.js.map +1 -1
  287. package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
  288. package/dist/src/usecases/purgeExcludedPoints.js +3 -2
  289. package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
  290. package/dist/src/usecases/readFiles.d.ts.map +1 -1
  291. package/dist/src/usecases/readFiles.js +99 -1
  292. package/dist/src/usecases/readFiles.js.map +1 -1
  293. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
  294. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
  295. package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
  296. package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
  297. package/dist/src/usecases/revokePrincipal.d.ts +7 -5
  298. package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
  299. package/dist/src/usecases/revokePrincipal.js +29 -7
  300. package/dist/src/usecases/revokePrincipal.js.map +1 -1
  301. package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
  302. package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
  303. package/dist/src/usecases/scheduleSourceSync.js +6 -5
  304. package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
  305. package/dist/src/usecases/syncPointWrites.d.ts +2 -0
  306. package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
  307. package/dist/src/usecases/syncPointWrites.js +5 -1
  308. package/dist/src/usecases/syncPointWrites.js.map +1 -1
  309. package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
  310. package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
  311. package/dist/src/usecases/triggerSourceSync.js +75 -0
  312. package/dist/src/usecases/triggerSourceSync.js.map +1 -0
  313. package/dist/src/usecases/uploadBinary.d.ts +2 -1
  314. package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
  315. package/dist/src/usecases/uploadBinary.js +2 -2
  316. package/dist/src/usecases/uploadBinary.js.map +1 -1
  317. package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
  318. package/dist/src/usecases/vector/endpointGuard.js +1 -1
  319. package/dist/src/usecases/vectorMaintenance.js +1 -1
  320. package/dist/src/usecases/vectorMaintenance.js.map +1 -1
  321. package/dist/src/usecases/write/content.js +1 -1
  322. package/dist/src/vector/qdrantClient.d.ts +1 -1
  323. package/dist/src/vector/qdrantClient.js +1 -1
  324. package/dist/src/wiring.d.ts +3 -0
  325. package/dist/src/wiring.d.ts.map +1 -1
  326. package/dist/src/wiring.js +72 -6
  327. package/dist/src/wiring.js.map +1 -1
  328. package/dist/tools/fs_list.json +1 -1
  329. package/dist/tools/fs_read.json +1 -1
  330. package/dist/tools/fs_search.json +1 -1
  331. package/dist/tools/fs_write.json +1 -1
  332. package/instructions/brain-model.md +1 -1
  333. package/package.json +212 -114
  334. package/rules/brain-rules.md +2 -2
  335. package/skills/brain-start/SKILL.md +1 -1
  336. package/skills/connect-business-data/SKILL.md +3 -3
  337. package/skills/create-package/SKILL.md +26 -26
  338. package/skills/create-package/references/config-and-credentials.md +16 -13
  339. package/skills/create-package/references/connectors-and-uri-policies.md +39 -25
  340. package/skills/create-package/references/manifest-and-discovery.md +12 -12
  341. package/skills/create-package/references/runtime-and-build.md +19 -19
  342. package/skills/create-package/references/skills-and-context.md +36 -20
  343. package/skills/create-package/references/surfaces-and-design.md +16 -16
  344. package/skills/create-package/references/team-and-workflows.md +2 -2
  345. package/skills/create-package/references/test-and-ship.md +21 -21
  346. package/skills/create-package/references/tools-and-schemas.md +5 -5
  347. package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +79 -65
  348. package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
  349. package/skills/git/SKILL.md +53 -51
  350. package/skills/manage-files/SKILL.md +22 -25
  351. package/skills/manage-sources/SKILL.md +128 -87
  352. package/skills/manage-uri-policy/SKILL.md +18 -15
  353. package/skills/organize/SKILL.md +1 -1
  354. package/skills/sync/SKILL.md +3 -3
  355. package/tools/fs_list.json +1 -1
  356. package/tools/fs_read.json +1 -1
  357. package/tools/fs_search.json +1 -1
  358. package/tools/fs_write.json +1 -1
  359. package/workflows/sources-digest.json +1 -1
  360. package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
  361. package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
  362. package/dist/src/connectors/LocalDiskConnector.js +0 -1233
  363. package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
  364. package/docs/drive-overview.md +0 -59
  365. package/skills/manage-files/scripts/copy.sh +0 -7
  366. package/skills/manage-files/scripts/create.sh +0 -7
  367. package/skills/manage-files/scripts/mkdir.sh +0 -7
  368. package/skills/manage-files/scripts/modify.sh +0 -7
  369. package/skills/manage-files/scripts/upload.sh +0 -16
  370. package/skills/manage-files/scripts/write.sh +0 -29
  371. package/skills/manage-sources/scripts/reembed.sh +0 -23
@@ -2,6 +2,12 @@
2
2
 
3
3
  import { useCallback, useMemo, useState } from 'react';
4
4
  import { Plus, Save, Trash2 } from 'lucide-react';
5
+ import {
6
+ BUILTIN_ROLE_PRIORITY,
7
+ MAX_ROLE_PRIORITY,
8
+ MIN_ROLE_PRIORITY,
9
+ isValidRolePriority,
10
+ } from '@neuralis/package-system/access';
5
11
  import { cn } from '../lib/cn';
6
12
  import type {
7
13
  HierarchicalPermissionsRecord,
@@ -19,9 +25,11 @@ import type {
19
25
  * - By User overrides.
20
26
  * - By Agent overrides ($self maps to the agent's own files).
21
27
  * - Path Rules (pattern + R/W/X + roles + agents + users + override).
28
+ * The role row names roles one by one and/or — `≥` — the caller's ROLE
29
+ * priority, n or stronger (`priority`; lower = stronger, 2 = admin); both ANDed.
22
30
  * `users` names the REQUESTER (`$owner` = the resource's creator, or a
23
- * user id) and is honoured only on a grant rule — OVR on, every set bit
24
- * allow; the server refuses any other shape.
31
+ * user id). `priority` and `users` are honoured only on a grant rule — OVR
32
+ * on, every set bit allow; the server refuses any other shape.
25
33
  *
26
34
  * Visual budget: dense, admin-style layout — pill-shaped R/W/X badges,
27
35
  * small uppercase section headers, no large card chrome. Designed to fit
@@ -45,7 +53,7 @@ type Props = {
45
53
  const DEFAULT_KNOWN_ROLES = ['member', 'owner', 'admin'];
46
54
 
47
55
  /** The persisted rule fields this editor round-trips beyond the shared record type. */
48
- type RuleRecord = PathPermissionRecord & { users?: string[]; floor?: true };
56
+ type RuleRecord = PathPermissionRecord & { users?: string[]; priority?: number; floor?: true };
49
57
  const PERMS = ['read', 'write', 'exec'] as const;
50
58
 
51
59
  function clonePermissions(p: HierarchicalPermissionsRecord): HierarchicalPermissionsRecord {
@@ -232,7 +240,7 @@ export function PolicyEditor({
232
240
 
233
241
  const toggleRuleRole = useCallback((index: number, role: string) => {
234
242
  setDraft((prev) => {
235
- const paths = [...(prev.paths ?? [])];
243
+ const paths = [...(prev.paths ?? [])] as RuleRecord[];
236
244
  const cur = paths[index];
237
245
  const roles = new Set(cur.roles ?? []);
238
246
  if (roles.has(role)) roles.delete(role);
@@ -242,6 +250,28 @@ export function PolicyEditor({
242
250
  });
243
251
  }, []);
244
252
 
253
+ // `priority` is a SEPARATE subject field ANDed with `roles` on the same rule:
254
+ // the toggle adds or removes only the threshold (admin by default) and never
255
+ // touches the named roles, so an AND row never reads wider than it is.
256
+ const toggleRulePriority = useCallback((index: number) => {
257
+ setDraft((prev) => {
258
+ const paths = [...(prev.paths ?? [])] as RuleRecord[];
259
+ const { priority, ...cur } = paths[index];
260
+ paths[index] = priority !== undefined ? cur : { ...cur, priority: BUILTIN_ROLE_PRIORITY.admin };
261
+ return { ...prev, paths };
262
+ });
263
+ }, []);
264
+
265
+ const setRulePriority = useCallback((index: number, raw: string) => {
266
+ setDraft((prev) => {
267
+ const paths = [...(prev.paths ?? [])] as RuleRecord[];
268
+ // An unparsable entry stays visible as NaN (the hint names the range)
269
+ // instead of silently dropping the condition, which would widen the grant.
270
+ paths[index] = { ...paths[index], priority: raw.trim() === '' ? Number.NaN : Number(raw) };
271
+ return { ...prev, paths };
272
+ });
273
+ }, []);
274
+
245
275
  const addRuleEntry = useCallback((index: number, field: RuleListField, value: string) => {
246
276
  const trimmed = value.trim();
247
277
  if (!trimmed) return;
@@ -486,29 +516,73 @@ export function PolicyEditor({
486
516
  </div>
487
517
  <div className="flex flex-wrap items-center gap-1 text-[9px] text-white/50">
488
518
  <span className="text-white/35 mr-0.5">Roles:</span>
489
- {knownRoles.map((role) => {
490
- const active = (rule.roles ?? []).includes(role);
491
- return (
492
- <button
493
- key={role}
494
- type="button"
519
+ <button
520
+ type="button"
521
+ disabled={readOnly}
522
+ onClick={() => toggleRulePriority(index)}
523
+ title={rule.priority !== undefined
524
+ ? 'Remove the role-priority condition (the named roles stay)'
525
+ : 'Also require the caller\'s role priority: n or stronger (lower = stronger); ANDed with the named roles'}
526
+ className={cn(
527
+ 'h-5 px-1.5 rounded text-[8px] font-semibold border transition disabled:cursor-default',
528
+ rule.priority !== undefined
529
+ ? 'bg-amber-500/15 text-amber-200 border-amber-400/30'
530
+ : 'bg-white/[0.04] text-white/35 border-white/[0.08] hover:text-white/65',
531
+ )}
532
+ >
533
+ ≥
534
+ </button>
535
+ <>
536
+ {[...new Set([...knownRoles, ...(rule.roles ?? [])])].map((role) => {
537
+ const active = (rule.roles ?? []).includes(role);
538
+ return (
539
+ <button
540
+ key={role}
541
+ type="button"
542
+ disabled={readOnly}
543
+ onClick={() => toggleRuleRole(index, role)}
544
+ className={cn(
545
+ 'h-5 px-1.5 rounded border text-[9px] font-medium capitalize transition disabled:cursor-default',
546
+ active
547
+ ? 'bg-cyan-500/15 text-cyan-200 border-cyan-400/30'
548
+ : 'bg-white/[0.04] text-white/40 border-white/[0.08] hover:text-white/70',
549
+ )}
550
+ >
551
+ {role}
552
+ </button>
553
+ );
554
+ })}
555
+ {(rule.roles ?? []).length === 0 && rule.priority === undefined && (
556
+ <span className="italic text-white/30">all roles</span>
557
+ )}
558
+ </>
559
+ {rule.priority !== undefined && (
560
+ <>
561
+ <span className="text-white/35">priority</span>
562
+ <input
563
+ type="number"
564
+ min={MIN_ROLE_PRIORITY}
565
+ max={MAX_ROLE_PRIORITY}
566
+ step={1}
495
567
  disabled={readOnly}
496
- onClick={() => toggleRuleRole(index, role)}
497
- className={cn(
498
- 'h-5 px-1.5 rounded border text-[9px] font-medium capitalize transition disabled:cursor-default',
499
- active
500
- ? 'bg-cyan-500/15 text-cyan-200 border-cyan-400/30'
501
- : 'bg-white/[0.04] text-white/40 border-white/[0.08] hover:text-white/70',
502
- )}
503
- >
504
- {role}
505
- </button>
506
- );
507
- })}
508
- {(rule.roles ?? []).length === 0 && (
509
- <span className="italic text-white/30">all roles</span>
568
+ value={Number.isFinite(rule.priority) ? rule.priority : ''}
569
+ onChange={(e) => setRulePriority(index, e.target.value)}
570
+ className="w-12 h-5 rounded bg-white/[0.04] border border-white/[0.08] px-1 text-[9px] font-mono text-white/85 outline-none focus:border-white/20 disabled:opacity-70"
571
+ />
572
+ <span className="italic text-white/30">or stronger{priorityHint(rule.priority)}</span>
573
+ </>
510
574
  )}
511
575
  </div>
576
+ {rule.priority !== undefined && !isValidRolePriority(rule.priority) && (
577
+ <div className="text-[9px] text-amber-200/80">
578
+ Priority is a whole number from {MIN_ROLE_PRIORITY} (owner) to {MAX_ROLE_PRIORITY}; lower = stronger.
579
+ </div>
580
+ )}
581
+ {rule.priority !== undefined && !isGrantRule(rule) && (
582
+ <div className="text-[9px] text-amber-200/80">
583
+ “Or stronger” applies only to an allow rule: turn on OVR and set every bit to allow.
584
+ </div>
585
+ )}
512
586
  <RuleChipRow
513
587
  label="Agents"
514
588
  values={rule.agents ?? []}
@@ -562,6 +636,17 @@ export function PolicyEditor({
562
636
 
563
637
  type RuleListField = 'agents' | 'users';
564
638
 
639
+ /** The built-in role anchored at `priority`, if any. */
640
+ function builtinNameAt(priority: number): string | undefined {
641
+ return Object.entries(BUILTIN_ROLE_PRIORITY).find(([, value]) => value === priority)?.[0];
642
+ }
643
+
644
+ /** ` (2 = admin)` beside a built-in anchor, nothing for a custom strength. */
645
+ function priorityHint(priority: number): string {
646
+ const named = builtinNameAt(priority);
647
+ return named ? ` (${priority} = ${named})` : '';
648
+ }
649
+
565
650
  /** `users` is honoured only here — the server's grant-only rule, mirrored for the hint. */
566
651
  function isGrantRule(rule: RuleRecord): boolean {
567
652
  const set = PERMS.map((p) => rule.permissions[p]).filter((v) => v !== undefined);
@@ -17,7 +17,7 @@ Re-exported from `app/filesystem/ui/index.ts`:
17
17
  - `FileTree`, `FileTreeNode`, `FileTreeSearch`, `SourceBranchHeader` — virtualised tree components (uses `@tanstack/react-virtual`). `FileTreeSearch` is the search PANEL body only (mode selector `keyword|semantic|grep`, filter row, results) — the query input rides inline in the sidebar HEADER (`FilesystemWidget`), panel visibility keys on `sidebarView === 'search'` alone (the header toggle/Escape clears + closes even with text), and the ≥2-char gate inside the panel only switches results-vs-hint. The store keeps the answer's `requested_mode`/`effective_mode`/`embedding_degraded`/`warning` as `searchEffect` (a `contextResetPatch` key) and the panel prints ONE `searchEffectLine` ("Searched as keyword — semantic search unavailable (<reason>)") plus an `embedding missing|stale` badge per row.
18
18
  - `FileTreeContextMenu`, `TreeDeleteConfirm` — the VS Code-style file-management surface (see the section below).
19
19
  - `ReembedDialog` — the ONE estimate-first embed dialog, shared by the Sources panel (a whole source) and the tree's context menu (a folder or a file): opening it asks `sources/reindex` for a sample-based ESTIMATE only (no queue job, no sync-marker change); the work starts on its button and nowhere else. `kind="reembed"` (default) = the forced re-embed of every file with the active model; `kind="pending"` = **Embed pending…**, the `embeddingOnly` debt pass (only missing/stale embeddings; the estimate is the scope's upper bound). Portaled like `TreeDeleteConfirm`.
20
- - `DiscoverDock` — the **Discover** list of attachable-but-unattached sources (`GET /sources/discoverable`), pinned below the tree. **It is a SIBLING of the virtualized scroller, never a `FlatItemKind`**: a sixth item kind would have to join the `rowEstimate`/`isMeasuredRow` contract for a list that is never long enough to need virtualizing. Two placements, one component — COLLAPSED it sits in normal flow below the scroller (takes height, covers nothing, bounded to 5 rows); EXPANDED the tree renders it as an `absolute bottom-0` overlay, the ONLY state in which it may hide tree rows. Never both, so the scroller's height cannot change while the overlay is up. Visible only while EVERY source branch is collapsed (`sourceBranchExpanded`, the React-local BRANCH state — not the store's `expandedFolders`) and only for a caller holding `drive.mount`. **Consequence to know:** `activeFile` is persisted and `FileTree`'s auto-expand opens its source branch, so a returning user with an open tab lands with the dock hidden until they collapse that branch. Clicking a row hands the whole suggestion to the Sources panel through the store (`pendingSourceSuggestion`) and opens it — the prefill body lives in `SourcesPanel.applySuggestion`, ONE body shared with the panel's own Discoverable list.
20
+ - `DiscoverDock` — the **Discover** list of attachable-but-unattached sources (`GET /sources/discoverable`), pinned below the tree. **It is a SIBLING of the virtualized scroller, never a `FlatItemKind`**: a sixth item kind would have to join the `rowEstimate`/`isMeasuredRow` contract for a list that is never long enough to need virtualizing. Two placements, one component — COLLAPSED it sits in normal flow below the scroller (takes height, covers nothing, bounded to 5 rows); EXPANDED the tree renders it as an `absolute bottom-0` overlay, the ONLY state in which it may hide tree rows — and the only one with a surface: a scrim derived from `--w-bg-rgb`, so it matches every theme (collapsed it has none). Never both, so the scroller's height cannot change while the overlay is up. Visible only while EVERY source branch is collapsed (`sourceBranchExpanded`, the React-local BRANCH state — not the store's `expandedFolders`) and only for a caller holding `sources.mount`. **Consequence to know:** `activeFile` is persisted and `FileTree`'s auto-expand opens its source branch, so a returning user with an open tab lands with the dock hidden until they collapse that branch. Clicking a row hands the whole suggestion to the Sources panel through the store (`pendingSourceSuggestion`) and opens it — the prefill body lives in `SourcesPanel.applySuggestion`, ONE body shared with the panel's own Discoverable list.
21
21
  - `FileEditor`, `FileTab`, `FileMetaBar`, `CodeEditor`, `ReviewDiffView`, `EditorTabBar`, `DiffModeToggle` — the **VS Code-like editor stack**.
22
22
  - **`CodeEditor`** (CodeMirror 6 + an in-repo Shiki bridge under `codeEditor/`) is the ONE editor instance behind BOTH the read view (`readOnly`) and edit — toggling Edit only flips the `readOnly` Compartment, so the `EditorView` never remounts (no flash / scroll loss / re-tokenize). Syntax colors are the exact VS Code `dark-plus` TextMate grammars via Shiki's **JavaScript regex engine** — so **no web worker, no `.wasm`, no host CSP change** (`codeEditor/shikiHighlighter.ts` = the memoized core + `tokenizeLines` for the git-diff path — the core, the engine, both themes and all 50 `@shikijs/langs` grammars are dynamic imports, one split chunk each, so nothing of Shiki is on the module's cold path; the 15 base grammars load WITH the core so a markdown fence in one of them colours at once, the rest on first use; `codeEditor/shikiChunks.ts` = the INCREMENTAL tokenizer — position-anchored, line-aligned chunks closed at 64 lines OR 4 KB of text (whichever first) with TextMate grammar-state continuation, chunk-relative token offsets, the visible chunks tokenized synchronously under a 16 KB per-paint character budget (CodeMirror's `visibleRanges` right after a whole-document replace spans thousands of unmeasured lines — the budget, not the viewport estimate, bounds the sync path) and the prefix + a 4-chunk horizon after the viewport in idle slices (a slice is at least one chunk: measured ~90 ms worst case on dense TS/TSX and on long-line markdown, ~50 ms typical code, ~2–5 ms short-line prose — chunks past the horizon wait until scrolled to, so opening a file costs the viewport, not the document), a keystroke re-tokenizing its own chunk and walking forward only until the grammar state converges, a far scroll coloured provisionally first and corrected by the idle pass, and a whole-document replace (every file switch / reload / revert is one) split back into chunks eagerly so the first paint never tokenizes the whole file; `codeEditor/shikiDecorations.ts` = the per-view `ViewPlugin`→`DecorationSet` bridge that owns one engine per editor, because `@shikijs/codemirror` does not exist). The 120k wall is gone; `MAX_HIGHLIGHT_CHARS` (2 MB) is a memory guard on the document (above it the file renders plain), not a tokenization budget. Shared 12px/18px base theme (`codeEditor/editorTheme.ts`).
23
23
  - **`ReviewDiffView`** renders the vector-sync **pending-review** diff on `@codemirror/merge` (unified / split, read-only, accept/reject chunk controls disabled — Approve/Revert stays the banner's job) on the SAME base theme, so the editor↔diff transition never jumps.
@@ -39,7 +39,7 @@ Re-exported from `app/filesystem/ui/index.ts`:
39
39
  - `PolicyEditor` — compact admin-style permissions editor for the source-config panel. Renders the persisted JSON tree (`default`, `byRole`, `byUser`, `byAgent`, `paths[]`) with tri-state R/W/X pill badges (allow / deny / inherit cycle), per-role/user/agent override rows (with `+ Add` input + trash), and path rules with clickable role chips + per-rule agent chips (`$self/agentId`, inline-add). `OVR` toggles the rule's `override` flag. Adding a rule **prepends** it (visible at the top of long policy sets; first-match-wins means the fresh rule also takes precedence). Two modes: `edit` (Save button visible when dirty) and `view` (read-only chat-config render). It is wired into `SourcesPanel`, laid out admin-style, and carries the By Role / By User / By Agent sections. Save threads through `handlePolicySave` → `updateSourceConfig` without disturbing the user's pending sync/credential edits.
40
40
  - `SourcesPanel` "Discoverable" section — lives INSIDE the Vector Health expandable, alongside a `mount.mts` explainer card. Suggestions arrive from `GET /sources/discoverable` and use the shared `dimmedSourceTone('unattached')` util. The icon name (`AppWindow`, `GitBranch`, `FolderTree`, …) resolves through the shared `resolveLucideIcon` helper (same `ICON_MAP` as the workspace dock-left). Click pre-fills the add-source form with `{ key, path, kind, hostRoot? }` from the suggestion, plus the suggestion's optional `suggestedAccess` (read/write/exec) which overrides the connector-kind default, and — for `seed:'discoverable'` package declarations (e.g. the admin app zone, suggestion id `discover:decl:app`) — the **declared `description`** prefills the add form's description field. The **Full Host** suggestion (native/non-Docker only — `/` is the readable host root; in Docker the host must first be bound via `pnpm neuralis:mount add /`) ships `suggestedAccess: { read:true, write:false, exec:false }` so the whole-host root attaches **read-only by default** — the owner widens it (or narrows the uri-policy) in the form before saving. A host source is readable from the container shell, but host-native binaries still won't execute under the container runtime (a host PTY broker is a separate, future capability).
41
41
  - `SourcesPanel` row actions say what they do: **Rescan changes** (the `reindex` route without `force` — changed files are embedded again, unchanged ones keep their vectors), **Re-embed…** (`ReembedDialog` — every file of the source with the active model, estimate first), **Embed pending…** (`ReembedDialog kind="pending"`; it and **Re-embed…** are disabled while the row's live `embedding_enabled` says off), **Forget sync state** (the `reset` route — the next sync compares every file again; deletes no file and no vector). A started re-embed is followed by the same job poll + count refresh as a rescan (`followReindex`); an embedding pass that ends `paused` (terminal, not failed) shows "Embedding paused: <reason>" with **Resume** (the same owed-only apply). The Modify-source body states `binarySkipLine` ("N binary files not indexed" — the last count a pass measured) and `embeddingLine` beside `mediaSkipLine`: off — the live `embedding_enabled`, never the last pass — reads neutral ("Semantic embedding off — text and field search only"); the last pass's other reasons read amber ("Semantic embedding pending: <reasons>").
42
- - `SourcesPanel` row is **name-first** (the redesign for the always-narrow widget). The source name stays left-aligned and is the dominant element (`flex-1`, larger/bold); the kind icon uses the SAME resolved icon set as the file tree (`resolveIconWithFallback` over the connector-manifest `kind_icon`/`meta.icon`) and is **coloured by `getSourceVisualTone`** — so the icon tone IS the sync/live signal, matching the tree (the old `Healthy/Idle/Error` status text + `statusTone` are gone). The per-row **trust badge** (`first-party`/…) and the **scope badge** (`project`/`user`/`agent` — already a tab filter above) are removed; only `observed`, `orphan`, `unsaved`, and the enabled toggle remain inline. Rows are **grouped by `origin_package`** — the **declaring package id** the server resolves per source (the CONTRACT-BACKED `PackageSourceDeclaration` owner, returned on each `SourceRuntimeSummary` / `s.origin_package`), one group **per package**: each real `@neuralis/*` (or any other) package sorts **alphabetically** and user-added mounts collapse into a single **`Mounted (user-added)`** group that sorts **LAST** (`sourceOriginGroup` / `MOUNTED_GROUP`). The small left-indented group header **replaces** the per-row trust/`first-party` badge. The old client-side heuristic that inferred the owner from the connector kind's npm scope (`sourceOriginLabel`) is **gone** — attribution is now resolved server-side from the declarations (`matchDeclaredSource`, slug + resolved-root for rooted kinds; kind-only for rootless sandbox kinds), so a user mount named `app` that is NOT rooted at the platform app zone is NOT attributed to admin — the grouping is **unspoofable**.
42
+ - `SourcesPanel` row is **name-first** (the redesign for the always-narrow widget). The source name stays left-aligned and is the dominant element (`flex-1`, larger/bold); the kind icon uses the SAME resolved icon set as the file tree (`resolveIconWithFallback` over the connector-manifest `kind_icon`/`meta.icon`) and is **coloured by `getSourceVisualTone`** — so the icon tone IS the sync/live signal, matching the tree (the old `Healthy/Idle/Error` status text + `statusTone` are gone). The per-row **trust badge** (`native`/…) and the **scope badge** (`project`/`user`/`agent` — already a tab filter above) are removed; only `observed`, `orphan`, `unsaved`, and the enabled toggle remain inline. Rows are **grouped by `origin_package`** — the **declaring package id** the server resolves per source (the CONTRACT-BACKED `PackageSourceDeclaration` owner, returned on each `SourceRuntimeSummary` / `s.origin_package`), one group **per package**: each real `@neuralis/*` (or any other) package sorts **alphabetically** and user-added mounts collapse into a single **`Mounted (user-added)`** group that sorts **LAST** (`sourceOriginGroup` / `MOUNTED_GROUP`). The small left-indented group header **replaces** the per-row trust/`native` badge. The old client-side heuristic that inferred the owner from the connector kind's npm scope (`sourceOriginLabel`) is **gone** — attribution is now resolved server-side from the declarations (`matchDeclaredSource`, slug + resolved-root for rooted kinds; kind-only for rootless sandbox kinds), so a user mount named `app` that is NOT rooted at the platform app zone is NOT attributed to admin — the grouping is **unspoofable**.
43
43
  - `SourcesPanel` source body: when a row is expanded it renders at **full height** (no `max-h` clamp) so every Path Rule / uri-policy is visible and the outer panel scrolls — the closed state keeps the collapse animation. The Sync sub-section shows a per-trigger explainer (Manual = only the Sync button; Auto = file-watch + reconcile; Scheduled = interval) and clarifies the "Always active" pin. Disabling sync (unchecking "Enable sync" + Save) sends `clearSync: true` — an omitted `sync` means "no change" server-side, so without the flag the disable would silently revert.
44
44
  - `SourcesPanel` per-source counts are **lazy** — the exact `connector` / `indexed` / `unindexed` stats row is fetched ONLY when its row is opened (`toggleSource` → `fetchSourceCounts` → a list scoped to `<source>://` with `include_counts`, so `computeSourcesRegistry` walks that ONE source), cached in `countsBySource`, and shown behind a "Loading counts…" placeholder. Panel-open itself does a **count-FREE** `refresh()` (no `withCounts`) — that still populates `sourceSummaries` (`s.sources`) for source discovery (observed-only sources, project scope) + per-row connector liveness dim, but fires **zero** project-wide count walks. There is deliberately no eager `refresh({withCounts:true})` on open — that, together with the Vector Health walk below, is what used to make opening one panel slow everything. Counted refreshes are narrowed to the operations that actually MOVE a number: sync, reindex, and a config save that changed the sync FILTER set (`syncFilterChanged` — exclude/include/`maxScanFiles`/a cleared sync block, which `connector.count()` applies, so the on-disk total shifts at once). A permissions save, a delete and an add refresh count-FREE: the indexed count is a project-scoped `countByFilter` with no per-caller policy input, the on-disk count is physical truth, a deleted row is gone and a brand-new row's numbers are one expand away. Every counted path also evicts that source's cached counts (`clearCachedCounts`) so the fresh summary is what renders — the lazy cache used to shadow it indefinitely after an exclude edit. `pollSyncJob` (1.5 s × `MAX_POLLS` 2400 = a sixty-minute ceiling) checks a `mountedRef` each turn and returns the moment the panel unmounts; the ref is cleared on UNMOUNT ONLY, never in a dependency-change cleanup, so a live poll is never dropped while the panel is still there. And EVERY hook in `SourcesPanel` sits above its `if (!open) return null` guard: two `useCallback`s used to live below it, so the component rendered a different number of hooks open than closed — React refuses that on one instance, and only the parent's mount gate carrying the same `workspacePanels.sources` flag kept it from ever firing.
45
45
  - `useFilesystemSSE` `sync:completed`: the handler is matched **before** the `if (!uri) return` guard — the event carries `source` (the slug), NOT a file `uri`. It runs the authoritative `reconcileSourceFolders` over that source's VISIBLE folders (a sync can have removed files), skips the pass entirely when every change count on the payload is zero (a missing count is unknown and still re-lists), and falls back to `refreshTree` only when no source is present. That pass rides the ONE per-source reconcile schedule shared with external-change events (a trailing quiet window with a max-wait), so a burst of changes becomes one pass. Owner: `app/filesystem/hooks/README.md`.
@@ -48,36 +48,37 @@ Re-exported from `app/filesystem/ui/index.ts`:
48
48
  - `VectorHealthPanel` heads its stats with ONE `semanticLine` from the report's `embedding` block (`Semantic: off / metadata only / dense (N missing · M stale)`).
49
49
  - `VectorHealthPanel` = **janitor** — the panel's role is "remove leftover records" (deleted past retention, orphaned chunks, duplicate report). Full per-source vector wipe lives on `SourceDeleteConfirm`, not here. `dryRun` default is `false` (clicking actually deletes); destructive buttons need a 2-click confirm; the Deleted stat card shows a `before → after (−Δ)` line after `GC`. Per-action explainer copy + tooltips spell out what each button does (GC / Repair are destructive; Deduplicate is read-only on the service side).
50
50
  - `VectorHealthPanel` is **lazy** — `SourcesPanel` renders it ONLY when the "Vector Health" section is expanded (`{healthOpen && …}`, the store's `vectorHealthOpen`, so a notification's `open-vector-health` nav can open it; a `contextResetPatch` key), never a CSS-only collapse, because the panel auto-fetches on mount; a mounted-but-hidden panel would fire the health request every time the Sources tab opens. On open it fetches the CHEAP count stats (`checkVectorHealth` → `/health`, count-only). The **Orphans** card and **Duplicate groups** count come from the EXPENSIVE on-demand scan (`scanVectorHealth` → `/health/scan`) behind an explicit **"Scan for problems"** button — they show `—` until the user runs it (and re-scan after a destructive action only if a scan was already loaded). This is the "ne lassítson 1 dolog mindent" rule: opening Sources triggers no collection walk.
51
- - `SourcesPanel` add form, the ROOT field — **derived from the declaration, not from `isDocker` and not from the kind name** (`lib/sourceKindForm.ts` `rootFieldMeta`, drift-tested against the real manifests in `test/ui/sourceKindForm.test.ts`). Every other field in that form already rendered from its declared `x-neuralis.ui`; `root` was the one hand-written, kind-BLIND exception, which is why the **Host Machine** kind used to show the `local` connector's `/mounts` volume picker, its `pnpm neuralis:mount add` callout and a `/mounts/...` placeholder while its own declaration said `Host root path` / `/home/you/projects` (warded-wren, closing the UI half of F-B5). The discriminator is the declared `x-neuralis.ui.control` on `root`: `container-path` gets the volume picker + callout + Browse; `host-path` gets none of them; a bare legacy `path` (or an absent control) is treated as container-plane, so an externally authored connector is unchanged. **Not `isHostPlaneKind`** — that is a NAME test whose own docstring says a future `ssh`/`smb` kind joins the plane by being added to its body, so a negative test on it would hand the container chrome to every future non-`local` kind. Browse is additionally gated on `canMount` (the shared `meetsRequires` mirror `FileTree` uses) and stays absent for host-plane roots at all times: `GET /sources/browse` readdirs in the SERVER process, the wrong machine for a broker-resolved host root — a host-plane root browser would need a broker-backed listing route, which does not exist.
51
+ - `SourcesPanel` add form, the ROOT field — **derived from the declaration, not from `isDocker` and not from the kind name** (`lib/sourceKindForm.ts` `rootFieldMeta`, drift-tested against the real manifests in `test/ui/sourceKindForm.test.ts`). Every other field in that form already rendered from its declared `x-neuralis.ui`; `root` was the one hand-written, kind-BLIND exception, which is why a device root used to show the in-process folder's `/mounts` volume picker, its `pnpm neuralis:mount add` callout and a `/mounts/...` placeholder while its own declaration said `Host root path` / `/home/you/projects` (closing the UI half of F-B5). The root's control is `container-path` (or a bare legacy `path` / an absent control, treated the same, so an externally authored connector is unchanged); `ui.browse` offers the folder picker. One kind may serve both planes: the `host` kind's `device` field is marked `x-neuralis.ui.transport`, so while it holds a device (`transportChosen`) the root takes the declared `deviceLabel`, the `devicePlaceholders` entry for that device's claimed `platform` (the WHERE option carries it — agent-core `devices.listMine` returns it for the caller's own devices only; an undeclared platform falls back to `devicePlaceholder`) and none of the container chrome; the WHERE field's declared `help` names both pairing routes for a computer not yet listed; the schema's `not: {required: [device, hostRoot]}` hides the in-process `hostRoot` mirror then (`hiddenByExclusion`), the scope picker drops `agent` and the prefill turns to user scope with nothing granted (the server's own device-folder default). **Never keyed on a kind name** — a name test would hand the container chrome to every future kind it does not list. The form is one page in decision order (`configFields`): kind → WHERE (the transport field) → the folder + Browse → plain fields → scope + access → sync (trigger included) → an "Advanced" fold (URI scheme, the `ui.advanced` host mirror path, description, package folders). Browse opens `FolderPicker` (the extracted former inline browser): on this server only for a `canMount` caller (the shared `meetsRequires` mirror `FileTree` uses), starting from the host-kind discoverable suggestions the panel already loaded (no request of its own), in Docker the `/mounts` zone and below; on a chosen device through `sources/browse?device=` (the device's owner only), starting from the folders that device allows this server, with "Ask on that device" for one it does not (the host value's `requestDeviceFolder` → agent-core `POST devices/:id/requests`, plus the pending count and the `neuralis device grant` line). The Exec chip shows only where a shell can run in the kind's sources (`sources/kinds` `shell`: the `host` kind's own planes — it declares no `exec` capability — or a connector declaring `exec`; not `brain`) — `execRequires` is its tooltip, never a reason to hide the policy bit; the package-folders field calls the kernel `isOwnerOrAdminEffective`, the server gate's own predicate.
52
+ - `SourcesPanel` add form, every OTHER field — renders from the closed control set in the same file (`fieldControl`): a text input (numeric for a number type), or `control: "select"`, whose options come from `x-neuralis.ui.optionsRoute` — a relative route in the DECLARING package (`kinds[].packageId`), read once per form through that package's own route client and quietly again after each burst of `devices` frames (the store's `deviceRevision`, bumped by `useFilesystemSSE`'s ONE `devices` subscription, which also re-reads the source summaries so a host source's liveness follows its device), label `name · OS · status · confinement` (the OS word from a row's `platform` — `Windows` · `macOS` · `Linux`, any other token raw — and the picked row's `platform` travels with the value to the root hint), a `revoked` row never offered, any other non-`online` row dimmed but selectable (the host connector's `device` field lists `sources/devices`: agent-core's `devices.listMine`, the caller's own devices). An `optionsRoute` that is absolute, carries a scheme, a `.`/`..` segment or a `%`/`\` is refused and the field renders as text with the reason. The list is UX only — the declaring package re-checks the value at create.
52
53
  - `SourcesPanel` description field — the VALUE is empty whenever the description is RESOLVED rather than stored (`description_effective`), and that is load-bearing: prefilling the value would freeze a copy of the package's wording into this source's config and stop it tracking the package. The resolved text therefore rides as the **placeholder** in edit state (with a one-line note saying an empty box keeps following the package) and as **labelled** text — `"… (from the package)"` — in the read state and the collapsed summary. **Both states must carry the label**: live-testing warded-wren caught it present in the read state and absent in the edit state, where the box simply looked empty; an opacity difference alone is not a label, because it does not survive the state that matters (editing).
53
54
 
54
55
  - `SourceDeleteConfirm` — inline rose-tinted popover under the row (no modal, matches the panel's "no overlay" pattern). Headline + one-line reminder that the host folder is not touched + `[ ] Purge vector index entries (~N point(s))` checkbox (default OFF — conservative) + Docker-bind-mount warning ribbon when `connection.config.hostRoot` is set, telling the user to run `neuralis mount remove <slug>` before restarting the container. On confirm, `SourcesPanel.handleDelete` calls `deleteSource({ purgeVectors })` and surfaces a transient `successMsg` ("Deleted "<source>" (vectors purged: N)").
55
56
  - **`SkillLauncher` coverage** (`@neuralis/package-system/client`, additive "+" launchers) — every filesystem function the LLM can drive has a visible composer-prefill trace. Clicking a launcher PREFILLS the chat composer with the skill (+ scripts) like a `/` slash pick; it never sends/streams. **No launcher states a feature list**: visibility is DERIVED from the host's per-caller skill catalog, so it cannot drift from each skill's own `SKILL.md` `requiredFeatures` and an unresolved catalog renders nothing (the gate named per row below is the SKILL's, documented here, not repeated in the JSX). `packageId` is the REGISTRY id (`@neuralis/brain-core`), matched as an exact pair with the skill id. The launchers resolve the `WorkspaceHostPort` from the `WorkspaceHostPortProvider` mounted once in `host/installFilesystemHostComponents.tsx` — without it they render `null` silently. Current placements:
56
- - `Toolbar` — `sync` (`drive.sync`, icon-`brain`, script-less) next to the Sources panel toggle, and `manage-files` (`drive.write`; `scripts: mkdir, copy, modify`) as the general file-ops handoff. The tree's CONTEXT MENU deliberately carries none: it is a keyboard-navigable click-to-act list with one uniform item model, and a composer-prefill affordance would need a bespoke item kind there.
57
- - `SourcesPanel` header — `manage-sources` (`drive.read`, attached-left to the `+ Source` button).
58
- - `SourcesPanel` per-source **Permissions** header — `manage-uri-policy` (`drive.policy` — no grant below the `admin` tier, grantable to any custom role) hands the URI-policy / path-permission edit for that source to the agent, alongside the `PolicyEditor`.
59
- - `FileTree` empty-state — `manage-files` (`drive.write`, label; `scripts: upload, modify`) to populate a source.
60
- - `GitControlPanel` header — `git` (`drive.read`) next to the refresh button. The `git` skill drives the SAME `/git/*` routes this panel does — its own SKILL.md says so — and the panel shipped with no way to reach it until 2026-07-28.
61
- - `PendingChangesPanel` toolbar — `manage-files` (`drive.write`) for bulk review; the per-row approve/revert buttons stay the primary path.
62
- - `VectorHealthPanel` actions row — `manage-sources` (`drive.read`). The index is per-SOURCE, so the agent-side equivalent of scan/repair is a source reindex plus a sync-status readout.
63
- - `PackagesPanel` header — `create-package` (label, script-less) hands the whole authoring flow (scaffold, manifest, tools, surfaces, connectors, build — including the WASM pipeline) to the agent; additive to the per-row `Build` button. This is the ONE launcher that narrows further, with `alsoRequires={["packages.author"]}`, even though the SKILL ships ungated (so it stays importable): it scaffolds into `packages://`, which needs write + install/build capability the default `member` role does not hold, so showing it to a caller whose first action would be denied is worse UX than hiding it. `alsoRequires` can only ever hide a launcher, never reveal one, so this cannot become a disclosure. Deliberate — do not "align" it with the skill frontmatter.
57
+ - `Toolbar` — `sync` (`sources.sync`, icon-`brain`, script-less) next to the Sources panel toggle, and `manage-files` (`files.write`; `scripts: mkdir, copy, modify`) as the general file-ops handoff. The tree's CONTEXT MENU deliberately carries none: it is a keyboard-navigable click-to-act list with one uniform item model, and a composer-prefill affordance would need a bespoke item kind there.
58
+ - `SourcesPanel` header — `manage-sources` (`files.read`, attached-left to the `+ Source` button).
59
+ - `SourcesPanel` per-source **Permissions** header — `manage-uri-policy` (`sources.policy` — no grant below the `admin` tier, grantable to any custom role) hands the URI-policy / path-permission edit for that source to the agent, alongside the `PolicyEditor`.
60
+ - `FileTree` empty-state — `manage-files` (`files.write`, label; `scripts: upload, modify`) to populate a source.
61
+ - `GitControlPanel` header — `git` (`files.read`) next to the refresh button. The `git` skill drives the SAME `/git/*` routes this panel does — its own SKILL.md says so — and the panel shipped with no way to reach it until 2026-07-28.
62
+ - `PendingChangesPanel` toolbar — `manage-files` (`files.write`) for bulk review; the per-row approve/revert buttons stay the primary path.
63
+ - `VectorHealthPanel` actions row — `manage-sources` (`files.read`). The index is per-SOURCE, so the agent-side equivalent of scan/repair is a source reindex plus a sync-status readout.
64
+ - `PackagesPanel` header — `create-package` (label, script-less) hands the whole authoring flow (scaffold, manifest, tools, surfaces, connectors, build — including the WASM pipeline) to the agent; additive — Build itself lives in Admin → Packages. This is the ONE launcher that narrows further, with `alsoRequires={["packages.author"]}`, even though the SKILL ships ungated (so it stays importable): it scaffolds into `packages://`, which needs write + install/build capability the default `member` role does not hold, so showing it to a caller whose first action would be denied is worse UX than hiding it. `alsoRequires` can only ever hide a launcher, never reveal one, so this cannot become a disclosure. Deliberate — do not "align" it with the skill frontmatter.
64
65
 
65
66
  ## How it plugs into the package-system kernel
66
67
 
67
68
  - Every component consumes the `useFilesystemHost()` context for agent state, file marks, accent colors, SSE URLs, and optional content renderers (the host can pass a markdown / code-highlight component).
68
- - API calls go through the `api/` module which wraps the kernel `PackageApi` — the brain-core route layer enforces `drive.read`, `drive.write`, `drive.search`, `drive.mount` features and source-scope checks server-side.
69
- - `PackagesPanel` calls host-side routes (`/api/packages/runtime`, `/api/projects/:id/packages/:slug/{build,trust,access-feature}`) through `api/projectPackages.ts`. The widget surfaces project-package operations here because they are project-scoped — they are NOT brain-core package routes.
69
+ - API calls go through the `api/` module which wraps the kernel `PackageApi` — the brain-core route layer enforces `files.read`, `files.write`, `files.search`, `sources.mount` features and source-scope checks server-side.
70
+ - `PackagesPanel` reads the host's scoped snapshot (`/api/packages/runtime` with the window agent's `x-agent-id`) through `api/projectPackages.ts` — NOT a brain-core package route.
70
71
 
71
- ### `recognizesPackages` source toggle + package access-feature governance
72
+ ### Package folders + the Packages panel
72
73
 
73
- - **`SourcesPanel`** renders a **Recognize packages** checkbox on BOTH the Add-source wizard and the Modify-source body — shown only for `local`-kind sources AND only to an owner/admin-effective caller (`host.currentRole.priority <= 2`); it is hidden otherwise and the server re-gates. On create it rides the `createSource` body; on edit it joins the shared draft + Save (and is **stripped** from the PATCH of any caller below that strength, so an unrelated description/sync edit never trips the server's strength gate). Turning it on makes the source a runtime package source (additive to `_packages/`); the loaded packages are advertised only within the source's own scope.
74
- - **`PackagesPanel`** is the consumption-governance surface. Per package (first-party AND project WASM — both appear in the snapshot it lists), an owner/admin-effective caller sees a **base-access feature** control: attach or clear an override feature id (`ProjectRecord.packageAccessFeature`, restrict-only) that hides the whole package from anyone lacking it. The control is a **datalist picker** sourced from the project feature catalog (`getGrantableFeatureIds` → `/api/admin/feature-catalog`; free-text fallback if the fetch fails) — datalist, not a native `<select>`, to avoid the option-popup opacity gotcha; the route rejects an unknown feature id (400). A manifest-declared `requires.accessFeature` shows read-only (cannot be cleared here; an override adds an additional requirement). Non-owners see the effective feature read-only. (Grant the chosen feature to a role in Admin → Roles — a declared/overridden access feature is grantable there.)
74
+ - **`SourcesPanel`** renders a **Package folders** field (`PackageFoldersField`, one folder per line relative to the source root, `.` = the whole folder) on BOTH the Add-source wizard and the Modify-source body — shown only for `host` sources AND only to an owner/admin-effective caller (the kernel `isOwnerOrAdminEffective` over `host.currentRole.priority` and `host.grantedFeatures` — the server's own predicate); it is hidden otherwise and the server re-gates. On create it rides the `createSource` body; on edit it joins the shared draft + Save (and is **stripped** from the PATCH of any caller below that strength, so an unrelated description/sync edit never trips the server's strength gate). Each listed folder is a runtime package source (additive to `_packages/`); the loaded packages are advertised only within the source's own scope, and a folder on another machine's device loads nothing.
75
+ - **`PackagesPanel`** lists exactly what the window's agent gets: the scoped snapshot for this caller and that agent (scope, base-access feature, project membership — the stream's own ladder; an agent-scoped package of another agent and a member's private one stay out), native packages left out (they are the host's, in Admin → Packages). Each row shows its state (a device package whose device is offline reads "Not available"), trust, runtime, contribution counts and its effective access feature, read-only. Trust, Build and the access feature moved to **Admin → Packages** (project scope); the panel's header link opens it (`host.openAdminPackages`) and shows only to a holder of `packages.manage` — the admin route's own gate, hidden (never disabled) otherwise.
75
76
 
76
77
  ## Security & scope
77
78
 
78
79
  - Components never display raw `_meta` payloads (runtime billing telemetry); only `content` and `structuredContent` are rendered.
79
- - Cross-agent file marks and source observations are gated server-side by `filesystem.observeScoped` — the UI surfaces what the route returns and never tries to bypass the gate.
80
- - `PackagesPanel.setPackageTrust` / `setPackageAccessFeature` write to the platform zone via host routes; both require owner / admin role (`canManagePackages`). The access-feature override is restrict-only — it can attach a required feature, never grant access. Owner-only UI controls (the `recognizesPackages` toggle, the access-feature override) hide for non-owners via `host.currentRole`, but the server route is the enforcement floor.
80
+ - Cross-agent file marks and source observations are gated server-side by `files.readOthers` — the UI surfaces what the route returns and never tries to bypass the gate.
81
+ - `PackagesPanel` writes nothing: the package changes are Admin → Packages calls whose host body re-derives `canManagePackages` (the governance flag or `packages.manage`). The package-folders field hides for non-owners via `host.currentRole`, but the server route is the enforcement floor.
81
82
 
82
83
  ## VS Code-style file management (context menu · drag · inline edit)
83
84
 
@@ -97,7 +98,7 @@ agent share the same server path, deny-by-default, server re-gated):
97
98
  target (rename = single file OR folder; New Folder disabled on `brain://`; Paste
98
99
  needs a clipboard; Re-embed… = a single file or folder, not on `brain://` — its
99
100
  content lives in the vector store, there is no file to read again — and only
100
- for a `drive.sync` holder, the reindex route's gate, mirrored hide-only).
101
+ for a `sources.sync` holder, the reindex route's gate, mirrored hide-only).
101
102
  - **Multi-select** — Ctrl/⌘-click toggles, Shift-click ranges over the flat visible
102
103
  list; bulk delete/cut/copy fan out client-side, each server-gated.
103
104
  - **Inline rename + inline New File / New Folder** — an `<input>` in the tree row
@@ -155,13 +156,19 @@ EPHEMERAL (never persisted).
155
156
  - **Gutter idiom.** The DIFF renderers use a per-row `DiffLineRow` gutter (scrolls horizontally with the row); the editor's gutter is CodeMirror's own. They look the same but differ on horizontal scroll by design.
156
157
  - **The virtualizer's `estimateSize` / `getItemKey` are STABLE `useCallback`s reading the row list through a ref** — `getItemKey` is the only memo dependency of virtual-core's measurement options, so an inline arrow (a new identity per render) invalidated the measurement cache and re-ran the estimator for every unmeasured row on each render. No `measureElement` override: the default measurer already reads `offsetHeight`.
157
158
  - **The tab strip keeps the ACTIVE tab in view and scrolls on a vertical wheel.** The strip lives in `FileEditor`'s `data-tab-scroller` div, whose scrollbar is an invisible overlay until hover. Measured 2026-09-04 in a 536 px widget with a 327 px sidebar: the strip is 165 px, and a third opened tab was ACTIVE yet fully outside it, with nothing on screen saying the strip scrolls. `EditorTabBar` applies two exported pure rules — `tabScrollDelta` (the active tab's overflow, applied to the scroller's `scrollLeft`; never `scrollIntoView`, which would also scroll the stage) and `wheelToHorizontal` (a native non-passive `wheel` listener — React's `onWheel` is passive and cannot `preventDefault`). The panel CLIPS the strip: a tab rect past the panel edge is geometry, not paint (`elementFromPoint` there hits the neighbour), so the defect was the invisible active tab, never an overflow.
158
- - **A tab's content is fetched by the STORE, never by a widget effect.** `rehydrateActiveTab` runs from `loadList` (a same-context mount — page reload) and from `setActiveFile` (a tab click), against the store's own active pair and the read STATE (`hasCompletedRead`: a string baseline or an error — a folder listing materialises content-less nodes, so FileNode presence means nothing). The effect it replaced decided from its render closure and re-opened the PREVIOUS agent's tab under the new one after a switch, and skipped the read for a listed-but-unread sibling tab ("Empty file" on click). While a read is pending the editor's placeholder says "Loading…"; "Empty file" is reserved for a completed read. The whole widget state is per context: a switch stashes the outgoing `(user, project, agent)` context's tree, tabs, caches and panels and restores the incoming one's (`contextStateByKey`, `../store/README.md`), so switching back finds the tree and the editor where they were.
159
- - **Root-listing failure is visible.** `loadList`'s catch sets `snapshotError`; the tree renders it as a rose banner with Retry (re-runs the root listing) and Dismiss, in place of "Empty Drive" when no source is known yet. A listing that answers clears it.
160
- - **Overlays.** `position: fixed` inside the widget is panel-anchored and panel-clipped at `dim` and `opaque` (the panel's `backdrop-filter`, load-bearing) and viewport-level only at `transparent` — so every toast and confirm dialog here (`PackagesPanel`'s toast + build log, `GitControlPanel`'s destructive-op confirm) is `createPortal`ed to `document.body` like `TreeDeleteConfirm`. The three header-picker click-away catchers in `GitControlPanel` are NOT scrims any more: their popovers stay inside the panel's isolated stacking context, so a body-level scrim would paint over them — they are one `useClickAway` capture-phase `mousedown` listener on the picker's zone, which also closes the picker on a click outside the widget (the clipped scrim never could).
159
+ - **A tab's content is fetched by the STORE, never by a widget effect.** `rehydrateActiveTab` runs from `loadList` (a same-context mount — page reload) and from `setActiveFile` (a tab click), against the store's own active pair and the read STATE (`hasCompletedRead`: a string baseline or an error — a folder listing materialises content-less nodes, so FileNode presence means nothing). The effect it replaced decided from its render closure and re-opened the PREVIOUS agent's tab under the new one after a switch, and skipped the read for a listed-but-unread sibling tab ("Empty file" on click). While a read is pending the editor's placeholder says "Loading…"; "Empty file" is reserved for a completed read, and a failed one says `Read failed: <label>` and disables Edit in-place until a read lands (nothing loaded is nothing to type over) — `CodeEditor` reconfigures its placeholder and aria label through a Compartment, because the one view outlives the file it shows. The whole widget state is per context: a switch stashes the outgoing `(user, project, agent)` context's tree, tabs, caches and panels and restores the incoming one's (`contextStateByKey`, `../store/README.md`), so switching back finds the tree and the editor where they were.
160
+ - **Root-listing failure is visible.** `loadList`'s catch sets `snapshotError`; the tree renders it as a rose banner with Retry (re-runs the root listing) and Dismiss, in place of "No files yet" when no source is known yet. A listing that answers clears it.
161
+ - **Overlays.** `position: fixed` inside the widget is panel-anchored and panel-clipped at `dim` and `opaque` (the panel's `backdrop-filter`, load-bearing) and viewport-level only at `transparent` — so every confirm dialog here (`GitControlPanel`'s destructive-op confirm) is `createPortal`ed to `document.body` like `TreeDeleteConfirm`. The three header-picker click-away catchers in `GitControlPanel` are NOT scrims any more: their popovers stay inside the panel's isolated stacking context, so a body-level scrim would paint over them — they are one `useClickAway` capture-phase `mousedown` listener on the picker's zone, which also closes the picker on a click outside the widget (the clipped scrim never could).
161
162
  - The editor diff is `whitespace-pre` + horizontal scroll (long lines scroll), NOT wrapping — matching the chat card (a deliberate change from the old `<table>` `whitespace-pre-wrap break-all`).
162
163
  - **Getting the pre-edit baseline: ONE current read.** The canonical way for the file editor and chat card modify diff is a single `readFiles({ agentId, files:[uri] })` whose response carries `version.baseline.content` (the pre-edit snapshot, locked while `pending_review`) — parse it with `parseFilePrev` (`store/helpers.ts`). The pending baseline is **cumulative** (locked to pre-FIRST-edit); approval preserves the stored baseline, so ONE read gives the editor both sides of the diff. `src/routes/read.ts` used to DROP `read_version`/`line_start`/`line_end`/`include_ids` from the body, which made a `read_version:'baseline'` request over the route silently return CURRENT; it now forwards all four, so that request works — but it still answers with the baseline ALONE, and the diff needs both sides. Keep the single current read for those surfaces; Changes instead uses the bounded pending preview, whose receipt fences subsequent resolution.
163
- - The file-tree sidebar is a **fixed-pixel** column (mirrors the admin widget), not a percentage split: resizing the outer widget never reflows the tree. Its width is drag-resizable via a custom pointer-capture handle and persisted in the store / `localStorage` (`sidebarWidth`, min `FILESYSTEM_SIDEBAR_MIN_WIDTH = 120`px). The drag upper bound is the live container width, so the tree can fully cover the editor (tree + chat only). The `sidebarOpen` toggle collapses the column to width `0`.
164
+ - The file-tree sidebar is a **fixed-pixel** column (mirrors the admin widget), not a percentage split: resizing the outer widget never reflows the tree until the widget edge reaches it — then the rendered width is capped at the container (`max-width`, CSS only) and grows back to the stored width on widening, and a drag starts from the rendered width; the editor column clips (`overflow-hidden`), so at zero width its empty state and line/char metrics never paint over the tree. Its width is drag-resizable via a custom pointer-capture handle and persisted in the store / `localStorage` (`sidebarWidth`, min `FILESYSTEM_SIDEBAR_MIN_WIDTH = 120`px). The drag upper bound is the live container width, so the tree can fully cover the editor (tree + chat only). The `sidebarOpen` toggle collapses the column to width `0`.
164
165
  - `FilesystemWidget` triggers `loadList()` when the agent, project or user id changes (the user comes from `port.useUserId()` through the install entry and is part of the context key); the in-flight dedupe in the list slice prevents thrash during rapid switches.
166
+ - **Git graph rows:** **hovering** a collapsed row floats a **full-message card** (subject + the full commit **body** — `%b`, carried on `GitGraphCommit.body`, a NUL-terminated `logGraph` record so a multi-line body is parsed safely, server-capped — plus sha/author/relative-date), anchored to the graph ROOT and `pointer-events-none` so it never steals the hover.
167
+ - **`source.git` row summary:** The field is CHEAP and bounded O(1)/source (a `.git` stat + an O(1) `.git/HEAD` branch read via `getSourceGitSummary`, TTL-cached — never a whole-repo status on the list path) and is stamped in the **two** source-summary builders: `FilesystemService.list`'s `filteredSources` (the tree) and `GET /sources` (the config panel), both AFTER the source-level access filter so a non-member never infers a project's git repos.
168
+ - **`source.git` row summary:** `dirty` stays deferred off the list path; ahead/behind now ride the ON-DEMAND panel status (below) — the source-ROW summary itself stays O(1) and tracking-free.
169
+ - **Diff expanders:** Expansion lazily fetches the NEW-side full source on first click (unchanged gaps are identical on both sides), routed per mode: an unstaged diff reads the working-tree file via the read API, a staged diff fetches the index blob via `GET /git/show?rev=index`, a commit diff fetches `rev=<sha>` and a range diff `rev=<head>` (never `<sha>^` — the show route rejects it); the result is cached per `(source, path, rev)` and any refusal (`not_a_blob`/`too_large`/`binary`/missing file) degrades the expanders to static "N hidden lines" text — fail-soft, never an error banner.
170
+ - **Git commit tab:** Clearing the descriptor (pill close) falls the active tab back to `file` — the store centralises the fallback so a `git-commit` tab id is never stranded without a target — and BOTH git targets reset on an agent/project switch.
171
+ - **Vector Health panel:** The UI panel (`VectorHealthPanel.tsx`) ships with `dryRun=false` by default plus an inline 2-click confirm on destructive buttons; the previous `dryRun=true` default made every click a no-op. Before/after deltas appear on the Deleted / Orphans stat cards after GC / Repair completes.
165
172
 
166
173
  ## Tests
167
174
 
@@ -0,0 +1,58 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { deviceRefusalLabel, sourceLiveReasonLabel } from '../lib/treeVisualState';
3
+
4
+ /**
5
+ * What a failed preview says, from the byte lane's own answer: a 413 names the
6
+ * ceiling it hit (`limit`, bytes) — "Too large to preview (limit N MB)" —, a
7
+ * 503 the source's live state in the Files tree's own words, a 403 the
8
+ * device's refusal code, and anything else is a plain "Preview failed". Never
9
+ * a broken-image icon.
10
+ */
11
+ export function previewFailureText(status: number, body: unknown): string {
12
+ const reason = (body as { reason?: unknown } | null)?.reason;
13
+ // A 503 is a device folder's answer only (the `host` connector's device transport).
14
+ const notReady = (body as { not_ready?: unknown } | null)?.not_ready;
15
+ if (status === 503 && typeof reason === 'string') {
16
+ return `Preview unavailable — ${sourceLiveReasonLabel(reason, { kind: 'host', notReady: Array.isArray(notReady) ? notReady : null })}`;
17
+ }
18
+ const refused = status === 403 ? deviceRefusalLabel(reason) : undefined;
19
+ if (refused) return refused;
20
+ const limit = (body as { limit?: unknown } | null)?.limit;
21
+ if (status === 413 && typeof limit === 'number' && Number.isFinite(limit) && limit > 0) {
22
+ const mb = limit / (1024 * 1024);
23
+ return `Too large to preview (limit ${mb >= 10 ? Math.round(mb) : Number(mb.toFixed(1))} MB)`;
24
+ }
25
+ if (status === 413) return 'Too large to preview';
26
+ return 'Preview failed';
27
+ }
28
+
29
+ /**
30
+ * An image or video preview from the raw byte lane. When the element fails to
31
+ * load, ONE request asks the route why and the honest line replaces the media
32
+ * (the route answers a refusal with a small JSON body, never the file).
33
+ */
34
+ export function RawMediaPreview(props: { kind: 'image' | 'video'; src: string; alt: string; className: string }) {
35
+ const [failure, setFailure] = useState<string | null>(null);
36
+ useEffect(() => { setFailure(null); }, [props.src]);
37
+
38
+ const onError = (): void => {
39
+ void (async () => {
40
+ try {
41
+ const res = await fetch(props.src, { credentials: 'same-origin' });
42
+ if (res.ok) { setFailure('Preview failed'); return; }
43
+ const body = await res.json().catch(() => null);
44
+ setFailure(previewFailureText(res.status, body));
45
+ } catch {
46
+ setFailure('Preview failed');
47
+ }
48
+ })();
49
+ };
50
+
51
+ if (failure) {
52
+ return <div role="status" className="rounded-xl border border-white/10 bg-white/[0.035] px-4 py-3 text-xs text-white/55">{failure}</div>;
53
+ }
54
+ return props.kind === 'image'
55
+ // eslint-disable-next-line @next/next/no-img-element
56
+ ? <img src={props.src} alt={props.alt} className={props.className} onError={onError} />
57
+ : <video src={props.src} controls className={props.className} onError={onError} />;
58
+ }
@@ -36,10 +36,10 @@ export function resolveSourceRepoHint(
36
36
 
37
37
  function SourceIcon({ info }: { info: SourceInfo }) {
38
38
  const Icon = resolveIconWithFallback(info.kind_icon ?? undefined, HardDrive);
39
- // Vector-backed sources (brain itself, and local-disk sources that sync into
40
- // the vector index) get a small overlay badge. Other source kinds — e.g.
41
- // machine, remote APIs — render the icon plain.
42
- const showVectorBadge = info.source_kind === 'brain' || info.source_kind === 'local-disk';
39
+ // Vector-backed sources (brain itself, and folders — on this server or on a
40
+ // device — that sync into the vector index) get a small overlay badge. Other
41
+ // source kinds — e.g. machine, remote APIs — render the icon plain.
42
+ const showVectorBadge = info.source_kind === 'brain' || info.source_kind === 'host';
43
43
  // Git indicator (G2.1 / F2) — a small GitBranch overlay when the source root
44
44
  // is a git repo OR its folders contain checkouts (`containsRepos`). ORTHOGONAL
45
45
  // to the sync tone (getSourceVisualTone owns the icon colour; this only adds a