@neuralis/brain-core 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -35
- package/app/README.md +1 -1
- package/app/cards/DiffView.tsx +9 -7
- package/app/cards/README.md +1 -1
- package/app/cards/diffLine.tsx +2 -4
- package/app/filesystem/README.md +4 -4
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +137 -123
- package/app/filesystem/domain/types.ts +36 -5
- package/app/filesystem/hooks/README.md +5 -5
- package/app/filesystem/hooks/useDiscoverableSources.ts +4 -2
- package/app/filesystem/hooks/useFilesystemSSE.ts +31 -23
- package/app/filesystem/hooks/useGitChangesInventory.ts +4 -2
- package/app/filesystem/host/README.md +3 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +22 -9
- package/app/filesystem/store/README.md +11 -7
- package/app/filesystem/store/filesystemStore.ts +454 -162
- package/app/filesystem/store/gitSlice.ts +6 -6
- package/app/filesystem/store/listSlice.ts +10 -6
- package/app/filesystem/store/searchSlice.ts +27 -4
- package/app/filesystem/ui/FileEditor.tsx +48 -35
- package/app/filesystem/ui/FileTree.tsx +10 -9
- package/app/filesystem/ui/FileTreeSearch.tsx +29 -2
- package/app/filesystem/ui/GitCommitChangesView.tsx +5 -3
- package/app/filesystem/ui/GitCommitGraph.tsx +6 -5
- package/app/filesystem/ui/GitControlPanel.tsx +14 -13
- package/app/filesystem/ui/GitDiffView.tsx +2 -1
- package/app/filesystem/ui/GitFileDiff.tsx +7 -6
- package/app/filesystem/ui/PendingChangesPanel.tsx +88 -28
- package/app/filesystem/ui/PendingReviewDialog.tsx +52 -24
- package/app/filesystem/ui/README.md +8 -7
- package/app/filesystem/ui/ReembedDialog.tsx +22 -9
- package/app/filesystem/ui/SourceDeleteConfirm.tsx +1 -1
- package/app/filesystem/ui/SourcesPanel.tsx +135 -42
- package/app/filesystem/ui/VectorHealthPanel.tsx +20 -4
- package/app/filesystem/ui/pendingChangesModel.ts +9 -5
- package/dist/app/chunks/{astro-LRAFGXNX.js → astro-B2J4AS2D.js} +1 -1
- package/dist/app/chunks/bash-KY6SAQTH.js +1 -0
- package/dist/app/chunks/c-OBOPYVZ7.js +1 -0
- package/dist/app/chunks/{chunk-5H7ZCOD3.js → chunk-2X3GNKJ3.js} +1 -1
- package/dist/app/chunks/{chunk-XIFGTNRM.js → chunk-JPJIVRFI.js} +1 -1
- package/dist/app/chunks/{chunk-RH3QY5LF.js → chunk-LEFIFWND.js} +1 -1
- package/dist/app/chunks/{chunk-G4RZ3UNX.js → chunk-N6RXX3PH.js} +1 -1
- package/dist/app/chunks/{chunk-QXXJ73P5.js → chunk-PZAW4HPB.js} +1 -1
- package/dist/app/chunks/{chunk-3QLE7Y4L.js → chunk-ZBYWWTO6.js} +1 -1
- package/dist/app/chunks/cpp-TVWVOTEQ.js +1 -0
- package/dist/app/chunks/css-MANZBYA4.js +1 -0
- package/dist/app/chunks/{elixir-M36XROA6.js → elixir-S725Z7EH.js} +1 -1
- package/dist/app/chunks/{elm-7BG6QAIK.js → elm-5WUUSTNB.js} +1 -1
- package/dist/app/chunks/graphql-UGOPR3WU.js +1 -0
- package/dist/app/chunks/html-F6FWTBSG.js +1 -0
- package/dist/app/chunks/java-ANIVV5H5.js +1 -0
- package/dist/app/chunks/javascript-IH374YWG.js +1 -0
- package/dist/app/chunks/json-WGGVVXSB.js +1 -0
- package/dist/app/chunks/jsx-OTXJQD32.js +1 -0
- package/dist/app/chunks/{latex-ZWK7SXB6.js → latex-QCZNITGN.js} +1 -1
- package/dist/app/chunks/lua-4CRZ64PU.js +1 -0
- package/dist/app/chunks/{nginx-2L6VRV3A.js → nginx-V4CYS7WR.js} +1 -1
- package/dist/app/chunks/{perl-PBUAXXK6.js → perl-BHTX7F5B.js} +1 -1
- package/dist/app/chunks/{php-QJX57PA6.js → php-AYGJ5WYZ.js} +1 -1
- package/dist/app/chunks/r-4YFCFA4B.js +1 -0
- package/dist/app/chunks/{ruby-RLOWMVPO.js → ruby-42NMJWAL.js} +1 -1
- package/dist/app/chunks/{scss-B5CAI3HR.js → scss-XD7VU2CU.js} +1 -1
- package/dist/app/chunks/sql-B55MH6JJ.js +1 -0
- package/dist/app/chunks/{svelte-GT3D4C44.js → svelte-WR3QPIJU.js} +1 -1
- package/dist/app/chunks/tsx-S73GAE2L.js +1 -0
- package/dist/app/chunks/typescript-NEWOAB5Z.js +1 -0
- package/dist/app/chunks/{vue-JOW62MHD.js → vue-CCDS4VZK.js} +1 -1
- package/dist/app/chunks/xml-UXKAGLDR.js +1 -0
- package/dist/app/chunks/yaml-UMWKDTK7.js +1 -0
- package/dist/app/host.js +46 -46
- package/dist/scripts/embeddingLockV3.d.ts +30 -0
- package/dist/scripts/embeddingLockV3.d.ts.map +1 -0
- package/dist/scripts/embeddingLockV3.js +100 -0
- package/dist/scripts/embeddingLockV3.js.map +1 -0
- package/dist/scripts/vectorProofV1.d.ts +90 -0
- package/dist/scripts/vectorProofV1.d.ts.map +1 -0
- package/dist/scripts/vectorProofV1.js +220 -0
- package/dist/scripts/vectorProofV1.js.map +1 -0
- package/dist/src/config/env.d.ts +21 -0
- package/dist/src/config/env.d.ts.map +1 -1
- package/dist/src/config/env.js +55 -0
- package/dist/src/config/env.js.map +1 -1
- package/dist/src/connectors/AppSourceConfigStore.d.ts +12 -2
- package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
- package/dist/src/connectors/AppSourceConfigStore.js +48 -40
- package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts +11 -14
- package/dist/src/connectors/LocalDiskConnector.d.ts.map +1 -1
- package/dist/src/connectors/LocalDiskConnector.js +87 -107
- package/dist/src/connectors/LocalDiskConnector.js.map +1 -1
- package/dist/src/connectors/sourceConfigMutation.d.ts +11 -1
- package/dist/src/connectors/sourceConfigMutation.d.ts.map +1 -1
- package/dist/src/connectors/sourceConfigMutation.js +36 -3
- package/dist/src/connectors/sourceConfigMutation.js.map +1 -1
- package/dist/src/domain/chunker.js +8 -3
- package/dist/src/domain/chunker.js.map +1 -1
- package/dist/src/domain/healthTypes.d.ts +11 -0
- package/dist/src/domain/healthTypes.d.ts.map +1 -1
- package/dist/src/domain/mime.d.ts +1 -0
- package/dist/src/domain/mime.d.ts.map +1 -1
- package/dist/src/domain/mime.js +48 -0
- package/dist/src/domain/mime.js.map +1 -1
- package/dist/src/domain/pendingChange.d.ts +7 -0
- package/dist/src/domain/pendingChange.d.ts.map +1 -1
- package/dist/src/domain/pendingChange.js +11 -0
- package/dist/src/domain/pendingChange.js.map +1 -1
- package/dist/src/domain/sourceConfig.d.ts +7 -14
- package/dist/src/domain/sourceConfig.d.ts.map +1 -1
- package/dist/src/domain/sourceConfig.js +1 -13
- package/dist/src/domain/sourceConfig.js.map +1 -1
- package/dist/src/domain/sourcePackages.d.ts +11 -2
- package/dist/src/domain/sourcePackages.d.ts.map +1 -1
- package/dist/src/domain/syncTrigger.d.ts +2 -3
- package/dist/src/domain/syncTrigger.d.ts.map +1 -1
- package/dist/src/domain/syncTrigger.js +2 -3
- package/dist/src/domain/syncTrigger.js.map +1 -1
- package/dist/src/domain/syncTypes.d.ts +21 -4
- package/dist/src/domain/syncTypes.d.ts.map +1 -1
- package/dist/src/domain/syncTypes.js.map +1 -1
- package/dist/src/domain/text.d.ts +3 -0
- package/dist/src/domain/text.d.ts.map +1 -1
- package/dist/src/domain/text.js +5 -0
- package/dist/src/domain/text.js.map +1 -1
- package/dist/src/domain/types.d.ts +9 -0
- package/dist/src/domain/types.d.ts.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.js +11 -5
- package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/OllamaEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/OllamaEmbedder.js +13 -6
- package/dist/src/embeddings/clients/OllamaEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.js +12 -9
- package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.js +12 -5
- package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js +12 -5
- package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
- package/dist/src/embeddings/factory.d.ts +2 -0
- package/dist/src/embeddings/factory.d.ts.map +1 -1
- package/dist/src/embeddings/factory.js +23 -3
- package/dist/src/embeddings/factory.js.map +1 -1
- package/dist/src/embeddings/lock.d.ts +22 -11
- package/dist/src/embeddings/lock.d.ts.map +1 -1
- package/dist/src/embeddings/lock.js +47 -29
- package/dist/src/embeddings/lock.js.map +1 -1
- package/dist/src/embeddings/retry.d.ts +5 -0
- package/dist/src/embeddings/retry.d.ts.map +1 -1
- package/dist/src/embeddings/retry.js +35 -0
- package/dist/src/embeddings/retry.js.map +1 -1
- package/dist/src/events/pending.d.ts +56 -0
- package/dist/src/events/pending.d.ts.map +1 -0
- package/dist/src/events/pending.js +108 -0
- package/dist/src/events/pending.js.map +1 -0
- package/dist/src/events/source.d.ts +60 -14
- package/dist/src/events/source.d.ts.map +1 -1
- package/dist/src/events/source.js +149 -31
- package/dist/src/events/source.js.map +1 -1
- package/dist/src/events/vector.d.ts +33 -0
- package/dist/src/events/vector.d.ts.map +1 -0
- package/dist/src/events/vector.js +57 -0
- package/dist/src/events/vector.js.map +1 -0
- package/dist/src/git/GitRunner.d.ts.map +1 -1
- package/dist/src/git/GitRunner.js +11 -8
- package/dist/src/git/GitRunner.js.map +1 -1
- package/dist/src/git/sourceGitControl.d.ts.map +1 -1
- package/dist/src/git/sourceGitControl.js +2 -1
- package/dist/src/git/sourceGitControl.js.map +1 -1
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle.d.ts +20 -4
- package/dist/src/lifecycle.d.ts.map +1 -1
- package/dist/src/lifecycle.js +5 -1
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/routes/health.js +6 -1
- package/dist/src/routes/health.js.map +1 -1
- package/dist/src/routes/pending.d.ts.map +1 -1
- package/dist/src/routes/pending.js +2 -1
- package/dist/src/routes/pending.js.map +1 -1
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +60 -11
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/services/BridgeService.d.ts +2 -2
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +11 -2
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +3 -1
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +3 -1
- package/dist/src/services/FilesystemService.js.map +1 -1
- package/dist/src/services/FilesystemUiService.js +1 -1
- package/dist/src/services/FilesystemUiService.js.map +1 -1
- package/dist/src/services/SyncScheduler.d.ts.map +1 -1
- package/dist/src/services/SyncScheduler.js +0 -14
- package/dist/src/services/SyncScheduler.js.map +1 -1
- package/dist/src/services/VectorHealthService.d.ts.map +1 -1
- package/dist/src/services/VectorHealthService.js +2 -46
- package/dist/src/services/VectorHealthService.js.map +1 -1
- package/dist/src/services/baselineRepair.d.ts +2 -2
- package/dist/src/services/baselineRepair.js +2 -2
- package/dist/src/services/declaredSources.d.ts +8 -1
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +19 -19
- package/dist/src/services/declaredSources.js.map +1 -1
- package/dist/src/store/dataFormats.d.ts +14 -3
- package/dist/src/store/dataFormats.d.ts.map +1 -1
- package/dist/src/store/dataFormats.js +15 -5
- package/dist/src/store/dataFormats.js.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.js +78 -243
- package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
- package/dist/src/sync/SyncJobQueue.d.ts +33 -4
- package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
- package/dist/src/sync/SyncJobQueue.js +179 -19
- package/dist/src/sync/SyncJobQueue.js.map +1 -1
- package/dist/src/sync/moveDetector.d.ts +2 -1
- package/dist/src/sync/moveDetector.d.ts.map +1 -1
- package/dist/src/sync/moveDetector.js +2 -1
- package/dist/src/sync/moveDetector.js.map +1 -1
- package/dist/src/tools/fs_search.d.ts.map +1 -1
- package/dist/src/tools/fs_search.js +8 -0
- package/dist/src/tools/fs_search.js.map +1 -1
- package/dist/src/tools/fs_write.js +3 -1
- package/dist/src/tools/fs_write.js.map +1 -1
- package/dist/src/usecases/chunkUpsert.d.ts +4 -0
- package/dist/src/usecases/chunkUpsert.d.ts.map +1 -1
- package/dist/src/usecases/chunkUpsert.js +103 -60
- package/dist/src/usecases/chunkUpsert.js.map +1 -1
- package/dist/src/usecases/embeddingBacklog.d.ts +31 -0
- package/dist/src/usecases/embeddingBacklog.d.ts.map +1 -0
- package/dist/src/usecases/embeddingBacklog.js +244 -0
- package/dist/src/usecases/embeddingBacklog.js.map +1 -0
- package/dist/src/usecases/listSourcePackages.js +17 -1
- package/dist/src/usecases/listSourcePackages.js.map +1 -1
- package/dist/src/usecases/mediaLane.d.ts +11 -4
- package/dist/src/usecases/mediaLane.d.ts.map +1 -1
- package/dist/src/usecases/mediaLane.js +67 -30
- package/dist/src/usecases/mediaLane.js.map +1 -1
- package/dist/src/usecases/pendingReview.d.ts.map +1 -1
- package/dist/src/usecases/pendingReview.js +24 -7
- package/dist/src/usecases/pendingReview.js.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.d.ts +2 -0
- package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.js +3 -2
- package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
- package/dist/src/usecases/readFiles.d.ts.map +1 -1
- package/dist/src/usecases/readFiles.js +33 -6
- package/dist/src/usecases/readFiles.js.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +31 -0
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -0
- package/dist/src/usecases/reconcileSourcePackageRoots.js +114 -0
- package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -0
- package/dist/src/usecases/searchFiles.d.ts.map +1 -1
- package/dist/src/usecases/searchFiles.js +62 -64
- package/dist/src/usecases/searchFiles.js.map +1 -1
- package/dist/src/usecases/syncIndexedFile.d.ts +42 -0
- package/dist/src/usecases/syncIndexedFile.d.ts.map +1 -0
- package/dist/src/usecases/syncIndexedFile.js +242 -0
- package/dist/src/usecases/syncIndexedFile.js.map +1 -0
- package/dist/src/usecases/syncPointWrites.d.ts +5 -0
- package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
- package/dist/src/usecases/syncPointWrites.js +32 -5
- package/dist/src/usecases/syncPointWrites.js.map +1 -1
- package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
- package/dist/src/usecases/uploadBinary.js +36 -49
- package/dist/src/usecases/uploadBinary.js.map +1 -1
- package/dist/src/usecases/vector/endpointGuard.js +2 -1
- package/dist/src/usecases/vector/endpointGuard.js.map +1 -1
- package/dist/src/usecases/vector/estimate.d.ts +7 -4
- package/dist/src/usecases/vector/estimate.d.ts.map +1 -1
- package/dist/src/usecases/vector/estimate.js +8 -5
- package/dist/src/usecases/vector/estimate.js.map +1 -1
- package/dist/src/usecases/vector/rebuildGeneration.d.ts +29 -5
- package/dist/src/usecases/vector/rebuildGeneration.d.ts.map +1 -1
- package/dist/src/usecases/vector/rebuildGeneration.js +224 -82
- package/dist/src/usecases/vector/rebuildGeneration.js.map +1 -1
- package/dist/src/usecases/vectorMaintenance.d.ts +46 -4
- package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
- package/dist/src/usecases/vectorMaintenance.js +190 -11
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/commit.d.ts +6 -8
- package/dist/src/usecases/write/commit.d.ts.map +1 -1
- package/dist/src/usecases/write/commit.js +96 -112
- package/dist/src/usecases/write/commit.js.map +1 -1
- package/dist/src/usecases/write/content.js +1 -2
- package/dist/src/usecases/write/content.js.map +1 -1
- package/dist/src/usecases/write/ports.d.ts +1 -0
- package/dist/src/usecases/write/ports.d.ts.map +1 -1
- package/dist/src/vector/FilesystemVectorIndex.d.ts +34 -3
- package/dist/src/vector/FilesystemVectorIndex.d.ts.map +1 -1
- package/dist/src/vector/FilesystemVectorIndex.js +254 -20
- package/dist/src/vector/FilesystemVectorIndex.js.map +1 -1
- package/dist/src/vector/InMemoryVectorIndex.d.ts +9 -2
- package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
- package/dist/src/vector/InMemoryVectorIndex.js +25 -6
- package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
- package/dist/src/vector/QdrantVectorIndex.d.ts +35 -9
- package/dist/src/vector/QdrantVectorIndex.d.ts.map +1 -1
- package/dist/src/vector/QdrantVectorIndex.js +209 -90
- package/dist/src/vector/QdrantVectorIndex.js.map +1 -1
- package/dist/src/vector/qdrantFilter.d.ts +3 -1
- package/dist/src/vector/qdrantFilter.d.ts.map +1 -1
- package/dist/src/vector/qdrantFilter.js +2 -0
- package/dist/src/vector/qdrantFilter.js.map +1 -1
- package/dist/src/vector/types.d.ts +81 -0
- package/dist/src/vector/types.d.ts.map +1 -1
- package/dist/src/vector/types.js +2 -0
- package/dist/src/vector/types.js.map +1 -1
- package/dist/src/wiring.d.ts +6 -0
- package/dist/src/wiring.d.ts.map +1 -1
- package/dist/src/wiring.js +62 -3
- package/dist/src/wiring.js.map +1 -1
- package/dist/tools/fs_search.json +2 -2
- package/docs/drive-overview.md +2 -2
- package/instructions/brain-model.md +1 -1
- package/package.json +1000 -11
- package/rules/brain-rules.md +1 -1
- package/skills/connect-business-data/SKILL.md +6 -4
- package/skills/create-package/SKILL.md +14 -12
- package/skills/create-package/references/config-and-credentials.md +3 -3
- package/skills/create-package/references/connectors-and-uri-policies.md +5 -3
- package/skills/create-package/references/manifest-and-discovery.md +5 -4
- package/skills/create-package/references/runtime-and-build.md +9 -9
- package/skills/create-package/references/skills-and-context.md +3 -3
- package/skills/create-package/references/surfaces-and-design.md +9 -6
- package/skills/create-package/references/team-and-workflows.md +2 -2
- package/skills/create-package/references/test-and-ship.md +15 -15
- package/skills/create-package/references/tools-and-schemas.md +1 -1
- package/skills/create-package/scripts/check-package.sh +50 -4
- package/skills/create-package/scripts/verify-loaded.sh +16 -16
- package/skills/manage-files/SKILL.md +2 -2
- package/skills/manage-sources/SKILL.md +16 -15
- package/skills/manage-sources/scripts/reembed.sh +9 -3
- package/skills/sync/SKILL.md +4 -4
- package/tools/fs_search.json +2 -2
- package/app/filesystem/hooks/useContainerWidth.ts +0 -39
- package/dist/app/chunks/bash-M45GHPBQ.js +0 -1
- package/dist/app/chunks/c-X3REOAWD.js +0 -1
- package/dist/app/chunks/cpp-VOWI743W.js +0 -1
- package/dist/app/chunks/css-2ZVPHE25.js +0 -1
- package/dist/app/chunks/graphql-7V2NP54C.js +0 -1
- package/dist/app/chunks/html-UF3LFLNA.js +0 -1
- package/dist/app/chunks/java-PNTAU4L3.js +0 -1
- package/dist/app/chunks/javascript-4JJTQGEL.js +0 -1
- package/dist/app/chunks/json-VLQ6D3JH.js +0 -1
- package/dist/app/chunks/jsx-KQKSLAAH.js +0 -1
- package/dist/app/chunks/lua-UFPYUJ2O.js +0 -1
- package/dist/app/chunks/r-QEZB45SR.js +0 -1
- package/dist/app/chunks/sql-DTZHNUZP.js +0 -1
- package/dist/app/chunks/tsx-WPR2WRZD.js +0 -1
- package/dist/app/chunks/typescript-LWO362OA.js +0 -1
- package/dist/app/chunks/xml-A5H72RTS.js +0 -1
- package/dist/app/chunks/yaml-MXHSIJCI.js +0 -1
- package/dist/src/services/purgePendingNodes.d.ts +0 -32
- package/dist/src/services/purgePendingNodes.d.ts.map +0 -1
- package/dist/src/services/purgePendingNodes.js +0 -61
- package/dist/src/services/purgePendingNodes.js.map +0 -1
- /package/dist/app/chunks/{chunk-737Y24HS.js → chunk-3JASFUPF.js} +0 -0
- /package/dist/app/chunks/{chunk-DKD2L57N.js → chunk-6EYLOQ4F.js} +0 -0
- /package/dist/app/chunks/{chunk-KYH5EHHY.js → chunk-7YRZ5LOQ.js} +0 -0
- /package/dist/app/chunks/{chunk-6TC3DVWV.js → chunk-C7LU4RID.js} +0 -0
- /package/dist/app/chunks/{chunk-TY33BMKR.js → chunk-CD52YDZP.js} +0 -0
- /package/dist/app/chunks/{chunk-OLEIKJ3X.js → chunk-DCJKTCJ5.js} +0 -0
- /package/dist/app/chunks/{chunk-QLBZ4JR2.js → chunk-EXIU2KBV.js} +0 -0
- /package/dist/app/chunks/{chunk-SFUNBBEA.js → chunk-FDP4LO4P.js} +0 -0
- /package/dist/app/chunks/{chunk-BGRVFATT.js → chunk-HNETUS7U.js} +0 -0
- /package/dist/app/chunks/{chunk-4QNMHAIR.js → chunk-LBFZJEDR.js} +0 -0
- /package/dist/app/chunks/{chunk-TRRUHDPN.js → chunk-ULRXL5VM.js} +0 -0
- /package/dist/app/chunks/{chunk-S6B4WWIF.js → chunk-XJK47SMJ.js} +0 -0
- /package/dist/app/chunks/{chunk-DZ5C5FKI.js → chunk-YYAAIAU2.js} +0 -0
- /package/dist/app/chunks/{clojure-OC6KPSJO.js → clojure-4QCN4GHG.js} +0 -0
- /package/dist/app/chunks/{core-KGE7TQJ3.js → core-ZHTX3P2Q.js} +0 -0
- /package/dist/app/chunks/{csharp-E7VIFITL.js → csharp-HQWR4N52.js} +0 -0
- /package/dist/app/chunks/{dark-plus-BOSCZXE4.js → dark-plus-GYPKKZES.js} +0 -0
- /package/dist/app/chunks/{dart-6Y3X27SZ.js → dart-FO4TTVO2.js} +0 -0
- /package/dist/app/chunks/{diff-TR2NPA64.js → diff-SHANV4YC.js} +0 -0
- /package/dist/app/chunks/{dist-T32U2VVR.js → dist-WSDF34DM.js} +0 -0
- /package/dist/app/chunks/{docker-N33HOW54.js → docker-VX55HTDS.js} +0 -0
- /package/dist/app/chunks/{go-6DGG5UTP.js → go-5UCPEC6S.js} +0 -0
- /package/dist/app/chunks/{haskell-KK36PMMM.js → haskell-25PJ3OSG.js} +0 -0
- /package/dist/app/chunks/{ini-VHVQ5TPB.js → ini-BJIVD5MK.js} +0 -0
- /package/dist/app/chunks/{kotlin-44WS3JHE.js → kotlin-MNR6XOEQ.js} +0 -0
- /package/dist/app/chunks/{less-5WZ3KLAK.js → less-KA5JURX6.js} +0 -0
- /package/dist/app/chunks/{light-plus-HGGBC43G.js → light-plus-QXH2UUWZ.js} +0 -0
- /package/dist/app/chunks/{make-X7RXMVW7.js → make-LPD5KOCO.js} +0 -0
- /package/dist/app/chunks/{markdown-46IKUAIB.js → markdown-LMWF6MQR.js} +0 -0
- /package/dist/app/chunks/{mdx-DGLHDAO7.js → mdx-WPMNN5BS.js} +0 -0
- /package/dist/app/chunks/{nix-Q6FYQZLX.js → nix-QGWLUUMZ.js} +0 -0
- /package/dist/app/chunks/{powershell-L4KJJ6DK.js → powershell-7GSBBEFF.js} +0 -0
- /package/dist/app/chunks/{prisma-IKS4LVH2.js → prisma-FPMPQISO.js} +0 -0
- /package/dist/app/chunks/{proto-AYCDOTJO.js → proto-7KDX5Z2U.js} +0 -0
- /package/dist/app/chunks/{python-KXMCSAFQ.js → python-CFG4FTQW.js} +0 -0
- /package/dist/app/chunks/{rust-J3BC3P6V.js → rust-X7A37WU3.js} +0 -0
- /package/dist/app/chunks/{scala-AYIIHBJK.js → scala-OYHJSUG2.js} +0 -0
- /package/dist/app/chunks/{swift-QPHHCQD3.js → swift-PE7VNYW5.js} +0 -0
- /package/dist/app/chunks/{toml-UV7JK7K7.js → toml-ETQRDILP.js} +0 -0
|
@@ -373,6 +373,10 @@ export interface SourceConfigRecord {
|
|
|
373
373
|
error?: string;
|
|
374
374
|
/** Media files the run did not embed, by reason — absent when none. */
|
|
375
375
|
mediaSkipped?: { textOnlyModel: number; off: number; tooLarge: number; unavailable: number; invalid: number };
|
|
376
|
+
/** Why the run left semantic embedding owed (`disabled`, `quota`, …) — the data still landed. */
|
|
377
|
+
embeddingDegraded?: string[];
|
|
378
|
+
/** Binary files the run did not index as text — absent when none. */
|
|
379
|
+
binarySkipped?: number;
|
|
376
380
|
};
|
|
377
381
|
};
|
|
378
382
|
last_updated?: string;
|
|
@@ -385,6 +389,8 @@ export interface SourceConfigRecord {
|
|
|
385
389
|
orphan?: boolean;
|
|
386
390
|
orphan_hint?: string | null;
|
|
387
391
|
observed?: boolean;
|
|
392
|
+
/** The platform's LIVE semantic-embedding intent (`embeddingEnabled`), read per request — never a last pass's echo. */
|
|
393
|
+
embedding_enabled?: boolean;
|
|
388
394
|
/** Server-evaluated source maintenance authority. */
|
|
389
395
|
canMaintain?: boolean;
|
|
390
396
|
owner?: { userId?: string; agentId?: string };
|
|
@@ -400,7 +406,7 @@ export interface SourceConfigRecord {
|
|
|
400
406
|
/** Why `live === false` — fixed enum, mapped to a display string in the UI. */
|
|
401
407
|
live_reason?: SourceLiveReason;
|
|
402
408
|
/**
|
|
403
|
-
* R2b — local-kind source ALSO acts as a
|
|
409
|
+
* R2b — local-kind source ALSO acts as a runtime package source (additive to the
|
|
404
410
|
* default `_packages/` drop-zone). Owner/admin-effective to set; honored for
|
|
405
411
|
* project AND user/agent scope (advertise isolation enforced downstream).
|
|
406
412
|
*/
|
|
@@ -456,7 +462,7 @@ export interface UpdateSourceConfigRequest {
|
|
|
456
462
|
* the existing config, so disabling sync needs this explicit flag.
|
|
457
463
|
*/
|
|
458
464
|
clearSync?: boolean;
|
|
459
|
-
/** R2b — toggle
|
|
465
|
+
/** R2b — toggle runtime package recognition (owner/admin-effective, local-only). */
|
|
460
466
|
recognizesPackages?: boolean;
|
|
461
467
|
}
|
|
462
468
|
|
|
@@ -478,7 +484,7 @@ export interface CreateSourceRequest {
|
|
|
478
484
|
/** Per-source override of the platform `brainMaxScanFiles` scan cap. */
|
|
479
485
|
maxScanFiles?: number;
|
|
480
486
|
};
|
|
481
|
-
/** R2b — create the (local-kind) source as a
|
|
487
|
+
/** R2b — create the (local-kind) source as a runtime package source (owner/admin). */
|
|
482
488
|
recognizesPackages?: boolean;
|
|
483
489
|
}
|
|
484
490
|
|
|
@@ -490,8 +496,8 @@ export interface CreateSourceRequest {
|
|
|
490
496
|
export interface SyncJobSnapshotRecord {
|
|
491
497
|
jobId: string;
|
|
492
498
|
source: string;
|
|
493
|
-
mode: 'full' | 'targeted' | 'purge';
|
|
494
|
-
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
499
|
+
mode: 'full' | 'targeted' | 'purge' | 'embedding';
|
|
500
|
+
status: 'pending' | 'running' | 'completed' | 'paused' | 'failed' | 'cancelled';
|
|
495
501
|
userId: string;
|
|
496
502
|
projectId: string;
|
|
497
503
|
agentId: string;
|
|
@@ -508,6 +514,8 @@ export interface SyncJobSnapshotRecord {
|
|
|
508
514
|
filesSkipped: number;
|
|
509
515
|
durationMs: number;
|
|
510
516
|
error?: string;
|
|
517
|
+
/** An embedding-only pass: what it embedded, and why it paused when it did. */
|
|
518
|
+
embedding?: { audited: number; embedded: number; settled: boolean; pausedReason?: string };
|
|
511
519
|
};
|
|
512
520
|
error?: string;
|
|
513
521
|
}
|
|
@@ -626,9 +634,12 @@ export interface FileSystemState {
|
|
|
626
634
|
chunk_end?: number;
|
|
627
635
|
match_type?: 'semantic' | 'keyword' | 'hybrid';
|
|
628
636
|
score?: number;
|
|
637
|
+
embedding_status?: string;
|
|
629
638
|
}>;
|
|
630
639
|
searchLoading: boolean;
|
|
631
640
|
searchError?: string;
|
|
641
|
+
/** What the last search ran as when that differs from what was asked (`SearchEffect`). */
|
|
642
|
+
searchEffect: { requestedMode?: string; effectiveMode?: string; embeddingDegraded?: string; warning?: string };
|
|
632
643
|
/**
|
|
633
644
|
* `true` = enumerated by a successful listing and current · `false` = enumerated
|
|
634
645
|
* once, then taken off screen by a collapse (no reconcile lists a hidden folder),
|
|
@@ -688,6 +699,18 @@ export interface PersistedFilesContext {
|
|
|
688
699
|
activeFolderPath: string | null;
|
|
689
700
|
}
|
|
690
701
|
|
|
702
|
+
/**
|
|
703
|
+
* ONE Files window's reload-surviving view, keyed in the persisted record by the
|
|
704
|
+
* host's window id: the same allowlist as `PersistedFilesContext`, plus the
|
|
705
|
+
* user, project and bound agent it was written for — a projection whose identity
|
|
706
|
+
* does not match the LIVE one is ignored, never applied.
|
|
707
|
+
*/
|
|
708
|
+
export interface PersistedFilesWindow extends PersistedFilesContext {
|
|
709
|
+
userId: string;
|
|
710
|
+
projectId: string;
|
|
711
|
+
agentId: string;
|
|
712
|
+
}
|
|
713
|
+
|
|
691
714
|
// ---------------------------------------------------------------------------
|
|
692
715
|
// API response shapes (re-declared locally so the package is self-contained)
|
|
693
716
|
// ---------------------------------------------------------------------------
|
|
@@ -740,10 +763,18 @@ export type FSSearchResponse = {
|
|
|
740
763
|
chunk_end?: number;
|
|
741
764
|
match_type?: 'semantic' | 'keyword' | 'hybrid';
|
|
742
765
|
version?: FileVersionInfo;
|
|
766
|
+
/** The point's dense proof: `current`, or why it is not (`missing` / `stale` / `unknown` / …). */
|
|
767
|
+
embedding_status?: string;
|
|
743
768
|
error?: string;
|
|
744
769
|
}>;
|
|
745
770
|
count: { found: number; errors: number };
|
|
746
771
|
usage?: { vectorQueries?: number; embeddingCalls?: number };
|
|
772
|
+
/** The mode asked for, and the one that ran — a semantic request runs as `filter` when embedding is unavailable. */
|
|
773
|
+
requested_mode?: 'semantic' | 'filter' | 'grep';
|
|
774
|
+
effective_mode?: 'semantic' | 'filter' | 'grep';
|
|
775
|
+
/** Why semantic search fell back (`disabled`, `quota`, `credentials`, …). */
|
|
776
|
+
embedding_degraded?: string;
|
|
777
|
+
warning?: string;
|
|
747
778
|
};
|
|
748
779
|
|
|
749
780
|
export type FSUploadResponse = {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# brain-core/app/filesystem/hooks
|
|
2
2
|
|
|
3
|
-
React hooks used by the Files widget:
|
|
3
|
+
React hooks used by the Files widget: viewport diff loading and the filesystem SSE consumer. The container-width hook is a shared kernel client export.
|
|
4
4
|
|
|
5
5
|
## Responsibility
|
|
6
6
|
|
|
7
|
-
- `
|
|
7
|
+
- `GitFileDiff` and `FileEditor` import `useContainerWidth` from `@neuralis/package-system/client`. There is no package-local responsive hook.
|
|
8
8
|
- `useFilesystemSSE(agentId, projectId)` — subscribe to the brain-core `filesystem` channel and dispatch the right store updates. Event types: `filesystem:created`/`updated`/`deleted`/`reverted` (per-file activity), `sync:completed` (source-scoped backstop — skipped when every change count, `filesMoved` and separate `chunksRemoved` included, says zero; a missing count is unknown and still re-lists, and a `purge` job never skips, because its removal count comes from a `deleteByFilter` and proves nothing), a `changeType:'badge'` git-state signal, and a `changeType:'structure'` + `external:true` EXTERNAL connector file change (brisk-kingfisher).
|
|
9
9
|
|
|
10
10
|
- `useVisibleDiffCards(root)` — one viewport observer (cards within ~2 screens count as visible) and `CONTENT_LANES` (3) content lanes per Changes/history scroller, plus an LRU answer cache (200 entries / ~8 MB estimated) keyed by the caller — record or file, its `updatedAt`/mode and the source revision — so a card re-entering the viewport sends nothing and any change is a new key; `fresh` replaces an answer after a mutation. Pending previews defer to the observer; a git diff in the virtualized Changes list reads on mount (the virtualizer — overscan 4 — is that list's ONE mount bound) and paints its first frame from the synchronous `peek(key, family)` (the exact key, else the target's newest cached revision, shown as stale until its own answer lands — staging stays disabled meanwhile). The caller validates its current revision before queued work runs.
|
|
@@ -14,7 +14,7 @@ React hooks used by the Files widget: container-width detection, viewport diff l
|
|
|
14
14
|
|
|
15
15
|
## Public Surface
|
|
16
16
|
|
|
17
|
-
-
|
|
17
|
+
- Responsive hook contracts live in the kernel client export map; existing Files callers retain their width thresholds and hysteresis.
|
|
18
18
|
- `useFilesystemSSE(agentId: string | undefined, projectId: string | null): void` — side-effecting hook; returns nothing.
|
|
19
19
|
- `useDiscoverableSources(params): readonly DiscoverableSourceSuggestion[]` — empty until the first response lands, and empty again immediately on a context change.
|
|
20
20
|
- **The "already fetched" latch is a REF, never state.** As state it is a dependency of the effect it guards: committing it re-runs the effect, the first instance's cleanup cancels the in-flight request, and the second returns at the guard and never re-fetches. Live symptom: the request 200s with real data, there is no console error and no banner, and the dock renders nothing — invisible to any presentational test. Pinned by `test/ui/useDiscoverableSources.test.tsx` (5 of its 8 cases fail on the old shape).
|
|
@@ -25,7 +25,7 @@ React hooks used by the Files widget: container-width detection, viewport diff l
|
|
|
25
25
|
## How it plugs into the package-system kernel
|
|
26
26
|
|
|
27
27
|
- `useFilesystemSSE` subscribes through `WorkspaceHostPort.subscribeRealtime({ channel: 'filesystem', agentId, projectId, onEvent })` (via `getFilesystemHostImperative().subscribeFilesystemEvents`) — the shared, multiplexed `/api/events` hub owned by the host, NOT a per-widget `EventSource` (the SSE-consolidation invariant: never open a per-widget connection — see `docs/architect/realtime-transport.md`). The hub owns reconnect/backoff; the host route is gated by `drive.read`.
|
|
28
|
-
- Handlers dispatch into `useFilesystemStore`
|
|
28
|
+
- Handlers dispatch into the actions of the window's OWN store (`useFilesystemStore` / `useFilesystemStoreApi`) per event; a frame the hub delivers after the scope changed (agent, project or store) is dropped, never applied to the next scope: `upsertFileNodeFromEvent` (per-file), `reconcileSourceFolders` (`sync:completed` and the `external:true` branch — both EARLY short-circuits BEFORE the `uri` guard, because the payload `uri` is the bare `<source>://` and would otherwise fabricate a phantom root node), `reconcileFolders` (a `changeType:'structure'` create/update/delete/revert — the affected PARENTS, never a global refresh), `ensureFolderLoaded({force})` + `refresh` (badge → `FileNode.git` subtree re-list + `SourceInfo.git` summaries), and `setSourceDirty` for a source that is not hydrated. An external change and a changing `sync:completed` share ONE per-source reconcile schedule: a trailing debounce (`RECONCILE_QUIET_MS` = 2 s — every event restarts it) bounded by a max-wait (`RECONCILE_MAX_WAIT_MS` = 6 s from the window's first event), so one agent turn's burst costs one or two passes of the visible folders instead of one per event, and a continuous burst still converges. The quiet window is sized on a measured turn, whose events arrive up to ~1.8 s apart. Pending passes are dropped on unmount and on an agent/project change. The badge branch keeps its own 250 ms timers (it does different work; a shared key would let one cancel the other). Badge/status and reconnect callbacks publish source revisions for Changes; timers are cancelled on context changes, and an epoch check prevents an old reconnect response from publishing into the new context.
|
|
29
29
|
- **The Changes badge re-count.** `pending:changed` (content-free, project-wide, derived server-side) never reaches the file handlers: it — plus a window focus / tab return and a successful reconnect probe — feeds ONE trailing schedule (`PENDING_COUNT_QUIET_MS` 1 000 ms, `PENDING_COUNT_MAX_WAIT_MS` 5 000 ms) that calls `refreshPendingCount`; a hidden tab skips the call, the mount counts once, a context change drops the timer. No poll, no timer while idle.
|
|
30
30
|
- **Reconnect recovery.** The filesystem channel is strictly live fan-out — the package route has no `Last-Event-ID`, no backfill and no cursor, and the host hub replays a snapshot only for `presence`. Everything emitted while the connection was down is lost. The hook therefore subscribes with `onConnChange` and, on a settled liveness flip, PROBES with the cheap summaries call and then re-converges every hydrated source. The hub's flip carries no direction, which is why the probe exists instead of a guess. This is what makes the toolbar Refresh a rare, deliberate action rather than routine repair.
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ React hooks used by the Files widget: container-width detection, viewport diff l
|
|
|
35
35
|
|
|
36
36
|
## Gotchas
|
|
37
37
|
|
|
38
|
-
-
|
|
38
|
+
- The shared width hook observes `clientWidth`. Preserve hysteresis when CSS changes layout in response to its result; do not recreate the deleted `useElementFits` hook without a consumer.
|
|
39
39
|
- `useFilesystemSSE` handlers must be defensive — a throw would break the shared hub subscription for every channel, not just this widget.
|
|
40
40
|
- The `external:true` branch reconciles the source's VISIBLE folders (adds new files; strikes through / prunes removed ones). It must stay an EARLY `return` short-circuit before `upsertFileNodeFromEvent` (the `<source>://` uri is truthy → a phantom node otherwise), exactly like the badge branch.
|
|
41
41
|
- **Anything guarded by `!uri` below the `if (!uri) return` is DEAD.** Three structural branches sat there and could never run; the intent (re-converge after a move/delete) is delivered now by the `changeType === 'structure'` parent reconcile. Do not reintroduce a `!uri` condition below that guard.
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
import { useEffect, useRef, useState } from 'react';
|
|
42
42
|
import { listDiscoverableSources, type DiscoverableSourceSuggestion } from '../api';
|
|
43
|
+
import { useFilesystemStore } from '../store/filesystemStore';
|
|
43
44
|
|
|
44
45
|
export function useDiscoverableSources(params: {
|
|
45
46
|
agentId: string | undefined;
|
|
@@ -47,6 +48,7 @@ export function useDiscoverableSources(params: {
|
|
|
47
48
|
/** Only fetch once the caller could actually use the result. */
|
|
48
49
|
enabled: boolean;
|
|
49
50
|
}): readonly DiscoverableSourceSuggestion[] {
|
|
51
|
+
const client = useFilesystemStore((s) => s.client);
|
|
50
52
|
const { agentId, projectId, enabled } = params;
|
|
51
53
|
const [suggestions, setSuggestions] = useState<readonly DiscoverableSourceSuggestion[]>([]);
|
|
52
54
|
|
|
@@ -69,13 +71,13 @@ export function useDiscoverableSources(params: {
|
|
|
69
71
|
useEffect(() => {
|
|
70
72
|
if (!agentId || !enabled || fetchedRef.current) return;
|
|
71
73
|
fetchedRef.current = true;
|
|
72
|
-
listDiscoverableSources({ agentId, ...(projectId ? { projectId } : {}) })
|
|
74
|
+
listDiscoverableSources({ agentId, ...(projectId ? { projectId } : {}) }, client)
|
|
73
75
|
.then((res) => { if (aliveRef.current) setSuggestions(res.suggestions ?? []); })
|
|
74
76
|
// Fail-soft: no dock, no banner. It is an affordance, not a result. The
|
|
75
77
|
// latch stays set, so a transient failure does not retry-loop; a context
|
|
76
78
|
// change (the effect above) is what re-arms it.
|
|
77
79
|
.catch(() => { if (aliveRef.current) setSuggestions([]); });
|
|
78
|
-
}, [agentId, projectId, enabled]);
|
|
80
|
+
}, [agentId, projectId, enabled, client]);
|
|
79
81
|
|
|
80
82
|
return suggestions;
|
|
81
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import { useFilesystemStore } from '../store/filesystemStore';
|
|
2
|
+
import { useFilesystemStore, useFilesystemStoreApi } from '../store/filesystemStore';
|
|
3
3
|
import { getFilesystemHostImperative } from '../FilesystemHost';
|
|
4
4
|
import { uriAncestors, sourceFromUri, folderUri } from '../store/helpers';
|
|
5
5
|
|
|
@@ -96,6 +96,9 @@ export function syncChangedNothing(payload: Record<string, unknown>): boolean {
|
|
|
96
96
|
* hook just filters the channel to its own agent + the known event types.
|
|
97
97
|
*/
|
|
98
98
|
export function useFilesystemSSE(agentId: string | undefined, projectId: string | null) {
|
|
99
|
+
// THIS window's store: every handler below reads and writes it, never another
|
|
100
|
+
// window's — a new store (another identity) is a new subscription.
|
|
101
|
+
const storeApi = useFilesystemStoreApi();
|
|
99
102
|
const refreshTree = useFilesystemStore((s) => s.refreshTree);
|
|
100
103
|
const loadSourceSummaries = useFilesystemStore((s) => s.refresh);
|
|
101
104
|
const ensureFolderLoaded = useFilesystemStore((s) => s.ensureFolderLoaded);
|
|
@@ -135,7 +138,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
135
138
|
for (const t of status.values()) clearTimeout(t);
|
|
136
139
|
status.clear();
|
|
137
140
|
};
|
|
138
|
-
}, [agentId, projectId]);
|
|
141
|
+
}, [agentId, projectId, storeApi]);
|
|
139
142
|
useEffect(() => {
|
|
140
143
|
const reconcile = reconcileTimersRef.current;
|
|
141
144
|
return () => {
|
|
@@ -143,7 +146,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
143
146
|
for (const entry of reconcile.values()) clearTimeout(entry.timer);
|
|
144
147
|
reconcile.clear();
|
|
145
148
|
};
|
|
146
|
-
}, [agentId, projectId]);
|
|
149
|
+
}, [agentId, projectId, storeApi]);
|
|
147
150
|
|
|
148
151
|
// ONE pending re-count schedule per context (see `PENDING_COUNT_QUIET_MS`).
|
|
149
152
|
const countTimerRef = useRef<{ timer: ReturnType<typeof setTimeout>; deadline: number } | null>(null);
|
|
@@ -198,7 +201,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
198
201
|
// `gitStatusSeq` guard drops any out-of-order response.
|
|
199
202
|
const scheduleGitStatusRefresh = (source: string): void => {
|
|
200
203
|
if (!agentId || !source) return;
|
|
201
|
-
const active =
|
|
204
|
+
const active = storeApi.getState().activeSource;
|
|
202
205
|
const focused =
|
|
203
206
|
typeof active === 'string' && active
|
|
204
207
|
? active.includes('://')
|
|
@@ -212,7 +215,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
212
215
|
source,
|
|
213
216
|
setTimeout(() => {
|
|
214
217
|
timers.delete(source);
|
|
215
|
-
|
|
218
|
+
storeApi.setState(s => ({ sourceChangeRevision: { ...s.sourceChangeRevision, [source]: (s.sourceChangeRevision[source] ?? 0) + 1 } }));
|
|
216
219
|
if (focused === source) void loadGitStatus({ agentId, projectId, source });
|
|
217
220
|
}, BADGE_REFRESH_DEBOUNCE_MS),
|
|
218
221
|
);
|
|
@@ -254,7 +257,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
254
257
|
// folder of the source and applies the absence semantics. On a source that
|
|
255
258
|
// is not hydrated yet it is a no-op by design; mark it dirty instead so the
|
|
256
259
|
// header can tell the user a refresh is worth it.
|
|
257
|
-
if (
|
|
260
|
+
if (storeApi.getState().loadedFolders?.[`${source}://`]) {
|
|
258
261
|
scheduleSourceReconcile(source);
|
|
259
262
|
} else {
|
|
260
263
|
setSourceDirty(source, true);
|
|
@@ -287,9 +290,9 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
287
290
|
// (recursive:false re-list) — acceptable v1.
|
|
288
291
|
if (payload.changeType === 'badge') {
|
|
289
292
|
if (!agentId) return;
|
|
290
|
-
// A
|
|
291
|
-
//
|
|
292
|
-
const storeAgent =
|
|
293
|
+
// A store whose context is not this hook's agent (mid-switch) must not have
|
|
294
|
+
// its context thrashed by a source-scoped badge.
|
|
295
|
+
const storeAgent = storeApi.getState().activeAgentId;
|
|
293
296
|
if (storeAgent && storeAgent !== agentId) return;
|
|
294
297
|
const source =
|
|
295
298
|
typeof payload.source === 'string' && payload.source
|
|
@@ -303,14 +306,14 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
303
306
|
source,
|
|
304
307
|
setTimeout(() => {
|
|
305
308
|
timers.delete(source);
|
|
306
|
-
|
|
309
|
+
storeApi.setState(s => ({ sourceChangeRevision: { ...s.sourceChangeRevision, [source]: (s.sourceChangeRevision[source] ?? 0) + 1 } }));
|
|
307
310
|
// FileNode.git for the source subtree (matches the sync:completed precedent).
|
|
308
311
|
void ensureFolderLoaded({ agentId, projectId, uri: `${source}://`, force: true });
|
|
309
312
|
// SourceInfo.git (repo-switcher, tree source header, panel branch hint).
|
|
310
313
|
// Summaries ONLY — a git badge must never trigger a per-source walk.
|
|
311
314
|
void loadSourceSummaries({ agentId, projectId });
|
|
312
315
|
// Git Control panel change-set + branch, only when this is the focused repo.
|
|
313
|
-
const active =
|
|
316
|
+
const active = storeApi.getState().activeSource;
|
|
314
317
|
const focused =
|
|
315
318
|
typeof active === 'string' && active
|
|
316
319
|
? active.includes('://')
|
|
@@ -341,9 +344,9 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
341
344
|
// shared per-source reconcile schedule, never the badge's timers.
|
|
342
345
|
if (payload.external === true) {
|
|
343
346
|
if (!agentId) return;
|
|
344
|
-
// A
|
|
345
|
-
//
|
|
346
|
-
const storeAgent =
|
|
347
|
+
// A store whose context is not this hook's agent (mid-switch) must not have
|
|
348
|
+
// its context thrashed by a source-scoped event.
|
|
349
|
+
const storeAgent = storeApi.getState().activeAgentId;
|
|
347
350
|
if (storeAgent && storeAgent !== agentId) return;
|
|
348
351
|
const source =
|
|
349
352
|
typeof payload.source === 'string' && payload.source
|
|
@@ -356,7 +359,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
356
359
|
// A source that has never been hydrated has nothing to reconcile against —
|
|
357
360
|
// an eager content list here would defeat lazy hydration. Flag it instead:
|
|
358
361
|
// the header shows the "stale" hint and the next expand/refresh converges it.
|
|
359
|
-
if (!
|
|
362
|
+
if (!storeApi.getState().loadedFolders?.[`${source}://`]) {
|
|
360
363
|
setSourceDirty(source, true);
|
|
361
364
|
return;
|
|
362
365
|
}
|
|
@@ -460,6 +463,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
460
463
|
[
|
|
461
464
|
agentId,
|
|
462
465
|
projectId,
|
|
466
|
+
storeApi,
|
|
463
467
|
refreshTree,
|
|
464
468
|
loadSourceSummaries,
|
|
465
469
|
ensureFolderLoaded,
|
|
@@ -511,8 +515,8 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
511
515
|
if (epoch !== contextEpoch.current) return;
|
|
512
516
|
// A gap may have hidden pending:changed frames — re-count once.
|
|
513
517
|
scheduleCountRef.current();
|
|
514
|
-
const state =
|
|
515
|
-
|
|
518
|
+
const state = storeApi.getState();
|
|
519
|
+
storeApi.setState(s => ({ sourceChangeRevision: Object.fromEntries(s.sources.map(source => [sourceFromUri(source.uri_root), (s.sourceChangeRevision[sourceFromUri(source.uri_root)] ?? 0) + 1])) }));
|
|
516
520
|
const sources = new Set<string>();
|
|
517
521
|
for (const [key, loaded] of Object.entries(state.loadedFolders || {})) {
|
|
518
522
|
if (!loaded) continue;
|
|
@@ -526,7 +530,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
526
530
|
);
|
|
527
531
|
})();
|
|
528
532
|
}, RECONNECT_RECONCILE_DEBOUNCE_MS);
|
|
529
|
-
}, [agentId, projectId, loadSourceSummaries, reconcileSourceFolders]);
|
|
533
|
+
}, [agentId, projectId, storeApi, loadSourceSummaries, reconcileSourceFolders]);
|
|
530
534
|
|
|
531
535
|
const handleConnChangeRef = useRef(handleConnChange);
|
|
532
536
|
handleConnChangeRef.current = handleConnChange;
|
|
@@ -536,14 +540,17 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
536
540
|
return () => {
|
|
537
541
|
if (timer.current) clearTimeout(timer.current);
|
|
538
542
|
};
|
|
539
|
-
}, [agentId, projectId]);
|
|
543
|
+
}, [agentId, projectId, storeApi]);
|
|
540
544
|
|
|
541
545
|
useEffect(() => {
|
|
542
546
|
if (!agentId || !projectId) return;
|
|
543
547
|
const host = getFilesystemHostImperative();
|
|
544
548
|
if (!host?.subscribeFilesystemEvents) return;
|
|
545
|
-
|
|
546
|
-
|
|
549
|
+
// Inert once this scope is gone (agent/project/store change, unmount): a frame
|
|
550
|
+
// the hub delivers after the unsubscribe must not reach the next scope's store.
|
|
551
|
+
let live = true;
|
|
552
|
+
const unsubscribe = host.subscribeFilesystemEvents(agentId, projectId, (raw) => {
|
|
553
|
+
if (!live) return;
|
|
547
554
|
// The hub carries the whole authorized agent set; keep only THIS agent's
|
|
548
555
|
// known event types (server already scoped by user/project).
|
|
549
556
|
const event = raw as FilesystemEvent | null;
|
|
@@ -565,6 +572,7 @@ export function useFilesystemSSE(agentId: string | undefined, projectId: string
|
|
|
565
572
|
const isExternal = (event.payload as { external?: boolean }).external === true;
|
|
566
573
|
if (!isBadge && !isExternal && event.agentId !== agentId) return;
|
|
567
574
|
handleEventRef.current(event);
|
|
568
|
-
}, () => handleConnChangeRef.current());
|
|
569
|
-
|
|
575
|
+
}, () => { if (live) handleConnChangeRef.current(); });
|
|
576
|
+
return () => { live = false; unsubscribe(); };
|
|
577
|
+
}, [agentId, projectId, storeApi]);
|
|
570
578
|
}
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { getGitRepos, getGitStatus } from '../api';
|
|
5
5
|
import type { SourceInfo } from '../domain/types';
|
|
6
6
|
import type { GitGroup } from '../ui/pendingChangesModel';
|
|
7
|
+
import { useFilesystemStore } from '../store/filesystemStore';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The Git half of the Changes inventory: every accessible checkout's status, one
|
|
@@ -15,6 +16,7 @@ import type { GitGroup } from '../ui/pendingChangesModel';
|
|
|
15
16
|
export function useGitChangesInventory(params: {
|
|
16
17
|
agentId: string; projectId: string | null; sources: SourceInfo[]; revisions: Record<string, number>; resetKey: string;
|
|
17
18
|
}): { groups: GitGroup[]; loading: boolean; refresh: (force: boolean) => Promise<void> } {
|
|
19
|
+
const client = useFilesystemStore((s) => s.client);
|
|
18
20
|
const { agentId, projectId, sources, revisions, resetKey } = params;
|
|
19
21
|
const [groups, setGroups] = useState<GitGroup[]>([]);
|
|
20
22
|
const [loading, setLoading] = useState(false);
|
|
@@ -42,7 +44,7 @@ export function useGitChangesInventory(params: {
|
|
|
42
44
|
const revision = revisions[slug] ?? 0;
|
|
43
45
|
if (!all && seen.current[slug] === revision) continue;
|
|
44
46
|
try {
|
|
45
|
-
const repos = await getGitRepos({ agentId, ...scope, source: slug });
|
|
47
|
+
const repos = await getGitRepos({ agentId, ...scope, source: slug }, client);
|
|
46
48
|
if (mine !== seq.current) return;
|
|
47
49
|
for (let index = next.length - 1; index >= 0; index--) {
|
|
48
50
|
if (next[index].source === slug && !repos.repos.some(repo => repo.rel === next[index].repo.rel)) next.splice(index, 1);
|
|
@@ -50,7 +52,7 @@ export function useGitChangesInventory(params: {
|
|
|
50
52
|
for (const repo of repos.repos) {
|
|
51
53
|
const group: GitGroup = { source: slug, repo };
|
|
52
54
|
try {
|
|
53
|
-
const result = await getGitStatus({ agentId, ...scope, source: slug, repo: repo.rel });
|
|
55
|
+
const result = await getGitStatus({ agentId, ...scope, source: slug, repo: repo.rel }, client);
|
|
54
56
|
if (mine !== seq.current) return;
|
|
55
57
|
if (result.ok === false) group.error = 'Git status unavailable'; else group.status = result.status;
|
|
56
58
|
} catch (error) { group.error = error instanceof Error ? error.message : 'Git status failed'; }
|
|
@@ -15,18 +15,19 @@ The host integration entry point — `installFilesystemHostComponents`, re-expor
|
|
|
15
15
|
- `FilesystemHostValue.currentRole` — `{ role?, priority? }` for the caller in the active project, derived from `WorkspaceHostPort.useCurrentUser()` (which now returns role + ordinal priority alongside name/email). Owner/admin-effective is `priority <= 2`. Used to HIDE owner-only Sources/Packages controls (the `recognizesPackages` toggle, the package access-feature override); UX-only — the server route stays the enforcement floor, so an absent signal hides the control (deny-by-default).
|
|
16
16
|
- `FilesystemHostValue.grantedFeatures` — `readonly string[]`, the caller's granted feature ids in the active project, read from `WorkspaceHostPort.getGrantedFeatures(projectId, userId)`. **Evaluate it ONLY through `hasFeature` / `meetsRequires` from `@neuralis/package-system/access`** — never a hand-rolled `includes` (it would miss the `'*'` wildcard), and never `currentRole`: the role NAME is not an authority source, not even for a hide-only gate. It is `readonly` so no UI file can mutate the caller's grants; the kernel predicates take a mutable array, so copy at the call site rather than widening the contract. UX-only exactly like `currentRole` — it hides an affordance the caller could not use (the S2 "hidden means absent" discipline: hidden entirely, never a greyed-out or locked row), and the route re-checks every call regardless. Absent/empty ⇒ hide, deny-by-default. First consumer: the Explorer tree's **Discover dock**, gated on `drive.mount`.
|
|
17
17
|
- The port returns a FRESH array on every call, so `buildFilesystemHostValue` memoises on a joined key rather than array identity — a raw pass-through would invalidate the whole host-value memo on every render.
|
|
18
|
-
- `installFilesystemHostComponents({ registry: PackageHostRegistry, port: WorkspaceHostPort })` — side-effecting; registers widget + card + the provider fill (one component per install, closed over the port; it ignores the slot's `snapshot`/`assetScope` props), and subscribes ONCE to the host's optional `port.onClientStateReset` so the dock broom (widget / agent / project level) reaches
|
|
18
|
+
- `installFilesystemHostComponents({ registry: PackageHostRegistry, port: WorkspaceHostPort })` — side-effecting; registers widget + card + the provider fill (one component per install, closed over the port; it ignores the slot's `snapshot`/`assetScope` props), and subscribes ONCE to the host's optional `port.onClientStateReset` with `resetFilesClientState`, so the dock broom (instance / widget / agent / project level) reaches every mounted Files window it scopes and the one persisted record. A host without the method simply never resets.
|
|
19
19
|
- `InstallFilesystemHostComponentsOptions`.
|
|
20
20
|
|
|
21
21
|
## How it plugs into the package-system kernel
|
|
22
22
|
|
|
23
23
|
- `WorkspaceHostPort` (from `@neuralis/package-system/contracts`) is the single point of contact between the host and brain-core's UI. The host has exactly one implementation; brain-core consumes the port without knowing which host runs it.
|
|
24
24
|
- `installFilesystemHostComponents` registers the widget under `componentImport: '@neuralis/brain-core/app/filesystem'` — the same string the package manifest declares for `app.surfaces[].component.import`. The host renderer resolves widgets by that import string at render time.
|
|
25
|
-
- The
|
|
25
|
+
- `FilesystemWidgetHost` is the window seam: it receives the window's `widgetId` and BOUND `agentId`, creates the window's own store (`useFilesWindowStore` — none, and nothing painted, until the live user and project are known) and mounts it under `FilesystemStoreProvider`, keyed on that identity so a different user or project remounts the whole Files tree instead of re-using callbacks of the previous one. The store's request `client` (`createPackageApi` over the window's project and bound agent) is what every API call passes; the surrounding `<PackageApiProvider slug="brain-core" projectId agentId userId>` keeps serving the kernel's own React consumers.
|
|
26
26
|
- The card registration uses `registerCardComponent('filesystem.change', FilesystemChangeCard, { owner: { kind: 'package-direct', packageId: '@neuralis/brain-core' }, renderer: 'direct', mountPolicy: 'virtualized', estimateHeight: estimateFilesystemChangeCardHeight })` from `@neuralis/agent-core/app/chat` — the agent-core chat surface owns the card registry; brain-core contributes an OWNED first-party direct (React) card. The `virtualized` mount policy keeps it out of the iframe/keep-alive overscan bump. The registry rejects any OTHER owner touching `filesystem.change` (`OWNER_MISMATCH`) and never lets a package overwrite a builtin. `estimateHeight` is the card's own row-height model (see `app/cards/README.md`); it MUST stay a module-level reference — the registry compares estimator identity when deciding whether a re-registration is idempotent, so an inline arrow here would churn the registry (fingerprint bump + retained-View eviction) on every call.
|
|
27
27
|
|
|
28
28
|
## Security & scope
|
|
29
29
|
|
|
30
|
+
- One window store per mounted Files window, hidden and minimized windows included (they stay mounted); a window id addresses a reset and keys a persisted view, never an authority.
|
|
30
31
|
- The widget never sees raw host internals. `WorkspaceHostPort` exposes only the active session ids, the agent map, file marks, the SSE URL builder, granted features (read-only), the docker flag, and the client-state reset subscription (a scope, never a package name — the host stays generic).
|
|
31
32
|
- File marks and event subscriptions are agent-scoped; cross-agent observation requires the corresponding `filesystem.observeScoped` / `filesystem.modifyScoped` features which the route layer enforces.
|
|
32
33
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
'use client';
|
|
11
11
|
|
|
12
|
-
import React, { Suspense, type ReactNode } from 'react';
|
|
12
|
+
import React, { Suspense, useEffect, type ReactNode } from 'react';
|
|
13
13
|
import type { WorkspaceHostPort } from '@neuralis/package-system/contracts';
|
|
14
14
|
import {
|
|
15
15
|
PackageApiProvider,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from '@neuralis/package-system/client';
|
|
19
19
|
import { registerCardComponent } from '@neuralis/agent-core/app/chat';
|
|
20
20
|
import { FilesystemWidget } from '../ui';
|
|
21
|
-
import {
|
|
21
|
+
import { FilesystemStoreProvider, resetFilesClientState, setFilesLiveUser, useFilesWindowStore } from '../store/filesystemStore';
|
|
22
22
|
import { FilesystemChangeCard } from '../../cards/FilesystemChangeCard';
|
|
23
23
|
import { estimateFilesystemChangeCardHeight } from '../../cards/FilesystemCardShell';
|
|
24
24
|
import { FILESYSTEM_CARD_MATCH_TOOLS, FILESYSTEM_COMPOSER_CONTRACT } from '../../cards/diffUtils';
|
|
@@ -48,10 +48,12 @@ type FilesystemWidgetProps = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* The dock broom's reach into the Files
|
|
51
|
+
* The dock broom's reach into the Files stores is wired ONCE per module, at
|
|
52
52
|
* install time, to the FIRST port that carries `onClientStateReset` — the only
|
|
53
|
-
* place the port is consumed outside `buildFilesystemHostValue
|
|
54
|
-
*
|
|
53
|
+
* place the port is consumed outside `buildFilesystemHostValue` and the window
|
|
54
|
+
* host. ONE subscription for every window: `resetFilesClientState` fans the scope
|
|
55
|
+
* out to the mounted windows it reaches and to the one persisted record. A host
|
|
56
|
+
* without the optional method simply never resets (fail-safe: the package
|
|
55
57
|
* assumes nothing); a second install with a different port is not re-wired
|
|
56
58
|
* (there is one host port per process).
|
|
57
59
|
*/
|
|
@@ -64,15 +66,17 @@ export function installFilesystemHostComponents(
|
|
|
64
66
|
|
|
65
67
|
if (!resetSubscribed && port.onClientStateReset) {
|
|
66
68
|
resetSubscribed = true;
|
|
67
|
-
port.onClientStateReset(
|
|
68
|
-
useFilesystemStore.getState().resetClientState(scope);
|
|
69
|
-
});
|
|
69
|
+
port.onClientStateReset(resetFilesClientState);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// The provider fill: ONE component per install (its identity is the host's
|
|
73
73
|
// memo key). The slot's `snapshot`/`assetScope` props are not read — the
|
|
74
74
|
// Files host value comes from the port alone.
|
|
75
75
|
function FilesystemWorkspaceProvider({ children }: { children?: ReactNode }) {
|
|
76
|
+
// Whose persisted Files records a broom prunes — the signed-in user, also
|
|
77
|
+
// when no Files window is open.
|
|
78
|
+
const userId = port.useUserId();
|
|
79
|
+
useEffect(() => { setFilesLiveUser(userId); }, [userId]);
|
|
76
80
|
return <FilesystemPackageHost port={port}>{children}</FilesystemPackageHost>;
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -82,13 +86,22 @@ export function installFilesystemHostComponents(
|
|
|
82
86
|
Component: FilesystemWorkspaceProvider,
|
|
83
87
|
});
|
|
84
88
|
|
|
89
|
+
// ONE store per window — the insertion seam. `agentId` is the window's BOUND
|
|
90
|
+
// agent and `widgetId` its id; a different user or project is a new store, and
|
|
91
|
+
// nothing renders before both are known. The key remounts the Files tree with a
|
|
92
|
+
// new store, so no component keeps a callback, client or local list of the
|
|
93
|
+
// previous identity.
|
|
85
94
|
function FilesystemWidgetHost({ agentId, widgetId, state }: FilesystemWidgetProps) {
|
|
86
95
|
const projectId = port.useProjectId();
|
|
87
96
|
const userId = port.useUserId();
|
|
97
|
+
const store = useFilesWindowStore(widgetId, userId, projectId, agentId);
|
|
98
|
+
if (!store) return LOADING;
|
|
88
99
|
return (
|
|
89
100
|
<WorkspaceHostPortProvider port={port}>
|
|
90
101
|
<PackageApiProvider slug="brain-core" projectId={projectId} agentId={agentId} userId={userId}>
|
|
91
|
-
<
|
|
102
|
+
<FilesystemStoreProvider key={`${widgetId}\u0000${userId}\u0000${projectId}\u0000${agentId}`} store={store}>
|
|
103
|
+
<FilesystemWidget agentId={agentId} projectId={projectId} userId={userId} widgetId={widgetId} state={state} />
|
|
104
|
+
</FilesystemStoreProvider>
|
|
92
105
|
</PackageApiProvider>
|
|
93
106
|
</WorkspaceHostPortProvider>
|
|
94
107
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# brain-core/app/filesystem/store
|
|
2
2
|
|
|
3
|
-
Zustand
|
|
3
|
+
Zustand stores powering the Files windows — ONE live store per window (file dictionary, expanded folders, sources registry, search and git state, list / search / git slices) and ONE persistence owner for all of them.
|
|
4
4
|
|
|
5
5
|
## Responsibility
|
|
6
6
|
|
|
@@ -28,12 +28,15 @@ Two guards make this safe against the races a plain re-list would lose:
|
|
|
28
28
|
|
|
29
29
|
## Public Surface
|
|
30
30
|
|
|
31
|
-
- `
|
|
31
|
+
- `createFilesystemStore(init)` — one Files store (a vanilla Zustand store exposing `FileSystemState` plus the list / search / git slice actions). Every asynchronous fence and dedup map is created with it (`resetEpoch`, the pending-count and search / git sequences, the rehydrate set, the folder-load and root-listing dedup maps, the reconcile jobs, the per-folder generations), so two windows — of two agents or of the same one — never fence, supersede or dedup each other. It persists nothing itself and carries its window's identity: `windowId` and `client`, the request client built over the window's project and BOUND agent that every API call of the store passes.
|
|
32
|
+
- `createFilesWindowStore(identity)` / `mountFilesWindow(store, identity)` / `useFilesWindowStore(...)` — a window's store for its LIVE `(windowId, userId, projectId, agentId)`, registered while mounted. `useFilesWindowStore` returns `null` until the user and project are known (nothing persisted is read and nothing paints before), and a different user or project is a different store.
|
|
33
|
+
- `FilesystemStoreProvider` / `useFilesystemStore(selector)` / `useFilesystemStoreApi()` — the window's store through React context. The hook keeps its name and reads THIS window's store; outside a `FilesystemStoreProvider` it throws (there is no module store to fall back to). Imperative readers (event handlers, the SSE hook) take the store from `useFilesystemStoreApi()`.
|
|
34
|
+
- `filesPersistence` — the ONE writer of `neuralis.filesystem.tabs` (v2, see the context space below), and `resetFilesClientState(scope)`, the broom's ONE entry.
|
|
32
35
|
- `helpers.ts` — URI helpers (`fileKey`, `fileKeyFromNode`, `folderUri`, `sourceFromUri`, `sourceRootUri`, `extractFilename`, `uriAncestors`, `parseFilePrev`, `extractFileContent`, `extractReadFilesPayload`, `ensureFolderChainForUri`, `hasCompletedRead` — a tab's read is done when it left a string baseline or an error; FileNode presence says nothing — and `contextKey(userId, projectId, agentId)`, the ONE key of the context space).
|
|
33
36
|
- `searchSlice.ts` — `SearchSliceState`, `SearchSliceActions`, `SearchSliceHit`.
|
|
34
37
|
- `listSlice.ts` — `ListSliceActions` (`loadList`, `refresh` — both source-summary loaders) + `contextResetPatch()`, the ONE per-context wipe (everything except the preference fields, the active pair and the in-flight `uploading` flag) shared by `loadList`'s agent/project switch and `resetClientState`.
|
|
35
38
|
- `FILES_LRU_HARD_CAP` — the cold-row cap (exported for the eviction tests, which seed `cap + n` rows instead of a hardcoded count).
|
|
36
|
-
- **The context space — `activeUserId` + `contextStateByKey`** (owner rule 2026-09-04: isolation between agents is not loss). The store is scoped the way the platform is: by `(user, project, agent)`. The ACTIVE context lives in the flat fields; every context the store has left sits in `contextStateByKey` under `contextKey(userId, projectId, agentId)` as its FULL per-context slice — exactly the keys of `contextResetPatch()` (`captureContextSlice`): tree nodes, expanded/loaded folders, tabs, content caches, search, git panel, sources, panels. `loadList` on a switch fences, stashes the outgoing slice, and restores the incoming one (or the reset defaults for a context never visited), then re-reads the restored active tab (`rehydrateActiveTab`) and reconciles the restored context's loaded sources — its realtime events were dropped while it was inactive, so a switch-back IS a gap. The widget passes `userId` (from `port.useUserId()`) only on its mount `loadList`; a store with no user ADOPTS the first real one in place, a different user is a switch.
|
|
39
|
+
- **The context space — `activeUserId` + `contextStateByKey`** (owner rule 2026-09-04: isolation between agents is not loss). The store is scoped the way the platform is: by `(user, project, agent)`. The ACTIVE context lives in the flat fields; every context the store has left sits in `contextStateByKey` under `contextKey(userId, projectId, agentId)` as its FULL per-context slice — exactly the keys of `contextResetPatch()` (`captureContextSlice`): tree nodes, expanded/loaded folders, tabs, content caches, search, git panel, sources, panels. `loadList` on a switch fences, stashes the outgoing slice, and restores the incoming one (or the reset defaults for a context never visited), then re-reads the restored active tab (`rehydrateActiveTab`) and reconciles the restored context's loaded sources — its realtime events were dropped while it was inactive, so a switch-back IS a gap. The widget passes `userId` (from `port.useUserId()`) only on its mount `loadList`; a store with no user ADOPTS the first real one in place, a different user is a switch. **Persistence has ONE owner** (`filesPersistence`) — window stores persist nothing, so two windows writing one key can never overwrite each other. The v2 record keeps its prefs, its active triple and the per-agent `contexts` projections (`PersistedFilesContext`: the tab shape, `expandedFolders` as an array, `sidebarView`, `activeWorkspaceTab`, `workspacePanels`, `activeFolderPath`) exactly as an older build wrote them, plus an OPTIONAL `windows` map: one `PersistedFilesWindow` per window id — the same allowlist plus the user, project and bound agent it was written for. Nodes and caches stay in memory and re-fetch. A window's store starts from its OWN projection when the record holds one for the LIVE identity (one written for another user, project or agent is ignored); a window with none starts from the defaults — the per-agent `contexts` entries are never read into a window (no translation from an agent key to a window id; an upgrade resets the open tabs once). While mounted, the window feeds its projection to the owner: per store update eight reference compares, and only a projection that changed is serialized and written (the whole allowlisted record, once). A record without `windows` is read without one and is not rewritten until a window really changes; a v1 payload's flat tab trio is lifted into the active context by `migrate` (pre-existing, not extended). Hiding a window or switching a Group keeps its projection; unmounting drops its subscription and fences its in-flight readers, never its projection. The dock broom is the ONE writer that drops persisted state, its own scope only (see below). It mirrors the host's per-context runtime map; the routes remain the enforcement floor — this is the client's mirror, never an access decision.
|
|
37
40
|
- `rehydrateActiveTab` — fetches the active tab's content when no read has completed for it (`hasCompletedRead`), against the store's OWN active pair; run by `loadList` on a same-context mount (page reload) and after a switch-back restore, and by `setActiveFile` (a tab click). It replaced a `FilesystemWidget` effect that decided from its render closure (the previous agent's tab re-opened under the new one after a switch) and from FileNode presence (a listed-but-unread tab rendered "Empty file"). A per-uri in-flight set stops a double activation from issuing two reads; the fence clears it (the same uri under another context is another file).
|
|
38
41
|
- `saveFileInPlace` / `markFileDeleted` — the editor's in-place mutation tails beside `moveFileToFolder`: move when the target uri differs, update content when it differs, then a targeted `reconcileFolders` of BOTH parents (the old folder loses the departed row — the upsert-only `refresh` never could) and a forced re-read; delete, then reconcile the parent (the tombstone comes from the reconcile). A rejected `/modify` is HTTP 200 — `ok !== true` throws the server's message and runs no tail.
|
|
39
42
|
- `snapshotError` / `clearSnapshotError` — the ROOT-listing failure (`loadList`'s catch: the service, not one source — a source's own failure is `hydrationState[uri]`); rendered by the tree with Retry, cleared by the next successful listing, the reset patch or the dismiss.
|
|
@@ -41,9 +44,9 @@ Two guards make this safe against the races a plain re-list would lose:
|
|
|
41
44
|
|
|
42
45
|
## How it plugs into the package-system kernel
|
|
43
46
|
|
|
44
|
-
- The store calls `api/index.ts`
|
|
47
|
+
- The store calls the `api/index.ts` functions with its own `client` (`createPackageApi` over the window's project and bound agent) — the X-Project-Id / X-Agent-Id headers are fixed before any await and never come from another mounted window; the routes resolve the real session from them.
|
|
45
48
|
- SSE events arrive via `useFilesystemSSE` (in `hooks/`) and dispatch into store actions; the event payload follows the `FilesystemEventPayload` shape from `src/domain/events.ts`.
|
|
46
|
-
- **The dock broom reaches
|
|
49
|
+
- **The dock broom reaches these stores through the kernel port**: `installFilesystemHostComponents` subscribes `port.onClientStateReset` ONCE at install time with `resetFilesClientState(scope)`, which calls `resetClientState(scope)` on every mounted window the scope reaches and then prunes the persisted record: `instance` (one window cleaned) drops that window's projection; `widget` (one window destroyed) drops it and the legacy context of its `(user, project, agent)`; `agent` / `project` drop the legacy contexts and the window projections of their scope — the signed-in user's only, never another project's or a sibling window's. The store action matches the scope against its ACTIVE `(activeProjectId, activeAgentId)` pair (`instance` / `widget`: also the `filesystem` widget type AND its own `windowId`; anything else is a no-op), applies `contextResetPatch()` (which includes an empty `tabMeta`) — so the persisted allowlist (`openFiles`/`activeFile`/`tabMeta`) is written EMPTY and a persisted tab cannot re-run the read/list race on the next mount — and fences everything in flight through the ONE `fenceInFlightWriters` action — the store's own `resetEpoch` that the `openFile` read, a reconcile pass's `set()`, `createNode`'s optimistic folder and the editor tails `saveFileInPlace` / `markFileDeleted` capture before their `await` and check after, plus a generation bump on every folder ever listed and a clear of the folder-load dedup map and the rehydrate set. **`loadList`'s agent/project/user switch runs the SAME fence before it swaps the context** — until 2026-09-04 only the broom fenced, so a read a tab click had started under the previous agent could land its file, and `activeFile`, under the new one. Preferences survive (`previewEditorsEnabled`, `diffViewMode`, `sidebarWidth`, `sidebarOpen`, `gitViewMode`, `searchMode`) and so does the active pair. Never a server call; `reconcileJobs` is deliberately not cleared (a running job's `finally` would delete the next job's entry and two passes would run at once — the epoch makes the running job inert instead).
|
|
47
50
|
|
|
48
51
|
## Security & scope
|
|
49
52
|
|
|
@@ -53,9 +56,9 @@ Two guards make this safe against the races a plain re-list would lose:
|
|
|
53
56
|
|
|
54
57
|
## Gotchas
|
|
55
58
|
|
|
56
|
-
-
|
|
59
|
+
- The search and git slices keep monotonic per-store sequences (`searchSeq`, `gitStatusSeq`, `gitBranchSeq`) that discard stale responses. Removing them re-introduces the classic "old query results overwrite new ones" race; sharing them across stores would let one window's request supersede another's.
|
|
57
60
|
- `fileKey` returns the URI as the canonical key. URIs MUST be canonicalised (`canonicalUri` in `src/usecases/list.ts`) before lookup — three-slash URIs (`brain:///foo`) collapse to two-slash form.
|
|
58
|
-
- `loadList` uses
|
|
61
|
+
- `loadList` uses a per-store in-flight map keyed by `(agentId, projectId, withCounts)` to deduplicate concurrent calls. Calling `refresh` while a load is pending no-ops the second call.
|
|
59
62
|
- **`/modify` answers HTTP 200 for a REJECTED operation** — the outcome is in `ok`, not the status code. Every mutation action branches on `ok === false` before applying an optimistic state, and reports through `organizeError` (a dismissible banner above the tree). `warning: 'moved_with_source_retained'` is a *partial success*: the copy landed, the source delete did not, and BOTH copies exist on purpose.
|
|
60
63
|
- A **folder** move or delete is decomposed server-side into a recursive copy + a delete of the old subtree. The reconcile only inspects DIRECT children, so the store prunes by URI PREFIX for those two cases — a parent-only reconcile would leave the old subtree's interior behind forever.
|
|
61
64
|
- **`removeFilePath` purges a URI from EVERY surface** — node, `openFiles`, `activeFile`, `tabMeta` and the content/baseline/error/prev side maps — and it must not require the node to exist. `tabMeta`/`openFiles` are PERSISTED, so a tab pointing at a deleted file outlives the node; gating the purge on `files[uri]` made it a no-op in exactly that case, and the tab could never close itself.
|
|
@@ -69,4 +72,5 @@ Two guards make this safe against the races a plain re-list would lose:
|
|
|
69
72
|
- `test/store/reconcileSourceFolders.test.ts` — the external-event reconcile path.
|
|
70
73
|
- `test/store/visibleReconcile.test.ts` — visible-only wide passes, collapse → stale → one narrow authoritative listing on re-open, and the bound.
|
|
71
74
|
- `test/store/syncCompletedRelist.test.tsx` — the real SSE hook + store: a zero-count `sync:completed` lists nothing, and one event burst's list-request count.
|
|
75
|
+
- `test/store/filesWindows.test.ts` — independent windows on the one owner: two same-agent windows keep distinct tabs and send their own client, interleaved writes survive a reload, an old record is read as it is and not rewritten until a real change, no content is persisted, a reset reaches exactly its scope, and a late answer never lands in the next identity's store.
|
|
72
76
|
- `test/ui/` covers the rest of the store actions: list merge with the source registry, SSE event reduction, search slice debouncing, and the eviction cap.
|