@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
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
Trash2,
|
|
30
30
|
type LucideIcon,
|
|
31
31
|
} from 'lucide-react';
|
|
32
|
-
import { meetsRequires } from '@neuralis/package-system/access';
|
|
33
|
-
import { SkillLauncher } from '@neuralis/package-system/client';
|
|
32
|
+
import { isOwnerOrAdminEffective, meetsRequires } from '@neuralis/package-system/access';
|
|
33
|
+
import { SkillLauncher, createPackageApi } from '@neuralis/package-system/client';
|
|
34
34
|
import { cn } from '../lib/cn';
|
|
35
35
|
import {
|
|
36
36
|
listSourceConfigs,
|
|
@@ -46,23 +46,23 @@ import {
|
|
|
46
46
|
getSyncStatus,
|
|
47
47
|
startReembed,
|
|
48
48
|
resetSync,
|
|
49
|
-
browseDirectories,
|
|
50
49
|
listSourceKinds,
|
|
51
50
|
setSourceEnabled,
|
|
52
|
-
listDiscoverableSources,
|
|
53
51
|
fetchSourceCounts,
|
|
52
|
+
browseDirectories,
|
|
54
53
|
type SourceKindRecord,
|
|
55
54
|
type DiscoverableSourceSuggestion,
|
|
56
55
|
} from '../api';
|
|
57
|
-
import { dimmedSourceTone, getSourceVisualTone, sourceLiveReasonLabel } from '../lib/treeVisualState';
|
|
58
|
-
import { rootFieldMeta } from '../lib/sourceKindForm';
|
|
56
|
+
import { dimmedSourceTone, getSourceVisualTone, sourceLiveReasonLabel, sourceWatchBadge, syncErrorLabel } from '../lib/treeVisualState';
|
|
57
|
+
import { configFields, fieldControl, kindSummary, rootFieldMeta, selectOptionsFrom, transportChosen, type ConfigField, type OptionsRoute, type SelectOption } from '../lib/sourceKindForm';
|
|
59
58
|
import { resolveIconWithFallback, resolveLucideIcon } from '../lib/iconResolver';
|
|
60
|
-
import { useFilesystemStore } from '../store/filesystemStore';
|
|
59
|
+
import { discoverableKey, useFilesystemStore } from '../store/filesystemStore';
|
|
61
60
|
import { fileKey, sourceFromUri, sourceRootUri } from '../store/helpers';
|
|
62
61
|
import { VectorHealthPanel } from './VectorHealthPanel';
|
|
63
62
|
import { PolicyEditor } from './PolicyEditor';
|
|
64
63
|
import { SourceDeleteConfirm } from './SourceDeleteConfirm';
|
|
65
64
|
import { ReembedDialog } from './ReembedDialog';
|
|
65
|
+
import { DEVICE_FOLDER_NOT_ALLOWED, DeviceFolderAsk, FolderPicker, type FolderSuggestion } from './FolderPicker';
|
|
66
66
|
import { useFilesystemHost } from '../FilesystemHost';
|
|
67
67
|
import type {
|
|
68
68
|
SourceConfigRecord,
|
|
@@ -115,10 +115,38 @@ type AddDraftState = {
|
|
|
115
115
|
* in the Policy editor after saving.
|
|
116
116
|
*/
|
|
117
117
|
policies: PathPermissionRecord[];
|
|
118
|
-
/** R2b —
|
|
119
|
-
|
|
118
|
+
/** R2b — the folder's package folders, one per line (`.` = the whole folder; owner/admin). */
|
|
119
|
+
packageFolders: string;
|
|
120
|
+
/** The chosen device's claimed platform (its WHERE option's), for the folder hint; never submitted. */
|
|
121
|
+
transportPlatform: string;
|
|
120
122
|
};
|
|
121
123
|
|
|
124
|
+
/** No suggestions held for the current pair — one stable reference, so a selector never churns. */
|
|
125
|
+
const NO_SUGGESTIONS: readonly DiscoverableSourceSuggestion[] = [];
|
|
126
|
+
|
|
127
|
+
/** The ONE folder kind (brain-core's connector; the stored `device` picks the transport). */
|
|
128
|
+
const HOST_KIND = 'host';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `packageFolders` ⇄ the one-per-line field both forms show: `.` (or `/`) is the
|
|
132
|
+
* whole folder (`''`), every other line a folder relative to the source root,
|
|
133
|
+
* surrounding slashes dropped. The server validates what this produces.
|
|
134
|
+
*/
|
|
135
|
+
function parsePackageFolders(text: string): string[] {
|
|
136
|
+
const out: string[] = [];
|
|
137
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
138
|
+
const line = raw.trim();
|
|
139
|
+
if (!line) continue;
|
|
140
|
+
const folder = line === '.' || line === '/' ? '' : line.replace(/^\/+|\/+$/g, '');
|
|
141
|
+
if (!out.includes(folder)) out.push(folder);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatPackageFolders(folders: readonly string[] | undefined): string {
|
|
147
|
+
return (folders ?? []).map((folder) => (folder === '' ? '.' : folder)).join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
const DEFAULT_SYNC: NonNullable<UpdateSourceConfigRequest['sync']> = {
|
|
123
151
|
exclude: [],
|
|
124
152
|
trigger: 'manual',
|
|
@@ -210,8 +238,14 @@ function suggestedSourceKey(params: {
|
|
|
210
238
|
return `${base}-${ids.agentSlug || shortToken(ids.agentId)}`;
|
|
211
239
|
}
|
|
212
240
|
|
|
213
|
-
|
|
214
|
-
|
|
241
|
+
/**
|
|
242
|
+
* The scopes the form offers. A DEVICE folder (a transport chosen) is the
|
|
243
|
+
* attaching owner's user or the project's — never an agent's — mirroring the
|
|
244
|
+
* server's refusal, so the form never offers what the create would 400.
|
|
245
|
+
*/
|
|
246
|
+
function scopeOptions(meta: SourceKindRecord | null, config: Readonly<Record<string, unknown>> = {}): SourceScopeKind[] {
|
|
247
|
+
const all: SourceScopeKind[] = meta?.allowedScopes?.length ? meta.allowedScopes : ['project', 'user', 'agent'];
|
|
248
|
+
return transportChosen(meta, config) ? all.filter((scope) => scope !== 'agent') : all;
|
|
215
249
|
}
|
|
216
250
|
|
|
217
251
|
function supportsSync(meta: SourceKindRecord | null): boolean {
|
|
@@ -246,8 +280,13 @@ function defaultAccess(meta: SourceKindRecord | null): { read: boolean; write: b
|
|
|
246
280
|
};
|
|
247
281
|
}
|
|
248
282
|
|
|
249
|
-
|
|
250
|
-
|
|
283
|
+
/**
|
|
284
|
+
* A fresh add-form draft for `meta`. `scopeHint` is the Sources list tab the
|
|
285
|
+
* person is on: the new source starts there when the kind allows that scope,
|
|
286
|
+
* otherwise at the kind's own default.
|
|
287
|
+
*/
|
|
288
|
+
function buildAddDraft(meta: SourceKindRecord | null, ids: SourceKeyIds, scopeHint?: SourceScopeKind): AddDraftState {
|
|
289
|
+
const scope = scopeHint && meta?.scopeSelectable && scopeOptions(meta).includes(scopeHint) ? scopeHint : meta?.defaultScope ?? 'project';
|
|
251
290
|
const access = defaultAccess(meta);
|
|
252
291
|
return {
|
|
253
292
|
// No folder picked yet → base falls back to the kind scheme.
|
|
@@ -262,7 +301,8 @@ function buildAddDraft(meta: SourceKindRecord | null, ids: SourceKeyIds): AddDra
|
|
|
262
301
|
description: '',
|
|
263
302
|
config: defaultConfig(meta),
|
|
264
303
|
policies: [],
|
|
265
|
-
|
|
304
|
+
packageFolders: '',
|
|
305
|
+
transportPlatform: '',
|
|
266
306
|
};
|
|
267
307
|
}
|
|
268
308
|
|
|
@@ -286,8 +326,8 @@ function buildDraft(c: SourceConfigRecord): UpdateSourceConfigRequest {
|
|
|
286
326
|
source: c.source,
|
|
287
327
|
permissions: normalizePerms(c.permissions),
|
|
288
328
|
description: c.description ?? '',
|
|
289
|
-
// R2b — include in the draft so the
|
|
290
|
-
|
|
329
|
+
// R2b — include in the draft so the list participates in isDirty + PATCH.
|
|
330
|
+
packageFolders: [...(c.packageFolders ?? [])],
|
|
291
331
|
...(c.sync ? {
|
|
292
332
|
sync: {
|
|
293
333
|
exclude: [...(c.sync.exclude ?? [])],
|
|
@@ -358,7 +398,7 @@ export function binarySkipLine(count: number | undefined): string | null {
|
|
|
358
398
|
* source's own exclude/include globs and its scan cap (`usecases/list.ts`
|
|
359
399
|
* `computeSourcesRegistry` pushes them into `connector.count()`), so editing
|
|
360
400
|
* them moves the count immediately. Everything else a save can carry —
|
|
361
|
-
* description, trigger cadence, `
|
|
401
|
+
* description, trigger cadence, `packageFolders`, the whole permissions
|
|
362
402
|
* tree — leaves both numbers where they were: the indexed count is a
|
|
363
403
|
* project-scoped `countByFilter` with no per-caller policy input, and the
|
|
364
404
|
* on-disk count is physical truth.
|
|
@@ -403,8 +443,12 @@ function triggerLabel(trigger: string): string {
|
|
|
403
443
|
}
|
|
404
444
|
|
|
405
445
|
function describeConn(c: SourceConfigRecord, meta: SourceKindRecord | null): string {
|
|
446
|
+
if (c.root_effective) return c.root_effective;
|
|
406
447
|
const cfg = (c.connection.config ?? {}) as Record<string, unknown>;
|
|
407
|
-
//
|
|
448
|
+
// A folder kind names its folder: the form stores the transport (`device`)
|
|
449
|
+
// first, so "the first value" would show a device id where the path belongs.
|
|
450
|
+
if (typeof cfg.root === 'string' && cfg.root.length > 0) return cfg.root;
|
|
451
|
+
// Otherwise a schema-hint-driven summary: show the first string/number value.
|
|
408
452
|
for (const [key, value] of Object.entries(cfg)) {
|
|
409
453
|
if (typeof value === 'string' && value.length > 0) return value;
|
|
410
454
|
if (typeof value === 'number') return String(value);
|
|
@@ -412,6 +456,12 @@ function describeConn(c: SourceConfigRecord, meta: SourceKindRecord | null): str
|
|
|
412
456
|
return meta?.label ?? c.connection.kind;
|
|
413
457
|
}
|
|
414
458
|
|
|
459
|
+
/** A source this server opens through its container half (`root_effective` is the stored `root`) that has a machine half. */
|
|
460
|
+
function isDockerBind(c: SourceConfigRecord): boolean {
|
|
461
|
+
const { root, hostRoot } = (c.connection?.config ?? {}) as { root?: unknown; hostRoot?: unknown };
|
|
462
|
+
return Boolean(hostRoot) && (c.root_effective === undefined || c.root_effective === root);
|
|
463
|
+
}
|
|
464
|
+
|
|
415
465
|
function sourceSummaryToObservedConfig(
|
|
416
466
|
summary: SourceInfo,
|
|
417
467
|
meta: SourceKindRecord | null,
|
|
@@ -435,6 +485,7 @@ function sourceSummaryToObservedConfig(
|
|
|
435
485
|
...(summary.origin_package ? { origin_package: summary.origin_package } : {}),
|
|
436
486
|
...(summary.live !== undefined ? { live: summary.live } : {}),
|
|
437
487
|
...(summary.live_reason ? { live_reason: summary.live_reason } : {}),
|
|
488
|
+
...(summary.live_not_ready ? { live_not_ready: summary.live_not_ready } : {}),
|
|
438
489
|
};
|
|
439
490
|
}
|
|
440
491
|
|
|
@@ -479,23 +530,28 @@ export function SourcesPanel(props: Props) {
|
|
|
479
530
|
const setPendingSourceSuggestion = useFilesystemStore((s) => s.setPendingSourceSuggestion);
|
|
480
531
|
const refresh = useFilesystemStore((s) => s.refresh);
|
|
481
532
|
const sourceSummaries = useFilesystemStore((s) => s.sources);
|
|
533
|
+
// A `devices` frame bumps it: the device `select` re-reads its options.
|
|
534
|
+
const deviceRevision = useFilesystemStore((s) => s.deviceRevision);
|
|
482
535
|
|
|
483
536
|
// Source-key suggestion inputs: the signed-in user (for the user-scope
|
|
484
537
|
// qualifier) and the active agent's name (for the agent-scope qualifier).
|
|
485
538
|
const host = useFilesystemHost();
|
|
486
539
|
const agentsState = host.useAgents();
|
|
487
540
|
const userSlug = useMemo(() => deriveUserSlug(host.currentUser), [host.currentUser]);
|
|
488
|
-
// R2b —
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
const isOwnerEffective = (
|
|
492
|
-
|
|
541
|
+
// R2b — the package-folders field shows for exactly the callers the server's
|
|
542
|
+
// gate admits: the SAME kernel predicate (`isOwnerOrAdminEffective`), so the
|
|
543
|
+
// hide cannot drift from the lock. The server stays the floor; no signal ⇒ hide.
|
|
544
|
+
const isOwnerEffective = isOwnerOrAdminEffective({
|
|
545
|
+
priority: host.currentRole?.priority,
|
|
546
|
+
grantedFeatures: host.grantedFeatures,
|
|
547
|
+
});
|
|
548
|
+
// warded-wren — affordance mirror for the `sources.mount` gate that now covers
|
|
493
549
|
// browse and the discoverable list. Same shape as `FileTree`'s: the ONE shared
|
|
494
550
|
// kernel predicate, a copy at the boundary because the host keeps the grant
|
|
495
551
|
// list readonly, and it authorizes NOTHING — the routes stay the boundary.
|
|
496
552
|
// Hidden, never a disabled row (the S2 discipline).
|
|
497
553
|
const canMount = useMemo(
|
|
498
|
-
() => meetsRequires({ grantedFeatures: [...(host.grantedFeatures ?? [])] }, { features: ['
|
|
554
|
+
() => meetsRequires({ grantedFeatures: [...(host.grantedFeatures ?? [])] }, { features: ['sources.mount'] }),
|
|
499
555
|
[host.grantedFeatures],
|
|
500
556
|
);
|
|
501
557
|
const agentSlug = useMemo(() => {
|
|
@@ -561,18 +617,18 @@ export function SourcesPanel(props: Props) {
|
|
|
561
617
|
const healthOpen = useFilesystemStore((s) => s.vectorHealthOpen);
|
|
562
618
|
const setVectorHealthOpen = useFilesystemStore((s) => s.setVectorHealthOpen);
|
|
563
619
|
const [mountOpen, setMountOpen] = useState(false);
|
|
564
|
-
|
|
565
|
-
const [
|
|
566
|
-
|
|
567
|
-
const [
|
|
568
|
-
const [browseError, setBrowseError] = useState<string | null>(null);
|
|
569
|
-
const [dockerMounts, setDockerMounts] = useState<Array<{ name: string; path: string }>>([]);
|
|
570
|
-
const [dockerMountsLoading, setDockerMountsLoading] = useState(false);
|
|
620
|
+
/** The folder picker under the Folder field (`FolderPicker` owns its listing state). */
|
|
621
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
622
|
+
/** The device folder a Create found outside that device's allowed folders: ask, or attach anyway. */
|
|
623
|
+
const [deviceGuard, setDeviceGuard] = useState<string | null>(null);
|
|
571
624
|
const [kinds, setKinds] = useState<SourceKindRecord[]>([]);
|
|
572
|
-
const [addKind, setAddKind] = useState<string>(
|
|
625
|
+
const [addKind, setAddKind] = useState<string>(HOST_KIND);
|
|
573
626
|
const [kindsError, setKindsError] = useState<string | null>(null);
|
|
574
|
-
// W3E — discoverable (well-known but unattached) source roots
|
|
575
|
-
|
|
627
|
+
// W3E — discoverable (well-known but unattached) source roots: the window
|
|
628
|
+
// store's ONE copy, shared with the tree's Discover dock (`loadDiscoverable`).
|
|
629
|
+
const loadDiscoverable = useFilesystemStore((s) => s.loadDiscoverable);
|
|
630
|
+
const discoverableKeyNow = discoverableKey(agentId, projectId ?? undefined);
|
|
631
|
+
const discoverable = useFilesystemStore((s) => (s.discoverable?.key === discoverableKeyNow ? s.discoverable.suggestions : NO_SUGGESTIONS));
|
|
576
632
|
|
|
577
633
|
// Load registered source kinds once per mount — this is what powers the
|
|
578
634
|
// schema-driven picker (replacing the old hardcoded KIND_META).
|
|
@@ -583,10 +639,10 @@ export function SourcesPanel(props: Props) {
|
|
|
583
639
|
if (cancelled) return;
|
|
584
640
|
setKinds(res.kinds ?? []);
|
|
585
641
|
setKindsError(null);
|
|
586
|
-
const preferred = res.kinds?.find((k) => k.kind ===
|
|
642
|
+
const preferred = res.kinds?.find((k) => k.kind === HOST_KIND) ?? res.kinds?.[0] ?? null;
|
|
587
643
|
if (preferred) {
|
|
588
644
|
setAddKind(preferred.kind);
|
|
589
|
-
const draft = buildAddDraft(preferred, sourceKeyIds);
|
|
645
|
+
const draft = buildAddDraft(preferred, sourceKeyIds, activeScope);
|
|
590
646
|
autoKeyRef.current = draft.key;
|
|
591
647
|
setAddDraft(draft);
|
|
592
648
|
}
|
|
@@ -605,13 +661,22 @@ export function SourcesPanel(props: Props) {
|
|
|
605
661
|
}, [kinds]);
|
|
606
662
|
const selectedKind = kindByName.get(addKind) ?? null;
|
|
607
663
|
const selectedKindSupportsSync = supportsSync(selectedKind);
|
|
608
|
-
const rootField = rootFieldMeta(selectedKind);
|
|
664
|
+
const rootField = rootFieldMeta(selectedKind, addDraft.config, addDraft.transportPlatform);
|
|
609
665
|
const selectedKindNeedsPath = rootField.needed;
|
|
610
|
-
// The
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
// the
|
|
614
|
-
const
|
|
666
|
+
// The non-root fields in the one-page order: WHERE first (it decides what the
|
|
667
|
+
// folder means), the plain fields, the advanced ones folded.
|
|
668
|
+
const formFields = configFields(selectedKind, addDraft.config);
|
|
669
|
+
// The device the folder lives on — the transport field's value — or `null` for this server.
|
|
670
|
+
const chosenDeviceValue = formFields.transport[0] ? addDraft.config[formFields.transport[0].key] : undefined;
|
|
671
|
+
const chosenDevice = typeof chosenDeviceValue === 'string' && chosenDeviceValue ? chosenDeviceValue : null;
|
|
672
|
+
// The picker's start on this server: the discoverable folders of the host kind
|
|
673
|
+
// (already loaded with the list — no request of its own).
|
|
674
|
+
const folderSuggestions = useMemo<FolderSuggestion[]>(
|
|
675
|
+
() => discoverable
|
|
676
|
+
.filter((s) => s.suggestedKind === HOST_KIND && typeof s.suggestedConfig.root === 'string')
|
|
677
|
+
.map((s) => ({ name: s.label, path: s.suggestedConfig.root as string })),
|
|
678
|
+
[discoverable],
|
|
679
|
+
);
|
|
615
680
|
|
|
616
681
|
/**
|
|
617
682
|
* Open the Add-Source form prefilled from a discoverable suggestion.
|
|
@@ -694,33 +759,34 @@ export function SourcesPanel(props: Props) {
|
|
|
694
759
|
/** Whether the key field still holds an auto-suggestion (safe to re-suggest). */
|
|
695
760
|
const keyIsUntouched = (key: string) => key === '' || key === autoKeyRef.current;
|
|
696
761
|
|
|
697
|
-
const load = useCallback(async () => {
|
|
762
|
+
const load = useCallback(async (fresh = true) => {
|
|
698
763
|
if (!projectId) return; // Don't fetch without projectId — it would query the wrong directory
|
|
699
764
|
setLoading(true);
|
|
700
765
|
setError(null);
|
|
701
766
|
try {
|
|
702
|
-
// W3E — load source configs and discoverable suggestions in parallel.
|
|
703
|
-
|
|
767
|
+
// W3E — load source configs and discoverable suggestions in parallel. The
|
|
768
|
+
// suggestions go through the store's one owner: on open an answer the
|
|
769
|
+
// tree's dock already holds (or is fetching) is reused; every other call
|
|
770
|
+
// (Refresh, after a mutation) re-reads, so an attached root drops out.
|
|
771
|
+
const [next] = await Promise.all([
|
|
704
772
|
listSourceConfigs({
|
|
705
773
|
agentId,
|
|
706
774
|
projectId,
|
|
707
775
|
scope: activeScope,
|
|
708
776
|
}, client),
|
|
709
|
-
|
|
710
|
-
.catch(() => ({ suggestions: [] as DiscoverableSourceSuggestion[] })),
|
|
777
|
+
loadDiscoverable({ agentId, projectId }, { fresh }),
|
|
711
778
|
]);
|
|
712
779
|
setConfigs(next);
|
|
713
780
|
setDrafts(Object.fromEntries(next.map((c) => [c.source, buildDraft(c)])));
|
|
714
781
|
setExpanded((prev) => Object.fromEntries(next.map((c) => [c.source, prev[c.source] ?? false])));
|
|
715
|
-
setDiscoverable(discoverableRes.suggestions ?? []);
|
|
716
782
|
} catch (err) {
|
|
717
783
|
setError(err instanceof Error ? err.message : 'Failed to load sources');
|
|
718
784
|
} finally {
|
|
719
785
|
setLoading(false);
|
|
720
786
|
}
|
|
721
|
-
}, [activeScope, agentId, projectId]);
|
|
787
|
+
}, [activeScope, agentId, projectId, loadDiscoverable]);
|
|
722
788
|
|
|
723
|
-
useEffect(() => { if (open) void load(); }, [open, load]);
|
|
789
|
+
useEffect(() => { if (open) void load(false); }, [open, load]);
|
|
724
790
|
|
|
725
791
|
// On panel open, do a COUNT-FREE refresh: the connector-first list merge still
|
|
726
792
|
// populates `sourceSummaries` (`s.sources`) with source discovery (observed-only
|
|
@@ -790,21 +856,6 @@ export function SourcesPanel(props: Props) {
|
|
|
790
856
|
return () => clearTimeout(t);
|
|
791
857
|
}, [successMsg]);
|
|
792
858
|
|
|
793
|
-
// Docker mode: load available volume mounts when +Source form opens
|
|
794
|
-
useEffect(() => {
|
|
795
|
-
if (!showAddForm || !isDocker || !canMount) return;
|
|
796
|
-
setDockerMountsLoading(true);
|
|
797
|
-
browseDirectories({ path: '/mounts', agentId, ...(projectId ? { projectId } : {}) }, client)
|
|
798
|
-
.then((result) => {
|
|
799
|
-
// Filter out volumes already configured as sources
|
|
800
|
-
const configuredSources = new Set(configs.map((c) => c.source));
|
|
801
|
-
const unmounted = result.entries.filter((e) => !configuredSources.has(e.name));
|
|
802
|
-
setDockerMounts(unmounted);
|
|
803
|
-
})
|
|
804
|
-
.catch(() => setDockerMounts([]))
|
|
805
|
-
.finally(() => setDockerMountsLoading(false));
|
|
806
|
-
}, [showAddForm, isDocker, canMount, agentId, projectId, configs]);
|
|
807
|
-
|
|
808
859
|
const merged = useMemo(() => {
|
|
809
860
|
const seen = new Set<string>();
|
|
810
861
|
const rows: Array<SourceConfigRecord & {
|
|
@@ -833,6 +884,8 @@ export function SourcesPanel(props: Props) {
|
|
|
833
884
|
// the row icon + name when the connector can't reach its backing store.
|
|
834
885
|
live: s?.live,
|
|
835
886
|
live_reason: s?.live_reason,
|
|
887
|
+
live_not_ready: s?.live_not_ready,
|
|
888
|
+
watch: s?.watch,
|
|
836
889
|
kind_icon: s?.kind_icon,
|
|
837
890
|
};
|
|
838
891
|
});
|
|
@@ -840,7 +893,9 @@ export function SourcesPanel(props: Props) {
|
|
|
840
893
|
if (activeScope === 'project') {
|
|
841
894
|
for (const summary of sourceSummaries) {
|
|
842
895
|
const source = sourceFromUri(summary.uri_root);
|
|
843
|
-
|
|
896
|
+
// A source configured at the user or agent scope is edited on its own
|
|
897
|
+
// tab — only one with no config at all is runtime-observed here.
|
|
898
|
+
if (!source || seen.has(source) || (summary.config_scope !== undefined && summary.config_scope !== 'project')) continue;
|
|
844
899
|
const meta = kindByName.get(summary.source_kind) ?? null;
|
|
845
900
|
const counts = countsBySource[source] ?? summary;
|
|
846
901
|
rows.push({
|
|
@@ -895,13 +950,13 @@ export function SourcesPanel(props: Props) {
|
|
|
895
950
|
// "no change" server-side and would preserve the old config. Send an
|
|
896
951
|
// explicit clear flag when the source HAD sync and the draft turned it off.
|
|
897
952
|
const clearSync = !draft.sync && !!config.sync;
|
|
898
|
-
// R2b — `
|
|
899
|
-
// server. `buildDraft` always carries it, so STRIP it for a non-owner
|
|
900
|
-
//
|
|
953
|
+
// R2b — `packageFolders` is owner/admin-effective + folder-kind-only on
|
|
954
|
+
// the server. `buildDraft` always carries it, so STRIP it for a non-owner
|
|
955
|
+
// (or another kind) to avoid tripping the server's owner-only gate on an
|
|
901
956
|
// unrelated description/sync edit. Owner-effective callers send the real value.
|
|
902
|
-
const
|
|
903
|
-
const {
|
|
904
|
-
const payload =
|
|
957
|
+
const canEditPackageFolders = isOwnerEffective && config.connection.kind === HOST_KIND;
|
|
958
|
+
const { packageFolders: _pf, ...draftRest } = draft;
|
|
959
|
+
const payload = canEditPackageFolders ? draft : draftRest;
|
|
905
960
|
const updated = await updateSourceConfig({
|
|
906
961
|
agentId,
|
|
907
962
|
...(projectId ? { projectId } : {}),
|
|
@@ -1002,17 +1057,18 @@ export function SourcesPanel(props: Props) {
|
|
|
1002
1057
|
// the same time. Keep every hook above the guard.
|
|
1003
1058
|
if (!open) return null;
|
|
1004
1059
|
|
|
1005
|
-
const handleSync = async (source: string) => {
|
|
1060
|
+
const handleSync = async (source: string, confirmEmpty = false) => {
|
|
1006
1061
|
setSyncingSource(source);
|
|
1007
1062
|
setError(null);
|
|
1008
1063
|
try {
|
|
1009
|
-
await triggerSourceSync({ agentId, ...(projectId ? { projectId } : {}), source }, client);
|
|
1064
|
+
await triggerSourceSync({ agentId, ...(projectId ? { projectId } : {}), source, ...(confirmEmpty ? { confirmEmpty } : {}) }, client);
|
|
1010
1065
|
await pollSyncJob(source);
|
|
1011
1066
|
await load();
|
|
1012
1067
|
await refresh({ agentId, projectId, withCounts: true });
|
|
1013
1068
|
clearCachedCounts(source);
|
|
1014
1069
|
} catch (err) {
|
|
1015
|
-
|
|
1070
|
+
await load(); // the row's last-sync error (and its Sync anyway) comes from the reloaded config
|
|
1071
|
+
setError(err instanceof Error ? syncErrorLabel(err.message) : 'Sync failed');
|
|
1016
1072
|
} finally {
|
|
1017
1073
|
setSyncingSource(null);
|
|
1018
1074
|
}
|
|
@@ -1147,25 +1203,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1147
1203
|
}
|
|
1148
1204
|
};
|
|
1149
1205
|
|
|
1150
|
-
const
|
|
1151
|
-
setBrowseOpen(true);
|
|
1152
|
-
setBrowseLoading(true);
|
|
1153
|
-
setBrowseError(null);
|
|
1154
|
-
try {
|
|
1155
|
-
const result = await browseDirectories({ path, agentId, ...(projectId ? { projectId } : {}) }, client);
|
|
1156
|
-
setBrowseEntries(result.entries);
|
|
1157
|
-
setBrowsePath(path);
|
|
1158
|
-
} catch {
|
|
1159
|
-
// One stable sentence, matching the route: the server never returns the
|
|
1160
|
-
// path or the errno, so there is nothing here worth surfacing verbatim.
|
|
1161
|
-
setBrowseError('That path is not available.');
|
|
1162
|
-
setBrowseEntries([]);
|
|
1163
|
-
} finally {
|
|
1164
|
-
setBrowseLoading(false);
|
|
1165
|
-
}
|
|
1166
|
-
};
|
|
1167
|
-
|
|
1168
|
-
const selectBrowsePath = (path: string) => {
|
|
1206
|
+
const selectPickedFolder = (path: string) => {
|
|
1169
1207
|
setAddDraft((d) => {
|
|
1170
1208
|
// Re-suggest the key from the picked folder's name — but only while the
|
|
1171
1209
|
// key is still an auto-suggestion. A manually-typed key is preserved.
|
|
@@ -1174,18 +1212,88 @@ export function SourcesPanel(props: Props) {
|
|
|
1174
1212
|
autoKeyRef.current = key;
|
|
1175
1213
|
return { ...d, path, key };
|
|
1176
1214
|
});
|
|
1177
|
-
|
|
1215
|
+
setPickerOpen(false);
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
/** One non-root config field (WHERE, a plain field, an advanced one) — its control from the declaration. */
|
|
1219
|
+
const renderConfigField = ({ key, prop, label, help, placeholder }: ConfigField) => {
|
|
1220
|
+
const field = fieldControl(prop, selectedKind?.packageId ?? '');
|
|
1221
|
+
const isTransport = formFields.transport.some((f) => f.key === key);
|
|
1222
|
+
const setValue = (value: string, option?: SelectOption) => {
|
|
1223
|
+
setAddDraft((prev) => {
|
|
1224
|
+
const d = isTransport ? { ...prev, transportPlatform: option?.platform ?? '' } : prev;
|
|
1225
|
+
const config = { ...d.config, [key]: value };
|
|
1226
|
+
const before = transportChosen(selectedKind, d.config);
|
|
1227
|
+
const after = transportChosen(selectedKind, config);
|
|
1228
|
+
if (before === after) return { ...d, config };
|
|
1229
|
+
// Choosing a device makes the folder its owner's own by default
|
|
1230
|
+
// (the server's default too): user scope, never agent, and
|
|
1231
|
+
// nothing granted until the owner widens it — a device folder
|
|
1232
|
+
// at project scope would otherwise open somebody's machine to
|
|
1233
|
+
// every member the moment it exists. Back to this server: the
|
|
1234
|
+
// kind's own defaults.
|
|
1235
|
+
return after
|
|
1236
|
+
? { ...d, config, scope: 'user' as SourceScopeKind, read: false, write: false, exec: false }
|
|
1237
|
+
: { ...d, config, scope: selectedKind?.defaultScope ?? 'project', ...defaultAccess(selectedKind) };
|
|
1238
|
+
});
|
|
1239
|
+
// A different machine: the open picker listed the other one.
|
|
1240
|
+
if (isTransport) setPickerOpen(false);
|
|
1241
|
+
};
|
|
1242
|
+
return (
|
|
1243
|
+
<div key={key} className="space-y-1">
|
|
1244
|
+
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">{label}</div>
|
|
1245
|
+
{field.control === 'select' ? (
|
|
1246
|
+
<OptionsRouteSelect
|
|
1247
|
+
route={field.options}
|
|
1248
|
+
{...(field.emptyLabel ? { emptyLabel: field.emptyLabel } : {})}
|
|
1249
|
+
revision={deviceRevision}
|
|
1250
|
+
projectId={projectId}
|
|
1251
|
+
agentId={agentId}
|
|
1252
|
+
value={String(addDraft.config[key] ?? '')}
|
|
1253
|
+
onChange={setValue}
|
|
1254
|
+
/>
|
|
1255
|
+
) : (
|
|
1256
|
+
<input
|
|
1257
|
+
type={field.numeric ? 'number' : 'text'}
|
|
1258
|
+
value={String(addDraft.config[key] ?? '')}
|
|
1259
|
+
onChange={(e) => setValue(e.target.value)}
|
|
1260
|
+
placeholder={placeholder ?? key}
|
|
1261
|
+
className={inputCls}
|
|
1262
|
+
/>
|
|
1263
|
+
)}
|
|
1264
|
+
{help ? <div className="text-[8px] text-white/30 italic">{help}</div> : null}
|
|
1265
|
+
{field.control === 'text' && field.refused ? (
|
|
1266
|
+
<div className="text-[9px] text-amber-300/80">{field.refused}</div>
|
|
1267
|
+
) : null}
|
|
1268
|
+
</div>
|
|
1269
|
+
);
|
|
1178
1270
|
};
|
|
1179
1271
|
|
|
1180
|
-
const handleAddSource = async () => {
|
|
1272
|
+
const handleAddSource = async (attachAnyway = false) => {
|
|
1181
1273
|
if (!addDraft.key || (selectedKindNeedsPath && !addDraft.path)) return;
|
|
1182
1274
|
setAddCreating(true);
|
|
1183
1275
|
setError(null);
|
|
1184
1276
|
try {
|
|
1277
|
+
// A folder outside what the device allows would attach and then answer
|
|
1278
|
+
// `host_fs_denied` on every read: say so BEFORE creating and offer the
|
|
1279
|
+
// ask — attaching first and granting later stays a choice. One browse of
|
|
1280
|
+
// that folder per Create (the device's clamp answers `ceiling_denied`
|
|
1281
|
+
// whether or not it exists); any other answer creates as before.
|
|
1282
|
+
if (chosenDevice && selectedKindNeedsPath && !attachAnyway) {
|
|
1283
|
+
const verdict = await browseDirectories({ path: addDraft.path, agentId, ...(projectId ? { projectId } : {}), device: chosenDevice }, client).catch(() => null);
|
|
1284
|
+
if (verdict?.state === 'ceiling_denied') {
|
|
1285
|
+
setDeviceGuard(addDraft.path);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
setDeviceGuard(null);
|
|
1290
|
+
// An optional field left empty is not sent: an empty `device` would be a
|
|
1291
|
+
// malformed device id, not "this server".
|
|
1185
1292
|
const config = {
|
|
1186
|
-
...addDraft.config,
|
|
1293
|
+
...Object.fromEntries(Object.entries(addDraft.config).filter(([, value]) => value !== '')),
|
|
1187
1294
|
...(selectedKindNeedsPath ? { root: addDraft.path } : {}),
|
|
1188
1295
|
};
|
|
1296
|
+
const packageFolders = parsePackageFolders(addDraft.packageFolders);
|
|
1189
1297
|
await createSource({
|
|
1190
1298
|
agentId,
|
|
1191
1299
|
...(projectId ? { projectId } : {}),
|
|
@@ -1208,15 +1316,16 @@ export function SourcesPanel(props: Props) {
|
|
|
1208
1316
|
...(linesToList(addDraft.include).length > 0 ? { include: linesToList(addDraft.include) } : {}),
|
|
1209
1317
|
},
|
|
1210
1318
|
} : {}),
|
|
1211
|
-
// R2b — only send when an owner/admin-effective caller
|
|
1212
|
-
//
|
|
1213
|
-
...(isOwnerEffective && addKind ===
|
|
1214
|
-
? {
|
|
1319
|
+
// R2b — only send when an owner/admin-effective caller listed package
|
|
1320
|
+
// folders. The server re-gates (owner/admin + folder kind only).
|
|
1321
|
+
...(isOwnerEffective && addKind === HOST_KIND && packageFolders.length > 0
|
|
1322
|
+
? { packageFolders }
|
|
1215
1323
|
: {}),
|
|
1216
1324
|
}, client);
|
|
1217
1325
|
setShowAddForm(false);
|
|
1326
|
+
setPickerOpen(false);
|
|
1218
1327
|
{
|
|
1219
|
-
const reset = buildAddDraft(selectedKind, sourceKeyIds);
|
|
1328
|
+
const reset = buildAddDraft(selectedKind, sourceKeyIds, activeScope);
|
|
1220
1329
|
autoKeyRef.current = reset.key;
|
|
1221
1330
|
setAddDraft(reset);
|
|
1222
1331
|
}
|
|
@@ -1249,7 +1358,16 @@ export function SourcesPanel(props: Props) {
|
|
|
1249
1358
|
<div className="inline-flex items-center gap-0.5">
|
|
1250
1359
|
<button
|
|
1251
1360
|
type="button"
|
|
1252
|
-
onClick={() =>
|
|
1361
|
+
onClick={() => {
|
|
1362
|
+
// A new source starts on the Sources tab the person is looking at
|
|
1363
|
+
// (when the kind allows that scope and the draft is untouched).
|
|
1364
|
+
if (!showAddForm && keyIsUntouched(addDraft.key) && !addDraft.path) {
|
|
1365
|
+
const draft = buildAddDraft(selectedKind, sourceKeyIds, activeScope);
|
|
1366
|
+
autoKeyRef.current = draft.key;
|
|
1367
|
+
setAddDraft(draft);
|
|
1368
|
+
}
|
|
1369
|
+
setShowAddForm((v) => !v);
|
|
1370
|
+
}}
|
|
1253
1371
|
className="h-7 px-2 rounded-l-md rounded-r-none text-cyan-300 hover:text-cyan-200 hover:bg-white/[0.05] transition inline-flex items-center gap-1 text-[10px] font-medium"
|
|
1254
1372
|
title="Add source"
|
|
1255
1373
|
>
|
|
@@ -1285,7 +1403,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1285
1403
|
|
|
1286
1404
|
{/* Description */}
|
|
1287
1405
|
<div className="mt-2 text-[10px] text-white/35 italic">
|
|
1288
|
-
Add filesystem-style sources from Neuralis packages. A source URI such as <code className="font-mono">neuralis://</code> or <code className="font-mono"
|
|
1406
|
+
Add filesystem-style sources from Neuralis packages. A source URI such as <code className="font-mono">neuralis://</code> or <code className="font-mono"><machine-source-slug>://</code> is the stable handle agents use with fs_* tools.
|
|
1289
1407
|
</div>
|
|
1290
1408
|
|
|
1291
1409
|
<div className="mt-2 flex items-center gap-1 rounded-md bg-white/[0.025] p-1">
|
|
@@ -1314,13 +1432,14 @@ export function SourcesPanel(props: Props) {
|
|
|
1314
1432
|
<div className="mt-2 rounded-md bg-red-500/8 px-3 py-2 text-[10px] text-red-200/80">{error}</div>
|
|
1315
1433
|
)}
|
|
1316
1434
|
|
|
1317
|
-
{/* Source wizard (inline)
|
|
1435
|
+
{/* Source wizard (inline) — one page, in the order a person decides:
|
|
1436
|
+
kind → where → folder → who/what → sync → advanced. */}
|
|
1318
1437
|
{showAddForm && (
|
|
1319
1438
|
<div className="mt-2 rounded-lg bg-white/[0.03] border border-white/[0.08] p-3 space-y-2">
|
|
1320
1439
|
<div className="text-[11px] font-semibold text-white/80">Add Source</div>
|
|
1321
1440
|
{kindsError && (
|
|
1322
1441
|
<div className="text-[9px] text-rose-300/80 bg-rose-500/10 rounded px-2 py-1">
|
|
1323
|
-
Could not load source kinds: {kindsError}. Falling back to
|
|
1442
|
+
Could not load source kinds: {kindsError}. Falling back to the host folder kind.
|
|
1324
1443
|
</div>
|
|
1325
1444
|
)}
|
|
1326
1445
|
{kinds.length > 1 && (
|
|
@@ -1332,7 +1451,8 @@ export function SourcesPanel(props: Props) {
|
|
|
1332
1451
|
const nextKind = e.target.value;
|
|
1333
1452
|
const nextMeta = kindByName.get(nextKind) ?? null;
|
|
1334
1453
|
setAddKind(nextKind);
|
|
1335
|
-
|
|
1454
|
+
setPickerOpen(false);
|
|
1455
|
+
const draft = buildAddDraft(nextMeta, sourceKeyIds, activeScope);
|
|
1336
1456
|
autoKeyRef.current = draft.key;
|
|
1337
1457
|
setAddDraft(draft);
|
|
1338
1458
|
}}
|
|
@@ -1345,10 +1465,64 @@ export function SourcesPanel(props: Props) {
|
|
|
1345
1465
|
))}
|
|
1346
1466
|
</select>
|
|
1347
1467
|
<div className="text-[9px] text-white/30 italic">
|
|
1348
|
-
{selectedKind?.description ?? ''}
|
|
1468
|
+
{kindSummary(selectedKind) ?? selectedKind?.description ?? ''}
|
|
1469
|
+
{selectedKind?.runtimeGated ? ' Serves only while its runtime runs.' : ''}
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
)}
|
|
1473
|
+
{formFields.transport.map(renderConfigField)}
|
|
1474
|
+
<div className="text-[10px] text-white/35 italic">
|
|
1475
|
+
{!selectedKindNeedsPath
|
|
1476
|
+
? 'This connector does not need a local path. Choose the scope; the package owns the runtime binding.'
|
|
1477
|
+
: !rootField.container
|
|
1478
|
+
? 'A folder on your own paired device. Browse opens the typed folder, or starts from the folders that device allows this server; for another one, ask on that device (tray → Allow a folder…, or neuralis device grant).'
|
|
1479
|
+
: isDocker
|
|
1480
|
+
? 'A folder under /mounts — this container’s bind mounts, or a folder inside one. Not there? Add a mount on the host (neuralis mount add) and restart, or attach it from your own device under Where.'
|
|
1481
|
+
: 'On this server: the project’s own folder (an owner may attach a protected zone). Another folder on your computer: choose your device under Where.'}
|
|
1482
|
+
</div>
|
|
1483
|
+
{selectedKindNeedsPath && (
|
|
1484
|
+
<div className="space-y-1">
|
|
1485
|
+
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">{rootField.label}</div>
|
|
1486
|
+
<div className="flex gap-1">
|
|
1487
|
+
<input
|
|
1488
|
+
type="text"
|
|
1489
|
+
value={addDraft.path}
|
|
1490
|
+
onChange={(e) => setAddDraft((d) => ({ ...d, path: e.target.value }))}
|
|
1491
|
+
placeholder={isDocker && rootField.container ? '/mounts/<name>' : rootField.placeholder}
|
|
1492
|
+
className={cn(inputCls, 'flex-1')}
|
|
1493
|
+
/>
|
|
1494
|
+
{/* Browse: a device folder through that device's broker (its
|
|
1495
|
+
owner only — the route refuses anyone else), a folder here
|
|
1496
|
+
only for a caller who could attach what they find. */}
|
|
1497
|
+
{rootField.browse && (chosenDevice || canMount) && (
|
|
1498
|
+
<button
|
|
1499
|
+
type="button"
|
|
1500
|
+
onClick={() => setPickerOpen((v) => !v)}
|
|
1501
|
+
className="h-7 px-2 rounded-md bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 hover:text-white/90 transition shrink-0"
|
|
1502
|
+
title="Browse folders"
|
|
1503
|
+
>
|
|
1504
|
+
<FolderOpen className="h-3.5 w-3.5" />
|
|
1505
|
+
</button>
|
|
1506
|
+
)}
|
|
1349
1507
|
</div>
|
|
1350
1508
|
</div>
|
|
1351
1509
|
)}
|
|
1510
|
+
{pickerOpen && selectedKindNeedsPath && (
|
|
1511
|
+
<FolderPicker
|
|
1512
|
+
key={chosenDevice ?? 'server'}
|
|
1513
|
+
agentId={agentId}
|
|
1514
|
+
projectId={projectId}
|
|
1515
|
+
client={client}
|
|
1516
|
+
device={chosenDevice}
|
|
1517
|
+
isDocker={isDocker === true}
|
|
1518
|
+
startPath={addDraft.path}
|
|
1519
|
+
suggestions={chosenDevice ? [] : folderSuggestions}
|
|
1520
|
+
wantWrite={addDraft.write}
|
|
1521
|
+
onSelect={selectPickedFolder}
|
|
1522
|
+
onClose={() => setPickerOpen(false)}
|
|
1523
|
+
/>
|
|
1524
|
+
)}
|
|
1525
|
+
{formFields.fields.map(renderConfigField)}
|
|
1352
1526
|
{selectedKind?.scopeSelectable && (
|
|
1353
1527
|
<div className="space-y-1">
|
|
1354
1528
|
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">Scope</div>
|
|
@@ -1367,197 +1541,27 @@ export function SourcesPanel(props: Props) {
|
|
|
1367
1541
|
}}
|
|
1368
1542
|
className={selectCls}
|
|
1369
1543
|
>
|
|
1370
|
-
{scopeOptions(selectedKind).map((scope) => (
|
|
1544
|
+
{scopeOptions(selectedKind, addDraft.config).map((scope) => (
|
|
1371
1545
|
<option key={scope} value={scope}>{scope}</option>
|
|
1372
1546
|
))}
|
|
1373
1547
|
</select>
|
|
1374
1548
|
</div>
|
|
1375
1549
|
)}
|
|
1376
|
-
{showContainerPathChrome && isDocker ? (
|
|
1377
|
-
<>
|
|
1378
|
-
<div className="text-[10px] text-amber-300/70 bg-amber-500/8 rounded px-2 py-1.5">
|
|
1379
|
-
Docker mode: host folders still need the CLI mount script:{' '}
|
|
1380
|
-
<code className="text-amber-200/90 font-mono">neuralis mount add</code>{' '}
|
|
1381
|
-
on the host, then restart the container. Package sources like Machine do not require a host path here.
|
|
1382
|
-
</div>
|
|
1383
|
-
{/* Unmounted volume mounts */}
|
|
1384
|
-
{dockerMountsLoading ? (
|
|
1385
|
-
<div className="flex items-center gap-1.5 py-1">
|
|
1386
|
-
<Loader2 className="h-3 w-3 animate-spin text-white/30" />
|
|
1387
|
-
<span className="text-[9px] text-white/35">Scanning volume mounts...</span>
|
|
1388
|
-
</div>
|
|
1389
|
-
) : dockerMounts.length > 0 ? (
|
|
1390
|
-
<div className="space-y-1">
|
|
1391
|
-
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">
|
|
1392
|
-
Available volume mounts (not yet configured)
|
|
1393
|
-
</div>
|
|
1394
|
-
{dockerMounts.map((mount) => (
|
|
1395
|
-
<div key={mount.path} className="flex items-center gap-2 rounded px-2 py-1.5 bg-white/[0.02] hover:bg-white/[0.04] transition">
|
|
1396
|
-
<Folder className="h-3 w-3 text-cyan-400/70 shrink-0" />
|
|
1397
|
-
<span className="text-[10px] text-white/75 font-medium flex-1 truncate">{mount.name}</span>
|
|
1398
|
-
<span className="text-[9px] text-white/30 font-mono truncate max-w-[120px]">{mount.path}</span>
|
|
1399
|
-
<button
|
|
1400
|
-
type="button"
|
|
1401
|
-
onClick={() => {
|
|
1402
|
-
setAddDraft((d) => ({ ...d, key: mount.name, path: mount.path }));
|
|
1403
|
-
}}
|
|
1404
|
-
className="px-2 py-0.5 rounded text-[8px] font-semibold text-cyan-300/80 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-400/20 transition shrink-0"
|
|
1405
|
-
>
|
|
1406
|
-
+ Add
|
|
1407
|
-
</button>
|
|
1408
|
-
</div>
|
|
1409
|
-
))}
|
|
1410
|
-
</div>
|
|
1411
|
-
) : (
|
|
1412
|
-
<div className="text-[9px] text-white/30 italic">
|
|
1413
|
-
All volume mounts are configured as sources.
|
|
1414
|
-
</div>
|
|
1415
|
-
)}
|
|
1416
|
-
</>
|
|
1417
|
-
) : (
|
|
1418
|
-
<div className="text-[10px] text-white/35 italic">
|
|
1419
|
-
{!selectedKindNeedsPath
|
|
1420
|
-
? 'This connector does not need a local path. Choose the scope and URI scheme; the package owns the runtime binding.'
|
|
1421
|
-
: rootField.container
|
|
1422
|
-
? 'Mount any folder from your system. The path must be accessible to the server process.'
|
|
1423
|
-
: 'A path on the operator\u2019s host machine, reached through the host broker. It needs no volume mount here, and the broker ceiling still clamps what is reachable.'}
|
|
1424
|
-
</div>
|
|
1425
|
-
)}
|
|
1426
|
-
<div className="space-y-2">
|
|
1427
|
-
<div className={cn('grid gap-2', selectedKindNeedsPath ? 'grid-cols-2' : 'grid-cols-1')}>
|
|
1428
|
-
<div className="space-y-1">
|
|
1429
|
-
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">URI scheme</div>
|
|
1430
|
-
<input
|
|
1431
|
-
type="text"
|
|
1432
|
-
value={addDraft.key}
|
|
1433
|
-
onChange={(e) => setAddDraft((d) => ({ ...d, key: e.target.value }))}
|
|
1434
|
-
placeholder={computeSuggestedKey(addDraft.scope, addDraft.path)}
|
|
1435
|
-
className={inputCls}
|
|
1436
|
-
/>
|
|
1437
|
-
<div className="text-[8px] text-white/25 font-mono">{addDraft.key || computeSuggestedKey(addDraft.scope, addDraft.path)}://</div>
|
|
1438
|
-
</div>
|
|
1439
|
-
{selectedKindNeedsPath && (
|
|
1440
|
-
<div className="space-y-1">
|
|
1441
|
-
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">{rootField.label}</div>
|
|
1442
|
-
<div className="flex gap-1">
|
|
1443
|
-
<input
|
|
1444
|
-
type="text"
|
|
1445
|
-
value={addDraft.path}
|
|
1446
|
-
onChange={(e) => setAddDraft((d) => ({ ...d, path: e.target.value }))}
|
|
1447
|
-
placeholder={rootField.placeholder}
|
|
1448
|
-
className={cn(inputCls, 'flex-1')}
|
|
1449
|
-
/>
|
|
1450
|
-
{/* Browse: container-plane roots only (the route readdirs in
|
|
1451
|
-
the SERVER process), outside Docker, and only for a caller
|
|
1452
|
-
who could actually attach what they find. */}
|
|
1453
|
-
{rootField.container && !isDocker && canMount && (
|
|
1454
|
-
<button
|
|
1455
|
-
type="button"
|
|
1456
|
-
onClick={() => void openBrowser(addDraft.path || '/')}
|
|
1457
|
-
className="h-7 px-2 rounded-md bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 hover:text-white/90 transition shrink-0"
|
|
1458
|
-
title="Browse directories"
|
|
1459
|
-
>
|
|
1460
|
-
<FolderOpen className="h-3.5 w-3.5" />
|
|
1461
|
-
</button>
|
|
1462
|
-
)}
|
|
1463
|
-
</div>
|
|
1464
|
-
</div>
|
|
1465
|
-
)}
|
|
1466
|
-
</div>
|
|
1467
|
-
{Object.entries(schemaObject(selectedKind).properties)
|
|
1468
|
-
.filter(([key]) => key !== 'root')
|
|
1469
|
-
.map(([key, prop]) => (
|
|
1470
|
-
<div key={key} className="space-y-1">
|
|
1471
|
-
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">
|
|
1472
|
-
{(prop as any)?.['x-neuralis']?.ui?.label ?? key}
|
|
1473
|
-
</div>
|
|
1474
|
-
<input
|
|
1475
|
-
type={(prop as any).type === 'number' || (prop as any).type === 'integer' ? 'number' : 'text'}
|
|
1476
|
-
value={String(addDraft.config[key] ?? '')}
|
|
1477
|
-
onChange={(e) => setAddDraft((d) => ({
|
|
1478
|
-
...d,
|
|
1479
|
-
config: { ...d.config, [key]: e.target.value },
|
|
1480
|
-
}))}
|
|
1481
|
-
placeholder={(prop as any)?.['x-neuralis']?.ui?.placeholder ?? (prop as any)?.description ?? key}
|
|
1482
|
-
className={inputCls}
|
|
1483
|
-
/>
|
|
1484
|
-
</div>
|
|
1485
|
-
))}
|
|
1486
|
-
{/* Directory browser (non-Docker only) */}
|
|
1487
|
-
{showContainerPathChrome && !isDocker && browseOpen && (
|
|
1488
|
-
<div className="rounded-md bg-white/[0.03] border border-white/[0.1] overflow-hidden">
|
|
1489
|
-
<div className="flex items-center gap-1.5 px-2 py-1.5 bg-white/[0.02] border-b border-white/[0.06]">
|
|
1490
|
-
<FolderOpen className="h-3 w-3 text-cyan-400 shrink-0" />
|
|
1491
|
-
<span className="text-[9px] font-mono text-white/60 truncate flex-1">{browsePath}</span>
|
|
1492
|
-
<button type="button" onClick={() => setBrowseOpen(false)}
|
|
1493
|
-
className="text-[9px] text-white/40 hover:text-white/70 px-1">Close</button>
|
|
1494
|
-
</div>
|
|
1495
|
-
{browseError && (
|
|
1496
|
-
<div className="px-2 py-1 text-[9px] text-red-300/80">{browseError}</div>
|
|
1497
|
-
)}
|
|
1498
|
-
{browseLoading ? (
|
|
1499
|
-
<div className="flex items-center justify-center py-3">
|
|
1500
|
-
<Loader2 className="h-3 w-3 animate-spin text-white/30" />
|
|
1501
|
-
</div>
|
|
1502
|
-
) : (
|
|
1503
|
-
<div className="max-h-[180px] overflow-y-auto">
|
|
1504
|
-
{browsePath !== '/' && (
|
|
1505
|
-
<button
|
|
1506
|
-
type="button"
|
|
1507
|
-
onClick={() => {
|
|
1508
|
-
const parent = browsePath.replace(/[/\\][^/\\]+$/, '') || '/';
|
|
1509
|
-
void openBrowser(parent);
|
|
1510
|
-
}}
|
|
1511
|
-
className="w-full flex items-center gap-1.5 px-2 py-1 text-left hover:bg-white/[0.04] transition"
|
|
1512
|
-
>
|
|
1513
|
-
<ChevronUp className="h-3 w-3 text-white/30" />
|
|
1514
|
-
<span className="text-[10px] text-white/50">..</span>
|
|
1515
|
-
</button>
|
|
1516
|
-
)}
|
|
1517
|
-
{browseEntries.length === 0 && !browseLoading && (
|
|
1518
|
-
<div className="px-2 py-2 text-[9px] text-white/30 italic">No subdirectories</div>
|
|
1519
|
-
)}
|
|
1520
|
-
{browseEntries.map((entry) => (
|
|
1521
|
-
<div key={entry.path} className="flex items-center gap-0.5 hover:bg-white/[0.04] transition">
|
|
1522
|
-
<button
|
|
1523
|
-
type="button"
|
|
1524
|
-
onClick={() => void openBrowser(entry.path)}
|
|
1525
|
-
className="flex items-center gap-1.5 px-2 py-1 flex-1 text-left min-w-0"
|
|
1526
|
-
>
|
|
1527
|
-
<Folder className="h-3 w-3 text-cyan-400/70 shrink-0" />
|
|
1528
|
-
<span className="text-[10px] text-white/75 truncate">{entry.name}</span>
|
|
1529
|
-
</button>
|
|
1530
|
-
<button
|
|
1531
|
-
type="button"
|
|
1532
|
-
onClick={() => selectBrowsePath(entry.path)}
|
|
1533
|
-
className="px-2 py-1 text-[8px] font-medium text-cyan-300/70 hover:text-cyan-200 shrink-0"
|
|
1534
|
-
>
|
|
1535
|
-
Select
|
|
1536
|
-
</button>
|
|
1537
|
-
</div>
|
|
1538
|
-
))}
|
|
1539
|
-
<button
|
|
1540
|
-
type="button"
|
|
1541
|
-
onClick={() => selectBrowsePath(browsePath)}
|
|
1542
|
-
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left bg-cyan-500/5 hover:bg-cyan-500/10 border-t border-white/[0.06] transition"
|
|
1543
|
-
>
|
|
1544
|
-
<CheckCircle2 className="h-3 w-3 text-cyan-400 shrink-0" />
|
|
1545
|
-
<span className="text-[10px] font-medium text-cyan-300/80">Select this folder</span>
|
|
1546
|
-
<span className="text-[9px] text-white/30 font-mono truncate ml-auto">{browsePath}</span>
|
|
1547
|
-
</button>
|
|
1548
|
-
</div>
|
|
1549
|
-
)}
|
|
1550
|
-
</div>
|
|
1551
|
-
)}
|
|
1552
|
-
</div>
|
|
1553
1550
|
<div className="flex items-center gap-2">
|
|
1554
1551
|
<span className="text-[9px] font-semibold uppercase tracking-wider text-white/35 w-16 shrink-0">Access</span>
|
|
1555
1552
|
{([
|
|
1556
1553
|
{ key: 'read' as const, label: 'Read', icon: Eye },
|
|
1557
1554
|
{ key: 'write' as const, label: 'Write', icon: Pencil },
|
|
1558
1555
|
{ key: 'exec' as const, label: 'Exec', icon: Terminal },
|
|
1559
|
-
])
|
|
1560
|
-
|
|
1556
|
+
])
|
|
1557
|
+
// Exec is a SOURCE policy bit: offered wherever a shell can run in
|
|
1558
|
+
// the kind's sources at all (`shell`, the server's answer). Who may
|
|
1559
|
+
// then run one is the caller's own feature (`execRequires`, the
|
|
1560
|
+
// tooltip) — never a reason to hide the bit from the person setting
|
|
1561
|
+
// it for others.
|
|
1562
|
+
.filter(({ key }) => key !== 'exec' || selectedKind?.shell !== false)
|
|
1563
|
+
.map(({ key, label, icon: PIcon }) => (
|
|
1564
|
+
<label key={key} title={key === 'exec' && selectedKind?.execRequires?.length ? `Running programs here needs ${selectedKind.execRequires.join(' or ')}` : undefined} className={cn(
|
|
1561
1565
|
'inline-flex items-center gap-1 rounded px-2 py-1 text-[9px] cursor-pointer transition',
|
|
1562
1566
|
addDraft[key] ? 'bg-white/[0.06] text-white/85' : 'bg-white/[0.02] text-white/40',
|
|
1563
1567
|
)}>
|
|
@@ -1568,20 +1572,25 @@ export function SourcesPanel(props: Props) {
|
|
|
1568
1572
|
</label>
|
|
1569
1573
|
))}
|
|
1570
1574
|
</div>
|
|
1571
|
-
<div className="space-y-1">
|
|
1572
|
-
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">Description <span className="text-white/20 normal-case font-normal">(optional — shown to the agent)</span></div>
|
|
1573
|
-
{/* The connector's own text as the PLACEHOLDER, not as a value:
|
|
1574
|
-
leaving it blank resolves through the same chain at read time,
|
|
1575
|
-
so the agent still gets the package's wording — and it keeps
|
|
1576
|
-
tracking the package instead of freezing a copy here. */}
|
|
1577
|
-
<input value={addDraft.description} onChange={(e) => setAddDraft((d) => ({ ...d, description: e.target.value }))} className={inputCls} placeholder={selectedKind?.description ?? 'What this source is for…'} maxLength={200} />
|
|
1578
|
-
</div>
|
|
1579
1575
|
{selectedKindSupportsSync && (
|
|
1580
1576
|
<div className="flex items-center gap-2">
|
|
1581
1577
|
<label className="inline-flex items-center gap-1.5 text-[10px] text-white/80 cursor-pointer">
|
|
1582
1578
|
<input type="checkbox" checked={addDraft.syncEnabled} onChange={(e) => setAddDraft((d) => ({ ...d, syncEnabled: e.target.checked }))} className="accent-emerald-400 h-3 w-3" />
|
|
1583
1579
|
Enable sync
|
|
1584
1580
|
</label>
|
|
1581
|
+
{addDraft.syncEnabled && (
|
|
1582
|
+
<select
|
|
1583
|
+
value={addDraft.trigger.startsWith('interval:') ? 'manual' : addDraft.trigger}
|
|
1584
|
+
onChange={(e) => setAddDraft((d) => ({ ...d, trigger: e.target.value as SyncTriggerValue }))}
|
|
1585
|
+
className={cn(selectCls, 'w-auto')}
|
|
1586
|
+
title="When the folder syncs (a schedule is set on the source after it is added)"
|
|
1587
|
+
>
|
|
1588
|
+
{addDraft.trigger.startsWith('interval:') && <option value={addDraft.trigger}>On schedule ({addDraft.trigger.slice('interval:'.length)})</option>}
|
|
1589
|
+
{TRIGGER_OPTIONS.filter((o) => o.value !== 'interval').map((o) => (
|
|
1590
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
1591
|
+
))}
|
|
1592
|
+
</select>
|
|
1593
|
+
)}
|
|
1585
1594
|
</div>
|
|
1586
1595
|
)}
|
|
1587
1596
|
{selectedKindSupportsSync && addDraft.syncEnabled && (
|
|
@@ -1597,22 +1606,48 @@ export function SourcesPanel(props: Props) {
|
|
|
1597
1606
|
<div className="text-[8px] text-white/25 italic">Non-empty: only matching files sync. Excludes always win — an excluded path can never be re-included. This narrows the INDEX, not what the agent can read: the source root does that.</div>
|
|
1598
1607
|
</div>
|
|
1599
1608
|
)}
|
|
1600
|
-
|
|
1601
|
-
<
|
|
1602
|
-
<
|
|
1609
|
+
<details className="group rounded-md bg-white/[0.02] px-2 py-1">
|
|
1610
|
+
<summary className="cursor-pointer select-none text-[9px] font-semibold uppercase tracking-wider text-white/35 hover:text-white/60">
|
|
1611
|
+
Advanced <span className="normal-case font-normal text-white/25">· {addDraft.key || computeSuggestedKey(addDraft.scope, addDraft.path)}://</span>
|
|
1612
|
+
</summary>
|
|
1613
|
+
<div className="mt-2 space-y-2 pb-1">
|
|
1614
|
+
<div className="space-y-1">
|
|
1615
|
+
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">URI scheme</div>
|
|
1603
1616
|
<input
|
|
1604
|
-
type="
|
|
1605
|
-
|
|
1606
|
-
onChange={(e) => setAddDraft((d) => ({ ...d,
|
|
1607
|
-
|
|
1617
|
+
type="text"
|
|
1618
|
+
value={addDraft.key}
|
|
1619
|
+
onChange={(e) => setAddDraft((d) => ({ ...d, key: e.target.value }))}
|
|
1620
|
+
placeholder={computeSuggestedKey(addDraft.scope, addDraft.path)}
|
|
1621
|
+
className={inputCls}
|
|
1608
1622
|
/>
|
|
1609
|
-
<
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1623
|
+
<div className="text-[8px] text-white/25 font-mono">{addDraft.key || computeSuggestedKey(addDraft.scope, addDraft.path)}://</div>
|
|
1624
|
+
</div>
|
|
1625
|
+
{formFields.advanced.map(renderConfigField)}
|
|
1626
|
+
<div className="space-y-1">
|
|
1627
|
+
<div className="text-[9px] font-semibold uppercase tracking-wider text-white/35">Description <span className="text-white/20 normal-case font-normal">(optional — shown to the agent)</span></div>
|
|
1628
|
+
{/* The connector's own text as the PLACEHOLDER, not as a value:
|
|
1629
|
+
leaving it blank resolves through the same chain at read time,
|
|
1630
|
+
so the agent still gets the package's wording — and it keeps
|
|
1631
|
+
tracking the package instead of freezing a copy here. */}
|
|
1632
|
+
<input value={addDraft.description} onChange={(e) => setAddDraft((d) => ({ ...d, description: e.target.value }))} className={inputCls} placeholder={selectedKind?.description ?? 'What this source is for…'} maxLength={200} />
|
|
1633
|
+
</div>
|
|
1634
|
+
{addKind === HOST_KIND && isOwnerEffective && (
|
|
1635
|
+
<PackageFoldersField
|
|
1636
|
+
value={addDraft.packageFolders}
|
|
1637
|
+
onChange={(packageFolders) => setAddDraft((d) => ({ ...d, packageFolders }))}
|
|
1638
|
+
note="as soon as the source is added"
|
|
1639
|
+
/>
|
|
1640
|
+
)}
|
|
1641
|
+
</div>
|
|
1642
|
+
</details>
|
|
1643
|
+
{chosenDevice && deviceGuard !== null && deviceGuard === addDraft.path && (
|
|
1644
|
+
<div className="rounded-md border border-amber-400/20 bg-amber-500/[0.04] px-2 py-1.5 space-y-1">
|
|
1645
|
+
<div className="text-[9px] text-amber-300/80">{DEVICE_FOLDER_NOT_ALLOWED} Ask there, or attach it now — it serves once the device allows it.</div>
|
|
1646
|
+
<DeviceFolderAsk key={deviceGuard} device={chosenDevice} projectId={projectId} agentId={agentId} path={deviceGuard} wantWrite={addDraft.write} />
|
|
1647
|
+
<button type="button" onClick={() => void handleAddSource(true)} disabled={addCreating}
|
|
1648
|
+
className="h-6 px-2.5 rounded-md bg-white/[0.04] border border-white/[0.1] text-[9px] font-semibold text-white/70 hover:bg-white/[0.08] transition disabled:opacity-35">
|
|
1649
|
+
Attach anyway — it serves once allowed
|
|
1650
|
+
</button>
|
|
1616
1651
|
</div>
|
|
1617
1652
|
)}
|
|
1618
1653
|
<div className="flex items-center gap-1.5 pt-1">
|
|
@@ -1637,7 +1672,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1637
1672
|
{grouped.map(([origin, rows]) => (
|
|
1638
1673
|
<div key={origin}>
|
|
1639
1674
|
{/* Origin group header — small, indented a touch LEFT of its rows,
|
|
1640
|
-
replaces the per-row "
|
|
1675
|
+
replaces the per-row "native" badge. */}
|
|
1641
1676
|
<div className="-ml-0.5 mb-0.5 flex items-center gap-1.5 px-1">
|
|
1642
1677
|
<Package className="h-3 w-3 shrink-0 text-white/30" />
|
|
1643
1678
|
<span className="truncate font-mono text-[10px] font-medium text-white/40">{origin}</span>
|
|
@@ -1669,13 +1704,14 @@ export function SourcesPanel(props: Props) {
|
|
|
1669
1704
|
const dirty = editable && isDirty(config, draft);
|
|
1670
1705
|
const tp = parseTrigger(syncDraft.trigger);
|
|
1671
1706
|
const sections = subSections[config.source] ?? {};
|
|
1707
|
+
const watchBadge = sourceWatchBadge(config.watch, config.live_reason);
|
|
1672
1708
|
|
|
1673
1709
|
return (
|
|
1674
1710
|
<div key={fileKey(sourceRootUri(config.source))} className="rounded-sm">
|
|
1675
1711
|
{/* Accordion header: selection and enabled actions are siblings. */}
|
|
1676
1712
|
<div className="w-full flex items-center gap-2 py-2 text-left group hover:bg-white/[0.02] rounded-md px-1 transition">
|
|
1677
1713
|
<button type="button" onClick={() => toggleSource(config.source)} aria-expanded={isOpen}
|
|
1678
|
-
title={disconnected ? sourceLiveReasonLabel(config.live_reason) : undefined}
|
|
1714
|
+
title={disconnected ? sourceLiveReasonLabel(config.live_reason, { kind: config.connection.kind, notReady: config.live_not_ready }) : undefined}
|
|
1679
1715
|
className="min-w-0 flex-1 flex items-center gap-2 text-left">
|
|
1680
1716
|
{/* Git indicator (G2.1) — a GitBranch overlay on a git-repo
|
|
1681
1717
|
source's icon, ORTHOGONAL to the sync tone (visualTone still
|
|
@@ -1689,6 +1725,17 @@ export function SourcesPanel(props: Props) {
|
|
|
1689
1725
|
<SourceIcon className={cn('h-4 w-4 shrink-0', visualTone.iconClassName)} />
|
|
1690
1726
|
)}
|
|
1691
1727
|
<span className={cn('min-w-0 flex-1 truncate text-[12.5px] font-semibold', dimmed ? 'text-white/45' : 'text-white/90')}>{config.source}</span>
|
|
1728
|
+
{watchBadge ? (
|
|
1729
|
+
<span
|
|
1730
|
+
className={cn(
|
|
1731
|
+
'shrink-0 rounded px-1 text-[9px] uppercase tracking-wider',
|
|
1732
|
+
watchBadge.tone === 'pending' ? 'text-white/55 bg-white/[0.06]' : 'text-amber-200/80 bg-amber-500/10',
|
|
1733
|
+
)}
|
|
1734
|
+
title={watchBadge.title}
|
|
1735
|
+
>
|
|
1736
|
+
{watchBadge.label}
|
|
1737
|
+
</span>
|
|
1738
|
+
) : null}
|
|
1692
1739
|
{config.git?.isRepo && config.git.branch ? (
|
|
1693
1740
|
<span
|
|
1694
1741
|
className="inline-flex max-w-[96px] shrink-0 items-center gap-0.5 text-[10px] text-violet-300/70"
|
|
@@ -1792,10 +1839,16 @@ export function SourcesPanel(props: Props) {
|
|
|
1792
1839
|
</div>
|
|
1793
1840
|
|
|
1794
1841
|
{configSupportsSync && !config.sync?.lastSync ? (
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1842
|
+
config.sync?.lastSyncResult?.error ? (
|
|
1843
|
+
<div className="rounded-md border border-rose-300/15 bg-rose-500/5 px-2 py-1.5 text-[10px] text-rose-100/70">
|
|
1844
|
+
{syncErrorLabel(config.sync.lastSyncResult.error)}
|
|
1845
|
+
</div>
|
|
1846
|
+
) : (
|
|
1847
|
+
<div className="rounded-md border border-sky-300/15 bg-sky-500/5 px-2 py-1.5 text-[10px] text-sky-100/70">
|
|
1848
|
+
Never synced — run a sync to index this source and load its packages
|
|
1849
|
+
(skills, rules, instructions) for agents.
|
|
1850
|
+
</div>
|
|
1851
|
+
)
|
|
1799
1852
|
) : null}
|
|
1800
1853
|
|
|
1801
1854
|
{config.observed ? (
|
|
@@ -1858,27 +1911,15 @@ export function SourcesPanel(props: Props) {
|
|
|
1858
1911
|
)}
|
|
1859
1912
|
</div>
|
|
1860
1913
|
|
|
1861
|
-
{/* R2b —
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
{editable && isOwnerEffective && (meta?.kind ?? config.connection.kind) ===
|
|
1865
|
-
<
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
checked={draft.recognizesPackages ?? false}
|
|
1871
|
-
onChange={(e) => updateDraft(config.source, (d) => ({ ...d, recognizesPackages: e.target.checked }))}
|
|
1872
|
-
className="accent-cyan-400 h-3 w-3 mt-0.5"
|
|
1873
|
-
/>
|
|
1874
|
-
<span className="text-[10px] text-white/70 leading-tight">
|
|
1875
|
-
Recognize packages
|
|
1876
|
-
<span className="block text-[9px] text-white/35 normal-case font-normal">
|
|
1877
|
-
Load packages found in this folder (in addition to <code>_packages/</code>) — applied on save; advertised only within this source's scope.
|
|
1878
|
-
</span>
|
|
1879
|
-
</span>
|
|
1880
|
-
</label>
|
|
1881
|
-
</div>
|
|
1914
|
+
{/* R2b — package folders. Owner/admin-effective + folder kind
|
|
1915
|
+
only (server re-gates). Makes those folders package sources
|
|
1916
|
+
too; advertised only within the source's own scope. */}
|
|
1917
|
+
{editable && isOwnerEffective && (meta?.kind ?? config.connection.kind) === HOST_KIND && (
|
|
1918
|
+
<PackageFoldersField
|
|
1919
|
+
value={formatPackageFolders(draft.packageFolders)}
|
|
1920
|
+
onChange={(text) => updateDraft(config.source, (d) => ({ ...d, packageFolders: parsePackageFolders(text) }))}
|
|
1921
|
+
note="applied on save"
|
|
1922
|
+
/>
|
|
1882
1923
|
)}
|
|
1883
1924
|
|
|
1884
1925
|
{/* Permissions section — admin-style compact editor.
|
|
@@ -1887,7 +1928,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1887
1928
|
adjacent SkillLauncher hands the same uri-policy edit to
|
|
1888
1929
|
the agent. Its visibility is DERIVED from the host's
|
|
1889
1930
|
per-caller skill catalog (the `manage-uri-policy` skill
|
|
1890
|
-
declares `
|
|
1931
|
+
declares `sources.policy` itself) — never re-stated here. */}
|
|
1891
1932
|
<div>
|
|
1892
1933
|
<div className="flex items-center gap-1.5 py-1">
|
|
1893
1934
|
<button type="button" onClick={() => toggleSubSection(config.source, 'perms')}
|
|
@@ -2049,7 +2090,10 @@ export function SourcesPanel(props: Props) {
|
|
|
2049
2090
|
<span>Last sync: +{config.sync.lastSyncResult.filesAdded} -{config.sync.lastSyncResult.filesRemoved} ~{config.sync.lastSyncResult.filesUpdated}</span>
|
|
2050
2091
|
<span>({(config.sync.lastSyncResult.durationMs / 1000).toFixed(1)}s)</span>
|
|
2051
2092
|
{config.sync.lastSyncResult.error && (
|
|
2052
|
-
<span className="text-rose-300/80 truncate max-w-[200px]" title={config.sync.lastSyncResult.error}>err</span>
|
|
2093
|
+
<span className="text-rose-300/80 truncate max-w-[200px]" title={syncErrorLabel(config.sync.lastSyncResult.error)}>err</span>
|
|
2094
|
+
)}
|
|
2095
|
+
{config.sync.lastSyncResult.error === 'root_empty' && (
|
|
2096
|
+
<button type="button" className="text-rose-200/80 underline" onClick={() => void handleSync(config.source, true)}>Sync anyway</button>
|
|
2053
2097
|
)}
|
|
2054
2098
|
</div>
|
|
2055
2099
|
)}
|
|
@@ -2193,9 +2237,7 @@ export function SourcesPanel(props: Props) {
|
|
|
2193
2237
|
<SourceDeleteConfirm
|
|
2194
2238
|
source={config.source}
|
|
2195
2239
|
vectorCount={typeof config.indexed_count === 'number' ? config.indexed_count : null}
|
|
2196
|
-
isDockerMount={
|
|
2197
|
-
(config.connection?.config as { hostRoot?: unknown } | undefined)?.hostRoot,
|
|
2198
|
-
)}
|
|
2240
|
+
isDockerMount={isDockerBind(config)}
|
|
2199
2241
|
busy={deleting}
|
|
2200
2242
|
onCancel={() => setDeleteConfirmFor(null)}
|
|
2201
2243
|
onConfirm={(opts) => void handleDelete(config.source, opts)}
|
|
@@ -2355,3 +2397,121 @@ export function SourcesPanel(props: Props) {
|
|
|
2355
2397
|
</div>
|
|
2356
2398
|
);
|
|
2357
2399
|
}
|
|
2400
|
+
|
|
2401
|
+
// ---------------------------------------------------------------------------
|
|
2402
|
+
// `select` field: options answered by a route in the DECLARING package
|
|
2403
|
+
// ---------------------------------------------------------------------------
|
|
2404
|
+
|
|
2405
|
+
/**
|
|
2406
|
+
* A dimmed option (an offline device): a muted text colour mixed OPAQUE into
|
|
2407
|
+
* the option's own surface, inline — the global `select option` rule outranks
|
|
2408
|
+
* any utility class, while an inline colour wins. Never `opacity`: the muted
|
|
2409
|
+
* colour is computed opaque against the same surface the option paints.
|
|
2410
|
+
*/
|
|
2411
|
+
const DIMMED_OPTION_STYLE = {
|
|
2412
|
+
color: 'color-mix(in srgb, var(--control-text) 48%, rgb(var(--w-composer-bg-rgb, 30, 32, 38)))',
|
|
2413
|
+
} as const;
|
|
2414
|
+
|
|
2415
|
+
/**
|
|
2416
|
+
* A config field declared `x-neuralis.ui.control: "select"`. ONE request per
|
|
2417
|
+
* mount (and per project/agent change) to the declaring package's
|
|
2418
|
+
* `optionsRoute`, through that package's own route client — never this
|
|
2419
|
+
* package's — and ONE quiet re-read per `revision` bump (a burst of `devices`
|
|
2420
|
+
* frames): the shown list stays until the new answer replaces it. A failed
|
|
2421
|
+
* load is SHOWN, never an empty list; the list is UX only, the declaring
|
|
2422
|
+
* package re-checks the value when the source is created.
|
|
2423
|
+
*/
|
|
2424
|
+
function OptionsRouteSelect({
|
|
2425
|
+
route,
|
|
2426
|
+
emptyLabel,
|
|
2427
|
+
revision,
|
|
2428
|
+
projectId,
|
|
2429
|
+
agentId,
|
|
2430
|
+
value,
|
|
2431
|
+
onChange,
|
|
2432
|
+
}: {
|
|
2433
|
+
route: OptionsRoute;
|
|
2434
|
+
/** An optional field's empty choice (`fieldControl`), selectable — absent ⇒ a disabled "Choose…". */
|
|
2435
|
+
emptyLabel?: string;
|
|
2436
|
+
revision: number;
|
|
2437
|
+
projectId?: string | null;
|
|
2438
|
+
agentId: string;
|
|
2439
|
+
value: string;
|
|
2440
|
+
/** The picked value and its row (`undefined` for the empty choice). */
|
|
2441
|
+
onChange: (value: string, option?: SelectOption) => void;
|
|
2442
|
+
}) {
|
|
2443
|
+
const [options, setOptions] = useState<SelectOption[] | null>(null);
|
|
2444
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
2445
|
+
// The route is re-derived on every render; the effect keys on its VALUE.
|
|
2446
|
+
const { slug, path } = route;
|
|
2447
|
+
const query = new URLSearchParams(route.params).toString();
|
|
2448
|
+
// A new route/project/agent starts from "Loading…"; a revision bump does not.
|
|
2449
|
+
const scopeKey = `${slug}\n${path}\n${query}\n${projectId ?? ''}\n${agentId}`;
|
|
2450
|
+
const loadedScope = useRef<string | null>(null);
|
|
2451
|
+
useEffect(() => {
|
|
2452
|
+
let live = true;
|
|
2453
|
+
if (loadedScope.current !== scopeKey) {
|
|
2454
|
+
loadedScope.current = scopeKey;
|
|
2455
|
+
setOptions(null);
|
|
2456
|
+
}
|
|
2457
|
+
setLoadError(null);
|
|
2458
|
+
const api = createPackageApi({
|
|
2459
|
+
slug,
|
|
2460
|
+
getProjectId: () => projectId ?? null,
|
|
2461
|
+
getAgentId: () => agentId || null,
|
|
2462
|
+
});
|
|
2463
|
+
api.get<unknown>(path, Object.fromEntries(new URLSearchParams(query))).then(
|
|
2464
|
+
(answer) => { if (live) setOptions(selectOptionsFrom(answer)); },
|
|
2465
|
+
(err: unknown) => { if (live) setLoadError(err instanceof Error ? err.message : 'Could not load the options'); },
|
|
2466
|
+
);
|
|
2467
|
+
return () => { live = false; };
|
|
2468
|
+
}, [slug, path, query, projectId, agentId, scopeKey, revision]);
|
|
2469
|
+
|
|
2470
|
+
if (loadError) return <div className="text-[9px] text-red-300/80">{loadError}</div>;
|
|
2471
|
+
if (options === null) return <div className="text-[9px] text-white/30 animate-pulse">Loading…</div>;
|
|
2472
|
+
if (options.length === 0 && !emptyLabel) return <div className="text-[9px] text-white/40 italic">Nothing to choose from.</div>;
|
|
2473
|
+
return (
|
|
2474
|
+
<select value={value} onChange={(e) => onChange(e.target.value, options.find((o) => o.value === e.target.value))} className={selectCls}>
|
|
2475
|
+
{emptyLabel ? <option value="">{emptyLabel}</option> : <option value="" disabled>Choose…</option>}
|
|
2476
|
+
{options.map((o) => (
|
|
2477
|
+
<option key={o.value} value={o.value} style={o.dimmed ? DIMMED_OPTION_STYLE : undefined}>
|
|
2478
|
+
{o.label}
|
|
2479
|
+
</option>
|
|
2480
|
+
))}
|
|
2481
|
+
</select>
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* The package-folders field both forms show — one folder per line, relative to
|
|
2487
|
+
* the source root, `.` for the whole folder. It keeps its OWN text (a list
|
|
2488
|
+
* round-trip would swallow the newline being typed) and reports every edit;
|
|
2489
|
+
* the caller parses (`parsePackageFolders`). The list is UX: the server
|
|
2490
|
+
* re-validates the folders and re-gates the owner/admin write.
|
|
2491
|
+
*/
|
|
2492
|
+
function PackageFoldersField({ value, onChange, note }: { value: string; onChange: (text: string) => void; note: string }) {
|
|
2493
|
+
const [text, setText] = useState(value);
|
|
2494
|
+
// An external change (a reset, a saved answer) replaces the text; an echo of
|
|
2495
|
+
// this field's own edit does not.
|
|
2496
|
+
useEffect(() => {
|
|
2497
|
+
setText((current) => (parsePackageFolders(current).join('\n') === parsePackageFolders(value).join('\n') ? current : value));
|
|
2498
|
+
}, [value]);
|
|
2499
|
+
return (
|
|
2500
|
+
<div className="space-y-1">
|
|
2501
|
+
<div className="flex items-center gap-1.5 text-[9px] font-semibold uppercase tracking-wider text-white/35">
|
|
2502
|
+
<Package className="h-3 w-3 text-cyan-400 shrink-0" />
|
|
2503
|
+
Package folders
|
|
2504
|
+
</div>
|
|
2505
|
+
<textarea
|
|
2506
|
+
value={text}
|
|
2507
|
+
onChange={(e) => { setText(e.target.value); onChange(e.target.value); }}
|
|
2508
|
+
rows={2}
|
|
2509
|
+
className={cn(textareaCls, 'font-mono')}
|
|
2510
|
+
placeholder={'# empty = none\n.\ntools/pkgs'}
|
|
2511
|
+
/>
|
|
2512
|
+
<div className="text-[8px] text-white/25 italic">
|
|
2513
|
+
Load the packages found in these folders (in addition to <code>_packages/</code>), {note}; <code>.</code> is the whole folder. Advertised only within this source's scope.
|
|
2514
|
+
</div>
|
|
2515
|
+
</div>
|
|
2516
|
+
);
|
|
2517
|
+
}
|