@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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createContext, createElement, useContext, useLayoutEffect, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { useStore } from 'zustand';
|
|
2
3
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
4
|
+
import { createStore, type StoreApi as ZustandStoreApi } from 'zustand/vanilla';
|
|
3
5
|
import type { ClientStateResetScope } from '@neuralis/package-system/contracts';
|
|
4
|
-
import
|
|
6
|
+
import { createPackageApi, type PackageApi } from '@neuralis/package-system/client';
|
|
7
|
+
import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, PersistedFilesWindow, SidebarView, WorkspacePanelId } from '../domain/types';
|
|
5
8
|
export type { SidebarView, WorkspacePanelId } from '../domain/types';
|
|
6
9
|
import { countPendingChanges, readFiles, uploadFile, writeFile, writeRefusalMessage } from '../api';
|
|
7
10
|
import {
|
|
@@ -42,44 +45,6 @@ import { gitSliceInitial, createGitSlice, type GitSliceState, type GitSliceActio
|
|
|
42
45
|
*/
|
|
43
46
|
export const FILES_LRU_HARD_CAP = 50_000;
|
|
44
47
|
|
|
45
|
-
/**
|
|
46
|
-
* Bumped by `fenceInFlightWriters` — the ONE fence BOTH reset paths run before
|
|
47
|
-
* their wipe: `resetClientState` (the dock broom) and `loadList`'s agent/project
|
|
48
|
-
* switch. Every async writer that materialises a node or content AFTER an await
|
|
49
|
-
* (the `openFile` read, a reconcile pass's `set`, the optimistic folder in
|
|
50
|
-
* `createNode`, the editor tails `saveFileInPlace` / `markFileDeleted`) captures
|
|
51
|
-
* it BEFORE the await and writes only when it still matches — a read, listing
|
|
52
|
-
* or mutation answer that resolves after the reset must not put a tab, its
|
|
53
|
-
* folder chain, its content or its parent's dirty flag into a store that now
|
|
54
|
-
* belongs to another context. Module-level like the generation map: the reset
|
|
55
|
-
* is a store-wide event. Until 2026-09-04 only the broom fenced; the switch
|
|
56
|
-
* wiped without fencing, so a read a tab click had started under the previous
|
|
57
|
-
* agent could land its file — and `activeFile` — under the new one.
|
|
58
|
-
*
|
|
59
|
-
* Writers that need NO epoch (documented, not oversight): `moveFileToFolder` /
|
|
60
|
-
* `renameNode` / `pasteNodes` / `deleteNodes` / `upload*` are no-ops on an empty
|
|
61
|
-
* store (`replaceFilePath`, `prunePrefix`, `patchFileNode`, the `openFiles.map`
|
|
62
|
-
* all find nothing) or set a hint flag; a late `loadList` answer writes
|
|
63
|
-
* `brain://` + `sources`, which the remounted widget re-requests anyway; late
|
|
64
|
-
* `runSearch` / `loadGitStatus` / `loadGitBranches` answers are overwritten on
|
|
65
|
-
* the next use; `ensureFolderLoaded`'s superseded and catch branches write
|
|
66
|
-
* `hydrationState[uri]` only (`'idle'` / `'error'` for a folder row that is gone);
|
|
67
|
-
* the SSE handlers are synchronous and die with the widget; `applyNav` /
|
|
68
|
-
* `autoExpandToFile` write synchronously before any await.
|
|
69
|
-
*/
|
|
70
|
-
let resetEpoch = 0;
|
|
71
|
-
/** The newest `refreshPendingCount` call — an older answer never overwrites a newer one. */
|
|
72
|
-
let pendingCountSeq = 0;
|
|
73
|
-
/**
|
|
74
|
-
* Tabs whose rehydrate read is in flight — a second activation must not issue a
|
|
75
|
-
* second read. Keyed by uri, so the fence CLEARS it: the same uri under another
|
|
76
|
-
* context is another file, and a stale key would suppress the new context's read
|
|
77
|
-
* for good (the old answer is fenced out, the baseline never arrives).
|
|
78
|
-
*/
|
|
79
|
-
const rehydrateInFlight = new Set<string>();
|
|
80
|
-
|
|
81
|
-
const liveFolderLoadInFlight = new Map<string, Promise<void>>();
|
|
82
|
-
|
|
83
48
|
/**
|
|
84
49
|
* Per-source reconcile job. The guard used to DROP a request that arrived while a
|
|
85
50
|
* reconcile was running, which made every mutation tail unsafe: the drop silently
|
|
@@ -89,29 +54,78 @@ const liveFolderLoadInFlight = new Map<string, Promise<void>>();
|
|
|
89
54
|
* and the caller awaits the whole chain.
|
|
90
55
|
*/
|
|
91
56
|
type ReconcileJob = { rerun: boolean; wide: boolean; extra: Set<string>; promise: Promise<void> };
|
|
92
|
-
const reconcileJobs = new Map<string, ReconcileJob>();
|
|
93
57
|
|
|
94
58
|
/** Bound on coalesced reruns so a hot event stream can't spin the loop forever. */
|
|
95
59
|
const MAX_RECONCILE_PASSES = 3;
|
|
96
60
|
|
|
97
61
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
62
|
+
* The asynchronous fences of ONE Files store — one set per window, created with
|
|
63
|
+
* it (`createFilesystemStore`). Two windows never share a fence: a reset or a
|
|
64
|
+
* context switch is that window's event, so it fences only that window's
|
|
65
|
+
* in-flight work, and a sibling window's listing can never supersede this one's.
|
|
66
|
+
*
|
|
67
|
+
* - `resetEpoch` — bumped by `fenceInFlightWriters`, the ONE fence BOTH reset
|
|
68
|
+
* paths run before their wipe: `resetClientState` (the dock broom) and
|
|
69
|
+
* `loadList`'s context switch. Every async writer that materialises a node or
|
|
70
|
+
* content AFTER an await (the `openFile` read, a reconcile pass's `set`, the
|
|
71
|
+
* optimistic folder in `createNode`, the editor tails `saveFileInPlace` /
|
|
72
|
+
* `markFileDeleted`) captures it BEFORE the await and writes only when it still
|
|
73
|
+
* matches — a read, listing or mutation answer that resolves after the reset
|
|
74
|
+
* must not put a tab, its folder chain, its content or its parent's dirty flag
|
|
75
|
+
* into a store that now belongs to another context. Writers that need NO epoch
|
|
76
|
+
* (documented, not oversight): `moveFileToFolder` / `renameNode` / `pasteNodes`
|
|
77
|
+
* / `deleteNodes` / `upload*` are no-ops on an empty store (`replaceFilePath`,
|
|
78
|
+
* `prunePrefix`, `patchFileNode`, the `openFiles.map` all find nothing) or set
|
|
79
|
+
* a hint flag; a late `loadList` answer writes `brain://` + `sources`, which the
|
|
80
|
+
* remounted widget re-requests anyway; late `runSearch` / `loadGitStatus` /
|
|
81
|
+
* `loadGitBranches` answers are overwritten on the next use;
|
|
82
|
+
* `ensureFolderLoaded`'s superseded and catch branches write
|
|
83
|
+
* `hydrationState[uri]` only; the SSE handlers are synchronous and die with the
|
|
84
|
+
* widget; `applyNav` / `autoExpandToFile` write synchronously before any await.
|
|
85
|
+
* - `pendingCountSeq` — the newest `refreshPendingCount` call; an older answer
|
|
86
|
+
* never overwrites a newer one.
|
|
87
|
+
* - `rehydrateInFlight` — tabs whose rehydrate read is in flight, so a second
|
|
88
|
+
* activation issues no second read. Keyed by uri, so the fence CLEARS it: the
|
|
89
|
+
* same uri under another context is another file, and a stale key would
|
|
90
|
+
* suppress the new context's read for good.
|
|
91
|
+
* - `liveFolderLoadInFlight` — the folder-listing dedup map.
|
|
92
|
+
* - `reconcileJobs` — the per-source coalescer (`ReconcileJob`).
|
|
93
|
+
* - `folderGeneration` — a monotonic list-generation token PER FOLDER (not per
|
|
94
|
+
* source). Two listings of the SAME folder can be in flight at once —
|
|
95
|
+
* `reconcileSourceFolders` lists its targets concurrently and
|
|
96
|
+
* `ensureFolderLoaded` can overlap it — so a slow response must not overwrite a
|
|
97
|
+
* newer one. Every list issue bumps its folder's counter; a response whose
|
|
98
|
+
* token is no longer current is discarded. A per-SOURCE token would not catch
|
|
99
|
+
* this: both listings share the source.
|
|
104
100
|
*/
|
|
105
|
-
|
|
101
|
+
type StoreFences = {
|
|
102
|
+
resetEpoch: number;
|
|
103
|
+
pendingCountSeq: number;
|
|
104
|
+
rehydrateInFlight: Set<string>;
|
|
105
|
+
liveFolderLoadInFlight: Map<string, Promise<void>>;
|
|
106
|
+
reconcileJobs: Map<string, ReconcileJob>;
|
|
107
|
+
folderGeneration: Map<string, number>;
|
|
108
|
+
};
|
|
106
109
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
function createStoreFences(): StoreFences {
|
|
111
|
+
return {
|
|
112
|
+
resetEpoch: 0,
|
|
113
|
+
pendingCountSeq: 0,
|
|
114
|
+
rehydrateInFlight: new Set<string>(),
|
|
115
|
+
liveFolderLoadInFlight: new Map<string, Promise<void>>(),
|
|
116
|
+
reconcileJobs: new Map<string, ReconcileJob>(),
|
|
117
|
+
folderGeneration: new Map<string, number>(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function nextFolderGeneration(fences: StoreFences, folderKey: string): number {
|
|
122
|
+
const gen = (fences.folderGeneration.get(folderKey) ?? 0) + 1;
|
|
123
|
+
fences.folderGeneration.set(folderKey, gen);
|
|
110
124
|
return gen;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
function isCurrentFolderGeneration(folderKey: string, gen: number): boolean {
|
|
114
|
-
return folderGeneration.get(folderKey) === gen;
|
|
127
|
+
function isCurrentFolderGeneration(fences: StoreFences, folderKey: string, gen: number): boolean {
|
|
128
|
+
return fences.folderGeneration.get(folderKey) === gen;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
/** `pendingReviewPaths` transitions the shared mapper observed, applied by the caller. */
|
|
@@ -280,8 +294,8 @@ function withPendingDelta(
|
|
|
280
294
|
return out;
|
|
281
295
|
}
|
|
282
296
|
|
|
283
|
-
/**
|
|
284
|
-
type StoreApi = { get: () => any; set: (fn: (s: any) => any) => void };
|
|
297
|
+
/** The store handle the module-level reconcile engine needs: state, its window's fences and request client. */
|
|
298
|
+
type StoreApi = { get: () => any; set: (fn: (s: any) => any) => void; fences: StoreFences; client: PackageApi };
|
|
285
299
|
|
|
286
300
|
/**
|
|
287
301
|
* How many folder listings one reconcile pass has in flight. Each costs a server
|
|
@@ -399,7 +413,7 @@ async function runReconcilePass(
|
|
|
399
413
|
// Captured at pass START: the per-folder generation guard sits in the worker,
|
|
400
414
|
// so a listing set that landed BEFORE a broom would still be written by the
|
|
401
415
|
// `set()` below unless the pass itself is fenced.
|
|
402
|
-
const epoch = resetEpoch;
|
|
416
|
+
const epoch = api.fences.resetEpoch;
|
|
403
417
|
const state0 = api.get();
|
|
404
418
|
const lf = state0.loadedFolders || {};
|
|
405
419
|
const targetSet = new Set<string>();
|
|
@@ -442,12 +456,12 @@ async function runReconcilePass(
|
|
|
442
456
|
// walk plus an index pass. A tree-wide Refresh across several sources multiplies
|
|
443
457
|
// that again.
|
|
444
458
|
const listed = await mapWithLimit(targets, RECONCILE_LIST_CONCURRENCY, async (folderKey) => {
|
|
445
|
-
const gen = nextFolderGeneration(folderKey);
|
|
459
|
+
const gen = nextFolderGeneration(api.fences, folderKey);
|
|
446
460
|
const issuedAt = Date.now();
|
|
447
461
|
try {
|
|
448
|
-
const res = await resolveList(agentId, projectId ?? undefined, { uri: folderKey, recursive: false });
|
|
462
|
+
const res = await resolveList(agentId, projectId ?? undefined, { uri: folderKey, recursive: false }, api.client);
|
|
449
463
|
// A newer listing of the SAME folder superseded this one → drop it whole.
|
|
450
|
-
if (!isCurrentFolderGeneration(folderKey, gen)) return { folderKey, res: null, issuedAt, ok: false as const };
|
|
464
|
+
if (!isCurrentFolderGeneration(api.fences, folderKey, gen)) return { folderKey, res: null, issuedAt, ok: false as const };
|
|
451
465
|
return { folderKey, res, issuedAt, ok: true as const };
|
|
452
466
|
} catch {
|
|
453
467
|
return { folderKey, res: null, issuedAt, ok: false as const };
|
|
@@ -455,7 +469,7 @@ async function runReconcilePass(
|
|
|
455
469
|
});
|
|
456
470
|
|
|
457
471
|
api.set((state: any) => {
|
|
458
|
-
if (epoch !== resetEpoch) return state; // the broom emptied the store mid-pass → inert
|
|
472
|
+
if (epoch !== api.fences.resetEpoch) return state; // the broom emptied the store mid-pass → inert
|
|
459
473
|
const next = { ...state.files };
|
|
460
474
|
const ts = Date.now();
|
|
461
475
|
const pendingDelta: ListedPendingDelta = { add: new Set(), remove: new Set() };
|
|
@@ -612,7 +626,7 @@ async function runSourceReconcile(
|
|
|
612
626
|
const { agentId, projectId, source, targets } = params;
|
|
613
627
|
const wide = params.wide !== false;
|
|
614
628
|
if (!agentId || !source) return;
|
|
615
|
-
const existing = reconcileJobs.get(source);
|
|
629
|
+
const existing = api.fences.reconcileJobs.get(source);
|
|
616
630
|
if (existing) {
|
|
617
631
|
existing.rerun = true;
|
|
618
632
|
if (wide) existing.wide = true;
|
|
@@ -621,7 +635,7 @@ async function runSourceReconcile(
|
|
|
621
635
|
return;
|
|
622
636
|
}
|
|
623
637
|
const job: ReconcileJob = { rerun: false, wide, extra: new Set(targets ?? []), promise: Promise.resolve() };
|
|
624
|
-
reconcileJobs.set(source, job);
|
|
638
|
+
api.fences.reconcileJobs.set(source, job);
|
|
625
639
|
job.promise = (async () => {
|
|
626
640
|
try {
|
|
627
641
|
for (let pass = 0; pass < MAX_RECONCILE_PASSES; pass++) {
|
|
@@ -634,7 +648,7 @@ async function runSourceReconcile(
|
|
|
634
648
|
if (!job.rerun) break;
|
|
635
649
|
}
|
|
636
650
|
} finally {
|
|
637
|
-
reconcileJobs.delete(source);
|
|
651
|
+
api.fences.reconcileJobs.delete(source);
|
|
638
652
|
}
|
|
639
653
|
})();
|
|
640
654
|
await job.promise;
|
|
@@ -756,7 +770,7 @@ function evictColdSubtrees(
|
|
|
756
770
|
}
|
|
757
771
|
|
|
758
772
|
type FilesystemNavRef =
|
|
759
|
-
| { kind: 'filesystem'; action: string; uri?: string; query?: string; filter?: PendingPanelFilter }
|
|
773
|
+
| { kind: 'filesystem'; action: string; uri?: string; query?: string; filter?: PendingPanelFilter; source?: string }
|
|
760
774
|
| undefined;
|
|
761
775
|
|
|
762
776
|
|
|
@@ -837,9 +851,11 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
837
851
|
refreshTree: (params: { agentId: string; projectId: string | null }) => Promise<void>;
|
|
838
852
|
/**
|
|
839
853
|
* The dock broom's reach into this store (host → kernel port
|
|
840
|
-
* `onClientStateReset` → here). Matches the scope
|
|
841
|
-
* `(activeProjectId, activeAgentId)` pair
|
|
842
|
-
* `
|
|
854
|
+
* `onClientStateReset` → `resetFilesClientState` → here). Matches the scope
|
|
855
|
+
* against the ACTIVE `(activeProjectId, activeAgentId)` pair; the `instance`
|
|
856
|
+
* and `widget` levels also need the `filesystem` widget type AND this store's
|
|
857
|
+
* own `windowId` — one window's clean or close never empties a sibling
|
|
858
|
+
* window, of the same agent included. A non-matching scope is a no-op. A match applies
|
|
843
859
|
* the ONE `contextResetPatch` (the same wipe an agent/project switch does),
|
|
844
860
|
* clears the persisted tab strip (`tabMeta` included — the allowlist is written
|
|
845
861
|
* EMPTY, so a persisted `activeFile` cannot re-run the read/list race on the
|
|
@@ -856,6 +872,18 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
856
872
|
* through `get()` — never a second copy.
|
|
857
873
|
*/
|
|
858
874
|
fenceInFlightWriters: () => void;
|
|
875
|
+
/**
|
|
876
|
+
* The host's id of the Files window this store belongs to (`null` for a store
|
|
877
|
+
* no window owns). Identity like `activeAgentId`: no reset touches it, and it
|
|
878
|
+
* is never an authority source — it only addresses an `instance`/`widget` reset.
|
|
879
|
+
*/
|
|
880
|
+
windowId: string | null;
|
|
881
|
+
/**
|
|
882
|
+
* The request client every API call of this store goes through — created with
|
|
883
|
+
* the store over the window's project and BOUND agent, so a request's headers
|
|
884
|
+
* never come from another mounted window (`api/index.ts`).
|
|
885
|
+
*/
|
|
886
|
+
client: PackageApi;
|
|
859
887
|
/** Dismiss the structural-operation banner (`organizeError`). */
|
|
860
888
|
clearOrganizeError: () => void;
|
|
861
889
|
/**
|
|
@@ -897,6 +925,14 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
897
925
|
*/
|
|
898
926
|
pendingSourceSuggestion: DiscoverableSourceSuggestion | null;
|
|
899
927
|
setPendingSourceSuggestion: (suggestion: DiscoverableSourceSuggestion | null) => void;
|
|
928
|
+
/**
|
|
929
|
+
* Is the Sources panel's Vector Health section open? In the STORE so a
|
|
930
|
+
* notification's `open-vector-health` nav can open it while the panel is not
|
|
931
|
+
* mounted; context data (a `contextResetPatch` key), never persisted. The
|
|
932
|
+
* section still mounts its panel only while open (no CSS-only collapse).
|
|
933
|
+
*/
|
|
934
|
+
vectorHealthOpen: boolean;
|
|
935
|
+
setVectorHealthOpen: (open: boolean) => void;
|
|
900
936
|
setWorkspacePanelOpen: (panel: WorkspacePanelId, open: boolean) => void;
|
|
901
937
|
setActiveWorkspaceTab: (tab: 'file' | 'git-diff' | 'git-commit' | WorkspacePanelId) => void;
|
|
902
938
|
diffViewMode: DiffViewMode;
|
|
@@ -1070,7 +1106,11 @@ function readStoredSidebarWidth(): number {
|
|
|
1070
1106
|
}
|
|
1071
1107
|
}
|
|
1072
1108
|
|
|
1073
|
-
/**
|
|
1109
|
+
/**
|
|
1110
|
+
* The localStorage payload (v2): prefs, the active triple, the per-agent context
|
|
1111
|
+
* projections, and — OPTIONAL, absent until a window writes one — the per-window
|
|
1112
|
+
* projections keyed by the host's window id (`PersistedFilesWindow`).
|
|
1113
|
+
*/
|
|
1074
1114
|
type PersistedFilesShape = {
|
|
1075
1115
|
previewEditorsEnabled: boolean;
|
|
1076
1116
|
diffViewMode: DiffViewMode;
|
|
@@ -1078,9 +1118,39 @@ type PersistedFilesShape = {
|
|
|
1078
1118
|
activeAgentId: string | null;
|
|
1079
1119
|
activeProjectId: string | null;
|
|
1080
1120
|
contexts: Record<string, PersistedFilesContext>;
|
|
1121
|
+
windows?: Record<string, PersistedFilesWindow>;
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
/** What a new store starts from: its window, its request client, the preferences and the seeded view. */
|
|
1125
|
+
export type FilesystemStoreInit = {
|
|
1126
|
+
/** The request client every API call of this store goes through (see `FilesystemStore.client`). */
|
|
1127
|
+
client: PackageApi;
|
|
1128
|
+
windowId?: string | null;
|
|
1129
|
+
userId?: string | null;
|
|
1130
|
+
projectId?: string | null;
|
|
1131
|
+
agentId?: string | null;
|
|
1132
|
+
previewEditorsEnabled?: boolean;
|
|
1133
|
+
diffViewMode?: DiffViewMode;
|
|
1134
|
+
/** The window's persisted view — its own projection (a new window has none and starts from the defaults). */
|
|
1135
|
+
projection?: Partial<PersistedFilesContext>;
|
|
1081
1136
|
};
|
|
1082
1137
|
|
|
1083
|
-
export
|
|
1138
|
+
export type FilesystemStoreApi = ZustandStoreApi<FilesystemStore>;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* ONE Files store per window: a live view that persists nothing itself — the
|
|
1142
|
+
* ONE writer of `neuralis.filesystem.tabs` is `filesPersistence` below, which a
|
|
1143
|
+
* mounted window feeds through `mountFilesWindow`. Every asynchronous fence and
|
|
1144
|
+
* dedup map is created with the store (`StoreFences`, the list/search/git slice
|
|
1145
|
+
* closures), so two windows — of two agents, or of the same agent — never fence,
|
|
1146
|
+
* supersede or dedup each other.
|
|
1147
|
+
*/
|
|
1148
|
+
export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStoreApi {
|
|
1149
|
+
const fences = createStoreFences();
|
|
1150
|
+
const client = init.client;
|
|
1151
|
+
return createStore<FilesystemStore>()((set, get) => ({
|
|
1152
|
+
windowId: init.windowId ?? null,
|
|
1153
|
+
client,
|
|
1084
1154
|
files: {},
|
|
1085
1155
|
folderBadges: {},
|
|
1086
1156
|
fileContentByPath: {},
|
|
@@ -1090,17 +1160,17 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1090
1160
|
openFiles: [],
|
|
1091
1161
|
activeFile: null,
|
|
1092
1162
|
tabMeta: {},
|
|
1093
|
-
activeUserId: null,
|
|
1163
|
+
activeUserId: init.userId ?? null,
|
|
1094
1164
|
contextStateByKey: {},
|
|
1095
|
-
previewEditorsEnabled: false,
|
|
1165
|
+
previewEditorsEnabled: init.previewEditorsEnabled ?? false,
|
|
1096
1166
|
// Nothing is pre-expanded — under lazy hydration an expanded entry is a FETCH,
|
|
1097
1167
|
// and every source branch (including `brain://`) starts collapsed.
|
|
1098
1168
|
expandedFolders: new Set<string>(),
|
|
1099
1169
|
sidebarOpen: true,
|
|
1100
1170
|
sidebarWidth: readStoredSidebarWidth(),
|
|
1101
1171
|
sidebarView: 'tree',
|
|
1102
|
-
activeAgentId: null,
|
|
1103
|
-
activeProjectId: null,
|
|
1172
|
+
activeAgentId: init.agentId ?? null,
|
|
1173
|
+
activeProjectId: init.projectId ?? null,
|
|
1104
1174
|
sources: [],
|
|
1105
1175
|
sourcesLoading: true,
|
|
1106
1176
|
activeSource: null,
|
|
@@ -1110,7 +1180,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1110
1180
|
workspacePanels: { upload: false, sources: false, packages: false, pending: false },
|
|
1111
1181
|
activeWorkspaceTab: 'file',
|
|
1112
1182
|
pendingSourceSuggestion: null,
|
|
1113
|
-
|
|
1183
|
+
vectorHealthOpen: false,
|
|
1184
|
+
diffViewMode: init.diffViewMode ?? 'auto',
|
|
1114
1185
|
pendingPanelFilter: {},
|
|
1115
1186
|
// Nothing is hydrated at boot. `brain://` is EXPANDED by default (below) and the
|
|
1116
1187
|
// tree's hydration effect lists it — marking it "loaded" here would suppress that
|
|
@@ -1180,6 +1251,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1180
1251
|
return { workspacePanels: nextPanels, activeWorkspaceTab };
|
|
1181
1252
|
}),
|
|
1182
1253
|
setPendingSourceSuggestion: (suggestion) => set(() => ({ pendingSourceSuggestion: suggestion })),
|
|
1254
|
+
setVectorHealthOpen: (open) => set(() => ({ vectorHealthOpen: open })),
|
|
1183
1255
|
setActiveWorkspaceTab: (tab) => set(() => ({ activeWorkspaceTab: tab })),
|
|
1184
1256
|
setDiffViewMode: (mode) => set(() => ({ diffViewMode: mode })),
|
|
1185
1257
|
setPreviewEditorsEnabled: (enabled) => set(() => ({ previewEditorsEnabled: enabled })),
|
|
@@ -1259,28 +1331,28 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1259
1331
|
// Enumerated once, then hidden — no reconcile listed it off screen, so its rows
|
|
1260
1332
|
// may be stale. ONE authoritative listing of exactly this folder (absence
|
|
1261
1333
|
// applies), never a re-list of the whole visible tree.
|
|
1262
|
-
await runSourceReconcile({ get, set }, { agentId, projectId, source: sourceFromUri(uri), targets: [uri], wide: false });
|
|
1334
|
+
await runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source: sourceFromUri(uri), targets: [uri], wide: false });
|
|
1263
1335
|
return;
|
|
1264
1336
|
}
|
|
1265
1337
|
|
|
1266
|
-
const existing = liveFolderLoadInFlight.get(uri);
|
|
1338
|
+
const existing = fences.liveFolderLoadInFlight.get(uri);
|
|
1267
1339
|
if (existing) { await existing; return; }
|
|
1268
1340
|
|
|
1269
1341
|
set((state: any) => ({ hydrationState: { ...(state.hydrationState || {}), [uri]: 'loading' as const } }));
|
|
1270
1342
|
|
|
1271
1343
|
const task = (async () => {
|
|
1272
|
-
const gen = nextFolderGeneration(uri);
|
|
1344
|
+
const gen = nextFolderGeneration(fences, uri);
|
|
1273
1345
|
const issuedAt = Date.now();
|
|
1274
1346
|
try {
|
|
1275
1347
|
// recursive:false → single `readdir` on the connector, ~50ms even on
|
|
1276
1348
|
// million-file subtrees. Replaces the previous full-tree snapshot call.
|
|
1277
|
-
const res = await resolveList(agentId, projectId ?? undefined, { uri, recursive: false });
|
|
1349
|
+
const res = await resolveList(agentId, projectId ?? undefined, { uri, recursive: false }, client);
|
|
1278
1350
|
// Superseded by a newer listing of the same folder (a concurrent reconcile
|
|
1279
1351
|
// or a second expand) → drop this response instead of overwriting. Restore
|
|
1280
1352
|
// the fetch state on the way out: returning bare left `loading` pinned
|
|
1281
1353
|
// forever, and the refresh control is `disabled={loading}` — the one control
|
|
1282
1354
|
// that could recover would have been disabled by the state it must clear.
|
|
1283
|
-
if (!isCurrentFolderGeneration(uri, gen)) {
|
|
1355
|
+
if (!isCurrentFolderGeneration(fences, uri, gen)) {
|
|
1284
1356
|
set((state: any) => ({
|
|
1285
1357
|
hydrationState: {
|
|
1286
1358
|
...(state.hydrationState || {}),
|
|
@@ -1317,8 +1389,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1317
1389
|
}
|
|
1318
1390
|
})();
|
|
1319
1391
|
|
|
1320
|
-
liveFolderLoadInFlight.set(uri, task);
|
|
1321
|
-
try { await task; } finally { liveFolderLoadInFlight.delete(uri); }
|
|
1392
|
+
fences.liveFolderLoadInFlight.set(uri, task);
|
|
1393
|
+
try { await task; } finally { fences.liveFolderLoadInFlight.delete(uri); }
|
|
1322
1394
|
},
|
|
1323
1395
|
|
|
1324
1396
|
clearOrganizeError: () => set(() => ({ organizeError: null })),
|
|
@@ -1335,31 +1407,32 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1335
1407
|
}),
|
|
1336
1408
|
|
|
1337
1409
|
async reconcileSourceFolders({ agentId, projectId, source }) {
|
|
1338
|
-
await runSourceReconcile({ get, set }, { agentId, projectId, source });
|
|
1410
|
+
await runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source });
|
|
1339
1411
|
},
|
|
1340
1412
|
|
|
1341
1413
|
fenceInFlightWriters() {
|
|
1342
|
-
resetEpoch += 1;
|
|
1343
|
-
for (const key of Array.from(folderGeneration.keys())) nextFolderGeneration(key);
|
|
1414
|
+
fences.resetEpoch += 1;
|
|
1415
|
+
for (const key of Array.from(fences.folderGeneration.keys())) nextFolderGeneration(fences, key);
|
|
1344
1416
|
// The dedup maps only; the in-flight tasks' own `finally` deletes a missing
|
|
1345
1417
|
// key as a no-op. `reconcileJobs` is deliberately NOT cleared: a running job's
|
|
1346
1418
|
// `finally` would then delete the NEXT job's entry and two passes would run
|
|
1347
1419
|
// concurrently — the job runs to completion and the epoch makes it inert.
|
|
1348
|
-
liveFolderLoadInFlight.clear();
|
|
1349
|
-
rehydrateInFlight.clear();
|
|
1420
|
+
fences.liveFolderLoadInFlight.clear();
|
|
1421
|
+
fences.rehydrateInFlight.clear();
|
|
1350
1422
|
},
|
|
1351
1423
|
|
|
1352
1424
|
resetClientState(scope) {
|
|
1353
1425
|
const s = get();
|
|
1354
1426
|
if (!s.activeProjectId || scope.projectId !== s.activeProjectId) return;
|
|
1355
1427
|
if (scope.level !== 'project' && scope.agentId !== s.activeAgentId) return;
|
|
1356
|
-
if (scope.level === 'widget'
|
|
1428
|
+
if ((scope.level === 'widget' || scope.level === 'instance')
|
|
1429
|
+
&& (scope.widgetType !== FILESYSTEM_WIDGET_TYPE || scope.instanceId !== s.windowId)) return;
|
|
1357
1430
|
// Fence FIRST, then wipe: a read/listing/reconcile that resolves after this
|
|
1358
1431
|
// point finds a stale epoch (or a stale folder generation) and writes nothing.
|
|
1359
1432
|
get().fenceInFlightWriters();
|
|
1360
1433
|
// The broom is the ONE writer that drops a stashed context — its own scope
|
|
1361
|
-
// only: the widget/agent levels drop this context, the project level
|
|
1362
|
-
// context of the project (this user's). Other projects are not its business.
|
|
1434
|
+
// only: the instance/widget/agent levels drop this context, the project level
|
|
1435
|
+
// every context of the project (this user's). Other projects are not its business.
|
|
1363
1436
|
const prefix = `${s.activeUserId ?? ''}::${s.activeProjectId}::`;
|
|
1364
1437
|
const own = contextKey(s.activeUserId, s.activeProjectId, s.activeAgentId ?? '');
|
|
1365
1438
|
const kept: Record<string, Record<string, unknown>> = {};
|
|
@@ -1398,7 +1471,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1398
1471
|
if (bySource.size === 0) return;
|
|
1399
1472
|
await Promise.all(
|
|
1400
1473
|
Array.from(bySource.entries()).map(([source, targets]) =>
|
|
1401
|
-
runSourceReconcile({ get, set }, { agentId, projectId, source, targets }),
|
|
1474
|
+
runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source, targets }),
|
|
1402
1475
|
),
|
|
1403
1476
|
);
|
|
1404
1477
|
},
|
|
@@ -1420,7 +1493,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1420
1493
|
if (source) sources.add(source);
|
|
1421
1494
|
}
|
|
1422
1495
|
await Promise.all(
|
|
1423
|
-
Array.from(sources).map((source) => runSourceReconcile({ get, set }, { agentId, projectId, source })),
|
|
1496
|
+
Array.from(sources).map((source) => runSourceReconcile({ get, set, fences, client }, { agentId, projectId, source })),
|
|
1424
1497
|
);
|
|
1425
1498
|
},
|
|
1426
1499
|
|
|
@@ -1435,8 +1508,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1435
1508
|
const agentId = s.activeAgentId;
|
|
1436
1509
|
if (!uri || !agentId) return;
|
|
1437
1510
|
if (hasCompletedRead(s, uri)) return;
|
|
1438
|
-
if (rehydrateInFlight.has(uri)) return;
|
|
1439
|
-
rehydrateInFlight.add(uri);
|
|
1511
|
+
if (fences.rehydrateInFlight.has(uri)) return;
|
|
1512
|
+
fences.rehydrateInFlight.add(uri);
|
|
1440
1513
|
try {
|
|
1441
1514
|
// A tab from another agent's source reads through that agent, as the tree
|
|
1442
1515
|
// open did — never through a SENTINEL (`__system__` is the writer the
|
|
@@ -1449,7 +1522,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1449
1522
|
// rehydrate is not a user open.
|
|
1450
1523
|
await s.openFile({ agentId, projectId: s.activeProjectId, uri, readAgentId, revealPane: false });
|
|
1451
1524
|
} finally {
|
|
1452
|
-
rehydrateInFlight.delete(uri);
|
|
1525
|
+
fences.rehydrateInFlight.delete(uri);
|
|
1453
1526
|
}
|
|
1454
1527
|
},
|
|
1455
1528
|
|
|
@@ -1669,8 +1742,8 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1669
1742
|
// The server-side work completes whatever happens to the store; every store
|
|
1670
1743
|
// write after an await is fenced — a context reset mid-save must not re-key,
|
|
1671
1744
|
// fill, dirty-flag or re-open anything under the new context.
|
|
1672
|
-
const epoch = resetEpoch;
|
|
1673
|
-
const fenced = () => epoch !== resetEpoch;
|
|
1745
|
+
const epoch = fences.resetEpoch;
|
|
1746
|
+
const fenced = () => epoch !== fences.resetEpoch;
|
|
1674
1747
|
|
|
1675
1748
|
let effectiveUri = fromUri;
|
|
1676
1749
|
if (moved) {
|
|
@@ -1680,7 +1753,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1680
1753
|
id: node.id,
|
|
1681
1754
|
uri: fromUri,
|
|
1682
1755
|
move: targetUri,
|
|
1683
|
-
});
|
|
1756
|
+
}, client);
|
|
1684
1757
|
if (!res || res.ok !== true) throw new Error(writeRefusalMessage(res, 'Move failed'));
|
|
1685
1758
|
effectiveUri = targetUri;
|
|
1686
1759
|
if (fenced()) return { uri: effectiveUri, moved, contentChanged: false };
|
|
@@ -1694,7 +1767,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1694
1767
|
id: node.id,
|
|
1695
1768
|
uri: effectiveUri,
|
|
1696
1769
|
overwrite: { content },
|
|
1697
|
-
});
|
|
1770
|
+
}, client);
|
|
1698
1771
|
if (!res || res.ok !== true) throw new Error(writeRefusalMessage(res, 'Save failed'));
|
|
1699
1772
|
if (fenced()) return { uri: effectiveUri, moved, contentChanged };
|
|
1700
1773
|
// After a move the node lives under its NEW uri (`replaceFilePath` re-keys
|
|
@@ -1716,32 +1789,32 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1716
1789
|
|
|
1717
1790
|
async refreshPendingCount({ agentId, projectId }) {
|
|
1718
1791
|
if (!agentId) return;
|
|
1719
|
-
const mine = ++pendingCountSeq;
|
|
1792
|
+
const mine = ++fences.pendingCountSeq;
|
|
1720
1793
|
// A failed count keeps the last known number — the next event, focus or reconnect re-asks.
|
|
1721
1794
|
let count: number;
|
|
1722
|
-
try { count = await countPendingChanges({ agentId, ...(projectId ? { projectId } : {}) }); } catch { return; }
|
|
1795
|
+
try { count = await countPendingChanges({ agentId, ...(projectId ? { projectId } : {}) }, client); } catch { return; }
|
|
1723
1796
|
// No reset-epoch fence: the count is the context's own server answer, so one
|
|
1724
1797
|
// that resolves after a reset of the SAME context is still right; the context
|
|
1725
1798
|
// check below is what keeps another context's number out.
|
|
1726
|
-
if (mine !== pendingCountSeq) return;
|
|
1799
|
+
if (mine !== fences.pendingCountSeq) return;
|
|
1727
1800
|
const s = get();
|
|
1728
1801
|
if (s.activeAgentId !== agentId || s.activeProjectId !== projectId) return;
|
|
1729
1802
|
set(() => ({ pendingCount: count }));
|
|
1730
1803
|
},
|
|
1731
1804
|
|
|
1732
1805
|
async markFileDeleted({ agentId, projectId, node }) {
|
|
1733
|
-
const epoch = resetEpoch;
|
|
1806
|
+
const epoch = fences.resetEpoch;
|
|
1734
1807
|
const res = await writeFile({
|
|
1735
1808
|
agentId,
|
|
1736
1809
|
...(projectId ? { projectId } : {}),
|
|
1737
1810
|
id: node.id,
|
|
1738
1811
|
uri: node.uri,
|
|
1739
1812
|
delete: true,
|
|
1740
|
-
});
|
|
1813
|
+
}, client);
|
|
1741
1814
|
if (!res || res.ok !== true) throw new Error(writeRefusalMessage(res, 'Delete failed'));
|
|
1742
1815
|
// The reconcile flags an unloaded source dirty — not a no-op on a store the
|
|
1743
1816
|
// reset just handed to another context.
|
|
1744
|
-
if (epoch !== resetEpoch) return;
|
|
1817
|
+
if (epoch !== fences.resetEpoch) return;
|
|
1745
1818
|
await get().reconcileFolders({ agentId, projectId, folders: [folderUri(node.uri)] });
|
|
1746
1819
|
},
|
|
1747
1820
|
|
|
@@ -1762,7 +1835,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1762
1835
|
id,
|
|
1763
1836
|
uri: fromUri,
|
|
1764
1837
|
move: destinationUri,
|
|
1765
|
-
});
|
|
1838
|
+
}, client);
|
|
1766
1839
|
} catch (e) {
|
|
1767
1840
|
console.error('[Filesystem] move failed', fromUri, e);
|
|
1768
1841
|
set(() => ({ organizeError: 'Move failed' }));
|
|
@@ -1791,7 +1864,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1791
1864
|
// of the old subtree would stay in the store forever. Prune by prefix (the
|
|
1792
1865
|
// pattern `renameNode` already proved) and force-relist the destination.
|
|
1793
1866
|
if (!retained) {
|
|
1794
|
-
prunePrefix({ get, set }, fromUri);
|
|
1867
|
+
prunePrefix({ get, set, fences, client }, fromUri);
|
|
1795
1868
|
}
|
|
1796
1869
|
await get().ensureFolderLoaded({ agentId, projectId, uri: toFolderUri, force: true });
|
|
1797
1870
|
} else {
|
|
@@ -1832,7 +1905,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1832
1905
|
id,
|
|
1833
1906
|
uri,
|
|
1834
1907
|
move: isFolder ? destinationUri : trimmed,
|
|
1835
|
-
});
|
|
1908
|
+
}, client);
|
|
1836
1909
|
} catch (e) {
|
|
1837
1910
|
console.error('[Filesystem] rename failed', uri, e);
|
|
1838
1911
|
set(() => ({ organizeError: 'Rename failed' }));
|
|
@@ -1851,7 +1924,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1851
1924
|
// tombstoned the old one. Prune the stale old subtree from the store and
|
|
1852
1925
|
// force-relist the parent so the renamed folder surfaces immediately (the
|
|
1853
1926
|
// non-recursive `refresh` alone would not re-list a nested folder's parent).
|
|
1854
|
-
prunePrefix({ get, set }, uri);
|
|
1927
|
+
prunePrefix({ get, set, fences, client }, uri);
|
|
1855
1928
|
await get().ensureFolderLoaded({ agentId, projectId, uri: parent, force: true });
|
|
1856
1929
|
} else {
|
|
1857
1930
|
get().replaceFilePath(uri, destinationUri);
|
|
@@ -1863,11 +1936,11 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1863
1936
|
const trimmed = (name ?? '').trim();
|
|
1864
1937
|
if (!agentId || !parentUri || !trimmed) return;
|
|
1865
1938
|
const targetUri = parentUri.endsWith('://') ? `${parentUri}${trimmed}` : `${parentUri}/${trimmed}`;
|
|
1866
|
-
const epoch = resetEpoch;
|
|
1939
|
+
const epoch = fences.resetEpoch;
|
|
1867
1940
|
try {
|
|
1868
1941
|
if (kind === 'folder') {
|
|
1869
|
-
const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: targetUri, mkdir: true });
|
|
1870
|
-
if (res?.ok && epoch === resetEpoch) {
|
|
1942
|
+
const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: targetUri, mkdir: true }, client);
|
|
1943
|
+
if (res?.ok && epoch === fences.resetEpoch) {
|
|
1871
1944
|
// Optimistic folder node — an EMPTY folder has no vector node, so only a
|
|
1872
1945
|
// connector-live re-list surfaces it; seed it now so it appears instantly.
|
|
1873
1946
|
set((state) => {
|
|
@@ -1898,7 +1971,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1898
1971
|
file: new File([''], trimmed),
|
|
1899
1972
|
uri: parentUri,
|
|
1900
1973
|
tags: [],
|
|
1901
|
-
});
|
|
1974
|
+
}, client);
|
|
1902
1975
|
}
|
|
1903
1976
|
} catch (e) {
|
|
1904
1977
|
console.error('[Filesystem] create failed', kind, targetUri, e);
|
|
@@ -1921,11 +1994,11 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1921
1994
|
touched.add(folderUri(src));
|
|
1922
1995
|
try {
|
|
1923
1996
|
if (op === 'copy') {
|
|
1924
|
-
const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, copy: dest });
|
|
1997
|
+
const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, copy: dest }, client);
|
|
1925
1998
|
if (res && res.ok === false) rejected.push(base);
|
|
1926
1999
|
} else {
|
|
1927
2000
|
// 200-with-`ok:false` is a REJECTED move — collect it, never treat it as done.
|
|
1928
|
-
const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, move: dest });
|
|
2001
|
+
const res = await writeFile({ agentId, ...(projectId ? { projectId } : {}), uri: src, move: dest }, client);
|
|
1929
2002
|
if (res && res.ok === false) rejected.push(base);
|
|
1930
2003
|
else if (res?.warning === 'moved_with_source_retained') rejected.push(`${base} (original retained)`);
|
|
1931
2004
|
else if (get().files[src]?.type === 'folder') {
|
|
@@ -1934,7 +2007,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1934
2007
|
// inspects direct children — so the old prefix has to go here or its
|
|
1935
2008
|
// interior lingers. Cut/paste reaches this path too, and only the drag
|
|
1936
2009
|
// path used to handle it.
|
|
1937
|
-
prunePrefix({ get, set }, src);
|
|
2010
|
+
prunePrefix({ get, set, fences, client }, src);
|
|
1938
2011
|
}
|
|
1939
2012
|
}
|
|
1940
2013
|
} catch (e) {
|
|
@@ -1964,7 +2037,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1964
2037
|
id: t.id,
|
|
1965
2038
|
uri: t.uri,
|
|
1966
2039
|
delete: true,
|
|
1967
|
-
});
|
|
2040
|
+
}, client);
|
|
1968
2041
|
if (res && res.ok === false) {
|
|
1969
2042
|
// 200-with-`ok:false`. A FOLDER delete can still have removed N files
|
|
1970
2043
|
// before failing — `affected`/`failed` carry that partial outcome, and
|
|
@@ -1980,7 +2053,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
1980
2053
|
if (isFolder) {
|
|
1981
2054
|
// A folder delete removes the whole subtree server-side; prune by prefix so
|
|
1982
2055
|
// the interior cannot linger (the reconcile only inspects direct children).
|
|
1983
|
-
prunePrefix({ get, set }, t.uri);
|
|
2056
|
+
prunePrefix({ get, set, fences, client }, t.uri);
|
|
1984
2057
|
} else {
|
|
1985
2058
|
// Optimistic: strike the node (revertible tombstone). The targeted
|
|
1986
2059
|
// reconcile below converges to server truth — a tracked node KEEPS the
|
|
@@ -2064,16 +2137,16 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
2064
2137
|
(typeof st.fileContentByPath[uri] === 'string' && (st.fileContentByPath[uri]?.length || 0) > 0);
|
|
2065
2138
|
if (!force && hasContent) return;
|
|
2066
2139
|
|
|
2067
|
-
const epoch = resetEpoch;
|
|
2140
|
+
const epoch = fences.resetEpoch;
|
|
2068
2141
|
try {
|
|
2069
2142
|
const res = await readFiles({
|
|
2070
2143
|
agentId: readAgentId || agentId,
|
|
2071
2144
|
...(projectId ? { projectId } : {}),
|
|
2072
2145
|
files: [uri],
|
|
2073
|
-
});
|
|
2146
|
+
}, client);
|
|
2074
2147
|
// The broom ran while the read was in flight: the tab is gone, the store is
|
|
2075
2148
|
// empty, and this answer must not put either back.
|
|
2076
|
-
if (epoch !== resetEpoch) return;
|
|
2149
|
+
if (epoch !== fences.resetEpoch) return;
|
|
2077
2150
|
const files = extractReadFilesPayload(res);
|
|
2078
2151
|
if (files.length > 0) {
|
|
2079
2152
|
const f = files[0];
|
|
@@ -2183,7 +2256,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
2183
2256
|
} catch (e) {
|
|
2184
2257
|
const message = e instanceof Error ? e.message : 'Read failed';
|
|
2185
2258
|
console.error('[Filesystem] read failed', e);
|
|
2186
|
-
if (epoch !== resetEpoch) return;
|
|
2259
|
+
if (epoch !== fences.resetEpoch) return;
|
|
2187
2260
|
set((state) => ({
|
|
2188
2261
|
files: {
|
|
2189
2262
|
...state.files,
|
|
@@ -2217,6 +2290,15 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
2217
2290
|
get().setWorkspacePanelOpen('pending', true);
|
|
2218
2291
|
return;
|
|
2219
2292
|
}
|
|
2293
|
+
// A notification's target: the Sources panel (a source's sync or embedding
|
|
2294
|
+
// outcome), or its Vector Health section (an operator's collection action).
|
|
2295
|
+
// Navigation only — the panel's own listing re-applies every gate.
|
|
2296
|
+
if (action === 'open-sources' || action === 'open-vector-health') {
|
|
2297
|
+
set(() => ({ activeAgentId: agentId, activeProjectId: projectId,
|
|
2298
|
+
...(action === 'open-vector-health' ? { vectorHealthOpen: true } : {}) }));
|
|
2299
|
+
get().setWorkspacePanelOpen('sources', true);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2220
2302
|
if (action === 'list' && uri) {
|
|
2221
2303
|
set((state) => {
|
|
2222
2304
|
const root = sourceRootUri(uri);
|
|
@@ -2234,7 +2316,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
2234
2316
|
const targetUri = uri || 'brain://docs';
|
|
2235
2317
|
set(() => ({ uploading: true }));
|
|
2236
2318
|
try {
|
|
2237
|
-
await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] });
|
|
2319
|
+
await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] }, client);
|
|
2238
2320
|
await get().reconcileFolders({ agentId, projectId, folders: [targetUri] });
|
|
2239
2321
|
} finally {
|
|
2240
2322
|
set(() => ({ uploading: false }));
|
|
@@ -2249,25 +2331,43 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
2249
2331
|
set(() => ({ uploading: true }));
|
|
2250
2332
|
try {
|
|
2251
2333
|
for (const file of list) {
|
|
2252
|
-
await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] });
|
|
2334
|
+
await uploadFile({ agentId, ...(projectId ? { projectId } : {}), file, uri: targetUri, tags: ['upload'] }, client);
|
|
2253
2335
|
}
|
|
2254
2336
|
await get().reconcileFolders({ agentId, projectId, folders: [targetUri] });
|
|
2255
2337
|
} finally {
|
|
2256
2338
|
set(() => ({ uploading: false }));
|
|
2257
2339
|
}
|
|
2258
2340
|
},
|
|
2341
|
+
// The window's persisted view, lifted into the flat fields — after every
|
|
2342
|
+
// default above, so a seeded tab strip is what the first render shows.
|
|
2343
|
+
...(init.projection ? sliceFromPersisted(init.projection) : {}),
|
|
2344
|
+
}));
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* The ONE writer of `neuralis.filesystem.tabs` (v2). Window stores persist
|
|
2349
|
+
* nothing; a mounted window feeds its projection here (`mountFilesWindow`), so
|
|
2350
|
+
* two windows writing the same key can never overwrite each other's view — the
|
|
2351
|
+
* whole record is written from this one in-memory owner.
|
|
2352
|
+
*
|
|
2353
|
+
* What it holds: the preferences, the legacy per-agent `contexts` (the record an
|
|
2354
|
+
* older build wrote — never read into a window, pruned by the broom, otherwise
|
|
2355
|
+
* left as it is: never rewritten, re-keyed or converted), and the optional
|
|
2356
|
+
* `windows` map — one `PersistedFilesWindow` per window, carrying the
|
|
2357
|
+
* user/project/agent it was written for. A record without `windows` reads
|
|
2358
|
+
* without one and is not written back until a window really changes.
|
|
2359
|
+
*/
|
|
2360
|
+
export const filesPersistence = createStore<PersistedFilesShape>()(persist((): PersistedFilesShape => ({
|
|
2361
|
+
previewEditorsEnabled: false,
|
|
2362
|
+
diffViewMode: 'auto',
|
|
2363
|
+
activeUserId: null,
|
|
2364
|
+
activeAgentId: null,
|
|
2365
|
+
activeProjectId: null,
|
|
2366
|
+
contexts: {},
|
|
2259
2367
|
}), {
|
|
2260
|
-
//
|
|
2261
|
-
//
|
|
2262
|
-
//
|
|
2263
|
-
// projection is taken off the flat fields on every write, the others off
|
|
2264
|
-
// `contextStateByKey`. NEVER the `files`/content dicts or a `Set` as such —
|
|
2265
|
-
// `pendingReviewPaths` is derived from `files` and stays in memory.
|
|
2266
|
-
// `sidebarWidth` keeps its own hand-persist. On rehydrate (`merge`) the active
|
|
2267
|
-
// context's projection lands in the flat fields so `loadList`'s same-context
|
|
2268
|
-
// guard keeps them on a page refresh, and every other context waits in the
|
|
2269
|
-
// space for its switch-back. v2 (2026-09-04): the flat tab trio became the
|
|
2270
|
-
// context space; `migrate` lifts a v1 payload's trio into the active context.
|
|
2368
|
+
// v2 (2026-09-04): the flat tab trio became the context space; `migrate` lifts
|
|
2369
|
+
// a v1 payload's trio into the active context. The optional `windows` map is
|
|
2370
|
+
// additive — no version bump, an absent map stays absent.
|
|
2271
2371
|
name: 'neuralis.filesystem.tabs',
|
|
2272
2372
|
version: 2,
|
|
2273
2373
|
migrate: (persistedState, version) => {
|
|
@@ -2303,44 +2403,236 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
|
|
|
2303
2403
|
merge: (persistedState, currentState) => {
|
|
2304
2404
|
const p = persistedState as Partial<PersistedFilesShape> | undefined;
|
|
2305
2405
|
if (!p) return currentState;
|
|
2306
|
-
const contexts = p.contexts ?? {};
|
|
2307
|
-
const activeKey = p.activeAgentId ? contextKey(p.activeUserId ?? null, p.activeProjectId ?? null, p.activeAgentId) : null;
|
|
2308
|
-
const space: Record<string, Record<string, unknown>> = {};
|
|
2309
|
-
for (const [key, projection] of Object.entries(contexts)) {
|
|
2310
|
-
if (key !== activeKey) space[key] = sliceFromPersisted(projection);
|
|
2311
|
-
}
|
|
2312
|
-
const active = activeKey && contexts[activeKey] ? sliceFromPersisted(contexts[activeKey]) : {};
|
|
2313
2406
|
return {
|
|
2314
|
-
...currentState,
|
|
2315
2407
|
previewEditorsEnabled: p.previewEditorsEnabled ?? currentState.previewEditorsEnabled,
|
|
2316
2408
|
diffViewMode: p.diffViewMode ?? currentState.diffViewMode,
|
|
2317
2409
|
activeUserId: p.activeUserId ?? null,
|
|
2318
2410
|
activeAgentId: p.activeAgentId ?? null,
|
|
2319
2411
|
activeProjectId: p.activeProjectId ?? null,
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
}
|
|
2412
|
+
contexts: p.contexts ?? {},
|
|
2413
|
+
...(p.windows ? { windows: p.windows } : {}),
|
|
2414
|
+
};
|
|
2323
2415
|
},
|
|
2324
2416
|
storage: createJSONStorage(() =>
|
|
2325
2417
|
typeof window !== 'undefined' && window.localStorage
|
|
2326
2418
|
? window.localStorage
|
|
2327
2419
|
: { getItem: () => null, setItem: () => undefined, removeItem: () => undefined },
|
|
2328
2420
|
),
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
activeAgentId: state.activeAgentId,
|
|
2342
|
-
activeProjectId: state.activeProjectId,
|
|
2343
|
-
contexts,
|
|
2344
|
-
};
|
|
2345
|
-
},
|
|
2421
|
+
// NEVER the `files`/content dicts or a `Set` as such — every projection is a
|
|
2422
|
+
// `persistedContext` allowlist (Sets as arrays). `sidebarWidth` keeps its own
|
|
2423
|
+
// hand-persist.
|
|
2424
|
+
partialize: (state): PersistedFilesShape => ({
|
|
2425
|
+
previewEditorsEnabled: state.previewEditorsEnabled,
|
|
2426
|
+
diffViewMode: state.diffViewMode,
|
|
2427
|
+
activeUserId: state.activeUserId,
|
|
2428
|
+
activeAgentId: state.activeAgentId,
|
|
2429
|
+
activeProjectId: state.activeProjectId,
|
|
2430
|
+
contexts: state.contexts,
|
|
2431
|
+
...(state.windows ? { windows: state.windows } : {}),
|
|
2432
|
+
}),
|
|
2346
2433
|
}));
|
|
2434
|
+
|
|
2435
|
+
/** The live identity of one Files window: the host's window id, the signed-in user, the project and the BOUND agent. */
|
|
2436
|
+
export type FilesWindowIdentity = { windowId: string; userId: string; projectId: string; agentId: string };
|
|
2437
|
+
|
|
2438
|
+
function sameWindowIdentity(entry: PersistedFilesWindow, id: FilesWindowIdentity): boolean {
|
|
2439
|
+
return entry.userId === id.userId && entry.projectId === id.projectId && entry.agentId === id.agentId;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/** A window projection without its identity fields — the shape `sliceFromPersisted` reads. */
|
|
2443
|
+
function viewOf(entry: PersistedFilesWindow): PersistedFilesContext {
|
|
2444
|
+
const { userId: _u, projectId: _p, agentId: _a, ...view } = entry;
|
|
2445
|
+
return view;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* A new window store. Its view comes from the window's OWN projection when the
|
|
2450
|
+
* record holds one written for this LIVE identity; a projection written for
|
|
2451
|
+
* another user, project or agent is ignored, and a window with no projection
|
|
2452
|
+
* of its own starts from the defaults. The older per-agent `contexts` entries
|
|
2453
|
+
* are never read into a window (no ID translation from an agent key to a
|
|
2454
|
+
* window id): an upgrade resets the open tabs once.
|
|
2455
|
+
*/
|
|
2456
|
+
export function createFilesWindowStore(id: FilesWindowIdentity): FilesystemStoreApi {
|
|
2457
|
+
const record = filesPersistence.getState();
|
|
2458
|
+
const own = record.windows?.[id.windowId];
|
|
2459
|
+
const projection = own && sameWindowIdentity(own, id) ? viewOf(own) : undefined;
|
|
2460
|
+
return createFilesystemStore({
|
|
2461
|
+
client: createPackageApi({ slug: 'brain-core', getProjectId: () => id.projectId, getAgentId: () => id.agentId }),
|
|
2462
|
+
windowId: id.windowId,
|
|
2463
|
+
userId: id.userId,
|
|
2464
|
+
projectId: id.projectId,
|
|
2465
|
+
agentId: id.agentId,
|
|
2466
|
+
previewEditorsEnabled: record.previewEditorsEnabled,
|
|
2467
|
+
diffViewMode: record.diffViewMode,
|
|
2468
|
+
...(projection ? { projection } : {}),
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/** The fields `persistedContext` reads — a store update that changed none of them changed no projection. */
|
|
2473
|
+
const PROJECTION_INPUTS = [
|
|
2474
|
+
'openFiles', 'activeFile', 'tabMeta', 'expandedFolders',
|
|
2475
|
+
'sidebarView', 'activeWorkspaceTab', 'workspacePanels', 'activeFolderPath',
|
|
2476
|
+
] as const;
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Feed ONE window's view into the persistence owner while it is mounted. Per
|
|
2480
|
+
* store update: O(1) — eight reference compares; only an update that touched a
|
|
2481
|
+
* projection input serializes the projection, and only a projection that
|
|
2482
|
+
* differs from the last one written reaches the owner (which serializes the
|
|
2483
|
+
* whole allowlisted record once). Returns the unsubscribe.
|
|
2484
|
+
*/
|
|
2485
|
+
function bindWindowPersistence(store: FilesystemStoreApi, id: FilesWindowIdentity): () => void {
|
|
2486
|
+
let last = JSON.stringify(persistedContext(store.getState() as unknown as Record<string, unknown>));
|
|
2487
|
+
return store.subscribe((state, prev) => {
|
|
2488
|
+
if (state.previewEditorsEnabled !== prev.previewEditorsEnabled || state.diffViewMode !== prev.diffViewMode) {
|
|
2489
|
+
const owner = filesPersistence.getState();
|
|
2490
|
+
if (owner.previewEditorsEnabled !== state.previewEditorsEnabled || owner.diffViewMode !== state.diffViewMode) {
|
|
2491
|
+
filesPersistence.setState({ previewEditorsEnabled: state.previewEditorsEnabled, diffViewMode: state.diffViewMode });
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (!PROJECTION_INPUTS.some((key) => state[key] !== prev[key])) return;
|
|
2495
|
+
// The view belongs to the window's own identity; a store that left it writes nothing.
|
|
2496
|
+
if (state.activeUserId !== id.userId || state.activeProjectId !== id.projectId || state.activeAgentId !== id.agentId) return;
|
|
2497
|
+
const view = persistedContext(state as unknown as Record<string, unknown>);
|
|
2498
|
+
const json = JSON.stringify(view);
|
|
2499
|
+
if (json === last) return;
|
|
2500
|
+
last = json;
|
|
2501
|
+
filesPersistence.setState((owner) => ({
|
|
2502
|
+
windows: {
|
|
2503
|
+
...(owner.windows ?? {}),
|
|
2504
|
+
[id.windowId]: { userId: id.userId, projectId: id.projectId, agentId: id.agentId, ...view },
|
|
2505
|
+
},
|
|
2506
|
+
}));
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
type LiveFilesWindow = { identity: FilesWindowIdentity; store: FilesystemStoreApi };
|
|
2511
|
+
|
|
2512
|
+
/** Every mounted Files window — hidden and minimized ones included (they stay mounted). */
|
|
2513
|
+
const liveFilesWindows = new Set<LiveFilesWindow>();
|
|
2514
|
+
|
|
2515
|
+
/** The signed-in user, from the workspace provider — which user's records a broom prunes. */
|
|
2516
|
+
let liveFilesUserId: string | null = null;
|
|
2517
|
+
|
|
2518
|
+
export function setFilesLiveUser(userId: string | null): void {
|
|
2519
|
+
liveFilesUserId = userId;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
* Register a window store while it is mounted: the reset fan-out reaches it and
|
|
2524
|
+
* its view feeds the persistence owner. The disposer drops the subscription and
|
|
2525
|
+
* fences the store's in-flight readers — the persisted view stays (closing or
|
|
2526
|
+
* hiding a window is not a reset).
|
|
2527
|
+
*/
|
|
2528
|
+
export function mountFilesWindow(store: FilesystemStoreApi, identity: FilesWindowIdentity): () => void {
|
|
2529
|
+
const entry: LiveFilesWindow = { identity, store };
|
|
2530
|
+
liveFilesWindows.add(entry);
|
|
2531
|
+
const unbind = bindWindowPersistence(store, identity);
|
|
2532
|
+
return () => {
|
|
2533
|
+
unbind();
|
|
2534
|
+
liveFilesWindows.delete(entry);
|
|
2535
|
+
store.getState().fenceInFlightWriters();
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function scopeReachesWindow(scope: ClientStateResetScope, id: FilesWindowIdentity): boolean {
|
|
2540
|
+
if (scope.projectId !== id.projectId) return false;
|
|
2541
|
+
if (scope.level === 'project') return true;
|
|
2542
|
+
if (scope.agentId !== id.agentId) return false;
|
|
2543
|
+
return scope.level === 'agent' || scope.instanceId === id.windowId;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
/**
|
|
2547
|
+
* The persisted half of a broom, applied to the ONE record: `instance` drops that
|
|
2548
|
+
* window's projection; `widget` drops it AND the legacy (user, project, agent)
|
|
2549
|
+
* context — the type-clean meaning that level always had; `agent` / `project`
|
|
2550
|
+
* drop the legacy contexts and the window projections of their scope. Only the
|
|
2551
|
+
* signed-in user's records; another user's, project's or agent's stay.
|
|
2552
|
+
*/
|
|
2553
|
+
function resetPersistedFiles(scope: ClientStateResetScope, userId: string | null): void {
|
|
2554
|
+
if ((scope.level === 'instance' || scope.level === 'widget') && scope.widgetType !== FILESYSTEM_WIDGET_TYPE) return;
|
|
2555
|
+
const record = filesPersistence.getState();
|
|
2556
|
+
const dropWindow = (windowId: string, entry: PersistedFilesWindow): boolean => {
|
|
2557
|
+
if (entry.projectId !== scope.projectId) return false;
|
|
2558
|
+
if (scope.level === 'instance' || scope.level === 'widget') return windowId === scope.instanceId && entry.agentId === scope.agentId;
|
|
2559
|
+
if (userId === null || entry.userId !== userId) return false;
|
|
2560
|
+
return scope.level === 'project' || entry.agentId === scope.agentId;
|
|
2561
|
+
};
|
|
2562
|
+
const dropContext = (key: string): boolean => {
|
|
2563
|
+
if (scope.level === 'instance') return false;
|
|
2564
|
+
if (scope.level === 'project') return key.startsWith(`${userId ?? ''}::${scope.projectId}::`);
|
|
2565
|
+
return key === contextKey(userId, scope.projectId, scope.agentId);
|
|
2566
|
+
};
|
|
2567
|
+
let windows = record.windows;
|
|
2568
|
+
for (const [windowId, entry] of Object.entries(record.windows ?? {})) {
|
|
2569
|
+
if (!dropWindow(windowId, entry)) continue;
|
|
2570
|
+
if (windows === record.windows) windows = { ...record.windows };
|
|
2571
|
+
delete windows![windowId];
|
|
2572
|
+
}
|
|
2573
|
+
let contexts = record.contexts;
|
|
2574
|
+
for (const key of Object.keys(record.contexts)) {
|
|
2575
|
+
if (!dropContext(key)) continue;
|
|
2576
|
+
if (contexts === record.contexts) contexts = { ...record.contexts };
|
|
2577
|
+
delete contexts[key];
|
|
2578
|
+
}
|
|
2579
|
+
if (windows === record.windows && contexts === record.contexts) return;
|
|
2580
|
+
filesPersistence.setState({ contexts, ...(windows ? { windows } : {}) });
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* The ONE handler behind the install-level `port.onClientStateReset`
|
|
2585
|
+
* subscription: the matching live window stores first (each wipe re-feeds its
|
|
2586
|
+
* own, now empty, view), then the persisted record — so an `instance` / `widget`
|
|
2587
|
+
* reset leaves that window's projection absent and a sibling's untouched.
|
|
2588
|
+
* O(mounted Files windows) per broom event.
|
|
2589
|
+
*/
|
|
2590
|
+
export function resetFilesClientState(scope: ClientStateResetScope): void {
|
|
2591
|
+
for (const live of liveFilesWindows) {
|
|
2592
|
+
if (scopeReachesWindow(scope, live.identity)) live.store.getState().resetClientState(scope);
|
|
2593
|
+
}
|
|
2594
|
+
resetPersistedFiles(scope, liveFilesUserId);
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/**
|
|
2598
|
+
* The store of the Files window being rendered: created once per LIVE identity
|
|
2599
|
+
* (window, user, project, bound agent) — a different user or project is a
|
|
2600
|
+
* different store, never a re-used one — and mounted for the window's lifetime.
|
|
2601
|
+
* `null` until the identity is known: nothing persisted is read, and nothing
|
|
2602
|
+
* paints, before the live user and project are.
|
|
2603
|
+
*/
|
|
2604
|
+
export function useFilesWindowStore(
|
|
2605
|
+
windowId: string | null | undefined,
|
|
2606
|
+
userId: string | null | undefined,
|
|
2607
|
+
projectId: string | null | undefined,
|
|
2608
|
+
agentId: string | null | undefined,
|
|
2609
|
+
): FilesystemStoreApi | null {
|
|
2610
|
+
const live = useMemo(() => {
|
|
2611
|
+
if (!windowId || !userId || !projectId || !agentId) return null;
|
|
2612
|
+
const identity: FilesWindowIdentity = { windowId, userId, projectId, agentId };
|
|
2613
|
+
return { identity, store: createFilesWindowStore(identity) };
|
|
2614
|
+
}, [windowId, userId, projectId, agentId]);
|
|
2615
|
+
// Layout phase: registered (and its persistence bound) before any child's
|
|
2616
|
+
// passive effect issues the first request; disposed before the next paint.
|
|
2617
|
+
useLayoutEffect(() => (live ? mountFilesWindow(live.store, live.identity) : undefined), [live]);
|
|
2618
|
+
return live?.store ?? null;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
const FilesystemStoreContext = createContext<FilesystemStoreApi | null>(null);
|
|
2622
|
+
|
|
2623
|
+
/** Puts a window's store under its Files tree; every `useFilesystemStore` below reads THIS window's store. */
|
|
2624
|
+
export function FilesystemStoreProvider({ store, children }: { store: FilesystemStoreApi; children?: ReactNode }) {
|
|
2625
|
+
return createElement(FilesystemStoreContext.Provider, { value: store }, children);
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
/** The window's store itself, for imperative reads/writes outside a selector (event handlers, the SSE hook). */
|
|
2629
|
+
export function useFilesystemStoreApi(): FilesystemStoreApi {
|
|
2630
|
+
const store = useContext(FilesystemStoreContext);
|
|
2631
|
+
if (!store) throw new Error('useFilesystemStore must be used inside a Files window (FilesystemStoreProvider)');
|
|
2632
|
+
return store;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/** Select from the store of the Files window this component renders in. */
|
|
2636
|
+
export function useFilesystemStore<T>(selector: (state: FilesystemStore) => T): T {
|
|
2637
|
+
return useStore(useFilesystemStoreApi(), selector);
|
|
2638
|
+
}
|