@volter/editor-core 0.5.57
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/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,1681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VGAI host adapter for Supercode's headless client.
|
|
3
|
+
*
|
|
4
|
+
* Supercode owns harness/session/runtime semantics, lifecycle normalization,
|
|
5
|
+
* transcript projection, retries, reconciliation, and concurrency. VGAI owns
|
|
6
|
+
* the trusted YOLO policy, HTTP/SSE boundary, shell-command presentation, and
|
|
7
|
+
* React visuals. Keep this file as a mapping layer; reusable agent logic
|
|
8
|
+
* belongs in @volter-ai-dev/supercode-client.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { existsSync, statSync } from 'node:fs';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
|
+
import type { NormalizedSession, SessionDescriptor } from '@volter-ai-dev/supercode-harness-sdk';
|
|
18
|
+
import {
|
|
19
|
+
type projectClientSnapshot,
|
|
20
|
+
projectSubagentInventory,
|
|
21
|
+
projectSubagentTranscript,
|
|
22
|
+
} from '@volter-ai-dev/supercode-ui/controller';
|
|
23
|
+
import type { SupercodeUiIntent, SupercodeUiState } from '@volter-ai-dev/supercode-ui/core';
|
|
24
|
+
import {
|
|
25
|
+
createRemoteControllerHost,
|
|
26
|
+
type RemoteControllerHost,
|
|
27
|
+
type RemoteUiFrame,
|
|
28
|
+
} from '@volter-ai-dev/supercode-ui/host';
|
|
29
|
+
import type {
|
|
30
|
+
HarnessChatCapabilities,
|
|
31
|
+
HarnessChatHarness,
|
|
32
|
+
HarnessChatSession,
|
|
33
|
+
HarnessChatSnapshot,
|
|
34
|
+
HarnessChatTaskPlan,
|
|
35
|
+
JsonValue,
|
|
36
|
+
} from '../src/harness-chat-types';
|
|
37
|
+
import { unavailableHarnessChatSnapshot } from '../src/harness-chat-types';
|
|
38
|
+
import type { ResolvedCodingInference } from './account-service';
|
|
39
|
+
import { type HarnessReadiness, withCodingInference } from './coding-inference-launch';
|
|
40
|
+
import {
|
|
41
|
+
type FrontendHandoff,
|
|
42
|
+
FRONTEND_UNAVAILABLE_ENV,
|
|
43
|
+
mintFrontendHandoff,
|
|
44
|
+
} from './frontend-handoff';
|
|
45
|
+
import { projectMcpServers } from './project-mcp-servers';
|
|
46
|
+
import type { HarnessChatCallerSession } from './harness-chat-caller';
|
|
47
|
+
|
|
48
|
+
type HeadlessUiSnapshot = Parameters<typeof projectClientSnapshot>[0];
|
|
49
|
+
|
|
50
|
+
// @volter-ai-dev/supercode-client is an optional runtime peer. The mandatory UI
|
|
51
|
+
// package already declares its exact frontend snapshot contract, so do not
|
|
52
|
+
// shadow that contract with a partial VGAI interface. The validator loaded
|
|
53
|
+
// atomically from the runtime peer remains the authority for production data.
|
|
54
|
+
type RuntimeCapabilities = HeadlessUiSnapshot['harnesses'][number]['effective_capabilities'];
|
|
55
|
+
type StructuredLaunch = Omit<NonNullable<HeadlessUiSnapshot['terminalLaunch']>, 'env'> & {
|
|
56
|
+
env?: Record<string, string>;
|
|
57
|
+
};
|
|
58
|
+
type HeadlessSessionDescriptor = SessionDescriptor & {
|
|
59
|
+
live_endpoint?: string | null;
|
|
60
|
+
live_status?: 'running' | 'busy' | 'idle' | null;
|
|
61
|
+
};
|
|
62
|
+
type HeadlessDescriptorMessage = NonNullable<
|
|
63
|
+
HeadlessSessionDescriptor['preview_candidates']
|
|
64
|
+
>[number];
|
|
65
|
+
type HeadlessConversationEntry = HeadlessUiSnapshot['conversation'][number];
|
|
66
|
+
type HeadlessLoadedSession = NonNullable<HeadlessUiSnapshot['activeSession']> & {
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
};
|
|
69
|
+
type HeadlessSnapshot = Omit<HeadlessUiSnapshot, 'activeSession' | 'availableActions'> & {
|
|
70
|
+
activeSession: HeadlessLoadedSession | null;
|
|
71
|
+
// Optional only for a rolling package upgrade: the new controller owns this
|
|
72
|
+
// field, while an already-running older optional peer must remain readable
|
|
73
|
+
// until the editor dependency update lands.
|
|
74
|
+
history?: { transcriptLimit: number | null; hasEarlier: boolean };
|
|
75
|
+
availableActions: HeadlessUiSnapshot['availableActions'] & { loadEarlier: boolean };
|
|
76
|
+
};
|
|
77
|
+
type HeadlessAction =
|
|
78
|
+
| { type: 'refresh'; autoObserve?: boolean; silent?: boolean }
|
|
79
|
+
| { type: 'observe'; sessionKey: string }
|
|
80
|
+
| { type: 'loadEarlier' }
|
|
81
|
+
| { type: 'start'; harness: string }
|
|
82
|
+
| { type: 'resume'; sessionKey: string }
|
|
83
|
+
| { type: 'attach'; sessionKey: string; baseUrl?: string }
|
|
84
|
+
| { type: 'branch'; sessionKey: string; targetHarness?: string }
|
|
85
|
+
| { type: 'reduce'; sessionKey: string; targetHarness?: string }
|
|
86
|
+
| { type: 'restore'; identity: string; connection: 'observe' | 'attach' }
|
|
87
|
+
| { type: 'detach' }
|
|
88
|
+
| { type: 'openTerminal' }
|
|
89
|
+
| {
|
|
90
|
+
type: 'send';
|
|
91
|
+
text: string;
|
|
92
|
+
context?: Array<{ id?: string; kind?: string; label: string; detail: string }>;
|
|
93
|
+
images?: Array<{ id?: string; label: string; url: string }>;
|
|
94
|
+
}
|
|
95
|
+
| { type: 'steer'; text: string }
|
|
96
|
+
| { type: 'interrupt' }
|
|
97
|
+
| {
|
|
98
|
+
type: 'configureHarness';
|
|
99
|
+
harness: string;
|
|
100
|
+
changes: Array<{ key: string; value: string | null }>;
|
|
101
|
+
expectedRevision: string;
|
|
102
|
+
}
|
|
103
|
+
| { type: 'respond'; requestId: JsonValue; optionId: string | null }
|
|
104
|
+
| { type: 'respond'; requestId: JsonValue; response: JsonValue };
|
|
105
|
+
type HeadlessController = {
|
|
106
|
+
getSnapshot(): HeadlessSnapshot;
|
|
107
|
+
subscribe(listener: () => void): () => void;
|
|
108
|
+
initialize(): Promise<HeadlessSnapshot>;
|
|
109
|
+
dispatch(action: HeadlessAction): Promise<HeadlessSnapshot>;
|
|
110
|
+
loadSession(sessionKey: string): Promise<HeadlessLoadedSession>;
|
|
111
|
+
setWorkspace(workspace: string, options?: { autoObserve?: boolean }): Promise<HeadlessSnapshot>;
|
|
112
|
+
close(): Promise<void>;
|
|
113
|
+
};
|
|
114
|
+
type HeadlessControllerConstructor = new (options: {
|
|
115
|
+
client: SupercodeClient;
|
|
116
|
+
workspace: string;
|
|
117
|
+
/** Supercode's own `SupercodeControllerOptions['policy']`. It decides whether the
|
|
118
|
+
* runtime launch skips the harness's permission prompts — see {@link RUNTIME_POLICY}. */
|
|
119
|
+
policy: 'default' | 'yolo';
|
|
120
|
+
ownsClient: true;
|
|
121
|
+
autoObserve: boolean;
|
|
122
|
+
allowHarnessConfiguration?: boolean;
|
|
123
|
+
}) => HeadlessController;
|
|
124
|
+
type HeadlessManagedRuntime = {
|
|
125
|
+
readonly closed?: boolean;
|
|
126
|
+
/** The SDK's own `RuntimeHandle`. `runtime_id` is the string the runtime wrote into
|
|
127
|
+
* its live receipt as `runtime_session_id`, so it is how the frontend handoff finds
|
|
128
|
+
* the loopback address and the mint door. */
|
|
129
|
+
readonly handle?: { harness: string; runtime_id: string; endpoint?: string | null };
|
|
130
|
+
steer(text: string): Promise<Record<string, never>>;
|
|
131
|
+
};
|
|
132
|
+
type HeadlessSnapshotAssertion = (value: unknown) => HeadlessSnapshot;
|
|
133
|
+
type HeadlessProjectConversation = (session: HeadlessLoadedSession) => HeadlessConversationEntry[];
|
|
134
|
+
type HeadlessDeriveTaskPlan = (session: HeadlessLoadedSession | null) => HarnessChatTaskPlan;
|
|
135
|
+
type HeadlessSessionIdentity = (locator: HeadlessSessionDescriptor['locator']) => Promise<string>;
|
|
136
|
+
type SupercodeClient = {
|
|
137
|
+
close(): Promise<void>;
|
|
138
|
+
discover(query: {
|
|
139
|
+
workspace?: string;
|
|
140
|
+
harnesses?: string[];
|
|
141
|
+
limit?: number;
|
|
142
|
+
include_topic_candidates?: boolean;
|
|
143
|
+
include_child_sessions?: boolean;
|
|
144
|
+
}): Promise<{ sessions: HeadlessSessionDescriptor[] }>;
|
|
145
|
+
load(
|
|
146
|
+
locator: HeadlessSessionDescriptor['locator'],
|
|
147
|
+
options?: {
|
|
148
|
+
view?: { tailMessages?: number; maxMessageChars?: number; includeSubagents?: boolean };
|
|
149
|
+
},
|
|
150
|
+
): Promise<{ session: NormalizedSession }>;
|
|
151
|
+
/** Supercode's own readiness inventory — `installed`, `auth`, and the `repair` text
|
|
152
|
+
* naming that harness's login command. The SDK's `listHarnesses`, not a shell-out. */
|
|
153
|
+
listHarnesses?(query?: {
|
|
154
|
+
workspace?: string;
|
|
155
|
+
harnesses?: string[];
|
|
156
|
+
probe?: 'passive' | 'handshake';
|
|
157
|
+
include_sessions?: boolean;
|
|
158
|
+
skip_versions?: boolean;
|
|
159
|
+
}): Promise<{ harnesses: SupercodeLocalHarness[] }>;
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
};
|
|
162
|
+
type SupercodeLocalHarness = {
|
|
163
|
+
id: string;
|
|
164
|
+
display_name: string;
|
|
165
|
+
installed: boolean;
|
|
166
|
+
auth: HarnessReadiness['auth'];
|
|
167
|
+
reason: string | null;
|
|
168
|
+
repair: string | null;
|
|
169
|
+
};
|
|
170
|
+
type SupercodeClientConstructor = new (options?: {
|
|
171
|
+
command?: string;
|
|
172
|
+
cwd?: string;
|
|
173
|
+
}) => SupercodeClient;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* WHETHER THE AGENT ASKS BEFORE IT ACTS, and the one place that is decided.
|
|
177
|
+
*
|
|
178
|
+
* Supercode's runtime launch is policy-gated: `harness_service.rs`'s
|
|
179
|
+
* `runtime_launch(params)` returns a launch ONLY for `yolo`, and the yolo launch for
|
|
180
|
+
* Claude Code is `claude --dangerously-skip-permissions --print …`. Under `default` it
|
|
181
|
+
* returns none and the backend falls back to its OWN prefix
|
|
182
|
+
* (`runtime/adapters.rs`'s `ClaudeCodeRuntimeBackend::new`), which carries
|
|
183
|
+
* `--permission-prompt-tool stdio` instead — so the CLI raises a `can_use_tool` control
|
|
184
|
+
* request, supercode publishes it on `frontend.v2`, and the panel draws Allow / Allow
|
|
185
|
+
* for session / Deny. The same flag decides the terminal attach launch
|
|
186
|
+
* (`resume_instructions`).
|
|
187
|
+
*
|
|
188
|
+
* So the skipped permissions B9c measured on the child process were OURS, not
|
|
189
|
+
* supercode's: the literal is in supercode, the `policy` that selects it was set here.
|
|
190
|
+
*
|
|
191
|
+
* IT HAS A VERSION FLOOR, and that floor is why `packages/editor/package.json` asks for
|
|
192
|
+
* `@volter-ai-dev/supercode` `^0.4.36`. `--permission-prompt-tool stdio` joined the Claude
|
|
193
|
+
* backend's own prefix on 2026-09-04 (`6ef3be26`); the binary an older install resolved here,
|
|
194
|
+
* 0.4.11, is built from 2026-08-24 and predates it. MEASURED on the child of a real session
|
|
195
|
+
* against 0.4.11: `claude --print … --verbose --session-id …` with NO permission handler at
|
|
196
|
+
* all — and supercode's own comment on that flag says a tool call that needs an answer is then
|
|
197
|
+
* refused outright with `permission_denied`. Against 0.4.36 the same launch carries the flag.
|
|
198
|
+
* `default` without the handler is WORSE than `yolo`, so the two move together.
|
|
199
|
+
* The person's approvals are the product's safety and the panel is where they answer
|
|
200
|
+
* them, so the default launch ASKS. Changing this constant changes both launches; there
|
|
201
|
+
* is deliberately no per-call override, because a chat that asks and a terminal that
|
|
202
|
+
* does not is one agent with two safety stories.
|
|
203
|
+
*/
|
|
204
|
+
const RUNTIME_POLICY = 'default' as const;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* WHICH AGENT FILLS THE CHAT VIEW. ARCHITECTURE-CORE §The core is Code-OSS, rule 7:
|
|
208
|
+
* "the same coding-agent session (Claude Code by default; any harness supercode runs)".
|
|
209
|
+
* There is deliberately no setting: a second harness in the panel is a decision the
|
|
210
|
+
* owner makes, and until then the answer is the one the doctrine already gives.
|
|
211
|
+
*/
|
|
212
|
+
const FRONTEND_HARNESS = 'claude-code';
|
|
213
|
+
|
|
214
|
+
/** What the host gets back: the environment to spawn the REH with, or why there is none. */
|
|
215
|
+
export interface FrontendHandoffResult {
|
|
216
|
+
readonly env: Readonly<Record<string, string>>;
|
|
217
|
+
readonly refusal: string | null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const MANAGED_RUNTIME_METHODS = new Set([
|
|
221
|
+
'startManagedRuntime',
|
|
222
|
+
'resumeManagedRuntime',
|
|
223
|
+
'attachManagedRuntime',
|
|
224
|
+
]);
|
|
225
|
+
const MANAGED_RUNTIME_START_TIMEOUT_MS = 180_000;
|
|
226
|
+
|
|
227
|
+
/** Retain the SDK's own managed-runtime object without changing its behavior. */
|
|
228
|
+
export function withManagedRuntimeObserver(
|
|
229
|
+
client: SupercodeClient,
|
|
230
|
+
observe: (runtime: HeadlessManagedRuntime) => void,
|
|
231
|
+
transformBackend: (backend: unknown) => Promise<unknown> = async (backend) => backend,
|
|
232
|
+
): SupercodeClient {
|
|
233
|
+
return new Proxy(client, {
|
|
234
|
+
get(target, property) {
|
|
235
|
+
const value = Reflect.get(target, property, target) as unknown;
|
|
236
|
+
if (!MANAGED_RUNTIME_METHODS.has(String(property)) || typeof value !== 'function') {
|
|
237
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
238
|
+
}
|
|
239
|
+
return async (...args: unknown[]) => {
|
|
240
|
+
const suppliedOptions =
|
|
241
|
+
args[1] && typeof args[1] === 'object' && !Array.isArray(args[1])
|
|
242
|
+
? (args[1] as Record<string, unknown>)
|
|
243
|
+
: {};
|
|
244
|
+
const runtime = (await Reflect.apply(value, target, [
|
|
245
|
+
await transformBackend(args[0]),
|
|
246
|
+
{ ...suppliedOptions, timeoutMs: MANAGED_RUNTIME_START_TIMEOUT_MS },
|
|
247
|
+
])) as HeadlessManagedRuntime;
|
|
248
|
+
observe(runtime);
|
|
249
|
+
return runtime;
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface HarnessChatServiceOptions {
|
|
256
|
+
engineRoot: string;
|
|
257
|
+
getProjectRoot: () => string;
|
|
258
|
+
onChange: (snapshot: HarnessChatSnapshot) => void;
|
|
259
|
+
initializeTimeoutMs?: number;
|
|
260
|
+
/** Test seam; production periodically discovers sessions launched after editor boot. */
|
|
261
|
+
discoveryPollMs?: number;
|
|
262
|
+
/** Sessions that invoked vgai for this project from outside its workspace. */
|
|
263
|
+
callerSessions?: readonly HarnessChatCallerSession[];
|
|
264
|
+
/** Trusted account route resolved only when Supercode launches a process. */
|
|
265
|
+
resolveCodingInference?: (workspace: string) => Promise<ResolvedCodingInference | null>;
|
|
266
|
+
/** Test seam. Production loads the real zero-dependency Supercode SDK. */
|
|
267
|
+
createClient?: ((workspace: string) => Promise<SupercodeClient>) | undefined;
|
|
268
|
+
/** Test seam for the framework-neutral controller. */
|
|
269
|
+
createController?:
|
|
270
|
+
| ((
|
|
271
|
+
client: SupercodeClient,
|
|
272
|
+
workspace: string,
|
|
273
|
+
options: { autoObserve: boolean },
|
|
274
|
+
) => HeadlessController)
|
|
275
|
+
| undefined;
|
|
276
|
+
/** Test seam for Supercode's collision-free descriptor identity. */
|
|
277
|
+
sessionReconnectIdentity?: HeadlessSessionIdentity;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function locatorKey(descriptor: HeadlessSessionDescriptor): string {
|
|
281
|
+
const { locator } = descriptor;
|
|
282
|
+
const storage =
|
|
283
|
+
locator.storage.kind === 'file'
|
|
284
|
+
? `file:${locator.storage.path}`
|
|
285
|
+
: `sqlite:${locator.storage.path}:${locator.storage.selector}`;
|
|
286
|
+
return `${locator.harness}\0${locator.session_id}\0${storage}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function descriptorPresentationFingerprint(sessions: readonly HeadlessSessionDescriptor[]): string {
|
|
290
|
+
return JSON.stringify(
|
|
291
|
+
sessions
|
|
292
|
+
.map((session) => ({
|
|
293
|
+
locator: locatorKey(session),
|
|
294
|
+
title: session.title,
|
|
295
|
+
previewCandidates: session.preview_candidates,
|
|
296
|
+
latestMessageCandidates: session.latest_message_candidates,
|
|
297
|
+
updatedAt: session.updated_at_ms,
|
|
298
|
+
messageCount: session.message_count,
|
|
299
|
+
liveStatus: session.live_status,
|
|
300
|
+
activity: session.activity
|
|
301
|
+
? {
|
|
302
|
+
presence: session.activity.presence,
|
|
303
|
+
turn: session.activity.turn,
|
|
304
|
+
source: session.activity.evidence.source,
|
|
305
|
+
nativeState: session.activity.evidence.native_state,
|
|
306
|
+
harnessVersion: session.activity.evidence.harness_version,
|
|
307
|
+
}
|
|
308
|
+
: null,
|
|
309
|
+
}))
|
|
310
|
+
.sort((left, right) => left.locator.localeCompare(right.locator)),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function sessionIdentityFingerprint(identities: Iterable<string>): string {
|
|
315
|
+
return JSON.stringify([...identities].sort());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Supercode's ordinary controller discovers by workspace. A caller session is
|
|
320
|
+
* different evidence: its native id is authoritative even when its cwd is
|
|
321
|
+
* elsewhere. Ask Supercode globally for only the named harnesses, select the
|
|
322
|
+
* exact ids, and union their full locators into the controller's normal
|
|
323
|
+
* discovery result. No session/workspace association is fabricated or stored.
|
|
324
|
+
*/
|
|
325
|
+
export function withCallerSessionDiscovery(
|
|
326
|
+
client: SupercodeClient,
|
|
327
|
+
getCallers: () => readonly HarnessChatCallerSession[],
|
|
328
|
+
onDiscover: (sessions: readonly HeadlessSessionDescriptor[]) => void | Promise<void> = () => {},
|
|
329
|
+
): SupercodeClient {
|
|
330
|
+
const topicCandidatesByLocator = new Map<string, HeadlessDescriptorMessage[]>();
|
|
331
|
+
const retainTopicCandidates = (
|
|
332
|
+
sessions: readonly HeadlessSessionDescriptor[],
|
|
333
|
+
): HeadlessSessionDescriptor[] =>
|
|
334
|
+
sessions.map((descriptor) => {
|
|
335
|
+
const key = locatorKey(descriptor);
|
|
336
|
+
if (descriptor.preview_candidates) {
|
|
337
|
+
topicCandidatesByLocator.set(key, descriptor.preview_candidates);
|
|
338
|
+
return descriptor;
|
|
339
|
+
}
|
|
340
|
+
const retained = topicCandidatesByLocator.get(key);
|
|
341
|
+
return retained ? { ...descriptor, preview_candidates: retained } : descriptor;
|
|
342
|
+
});
|
|
343
|
+
return new Proxy(client, {
|
|
344
|
+
get(target, property) {
|
|
345
|
+
if (property !== 'discover') {
|
|
346
|
+
const value = Reflect.get(target, property, target) as unknown;
|
|
347
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
348
|
+
}
|
|
349
|
+
return async (query: {
|
|
350
|
+
workspace?: string;
|
|
351
|
+
harnesses?: string[];
|
|
352
|
+
limit?: number;
|
|
353
|
+
include_topic_candidates?: boolean;
|
|
354
|
+
include_child_sessions?: boolean;
|
|
355
|
+
}): Promise<{ sessions: HeadlessSessionDescriptor[] }> => {
|
|
356
|
+
const discover = target.discover.bind(target);
|
|
357
|
+
const includeTopicCandidates = query.include_topic_candidates !== false;
|
|
358
|
+
const base = await discover({ ...query, include_topic_candidates: includeTopicCandidates });
|
|
359
|
+
const callers = getCallers();
|
|
360
|
+
if (callers.length === 0) {
|
|
361
|
+
const sessions = retainTopicCandidates(base.sessions);
|
|
362
|
+
await onDiscover(sessions);
|
|
363
|
+
return { sessions };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const wantedByHarness = new Map<string, Set<string>>();
|
|
367
|
+
for (const caller of callers) {
|
|
368
|
+
const ids = wantedByHarness.get(caller.harness) ?? new Set<string>();
|
|
369
|
+
ids.add(caller.sessionId);
|
|
370
|
+
wantedByHarness.set(caller.harness, ids);
|
|
371
|
+
}
|
|
372
|
+
const supplemental = await Promise.all(
|
|
373
|
+
[...wantedByHarness].map(async ([harness, ids]) => {
|
|
374
|
+
const global = await discover({
|
|
375
|
+
harnesses: [harness],
|
|
376
|
+
include_topic_candidates: includeTopicCandidates,
|
|
377
|
+
});
|
|
378
|
+
return global.sessions.filter(
|
|
379
|
+
(descriptor) =>
|
|
380
|
+
descriptor.locator.harness === harness && ids.has(descriptor.locator.session_id),
|
|
381
|
+
);
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
const union = new Map(
|
|
385
|
+
base.sessions.map((descriptor) => [locatorKey(descriptor), descriptor]),
|
|
386
|
+
);
|
|
387
|
+
for (const descriptor of supplemental.flat()) {
|
|
388
|
+
union.set(locatorKey(descriptor), descriptor);
|
|
389
|
+
}
|
|
390
|
+
const sessions = retainTopicCandidates([...union.values()]);
|
|
391
|
+
await onDiscover(sessions);
|
|
392
|
+
return { sessions };
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const EMPTY_CAPABILITIES: HarnessChatCapabilities = {
|
|
399
|
+
startSession: false,
|
|
400
|
+
resumeSession: false,
|
|
401
|
+
attachExistingProcess: false,
|
|
402
|
+
sendInput: false,
|
|
403
|
+
streamEvents: false,
|
|
404
|
+
interrupt: false,
|
|
405
|
+
respondToRequests: false,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
function capabilities(value: RuntimeCapabilities | undefined): HarnessChatCapabilities {
|
|
409
|
+
if (!value) return { ...EMPTY_CAPABILITIES };
|
|
410
|
+
return {
|
|
411
|
+
startSession: value.start_session,
|
|
412
|
+
resumeSession: value.resume_session,
|
|
413
|
+
attachExistingProcess: value.attach_existing_process,
|
|
414
|
+
sendInput: value.send_input,
|
|
415
|
+
streamEvents: value.stream_events,
|
|
416
|
+
interrupt: value.interrupt,
|
|
417
|
+
respondToRequests: value.respond_to_requests ?? false,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function errorMessage(error: unknown): string {
|
|
422
|
+
return error instanceof Error ? error.message : String(error);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
426
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
427
|
+
try {
|
|
428
|
+
return await Promise.race([
|
|
429
|
+
promise,
|
|
430
|
+
new Promise<never>((_, reject) => {
|
|
431
|
+
timer = setTimeout(
|
|
432
|
+
() => reject(new Error(`${label} timed out after ${timeoutMs}ms.`)),
|
|
433
|
+
timeoutMs,
|
|
434
|
+
);
|
|
435
|
+
}),
|
|
436
|
+
]);
|
|
437
|
+
} finally {
|
|
438
|
+
if (timer) clearTimeout(timer);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// A cold editor boot can be optimizing Vite dependencies while Supercode scans
|
|
443
|
+
// several local harness stores. Real five-harness startup can cross ten seconds
|
|
444
|
+
// without being stuck, so keep the failure bounded but leave enough cold-start
|
|
445
|
+
// grace for the first inventory pass to finish.
|
|
446
|
+
const DEFAULT_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
447
|
+
|
|
448
|
+
function moduleCandidate(explicit: string | undefined, defaultFile: string): string[] {
|
|
449
|
+
if (!explicit) return [];
|
|
450
|
+
const absolute = resolve(explicit);
|
|
451
|
+
try {
|
|
452
|
+
return [
|
|
453
|
+
existsSync(absolute) && statSync(absolute).isDirectory()
|
|
454
|
+
? join(absolute, defaultFile)
|
|
455
|
+
: absolute,
|
|
456
|
+
];
|
|
457
|
+
} catch {
|
|
458
|
+
return [absolute];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** The thrown message reaches the chat panel verbatim, so it stays one
|
|
463
|
+
* human-sized sentence; the per-candidate resolution dump (absolute paths of
|
|
464
|
+
* every import attempt) goes to the editor server log instead. */
|
|
465
|
+
function unavailableModule(label: string, failures: string[]): Error {
|
|
466
|
+
console.error(`[harness-chat] ${label} failed to load:\n ${failures.join('\n ')}`);
|
|
467
|
+
return new Error(
|
|
468
|
+
`${label} could not be loaded in this editor install. The editor server log lists every import location tried.`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function importOptional<T>(
|
|
473
|
+
label: string,
|
|
474
|
+
exportName: string,
|
|
475
|
+
candidates: string[],
|
|
476
|
+
): Promise<T> {
|
|
477
|
+
const failures: string[] = [];
|
|
478
|
+
for (const candidate of candidates) {
|
|
479
|
+
try {
|
|
480
|
+
const specifier = candidate.startsWith('/') ? pathToFileURL(candidate).href : candidate;
|
|
481
|
+
const module = (await import(specifier)) as Record<string, unknown>;
|
|
482
|
+
const value = module[exportName];
|
|
483
|
+
if (value) return value as T;
|
|
484
|
+
failures.push(`${candidate}: missing ${exportName} export`);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
failures.push(`${candidate}: ${errorMessage(error)}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
throw unavailableModule(label, failures);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function importOptionalModule<const Names extends readonly string[]>(
|
|
493
|
+
label: string,
|
|
494
|
+
exportNames: Names,
|
|
495
|
+
candidates: string[],
|
|
496
|
+
): Promise<{ [Key in Names[number]]: unknown }> {
|
|
497
|
+
const failures: string[] = [];
|
|
498
|
+
for (const candidate of candidates) {
|
|
499
|
+
try {
|
|
500
|
+
const specifier = candidate.startsWith('/') ? pathToFileURL(candidate).href : candidate;
|
|
501
|
+
const module = (await import(specifier)) as Record<string, unknown>;
|
|
502
|
+
const missing = exportNames.filter((name) => !module[name]);
|
|
503
|
+
if (missing.length === 0) return module as { [Key in Names[number]]: unknown };
|
|
504
|
+
failures.push(`${candidate}: missing ${missing.join(', ')} export`);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
failures.push(`${candidate}: ${errorMessage(error)}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
throw unavailableModule(label, failures);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function importSupercodePackages(engineRoot: string): Promise<{
|
|
513
|
+
SupercodeHarnessClient: SupercodeClientConstructor;
|
|
514
|
+
SupercodeController: HeadlessControllerConstructor;
|
|
515
|
+
assertSupercodeClientSnapshot: HeadlessSnapshotAssertion;
|
|
516
|
+
projectConversation: HeadlessProjectConversation;
|
|
517
|
+
deriveTaskPlan: HeadlessDeriveTaskPlan;
|
|
518
|
+
sessionReconnectIdentity: HeadlessSessionIdentity;
|
|
519
|
+
}> {
|
|
520
|
+
const sibling = join(dirname(engineRoot), 'supercode', 'sdk');
|
|
521
|
+
const cwdSibling = join(dirname(resolve(process.cwd())), 'supercode', 'sdk');
|
|
522
|
+
const controllerCandidates = [
|
|
523
|
+
...moduleCandidate(process.env['SUPERCODE_CLIENT_PATH'], 'client.mjs'),
|
|
524
|
+
'@volter-ai-dev/supercode-client',
|
|
525
|
+
join(sibling, 'client', 'client.mjs'),
|
|
526
|
+
join(cwdSibling, 'client', 'client.mjs'),
|
|
527
|
+
];
|
|
528
|
+
const [SupercodeHarnessClient, clientModule] = await Promise.all([
|
|
529
|
+
importOptional<SupercodeClientConstructor>('Supercode harness SDK', 'SupercodeHarnessClient', [
|
|
530
|
+
...moduleCandidate(process.env['SUPERCODE_SDK_PATH'], 'client.mjs'),
|
|
531
|
+
'@volter-ai-dev/supercode-harness-sdk',
|
|
532
|
+
join(sibling, 'typescript', 'client.mjs'),
|
|
533
|
+
join(cwdSibling, 'typescript', 'client.mjs'),
|
|
534
|
+
]),
|
|
535
|
+
importOptionalModule(
|
|
536
|
+
'Supercode headless client',
|
|
537
|
+
[
|
|
538
|
+
'SupercodeController',
|
|
539
|
+
'assertSupercodeClientSnapshot',
|
|
540
|
+
'projectConversation',
|
|
541
|
+
'deriveTaskPlan',
|
|
542
|
+
'sessionReconnectIdentity',
|
|
543
|
+
] as const,
|
|
544
|
+
controllerCandidates,
|
|
545
|
+
),
|
|
546
|
+
]);
|
|
547
|
+
const SupercodeController = clientModule.SupercodeController as HeadlessControllerConstructor;
|
|
548
|
+
const assertSupercodeClientSnapshot =
|
|
549
|
+
clientModule.assertSupercodeClientSnapshot as HeadlessSnapshotAssertion;
|
|
550
|
+
return {
|
|
551
|
+
SupercodeHarnessClient,
|
|
552
|
+
SupercodeController,
|
|
553
|
+
assertSupercodeClientSnapshot,
|
|
554
|
+
projectConversation: clientModule.projectConversation as HeadlessProjectConversation,
|
|
555
|
+
deriveTaskPlan: clientModule.deriveTaskPlan as HeadlessDeriveTaskPlan,
|
|
556
|
+
sessionReconnectIdentity: clientModule.sessionReconnectIdentity as HeadlessSessionIdentity,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function importSupercodeController(engineRoot: string): Promise<{
|
|
561
|
+
SupercodeController: HeadlessControllerConstructor;
|
|
562
|
+
assertSupercodeClientSnapshot: HeadlessSnapshotAssertion;
|
|
563
|
+
projectConversation: HeadlessProjectConversation;
|
|
564
|
+
deriveTaskPlan: HeadlessDeriveTaskPlan;
|
|
565
|
+
sessionReconnectIdentity: HeadlessSessionIdentity;
|
|
566
|
+
}> {
|
|
567
|
+
const sibling = join(dirname(engineRoot), 'supercode', 'sdk', 'client', 'client.mjs');
|
|
568
|
+
const cwdSibling = join(
|
|
569
|
+
dirname(resolve(process.cwd())),
|
|
570
|
+
'supercode',
|
|
571
|
+
'sdk',
|
|
572
|
+
'client',
|
|
573
|
+
'client.mjs',
|
|
574
|
+
);
|
|
575
|
+
const candidates = [
|
|
576
|
+
...moduleCandidate(process.env['SUPERCODE_CLIENT_PATH'], 'client.mjs'),
|
|
577
|
+
'@volter-ai-dev/supercode-client',
|
|
578
|
+
sibling,
|
|
579
|
+
cwdSibling,
|
|
580
|
+
];
|
|
581
|
+
const clientModule = await importOptionalModule(
|
|
582
|
+
'Supercode headless client',
|
|
583
|
+
[
|
|
584
|
+
'SupercodeController',
|
|
585
|
+
'assertSupercodeClientSnapshot',
|
|
586
|
+
'projectConversation',
|
|
587
|
+
'deriveTaskPlan',
|
|
588
|
+
'sessionReconnectIdentity',
|
|
589
|
+
] as const,
|
|
590
|
+
candidates,
|
|
591
|
+
);
|
|
592
|
+
const SupercodeController = clientModule.SupercodeController as HeadlessControllerConstructor;
|
|
593
|
+
const assertSupercodeClientSnapshot =
|
|
594
|
+
clientModule.assertSupercodeClientSnapshot as HeadlessSnapshotAssertion;
|
|
595
|
+
return {
|
|
596
|
+
SupercodeController,
|
|
597
|
+
assertSupercodeClientSnapshot,
|
|
598
|
+
projectConversation: clientModule.projectConversation as HeadlessProjectConversation,
|
|
599
|
+
deriveTaskPlan: clientModule.deriveTaskPlan as HeadlessDeriveTaskPlan,
|
|
600
|
+
sessionReconnectIdentity: clientModule.sessionReconnectIdentity as HeadlessSessionIdentity,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function findSourceLinkedSupercodeCommand(): string | undefined {
|
|
605
|
+
let clientRoot: string;
|
|
606
|
+
try {
|
|
607
|
+
clientRoot = dirname(fileURLToPath(import.meta.resolve('@volter-ai-dev/supercode-client')));
|
|
608
|
+
} catch {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
const sourceRoot = resolve(clientRoot, '..', '..');
|
|
612
|
+
if (!existsSync(join(sourceRoot, 'Cargo.toml'))) return undefined;
|
|
613
|
+
for (const candidate of [
|
|
614
|
+
join(sourceRoot, 'target', 'release', 'supercode'),
|
|
615
|
+
join(sourceRoot, 'target', 'debug', 'supercode'),
|
|
616
|
+
]) {
|
|
617
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
618
|
+
}
|
|
619
|
+
throw new Error(
|
|
620
|
+
`The source-linked Supercode SDK at ${clientRoot} has no built core binary. Run \`cargo build --bin supercode\` in ${sourceRoot}.`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function findSupercodeCommand(engineRoot: string): string | undefined {
|
|
625
|
+
if (process.env['SUPERCODE_BIN']) return process.env['SUPERCODE_BIN'];
|
|
626
|
+
// A source-linked SDK must run the core from that same checkout. Falling
|
|
627
|
+
// through to an unrelated npm install creates a mixed-version system that
|
|
628
|
+
// can be protocol-compatible enough to start while returning stale or
|
|
629
|
+
// incorrectly classified sessions.
|
|
630
|
+
const sourceCommand = findSourceLinkedSupercodeCommand();
|
|
631
|
+
if (sourceCommand) return sourceCommand;
|
|
632
|
+
const require = createRequire(import.meta.url);
|
|
633
|
+
try {
|
|
634
|
+
const packageRoot = dirname(require.resolve('@volter-ai-dev/supercode/package.json'));
|
|
635
|
+
const installedCommand = join(packageRoot, 'bin', 'supercode.js');
|
|
636
|
+
if (existsSync(installedCommand) && statSync(installedCommand).isFile())
|
|
637
|
+
return installedCommand;
|
|
638
|
+
} catch {
|
|
639
|
+
// Optional dependency omitted: retain source-checkout fallbacks below.
|
|
640
|
+
}
|
|
641
|
+
for (const candidate of [
|
|
642
|
+
join(dirname(engineRoot), 'supercode', 'target', 'release', 'supercode'),
|
|
643
|
+
join(dirname(engineRoot), 'supercode', 'target', 'debug', 'supercode'),
|
|
644
|
+
]) {
|
|
645
|
+
if (existsSync(candidate)) return candidate;
|
|
646
|
+
}
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function shellQuote(value: string): string {
|
|
651
|
+
if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
652
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function launchCommand(launch: StructuredLaunch): string {
|
|
656
|
+
return `cd ${shellQuote(launch.cwd)} && ${[launch.program, ...launch.arguments].map(shellQuote).join(' ')}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const TRANSCRIPT_SCAN_MULTIPLIER = 4;
|
|
660
|
+
|
|
661
|
+
function snapshotStatus(snapshot: HeadlessSnapshot): HarnessChatSnapshot['status'] {
|
|
662
|
+
if (snapshot.availability === 'unavailable') return 'unavailable';
|
|
663
|
+
if (snapshot.availability === 'error') return 'error';
|
|
664
|
+
if (snapshot.availability === 'loading') return 'loading';
|
|
665
|
+
return snapshot.turn.state === 'idle' ? 'ready' : 'working';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function snapshotMode(snapshot: HeadlessSnapshot): HarnessChatSnapshot['mode'] {
|
|
669
|
+
if (snapshot.connection.mode === 'mirror') return 'observe';
|
|
670
|
+
if (snapshot.connection.mode === 'control') return 'control';
|
|
671
|
+
return 'none';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function toSnapshot(snapshot: HeadlessSnapshot, frame: RemoteUiFrame): HarnessChatSnapshot {
|
|
675
|
+
const taskPlan = snapshot.taskPlan ?? {
|
|
676
|
+
source: 'none',
|
|
677
|
+
items: [],
|
|
678
|
+
residue: [],
|
|
679
|
+
observedAt: null,
|
|
680
|
+
};
|
|
681
|
+
const harnesses: HarnessChatHarness[] = snapshot.harnesses.map((item) => ({
|
|
682
|
+
id: item.id,
|
|
683
|
+
label: item.display_name,
|
|
684
|
+
installed: item.installed,
|
|
685
|
+
auth: item.auth,
|
|
686
|
+
runtime: item.runtime,
|
|
687
|
+
protocol: item.protocol,
|
|
688
|
+
capabilities: capabilities(item.effective_capabilities),
|
|
689
|
+
availableActions: { ...item.availableActions },
|
|
690
|
+
reason: item.reason,
|
|
691
|
+
repair: item.repair,
|
|
692
|
+
}));
|
|
693
|
+
const rowByKey = new Map(frame.state.sessions.map((row) => [row.key, row]));
|
|
694
|
+
// The controller key is the React key of every messenger row. Two rows under
|
|
695
|
+
// one key made React warn 51 times per frame and the messenger's effects
|
|
696
|
+
// loop until "Maximum update depth exceeded" — the crash that took the
|
|
697
|
+
// Agents panel down in every Claude session of the blind modeling bench
|
|
698
|
+
// (2026-09-05). The server saw ONE row whenever it was asked, so the double
|
|
699
|
+
// is transient; it is dropped here (first wins) and journaled with both
|
|
700
|
+
// identities so the next occurrence names its source instead of a stack.
|
|
701
|
+
const seenKeys = new Set<string>();
|
|
702
|
+
const doubled: string[] = [];
|
|
703
|
+
const uniqueSessions = snapshot.sessions.filter((item) => {
|
|
704
|
+
if (seenKeys.has(item.key)) {
|
|
705
|
+
doubled.push(`${item.key} (${item.harness} ${item.sessionId} ${item.identity})`);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
seenKeys.add(item.key);
|
|
709
|
+
return true;
|
|
710
|
+
});
|
|
711
|
+
if (doubled.length > 0) {
|
|
712
|
+
console.warn(
|
|
713
|
+
`[harness-chat] the controller listed one session key twice; keeping the first: ${doubled.join('; ')}`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const seenRowKeys = new Set<string>();
|
|
717
|
+
const uniqueRows = frame.state.sessions.filter((row) => {
|
|
718
|
+
if (seenRowKeys.has(row.key)) return false;
|
|
719
|
+
seenRowKeys.add(row.key);
|
|
720
|
+
return true;
|
|
721
|
+
});
|
|
722
|
+
const dedupedFrame =
|
|
723
|
+
uniqueRows.length === frame.state.sessions.length
|
|
724
|
+
? frame
|
|
725
|
+
: { ...frame, state: { ...frame.state, sessions: uniqueRows } };
|
|
726
|
+
const sessions: HarnessChatSession[] = uniqueSessions.map((item) => ({
|
|
727
|
+
// The controller key is intentionally opaque and collision-free. Native
|
|
728
|
+
// IDs can collide between harnesses and stores.
|
|
729
|
+
id: item.key,
|
|
730
|
+
identity: item.identity,
|
|
731
|
+
nativeId: item.sessionId,
|
|
732
|
+
harness: item.harness,
|
|
733
|
+
title: rowByKey.get(item.key)?.title || item.displayTitle?.trim() || 'Untitled chat',
|
|
734
|
+
cwd: item.cwd,
|
|
735
|
+
updatedAt: item.updatedAt,
|
|
736
|
+
messageCount: item.messageCount,
|
|
737
|
+
model: item.model,
|
|
738
|
+
storage: item.storage,
|
|
739
|
+
live: item.liveStatus ?? (item.liveEndpoint ? 'running' : null),
|
|
740
|
+
}));
|
|
741
|
+
const {
|
|
742
|
+
branch: canFork,
|
|
743
|
+
loadEarlier: _canLoadEarlier,
|
|
744
|
+
...availableActions
|
|
745
|
+
} = snapshot.availableActions;
|
|
746
|
+
return {
|
|
747
|
+
workspace: snapshot.workspace,
|
|
748
|
+
status: snapshotStatus(snapshot),
|
|
749
|
+
mode: snapshotMode(snapshot),
|
|
750
|
+
schema: 'vgai.harness-chat.v4',
|
|
751
|
+
serverInstanceId: frame.hostInstanceId,
|
|
752
|
+
revision: snapshot.revision,
|
|
753
|
+
workspaceGeneration: frame.generation,
|
|
754
|
+
operation: snapshot.operation === 'branch' ? 'fork' : snapshot.operation,
|
|
755
|
+
harnesses,
|
|
756
|
+
sessions,
|
|
757
|
+
activeHarness: snapshot.activeHarness,
|
|
758
|
+
activeSessionId: snapshot.activeSessionKey,
|
|
759
|
+
connection: {
|
|
760
|
+
strategy: snapshot.connection.strategy === 'branch' ? 'fork' : snapshot.connection.strategy,
|
|
761
|
+
follow: snapshot.connection.follow,
|
|
762
|
+
ownsRuntime: snapshot.connection.ownsRuntime,
|
|
763
|
+
},
|
|
764
|
+
turn: { ...snapshot.turn },
|
|
765
|
+
frame: dedupedFrame,
|
|
766
|
+
taskPlan: {
|
|
767
|
+
...taskPlan,
|
|
768
|
+
items: taskPlan.items.map((item) => ({
|
|
769
|
+
...item,
|
|
770
|
+
...(item.blockedBy ? { blockedBy: [...item.blockedBy] } : {}),
|
|
771
|
+
})),
|
|
772
|
+
residue: structuredClone(taskPlan.residue),
|
|
773
|
+
},
|
|
774
|
+
requests: snapshot.requests.map((request) => ({
|
|
775
|
+
...request,
|
|
776
|
+
options: request.options.map((option) => ({ ...option })),
|
|
777
|
+
})),
|
|
778
|
+
availableActions: { ...availableActions, fork: canFork },
|
|
779
|
+
error: snapshot.error
|
|
780
|
+
? {
|
|
781
|
+
...snapshot.error,
|
|
782
|
+
operation: snapshot.error.operation === 'branch' ? 'fork' : snapshot.error.operation,
|
|
783
|
+
}
|
|
784
|
+
: null,
|
|
785
|
+
terminalCommand: snapshot.terminalLaunch ? launchCommand(snapshot.terminalLaunch) : null,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
function unavailable(
|
|
791
|
+
workspace: string | null,
|
|
792
|
+
error: string,
|
|
793
|
+
workspaceGeneration = 0,
|
|
794
|
+
serverInstanceId: string | null = null,
|
|
795
|
+
): HarnessChatSnapshot {
|
|
796
|
+
// The ~40-field literal lives in ONE place (`harness-chat-types.ts`); this
|
|
797
|
+
// wrapper only supplies the server's workspace identity on top.
|
|
798
|
+
return {
|
|
799
|
+
...unavailableHarnessChatSnapshot(error, { code: 'unavailable', recoverable: true }),
|
|
800
|
+
workspace,
|
|
801
|
+
workspaceGeneration,
|
|
802
|
+
serverInstanceId,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function loading(
|
|
807
|
+
workspace: string,
|
|
808
|
+
workspaceGeneration: number,
|
|
809
|
+
serverInstanceId: string,
|
|
810
|
+
): HarnessChatSnapshot {
|
|
811
|
+
return {
|
|
812
|
+
...unavailable(workspace, 'Supercode is loading.', workspaceGeneration, serverInstanceId),
|
|
813
|
+
status: 'loading' as const,
|
|
814
|
+
error: null,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export class HarnessChatService {
|
|
819
|
+
private readonly serverInstanceId = randomUUID();
|
|
820
|
+
private workspace: string | null = null;
|
|
821
|
+
private controller: HeadlessController | null = null;
|
|
822
|
+
private remoteHost: RemoteControllerHost | null = null;
|
|
823
|
+
private unsubscribe: (() => void) | null = null;
|
|
824
|
+
private starting: Promise<void> | null = null;
|
|
825
|
+
private closing: Promise<void> | null = null;
|
|
826
|
+
private workspaceGeneration = 0;
|
|
827
|
+
private closed = false;
|
|
828
|
+
private discoveryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
829
|
+
private discoveryInFlight = false;
|
|
830
|
+
private discoveryClient: SupercodeClient | null = null;
|
|
831
|
+
private assertSnapshot: HeadlessSnapshotAssertion = (value) => value as HeadlessSnapshot;
|
|
832
|
+
private projectConversation: HeadlessProjectConversation = () => [];
|
|
833
|
+
private deriveTaskPlan: HeadlessDeriveTaskPlan = () => ({
|
|
834
|
+
source: 'none',
|
|
835
|
+
items: [],
|
|
836
|
+
residue: [],
|
|
837
|
+
observedAt: null,
|
|
838
|
+
});
|
|
839
|
+
private lastHeadlessSnapshot: HeadlessSnapshot | null = null;
|
|
840
|
+
private sessionDescriptorsByIdentity = new Map<string, HeadlessSessionDescriptor>();
|
|
841
|
+
private sessionIdentity: HeadlessSessionIdentity | null = null;
|
|
842
|
+
private subagentDescriptorsByKey = new Map<string, HeadlessSessionDescriptor>();
|
|
843
|
+
private subagentInspector: SupercodeUiState['subagentInspector'] = null;
|
|
844
|
+
private inventoryCaptureGeneration = 0;
|
|
845
|
+
private renderedInventoryFingerprint: string | null = null;
|
|
846
|
+
private readonly bridgeListeners = new Set<() => void>();
|
|
847
|
+
private lastSnapshot: HarnessChatSnapshot = unavailable(
|
|
848
|
+
null,
|
|
849
|
+
'Supercode is loading.',
|
|
850
|
+
0,
|
|
851
|
+
this.serverInstanceId,
|
|
852
|
+
);
|
|
853
|
+
/** The last managed runtime the SDK handed back, for the frontend handoff's receipt lookup. */
|
|
854
|
+
private managedRuntime: HeadlessManagedRuntime | null = null;
|
|
855
|
+
private frontendHandoffValue: FrontendHandoff | null = null;
|
|
856
|
+
/** The standing reason the Chat view has no agent, or `null`. Held because a refusal
|
|
857
|
+
* OUTLIVES a page load and the console ledger's clearing rule (a) retires an entry whose
|
|
858
|
+
* last load is older than the current one — so the condition is re-observed per load
|
|
859
|
+
* rather than raised once and swept. */
|
|
860
|
+
private frontendRefusalValue: string | null = null;
|
|
861
|
+
private frontendHandoffInFlight: Promise<FrontendHandoffResult> | null = null;
|
|
862
|
+
private readonly callerSessionsByWorkspace = new Map<
|
|
863
|
+
string,
|
|
864
|
+
Map<string, HarnessChatCallerSession>
|
|
865
|
+
>();
|
|
866
|
+
|
|
867
|
+
constructor(private readonly options: HarnessChatServiceOptions) {
|
|
868
|
+
const workspace = resolve(options.getProjectRoot());
|
|
869
|
+
for (const caller of options.callerSessions ?? []) this.rememberCaller(workspace, caller);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private callerKey(caller: HarnessChatCallerSession): string {
|
|
873
|
+
return `${caller.harness}\0${caller.sessionId}`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private rememberCaller(workspace: string, caller: HarnessChatCallerSession): boolean {
|
|
877
|
+
const callers = this.callerSessionsByWorkspace.get(workspace) ?? new Map();
|
|
878
|
+
this.callerSessionsByWorkspace.set(workspace, callers);
|
|
879
|
+
const key = this.callerKey(caller);
|
|
880
|
+
if (callers.has(key)) return false;
|
|
881
|
+
callers.set(key, caller);
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private callerSessions(): readonly HarnessChatCallerSession[] {
|
|
886
|
+
if (!this.workspace) return [];
|
|
887
|
+
return [...(this.callerSessionsByWorkspace.get(this.workspace)?.values() ?? [])];
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async includeCallerSession(caller: HarnessChatCallerSession): Promise<HarnessChatSnapshot> {
|
|
891
|
+
const workspace = resolve(this.options.getProjectRoot());
|
|
892
|
+
if (!this.rememberCaller(workspace, caller)) return this.snapshot();
|
|
893
|
+
if (this.controller && this.workspace === workspace) {
|
|
894
|
+
await this.controller.dispatch({ type: 'refresh', autoObserve: false });
|
|
895
|
+
this.capture();
|
|
896
|
+
}
|
|
897
|
+
return this.snapshot();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
snapshot(): HarnessChatSnapshot {
|
|
901
|
+
return {
|
|
902
|
+
...this.lastSnapshot,
|
|
903
|
+
harnesses: this.lastSnapshot.harnesses.map((item) => ({
|
|
904
|
+
...item,
|
|
905
|
+
capabilities: { ...item.capabilities },
|
|
906
|
+
availableActions: { ...item.availableActions },
|
|
907
|
+
})),
|
|
908
|
+
sessions: this.lastSnapshot.sessions.map((item) => ({ ...item })),
|
|
909
|
+
connection: { ...this.lastSnapshot.connection },
|
|
910
|
+
turn: { ...this.lastSnapshot.turn },
|
|
911
|
+
frame: this.lastSnapshot.frame ? structuredClone(this.lastSnapshot.frame) : null,
|
|
912
|
+
taskPlan: {
|
|
913
|
+
...this.lastSnapshot.taskPlan,
|
|
914
|
+
items: this.lastSnapshot.taskPlan.items.map((item) => ({
|
|
915
|
+
...item,
|
|
916
|
+
...(item.blockedBy ? { blockedBy: [...item.blockedBy] } : {}),
|
|
917
|
+
})),
|
|
918
|
+
residue: structuredClone(this.lastSnapshot.taskPlan.residue),
|
|
919
|
+
},
|
|
920
|
+
requests: this.lastSnapshot.requests.map((item) => ({
|
|
921
|
+
...item,
|
|
922
|
+
options: item.options.map((option) => ({ ...option })),
|
|
923
|
+
})),
|
|
924
|
+
availableActions: { ...this.lastSnapshot.availableActions },
|
|
925
|
+
error: this.lastSnapshot.error ? { ...this.lastSnapshot.error } : null,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async refresh(autoObserve = true): Promise<HarnessChatSnapshot> {
|
|
930
|
+
const created = !this.controller;
|
|
931
|
+
await this.ensureController(autoObserve);
|
|
932
|
+
if (!this.controller) return this.snapshot();
|
|
933
|
+
if (!created) await this.controller.dispatch({ type: 'refresh', autoObserve });
|
|
934
|
+
this.capture();
|
|
935
|
+
return this.snapshot();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Ensure the live controller exists without re-running full harness/session
|
|
939
|
+
* discovery when an editor tab mounts or reloads. The service's idle probe
|
|
940
|
+
* detects inventory changes without coupling discovery to browser views. */
|
|
941
|
+
async load(autoObserve = true): Promise<HarnessChatSnapshot> {
|
|
942
|
+
const next = resolve(this.options.getProjectRoot());
|
|
943
|
+
if (!this.controller && !this.starting) {
|
|
944
|
+
this.beginWorkspace(next);
|
|
945
|
+
void this.ensureController(autoObserve);
|
|
946
|
+
} else if (this.controller && this.workspace === next) {
|
|
947
|
+
this.capture();
|
|
948
|
+
} else if (this.workspace !== next) {
|
|
949
|
+
this.beginWorkspace(next);
|
|
950
|
+
void this.ensureController(autoObserve);
|
|
951
|
+
}
|
|
952
|
+
return this.snapshot();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
/** Dispatch the package-owned messenger intent without translating it into
|
|
957
|
+
* a second VGAI action vocabulary. */
|
|
958
|
+
async actIntent(intent: SupercodeUiIntent): Promise<HarnessChatSnapshot> {
|
|
959
|
+
await this.ensureController();
|
|
960
|
+
const host = this.remoteHost;
|
|
961
|
+
if (!host) {
|
|
962
|
+
throw new Error(this.lastSnapshot.error?.message ?? 'Supercode is unavailable.');
|
|
963
|
+
}
|
|
964
|
+
await host.dispatch(intent);
|
|
965
|
+
this.capture();
|
|
966
|
+
return this.snapshot();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* THE CHAT VIEW'S RUNTIME, as four environment variables (`frontend-handoff.ts`).
|
|
972
|
+
*
|
|
973
|
+
* Called ONCE per session, by the host, BEFORE it spawns the REH — because the
|
|
974
|
+
* extension host inherits the REH's environment and that environment is fixed at
|
|
975
|
+
* spawn. So this is also what decides that a `vgai edit` starts a harness runtime at
|
|
976
|
+
* all: the panel is the product's agent surface, and a panel with nothing behind it
|
|
977
|
+
* is the silent degrade rule 7 exists to prevent.
|
|
978
|
+
*
|
|
979
|
+
* It never throws. A box with no signed-in harness is an ordinary state (B9c: the
|
|
980
|
+
* refusal names the harness and its own login command), and the session still opens —
|
|
981
|
+
* the sentence goes to the cover and the REH starts without the variables, which is
|
|
982
|
+
* exactly the extension's own empty state.
|
|
983
|
+
*/
|
|
984
|
+
/** The standing refusal, for a caller that must re-raise it (the console ledger is
|
|
985
|
+
* page-scoped and this condition is not). `null` once a runtime is attached. */
|
|
986
|
+
frontendRefusal(): string | null {
|
|
987
|
+
return this.frontendRefusalValue;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async frontendHandoff(): Promise<FrontendHandoffResult> {
|
|
991
|
+
if (this.frontendHandoffValue) {
|
|
992
|
+
return { env: { ...this.frontendHandoffValue.env }, refusal: null };
|
|
993
|
+
}
|
|
994
|
+
this.frontendHandoffInFlight ??= this.mintFrontendHandoff().finally(() => {
|
|
995
|
+
this.frontendHandoffInFlight = null;
|
|
996
|
+
});
|
|
997
|
+
return this.frontendHandoffInFlight;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* The key of this project's most recent {@link FRONTEND_HARNESS} session, or `null` when it
|
|
1002
|
+
* has none. Only sessions whose `cwd` IS this project count: a session the person ran
|
|
1003
|
+
* somewhere else is not this project's history, and resuming it would put another folder's
|
|
1004
|
+
* conversation in this folder's panel.
|
|
1005
|
+
*/
|
|
1006
|
+
private resumableSessionKey(): string | null {
|
|
1007
|
+
const root = resolve(this.options.getProjectRoot());
|
|
1008
|
+
const mine = this.lastSnapshot.sessions
|
|
1009
|
+
.filter((session) => session.harness === FRONTEND_HARNESS && session.cwd !== null)
|
|
1010
|
+
.filter((session) => resolve(session.cwd as string) === root)
|
|
1011
|
+
.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
|
|
1012
|
+
return mine[0]?.id ?? null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private async mintFrontendHandoff(): Promise<FrontendHandoffResult> {
|
|
1016
|
+
try {
|
|
1017
|
+
await this.ensureController();
|
|
1018
|
+
const controller = this.controller;
|
|
1019
|
+
if (!controller) throw new Error('Supercode is unavailable.');
|
|
1020
|
+
if (!this.managedRuntime || this.managedRuntime.closed) {
|
|
1021
|
+
// REOPENING A PROJECT RESUMES ITS LAST SESSION, it does not start a second one.
|
|
1022
|
+
// `vgai close` ends the runtime with the session, so without this every reopen
|
|
1023
|
+
// handed the panel a FRESH Claude session and the person's own conversation was
|
|
1024
|
+
// gone — measured: close, reopen, and the Chat view came back empty while the
|
|
1025
|
+
// extension's status door listed a brand new runtime id.
|
|
1026
|
+
//
|
|
1027
|
+
// Both doors are the controller's own and both ask readiness first, so a harness
|
|
1028
|
+
// that is not signed in is still refused BY NAME with its own login command —
|
|
1029
|
+
// which is the sentence a person needs, and the one that travels to the cover,
|
|
1030
|
+
// `.vgai/session.json` and the console ledger.
|
|
1031
|
+
const resumable = this.resumableSessionKey();
|
|
1032
|
+
await controller.dispatch(
|
|
1033
|
+
resumable === null
|
|
1034
|
+
? { type: 'start', harness: FRONTEND_HARNESS }
|
|
1035
|
+
: { type: 'resume', sessionKey: resumable },
|
|
1036
|
+
);
|
|
1037
|
+
this.capture();
|
|
1038
|
+
}
|
|
1039
|
+
const runtimeId = this.managedRuntime?.handle?.runtime_id;
|
|
1040
|
+
if (!runtimeId) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Supercode started no ${FRONTEND_HARNESS} runtime for this project, so the Chat view has nothing to attach to.`,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
const handoff = await mintFrontendHandoff({
|
|
1046
|
+
engineRoot: this.options.engineRoot,
|
|
1047
|
+
runtimeSessionId: runtimeId,
|
|
1048
|
+
directory: join(homedir(), '.vgai', 'runtime', `frontend-${process.pid}`),
|
|
1049
|
+
});
|
|
1050
|
+
this.frontendHandoffValue = handoff;
|
|
1051
|
+
this.frontendRefusalValue = null;
|
|
1052
|
+
return { env: { ...handoff.env }, refusal: null };
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
// THE REFUSAL TRAVELS TO THE PANEL. The extension reads
|
|
1055
|
+
// `SUPERCODE_FRONTEND_UNAVAILABLE` at activation and shows it as the session's first
|
|
1056
|
+
// turn, so the sentence a person needs — the harness named, with its own login command —
|
|
1057
|
+
// is on the screen they are looking at, not only in this terminal and the ledger.
|
|
1058
|
+
this.frontendRefusalValue = errorMessage(error);
|
|
1059
|
+
return {
|
|
1060
|
+
env: { [FRONTEND_UNAVAILABLE_ENV]: this.frontendRefusalValue },
|
|
1061
|
+
refusal: this.frontendRefusalValue,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async setWorkspace(): Promise<void> {
|
|
1067
|
+
const next = resolve(this.options.getProjectRoot());
|
|
1068
|
+
if (this.workspace === next) return;
|
|
1069
|
+
this.beginWorkspace(next);
|
|
1070
|
+
if (this.controller) {
|
|
1071
|
+
await this.controller.setWorkspace(next, { autoObserve: true });
|
|
1072
|
+
this.capture();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
await this.ensureController(true);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async close(): Promise<void> {
|
|
1079
|
+
if (this.closing) return this.closing;
|
|
1080
|
+
this.closed = true;
|
|
1081
|
+
if (this.discoveryTimer) clearTimeout(this.discoveryTimer);
|
|
1082
|
+
this.discoveryTimer = null;
|
|
1083
|
+
this.unsubscribe?.();
|
|
1084
|
+
this.unsubscribe = null;
|
|
1085
|
+
this.remoteHost?.close();
|
|
1086
|
+
this.remoteHost = null;
|
|
1087
|
+
const controller = this.controller;
|
|
1088
|
+
this.controller = null;
|
|
1089
|
+
this.discoveryClient = null;
|
|
1090
|
+
this.lastHeadlessSnapshot = null;
|
|
1091
|
+
this.sessionDescriptorsByIdentity.clear();
|
|
1092
|
+
this.sessionIdentity = null;
|
|
1093
|
+
this.subagentDescriptorsByKey.clear();
|
|
1094
|
+
this.subagentInspector = null;
|
|
1095
|
+
this.inventoryCaptureGeneration++;
|
|
1096
|
+
this.renderedInventoryFingerprint = null;
|
|
1097
|
+
this.bridgeListeners.clear();
|
|
1098
|
+
const handoff = this.frontendHandoffValue;
|
|
1099
|
+
this.frontendHandoffValue = null;
|
|
1100
|
+
this.managedRuntime = null;
|
|
1101
|
+
void handoff?.dispose();
|
|
1102
|
+
this.closing = controller
|
|
1103
|
+
? withTimeout(controller.close(), 2_500, 'Supercode shutdown').then(
|
|
1104
|
+
() => undefined,
|
|
1105
|
+
() => undefined,
|
|
1106
|
+
)
|
|
1107
|
+
: Promise.resolve();
|
|
1108
|
+
return this.closing;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
private async ensureController(autoObserve = true): Promise<void> {
|
|
1112
|
+
if (this.closed) throw new Error('Harness Chat service is closed.');
|
|
1113
|
+
const next = resolve(this.options.getProjectRoot());
|
|
1114
|
+
if (this.controller) {
|
|
1115
|
+
if (this.workspace !== next) {
|
|
1116
|
+
this.beginWorkspace(next);
|
|
1117
|
+
await this.controller.setWorkspace(next, { autoObserve: true });
|
|
1118
|
+
this.capture();
|
|
1119
|
+
}
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (this.starting) {
|
|
1123
|
+
await this.starting;
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
this.beginWorkspace(next);
|
|
1127
|
+
const generation = this.workspaceGeneration;
|
|
1128
|
+
this.starting = this.startController(next, autoObserve, generation);
|
|
1129
|
+
try {
|
|
1130
|
+
await this.starting;
|
|
1131
|
+
} finally {
|
|
1132
|
+
this.starting = null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private async startController(
|
|
1137
|
+
workspace: string,
|
|
1138
|
+
autoObserve: boolean,
|
|
1139
|
+
generation: number,
|
|
1140
|
+
): Promise<void> {
|
|
1141
|
+
let controller: HeadlessController | null = null;
|
|
1142
|
+
try {
|
|
1143
|
+
controller = await this.createHeadlessController(workspace, autoObserve);
|
|
1144
|
+
if (this.closed || generation !== this.workspaceGeneration) {
|
|
1145
|
+
await withTimeout(controller.close(), 2_500, 'Supercode shutdown').catch(() => undefined);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
this.workspace = workspace;
|
|
1149
|
+
this.controller = controller;
|
|
1150
|
+
this.remoteHost = createRemoteControllerHost(
|
|
1151
|
+
controller as unknown as Parameters<typeof createRemoteControllerHost>[0],
|
|
1152
|
+
{
|
|
1153
|
+
hostInstanceId: this.serverInstanceId,
|
|
1154
|
+
projection: (state) => this.projectionOptions(state),
|
|
1155
|
+
handleIntent: (intent) => intent.action === 'release',
|
|
1156
|
+
onLoadSessions: async () => {
|
|
1157
|
+
await controller?.dispatch({ type: 'refresh', autoObserve: false });
|
|
1158
|
+
},
|
|
1159
|
+
onOpenSubagents: (key) => this.openSubagents(key),
|
|
1160
|
+
onOpenSubagent: (parentKey, key) => this.openSubagent(parentKey, key),
|
|
1161
|
+
onCloseSubagents: () => this.closeSubagents(),
|
|
1162
|
+
onStartTerminal: () => {
|
|
1163
|
+
throw new Error('Starting directly in a terminal is not available in this editor.');
|
|
1164
|
+
},
|
|
1165
|
+
onResumeTerminal: () => {
|
|
1166
|
+
throw new Error('Terminal continuation must be opened by the editor shell.');
|
|
1167
|
+
},
|
|
1168
|
+
onUnsupported: (intent) => {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
`Supercode UI action is not available in this editor: ${intent.action}`,
|
|
1171
|
+
);
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
);
|
|
1175
|
+
this.unsubscribe = controller.subscribe(() => this.capture());
|
|
1176
|
+
this.capture();
|
|
1177
|
+
const timeoutMs = this.options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
1178
|
+
await withTimeout(controller.initialize(), timeoutMs, 'Supercode initialization');
|
|
1179
|
+
this.capture();
|
|
1180
|
+
this.scheduleDiscovery();
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
if (controller && controller === this.controller) {
|
|
1183
|
+
this.unsubscribe?.();
|
|
1184
|
+
this.unsubscribe = null;
|
|
1185
|
+
this.remoteHost?.close();
|
|
1186
|
+
this.remoteHost = null;
|
|
1187
|
+
this.controller = null;
|
|
1188
|
+
}
|
|
1189
|
+
if (generation === this.workspaceGeneration && !this.closed) {
|
|
1190
|
+
this.lastSnapshot = unavailable(
|
|
1191
|
+
workspace,
|
|
1192
|
+
errorMessage(error),
|
|
1193
|
+
generation,
|
|
1194
|
+
this.serverInstanceId,
|
|
1195
|
+
);
|
|
1196
|
+
this.options.onChange(this.snapshot());
|
|
1197
|
+
}
|
|
1198
|
+
if (controller) {
|
|
1199
|
+
await withTimeout(controller.close(), 2_500, 'Supercode shutdown').catch(() => undefined);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private async createHeadlessController(
|
|
1205
|
+
workspace: string,
|
|
1206
|
+
autoObserve: boolean,
|
|
1207
|
+
): Promise<HeadlessController> {
|
|
1208
|
+
const piExtensionPath = join(homedir(), '.vgai', 'runtime', 'pi-openrouter-extension.mjs');
|
|
1209
|
+
// Every managed-runtime start passes through here. By default it injects nothing and
|
|
1210
|
+
// the stock CLI runs on the person's own login, so readiness is asked for FIRST and a
|
|
1211
|
+
// harness that is not signed in is refused by name (ARCHITECTURE-CORE §Managed
|
|
1212
|
+
// services, "A coding harness is not a provider").
|
|
1213
|
+
const transformBackend = async (backend: unknown) => {
|
|
1214
|
+
const params = await withCodingInference(
|
|
1215
|
+
backend,
|
|
1216
|
+
() => this.options.resolveCodingInference?.(workspace) ?? Promise.resolve(null),
|
|
1217
|
+
piExtensionPath,
|
|
1218
|
+
(harness) => this.harnessReadiness(harness, workspace),
|
|
1219
|
+
);
|
|
1220
|
+
// THE PROJECT'S OWN MCP SERVERS ride the start, because discovery cannot reach them:
|
|
1221
|
+
// a `--print` runtime has no trust dialog, and Claude Code loads a project `.mcp.json`
|
|
1222
|
+
// only for a project the person has already approved by hand
|
|
1223
|
+
// (`project-mcp-servers.ts` carries the measurement). Supplied ones are never
|
|
1224
|
+
// overwritten — a caller that already named them meant it.
|
|
1225
|
+
if (params === null || typeof params !== 'object') return params;
|
|
1226
|
+
const supplied = params as { mcp_servers?: unknown };
|
|
1227
|
+
if (supplied.mcp_servers !== undefined) return params;
|
|
1228
|
+
const servers = projectMcpServers(workspace);
|
|
1229
|
+
return servers.length > 0 ? { ...supplied, mcp_servers: servers } : params;
|
|
1230
|
+
};
|
|
1231
|
+
if (this.options.createClient) {
|
|
1232
|
+
const module = this.options.createController
|
|
1233
|
+
? null
|
|
1234
|
+
: await importSupercodeController(this.options.engineRoot);
|
|
1235
|
+
const sessionReconnectIdentity =
|
|
1236
|
+
this.options.sessionReconnectIdentity ?? module?.sessionReconnectIdentity;
|
|
1237
|
+
if (!sessionReconnectIdentity) {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
'An injected Supercode controller must provide its sessionReconnectIdentity test seam.',
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
this.sessionIdentity = sessionReconnectIdentity;
|
|
1243
|
+
const client = withCallerSessionDiscovery(
|
|
1244
|
+
withManagedRuntimeObserver(
|
|
1245
|
+
await this.options.createClient(workspace),
|
|
1246
|
+
(runtime) => { this.managedRuntime = runtime; },
|
|
1247
|
+
transformBackend,
|
|
1248
|
+
),
|
|
1249
|
+
() => this.callerSessions(),
|
|
1250
|
+
(sessions) => this.rememberSessionInventory(sessions, sessionReconnectIdentity, workspace),
|
|
1251
|
+
);
|
|
1252
|
+
this.discoveryClient = client;
|
|
1253
|
+
if (this.options.createController) {
|
|
1254
|
+
return this.options.createController(client, workspace, { autoObserve });
|
|
1255
|
+
}
|
|
1256
|
+
if (!module) throw new Error('Supercode controller could not be loaded.');
|
|
1257
|
+
this.assertSnapshot = module.assertSupercodeClientSnapshot;
|
|
1258
|
+
this.projectConversation = module.projectConversation;
|
|
1259
|
+
this.deriveTaskPlan = module.deriveTaskPlan;
|
|
1260
|
+
return new module.SupercodeController({
|
|
1261
|
+
client,
|
|
1262
|
+
workspace,
|
|
1263
|
+
policy: RUNTIME_POLICY,
|
|
1264
|
+
ownsClient: true,
|
|
1265
|
+
autoObserve,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const {
|
|
1269
|
+
SupercodeHarnessClient,
|
|
1270
|
+
SupercodeController,
|
|
1271
|
+
assertSupercodeClientSnapshot,
|
|
1272
|
+
projectConversation,
|
|
1273
|
+
deriveTaskPlan,
|
|
1274
|
+
sessionReconnectIdentity,
|
|
1275
|
+
} = await importSupercodePackages(this.options.engineRoot);
|
|
1276
|
+
this.assertSnapshot = assertSupercodeClientSnapshot;
|
|
1277
|
+
this.projectConversation = projectConversation;
|
|
1278
|
+
this.deriveTaskPlan = deriveTaskPlan;
|
|
1279
|
+
this.sessionIdentity = sessionReconnectIdentity;
|
|
1280
|
+
const command = findSupercodeCommand(this.options.engineRoot);
|
|
1281
|
+
const client = withCallerSessionDiscovery(
|
|
1282
|
+
withManagedRuntimeObserver(
|
|
1283
|
+
new SupercodeHarnessClient({
|
|
1284
|
+
cwd: workspace,
|
|
1285
|
+
...(command ? { command } : {}),
|
|
1286
|
+
}),
|
|
1287
|
+
(runtime) => { this.managedRuntime = runtime; },
|
|
1288
|
+
transformBackend,
|
|
1289
|
+
),
|
|
1290
|
+
() => this.callerSessions(),
|
|
1291
|
+
(sessions) => this.rememberSessionInventory(sessions, sessionReconnectIdentity, workspace),
|
|
1292
|
+
);
|
|
1293
|
+
this.discoveryClient = client;
|
|
1294
|
+
return new SupercodeController({
|
|
1295
|
+
client,
|
|
1296
|
+
workspace,
|
|
1297
|
+
policy: RUNTIME_POLICY,
|
|
1298
|
+
ownsClient: true,
|
|
1299
|
+
autoObserve,
|
|
1300
|
+
allowHarnessConfiguration: true,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Ask Supercode whether this harness can start on the login the person already did.
|
|
1306
|
+
* The SDK's own `listHarnesses` is the door (the same report `supercode harness list
|
|
1307
|
+
* --json` prints), asked fresh at launch time so a sign-in completed in a terminal a
|
|
1308
|
+
* moment ago counts; the controller's last inventory answers when a test seam supplies
|
|
1309
|
+
* a client without it. `null` means Supercode had nothing to say, and nothing is
|
|
1310
|
+
* refused on silence.
|
|
1311
|
+
*
|
|
1312
|
+
* A passive probe that answers `unknown` is escalated to the HANDSHAKE probe for this
|
|
1313
|
+
* one harness — Supercode's own recommended next step, and the only way to tell "not
|
|
1314
|
+
* signed in" from "cannot tell without opening it". Measured 2026-09-21: OpenCode on
|
|
1315
|
+
* this box reads `auth: unknown` passively and `auth: ready` under the handshake (3.9s),
|
|
1316
|
+
* so treating the passive answer as a refusal would have blocked a working harness.
|
|
1317
|
+
*
|
|
1318
|
+
* WHAT THIS ADDS OVER SUPERCODE'S OWN DOOR, so the next reader does not take it for a
|
|
1319
|
+
* duplicate: the controller already refuses a not-installed or `required` harness in
|
|
1320
|
+
* `harnessActions` (`start: installed && auth !== 'required' && …`) from its LAST
|
|
1321
|
+
* INVENTORY — measured verbatim here on a not-installed harness, which never reaches
|
|
1322
|
+
* this seam. This read is FRESH at the moment of launch, so a sign-in or sign-out since
|
|
1323
|
+
* that refresh counts, and it escalates the `unknown` the controller treats as
|
|
1324
|
+
* startable into a verdict instead of an opaque failure inside the harness.
|
|
1325
|
+
*/
|
|
1326
|
+
private async harnessReadiness(
|
|
1327
|
+
harness: string,
|
|
1328
|
+
workspace: string,
|
|
1329
|
+
): Promise<HarnessReadiness | null> {
|
|
1330
|
+
const client = this.discoveryClient;
|
|
1331
|
+
if (client?.listHarnesses) {
|
|
1332
|
+
const ask = async (probe: 'passive' | 'handshake') => {
|
|
1333
|
+
const { harnesses } = (await client.listHarnesses?.({
|
|
1334
|
+
workspace,
|
|
1335
|
+
harnesses: [harness],
|
|
1336
|
+
probe,
|
|
1337
|
+
skip_versions: true,
|
|
1338
|
+
})) ?? { harnesses: [] };
|
|
1339
|
+
return harnesses.find((item) => item.id === harness);
|
|
1340
|
+
};
|
|
1341
|
+
try {
|
|
1342
|
+
let entry = await ask('passive');
|
|
1343
|
+
if (entry?.installed && entry.auth === 'unknown') entry = (await ask('handshake')) ?? entry;
|
|
1344
|
+
if (entry) {
|
|
1345
|
+
return {
|
|
1346
|
+
id: entry.id,
|
|
1347
|
+
label: entry.display_name,
|
|
1348
|
+
installed: entry.installed,
|
|
1349
|
+
auth: entry.auth,
|
|
1350
|
+
reason: entry.reason,
|
|
1351
|
+
repair: entry.repair,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
} catch {
|
|
1355
|
+
// An inventory read that fails is not evidence the harness is unusable.
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const known = this.lastSnapshot.harnesses.find((item) => item.id === harness);
|
|
1359
|
+
return known
|
|
1360
|
+
? {
|
|
1361
|
+
id: known.id,
|
|
1362
|
+
label: known.label,
|
|
1363
|
+
installed: known.installed,
|
|
1364
|
+
auth: known.auth,
|
|
1365
|
+
reason: known.reason,
|
|
1366
|
+
repair: known.repair,
|
|
1367
|
+
}
|
|
1368
|
+
: null;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
private async rememberSessionInventory(
|
|
1372
|
+
sessions: readonly HeadlessSessionDescriptor[],
|
|
1373
|
+
sessionReconnectIdentity: HeadlessSessionIdentity,
|
|
1374
|
+
workspace: string,
|
|
1375
|
+
): Promise<void> {
|
|
1376
|
+
const captureGeneration = ++this.inventoryCaptureGeneration;
|
|
1377
|
+
const entries = await Promise.all(
|
|
1378
|
+
sessions.map(
|
|
1379
|
+
async (descriptor) =>
|
|
1380
|
+
[await sessionReconnectIdentity(descriptor.locator), descriptor] as const,
|
|
1381
|
+
),
|
|
1382
|
+
);
|
|
1383
|
+
if (this.workspace === workspace && captureGeneration === this.inventoryCaptureGeneration) {
|
|
1384
|
+
this.sessionDescriptorsByIdentity = new Map(entries);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private publishSubagentInspector(inspector: SupercodeUiState['subagentInspector']): void {
|
|
1389
|
+
this.subagentInspector = inspector;
|
|
1390
|
+
if (!this.remoteHost) return;
|
|
1391
|
+
this.remoteHost.refreshProjection();
|
|
1392
|
+
this.capture();
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private async openSubagents(parentKey: string): Promise<void> {
|
|
1396
|
+
const snapshot = this.lastHeadlessSnapshot;
|
|
1397
|
+
const client = this.discoveryClient;
|
|
1398
|
+
const identityFor = this.sessionIdentity;
|
|
1399
|
+
const parent = snapshot?.sessions.find((session) => session.key === parentKey);
|
|
1400
|
+
const parentTitle =
|
|
1401
|
+
this.remoteHost?.getFrame().state.sessions.find((row) => row.key === parentKey)?.title ??
|
|
1402
|
+
'Conversation';
|
|
1403
|
+
this.subagentDescriptorsByKey.clear();
|
|
1404
|
+
this.publishSubagentInspector({
|
|
1405
|
+
parentKey,
|
|
1406
|
+
parentTitle,
|
|
1407
|
+
status: 'loading',
|
|
1408
|
+
items: [],
|
|
1409
|
+
selectedKey: null,
|
|
1410
|
+
transcript: [],
|
|
1411
|
+
error: null,
|
|
1412
|
+
});
|
|
1413
|
+
if (!parent || !client || !identityFor || !this.workspace) {
|
|
1414
|
+
this.publishSubagentInspector({
|
|
1415
|
+
parentKey,
|
|
1416
|
+
parentTitle,
|
|
1417
|
+
status: 'error',
|
|
1418
|
+
items: [],
|
|
1419
|
+
selectedKey: null,
|
|
1420
|
+
transcript: [],
|
|
1421
|
+
error: 'This conversation family is no longer available.',
|
|
1422
|
+
});
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
const discovered = await client.discover({
|
|
1427
|
+
workspace: this.workspace,
|
|
1428
|
+
limit: 500,
|
|
1429
|
+
include_topic_candidates: true,
|
|
1430
|
+
include_child_sessions: true,
|
|
1431
|
+
});
|
|
1432
|
+
const descendants: HeadlessSessionDescriptor[] = [];
|
|
1433
|
+
const pendingParents = new Set([parent.sessionId]);
|
|
1434
|
+
let changed = true;
|
|
1435
|
+
while (changed) {
|
|
1436
|
+
changed = false;
|
|
1437
|
+
for (const descriptor of discovered.sessions) {
|
|
1438
|
+
if (
|
|
1439
|
+
!descriptor.parent_session_id ||
|
|
1440
|
+
!pendingParents.has(descriptor.parent_session_id) ||
|
|
1441
|
+
descendants.includes(descriptor)
|
|
1442
|
+
) {
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
descendants.push(descriptor);
|
|
1446
|
+
pendingParents.add(descriptor.locator.session_id);
|
|
1447
|
+
changed = true;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
const keys = new Map<string, string>();
|
|
1451
|
+
for (const descriptor of descendants) {
|
|
1452
|
+
const key = await identityFor(descriptor.locator);
|
|
1453
|
+
keys.set(locatorKey(descriptor), key);
|
|
1454
|
+
this.subagentDescriptorsByKey.set(key, descriptor);
|
|
1455
|
+
}
|
|
1456
|
+
const items = projectSubagentInventory(descendants, {
|
|
1457
|
+
keyFor: (descriptor) => keys.get(locatorKey(descriptor)) ?? '',
|
|
1458
|
+
home: homedir(),
|
|
1459
|
+
preserveOrder: true,
|
|
1460
|
+
});
|
|
1461
|
+
this.publishSubagentInspector({
|
|
1462
|
+
parentKey,
|
|
1463
|
+
parentTitle,
|
|
1464
|
+
status: 'ready',
|
|
1465
|
+
items,
|
|
1466
|
+
selectedKey: null,
|
|
1467
|
+
transcript: [],
|
|
1468
|
+
error: null,
|
|
1469
|
+
});
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
this.publishSubagentInspector({
|
|
1472
|
+
parentKey,
|
|
1473
|
+
parentTitle,
|
|
1474
|
+
status: 'error',
|
|
1475
|
+
items: [],
|
|
1476
|
+
selectedKey: null,
|
|
1477
|
+
transcript: [],
|
|
1478
|
+
error: errorMessage(error),
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
private async openSubagent(parentKey: string, key: string): Promise<void> {
|
|
1484
|
+
const inspector = this.subagentInspector;
|
|
1485
|
+
const descriptor = this.subagentDescriptorsByKey.get(key);
|
|
1486
|
+
const client = this.discoveryClient;
|
|
1487
|
+
if (!inspector || inspector.parentKey !== parentKey || !descriptor || !client) return;
|
|
1488
|
+
this.publishSubagentInspector({
|
|
1489
|
+
...inspector,
|
|
1490
|
+
status: 'loading',
|
|
1491
|
+
selectedKey: key,
|
|
1492
|
+
transcript: [],
|
|
1493
|
+
error: null,
|
|
1494
|
+
});
|
|
1495
|
+
try {
|
|
1496
|
+
const loaded = await client.load(descriptor.locator, {
|
|
1497
|
+
view: { tailMessages: 120, maxMessageChars: 16_000, includeSubagents: false },
|
|
1498
|
+
});
|
|
1499
|
+
this.publishSubagentInspector({
|
|
1500
|
+
...inspector,
|
|
1501
|
+
status: 'ready',
|
|
1502
|
+
selectedKey: key,
|
|
1503
|
+
transcript: projectSubagentTranscript(loaded.session, {
|
|
1504
|
+
maxEntries: 120,
|
|
1505
|
+
maxScanEntries: 480,
|
|
1506
|
+
maxEntryChars: 8_000,
|
|
1507
|
+
prefix: key,
|
|
1508
|
+
}),
|
|
1509
|
+
error: null,
|
|
1510
|
+
});
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
this.publishSubagentInspector({
|
|
1513
|
+
...inspector,
|
|
1514
|
+
status: 'error',
|
|
1515
|
+
selectedKey: key,
|
|
1516
|
+
transcript: [],
|
|
1517
|
+
error: errorMessage(error),
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
private closeSubagents(): void {
|
|
1523
|
+
this.subagentDescriptorsByKey.clear();
|
|
1524
|
+
this.publishSubagentInspector(null);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/** The ONE projection the frame and the image registry are both built from.
|
|
1528
|
+
* A reference only resolves while it is inside this same bounded window. */
|
|
1529
|
+
private projectionOptions(state: { history?: HeadlessSnapshot['history'] }) {
|
|
1530
|
+
const transcriptLimit = state.history?.transcriptLimit ?? 120;
|
|
1531
|
+
return {
|
|
1532
|
+
maxEntries: transcriptLimit,
|
|
1533
|
+
maxEntryChars: 8_000,
|
|
1534
|
+
maxScanEntries: transcriptLimit * TRANSCRIPT_SCAN_MULTIPLIER,
|
|
1535
|
+
history: {
|
|
1536
|
+
transcriptLimit,
|
|
1537
|
+
hasEarlier: state.history?.hasEarlier ?? false,
|
|
1538
|
+
},
|
|
1539
|
+
subagentInspector: this.subagentInspector,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
private capture(): void {
|
|
1545
|
+
if (!this.controller || !this.remoteHost) return;
|
|
1546
|
+
const snapshot = this.assertSnapshot(this.controller.getSnapshot());
|
|
1547
|
+
if (snapshot.workspace !== this.workspace) return;
|
|
1548
|
+
let frame = this.remoteHost.getFrame();
|
|
1549
|
+
if (frame.controllerRevision !== snapshot.revision) {
|
|
1550
|
+
frame = this.remoteHost.refreshProjection();
|
|
1551
|
+
}
|
|
1552
|
+
this.lastHeadlessSnapshot = snapshot;
|
|
1553
|
+
this.lastSnapshot = toSnapshot(snapshot, frame);
|
|
1554
|
+
this.renderedInventoryFingerprint = descriptorPresentationFingerprint([
|
|
1555
|
+
...this.sessionDescriptorsByIdentity.values(),
|
|
1556
|
+
]);
|
|
1557
|
+
this.options.onChange(this.snapshot());
|
|
1558
|
+
for (const listener of this.bridgeListeners) listener();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
private scheduleDiscovery(): void {
|
|
1562
|
+
if (this.discoveryTimer) clearTimeout(this.discoveryTimer);
|
|
1563
|
+
this.discoveryTimer = null;
|
|
1564
|
+
const delay = this.options.discoveryPollMs ?? 4_000;
|
|
1565
|
+
if (this.closed || delay <= 0) return;
|
|
1566
|
+
this.discoveryTimer = setTimeout(() => {
|
|
1567
|
+
this.discoveryTimer = null;
|
|
1568
|
+
void this.pollDiscovery();
|
|
1569
|
+
}, delay);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
private async pollDiscovery(): Promise<void> {
|
|
1573
|
+
if (this.closed || this.discoveryInFlight) return;
|
|
1574
|
+
const controller = this.controller;
|
|
1575
|
+
const client = this.discoveryClient;
|
|
1576
|
+
const snapshot = this.lastSnapshot;
|
|
1577
|
+
const raw = this.lastHeadlessSnapshot;
|
|
1578
|
+
if (
|
|
1579
|
+
!controller ||
|
|
1580
|
+
!client ||
|
|
1581
|
+
!raw ||
|
|
1582
|
+
!this.workspace ||
|
|
1583
|
+
snapshot.operation !== null ||
|
|
1584
|
+
snapshot.turn.state !== 'idle'
|
|
1585
|
+
) {
|
|
1586
|
+
this.scheduleDiscovery();
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
this.discoveryInFlight = true;
|
|
1590
|
+
try {
|
|
1591
|
+
const discovered = await client.discover({
|
|
1592
|
+
workspace: this.workspace,
|
|
1593
|
+
limit: 100,
|
|
1594
|
+
include_topic_candidates: false,
|
|
1595
|
+
});
|
|
1596
|
+
const presentationFingerprint = descriptorPresentationFingerprint(discovered.sessions);
|
|
1597
|
+
const controllerInventoryChanged =
|
|
1598
|
+
sessionIdentityFingerprint(this.sessionDescriptorsByIdentity.keys()) !==
|
|
1599
|
+
sessionIdentityFingerprint(raw.sessions.map((session) => session.identity));
|
|
1600
|
+
if (controllerInventoryChanged) {
|
|
1601
|
+
await controller.dispatch({
|
|
1602
|
+
type: 'refresh',
|
|
1603
|
+
autoObserve: false,
|
|
1604
|
+
silent: true,
|
|
1605
|
+
});
|
|
1606
|
+
if (!this.closed && controller === this.controller) this.capture();
|
|
1607
|
+
} else if (presentationFingerprint !== this.renderedInventoryFingerprint) {
|
|
1608
|
+
// Presentation evidence belongs in the controller snapshot too. Feed
|
|
1609
|
+
// the changed inventory back through Supercode instead of joining raw
|
|
1610
|
+
// descriptors into a second VGAI-owned list projection.
|
|
1611
|
+
await controller.dispatch({
|
|
1612
|
+
type: 'refresh',
|
|
1613
|
+
autoObserve: false,
|
|
1614
|
+
silent: true,
|
|
1615
|
+
});
|
|
1616
|
+
if (!this.closed && controller === this.controller) this.capture();
|
|
1617
|
+
}
|
|
1618
|
+
} catch {
|
|
1619
|
+
// Explicit Refresh remains the visible recovery path. Background discovery
|
|
1620
|
+
// is observational and must not replace a healthy chat snapshot with noise.
|
|
1621
|
+
} finally {
|
|
1622
|
+
this.discoveryInFlight = false;
|
|
1623
|
+
this.scheduleDiscovery();
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
private beginWorkspace(workspace: string): void {
|
|
1628
|
+
if (this.workspace === workspace) return;
|
|
1629
|
+
this.workspace = workspace;
|
|
1630
|
+
this.workspaceGeneration++;
|
|
1631
|
+
this.lastHeadlessSnapshot = null;
|
|
1632
|
+
this.sessionDescriptorsByIdentity.clear();
|
|
1633
|
+
this.subagentDescriptorsByKey.clear();
|
|
1634
|
+
this.subagentInspector = null;
|
|
1635
|
+
this.inventoryCaptureGeneration++;
|
|
1636
|
+
this.renderedInventoryFingerprint = null;
|
|
1637
|
+
this.lastSnapshot = loading(workspace, this.workspaceGeneration, this.serverInstanceId);
|
|
1638
|
+
this.options.onChange(this.snapshot());
|
|
1639
|
+
for (const listener of this.bridgeListeners) listener();
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** The project-work bridge observes the existing controller; it never creates another one. */
|
|
1643
|
+
bridgeAdapter() {
|
|
1644
|
+
return {
|
|
1645
|
+
getSnapshot: () => {
|
|
1646
|
+
const raw = this.lastHeadlessSnapshot;
|
|
1647
|
+
if (raw)
|
|
1648
|
+
return {
|
|
1649
|
+
sessions: raw.sessions,
|
|
1650
|
+
activeSessionKey: raw.activeSessionKey,
|
|
1651
|
+
activeSession: raw.activeSession ?? null,
|
|
1652
|
+
conversation: raw.conversation,
|
|
1653
|
+
taskPlan: raw.taskPlan ?? this.deriveTaskPlan(raw.activeSession ?? null),
|
|
1654
|
+
turn: raw.turn,
|
|
1655
|
+
requests: raw.requests,
|
|
1656
|
+
};
|
|
1657
|
+
return {
|
|
1658
|
+
sessions: [],
|
|
1659
|
+
activeSessionKey: null,
|
|
1660
|
+
activeSession: null,
|
|
1661
|
+
conversation: [],
|
|
1662
|
+
taskPlan: { source: 'none' as const, items: [], residue: [], observedAt: null },
|
|
1663
|
+
turn: { state: 'idle' as const },
|
|
1664
|
+
requests: [],
|
|
1665
|
+
};
|
|
1666
|
+
},
|
|
1667
|
+
subscribe: (listener: () => void) => {
|
|
1668
|
+
this.bridgeListeners.add(listener);
|
|
1669
|
+
return () => this.bridgeListeners.delete(listener);
|
|
1670
|
+
},
|
|
1671
|
+
projectConversation: (session: HeadlessLoadedSession) => this.projectConversation(session),
|
|
1672
|
+
deriveTaskPlan: (session: HeadlessLoadedSession | null) => this.deriveTaskPlan(session),
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async loadSessionForProjectWork(sessionKey: string): Promise<HeadlessLoadedSession> {
|
|
1677
|
+
await this.ensureController(false);
|
|
1678
|
+
if (!this.controller) throw new Error('Supercode is unavailable.');
|
|
1679
|
+
return this.controller.loadSession(sessionKey);
|
|
1680
|
+
}
|
|
1681
|
+
}
|