@neuralis/brain-core 0.3.2 → 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 (311) hide show
  1. package/README.md +79 -1409
  2. package/app/README.md +1 -1
  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 +17 -2
  8. package/app/filesystem/api/README.md +3 -3
  9. package/app/filesystem/api/index.ts +35 -11
  10. package/app/filesystem/api/projectPackages.ts +13 -102
  11. package/app/filesystem/domain/types.ts +30 -10
  12. package/app/filesystem/hooks/README.md +3 -3
  13. package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
  14. package/app/filesystem/hooks/useFilesystemSSE.ts +1 -1
  15. package/app/filesystem/host/README.md +5 -3
  16. package/app/filesystem/host/buildFilesystemHostValue.ts +19 -0
  17. package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
  18. package/app/filesystem/lib/sourceKindForm.ts +118 -39
  19. package/app/filesystem/lib/treeVisualState.tsx +98 -4
  20. package/app/filesystem/store/README.md +1 -0
  21. package/app/filesystem/store/filesystemStore.ts +86 -5
  22. package/app/filesystem/store/gitSlice.ts +5 -0
  23. package/app/filesystem/store/helpers.ts +17 -0
  24. package/app/filesystem/store/listSlice.ts +8 -0
  25. package/app/filesystem/ui/CodeEditor.tsx +18 -3
  26. package/app/filesystem/ui/FileEditor.tsx +15 -10
  27. package/app/filesystem/ui/FileTree.tsx +41 -16
  28. package/app/filesystem/ui/FolderPicker.tsx +251 -0
  29. package/app/filesystem/ui/GitControlPanel.tsx +5 -0
  30. package/app/filesystem/ui/PackagesPanel.tsx +52 -374
  31. package/app/filesystem/ui/PolicyEditor.tsx +109 -24
  32. package/app/filesystem/ui/README.md +17 -11
  33. package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
  34. package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
  35. package/app/filesystem/ui/SourcesPanel.tsx +420 -360
  36. package/app/filesystem/ui/Toolbar.tsx +1 -2
  37. package/app/shared/filesystem.png +0 -0
  38. package/dist/app/host.js +57 -49
  39. package/dist/scripts/sourceRootPairV1.d.ts +47 -0
  40. package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
  41. package/dist/scripts/sourceRootPairV1.js +147 -0
  42. package/dist/scripts/sourceRootPairV1.js.map +1 -0
  43. package/dist/scripts/sourceToFolderV1.d.ts +56 -0
  44. package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
  45. package/dist/scripts/sourceToFolderV1.js +152 -0
  46. package/dist/scripts/sourceToFolderV1.js.map +1 -0
  47. package/dist/scripts/sourceToHostV1.d.ts +56 -0
  48. package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
  49. package/dist/scripts/sourceToHostV1.js +132 -0
  50. package/dist/scripts/sourceToHostV1.js.map +1 -0
  51. package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
  52. package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
  53. package/dist/src/connectors/AppSourceConfigStore.js +46 -6
  54. package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
  55. package/dist/src/connectors/HostConnector.d.ts +201 -0
  56. package/dist/src/connectors/HostConnector.d.ts.map +1 -0
  57. package/dist/src/connectors/HostConnector.js +903 -0
  58. package/dist/src/connectors/HostConnector.js.map +1 -0
  59. package/dist/src/connectors/inProcessTransport.d.ts +131 -0
  60. package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
  61. package/dist/src/connectors/inProcessTransport.js +447 -0
  62. package/dist/src/connectors/inProcessTransport.js.map +1 -0
  63. package/dist/src/domain/access/callerPermissions.d.ts +29 -0
  64. package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
  65. package/dist/src/domain/access/callerPermissions.js +85 -5
  66. package/dist/src/domain/access/callerPermissions.js.map +1 -1
  67. package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
  68. package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
  69. package/dist/src/domain/access/nativeLocalFence.js +171 -0
  70. package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
  71. package/dist/src/domain/access/uriPermission.js +3 -3
  72. package/dist/src/domain/access/uriPermission.js.map +1 -1
  73. package/dist/src/domain/byteRange.d.ts +15 -0
  74. package/dist/src/domain/byteRange.d.ts.map +1 -0
  75. package/dist/src/domain/byteRange.js +30 -0
  76. package/dist/src/domain/byteRange.js.map +1 -0
  77. package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
  78. package/dist/src/domain/defaultPermissions.js +29 -0
  79. package/dist/src/domain/defaultPermissions.js.map +1 -1
  80. package/dist/src/domain/mime.d.ts +8 -16
  81. package/dist/src/domain/mime.d.ts.map +1 -1
  82. package/dist/src/domain/mime.js +10 -99
  83. package/dist/src/domain/mime.js.map +1 -1
  84. package/dist/src/domain/paths.d.ts.map +1 -1
  85. package/dist/src/domain/paths.js +5 -3
  86. package/dist/src/domain/paths.js.map +1 -1
  87. package/dist/src/domain/sourceConfig.d.ts +52 -19
  88. package/dist/src/domain/sourceConfig.d.ts.map +1 -1
  89. package/dist/src/domain/sourceConfig.js +103 -19
  90. package/dist/src/domain/sourceConfig.js.map +1 -1
  91. package/dist/src/domain/sourcePackages.d.ts +8 -8
  92. package/dist/src/domain/sourcePackages.d.ts.map +1 -1
  93. package/dist/src/domain/sourceRootPair.d.ts +88 -0
  94. package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
  95. package/dist/src/domain/sourceRootPair.js +108 -0
  96. package/dist/src/domain/sourceRootPair.js.map +1 -0
  97. package/dist/src/domain/syncTypes.d.ts +8 -1
  98. package/dist/src/domain/syncTypes.d.ts.map +1 -1
  99. package/dist/src/domain/syncTypes.js +1 -0
  100. package/dist/src/domain/syncTypes.js.map +1 -1
  101. package/dist/src/domain/types.d.ts +24 -1
  102. package/dist/src/domain/types.d.ts.map +1 -1
  103. package/dist/src/domain/uri.d.ts.map +1 -1
  104. package/dist/src/domain/uri.js +3 -2
  105. package/dist/src/domain/uri.js.map +1 -1
  106. package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
  107. package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
  108. package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
  109. package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
  110. package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
  111. package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
  112. package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
  113. package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
  114. package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
  115. package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
  116. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
  117. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
  118. package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
  119. package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
  120. package/dist/src/embeddings/factory.d.ts +5 -3
  121. package/dist/src/embeddings/factory.d.ts.map +1 -1
  122. package/dist/src/embeddings/factory.js +7 -3
  123. package/dist/src/embeddings/factory.js.map +1 -1
  124. package/dist/src/embeddings/models.d.ts +1 -1
  125. package/dist/src/embeddings/models.d.ts.map +1 -1
  126. package/dist/src/embeddings/retry.js +1 -1
  127. package/dist/src/embeddings/retry.js.map +1 -1
  128. package/dist/src/embeddings/types.d.ts +6 -0
  129. package/dist/src/embeddings/types.d.ts.map +1 -1
  130. package/dist/src/embeddings/types.js +11 -0
  131. package/dist/src/embeddings/types.js.map +1 -1
  132. package/dist/src/git/GitRunner.d.ts +110 -30
  133. package/dist/src/git/GitRunner.d.ts.map +1 -1
  134. package/dist/src/git/GitRunner.js +511 -199
  135. package/dist/src/git/GitRunner.js.map +1 -1
  136. package/dist/src/git/GitWatchRegistry.d.ts +41 -35
  137. package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
  138. package/dist/src/git/GitWatchRegistry.js +61 -79
  139. package/dist/src/git/GitWatchRegistry.js.map +1 -1
  140. package/dist/src/git/gitPlace.d.ts +61 -0
  141. package/dist/src/git/gitPlace.d.ts.map +1 -0
  142. package/dist/src/git/gitPlace.js +74 -0
  143. package/dist/src/git/gitPlace.js.map +1 -0
  144. package/dist/src/git/sourceGitControl.d.ts +31 -19
  145. package/dist/src/git/sourceGitControl.d.ts.map +1 -1
  146. package/dist/src/git/sourceGitControl.js +208 -92
  147. package/dist/src/git/sourceGitControl.js.map +1 -1
  148. package/dist/src/git/sourceGitStatus.js +1 -1
  149. package/dist/src/git/sourceGitStatus.js.map +1 -1
  150. package/dist/src/git/untrackedStats.js +1 -1
  151. package/dist/src/git/untrackedStats.js.map +1 -1
  152. package/dist/src/index.d.ts +2 -1
  153. package/dist/src/index.d.ts.map +1 -1
  154. package/dist/src/index.js +2 -1
  155. package/dist/src/index.js.map +1 -1
  156. package/dist/src/lifecycle.d.ts +8 -11
  157. package/dist/src/lifecycle.d.ts.map +1 -1
  158. package/dist/src/lifecycle.js +2 -1
  159. package/dist/src/lifecycle.js.map +1 -1
  160. package/dist/src/routes/git.d.ts.map +1 -1
  161. package/dist/src/routes/git.js +183 -54
  162. package/dist/src/routes/git.js.map +1 -1
  163. package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
  164. package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
  165. package/dist/src/routes/helpers/uriGuard.js +3 -2
  166. package/dist/src/routes/helpers/uriGuard.js.map +1 -1
  167. package/dist/src/routes/helpers.d.ts +10 -3
  168. package/dist/src/routes/helpers.d.ts.map +1 -1
  169. package/dist/src/routes/helpers.js +12 -5
  170. package/dist/src/routes/helpers.js.map +1 -1
  171. package/dist/src/routes/list.d.ts.map +1 -1
  172. package/dist/src/routes/list.js +2 -3
  173. package/dist/src/routes/list.js.map +1 -1
  174. package/dist/src/routes/policies.d.ts +6 -4
  175. package/dist/src/routes/policies.d.ts.map +1 -1
  176. package/dist/src/routes/policies.js +22 -16
  177. package/dist/src/routes/policies.js.map +1 -1
  178. package/dist/src/routes/raw.d.ts.map +1 -1
  179. package/dist/src/routes/raw.js +65 -11
  180. package/dist/src/routes/raw.js.map +1 -1
  181. package/dist/src/routes/sources.d.ts +60 -18
  182. package/dist/src/routes/sources.d.ts.map +1 -1
  183. package/dist/src/routes/sources.js +411 -299
  184. package/dist/src/routes/sources.js.map +1 -1
  185. package/dist/src/services/BridgeService.d.ts +15 -35
  186. package/dist/src/services/BridgeService.d.ts.map +1 -1
  187. package/dist/src/services/BridgeService.js +30 -88
  188. package/dist/src/services/BridgeService.js.map +1 -1
  189. package/dist/src/services/FilesystemService.d.ts +3 -2
  190. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  191. package/dist/src/services/FilesystemService.js +110 -32
  192. package/dist/src/services/FilesystemService.js.map +1 -1
  193. package/dist/src/services/FilesystemUiService.d.ts +4 -1
  194. package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
  195. package/dist/src/services/FilesystemUiService.js +66 -25
  196. package/dist/src/services/FilesystemUiService.js.map +1 -1
  197. package/dist/src/services/SyncScheduler.d.ts +12 -4
  198. package/dist/src/services/SyncScheduler.d.ts.map +1 -1
  199. package/dist/src/services/SyncScheduler.js +37 -8
  200. package/dist/src/services/SyncScheduler.js.map +1 -1
  201. package/dist/src/services/baselineRepair.d.ts +13 -3
  202. package/dist/src/services/baselineRepair.d.ts.map +1 -1
  203. package/dist/src/services/baselineRepair.js +13 -3
  204. package/dist/src/services/baselineRepair.js.map +1 -1
  205. package/dist/src/services/declaredSources.d.ts +125 -42
  206. package/dist/src/services/declaredSources.d.ts.map +1 -1
  207. package/dist/src/services/declaredSources.js +117 -45
  208. package/dist/src/services/declaredSources.js.map +1 -1
  209. package/dist/src/store/dataFormats.d.ts +17 -2
  210. package/dist/src/store/dataFormats.d.ts.map +1 -1
  211. package/dist/src/store/dataFormats.js +18 -3
  212. package/dist/src/store/dataFormats.js.map +1 -1
  213. package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
  214. package/dist/src/sync/DeltaSyncRunner.js +144 -4
  215. package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
  216. package/dist/src/sync/SyncJobQueue.d.ts +7 -2
  217. package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
  218. package/dist/src/sync/SyncJobQueue.js +16 -10
  219. package/dist/src/sync/SyncJobQueue.js.map +1 -1
  220. package/dist/src/sync/SyncWatchService.d.ts +21 -4
  221. package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
  222. package/dist/src/sync/SyncWatchService.js +115 -10
  223. package/dist/src/sync/SyncWatchService.js.map +1 -1
  224. package/dist/src/sync/categoryInference.d.ts +1 -1
  225. package/dist/src/sync/categoryInference.js +1 -1
  226. package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
  227. package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
  228. package/dist/src/usecases/admitSourceRoot.js +85 -0
  229. package/dist/src/usecases/admitSourceRoot.js.map +1 -0
  230. package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
  231. package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
  232. package/dist/src/usecases/browseSourceFolders.js +210 -0
  233. package/dist/src/usecases/browseSourceFolders.js.map +1 -0
  234. package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
  235. package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
  236. package/dist/src/usecases/createDeclaredSources.js +39 -0
  237. package/dist/src/usecases/createDeclaredSources.js.map +1 -1
  238. package/dist/src/usecases/list.d.ts.map +1 -1
  239. package/dist/src/usecases/list.js +83 -26
  240. package/dist/src/usecases/list.js.map +1 -1
  241. package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
  242. package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
  243. package/dist/src/usecases/listAttachableDevices.js +10 -0
  244. package/dist/src/usecases/listAttachableDevices.js.map +1 -0
  245. package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
  246. package/dist/src/usecases/listSourcePackages.js +2 -5
  247. package/dist/src/usecases/listSourcePackages.js.map +1 -1
  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 +15 -0
  253. package/dist/src/usecases/readFiles.js.map +1 -1
  254. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
  255. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
  256. package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
  257. package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
  258. package/dist/src/usecases/revokePrincipal.d.ts +7 -5
  259. package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
  260. package/dist/src/usecases/revokePrincipal.js +29 -7
  261. package/dist/src/usecases/revokePrincipal.js.map +1 -1
  262. package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
  263. package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
  264. package/dist/src/usecases/scheduleSourceSync.js +6 -5
  265. package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
  266. package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
  267. package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
  268. package/dist/src/usecases/triggerSourceSync.js +75 -0
  269. package/dist/src/usecases/triggerSourceSync.js.map +1 -0
  270. package/dist/src/usecases/uploadBinary.d.ts +2 -1
  271. package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
  272. package/dist/src/usecases/uploadBinary.js +2 -2
  273. package/dist/src/usecases/uploadBinary.js.map +1 -1
  274. package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
  275. package/dist/src/usecases/vector/endpointGuard.js +1 -1
  276. package/dist/src/vector/qdrantClient.d.ts +1 -1
  277. package/dist/src/vector/qdrantClient.js +1 -1
  278. package/dist/src/wiring.d.ts +3 -0
  279. package/dist/src/wiring.d.ts.map +1 -1
  280. package/dist/src/wiring.js +72 -6
  281. package/dist/src/wiring.js.map +1 -1
  282. package/package.json +145 -54
  283. package/skills/connect-business-data/SKILL.md +2 -2
  284. package/skills/create-package/SKILL.md +18 -18
  285. package/skills/create-package/references/config-and-credentials.md +16 -13
  286. package/skills/create-package/references/connectors-and-uri-policies.md +36 -27
  287. package/skills/create-package/references/manifest-and-discovery.md +12 -12
  288. package/skills/create-package/references/runtime-and-build.md +18 -18
  289. package/skills/create-package/references/skills-and-context.md +35 -19
  290. package/skills/create-package/references/surfaces-and-design.md +16 -16
  291. package/skills/create-package/references/team-and-workflows.md +2 -2
  292. package/skills/create-package/references/test-and-ship.md +17 -17
  293. package/skills/create-package/references/tools-and-schemas.md +5 -5
  294. package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +78 -64
  295. package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
  296. package/skills/git/SKILL.md +31 -29
  297. package/skills/manage-files/SKILL.md +11 -14
  298. package/skills/manage-sources/SKILL.md +85 -45
  299. package/skills/manage-uri-policy/SKILL.md +9 -6
  300. package/skills/sync/SKILL.md +1 -1
  301. package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
  302. package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
  303. package/dist/src/connectors/LocalDiskConnector.js +0 -1233
  304. package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
  305. package/skills/manage-files/scripts/copy.sh +0 -7
  306. package/skills/manage-files/scripts/create.sh +0 -7
  307. package/skills/manage-files/scripts/mkdir.sh +0 -7
  308. package/skills/manage-files/scripts/modify.sh +0 -7
  309. package/skills/manage-files/scripts/upload.sh +0 -16
  310. package/skills/manage-files/scripts/write.sh +0 -29
  311. package/skills/manage-sources/scripts/reembed.sh +0 -23
package/app/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Brain UI
2
2
 
3
- React surface contributed by `@neuralis/brain-core`: the Files widget, the `filesystem.change` card, and the host integration entry points the workspace uses to mount them.
3
+ React surface contributed by `@neuralis/brain-core`: the Files widget, the `filesystem.change` card, and the host integration entry points the workspace uses to mount them. The Filesystem dock declares `app/shared/filesystem.png`, retaining Folder as its glyph fallback.
4
4
 
5
5
  ## Responsibility
6
6
 
@@ -13,7 +13,7 @@
13
13
  import React, { memo, useCallback, useMemo } from 'react';
14
14
  import { FileText } from 'lucide-react';
15
15
  import { CardShell, ToolStatusIcon } from '@neuralis/agent-core/app/chat';
16
- import { computeLineCounts, isContentAction, parseToolInput } from './diffUtils';
16
+ import { computeLineCounts, computeStreamingHunks, extractContentString, isContentAction, parseToolInput, streamingWriteVerb } from './diffUtils';
17
17
 
18
18
  /**
19
19
  * The chrome this card actually renders: its OWN `renderHeader` (a single `px-3 py-1` row
@@ -77,8 +77,11 @@ function cacheEntryFor(tool: object, status: unknown): FsHeightCacheEntry {
77
77
  * `FilesystemChangeCard` (the estimate has to agree with the renderer or it is
78
78
  * guessing):
79
79
  *
80
- * - pending → the "Running…" mount state (32). Streamed content growing
81
- * toward the 90 cap is an in-place, bottom-pinned resize.
80
+ * - pending content → the 90 CEILING: a content write renders its diff rows
81
+ * while the arguments stream, growing toward the 90 cap
82
+ * every delta, so the reserve is the cap from the first
83
+ * delta that names the verb (over-reserve, the cheap side).
84
+ * - pending, otherwise → the "Running…" mount state / one-line preview (32).
82
85
  * - non-content op → `NonContentOpView`, one text row (32).
83
86
  * - content MODIFY → the 90 CEILING, deliberately (H2 pre-reserve): the real
84
87
  * baseline→current LCS diff arrives from an async fetch and
@@ -101,7 +104,14 @@ function filesystemCompactBodyHeight(
101
104
 
102
105
  let body: number;
103
106
  if (isPending) {
104
- body = COMPACT_FALLBACK_ROW_PX;
107
+ if (!isPendingContentWrite(tool.input)) {
108
+ // Not cached while the input still streams: the verb can arrive on a later
109
+ // delta under the same status, and a cached 32 would then under-reserve.
110
+ if (typeof tool.input === 'string') return COMPACT_FALLBACK_ROW_PX;
111
+ body = COMPACT_FALLBACK_ROW_PX;
112
+ } else {
113
+ body = FILESYSTEM_CARD_COMPACT_BODY_PX;
114
+ }
105
115
  } else {
106
116
  const input = parseToolInput(tool.input);
107
117
  const action = input && typeof input.action === 'string' ? input.action : undefined;
@@ -127,14 +137,42 @@ function filesystemCompactBodyHeight(
127
137
  return body;
128
138
  }
129
139
 
140
+ /**
141
+ * The pending card renders a diff (see `FilesystemChangeCard` `pendingHunks`): a
142
+ * streaming string whose `fs_write` content verb has opened, or a settled input
143
+ * that is a content action or a create.
144
+ */
145
+ function isPendingContentWrite(raw: unknown): boolean {
146
+ if (streamingWriteVerb(raw)) return true;
147
+ const input = parseToolInput(raw);
148
+ if (!input) return false;
149
+ const action = typeof input.action === 'string' ? input.action : undefined;
150
+ return action ? isContentAction(action) : extractContentString(input) !== null;
151
+ }
152
+
153
+ /** The expanded card's row cap — `FilesystemChangeCard`'s `EXPANDED_MAX_DIFF_LINES`. */
154
+ const EXPANDED_MAX_DIFF_LINES = 200;
155
+
130
156
  /**
131
157
  * EXPANDED body — the cap is gone and the body is the whole diff. This branch is not an
132
158
  * optimization — it is the single largest measured defect in the model. Live-measured
133
159
  * 2026-08-02: this card estimated **114px** while rendering **1479px** expanded, a
134
160
  * **−1353px** under-reserve, which paints the following row directly on top of it. Line
135
161
  * counts come from the SAME `computeLineCounts` the header badge uses.
162
+ *
163
+ * A STREAMING content write (the input is still the delta string) renders the TAIL of
164
+ * the rows `computeStreamingHunks` builds, up to the 200-row cap plus the "lines skipped"
165
+ * row — so it is estimated from the same rows, input-only and exact (no context
166
+ * allowance), and NEVER cached: the next delta grows it under the same status.
136
167
  */
137
168
  function filesystemExpandedBodyHeight(tool: { input?: unknown }, status: unknown): number {
169
+ const streaming = typeof tool.input === 'string' ? computeStreamingHunks(tool.input) : null;
170
+ if (streaming) {
171
+ let rows = 0;
172
+ for (const hunk of streaming) rows += hunk.lines.length;
173
+ const shown = rows > EXPANDED_MAX_DIFF_LINES ? EXPANDED_MAX_DIFF_LINES + 1 : rows;
174
+ return Math.max(FILESYSTEM_CARD_COMPACT_BODY_PX, DIFF_BODY_PADDING_PX + shown * DIFF_LINE_PX);
175
+ }
138
176
  const entry = cacheEntryFor(tool as object, status);
139
177
  if (entry.expanded !== undefined) return entry.expanded;
140
178
 
@@ -20,7 +20,7 @@
20
20
 
21
21
  import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
22
22
  import type { CardProps } from '@neuralis/agent-core/app/chat';
23
- import { previewInput, extractPathsFromToolCall } from '@neuralis/agent-core/app/chat';
23
+ import { previewInput, extractPathsFromToolCall, extractStreamingField } from '@neuralis/agent-core/app/chat';
24
24
  import { useChatHost, useHasRowIdentity, useRowDisclosureValue } from '@neuralis/agent-core/app/chat';
25
25
  import { useWorkspaceChatStore } from '@neuralis/agent-core/app/chat/store';
26
26
  import { readFiles } from '../filesystem/api';
@@ -36,12 +36,14 @@ import {
36
36
  computeTextInsertDiff,
37
37
  computeLineCounts,
38
38
  computeLcsDiff,
39
+ computeStreamingHunks,
39
40
  countHunks,
40
41
  detectLanguage,
41
42
  isContentAction,
42
43
  extractContentString,
43
44
  extractResultVersionDiff,
44
45
  parseToolInput,
46
+ tailDiffHunks,
45
47
  } from './diffUtils';
46
48
  import type { DiffHunk } from './diffUtils';
47
49
 
@@ -91,9 +93,15 @@ export const FilesystemChangeCard = memo(function FilesystemChangeCard({ tool, s
91
93
  const maxDiffLines = isCollapsedRow ? COMPACT_MAX_DIFF_LINES : EXPANDED_MAX_DIFF_LINES;
92
94
 
93
95
  const input = useMemo(() => parseToolInput(tool.input), [tool.input]);
96
+ // While the arguments stream, `tool.input` is the partial STRING: the uri is
97
+ // read out of it (head-windowed, trailing-partial) so the header names the file
98
+ // before the call settles.
94
99
  const uri = useMemo(() => {
95
100
  const paths = extractPathsFromToolCall(tool);
96
- return paths[0] || (input ? (strField(input, 'uri') || strField(input, 'path')) : undefined);
101
+ return paths[0]
102
+ || (input ? (strField(input, 'uri') || strField(input, 'path')) : undefined)
103
+ || extractStreamingField(tool.input, 'uri')
104
+ || extractStreamingField(tool.input, 'path');
97
105
  }, [tool, input]);
98
106
 
99
107
  const normalizedStatus = String(status || '').toLowerCase();
@@ -129,7 +137,7 @@ export const FilesystemChangeCard = memo(function FilesystemChangeCard({ tool, s
129
137
 
130
138
  const hunks = useMemo((): DiffHunk[] => {
131
139
  if (!input) return [];
132
- if (isPending) return []; // During streaming, we show raw content instead
140
+ if (isPending) return []; // the pending branch renders `pendingHunks`
133
141
  return computeHunks(action, input);
134
142
  }, [action, input, isPending]);
135
143
 
@@ -139,14 +147,25 @@ export const FilesystemChangeCard = memo(function FilesystemChangeCard({ tool, s
139
147
  host.openFilesystemWidget?.(agentId, clickedUri);
140
148
  }, [conversationAgentId, host]);
141
149
 
142
- // Streaming preview — the shared `previewInput` (agent-core's streaming-helper
143
- // carrier): a pending-scoped 32 000-char head window over `extractToolDetails`,
144
- // O(window) per delta with a byte-identical preview. The input itself is never
145
- // truncated (the completed object renders in full above).
150
+ // Streaming diff — while the call runs, a CONTENT write renders its red/green
151
+ // rows from the input as it arrives: a settled input through the same builders
152
+ // as the completed card, a partial string through `computeStreamingHunks`
153
+ // (head-windowed carrier reads, O(window) per delta, never the LCS). The rows
154
+ // follow the text being written (`tailDiffHunks`). The input itself is never
155
+ // truncated. `null` = no content verb (yet): the plain `previewInput` preview.
156
+ const pendingHunks = useMemo((): DiffHunk[] | null => {
157
+ if (!isPending) return null;
158
+ if (input) return !action || isContentAction(action) ? computeHunks(action, input) : null;
159
+ return computeStreamingHunks(tool.input);
160
+ }, [isPending, input, action, tool.input]);
146
161
  const streamContent = useMemo(() => {
147
- if (!isPending) return undefined;
162
+ if (!isPending || pendingHunks) return undefined;
148
163
  return previewInput(tool.input, true);
149
- }, [isPending, tool.input]);
164
+ }, [isPending, pendingHunks, tool.input]);
165
+ const pendingView = useMemo(
166
+ () => (pendingHunks && pendingHunks.length > 0 ? tailDiffHunks(pendingHunks, maxDiffLines) : null),
167
+ [pendingHunks, maxDiffLines],
168
+ );
150
169
 
151
170
  // --- Real baseline→current diff for completed MODIFY (not create) ---
152
171
  // The tool result carries only `version.diff` COUNTS, not the old content, so
@@ -227,7 +246,7 @@ export const FilesystemChangeCard = memo(function FilesystemChangeCard({ tool, s
227
246
  const displayHunks = realHunks ?? hunks;
228
247
  const displayCounts = realHunks ? countHunks(realHunks) : lineCounts;
229
248
 
230
- // --- Streaming state: show raw content with green tint ---
249
+ // --- Streaming state: the input-only diff, or the plain preview ---
231
250
  if (isPending) {
232
251
  return (
233
252
  <FilesystemCardShell
@@ -239,7 +258,11 @@ export const FilesystemChangeCard = memo(function FilesystemChangeCard({ tool, s
239
258
  isInterrupted={false}
240
259
  onUriClick={uri ? handleOpenUri : undefined}
241
260
  >
242
- <StreamingContent content={streamContent} />
261
+ {pendingView ? (
262
+ <DiffView hunks={pendingView} language={language} maxLines={maxDiffLines} />
263
+ ) : (
264
+ <StreamingContent content={streamContent} />
265
+ )}
243
266
  </FilesystemCardShell>
244
267
  );
245
268
  }
@@ -10,15 +10,16 @@ Filesystem agent-card: an IDE-style diff view rendered for `fs_write` tool calls
10
10
 
11
11
  ## Public Surface
12
12
 
13
- - `FilesystemChangeCard` — `(props: CardProps) => JSX.Element`. Memoised; reads its streaming preview via `previewInput` and paths via `extractPathsFromToolCall`, both from `@neuralis/agent-core/app/chat`.
14
- - **Never re-scan the whole growing input on every `tool_use_delta`.** While the tool is streaming, `tool.input` is a raw accumulating JSON STRING, and both readers ran over ALL of it per delta — O(n²) across a large `fs_create`, and one of the per-commit costs behind the React #185 `rate × saturation` class (see agent-core `app/chat/ui/timeline/README.md`). Two O(1)/O(window) guards, neither of which truncates the input — both now backed by agent-core's shared streaming-helper carrier (`domain/toolPresentation.ts`, streaming parity): `parseToolInput` (in `diffUtils.ts` — the fs-specific NAME, DELEGATING to the shared `parseSettledObject`, the ONE `}`-tail-gate implementation; still the one owner shared with the height model so estimate and render discriminate variants from the SAME parse) skips the guaranteed-to-throw parse of partial JSON while a completed object still parses in full, and `streamContent` is the shared `previewInput(tool.input, true)` (the pending-scoped 32 000-char `LIVE_PARSE_WINDOW` head window over `extractToolDetails`, which caps its own output at 20 000 chars — the preview is byte-identical; the former private copy of this window here is gone). Do not remove either guard, do not re-inline a private copy, and do not "extend" them into truncating the stored input.
13
+ - `FilesystemChangeCard` — `(props: CardProps) => JSX.Element`. Memoised; reads paths via `extractPathsFromToolCall` and, while the call streams, its uri via `extractStreamingField` and its rows via `computeStreamingHunks` (below), all through `@neuralis/agent-core/app/chat`.
14
+ - **The pending card renders the diff while the arguments stream.** A content `fs_write` (`edit` / `insert` / `lines` / `overwrite` / `create`) shows the uri in its header and red/green rows from the partial input: the closed `edit[]` elements (`extractSettledArrayItems` + `extractJsonStringField`) plus the still-open tail element (`extractStreamingArrayTail` + `extractStreamingField`), or the verb's one text field. The rows are the input-only line split — `computeLcsDiff` never runs while pending — cut to the LAST `maxDiffLines` rows (`tailDiffHunks`, the dropped head counted) so the card follows the text being written; the `+N/−M` badge stays off until the call settles. A structural verb (move / copy / …) and a call whose verb has not arrived keep the one-line `previewInput` preview. No animation, no remount: the row is the same card, re-rendered per delta.
15
+ - **Never re-scan the whole growing input on every `tool_use_delta`.** While the tool is streaming, `tool.input` is a raw accumulating JSON STRING, and both readers ran over ALL of it per delta — O(n²) across a large `fs_create`, and one of the per-commit costs behind the React #185 `rate × saturation` class (see agent-core `app/chat/ui/timeline/README.md`). Every pending read is an O(1)/O(window) guard and none truncates the input — both now backed by agent-core's shared streaming-helper carrier (`domain/toolPresentation.ts`, streaming parity): `parseToolInput` (in `diffUtils.ts` — the fs-specific NAME, DELEGATING to the shared `parseSettledObject`, the ONE `}`-tail-gate implementation; still the one owner shared with the height model so estimate and render discriminate variants from the SAME parse) skips the guaranteed-to-throw parse of partial JSON while a completed object still parses in full, the streaming rows read only the 32 000-char `LIVE_PARSE_WINDOW` head (a longer call stops at the window; nothing past it is parsed), and `streamContent` is the shared `previewInput(tool.input, true)` (the same head window over `extractToolDetails`, which caps its own output at 20 000 chars — the preview is byte-identical; the former private copy of this window here is gone). Do not remove a guard, do not re-inline a private copy, and do not "extend" them into truncating the stored input.
15
16
  - **The compact diff mount is CLAMPED to 8 rows** (`COMPACT_MAX_DIFF_LINES`): the compact body clips at 90px, so at most ~6 rows are ever visible, and mounting the full 200-row `DiffLineRow` set into a clipped body was pure per-mount cost on the scroll path (H6, residual-slide audit 2026-08-07). The collapsed state is read via agent-core's READ-ONLY `useRowDisclosureValue('card')` + `useHasRowIdentity()` (never a second `useRowDisclosure` — that would fight the owning `CardShell` mirror); outside the virtualized timeline (transcript overlays, the Pending tab) there is no row identity and the full 200-line render stands. The clamp never changes the clipped visible height (8 × 18px ≥ 90px cap).
16
17
  - `FilesystemCardShell` — filesystem-specific header atop the unified `CardShell`. Two status inputs that must never be conflated: `isPending` is the tool CALL running (the ONLY spinner source) and `reviewPending` is the RESULT having landed `pending_review` — a governance state frozen in the result snapshot, rendered as the settled check's accessible name (`pending review`, `ToolStatusIcon statusLabel`) on every op alike. `CardShell` receives `committed` for such a result: a `pending_*` word there would re-arm its stream-follow pin and, through `resolveCardTone`'s substring match, the spinner.
17
- - `estimateFilesystemChangeCardHeight` (exported from `FilesystemCardShell.tsx`) — the card's OWN row-height model, handed to agent-core's card registry at install time. agent-core's timeline asks the registry for it before a row has been measured; a card with no model falls back to a generic constant. It is declared next to the chrome it describes because it drifted badly when it lived in the timeline: the host-side table once claimed 240 against a real ~117 — a live-measured `first-measure` delta of **−181px**, the worst of any builtin. The **COMPACT branch is variant-aware** (H1, residual-slide audit 2026-08-07): the body is content-height clipped at `maxHeight: 90`, NOT a constant, so the model is `27 + min(90, bodyContent)` where `bodyContent` mirrors the render dispatch — `18 × diffLines` for a CREATE's input-derived rows, `32` for the one-line fallbacks (pending "Running…", "Done"/"Operation failed", non-content ops), and the **90 CEILING for a completed content MODIFY deliberately** (H2 pre-reserve: the real LCS diff arrives from an async fetch and adds context rows the input-derived count cannot see — ceiling bias accepted so the arrival does not resize the row in view). The previous constant-117 model over-reserved every short-bodied variant by +18…+72. The **EXPANDED branch** models the diff body from `computeLineCounts` (the same helper the header badge uses) with a deliberate upward margin, and is not optional: returning the compact height for a row the disclosure store says is expanded measured **−1353px** live (114 estimated against 1479 rendered), and an under-reserve does not leave a gap — it paints the next row on top of this one. The memo is keyed by the tool object **with the status it computed at** — the tool mutates by reference across pending→settle, so a reference-only memo served the pending height after settle (the H3 class). Keep both branches in step with `renderHeader` / `compactHeight` / `DiffLineRow`'s `h-[18px]`, and keep the export a module-level reference (the registry compares estimator identity). Contract: agent-core `app/chat/cards/README.md` → "Card height model".
18
+ - `estimateFilesystemChangeCardHeight` (exported from `FilesystemCardShell.tsx`) — the card's OWN row-height model, handed to agent-core's card registry at install time. agent-core's timeline asks the registry for it before a row has been measured; a card with no model falls back to a generic constant. It is declared next to the chrome it describes because it drifted badly when it lived in the timeline: the host-side table once claimed 240 against a real ~117 — a live-measured `first-measure` delta of **−181px**, the worst of any builtin. The **COMPACT branch is variant-aware** (H1, residual-slide audit 2026-08-07): the body is content-height clipped at `maxHeight: 90`, NOT a constant, so the model is `27 + min(90, bodyContent)` where `bodyContent` mirrors the render dispatch — `18 × diffLines` for a CREATE's input-derived rows, `32` for the one-line fallbacks (a pending call with no content verb yet, "Done"/"Operation failed", non-content ops), the **90 CEILING for a PENDING content write** (its rows grow toward the cap every delta; decided from the same `streamingWriteVerb` / `parseToolInput` the render reads, and a still-undecided streaming estimate is never memoized), and the **90 CEILING for a completed content MODIFY deliberately** (H2 pre-reserve: the real LCS diff arrives from an async fetch and adds context rows the input-derived count cannot see — ceiling bias accepted so the arrival does not resize the row in view). The previous constant-117 model over-reserved every short-bodied variant by +18…+72. The **EXPANDED branch** models the diff body from `computeLineCounts` (the same helper the header badge uses) with a deliberate upward margin, and is not optional: returning the compact height for a row the disclosure store says is expanded measured **−1353px** live (114 estimated against 1479 rendered), and an under-reserve does not leave a gap — it paints the next row on top of this one. The memo is keyed by the tool object **with the status it computed at** — the tool mutates by reference across pending→settle, so a reference-only memo served the pending height after settle (the H3 class). Keep both branches in step with `renderHeader` / `compactHeight` / `DiffLineRow`'s `h-[18px]`, and keep the export a module-level reference (the registry compares estimator identity). Contract: agent-core `app/chat/cards/README.md` → "Card height model".
18
19
  - `DiffView` — green/red line renderer with single-column line numbers, truncation and skipped-context markers.
19
20
  - `NonContentOpView` — summary renderer for `set_fields`, `move`, `delete`, `revert`.
20
21
  - `diffLine.tsx` — the **shared single-column diff-line primitive**: `DiffLineRow({ lineNo?, type, content })` + the `DIFF_LINE_BG` palette (`add` emerald / `remove` red / `context` colourless). ONE owning file for the green/red/colourless look + the line-number gutter. Its only consumer today is this card's `DiffView` — the Files-widget review diff moved to `@codemirror/merge` (`ReviewDiffView`) and the old hand-rolled `app/filesystem/ui/DiffView.tsx` was deleted. Keep it a standalone primitive: it is still the shared shape any future single-column diff renderer should reuse. No `+`/`-` prefix char — the row background conveys add/remove. `DiffView` owns one shared content-width wrapper; every row fills it so shorter coloured lines also cover the horizontal scroll extent. **Gutter is git / VS Code unified-diff style:** every row numbered — REMOVE rows show their OLD (pre-edit) file line number, ADD + CONTEXT show the NEW one (so the column intentionally jumps back at a block replacement: old positions then new).
21
- - Diff utilities (`diffUtils.ts`): input-derived hunk builders (`computeCreateDiff`, `computeReplaceTextDiff`, `computeMultiEditDiff`, `computeUpdateDiff`, `computeTextInsertDiff`, `computeLineCounts`) AND the real baseline→current LCS (`computeLcsDiff` + `countHunks`) shared with Changes. Callers must bound the O(n·m) matrix before invoking `computeLcsDiff`; pending previews use `pendingDiffWithinBudget`, because a byte cap alone does not bound newline-dense text. Plus `detectLanguage`, `isContentAction`, `extractContentString`, and `parseToolInput` (the streaming-tolerant `}`-tail-checked input parse — the fs-specific name for agent-core's shared `parseSettledObject`; ONE owner within brain-core for the card AND the height model) — which now also runs `normalizeCardInput`, the ONE place two write vocabularies are reconciled. `fs_write` carries its intent in KEYS (`edit`, `insert`, `lines`, `overwrite`, `create`, `fields`, `move`, `copy`, `mkdir`, `revert`, `delete`); the old pair carried it in `action` plus a flat field per shape. A verb-key input is PROJECTED onto the shape the views already read (`edit[].old_string/new_string` → the `replace_text` `edits[]` pair, so the red/green hunk appears before the tool result lands and a pending approval renders the PROPOSITION), and a row written by `fs_create`/`fs_modify` passes through untouched — which is what keeps the stored history rendering. A composed call previews the most informative verb (`edit` before `lines`, `insert`, `overwrite`, `create`). Pinned by `test/ui/verbKeyCardInput.test.ts`. Types: `DiffHunk`, `DiffLine`, `DiffLineType`.
22
+ - Diff utilities (`diffUtils.ts`): input-derived hunk builders (`computeCreateDiff`, `computeReplaceTextDiff`, `computeMultiEditDiff`, `computeUpdateDiff`, `computeTextInsertDiff`, `computeLineCounts`) AND the real baseline→current LCS (`computeLcsDiff` + `countHunks`) shared with Changes. Callers must bound the O(n·m) matrix before invoking `computeLcsDiff`; pending previews use `pendingDiffWithinBudget`, because a byte cap alone does not bound newline-dense text. Plus `detectLanguage`, `isContentAction`, `extractContentString`, and `parseToolInput` (the streaming-tolerant `}`-tail-checked input parse — the fs-specific name for agent-core's shared `parseSettledObject`; ONE owner within brain-core for the card AND the height model) — which now also runs `normalizeCardInput`, the ONE place two write vocabularies are reconciled. `fs_write` carries its intent in KEYS (`edit`, `insert`, `lines`, `overwrite`, `create`, `fields`, `move`, `copy`, `mkdir`, `revert`, `delete`); the old pair carried it in `action` plus a flat field per shape. A verb-key input is PROJECTED onto the shape the views already read (`edit[].old_string/new_string` → the `replace_text` `edits[]` pair, so the red/green hunk appears before the tool result lands and a pending approval renders the PROPOSITION), and a row written by `fs_create`/`fs_modify` passes through untouched — which is what keeps the stored history rendering. A composed call previews the most informative verb (`edit` before `lines`, `insert`, `overwrite`, `create`). Pinned by `test/ui/verbKeyCardInput.test.ts`. The STREAMING twin of that projection is `streamingWriteVerb` (the first `fs_write` content-verb key whose value has opened in the partial string's head window — the card's pending branch AND the height model decide from it) + `computeStreamingHunks` (the input-only builders over the carrier reads, `null` when no content verb has arrived) + `tailDiffHunks`. Types: `DiffHunk`, `DiffLine`, `DiffLineType`, `StreamingWriteVerb`.
22
23
  - Composer-aggregation ownership (`diffUtils.ts`, the composer-contract inversion): `extractResultVersionDiff` (the ONE `structuredContent.version.diff` reader — the card's `+N/−M` header badge AND the composer extractor share it; moved here from both its former private copy in `FilesystemChangeCard` and agent-core's deleted `extractResultDiff`), `extractFilesystemComposerChanges` (paths via agent-core's shared `extractPathsFromToolCall` + the former `deriveOpenPath` input-uri fallback for `ok:false` results; diff on the first entry), and `FILESYSTEM_COMPOSER_CONTRACT` (`{ tab: 'changes', changes }`, module-level stable) — registered in `installFilesystemHostComponents.tsx` so agent-core's Changes tab + post-stream summary learn about fs changes from the card REGISTRY, never from literals. Pinned by `test/ui/composerContract.test.ts` (parity: success uri+diff, diff-less results, `ok:false` input fallback, files[] multi-path, empty, and the reducer's string-input forms — a COMPLETE JSON string parses via the settled-object gate, a PARTIAL in-flight string yields nothing and is covered by agent-core's aggregate `openPath || label` fallback row).
23
24
 
24
25
  ## How it plugs into the package-system kernel
@@ -44,4 +45,4 @@ Filesystem agent-card: an IDE-style diff view rendered for `fs_write` tool calls
44
45
 
45
46
  ## Tests
46
47
 
47
- `test/ui/filesystemCardHeight.test.ts` pins the card's height model; `test/ui/filesystemCardPendingReview.test.tsx` renders the card (jsdom, a cast `ChatHostProvider` stub) and pins the `pending_review`-is-a-name floor with an in-flight control; `test/ui/composerContract.test.ts` pins the composer extractor. The chat registry and the settled-row animation guard live in agent-core (`app/chat/ui/timeline/__tests__/settledRowsNoInfiniteAnimation.test.tsx`). Diff utilities have inline unit coverage.
48
+ `test/ui/filesystemCardHeight.test.ts` pins the card's height model; `test/ui/filesystemCardStreaming.test.tsx` renders a streaming `fs_write` edit at three cut points (uri + red/green rows), a >32K create stopping at the parse window, the pending estimate against the rendered rows and a structural-verb control; `test/ui/filesystemCardPendingReview.test.tsx` renders the card (jsdom, a cast `ChatHostProvider` stub) and pins the `pending_review`-is-a-name floor with an in-flight control; `test/ui/composerContract.test.ts` pins the composer extractor. The chat registry and the settled-row animation guard live in agent-core (`app/chat/ui/timeline/__tests__/settledRowsNoInfiniteAnimation.test.tsx`). Diff utilities have inline unit coverage.
@@ -16,7 +16,15 @@
16
16
  * Changes tab AND the chat card's completed-MODIFY branch, mirroring the file editor.
17
17
  */
18
18
 
19
- import { extractPathsFromToolCall, parseSettledObject } from '@neuralis/agent-core/app/chat';
19
+ import {
20
+ extractJsonStringField,
21
+ extractPathsFromToolCall,
22
+ extractSettledArrayItems,
23
+ extractStreamingArrayTail,
24
+ extractStreamingField,
25
+ LIVE_PARSE_WINDOW,
26
+ parseSettledObject,
27
+ } from '@neuralis/agent-core/app/chat';
20
28
  import { splitDiffLines } from '@neuralis/brain-core/domain/pending-change';
21
29
  import type {
22
30
  CardComposerContract,
@@ -185,6 +193,96 @@ export function parseToolInput(raw: unknown): Record<string, unknown> | null {
185
193
  return normalizeCardInput(parseSettledObject(raw));
186
194
  }
187
195
 
196
+ // ---------------------------------------------------------------------------
197
+ // The STREAMING preview — `fs_write` while its arguments still arrive
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /** The `fs_write` content verbs a streaming card can preview, in schema order. */
201
+ export type StreamingWriteVerb = 'edit' | 'insert' | 'lines' | 'overwrite' | 'create';
202
+
203
+ const STREAMING_VERB_RE = /"(edit|insert|lines|overwrite|create)"\s*:\s*[[{]/;
204
+
205
+ /**
206
+ * The content verb of an `fs_write` call whose input is still the accumulating
207
+ * `tool_use_delta` STRING — the first verb KEY whose value has opened, read from
208
+ * the head window. A key inside a string value cannot match (its quotes arrive
209
+ * escaped). `undefined` until one arrives, for a structural verb, and for any
210
+ * non-string input (a settled input is read through `parseToolInput`).
211
+ *
212
+ * ONE owner: the card's pending branch and the height model both decide from it.
213
+ */
214
+ export function streamingWriteVerb(input: unknown): StreamingWriteVerb | undefined {
215
+ if (typeof input !== 'string') return undefined;
216
+ const head = input.length > LIVE_PARSE_WINDOW ? input.slice(0, LIVE_PARSE_WINDOW) : input;
217
+ return STREAMING_VERB_RE.exec(head)?.[1] as StreamingWriteVerb | undefined;
218
+ }
219
+
220
+ /**
221
+ * Line-split hunks for a STREAMING `fs_write` content verb, or `null` when no
222
+ * content verb has arrived (the caller keeps its plain preview). Edit pairs come
223
+ * from the closed `edit[]` elements plus the still-open tail element; the other
224
+ * verbs stream their one text field. Every read is head-windowed
225
+ * (`LIVE_PARSE_WINDOW`), never a `JSON.parse`, and the hunks use the input-only
226
+ * builders — never `computeLcsDiff`, whose O(n·m) matrix has no place per delta.
227
+ */
228
+ export function computeStreamingHunks(input: unknown): DiffHunk[] | null {
229
+ const verb = streamingWriteVerb(input);
230
+ if (!verb) return null;
231
+ if (verb === 'edit') {
232
+ const edits: Array<{ find: string; replace: string }> = [];
233
+ for (const item of extractSettledArrayItems(input, 'edit')) {
234
+ edits.push({
235
+ find: extractJsonStringField(item, 'old_string') ?? '',
236
+ replace: extractJsonStringField(item, 'new_string') ?? '',
237
+ });
238
+ }
239
+ const tail = extractStreamingArrayTail(input, 'edit');
240
+ if (tail) {
241
+ edits.push({ find: readStreamingString(tail, 'old_string'), replace: readStreamingString(tail, 'new_string') });
242
+ }
243
+ return computeMultiEditDiff(edits);
244
+ }
245
+ if (verb === 'insert') return computeTextInsertDiff(readStreamingString(input, 'insert_text'));
246
+ if (verb === 'lines') return computeUpdateDiff(readStreamingString(input, 'content'));
247
+ return computeCreateDiff(readStreamingString(input, 'content'));
248
+ }
249
+
250
+ /**
251
+ * `extractStreamingField`, tolerant of a delta that ended inside an escape: an odd
252
+ * trailing backslash has no complete capture, so the value would vanish for one
253
+ * delta and the rows would flicker. The half escape is left for the next delta.
254
+ */
255
+ function readStreamingString(text: unknown, field: string): string {
256
+ const value = extractStreamingField(text, field);
257
+ if (value !== undefined || typeof text !== 'string' || !text.endsWith('\\')) return value ?? '';
258
+ return extractStreamingField(text.slice(0, -1), field) ?? '';
259
+ }
260
+
261
+ /**
262
+ * The LAST `maxLines` rows of `hunks` — a streaming card follows the text being
263
+ * written, so the cut drops the HEAD and says how many rows it dropped (DiffView's
264
+ * own `maxLines` cut drops the tail, right for a settled card).
265
+ */
266
+ export function tailDiffHunks(hunks: DiffHunk[], maxLines: number): DiffHunk[] {
267
+ let total = 0;
268
+ for (const h of hunks) total += h.lines.length;
269
+ if (total <= maxLines) return hunks;
270
+ const kept: DiffHunk[] = [];
271
+ let budget = maxLines;
272
+ for (let i = hunks.length - 1; i >= 0 && budget > 0; i--) {
273
+ const h = hunks[i];
274
+ if (h.lines.length <= budget) {
275
+ kept.unshift(h);
276
+ budget -= h.lines.length;
277
+ } else {
278
+ kept.unshift({ skippedBefore: 0, lines: h.lines.slice(h.lines.length - budget) });
279
+ budget = 0;
280
+ }
281
+ }
282
+ kept[0] = { ...kept[0], skippedBefore: total - maxLines };
283
+ return kept;
284
+ }
285
+
188
286
  // ---------------------------------------------------------------------------
189
287
  // Real baseline → current diff (LCS) — shared by the Changes tab + the chat card
190
288
  // ---------------------------------------------------------------------------
@@ -47,8 +47,8 @@ export interface FilesystemHostValue {
47
47
  currentUser?: { name?: string; email?: string };
48
48
  /**
49
49
  * R2b — the signed-in user's role + ordinal priority within the active
50
- * project. Used to HIDE owner/admin-only controls (the `recognizesPackages`
51
- * source toggle, the package access-feature override) for non-owner callers.
50
+ * project. Used to HIDE owner/admin-only controls (the package-folders field,
51
+ * the package access-feature override) for non-owner callers.
52
52
  * UX-only: the server route is always the enforcement floor. Omitted ⇒ the
53
53
  * controls hide (deny-by-default). Owner/admin-effective = priority ≤ 2.
54
54
  */
@@ -66,6 +66,21 @@ export interface FilesystemHostValue {
66
66
  * greyed-out or locked row). Omitted / empty ⇒ hide, deny-by-default.
67
67
  */
68
68
  grantedFeatures?: readonly string[];
69
+ /**
70
+ * Open Admin → Packages (project scope) for this agent's window — where a
71
+ * package manager changes a package's trust, builds it or gates it with a
72
+ * feature. Optional: a host without it shows no link.
73
+ */
74
+ openAdminPackages?(agentId: string): void;
75
+ /**
76
+ * The folder picker's device asks, both agent-core routes: record a folder on
77
+ * the caller's OWN device for its owner to allow there (`POST
78
+ * devices/:id/requests`), and how many asks that device holds for this
79
+ * project (`GET devices/:id/requests`; `null` = could not ask). The grant
80
+ * stays on the machine. Optional: a host without them shows the CLI line only.
81
+ */
82
+ requestDeviceFolder?(input: { deviceId: string; path: string; list: 'read' | 'write'; projectId: string; agentId: string }): Promise<{ recorded: boolean; reason?: string }>;
83
+ countDeviceFolderRequests?(input: { deviceId: string; projectId: string; agentId: string }): Promise<number | null>;
69
84
  /**
70
85
  * Optional: resolve a package's dock-icon by slug. Used by the filetree
71
86
  * to show the package icon on `packages://<slug>/` and `data://<slug>/`
@@ -6,7 +6,7 @@ Typed client for brain-core package routes plus a small set of host-side helpers
6
6
 
7
7
  - Wrap a kernel `PackageApi` into typed functions that mirror every brain-core route; each takes that client as its LAST argument.
8
8
  - Build raw-file URLs (`getRawFileUrl`) for the `<img>` / `<video>` preview elements. **Not for an iframe or any other document embed** — the byte lane serves the executable document class (SVG, HTML, XML) as `content-disposition: attachment`, so a document embed against it renders nothing, and making it render again would re-open a stored-XSS path. There is exactly one call site today (`FileEditor`).
9
- - Provide the host-side helpers (`projectPackages.ts`) used by the Packages tab — these talk to the host's `/api/packages/runtime` and `/api/projects/:id/packages/:slug/{build,trust}` routes, NOT to brain-core.
9
+ - Provide the host-side reads (`projectPackages.ts`) used by the Packages tab — the host's scoped snapshot `/api/packages/runtime` (with the window's `x-agent-id`) and agent-core's diagnostics, NOT brain-core.
10
10
 
11
11
  ## Public Surface
12
12
 
@@ -14,13 +14,13 @@ Typed client for brain-core package routes plus a small set of host-side helpers
14
14
  - Pending changes (`index.ts`): `listPendingChanges` (with `include_orphans` it returns `orphanIds` beside the rows, from the server's SAME listing pass — the panel needs both on every mount, and two calls ran the whole dual-source listing twice), `previewPendingChange` (authoritative current/baseline states and fingerprint), `revertPendingChange`, `scanOrphanedPending` (the complete scanned page in `changes`, its orphan subset in `orphans`, and the same continuation), `discardPendingChange`, `dismissPendingChange`, `discardOrphanedPending`. Pending-backed approve/dismiss/revert carry the fresh `expectedFingerprint` and any required acknowledgement; a stale intent returns409. Types: `PendingChangeRecord`, `PendingChangesQuery`, `PendingPreview`, `PendingReviewIntent`. Pending clients preserve the page envelope and URI/cursor filters; count returns `{count, exact, complete, partial}` and safe unavailable branches; the store preserves its previous count when both branches fail.
15
15
  - Git (`index.ts`) — the `/git/*` client behind the Git Control panel. Reads: `getGitRepos`, `getGitStatus`, `getGitBranches`, `getGitDiff`, `getGitLog`, `getGitLogStats`, `getGitCommitFiles`, `getGitOutgoing`, `getGitShow`. Writes: `stagePaths`, `unstagePaths`, `stageHunk`, `unstageHunk`, `discardPaths`, `commitGit`, `createGitBranch`, `deleteGitBranch`, `restoreGit`, `checkoutGitBranch`. Remote (uri-policy `exec`): `fetchGit`, `pullGit`, `pushGit`. Type: `GitRescue` — the typed carrier a destructive op returns and `restoreGit` echoes back.
16
16
  - Sources (`index.ts`): `listSourceConfigs`, `updateSourceConfig`, `createSource`, `deleteSource`, `triggerSourceSync`, `getSyncStatus`, `cancelSync`, `resetSync`, `reindexSource` ("Rescan changes"), `estimateReembed` / `startReembed` (the same route with a `folder` / `uri` scope and `force: true` — a re-embed with the active model; the estimate answers before any side effect, a scope with nothing readable queues nothing), `purgeSourceIndex` (source/folder/file estimate or apply; keeps source and recovery content), `getSourcesHealth`, `browseDirectories`, `listSourceKinds`, `getSourceKindSchema`, `setSourceEnabled` (W3K), `listDiscoverableSources` (W3E). Types: `ReembedScope`, `ReembedEstimate`, `SourceKindRecord`, `SourceKindSchema`, `ResolveListOpts`, `DiscoverableSourceSuggestion`.
17
- - Project packages (`projectPackages.ts`): `listProjectPackages`, `buildPackage`, `setPackageTrust`, `setPackageAccessFeature`, `getGrantableFeatureIds` (the per-project base-access governance pair — see `../ui/README.md`), `getPackagesDiagnostics` (W3F — also consumed by chat-config Packages Overview through the agent-core ports). Types: `ProjectPackageRow`, `BuildResponse`, `TrustValue`, `PackageDiagnosticEntry`, `PackagesDiagnosticsResponse`.
17
+ - Project packages (`projectPackages.ts`): `listProjectPackages(projectId, agentId?)` (what the agent gets, native packages left out), `getPackagesDiagnostics` (W3F — also consumed by chat-config Packages Overview through the agent-core ports). Types: `ProjectPackageRow`, `PackageDiagnosticEntry`, `PackagesDiagnosticsResponse`. Trust, Build and the access feature are Admin → Packages calls, not here.
18
18
 
19
19
  ## How it plugs into the package-system kernel
20
20
 
21
21
  - A Files window passes the client its store was created with — `createPackageApi` over the window's project and BOUND agent — so the X-Project-Id / X-Agent-Id headers are the window's own, fixed before any await, whatever other window is mounted beside it; a body may still name another agent (a file read through its writing agent). `readFiles` alone also runs without a client, for the chat change card, which is not a Files window and resolves the first registered `brain-core` provider.
22
22
  - The `PackageApi` provided by `@neuralis/package-system/client` translates `.get(path, qs)`, `.post(path, body)`, `.patch`, `.del`, `.upload` into authenticated requests against `/api/packages/brain-core/<path>` with the active session attached by the host.
23
- - Project-package helpers go directly to host routes (`/api/packages/runtime`, `/api/projects/:id/packages/:slug/build`, `/.../trust`) — these are NOT package routes and are not part of the brain-core surface; the helpers live here only because the Files widget's Packages tab consumes them.
23
+ - Project-package reads go directly to the host's `/api/packages/runtime` — NOT a package route and not part of the brain-core surface; the helper lives here only because the Files widget's Packages tab consumes it.
24
24
 
25
25
  ## Security & scope
26
26
 
@@ -153,6 +153,8 @@ export async function triggerSourceSync(params: {
153
153
  agentId: string;
154
154
  projectId?: string;
155
155
  source: string;
156
+ /** One full pass may tombstone a source whose root now holds nothing (`root_empty`). */
157
+ confirmEmpty?: boolean;
156
158
  }, client: PackageApi): Promise<TriggerSourceSyncResponse> {
157
159
  return client.post('sources/sync', params);
158
160
  }
@@ -297,6 +299,7 @@ export async function getGitRepos(params: {
297
299
  * Whole-repo change-set + current branch for the focused git-repo source.
298
300
  * `status.upstream`/`ahead`/`behind` (C3) are present only when the branch has
299
301
  * an upstream — the branch-bar `↑N ↓M` chips (local refs; fetch lands in G4).
302
+ * A checkout git could not read answers `ok:false` with a fixed `error`.
300
303
  */
301
304
  export async function getGitStatus(params: {
302
305
  agentId: string;
@@ -304,12 +307,12 @@ export async function getGitStatus(params: {
304
307
  source: string;
305
308
  repo?: string;
306
309
  }, client: PackageApi): Promise<{
307
- ok: boolean;
310
+ ok: true;
308
311
  status: GitControlStatus;
309
312
  /** Bucket `+/-` totals for the section headers, summed server-side over the
310
313
  * rows in `status` — see `GitDiffTotals`. Absent on an older server. */
311
314
  totals?: { changes: GitDiffTotals; staged: GitDiffTotals };
312
- }> {
315
+ } | { ok: false; code: 'unavailable'; error: string }> {
313
316
  const qs: Record<string, string> = { agentId: params.agentId, source: params.source, ...repoQuery(params.repo) };
314
317
  if (params.projectId) qs.projectId = params.projectId;
315
318
  return client.get('git/status', qs);
@@ -1253,23 +1256,38 @@ export async function getSourcesHealth(params: {
1253
1256
  return client.get('sources/health', qs);
1254
1257
  }
1255
1258
 
1259
+ /** One level of a folder listing (`GET sources/browse`). */
1260
+ export type BrowseFoldersResult = {
1261
+ entries: Array<{ name: string; path: string; type: 'directory'; access?: 'read' | 'write' }>;
1262
+ atRoot?: boolean;
1263
+ /** Device only: the listing is the folders that device allows this host (empty path). */
1264
+ roots?: boolean;
1265
+ /** Device only: `ok`, or the device's fixed class (`ceiling_denied`, `device_offline`, `broker_outdated`, …). */
1266
+ state?: string;
1267
+ truncated?: boolean;
1268
+ /** Device only: linked folders, counted and not followed. */
1269
+ symlinks?: number;
1270
+ };
1271
+
1256
1272
  /**
1257
- * One level of directory listing, before attaching a source.
1273
+ * One level of folders before attaching a source — on this server, or with
1274
+ * `device` on the caller's OWN paired device (empty path there = the folders
1275
+ * the device allows this host).
1258
1276
  *
1259
- * `agentId` is REQUIRED (warded-wren): the route runs `ensureAccess` like every
1260
- * sibling handler, and that refuses a call with no agent in context. Every other
1261
- * source call on this client already sends it; browse was the one that did not.
1262
- * The route gate itself is `sources.mount` — `sources.privileged` for the
1263
- * filesystem root and the protected host zones — so a caller who cannot attach
1264
- * a source gets a 403 here too.
1277
+ * `agentId` is REQUIRED: the route runs `ensureAccess` like every sibling
1278
+ * handler. On this server the gate is `sources.mount` (`sources.privileged` for
1279
+ * the filesystem root and the protected host zones); on a device it is the
1280
+ * device's owner with `device.own` — anyone else gets one fixed 403.
1265
1281
  */
1266
1282
  export async function browseDirectories(params: {
1267
1283
  path: string;
1268
1284
  agentId: string;
1269
1285
  projectId?: string;
1270
- }, client: PackageApi): Promise<{ entries: Array<{ name: string; path: string; type: 'directory' }> }> {
1286
+ device?: string;
1287
+ }, client: PackageApi): Promise<BrowseFoldersResult> {
1271
1288
  const qs: Record<string, string> = { path: params.path, agentId: params.agentId };
1272
1289
  if (params.projectId) qs.projectId = params.projectId;
1290
+ if (params.device) qs.device = params.device;
1273
1291
  return client.get('sources/browse', qs);
1274
1292
  }
1275
1293
 
@@ -1279,8 +1297,14 @@ export type SourceKindRecord = {
1279
1297
  description: string | null;
1280
1298
  icon: string | null;
1281
1299
  packageId: string;
1282
- trust: 'first-party' | 'trusted' | 'untrusted';
1300
+ trust: 'native' | 'trusted' | 'untrusted';
1283
1301
  capabilities: string[];
1302
+ /** A shell can run in this kind's sources (the host kind's planes, or a connector declaring `exec`) — the Exec policy bit applies. */
1303
+ shell?: boolean;
1304
+ /** The entry features a shell on this kind needs (an `exec` connector) — tooltip information, never a gate. */
1305
+ execRequires?: string[];
1306
+ /** The kind serves only while its runtime runs (the Machine desktop). */
1307
+ runtimeGated?: boolean;
1284
1308
  urlScheme: string;
1285
1309
  configSchema: Record<string, unknown>;
1286
1310
  defaultScope: 'project' | 'user' | 'agent';