@neuralis/brain-core 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -1417
- package/app/README.md +4 -4
- package/app/cards/FilesystemCardShell.tsx +42 -4
- package/app/cards/FilesystemChangeCard.tsx +34 -11
- package/app/cards/README.md +6 -5
- package/app/cards/diffUtils.ts +99 -1
- package/app/filesystem/FilesystemHost.tsx +24 -2
- package/app/filesystem/README.md +1 -1
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +38 -14
- package/app/filesystem/api/projectPackages.ts +13 -102
- package/app/filesystem/domain/types.ts +30 -10
- package/app/filesystem/hooks/README.md +4 -4
- package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
- package/app/filesystem/hooks/useFilesystemSSE.ts +42 -2
- package/app/filesystem/host/README.md +6 -4
- package/app/filesystem/host/buildFilesystemHostValue.ts +28 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
- package/app/filesystem/lib/sourceKindForm.ts +210 -27
- package/app/filesystem/lib/treeVisualState.tsx +98 -4
- package/app/filesystem/store/README.md +1 -0
- package/app/filesystem/store/filesystemStore.ts +99 -8
- package/app/filesystem/store/gitSlice.ts +5 -0
- package/app/filesystem/store/helpers.ts +17 -0
- package/app/filesystem/store/listSlice.ts +8 -0
- package/app/filesystem/ui/CodeEditor.tsx +18 -3
- package/app/filesystem/ui/DiscoverDock.tsx +5 -1
- package/app/filesystem/ui/FileEditor.tsx +15 -10
- package/app/filesystem/ui/FileTree.tsx +75 -24
- package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
- package/app/filesystem/ui/FolderPicker.tsx +251 -0
- package/app/filesystem/ui/GitControlPanel.tsx +7 -2
- package/app/filesystem/ui/PackagesPanel.tsx +52 -374
- package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
- package/app/filesystem/ui/PolicyEditor.tsx +109 -24
- package/app/filesystem/ui/README.md +30 -23
- package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
- package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
- package/app/filesystem/ui/SourcesPanel.tsx +502 -342
- package/app/filesystem/ui/Toolbar.tsx +1 -2
- package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
- package/app/shared/filesystem.png +0 -0
- package/dist/app/host.js +58 -46
- package/dist/scripts/sourceRootPairV1.d.ts +47 -0
- package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
- package/dist/scripts/sourceRootPairV1.js +147 -0
- package/dist/scripts/sourceRootPairV1.js.map +1 -0
- package/dist/scripts/sourceToFolderV1.d.ts +56 -0
- package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
- package/dist/scripts/sourceToFolderV1.js +152 -0
- package/dist/scripts/sourceToFolderV1.js.map +1 -0
- package/dist/scripts/sourceToHostV1.d.ts +56 -0
- package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
- package/dist/scripts/sourceToHostV1.js +132 -0
- package/dist/scripts/sourceToHostV1.js.map +1 -0
- package/dist/src/channels/filesystem.d.ts +1 -1
- package/dist/src/channels/filesystem.js +1 -1
- package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
- package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
- package/dist/src/connectors/AppSourceConfigStore.js +46 -6
- package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
- package/dist/src/connectors/HostConnector.d.ts +201 -0
- package/dist/src/connectors/HostConnector.d.ts.map +1 -0
- package/dist/src/connectors/HostConnector.js +903 -0
- package/dist/src/connectors/HostConnector.js.map +1 -0
- package/dist/src/connectors/inProcessTransport.d.ts +131 -0
- package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
- package/dist/src/connectors/inProcessTransport.js +447 -0
- package/dist/src/connectors/inProcessTransport.js.map +1 -0
- package/dist/src/domain/access/callerPermissions.d.ts +29 -0
- package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
- package/dist/src/domain/access/callerPermissions.js +85 -5
- package/dist/src/domain/access/callerPermissions.js.map +1 -1
- package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
- package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
- package/dist/src/domain/access/nativeLocalFence.js +171 -0
- package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
- package/dist/src/domain/access/uriPermission.js +3 -3
- package/dist/src/domain/access/uriPermission.js.map +1 -1
- package/dist/src/domain/byteRange.d.ts +15 -0
- package/dist/src/domain/byteRange.d.ts.map +1 -0
- package/dist/src/domain/byteRange.js +30 -0
- package/dist/src/domain/byteRange.js.map +1 -0
- package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
- package/dist/src/domain/defaultPermissions.js +29 -0
- package/dist/src/domain/defaultPermissions.js.map +1 -1
- package/dist/src/domain/mime.d.ts +11 -19
- package/dist/src/domain/mime.d.ts.map +1 -1
- package/dist/src/domain/mime.js +13 -102
- package/dist/src/domain/mime.js.map +1 -1
- package/dist/src/domain/paths.d.ts.map +1 -1
- package/dist/src/domain/paths.js +5 -3
- package/dist/src/domain/paths.js.map +1 -1
- package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
- package/dist/src/domain/scopeAccessCtx.js +2 -2
- package/dist/src/domain/sourceConfig.d.ts +52 -19
- package/dist/src/domain/sourceConfig.d.ts.map +1 -1
- package/dist/src/domain/sourceConfig.js +103 -19
- package/dist/src/domain/sourceConfig.js.map +1 -1
- package/dist/src/domain/sourcePackages.d.ts +8 -8
- package/dist/src/domain/sourcePackages.d.ts.map +1 -1
- package/dist/src/domain/sourceRootPair.d.ts +88 -0
- package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
- package/dist/src/domain/sourceRootPair.js +108 -0
- package/dist/src/domain/sourceRootPair.js.map +1 -0
- package/dist/src/domain/syncTypes.d.ts +8 -1
- package/dist/src/domain/syncTypes.d.ts.map +1 -1
- package/dist/src/domain/syncTypes.js +1 -0
- package/dist/src/domain/syncTypes.js.map +1 -1
- package/dist/src/domain/types.d.ts +32 -2
- package/dist/src/domain/types.d.ts.map +1 -1
- package/dist/src/domain/uri.d.ts.map +1 -1
- package/dist/src/domain/uri.js +3 -2
- package/dist/src/domain/uri.js.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
- package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
- package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
- package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
- package/dist/src/embeddings/factory.d.ts +5 -3
- package/dist/src/embeddings/factory.d.ts.map +1 -1
- package/dist/src/embeddings/factory.js +7 -3
- package/dist/src/embeddings/factory.js.map +1 -1
- package/dist/src/embeddings/models.d.ts +1 -1
- package/dist/src/embeddings/models.d.ts.map +1 -1
- package/dist/src/embeddings/retry.js +1 -1
- package/dist/src/embeddings/retry.js.map +1 -1
- package/dist/src/embeddings/types.d.ts +6 -0
- package/dist/src/embeddings/types.d.ts.map +1 -1
- package/dist/src/embeddings/types.js +11 -0
- package/dist/src/embeddings/types.js.map +1 -1
- package/dist/src/events/pending.d.ts +1 -1
- package/dist/src/events/pending.js +1 -1
- package/dist/src/git/GitRunner.d.ts +110 -30
- package/dist/src/git/GitRunner.d.ts.map +1 -1
- package/dist/src/git/GitRunner.js +511 -199
- package/dist/src/git/GitRunner.js.map +1 -1
- package/dist/src/git/GitWatchRegistry.d.ts +41 -35
- package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
- package/dist/src/git/GitWatchRegistry.js +61 -79
- package/dist/src/git/GitWatchRegistry.js.map +1 -1
- package/dist/src/git/gitPlace.d.ts +61 -0
- package/dist/src/git/gitPlace.d.ts.map +1 -0
- package/dist/src/git/gitPlace.js +74 -0
- package/dist/src/git/gitPlace.js.map +1 -0
- package/dist/src/git/sourceGitControl.d.ts +33 -21
- package/dist/src/git/sourceGitControl.d.ts.map +1 -1
- package/dist/src/git/sourceGitControl.js +211 -95
- package/dist/src/git/sourceGitControl.js.map +1 -1
- package/dist/src/git/sourceGitStatus.js +1 -1
- package/dist/src/git/sourceGitStatus.js.map +1 -1
- package/dist/src/git/untrackedStats.js +1 -1
- package/dist/src/git/untrackedStats.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle.d.ts +9 -12
- package/dist/src/lifecycle.d.ts.map +1 -1
- package/dist/src/lifecycle.js +2 -1
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/routes/approve.d.ts +1 -1
- package/dist/src/routes/approve.js +1 -1
- package/dist/src/routes/events.d.ts +1 -1
- package/dist/src/routes/events.js +1 -1
- package/dist/src/routes/git.d.ts +7 -7
- package/dist/src/routes/git.d.ts.map +1 -1
- package/dist/src/routes/git.js +201 -72
- package/dist/src/routes/git.js.map +1 -1
- package/dist/src/routes/health.d.ts +6 -6
- package/dist/src/routes/health.js +12 -12
- package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
- package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
- package/dist/src/routes/helpers/uriGuard.js +3 -2
- package/dist/src/routes/helpers/uriGuard.js.map +1 -1
- package/dist/src/routes/helpers.d.ts +11 -4
- package/dist/src/routes/helpers.d.ts.map +1 -1
- package/dist/src/routes/helpers.js +13 -6
- package/dist/src/routes/helpers.js.map +1 -1
- package/dist/src/routes/list.d.ts +1 -1
- package/dist/src/routes/list.d.ts.map +1 -1
- package/dist/src/routes/list.js +3 -4
- package/dist/src/routes/list.js.map +1 -1
- package/dist/src/routes/pending.d.ts +4 -4
- package/dist/src/routes/pending.js +9 -9
- package/dist/src/routes/policies.d.ts +10 -8
- package/dist/src/routes/policies.d.ts.map +1 -1
- package/dist/src/routes/policies.js +31 -25
- package/dist/src/routes/policies.js.map +1 -1
- package/dist/src/routes/raw.d.ts +1 -1
- package/dist/src/routes/raw.d.ts.map +1 -1
- package/dist/src/routes/raw.js +66 -12
- package/dist/src/routes/raw.js.map +1 -1
- package/dist/src/routes/read.d.ts +1 -1
- package/dist/src/routes/read.js +1 -1
- package/dist/src/routes/search.d.ts +1 -1
- package/dist/src/routes/search.js +1 -1
- package/dist/src/routes/sources.d.ts +63 -6
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +494 -382
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/routes/upload.d.ts +1 -1
- package/dist/src/routes/upload.js +1 -1
- package/dist/src/routes/write.d.ts +2 -2
- package/dist/src/routes/write.js +2 -2
- package/dist/src/services/BridgeService.d.ts +21 -35
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +42 -88
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +4 -3
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +112 -34
- package/dist/src/services/FilesystemService.js.map +1 -1
- package/dist/src/services/FilesystemUiService.d.ts +4 -1
- package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
- package/dist/src/services/FilesystemUiService.js +66 -25
- package/dist/src/services/FilesystemUiService.js.map +1 -1
- package/dist/src/services/SyncScheduler.d.ts +12 -4
- package/dist/src/services/SyncScheduler.d.ts.map +1 -1
- package/dist/src/services/SyncScheduler.js +37 -8
- package/dist/src/services/SyncScheduler.js.map +1 -1
- package/dist/src/services/baselineRepair.d.ts +13 -3
- package/dist/src/services/baselineRepair.d.ts.map +1 -1
- package/dist/src/services/baselineRepair.js +13 -3
- package/dist/src/services/baselineRepair.js.map +1 -1
- package/dist/src/services/declaredSources.d.ts +132 -43
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +118 -45
- package/dist/src/services/declaredSources.js.map +1 -1
- package/dist/src/store/dataFormats.d.ts +17 -2
- package/dist/src/store/dataFormats.d.ts.map +1 -1
- package/dist/src/store/dataFormats.js +18 -3
- 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 +144 -4
- package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
- package/dist/src/sync/SyncJobQueue.d.ts +7 -2
- package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
- package/dist/src/sync/SyncJobQueue.js +16 -10
- package/dist/src/sync/SyncJobQueue.js.map +1 -1
- package/dist/src/sync/SyncWatchService.d.ts +21 -4
- package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
- package/dist/src/sync/SyncWatchService.js +115 -10
- package/dist/src/sync/SyncWatchService.js.map +1 -1
- package/dist/src/sync/categoryInference.d.ts +1 -1
- package/dist/src/sync/categoryInference.js +1 -1
- package/dist/src/tools/execCtx.d.ts +2 -2
- package/dist/src/tools/execCtx.js +2 -2
- package/dist/src/tools/fs_read.d.ts.map +1 -1
- package/dist/src/tools/fs_read.js +5 -0
- package/dist/src/tools/fs_read.js.map +1 -1
- package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
- package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
- package/dist/src/usecases/admitSourceRoot.js +85 -0
- package/dist/src/usecases/admitSourceRoot.js.map +1 -0
- package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
- package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
- package/dist/src/usecases/browseSourceFolders.js +210 -0
- package/dist/src/usecases/browseSourceFolders.js.map +1 -0
- package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
- package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
- package/dist/src/usecases/createDeclaredSources.js +39 -0
- package/dist/src/usecases/createDeclaredSources.js.map +1 -1
- package/dist/src/usecases/embeddingBacklog.js +1 -1
- package/dist/src/usecases/embeddingBacklog.js.map +1 -1
- package/dist/src/usecases/list.d.ts.map +1 -1
- package/dist/src/usecases/list.js +83 -26
- package/dist/src/usecases/list.js.map +1 -1
- package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
- package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
- package/dist/src/usecases/listAttachableDevices.js +10 -0
- package/dist/src/usecases/listAttachableDevices.js.map +1 -0
- package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
- package/dist/src/usecases/listSourcePackages.js +2 -5
- package/dist/src/usecases/listSourcePackages.js.map +1 -1
- 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 +99 -1
- package/dist/src/usecases/readFiles.js.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
- package/dist/src/usecases/revokePrincipal.d.ts +7 -5
- package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
- package/dist/src/usecases/revokePrincipal.js +29 -7
- package/dist/src/usecases/revokePrincipal.js.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
- package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.js +6 -5
- package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
- package/dist/src/usecases/syncPointWrites.d.ts +2 -0
- package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
- package/dist/src/usecases/syncPointWrites.js +5 -1
- package/dist/src/usecases/syncPointWrites.js.map +1 -1
- package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
- package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
- package/dist/src/usecases/triggerSourceSync.js +75 -0
- package/dist/src/usecases/triggerSourceSync.js.map +1 -0
- package/dist/src/usecases/uploadBinary.d.ts +2 -1
- package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
- package/dist/src/usecases/uploadBinary.js +2 -2
- package/dist/src/usecases/uploadBinary.js.map +1 -1
- package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
- package/dist/src/usecases/vector/endpointGuard.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/content.js +1 -1
- package/dist/src/vector/qdrantClient.d.ts +1 -1
- package/dist/src/vector/qdrantClient.js +1 -1
- package/dist/src/wiring.d.ts +3 -0
- package/dist/src/wiring.d.ts.map +1 -1
- package/dist/src/wiring.js +72 -6
- package/dist/src/wiring.js.map +1 -1
- package/dist/tools/fs_list.json +1 -1
- package/dist/tools/fs_read.json +1 -1
- package/dist/tools/fs_search.json +1 -1
- package/dist/tools/fs_write.json +1 -1
- package/instructions/brain-model.md +1 -1
- package/package.json +212 -114
- package/rules/brain-rules.md +2 -2
- package/skills/brain-start/SKILL.md +1 -1
- package/skills/connect-business-data/SKILL.md +3 -3
- package/skills/create-package/SKILL.md +26 -26
- package/skills/create-package/references/config-and-credentials.md +16 -13
- package/skills/create-package/references/connectors-and-uri-policies.md +39 -25
- package/skills/create-package/references/manifest-and-discovery.md +12 -12
- package/skills/create-package/references/runtime-and-build.md +19 -19
- package/skills/create-package/references/skills-and-context.md +36 -20
- package/skills/create-package/references/surfaces-and-design.md +16 -16
- package/skills/create-package/references/team-and-workflows.md +2 -2
- package/skills/create-package/references/test-and-ship.md +21 -21
- package/skills/create-package/references/tools-and-schemas.md +5 -5
- package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +79 -65
- package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
- package/skills/git/SKILL.md +53 -51
- package/skills/manage-files/SKILL.md +22 -25
- package/skills/manage-sources/SKILL.md +128 -87
- package/skills/manage-uri-policy/SKILL.md +18 -15
- package/skills/organize/SKILL.md +1 -1
- package/skills/sync/SKILL.md +3 -3
- package/tools/fs_list.json +1 -1
- package/tools/fs_read.json +1 -1
- package/tools/fs_search.json +1 -1
- package/tools/fs_write.json +1 -1
- package/workflows/sources-digest.json +1 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
- package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
- package/dist/src/connectors/LocalDiskConnector.js +0 -1233
- package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
- package/docs/drive-overview.md +0 -59
- package/skills/manage-files/scripts/copy.sh +0 -7
- package/skills/manage-files/scripts/create.sh +0 -7
- package/skills/manage-files/scripts/mkdir.sh +0 -7
- package/skills/manage-files/scripts/modify.sh +0 -7
- package/skills/manage-files/scripts/upload.sh +0 -16
- package/skills/manage-files/scripts/write.sh +0 -29
- package/skills/manage-sources/scripts/reembed.sh +0 -23
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How a connector kind's
|
|
2
|
+
* How a connector kind's config fields render in the Add-Source form — derived
|
|
3
3
|
* from the DECLARATION, never from the kind name and never from `isDocker`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `x-neuralis.ui
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `/home/you/projects` (warded-wren, closing the UI half of F-B5).
|
|
5
|
+
* The form is one page, in the order a person decides: the kind, then WHERE
|
|
6
|
+
* (a field the declaration marks `x-neuralis.ui.transport` — the `host` kind's
|
|
7
|
+
* `device`), then the ROOT folder (with Browse when the root declares
|
|
8
|
+
* `ui.browse`), then the plain fields, and the fields marked `ui.advanced`
|
|
9
|
+
* folded under "Advanced". `configFields` returns that order.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* (`kind === 'host' || kind.startsWith('host-')`) whose own docstring says a
|
|
24
|
-
* future `ssh`/`smb` connector joins the plane by being added to its body. A
|
|
25
|
-
* negative test on that axis would hand the container chrome to every future
|
|
26
|
-
* non-`local` kind — re-creating, in the UI, the kind-blindness this replaces.
|
|
11
|
+
* The root's meaning follows the transport, not a control name: until the
|
|
12
|
+
* transport field holds a value the root is a path in THIS server's filesystem
|
|
13
|
+
* (`container` — the `/mounts` suggestions and the mount-CLI callout apply in
|
|
14
|
+
* Docker); with a device chosen it is a path ON THAT DEVICE
|
|
15
|
+
* (`deviceLabel`, and `devicePlaceholders[<the device's platform>]` else
|
|
16
|
+
* `devicePlaceholder`), browsed through that device's broker and bounded by
|
|
17
|
+
* the folders its owner allowed. A root control is `container-path`
|
|
18
|
+
* or the pre-existing bare `path`, so an externally authored connector keeps
|
|
19
|
+
* working unchanged. Keyed on the DECLARATION and the form's own values, never
|
|
20
|
+
* on a kind name — a name test would hand one kind's chrome to every future
|
|
21
|
+
* kind it does not list.
|
|
27
22
|
*/
|
|
28
23
|
export type SourceKindFormMeta = {
|
|
29
24
|
kind: string;
|
|
@@ -37,6 +32,8 @@ export type RootFieldMeta = {
|
|
|
37
32
|
container: boolean;
|
|
38
33
|
label: string;
|
|
39
34
|
placeholder: string;
|
|
35
|
+
/** The root declares `ui.browse`: the form offers the folder picker (on this server, or on the chosen device). */
|
|
36
|
+
browse: boolean;
|
|
40
37
|
};
|
|
41
38
|
|
|
42
39
|
type SchemaShape = { required: string[]; properties: Record<string, any> };
|
|
@@ -50,17 +47,203 @@ export function schemaShape(meta: SourceKindFormMeta | null | undefined): Schema
|
|
|
50
47
|
};
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
const PATH_CONTROLS = new Set(['path', 'container-path'
|
|
50
|
+
const PATH_CONTROLS = new Set(['path', 'container-path']);
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `platform` is the chosen device's claimed platform (its WHERE option's
|
|
54
|
+
* `platform`): the placeholder takes the declared `devicePlaceholders` entry
|
|
55
|
+
* for it, else the `devicePlaceholder` default — so a Windows device hints
|
|
56
|
+
* `C:\…`, never another machine's path style.
|
|
57
|
+
*/
|
|
58
|
+
export function rootFieldMeta(
|
|
59
|
+
meta: SourceKindFormMeta | null | undefined,
|
|
60
|
+
values: Readonly<Record<string, unknown>> = {},
|
|
61
|
+
platform?: string,
|
|
62
|
+
): RootFieldMeta {
|
|
56
63
|
const schema = schemaShape(meta);
|
|
57
64
|
const root = schema.properties.root;
|
|
58
65
|
const ui = root?.['x-neuralis']?.ui;
|
|
59
66
|
const control = typeof ui?.control === 'string' ? (ui.control as string) : undefined;
|
|
67
|
+
const onDevice = transportChosen(meta, values);
|
|
68
|
+
const pick = (deviceKey: string, key: string, fallback: string): string =>
|
|
69
|
+
onDevice && typeof ui?.[deviceKey] === 'string' ? ui[deviceKey] : typeof ui?.[key] === 'string' ? ui[key] : fallback;
|
|
70
|
+
const perPlatform = onDevice && platform ? ui?.devicePlaceholders?.[platform] : undefined;
|
|
60
71
|
return {
|
|
61
72
|
needed: Boolean(root && (schema.required.includes('root') || (control && PATH_CONTROLS.has(control)))),
|
|
62
|
-
container:
|
|
63
|
-
label:
|
|
64
|
-
placeholder: typeof
|
|
73
|
+
container: !onDevice,
|
|
74
|
+
label: pick('deviceLabel', 'label', 'Path'),
|
|
75
|
+
placeholder: typeof perPlatform === 'string' ? perPlatform : pick('devicePlaceholder', 'placeholder', ''),
|
|
76
|
+
browse: ui?.browse === true,
|
|
65
77
|
};
|
|
66
78
|
}
|
|
79
|
+
|
|
80
|
+
/** One non-root config field, with the hints the form reads off its `x-neuralis.ui`. */
|
|
81
|
+
export type ConfigField = { key: string; prop: unknown; label: string; help?: string; placeholder?: string };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The non-root config fields in the order the form renders them: `transport`
|
|
85
|
+
* (WHERE — before the root, because it decides what the root means), `fields`,
|
|
86
|
+
* and `advanced` (`ui.advanced`, folded). A field the schema's exclusion hides
|
|
87
|
+
* for the current values (`hiddenByExclusion`) is not offered at all.
|
|
88
|
+
*/
|
|
89
|
+
export function configFields(
|
|
90
|
+
meta: SourceKindFormMeta | null | undefined,
|
|
91
|
+
values: Readonly<Record<string, unknown>>,
|
|
92
|
+
): { transport: ConfigField[]; fields: ConfigField[]; advanced: ConfigField[] } {
|
|
93
|
+
const out = { transport: [] as ConfigField[], fields: [] as ConfigField[], advanced: [] as ConfigField[] };
|
|
94
|
+
for (const [key, prop] of Object.entries(schemaShape(meta).properties)) {
|
|
95
|
+
if (key === 'root' || hiddenByExclusion(meta, values, key)) continue;
|
|
96
|
+
const ui = prop?.['x-neuralis']?.ui ?? {};
|
|
97
|
+
const field: ConfigField = {
|
|
98
|
+
key,
|
|
99
|
+
prop,
|
|
100
|
+
label: typeof ui.label === 'string' ? ui.label : key,
|
|
101
|
+
...(typeof ui.help === 'string' ? { help: ui.help } : {}),
|
|
102
|
+
...(typeof ui.placeholder === 'string' ? { placeholder: ui.placeholder } : {}),
|
|
103
|
+
};
|
|
104
|
+
(ui.transport === true ? out.transport : ui.advanced === true ? out.advanced : out.fields).push(field);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The kind's one-line card text (`configSchema["x-neuralis"].ui.summary`); the long `description` is the agent's. */
|
|
110
|
+
export function kindSummary(meta: SourceKindFormMeta | null | undefined): string | null {
|
|
111
|
+
const summary = (meta as any)?.configSchema?.['x-neuralis']?.ui?.summary;
|
|
112
|
+
return typeof summary === 'string' && summary ? summary : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Has the form chosen a TRANSPORT — a value in a field the declaration marks
|
|
117
|
+
* `x-neuralis.ui.transport` (the `host` kind's `device`)? Then the root is a
|
|
118
|
+
* path on that device, not in this server's filesystem.
|
|
119
|
+
*/
|
|
120
|
+
export function transportChosen(meta: SourceKindFormMeta | null | undefined, values: Readonly<Record<string, unknown>>): boolean {
|
|
121
|
+
const { properties } = schemaShape(meta);
|
|
122
|
+
return Object.entries(properties).some(([key, prop]) =>
|
|
123
|
+
prop?.['x-neuralis']?.ui?.transport === true && typeof values[key] === 'string' && values[key] !== '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Is this field hidden because a field it is mutually exclusive with (the
|
|
128
|
+
* schema's `not: { required: [...] }`) already holds a value? The kind's
|
|
129
|
+
* validator refuses the pair, so the form never offers it.
|
|
130
|
+
*/
|
|
131
|
+
export function hiddenByExclusion(meta: SourceKindFormMeta | null | undefined, values: Readonly<Record<string, unknown>>, key: string): boolean {
|
|
132
|
+
const exclusive = (meta as any)?.configSchema?.not?.required;
|
|
133
|
+
if (!Array.isArray(exclusive) || !exclusive.includes(key)) return false;
|
|
134
|
+
return exclusive.some((other: unknown) => other !== key && typeof other === 'string'
|
|
135
|
+
&& values[other] !== undefined && values[other] !== '');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* How a NON-root config field renders. A CLOSED set: `text` (the default — a
|
|
140
|
+
* plain input, `number` for a numeric type) and `select`, whose options a
|
|
141
|
+
* route in the DECLARING package answers (`x-neuralis.ui.optionsRoute`).
|
|
142
|
+
*
|
|
143
|
+
* The list a `select` shows is UX only: the declaring package re-checks the
|
|
144
|
+
* picked value when the source is created (a host source's `device` is
|
|
145
|
+
* re-checked by the device's owner rule), so the form never decides who may
|
|
146
|
+
* attach what. An `optionsRoute` the form refuses (absolute, a scheme, a `..`
|
|
147
|
+
* segment, an escape) renders the field as `text` and names the refusal —
|
|
148
|
+
* never a request to somewhere the declaration did not mean.
|
|
149
|
+
*/
|
|
150
|
+
export type FieldControl =
|
|
151
|
+
| { control: 'text'; numeric: boolean; refused?: string }
|
|
152
|
+
| { control: 'select'; numeric: boolean; options: OptionsRoute; emptyLabel?: string };
|
|
153
|
+
|
|
154
|
+
/** A validated options route: one package-relative path + its query. */
|
|
155
|
+
export type OptionsRoute = { slug: string; path: string; params: Record<string, string> };
|
|
156
|
+
|
|
157
|
+
const ROUTE_PATH = /^[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9._~-]+)*$/;
|
|
158
|
+
const ROUTE_QUERY_PART = /^[A-Za-z0-9._~-]+$/;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The `/api/packages/<slug>` segment of a registry id: the native scope is
|
|
162
|
+
* dropped (`@neuralis/agent-core` → `agent-core`, the same rule the route
|
|
163
|
+
* gateway resolves), any other id travels as ONE encoded segment.
|
|
164
|
+
*/
|
|
165
|
+
export function packageRouteSlug(packageId: string): string {
|
|
166
|
+
return encodeURIComponent(packageId.replace(/^@neuralis\//, ''));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse a declared `optionsRoute` against the package that declares the field.
|
|
171
|
+
* Relative only, plain path segments (no `.`/`..`, no `%`, no `\`), an optional
|
|
172
|
+
* `key=value&…` query of the same characters, ≤ 200 characters.
|
|
173
|
+
*/
|
|
174
|
+
export function parseOptionsRoute(raw: unknown, packageId: string): OptionsRoute | { refused: string } {
|
|
175
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.length > 200) {
|
|
176
|
+
return { refused: 'optionsRoute must be a non-empty string of at most 200 characters' };
|
|
177
|
+
}
|
|
178
|
+
if (!packageId) return { refused: 'optionsRoute has no declaring package' };
|
|
179
|
+
const [path, query, extra] = raw.split('?');
|
|
180
|
+
if (extra !== undefined || !ROUTE_PATH.test(path ?? '')) {
|
|
181
|
+
return { refused: 'optionsRoute must be a relative route inside the declaring package' };
|
|
182
|
+
}
|
|
183
|
+
if ((path ?? '').split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
184
|
+
return { refused: 'optionsRoute must be a relative route inside the declaring package' };
|
|
185
|
+
}
|
|
186
|
+
const params: Record<string, string> = {};
|
|
187
|
+
if (query !== undefined) {
|
|
188
|
+
for (const pair of query.split('&')) {
|
|
189
|
+
const [key, value, more] = pair.split('=');
|
|
190
|
+
if (more !== undefined || !key || !ROUTE_QUERY_PART.test(key) || (value !== undefined && value !== '' && !ROUTE_QUERY_PART.test(value))) {
|
|
191
|
+
return { refused: 'optionsRoute query must be plain key=value pairs' };
|
|
192
|
+
}
|
|
193
|
+
params[key] = value ?? '';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { slug: packageRouteSlug(packageId), path: path as string, params };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* An OPTIONAL select (`ui.optional`) offers its `emptyLabel` as a real choice —
|
|
201
|
+
* the empty value (for the `host` kind's `device`: "This server"); a required
|
|
202
|
+
* one only lists the route's rows.
|
|
203
|
+
*/
|
|
204
|
+
export function fieldControl(prop: unknown, packageId: string): FieldControl {
|
|
205
|
+
const p = (prop ?? {}) as { type?: unknown; 'x-neuralis'?: { ui?: { control?: unknown; optionsRoute?: unknown; optional?: unknown; emptyLabel?: unknown } } };
|
|
206
|
+
const numeric = p.type === 'number' || p.type === 'integer';
|
|
207
|
+
const ui = p['x-neuralis']?.ui;
|
|
208
|
+
if (ui?.control !== 'select') return { control: 'text', numeric };
|
|
209
|
+
const route = parseOptionsRoute(ui.optionsRoute, packageId);
|
|
210
|
+
if ('refused' in route) return { control: 'text', numeric, refused: route.refused };
|
|
211
|
+
const emptyLabel = ui.optional === true ? (typeof ui.emptyLabel === 'string' && ui.emptyLabel ? ui.emptyLabel : 'None') : undefined;
|
|
212
|
+
return { control: 'select', numeric, options: route, ...(emptyLabel ? { emptyLabel } : {}) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** One row of a `select`: the stored value, its label, whether it reads dimmed, and a device row's claimed `platform`. */
|
|
216
|
+
export type SelectOption = { value: string; label: string; dimmed: boolean; platform?: string };
|
|
217
|
+
|
|
218
|
+
/** The OS word a device row's `platform` reads as; any other token prints as it came. */
|
|
219
|
+
const PLATFORM_WORDS: Readonly<Record<string, string>> = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' };
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The options an `optionsRoute` answered. The answer is a JSON object whose
|
|
223
|
+
* FIRST array-valued property lists the rows; a row is `{id, name?, platform?,
|
|
224
|
+
* status?, confinement?}` (the shape of a device list). The label is
|
|
225
|
+
* `name · OS · status · confinement`; a `revoked` row is not offered at all; any
|
|
226
|
+
* status other than `online` reads dimmed but stays selectable — attaching a
|
|
227
|
+
* source to an offline device is allowed, it simply answers once the device is
|
|
228
|
+
* back. Rows without a string `id` are dropped.
|
|
229
|
+
*/
|
|
230
|
+
export function selectOptionsFrom(answer: unknown): SelectOption[] {
|
|
231
|
+
if (!answer || typeof answer !== 'object') return [];
|
|
232
|
+
const rows = Object.values(answer as Record<string, unknown>).find(Array.isArray) as unknown[] | undefined;
|
|
233
|
+
if (!rows) return [];
|
|
234
|
+
const out: SelectOption[] = [];
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
if (!row || typeof row !== 'object') continue;
|
|
237
|
+
const r = row as { id?: unknown; name?: unknown; platform?: unknown; status?: unknown; confinement?: unknown };
|
|
238
|
+
if (typeof r.id !== 'string' || r.id.length === 0) continue;
|
|
239
|
+
const status = typeof r.status === 'string' ? r.status : undefined;
|
|
240
|
+
if (status === 'revoked') continue;
|
|
241
|
+
const platform = typeof r.platform === 'string' && r.platform ? r.platform : undefined;
|
|
242
|
+
const parts = [typeof r.name === 'string' && r.name ? r.name : r.id];
|
|
243
|
+
if (platform) parts.push(PLATFORM_WORDS[platform] ?? platform);
|
|
244
|
+
if (status) parts.push(status.replace(/_/g, ' '));
|
|
245
|
+
if (typeof r.confinement === 'string') parts.push(r.confinement);
|
|
246
|
+
out.push({ value: r.id, label: parts.join(' · '), dimmed: status !== undefined && status !== 'online', ...(platform ? { platform } : {}) });
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
@@ -326,27 +326,121 @@ export function getSourceVisualTone(info: SourceInfo): {
|
|
|
326
326
|
// gone. Returned as a plain string for the parent button's `title` tooltip.
|
|
327
327
|
/** Display strings for the `SourceLiveReason` enum (the wire value is an enum to
|
|
328
328
|
* keep it parseable / leak-free — the human text is mapped here, in the UI). */
|
|
329
|
+
/** How a device's owner updates the CLI that serves it: the desktop app carries it, an npm install updates by npm (`neuralis update` is the SERVER's verb, never a device's). */
|
|
330
|
+
const DEVICE_UPDATE_STEPS = 'update the Neuralis desktop app on that computer, or run npm i -g @neuralis/cli@latest there, then neuralis device install';
|
|
331
|
+
|
|
329
332
|
const LIVE_REASON_LABEL: Record<string, string> = {
|
|
330
333
|
runtime_offline: 'Machine offline — container not running',
|
|
331
334
|
fs_unreachable: 'Filesystem unreachable — mount missing',
|
|
332
335
|
fs_permission: 'Filesystem unreadable — permission denied',
|
|
333
336
|
store_unreachable: 'Store offline — backing service unreachable',
|
|
337
|
+
broker_outdated: `Device needs an update — ${DEVICE_UPDATE_STEPS}`,
|
|
338
|
+
devices_disabled: 'Devices are turned off on this server — an admin turns them on in Admin → Config',
|
|
339
|
+
device_not_ready: 'Device not ready — neuralis device status on that computer says why',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/** A `host` folder is offline only through its device's link (an in-process folder answers `fs_*`). */
|
|
343
|
+
const DEVICE_OFFLINE_LABEL = 'Device offline — it is not connected to this server';
|
|
344
|
+
|
|
345
|
+
/** What a device's owner fixes there, per broker readiness code (`DeviceNotReadyCode`). */
|
|
346
|
+
const DEVICE_NOT_READY_LABEL: Record<string, string> = {
|
|
347
|
+
ceiling_missing_or_empty: 'no folder is allowed on it yet (neuralis device grant on that computer)',
|
|
348
|
+
sandbox_helper_missing: 'its sandbox helper is not usable (neuralis device install on that computer)',
|
|
349
|
+
socket_isolation_failed: 'its sandbox cannot keep commands away from the computer\'s own control sockets (neuralis device status on that computer says how to fix it)',
|
|
350
|
+
secret_file_missing_or_too_short: 'its pairing secret is missing (pair it again)',
|
|
351
|
+
scratch_root_missing: 'its scratch folder is missing (neuralis device install on that computer)',
|
|
334
352
|
};
|
|
335
353
|
|
|
336
354
|
/** Map a `SourceLiveReason` (or undefined) to its display string. Shared by the
|
|
337
|
-
* file tree tooltip
|
|
338
|
-
|
|
355
|
+
* file tree tooltip, the Sources panel and the read/preview failures so all
|
|
356
|
+
* render the same text: `kind` words an offline `host` folder as its device,
|
|
357
|
+
* `notReady` names a not-ready device's own codes. */
|
|
358
|
+
export function sourceLiveReasonLabel(reason?: string | null, opts: { kind?: string | null; notReady?: readonly unknown[] | null } = {}): string {
|
|
359
|
+
if (reason === 'runtime_offline' && opts.kind === 'host') return DEVICE_OFFLINE_LABEL;
|
|
360
|
+
if (reason === 'device_not_ready') {
|
|
361
|
+
const known = (opts.notReady ?? []).flatMap((code) => (typeof code === 'string' && DEVICE_NOT_READY_LABEL[code] ? [DEVICE_NOT_READY_LABEL[code]] : []));
|
|
362
|
+
if (known.length > 0) return `Device not ready — ${known.join('; ')}`;
|
|
363
|
+
}
|
|
339
364
|
return (reason ? LIVE_REASON_LABEL[reason] : undefined)
|
|
340
365
|
?? 'Disconnected — connector cannot reach its backing filesystem';
|
|
341
366
|
}
|
|
342
367
|
|
|
368
|
+
/**
|
|
369
|
+
* The source row's live-watch badge: `null` while the watch covers the whole
|
|
370
|
+
* source (or none is meant for it), else the label and the tooltip that says
|
|
371
|
+
* what is and is not live right now.
|
|
372
|
+
*/
|
|
373
|
+
export function sourceWatchBadge(
|
|
374
|
+
watch: string | null | undefined,
|
|
375
|
+
liveReason?: string | null,
|
|
376
|
+
): { label: string; title: string; tone: 'pending' | 'warn' } | null {
|
|
377
|
+
if (watch === 'starting') {
|
|
378
|
+
return {
|
|
379
|
+
label: 'Live: starting',
|
|
380
|
+
title: 'Live changes are starting: the watcher is still walking this source\'s folders. Until it is ready, a change can arrive only with the next reconcile or Sync now.',
|
|
381
|
+
tone: 'pending',
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (watch === 'partial') {
|
|
385
|
+
return {
|
|
386
|
+
label: 'Live: partial',
|
|
387
|
+
title: 'Live changes cover part of this source: it has more folders than the watcher budget (brainWatchMaxFolders) or this computer\'s watch limit allows. Changes in the folders past it, new folders there included, are picked up by the next full reconcile (every 24 h by default) or by Sync now.',
|
|
388
|
+
tone: 'warn',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (watch === 'off') {
|
|
392
|
+
return {
|
|
393
|
+
label: 'Live: off',
|
|
394
|
+
title: liveReason === 'broker_outdated'
|
|
395
|
+
? `No live changes for this source: its device runs a CLI too old to watch — ${DEVICE_UPDATE_STEPS}. Changes arrive with the regular sync.`
|
|
396
|
+
: `No live changes for this source: its watch is not running (a device may be offline, or run a CLI too old to watch — ${DEVICE_UPDATE_STEPS}). Changes arrive with the regular sync.`,
|
|
397
|
+
tone: 'warn',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** A device refusal code its owner can act on, in words — the file read, the preview and the source row say it alike. */
|
|
404
|
+
const DEVICE_REFUSAL_LABEL: Record<string, string> = {
|
|
405
|
+
host_fs_denied: 'Not allowed on that device yet — ask there (tray → Allow a folder…, or neuralis device grant); it serves once allowed',
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** A device's refusal of one file, by its fixed code; `undefined` for anything that is not a code. */
|
|
409
|
+
export function deviceRefusalLabel(reason: unknown): string | undefined {
|
|
410
|
+
if (typeof reason !== 'string' || !/^[a-z][a-z0-9_]{0,63}$/.test(reason)) return undefined;
|
|
411
|
+
return DEVICE_REFUSAL_LABEL[reason] ?? `The device refused this file (${reason})`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The fixed codes a refused sync persists as `lastSyncResult.error` (a device's refusal codes too, by the map above). */
|
|
415
|
+
const SYNC_REFUSAL_LABEL: Record<string, string> = {
|
|
416
|
+
root_unreachable: 'Sync stopped — the source folder cannot be reached (unmounted, renamed or offline). Nothing was removed from the index.',
|
|
417
|
+
root_empty: 'Sync stopped — the source folder is empty, but the index still holds its files. Nothing was removed. Choose Sync anyway if the folder really is empty.',
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/** A sync error for display: the label of a fixed refusal code, any other text unchanged. */
|
|
421
|
+
export function syncErrorLabel(error: string): string {
|
|
422
|
+
return SYNC_REFUSAL_LABEL[error] ?? DEVICE_REFUSAL_LABEL[error] ?? error;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Why a source that lists nothing is UNAVAILABLE rather than empty: its live
|
|
427
|
+
* state, else the code its walk, count or last sync stopped on (`last_error` —
|
|
428
|
+
* a failed walk or count never sets `sync_status`); `undefined` = truly empty.
|
|
429
|
+
*/
|
|
430
|
+
export function sourceUnavailableLabel(info: SourceInfo): string | undefined {
|
|
431
|
+
if (info.live === false) return sourceLiveReasonLabel(info.live_reason, { kind: info.source_kind, notReady: info.live_not_ready });
|
|
432
|
+
return info.last_error ? syncErrorLabel(info.last_error) : undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
343
435
|
export function resolveSourceToneTitle(info: SourceInfo): string | null {
|
|
344
436
|
if (info.enabled === false) return 'Source disabled';
|
|
345
437
|
if (info.enabled === 'auto' && info.enabled_effective === false) return 'Source auto — currently stopped';
|
|
346
|
-
if (info.live === false) return sourceLiveReasonLabel(info.live_reason);
|
|
347
|
-
if (info.sync_status === 'error') return info.last_error
|
|
438
|
+
if (info.live === false) return sourceLiveReasonLabel(info.live_reason, { kind: info.source_kind, notReady: info.live_not_ready });
|
|
439
|
+
if (info.sync_status === 'error') return info.last_error ? syncErrorLabel(info.last_error) : 'Sync error';
|
|
348
440
|
if (info.sync_status === 'paused') return 'Sync paused';
|
|
349
441
|
if (info.sync_status === 'active') return 'Sync active';
|
|
442
|
+
// A failed walk or count leaves `sync_status` alone: neutral tone, the code as the title.
|
|
443
|
+
if (info.last_error) return syncErrorLabel(info.last_error);
|
|
350
444
|
return null;
|
|
351
445
|
}
|
|
352
446
|
|
|
@@ -65,6 +65,7 @@ Two guards make this safe against the races a plain re-list would lose:
|
|
|
65
65
|
- **`pendingSourceSuggestion` is a HANDOFF, not state.** The tree's Discover dock cannot call into `SourcesPanel` — the panel is a conditional mount and does not exist at click time — so the pick travels through the store and the panel consumes it on mount, clearing it in the same tick. It is deliberately OUT of the persist allowlist (a reload that re-opened a prefilled Add-Source form by itself is the ghost class this store already fought) and `loadList`'s context reset clears it beside `workspacePanels`, so a pick can never strand across an agent/project switch and prefill a root the new context does not have.
|
|
66
66
|
- `loadedFolders[uri]` means "this folder HAS been enumerated". Only a successful listing sets it. Marking a source loaded that was never listed (the old `loadList` did this for every source, connector-failed ones included) lets a later absence pass prune everything under it. `false` is a third state, STALE: enumerated once, then taken off screen by a collapse. Every truthiness reader treats it as not loaded; `ensureFolderLoaded` on a stale folder runs ONE narrow authoritative pass over exactly that folder (`runSourceReconcile({ wide: false })`, absence applies) instead of an upsert-only listing, and that pass marks it `true` again; a stale folder put back on screen another way (`autoExpandToFile`, a nav link) is re-listed by the next wide pass. A narrow re-open pass whose listing FAILS leaves the folder stale, with only the source header's "may be out of date" hint; the next collapse and re-open retries it.
|
|
67
67
|
- **Collapse and the bound.** `toggleFolder` collapsing a folder drops every expanded descendant from `expandedFolders` and marks every enumerated folder at or under it stale — so no reconcile keeps listing what nobody can see. The expanded set is capped at `EXPANDED_FOLDERS_CAP` (`listSlice.ts`); `expandPatch` moves each newly expanded folder to most-recent and `boundExpandedFolders` drops the oldest LEAF (never an ancestor of a kept folder, never a source root), marking it stale like a collapse. `autoExpandToFile` is the writer that needs it: it adds a folder chain for every pending write an agent makes. The persisted projection and a rehydrated legacy payload are bounded the same way.
|
|
68
|
+
- **Per-source refresh:** It works on a collapsed source too — an explicit click is a user intent, unlike the automatic event-driven loads, which never list a collapsed source — and it opens the branch so the result is visible.
|
|
68
69
|
|
|
69
70
|
## Tests
|
|
70
71
|
|
|
@@ -4,7 +4,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
|
|
4
4
|
import { createStore, type StoreApi as ZustandStoreApi } from 'zustand/vanilla';
|
|
5
5
|
import type { ClientStateResetScope } from '@neuralis/package-system/contracts';
|
|
6
6
|
import { createPackageApi, type PackageApi } from '@neuralis/package-system/client';
|
|
7
|
-
import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, PersistedFilesWindow, SidebarView, WorkspacePanelId } from '../domain/types';
|
|
7
|
+
import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, PersistedFilesWindow, SidebarView, SourceInfo, WorkspacePanelId } from '../domain/types';
|
|
8
8
|
export type { SidebarView, WorkspacePanelId } from '../domain/types';
|
|
9
9
|
import { countPendingChanges, readFiles, uploadFile, writeFile, writeRefusalMessage } from '../api';
|
|
10
10
|
import {
|
|
@@ -21,8 +21,9 @@ import {
|
|
|
21
21
|
sourceRootUri,
|
|
22
22
|
uriAncestors,
|
|
23
23
|
parseFilePrev,
|
|
24
|
+
readFailureMessage,
|
|
24
25
|
} from './helpers';
|
|
25
|
-
import { resolveList, type DiscoverableSourceSuggestion } from '../api';
|
|
26
|
+
import { listDiscoverableSources, resolveList, type DiscoverableSourceSuggestion } from '../api';
|
|
26
27
|
import { searchSliceInitial, createSearchSlice, type SearchSliceActions } from './searchSlice';
|
|
27
28
|
import { boundExpandedFolders, contextResetPatch, createListSlice, persistedContext, sliceFromPersisted, type ListSliceActions } from './listSlice';
|
|
28
29
|
import { gitSliceInitial, createGitSlice, type GitSliceState, type GitSliceActions } from './gitSlice';
|
|
@@ -185,6 +186,38 @@ function repairParentUri(next: Record<string, any>, rawUri: string, prev: any):
|
|
|
185
186
|
return fixed;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* A folder listing carries its source's CURRENT state. The tree's empty row and
|
|
191
|
+
* header read `state.sources`, which otherwise only the sources-only listing
|
|
192
|
+
* writes — so a Refresh that found the device gone would show "No files" until
|
|
193
|
+
* the next background pass. Only the live/sync fields move; counts and every
|
|
194
|
+
* other field stay with the listing that asked for them.
|
|
195
|
+
*/
|
|
196
|
+
export function mergeListedSourceState(current: SourceInfo[], res: { sources?: unknown }): SourceInfo[] {
|
|
197
|
+
const listed = Array.isArray(res.sources) ? (res.sources as SourceInfo[]) : [];
|
|
198
|
+
if (listed.length === 0 || current.length === 0) return current;
|
|
199
|
+
let out = current;
|
|
200
|
+
for (const fresh of listed) {
|
|
201
|
+
const i = current.findIndex((s) => s.uri_root === fresh.uri_root);
|
|
202
|
+
if (i < 0) continue;
|
|
203
|
+
const old = current[i];
|
|
204
|
+
const patch: SourceInfo = {
|
|
205
|
+
...old,
|
|
206
|
+
live: fresh.live,
|
|
207
|
+
live_reason: fresh.live_reason,
|
|
208
|
+
live_not_ready: fresh.live_not_ready,
|
|
209
|
+
sync_status: fresh.sync_status,
|
|
210
|
+
last_error: fresh.last_error,
|
|
211
|
+
};
|
|
212
|
+
if (patch.live === old.live && patch.live_reason === old.live_reason
|
|
213
|
+
&& patch.live_not_ready === old.live_not_ready && patch.sync_status === old.sync_status
|
|
214
|
+
&& patch.last_error === old.last_error) continue;
|
|
215
|
+
if (out === current) out = current.slice();
|
|
216
|
+
out[i] = patch;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
188
221
|
function applyListedNodes(
|
|
189
222
|
next: Record<string, any>,
|
|
190
223
|
res: { folders?: unknown[]; files?: unknown[] },
|
|
@@ -476,6 +509,10 @@ async function runReconcilePass(
|
|
|
476
509
|
const pruned: string[] = [];
|
|
477
510
|
let anyAuthoritative = false;
|
|
478
511
|
let incomplete = false;
|
|
512
|
+
const listedSources = (st: any): SourceInfo[] => listed.reduce(
|
|
513
|
+
(acc: SourceInfo[], entry) => (entry.ok && entry.res ? mergeListedSourceState(acc, entry.res) : acc),
|
|
514
|
+
st.sources ?? [],
|
|
515
|
+
);
|
|
479
516
|
|
|
480
517
|
for (const entry of listed) {
|
|
481
518
|
if (!entry.ok || !entry.res) { incomplete = true; continue; }
|
|
@@ -504,7 +541,7 @@ async function runReconcilePass(
|
|
|
504
541
|
if (!anyAuthoritative) {
|
|
505
542
|
// Every listing failed or was truncated → upsert-only, no absence pass.
|
|
506
543
|
return withPendingDelta(
|
|
507
|
-
{ files: evictColdSubtrees(next, state.expandedFolders, state.openFiles), dirtySources, loadedFolders },
|
|
544
|
+
{ files: evictColdSubtrees(next, state.expandedFolders, state.openFiles), dirtySources, loadedFolders, sources: listedSources(state) },
|
|
508
545
|
state.pendingReviewPaths,
|
|
509
546
|
pendingDelta,
|
|
510
547
|
);
|
|
@@ -605,6 +642,7 @@ async function runReconcilePass(
|
|
|
605
642
|
out.fileContentByPath = cbp; out.fileBaseContentByPath = bbp;
|
|
606
643
|
out.fileErrorByPath = ebp; out.filePrevByPath = pbp;
|
|
607
644
|
}
|
|
645
|
+
out.sources = listedSources(state);
|
|
608
646
|
return withPendingDelta(out, state.pendingReviewPaths, pendingDelta);
|
|
609
647
|
});
|
|
610
648
|
}
|
|
@@ -810,7 +848,7 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
810
848
|
force?: boolean;
|
|
811
849
|
}) => Promise<void>;
|
|
812
850
|
/**
|
|
813
|
-
*
|
|
851
|
+
* Reconcile the open (expanded) folders of ONE source against
|
|
814
852
|
* the connector's CURRENT truth, in response to an EXTERNAL file change SSE event.
|
|
815
853
|
* Re-lists each expanded+loaded folder under the source and, per direct child
|
|
816
854
|
* absent from the fresh (non-truncated) listing: prunes an `unindexed` external-
|
|
@@ -900,6 +938,13 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
900
938
|
activeFolderPath: string | null;
|
|
901
939
|
|
|
902
940
|
uploading: boolean;
|
|
941
|
+
/**
|
|
942
|
+
* Bumped once per burst of `devices` frames (and on a hub reconnect): a
|
|
943
|
+
* paired device changed state. Consumers holding their own device fetch —
|
|
944
|
+
* the source form's device `select` — re-read on it. Not per context and
|
|
945
|
+
* never persisted.
|
|
946
|
+
*/
|
|
947
|
+
deviceRevision: number;
|
|
903
948
|
workspacePanels: Record<WorkspacePanelId, boolean>;
|
|
904
949
|
// `'git-diff'` (G2.2) and `'git-commit'` (G2.4/C2 — the multi-file stacked
|
|
905
950
|
// commit/outgoing view) are VIEWS, not toggleable panels — they live outside
|
|
@@ -925,6 +970,22 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
925
970
|
*/
|
|
926
971
|
pendingSourceSuggestion: DiscoverableSourceSuggestion | null;
|
|
927
972
|
setPendingSourceSuggestion: (suggestion: DiscoverableSourceSuggestion | null) => void;
|
|
973
|
+
/**
|
|
974
|
+
* The attachable-but-unattached source suggestions (`GET sources/discoverable`)
|
|
975
|
+
* for ONE (agent, project) pair — the ONE owner both consumers read: the
|
|
976
|
+
* tree's Discover dock (`useDiscoverableSources`) and the Sources panel (its
|
|
977
|
+
* list + the folder picker's Suggested row). `key` names the pair the answer
|
|
978
|
+
* belongs to; a reader on another pair sees nothing. Not persisted.
|
|
979
|
+
*/
|
|
980
|
+
discoverable: { key: string; suggestions: readonly DiscoverableSourceSuggestion[] } | null;
|
|
981
|
+
/**
|
|
982
|
+
* Fetch the suggestions for a pair: an answer already held for it (or one in
|
|
983
|
+
* flight) is reused, so opening Files costs ONE request whoever asks first;
|
|
984
|
+
* `fresh` re-reads (the panel's Refresh, after a mutation). Fail-soft: a
|
|
985
|
+
* failure holds an empty list for the pair (no retry loop) until the next
|
|
986
|
+
* fresh read or a context change.
|
|
987
|
+
*/
|
|
988
|
+
loadDiscoverable: (ctx: { agentId: string; projectId?: string }, opts?: { fresh?: boolean }) => Promise<readonly DiscoverableSourceSuggestion[]>;
|
|
928
989
|
/**
|
|
929
990
|
* Is the Sources panel's Vector Health section open? In the STORE so a
|
|
930
991
|
* notification's `open-vector-health` nav can open it while the panel is not
|
|
@@ -1088,9 +1149,11 @@ const FILESYSTEM_WIDGET_TYPE = 'filesystem';
|
|
|
1088
1149
|
/**
|
|
1089
1150
|
* File-tree sidebar is a **fixed pixel** column (mirrors the admin widget):
|
|
1090
1151
|
* its width is independent of the outer widget size, so resizing the widget in
|
|
1091
|
-
* the workspace never reflows the tree
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1152
|
+
* the workspace never reflows the tree until the widget edge reaches it — then
|
|
1153
|
+
* the rendered width is capped to the container and grows back to this stored
|
|
1154
|
+
* width on widening. The width is user-draggable and survives reloads via
|
|
1155
|
+
* localStorage. The drag handle clamps the upper bound to the live container
|
|
1156
|
+
* width so the tree can fully cover the file editor.
|
|
1094
1157
|
*/
|
|
1095
1158
|
export const FILESYSTEM_SIDEBAR_MIN_WIDTH = 120;
|
|
1096
1159
|
const SIDEBAR_WIDTH_DEFAULT = 240;
|
|
@@ -1138,6 +1201,11 @@ export type FilesystemStoreInit = {
|
|
|
1138
1201
|
|
|
1139
1202
|
export type FilesystemStoreApi = ZustandStoreApi<FilesystemStore>;
|
|
1140
1203
|
|
|
1204
|
+
/** The (agent, project) pair a discoverable answer belongs to. */
|
|
1205
|
+
export function discoverableKey(agentId: string, projectId: string | undefined): string {
|
|
1206
|
+
return `${agentId}\n${projectId ?? ''}`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1141
1209
|
/**
|
|
1142
1210
|
* ONE Files store per window: a live view that persists nothing itself — the
|
|
1143
1211
|
* ONE writer of `neuralis.filesystem.tabs` is `filesPersistence` below, which a
|
|
@@ -1149,6 +1217,8 @@ export type FilesystemStoreApi = ZustandStoreApi<FilesystemStore>;
|
|
|
1149
1217
|
export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStoreApi {
|
|
1150
1218
|
const fences = createStoreFences();
|
|
1151
1219
|
const client = init.client;
|
|
1220
|
+
// The one discoverable read in flight for this window (`loadDiscoverable`).
|
|
1221
|
+
let discoverableInFlight: { key: string; promise: Promise<readonly DiscoverableSourceSuggestion[]> } | null = null;
|
|
1152
1222
|
return createStore<FilesystemStore>()((set, get) => ({
|
|
1153
1223
|
windowId: init.windowId ?? null,
|
|
1154
1224
|
client,
|
|
@@ -1178,9 +1248,11 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
1178
1248
|
activeRepoRel: '',
|
|
1179
1249
|
activeFolderPath: 'brain://docs',
|
|
1180
1250
|
uploading: false,
|
|
1251
|
+
deviceRevision: 0,
|
|
1181
1252
|
workspacePanels: { upload: false, sources: false, packages: false, pending: false },
|
|
1182
1253
|
activeWorkspaceTab: 'file',
|
|
1183
1254
|
pendingSourceSuggestion: null,
|
|
1255
|
+
discoverable: null,
|
|
1184
1256
|
vectorHealthOpen: false,
|
|
1185
1257
|
diffViewMode: init.diffViewMode ?? 'auto',
|
|
1186
1258
|
pendingPanelFilter: {},
|
|
@@ -1253,6 +1325,24 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
1253
1325
|
return { workspacePanels: nextPanels, activeWorkspaceTab };
|
|
1254
1326
|
}),
|
|
1255
1327
|
setPendingSourceSuggestion: (suggestion) => set(() => ({ pendingSourceSuggestion: suggestion })),
|
|
1328
|
+
loadDiscoverable: (ctx, opts) => {
|
|
1329
|
+
const key = discoverableKey(ctx.agentId, ctx.projectId);
|
|
1330
|
+
const held = get().discoverable;
|
|
1331
|
+
if (!opts?.fresh && held?.key === key) return Promise.resolve(held.suggestions);
|
|
1332
|
+
if (!opts?.fresh && discoverableInFlight?.key === key) return discoverableInFlight.promise;
|
|
1333
|
+
const promise = listDiscoverableSources({ agentId: ctx.agentId, ...(ctx.projectId ? { projectId: ctx.projectId } : {}) }, client)
|
|
1334
|
+
.then((res) => res.suggestions ?? [], () => [] as DiscoverableSourceSuggestion[])
|
|
1335
|
+
.then((suggestions) => {
|
|
1336
|
+
// Only the newest read for a pair writes it; a superseded one answers its caller alone.
|
|
1337
|
+
if (discoverableInFlight?.promise === promise) {
|
|
1338
|
+
discoverableInFlight = null;
|
|
1339
|
+
set(() => ({ discoverable: { key, suggestions } }));
|
|
1340
|
+
}
|
|
1341
|
+
return suggestions as readonly DiscoverableSourceSuggestion[];
|
|
1342
|
+
});
|
|
1343
|
+
discoverableInFlight = { key, promise };
|
|
1344
|
+
return promise;
|
|
1345
|
+
},
|
|
1256
1346
|
setVectorHealthOpen: (open) => set(() => ({ vectorHealthOpen: open })),
|
|
1257
1347
|
setActiveWorkspaceTab: (tab) => set(() => ({ activeWorkspaceTab: tab })),
|
|
1258
1348
|
setDiffViewMode: (mode) => set(() => ({ diffViewMode: mode })),
|
|
@@ -1378,6 +1468,7 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
1378
1468
|
// to list must not have its (never-listed) children pruned.
|
|
1379
1469
|
loadedFolders: { ...(state.loadedFolders || {}), [uri]: true },
|
|
1380
1470
|
hydrationState: { ...(state.hydrationState || {}), [uri]: 'ready' as const },
|
|
1471
|
+
sources: mergeListedSourceState(state.sources ?? [], res),
|
|
1381
1472
|
},
|
|
1382
1473
|
state.pendingReviewPaths,
|
|
1383
1474
|
pendingDelta,
|
|
@@ -2159,7 +2250,7 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
2159
2250
|
if (files.length > 0) {
|
|
2160
2251
|
const f = files[0];
|
|
2161
2252
|
const content = extractFileContent(f);
|
|
2162
|
-
const error =
|
|
2253
|
+
const error = readFailureMessage(f);
|
|
2163
2254
|
// The file is GONE, not merely unreadable. This matters because the open-tab
|
|
2164
2255
|
// shape is PERSISTED: a tab pointing at a deleted file re-materialises its
|
|
2165
2256
|
// node (and its whole folder chain) on every page load, producing a tree row
|
|
@@ -198,6 +198,11 @@ export function createGitSlice(
|
|
|
198
198
|
...(repoRel ? { repo: repoRel } : {}),
|
|
199
199
|
}, get().client);
|
|
200
200
|
if (mySeq !== gitStatusSeq) return;
|
|
201
|
+
if (res?.ok === false) {
|
|
202
|
+
// A checkout git could not read is never shown as "No changes".
|
|
203
|
+
set(() => ({ gitStatus: null, gitTotals: null, gitLoading: false, gitError: res.error }));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
201
206
|
// `totals` rides the SAME response as the rows it sums (the server
|
|
202
207
|
// computes it after its read filter and cap), so the header can never
|
|
203
208
|
// show a number the list below contradicts.
|