@timurproko/a1 0.1.8-dev.472 → 0.1.8-dev.508
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/bin/cli.js +4 -5
- package/bin/guardian.js +5 -1
- package/bin/module-identity.js +53 -98
- package/bin/module-resolver.js +138 -0
- package/bin/pinned-pi-public.js +25 -6
- package/bin/ui.js +7 -2
- package/bin/update-recovery.js +76 -7
- package/bin/warmup.js +3 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-diagnostics.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-image.js +1 -1
- package/dist/app/session-shell/helper-pool.d.ts +33 -0
- package/dist/app/session-shell/helper-pool.js +98 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation-client.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation-client.js +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation.js +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-source.js +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-worker.js +1 -1
- package/dist/app/session-shell/index.d.ts +7 -0
- package/dist/app/session-shell/index.js +4 -0
- package/dist/app/session-shell/paste-executor.d.ts +18 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-executor.js +50 -29
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-helper.js +1 -1
- package/dist/app/session-shell/paste-preparation-client.d.ts +31 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-preparation-client.js +19 -8
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-protocol.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-protocol.js +1 -1
- package/dist/app/session-shell/paste-text-preparation.d.ts +6 -0
- package/dist/{integrations/pi/session-ui/paste-text-preparation.d.ts → app/session-shell/paste-types.d.ts} +1 -4
- package/dist/{integrations/pi/session-ui → app/session-shell}/path-chip-presentation.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-chips.d.ts +5 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-chips.js +6 -2
- package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-history-controller.d.ts +2 -2
- package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-history-controller.js +2 -2
- package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-suggestion-controller.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-suggestion-controller.js +2 -2
- package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro.js +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-coordinator.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-coordinator.js +3 -5
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-helper.js +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-protocol.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-transport.d.ts +10 -2
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-transport.js +45 -19
- package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell-root.d.ts +75 -53
- package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell-root.js +30 -20
- package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell.d.ts +9 -8
- package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell.js +108 -67
- package/dist/{integrations/pi/session-ui → app/session-shell}/session-viewport-controller.d.ts +5 -5
- package/dist/{integrations/pi/session-ui → app/session-shell}/session-viewport-controller.js +5 -5
- package/dist/{integrations/pi/session-ui → app/session-shell}/system-clipboard.d.ts +1 -1
- package/dist/{integrations/pi/session-ui → app/session-shell}/system-clipboard.js +1 -1
- package/dist/cli/index.d.ts +8 -5
- package/dist/cli/index.js +4 -5
- package/dist/composition/index.d.ts +0 -1
- package/dist/composition/index.js +0 -1
- package/dist/composition/owned-ui.d.ts +2 -2
- package/dist/composition/owned-ui.js +35 -36
- package/dist/composition/settings-route-host.d.ts +2 -2
- package/dist/contracts/agent-engine/domain.d.ts +6 -1
- package/dist/contracts/agent-engine/index.d.ts +11 -8
- package/dist/contracts/agent-engine/index.js +6 -8
- package/dist/contracts/owned-ui/extension-ui.d.ts +8 -0
- package/dist/contracts/owned-ui/index.d.ts +12 -8
- package/dist/contracts/owned-ui/index.js +8 -8
- package/dist/features/launch/index.d.ts +12 -6
- package/dist/features/launch/index.js +6 -6
- package/dist/features/launch/intent.d.ts +5 -5
- package/dist/features/launch/intent.js +2 -2
- package/dist/features/owned-ui/index.d.ts +6 -6
- package/dist/features/owned-ui/index.js +4 -6
- package/dist/features/owned-ui/run.d.ts +2 -2
- package/dist/features/owned-ui/settings-app.d.ts +2 -2
- package/dist/features/owned-ui/settings-app.js +18 -5
- package/dist/features/prompt-history/service.d.ts +1 -1
- package/dist/features/prompt-history/service.js +1 -1
- package/dist/features/prompt-history/store.js +3 -7
- package/dist/features/prompt-suggestions/diagnostics.js +1 -1
- package/dist/features/prompt-suggestions/index.d.ts +2 -1
- package/dist/features/prompt-suggestions/index.js +1 -1
- package/dist/foundation/launch-guardian/index.d.ts +2 -1
- package/dist/foundation/launch-guardian/index.js +1 -1
- package/dist/foundation/lifecycle/index.d.ts +11 -6
- package/dist/foundation/lifecycle/index.js +5 -6
- package/dist/foundation/process-containment/index.d.ts +8 -6
- package/dist/foundation/process-containment/index.js +6 -6
- package/dist/foundation/process-containment/native-guardian-containment.js +3 -3
- package/dist/foundation/protocol/index.d.ts +3 -2
- package/dist/foundation/protocol/index.js +2 -2
- package/dist/foundation/release/bootstrap.d.ts +0 -4
- package/dist/foundation/release/bootstrap.js +1 -82
- package/dist/foundation/release/dependency-layer.d.ts +2 -5
- package/dist/foundation/release/endpoints.d.ts +4 -0
- package/dist/foundation/release/endpoints.js +86 -2
- package/dist/foundation/release/index.d.ts +30 -16
- package/dist/foundation/release/index.js +16 -16
- package/dist/foundation/release/release.d.ts +1 -1
- package/dist/foundation/release/types.d.ts +11 -0
- package/dist/foundation/release/types.js +1 -0
- package/dist/foundation/release/update-transaction.d.ts +1 -1
- package/dist/foundation/release/update.d.ts +2 -1
- package/dist/foundation/release/update.js +3 -18
- package/dist/foundation/startup/index.d.ts +4 -2
- package/dist/foundation/startup/index.js +2 -2
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/foundation/storage/control-store.d.ts +1 -45
- package/dist/foundation/storage/control-store.js +54 -277
- package/dist/foundation/storage/index.d.ts +1 -1
- package/dist/foundation/storage/index.js +1 -1
- package/dist/foundation/supervision/index.d.ts +2 -2
- package/dist/foundation/supervision/index.js +2 -2
- package/dist/foundation/supervision/server.d.ts +5 -10
- package/dist/foundation/supervision/server.js +7 -7
- package/dist/foundation/terminal-cleanup/index.d.ts +3 -2
- package/dist/foundation/terminal-cleanup/index.js +2 -2
- package/dist/integrations/pi/components/editor-interaction.d.ts +1 -1
- package/dist/integrations/pi/components/history-editor-loader.d.ts +1 -1
- package/dist/integrations/pi/components/index.d.ts +20 -13
- package/dist/integrations/pi/components/index.js +12 -13
- package/dist/integrations/pi/components/owned-editor-ux.js +5 -8
- package/dist/integrations/pi/components/resources/builtin-themes.d.ts +7 -5
- package/dist/integrations/pi/components/resources/builtin-themes.js +7 -5
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +1 -1
- package/dist/integrations/pi/components/shell-extension-ui.js +2 -1
- package/dist/integrations/pi/components/shell-footer-status.js +1 -1
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +1 -1
- package/dist/integrations/pi/components/shell-presenters-info.js +1 -1
- package/dist/integrations/pi/components/shell-presenters-transcript.js +1 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +9 -3
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +9 -6
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +3 -1
- package/dist/integrations/pi/components/shell-shared-facade.js +4 -2
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +1 -1
- package/dist/integrations/pi/components/tool-image-presentation.d.ts +1 -1
- package/dist/integrations/pi/components/tool-image-presentation.js +4 -7
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +36 -22
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +43 -7
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
- package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +6 -3
- package/dist/integrations/pi/components/upstream/components/daxnuts.d.ts +8 -1
- package/dist/integrations/pi/components/upstream/components/daxnuts.js +7 -3
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +10 -4
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +7 -4
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -4
- package/dist/integrations/pi/components/upstream/components/mermaid.js +8 -4
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +9 -5
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +10 -6
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +8 -4
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +27 -25
- package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +8 -10
- package/dist/integrations/pi/components/upstream/components/session-selector-search.js +6 -4
- package/dist/integrations/pi/components/upstream/components/session-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector.js +8 -4
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +7 -5
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +8 -6
- package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +9 -4
- package/dist/integrations/pi/components/upstream/components/status-indicator.js +15 -5
- package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +8 -2
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +58 -37
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +7 -4
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +7 -4
- package/dist/integrations/pi/components/upstream/components/trust-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +10 -5
- package/dist/integrations/pi/components/upstream/external-editor.js +8 -4
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +13 -4
- package/dist/integrations/pi/components/upstream/history/editor-core.js +112 -30
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +6 -3
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +6 -3
- package/dist/integrations/pi/components/upstream/history/printable-key.js +7 -4
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +6 -3
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +6 -3
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +6 -3
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +6 -3
- package/dist/integrations/pi/components/upstream/model-search.d.ts +5 -2
- package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +8 -0
- package/dist/integrations/pi/components/upstream/theme/theme-controller.js +0 -3
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/theme/theme.js +17 -6
- package/dist/integrations/pi/engine/adapter.d.ts +39 -138
- package/dist/integrations/pi/engine/adapter.js +338 -3226
- package/dist/integrations/pi/engine/command-dispatch.d.ts +41 -0
- package/dist/integrations/pi/engine/command-dispatch.js +125 -0
- package/dist/integrations/pi/engine/event-delivery.d.ts +100 -0
- package/dist/integrations/pi/engine/event-delivery.js +184 -0
- package/dist/integrations/pi/engine/extension-ui-binding.d.ts +35 -0
- package/dist/integrations/pi/engine/extension-ui-binding.js +81 -0
- package/dist/integrations/pi/engine/index.d.ts +18 -14
- package/dist/integrations/pi/engine/index.js +10 -14
- package/dist/integrations/pi/engine/message-values.d.ts +29 -0
- package/dist/integrations/pi/engine/message-values.js +168 -0
- package/dist/integrations/pi/engine/pending-delivery.d.ts +2 -2
- package/dist/integrations/pi/engine/pending-delivery.js +2 -4
- package/dist/integrations/pi/engine/pi-settings-metadata.json +382 -371
- package/dist/integrations/pi/engine/prompt-suggestions.d.ts +30 -0
- package/dist/integrations/pi/engine/prompt-suggestions.js +102 -0
- package/dist/integrations/pi/engine/provider-authentication.d.ts +34 -0
- package/dist/integrations/pi/engine/provider-authentication.js +209 -0
- package/dist/integrations/pi/engine/resource-catalog.d.ts +49 -0
- package/dist/integrations/pi/engine/resource-catalog.js +289 -0
- package/dist/integrations/pi/engine/resources/changelog.json +4 -4
- package/dist/integrations/pi/engine/session-events.d.ts +48 -0
- package/dist/integrations/pi/engine/session-events.js +223 -0
- package/dist/integrations/pi/engine/session-integration.js +2 -4
- package/dist/integrations/pi/engine/session-runtime.d.ts +84 -0
- package/dist/integrations/pi/engine/session-runtime.js +249 -0
- package/dist/integrations/pi/engine/{settings-effects.d.ts → settings-bridge.d.ts} +57 -2
- package/dist/integrations/pi/engine/settings-bridge.js +514 -0
- package/dist/integrations/pi/engine/settings-metadata.d.ts +31 -0
- package/dist/integrations/pi/engine/settings-metadata.js +67 -0
- package/dist/integrations/pi/engine/settings-port.d.ts +42 -0
- package/dist/integrations/pi/engine/settings-port.js +353 -0
- package/dist/integrations/pi/engine/transcript-projection.d.ts +39 -0
- package/dist/integrations/pi/engine/transcript-projection.js +405 -0
- package/dist/integrations/pi/engine/usage-view.d.ts +4 -0
- package/dist/integrations/pi/engine/usage-view.js +56 -0
- package/dist/integrations/pi/engine/workflow-contexts.d.ts +59 -0
- package/dist/integrations/pi/engine/workflow-contexts.js +316 -0
- package/dist/integrations/pi/engine/workflow-runner.d.ts +60 -0
- package/dist/integrations/pi/engine/workflow-runner.js +509 -0
- package/dist/integrations/pi/engine/workflow-support.d.ts +33 -0
- package/dist/integrations/pi/engine/workflow-support.js +352 -0
- package/dist/integrations/pi/engine/workflows.d.ts +63 -2
- package/dist/integrations/pi/engine/workflows.js +5 -2
- package/dist/integrations/pi/startup-public.js +576 -554
- package/dist/integrations/pi/startup-public.manifest.json +1730 -984
- package/dist/integrations/pi/tui-runtime/adapter.js +10 -13
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +11 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +4 -7
- package/dist/integrations/pi/tui-runtime/index.d.ts +8 -6
- package/dist/integrations/pi/tui-runtime/index.js +4 -6
- package/dist/integrations/pi/tui-runtime/mouse-report-input.js +2 -4
- package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
- package/dist/integrations/pi/tui-runtime/overlay-geometry.js +4 -5
- package/dist/integrations/pi/tui-runtime/presentation-adapter.js +1 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/runtime-payload-inventory.json +51320 -46509
- package/dist/ui/apps/index.d.ts +5 -3
- package/dist/ui/apps/index.js +3 -3
- package/dist/ui/components/index.d.ts +47 -25
- package/dist/ui/components/index.js +25 -25
- package/dist/ui/components/prompt-input.js +1 -4
- package/dist/ui/settings/declarations.d.ts +119 -0
- package/dist/ui/settings/declarations.js +20 -11
- package/dist/ui/settings/index.d.ts +10 -6
- package/dist/ui/settings/index.js +5 -6
- package/dist/ui/settings/manager.d.ts +54 -0
- package/dist/ui/settings/{session.js → manager.js} +101 -9
- package/dist/ui/settings/sections.d.ts +2 -7
- package/docs/architecture/boundaries.md +4 -4
- package/docs/architecture/history-editor-provenance.md +1 -1
- package/docs/architecture/project-structure.md +6 -6
- package/docs/architecture/prompt-suggestions.md +1 -1
- package/docs/architecture/terminal-host-proof-gate.md +1 -1
- package/docs/architecture/terminal-host-spike-evidence.md +1 -1
- package/docs/architecture/toolchain.md +8 -0
- package/docs/architecture/ui-reference-provenance.md +4 -4
- package/docs/ci-release-runbook.md +7 -1
- package/docs/features/modal-content-interaction.md +2 -2
- package/docs/local-worktree-cleanup.md +4 -2
- package/docs/validation.md +6 -4
- package/package.json +7 -11
- package/bin/pi-tui.d.ts +0 -6
- package/bin/pi-tui.js +0 -28
- package/bin/sync-pi-tui-proxy.js +0 -97
- package/dist/cli/version.d.ts +0 -2
- package/dist/cli/version.js +0 -23
- package/dist/composition/agent-engine-bridge.d.ts +0 -3
- package/dist/composition/agent-engine-bridge.js +0 -108
- package/dist/composition/process.d.ts +0 -16
- package/dist/composition/process.js +0 -45
- package/dist/contracts/workspace/index.d.ts +0 -2
- package/dist/contracts/workspace/index.js +0 -2
- package/dist/contracts/workspace/model.d.ts +0 -248
- package/dist/contracts/workspace/model.js +0 -2
- package/dist/contracts/workspace/validation.d.ts +0 -13
- package/dist/contracts/workspace/validation.js +0 -335
- package/dist/features/owned-ui/customization.d.ts +0 -40
- package/dist/features/owned-ui/customization.js +0 -110
- package/dist/features/owned-ui/diagnostics.d.ts +0 -34
- package/dist/features/owned-ui/diagnostics.js +0 -95
- package/dist/features/workspace/capabilities.d.ts +0 -31
- package/dist/features/workspace/capabilities.js +0 -75
- package/dist/features/workspace/index.d.ts +0 -6
- package/dist/features/workspace/index.js +0 -6
- package/dist/features/workspace/presentation.d.ts +0 -30
- package/dist/features/workspace/presentation.js +0 -70
- package/dist/features/workspace/reconciliation.d.ts +0 -15
- package/dist/features/workspace/reconciliation.js +0 -82
- package/dist/features/workspace/reducer.d.ts +0 -43
- package/dist/features/workspace/reducer.js +0 -214
- package/dist/features/workspace/router.d.ts +0 -32
- package/dist/features/workspace/router.js +0 -125
- package/dist/features/workspace/store.d.ts +0 -18
- package/dist/features/workspace/store.js +0 -55
- package/dist/foundation/native-host-protocol/codec.d.ts +0 -13
- package/dist/foundation/native-host-protocol/codec.js +0 -58
- package/dist/foundation/native-host-protocol/evidence.d.ts +0 -67
- package/dist/foundation/native-host-protocol/evidence.js +0 -141
- package/dist/foundation/native-host-protocol/index.d.ts +0 -4
- package/dist/foundation/native-host-protocol/index.js +0 -4
- package/dist/foundation/native-host-protocol/messages.d.ts +0 -56
- package/dist/foundation/native-host-protocol/messages.js +0 -149
- package/dist/foundation/native-host-protocol/proof-gate.d.ts +0 -13
- package/dist/foundation/native-host-protocol/proof-gate.js +0 -33
- package/dist/foundation/structured-agent-runtime/backpressure.d.ts +0 -47
- package/dist/foundation/structured-agent-runtime/backpressure.js +0 -119
- package/dist/foundation/structured-agent-runtime/commands.d.ts +0 -53
- package/dist/foundation/structured-agent-runtime/commands.js +0 -165
- package/dist/foundation/structured-agent-runtime/handshake.d.ts +0 -41
- package/dist/foundation/structured-agent-runtime/handshake.js +0 -151
- package/dist/foundation/structured-agent-runtime/index.d.ts +0 -5
- package/dist/foundation/structured-agent-runtime/index.js +0 -5
- package/dist/foundation/structured-agent-runtime/reconnection.d.ts +0 -38
- package/dist/foundation/structured-agent-runtime/reconnection.js +0 -130
- package/dist/foundation/structured-agent-runtime/state.d.ts +0 -57
- package/dist/foundation/structured-agent-runtime/state.js +0 -119
- package/dist/integrations/pi/components/upstream/components/custom-entry.d.ts +0 -17
- package/dist/integrations/pi/components/upstream/components/custom-entry.js +0 -51
- package/dist/integrations/pi/components/upstream/components/first-time-setup.d.ts +0 -24
- package/dist/integrations/pi/components/upstream/components/first-time-setup.js +0 -105
- package/dist/integrations/pi/components/upstream/components/markdown-transform.d.ts +0 -2
- package/dist/integrations/pi/components/upstream/components/markdown-transform.js +0 -18
- package/dist/integrations/pi/engine/model-auth-integration.d.ts +0 -31
- package/dist/integrations/pi/engine/model-auth-integration.js +0 -62
- package/dist/integrations/pi/engine/resource-extension-integration.d.ts +0 -32
- package/dist/integrations/pi/engine/resource-extension-integration.js +0 -81
- package/dist/integrations/pi/engine/settings-effects.js +0 -249
- package/dist/integrations/pi/engine/settings-integration.d.ts +0 -43
- package/dist/integrations/pi/engine/settings-integration.js +0 -227
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +0 -21
- package/dist/integrations/pi/engine/workflow-controllers.js +0 -49
- package/dist/integrations/pi/session-ui/index.d.ts +0 -5
- package/dist/integrations/pi/session-ui/index.js +0 -5
- package/dist/integrations/pi/session-ui/paste-executor.d.ts +0 -10
- package/dist/integrations/pi/session-ui/paste-preparation-client.d.ts +0 -16
- package/dist/integrations/pi/session-ui/route-host.d.ts +0 -5
- package/dist/integrations/pi/tui-runtime/conformance.d.ts +0 -18
- package/dist/integrations/pi/tui-runtime/conformance.js +0 -145
- package/dist/ui/components/prompt-row.d.ts +0 -14
- package/dist/ui/components/prompt-row.js +0 -20
- package/dist/ui/settings/session.d.ts +0 -36
- package/dist/ui/settings/store.d.ts +0 -29
- package/dist/ui/settings/store.js +0 -91
- package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-diagnostics.js +2 -2
- package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-image.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/editor-hyperlink-budget.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/editor-hyperlink-budget.js +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-source.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/image-worker.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-helper.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-text-preparation.js +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-text-worker.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/paste-text-worker.js +0 -0
- package/dist/{integrations/pi/session-ui/response-copy-helper.d.ts → app/session-shell/paste-types.js} +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/path-chip-presentation.js +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro-effects.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro-effects.js +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro.d.ts +0 -0
- package/dist/{integrations/pi/session-ui/route-host.js → app/session-shell/response-copy-helper.d.ts} +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-protocol.js +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/stream-presentation-coalescer.d.ts +0 -0
- package/dist/{integrations/pi/session-ui → app/session-shell}/stream-presentation-coalescer.js +3 -3
- /package/dist/{integrations/pi/session-ui → app/session-shell}/text-paste.d.ts +0 -0
- /package/dist/{integrations/pi/session-ui → app/session-shell}/text-paste.js +0 -0
|
@@ -1,80 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { observeCompactionProgress } from "./compaction-progress.js";
|
|
18
|
-
import { PiSettingsIntegration } from "./settings-integration.js";
|
|
19
|
-
import { PendingEngineDelivery } from "./pending-delivery.js";
|
|
1
|
+
import { PiTranscriptProjection } from "./transcript-projection.js";
|
|
2
|
+
import { isRecord, readModel, readStringArray, readThinkingLevel, stringProperty } from "./message-values.js";
|
|
3
|
+
import { getAgentDir } from "../startup-public.js";
|
|
4
|
+
import { assertOwnedUiSnapshot, } from "../../../contracts/owned-ui/index.js";
|
|
5
|
+
import { PiCommandDispatch } from "./command-dispatch.js";
|
|
6
|
+
import { PiEventDelivery } from "./event-delivery.js";
|
|
7
|
+
import { PiExtensionUiBinding } from "./extension-ui-binding.js";
|
|
8
|
+
import { PiPromptSuggestions } from "./prompt-suggestions.js";
|
|
9
|
+
import { PiResourceCatalog } from "./resource-catalog.js";
|
|
10
|
+
import { PiEngineRuntime } from "./session-runtime.js";
|
|
11
|
+
import { PiEngineSettings } from "./settings-port.js";
|
|
12
|
+
import { PiSessionEvents } from "./session-events.js";
|
|
13
|
+
import { readUsageView } from "./usage-view.js";
|
|
14
|
+
import { PiWorkflowContexts } from "./workflow-contexts.js";
|
|
15
|
+
import { PiWorkflowRunner } from "./workflow-runner.js";
|
|
16
|
+
import { defaultWorkflowHost } from "./workflow-support.js";
|
|
20
17
|
/** Explicit flush failure when required delivery was interrupted rather than completed. */
|
|
21
18
|
export class EngineDeliveryError extends Error {
|
|
22
19
|
constructor() { super("Engine delivery did not complete"); this.name = "EngineDeliveryError"; }
|
|
23
20
|
}
|
|
24
|
-
const execFileAsync = promisify(execFile);
|
|
25
|
-
/**
|
|
26
|
-
* Engine events delivered before the queue hands the event loop a turn. Small enough
|
|
27
|
-
* that a streaming burst never holds input, large enough that an ordinary turn is one
|
|
28
|
-
* batch.
|
|
29
|
-
*/
|
|
30
|
-
// Performance: deliver at most one engine event per event-loop turn. Transcript updates can
|
|
31
|
-
// be expensive in long sessions; a larger synchronous batch starves terminal
|
|
32
|
-
// input and makes an in-progress mouse selection appear frozen.
|
|
33
|
-
const EVENT_DELIVERY_BATCH = 1;
|
|
34
|
-
const AUTH_REFRESH_TIMEOUT_MS = 15_000;
|
|
35
|
-
// Provenance: Pi 0.84.2 core/model-resolver.ts defaultModelPerProvider.
|
|
36
|
-
const PINNED_DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
|
|
37
|
-
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
|
38
|
-
"ant-ling": "Ring-2.6-1T",
|
|
39
|
-
anthropic: "claude-opus-4-8",
|
|
40
|
-
openai: "gpt-5.5",
|
|
41
|
-
"azure-openai-responses": "gpt-5.4",
|
|
42
|
-
"openai-codex": "gpt-5.5",
|
|
43
|
-
radius: "auto",
|
|
44
|
-
nvidia: "nvidia/nemotron-3-super-120b-a12b",
|
|
45
|
-
deepseek: "deepseek-v4-pro",
|
|
46
|
-
google: "gemini-3.1-pro-preview",
|
|
47
|
-
"google-vertex": "gemini-3.1-pro-preview",
|
|
48
|
-
"github-copilot": "gpt-5.4",
|
|
49
|
-
openrouter: "moonshotai/kimi-k2.6",
|
|
50
|
-
"vercel-ai-gateway": "zai/glm-5.1",
|
|
51
|
-
xai: "grok-4.5",
|
|
52
|
-
groq: "openai/gpt-oss-120b",
|
|
53
|
-
cerebras: "zai-glm-4.7",
|
|
54
|
-
zai: "glm-5.1",
|
|
55
|
-
"zai-coding-cn": "glm-5.1",
|
|
56
|
-
mistral: "devstral-medium-latest",
|
|
57
|
-
minimax: "MiniMax-M2.7",
|
|
58
|
-
"minimax-cn": "MiniMax-M2.7",
|
|
59
|
-
moonshotai: "kimi-k2.6",
|
|
60
|
-
"moonshotai-cn": "kimi-k2.6",
|
|
61
|
-
huggingface: "moonshotai/Kimi-K2.6",
|
|
62
|
-
fireworks: "accounts/fireworks/models/kimi-k2p6",
|
|
63
|
-
together: "moonshotai/Kimi-K2.6",
|
|
64
|
-
baseten: "zai-org/GLM-5.2",
|
|
65
|
-
opencode: "kimi-k2.6",
|
|
66
|
-
"opencode-go": "kimi-k2.6",
|
|
67
|
-
"kimi-coding": "kimi-for-coding",
|
|
68
|
-
"cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6",
|
|
69
|
-
"cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6",
|
|
70
|
-
"qwen-token-plan": "qwen3.7-max",
|
|
71
|
-
"qwen-token-plan-cn": "qwen3.7-max",
|
|
72
|
-
"qwen-token-plan-individual": "qwen3.8-max",
|
|
73
|
-
xiaomi: "mimo-v2.5-pro",
|
|
74
|
-
"xiaomi-token-plan-cn": "mimo-v2.5-pro",
|
|
75
|
-
"xiaomi-token-plan-ams": "mimo-v2.5-pro",
|
|
76
|
-
"xiaomi-token-plan-sgp": "mimo-v2.5-pro",
|
|
77
|
-
});
|
|
78
21
|
const DEFAULT_SURFACE = {
|
|
79
22
|
columns: 100,
|
|
80
23
|
rows: 32,
|
|
@@ -83,22 +26,16 @@ const DEFAULT_SURFACE = {
|
|
|
83
26
|
};
|
|
84
27
|
/** Owns the pinned Pi session lifecycle and translates its events into the neutral agent-engine contract. */
|
|
85
28
|
export class PiEngineAdapter {
|
|
86
|
-
#runtimeFactory;
|
|
87
|
-
#checkPackageUpdates;
|
|
88
|
-
#cwd;
|
|
89
29
|
#agentDir;
|
|
90
30
|
#sessionId;
|
|
91
|
-
#sessionPath;
|
|
92
|
-
#sessionSelection;
|
|
93
|
-
#sessionForkPrompt;
|
|
94
31
|
#workflowHost;
|
|
95
|
-
#
|
|
32
|
+
#engine;
|
|
96
33
|
#workflowInteraction;
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
34
|
+
#contexts;
|
|
35
|
+
#workflows;
|
|
36
|
+
#resources;
|
|
37
|
+
#settings;
|
|
38
|
+
#suggestions;
|
|
102
39
|
#viewRevision = 0;
|
|
103
40
|
#lifecycle = "starting";
|
|
104
41
|
#editor = {
|
|
@@ -109,8 +46,6 @@ export class PiEngineAdapter {
|
|
|
109
46
|
historyRevision: 0,
|
|
110
47
|
submitEnabled: false,
|
|
111
48
|
};
|
|
112
|
-
#sessionGeneration = 0;
|
|
113
|
-
#sessionBindingGeneration = 0;
|
|
114
49
|
#status = {
|
|
115
50
|
title: "Pi",
|
|
116
51
|
workingMessage: null,
|
|
@@ -120,62 +55,161 @@ export class PiEngineAdapter {
|
|
|
120
55
|
#terminal = DEFAULT_SURFACE;
|
|
121
56
|
#activeModel = null;
|
|
122
57
|
#thinkingLevel = "medium";
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
#transcriptSnapshot;
|
|
128
|
-
#messageBlockIds = new WeakMap();
|
|
129
|
-
#messageFallbackIds = new Map();
|
|
130
|
-
#toolBlockIds = new Map();
|
|
131
|
-
#transcriptImageAssets = new TranscriptImageAssets();
|
|
58
|
+
#projection = new PiTranscriptProjection({
|
|
59
|
+
retryAttempt: () => this.#engine.session?.retryAttempt ?? 0,
|
|
60
|
+
blockChanged: block => this.#emitEvent({ type: "transcript-block", block }),
|
|
61
|
+
});
|
|
132
62
|
#usageCache;
|
|
133
|
-
#nextBlockSequence = 0;
|
|
134
63
|
#diagnostics = [];
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
64
|
+
// Invariant: every port closes over adapter state read at call time, so delivery is built before a session binds.
|
|
65
|
+
#delivery = new PiEventDelivery({
|
|
66
|
+
sessionId: () => this.#sessionId,
|
|
67
|
+
sessionGeneration: () => this.#engine.generation,
|
|
68
|
+
transcriptBlock: id => this.#projection.block(id),
|
|
69
|
+
retainEvent: event => this.#projection.assets.retainEvent(event),
|
|
70
|
+
isPendingCommand: correlationId => this.#commands.isPending(correlationId),
|
|
71
|
+
cancelForOverload: () => this.#cancelForOverload(),
|
|
72
|
+
reconcileOverload: cancelled => this.#reconcileOverload(cancelled),
|
|
73
|
+
listenerFailed: message => { this.#recordDiagnostic("warning", "event-listener", message, true); },
|
|
74
|
+
});
|
|
140
75
|
#admissionStopped = false;
|
|
141
76
|
#runningCommands = 0;
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
77
|
+
#commands;
|
|
78
|
+
#events = new PiSessionEvents(this.#projection, {
|
|
79
|
+
sessionGeneration: () => this.#engine.generation,
|
|
80
|
+
sessionMessages: () => this.#engine.session?.messages ?? [],
|
|
81
|
+
activeModel: () => this.#activeModel,
|
|
82
|
+
replaceTranscript: blocks => { this.#setTranscript(blocks); },
|
|
83
|
+
emit: value => { this.#emitEvent(value); },
|
|
84
|
+
emitView: () => { this.#emitView(); },
|
|
85
|
+
enterWork: message => {
|
|
86
|
+
const wasBusy = this.#lifecycle === "busy";
|
|
87
|
+
this.#lifecycle = "busy";
|
|
88
|
+
this.#status = { ...this.#status, workingMessage: message, workingProgress: null };
|
|
89
|
+
if (!wasBusy)
|
|
90
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
91
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
92
|
+
},
|
|
93
|
+
leaveWork: () => {
|
|
94
|
+
this.#lifecycle = "ready";
|
|
95
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
96
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
97
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
98
|
+
},
|
|
99
|
+
workProgress: percent => {
|
|
100
|
+
if (this.#status.workingProgress === percent)
|
|
101
|
+
return;
|
|
102
|
+
this.#status = { ...this.#status, workingProgress: percent };
|
|
103
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
104
|
+
},
|
|
105
|
+
queueChanged: queuedSubmissions => {
|
|
106
|
+
this.#editor = { ...this.#editor, queuedSubmissions: [...queuedSubmissions], historyRevision: this.#editor.historyRevision + 1 };
|
|
107
|
+
this.#emitEvent({ type: "editor-state", editor: this.#editor });
|
|
108
|
+
},
|
|
109
|
+
thinkingLevelChanged: level => { this.#thinkingLevel = level; },
|
|
110
|
+
usageInvalidated: () => { this.#usageCache = undefined; },
|
|
111
|
+
compactionStarted: () => { this.#engine.compactionStarted(); },
|
|
112
|
+
compactionEnded: () => { this.#engine.compactionEnded(); },
|
|
113
|
+
deliverQueuedAfterCompaction: () => { this.#engine.deliverQueuedAfterCompaction(); },
|
|
114
|
+
});
|
|
115
|
+
#extensions = new PiExtensionUiBinding({
|
|
116
|
+
session: () => this.#engine.session,
|
|
117
|
+
diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
|
|
118
|
+
emitView: () => { this.#emitView(); },
|
|
119
|
+
});
|
|
156
120
|
#disposed = false;
|
|
157
|
-
#settingsProductMode;
|
|
158
|
-
#projectTrustPrompt;
|
|
159
|
-
#settingsIntegration;
|
|
160
|
-
#settingsIntegrationManager;
|
|
161
|
-
#availableThemes;
|
|
162
121
|
constructor(options = {}) {
|
|
163
|
-
this.#cwd = options.cwd ?? process.cwd();
|
|
164
122
|
this.#agentDir = options.agentDir ?? getAgentDir();
|
|
165
123
|
this.#sessionId = options.sessionId ?? "owned-session-1";
|
|
166
|
-
this.#sessionPath = options.sessionPath;
|
|
167
|
-
this.#sessionSelection = options.sessionSelection;
|
|
168
|
-
this.#sessionForkPrompt = options.sessionForkPrompt;
|
|
169
|
-
this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
|
|
170
|
-
this.#checkPackageUpdates = options.checkPackageUpdates
|
|
171
|
-
?? (options.createRuntime
|
|
172
|
-
? async () => []
|
|
173
|
-
: settingsManager => checkDefaultPiPackageUpdates(this.#cwd, this.#agentDir, settingsManager));
|
|
174
124
|
this.#workflowHost = options.workflowHost ?? defaultWorkflowHost();
|
|
175
|
-
this.#availableThemes = options.availableThemes ?? null;
|
|
176
|
-
this.#settingsProductMode = options.settingsProductMode ?? "bare";
|
|
177
|
-
this.#projectTrustPrompt = options.projectTrustPrompt;
|
|
178
125
|
this.#workflowInteraction = { prompt: async () => null, notify() { } };
|
|
126
|
+
this.#engine = new PiEngineRuntime({
|
|
127
|
+
cwd: options.cwd ?? process.cwd(),
|
|
128
|
+
agentDir: this.#agentDir,
|
|
129
|
+
sessionId: this.#sessionId,
|
|
130
|
+
sessionPath: options.sessionPath,
|
|
131
|
+
sessionSelection: options.sessionSelection,
|
|
132
|
+
sessionForkPrompt: options.sessionForkPrompt,
|
|
133
|
+
projectTrustPrompt: options.projectTrustPrompt,
|
|
134
|
+
createRuntime: options.createRuntime,
|
|
135
|
+
checkPackageUpdates: options.checkPackageUpdates,
|
|
136
|
+
host: this.#workflowHost,
|
|
137
|
+
}, {
|
|
138
|
+
disposed: () => this.#disposed,
|
|
139
|
+
started: runtime => {
|
|
140
|
+
this.#terminal = {
|
|
141
|
+
...this.#terminal,
|
|
142
|
+
hardwareCursor: runtime.services.settingsManager?.getShowHardwareCursor?.() ?? this.#terminal.hardwareCursor,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
rebindBlocked: () => this.#delivery.overloaded || this.#admissionStopped || this.#disposed,
|
|
146
|
+
sessionReplacing: () => { this.#commands.cancelPending(["new-session", "resume-session"]); },
|
|
147
|
+
sessionReplaced: session => { this.#sessionReplaced(session); },
|
|
148
|
+
rebound: () => { this.#emitView(); },
|
|
149
|
+
event: event => { this.#events.handle(event); },
|
|
150
|
+
compactionProgress: percent => { this.#events.compactionProgress(percent); },
|
|
151
|
+
diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
|
|
152
|
+
emitView: () => { this.#emitView(); },
|
|
153
|
+
});
|
|
154
|
+
this.#contexts = new PiWorkflowContexts({ agentDir: this.#agentDir }, {
|
|
155
|
+
cwd: () => this.#engine.cwd,
|
|
156
|
+
session: () => this.#engine.session,
|
|
157
|
+
runtime: () => this.#engine.runtime,
|
|
158
|
+
disposed: () => this.#disposed,
|
|
159
|
+
activeModel: () => this.#activeModel,
|
|
160
|
+
emitView: () => { this.#emitView(); },
|
|
161
|
+
});
|
|
162
|
+
this.#workflows = new PiWorkflowRunner({ agentDir: this.#agentDir, host: this.#workflowHost, contexts: this.#contexts }, {
|
|
163
|
+
session: () => this.#engine.session,
|
|
164
|
+
runtime: () => this.#engine.runtime,
|
|
165
|
+
disposed: () => this.#disposed,
|
|
166
|
+
sessionGeneration: () => this.#engine.generation,
|
|
167
|
+
interaction: () => this.#workflowInteraction,
|
|
168
|
+
admissionStopped: () => this.#delivery.overloaded || this.#admissionStopped || this.#disposed,
|
|
169
|
+
pendingCommandCount: () => this.#commands.pendingCount,
|
|
170
|
+
beginRunning: () => { this.#runningCommands++; },
|
|
171
|
+
endRunning: () => { this.#runningCommands--; },
|
|
172
|
+
activeModel: () => this.#activeModel,
|
|
173
|
+
setActiveModel: model => { this.#activeModel = model; },
|
|
174
|
+
thinkingLevel: () => this.#thinkingLevel,
|
|
175
|
+
setThinkingLevel: level => { this.#thinkingLevel = level; },
|
|
176
|
+
emitView: () => { this.#emitView(); },
|
|
177
|
+
reconcileActiveModelAvailability: () => { this.#reconcileActiveModelAvailability(); },
|
|
178
|
+
bindExtensionUiToSession: () => this.#extensions.rebind(),
|
|
179
|
+
applyPinnedSetting: selection => this.#settings.applyPinnedSetting(selection),
|
|
180
|
+
snapshot: () => this.snapshot(),
|
|
181
|
+
dispose: () => this.dispose(),
|
|
182
|
+
});
|
|
183
|
+
this.#commands = new PiCommandDispatch({
|
|
184
|
+
sessionId: () => this.#sessionId,
|
|
185
|
+
notRunning: () => this.#disposed || !this.#engine.runtime || !this.#engine.session,
|
|
186
|
+
admissionStopped: () => this.#delivery.overloaded || this.#admissionStopped,
|
|
187
|
+
pendingWorkflowCount: () => this.#workflows.pendingCount,
|
|
188
|
+
sessionGeneration: () => this.#engine.generation,
|
|
189
|
+
beginRunning: () => { this.#runningCommands++; },
|
|
190
|
+
endRunning: () => { this.#runningCommands--; },
|
|
191
|
+
perform: command => this.#perform(command),
|
|
192
|
+
emit: value => { this.#emitEvent(value); },
|
|
193
|
+
emitView: () => { this.#emitView(); },
|
|
194
|
+
diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
|
|
195
|
+
});
|
|
196
|
+
this.#resources = new PiResourceCatalog({ contexts: this.#contexts }, {
|
|
197
|
+
session: () => this.#engine.session,
|
|
198
|
+
runtime: () => this.#engine.runtime,
|
|
199
|
+
});
|
|
200
|
+
this.#settings = new PiEngineSettings({ availableThemes: options.availableThemes ?? null, productMode: options.settingsProductMode ?? "bare" }, {
|
|
201
|
+
runtime: () => this.#engine.runtime,
|
|
202
|
+
requireSession: () => this.#requireWorkflowSession(),
|
|
203
|
+
thinkingLevelChanged: level => { this.#thinkingLevel = level; this.#emitView(); },
|
|
204
|
+
emitView: () => { this.#emitView(); },
|
|
205
|
+
});
|
|
206
|
+
this.#suggestions = new PiPromptSuggestions({
|
|
207
|
+
session: () => this.#engine.session,
|
|
208
|
+
runtime: () => this.#engine.runtime,
|
|
209
|
+
activeModel: () => this.#activeModel,
|
|
210
|
+
identity: () => ({ sessionId: this.#sessionId, sessionGeneration: this.#engine.generation, runSequence: this.#events.runSequence, responseSequence: this.#events.responseSequence }),
|
|
211
|
+
unavailable: () => this.#disposed || this.#delivery.overloaded || this.#admissionStopped,
|
|
212
|
+
});
|
|
179
213
|
}
|
|
180
214
|
setWorkflowInteractionHost(interaction) {
|
|
181
215
|
this.#workflowInteraction = interaction;
|
|
@@ -184,855 +218,170 @@ export class PiEngineAdapter {
|
|
|
184
218
|
return this.#sessionId;
|
|
185
219
|
}
|
|
186
220
|
get sessionGeneration() {
|
|
187
|
-
return this.#
|
|
221
|
+
return this.#engine.generation;
|
|
188
222
|
}
|
|
189
223
|
/** Actual session replacements, excluding delivery-only invalidation of callbacks. */
|
|
190
|
-
get sessionBindingGeneration() { return this.#
|
|
224
|
+
get sessionBindingGeneration() { return this.#engine.bindingGeneration; }
|
|
191
225
|
get cwd() {
|
|
192
|
-
return this.#
|
|
226
|
+
return this.#engine.cwd;
|
|
193
227
|
}
|
|
194
228
|
get agentDir() {
|
|
195
229
|
return this.#agentDir;
|
|
196
230
|
}
|
|
197
231
|
resolveTranscriptImage(assetId) {
|
|
198
|
-
return this.#
|
|
232
|
+
return this.#projection.assets.resolve(assetId);
|
|
199
233
|
}
|
|
200
234
|
currentSessionFile() {
|
|
201
|
-
|
|
202
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
235
|
+
return this.#engine.currentSessionFile();
|
|
203
236
|
}
|
|
204
237
|
currentSessionResumeMetadata() {
|
|
205
|
-
|
|
206
|
-
if (manager === undefined
|
|
207
|
-
|| typeof manager.isPersisted !== "function"
|
|
208
|
-
|| typeof manager.getSessionId !== "function"
|
|
209
|
-
|| typeof manager.getSessionDir !== "function"
|
|
210
|
-
|| typeof manager.usesDefaultSessionDir !== "function"
|
|
211
|
-
|| manager.isPersisted() !== true
|
|
212
|
-
|| !existsSync(this.currentSessionFile() ?? ""))
|
|
213
|
-
return null;
|
|
214
|
-
const sessionId = manager.getSessionId();
|
|
215
|
-
const sessionDir = manager.getSessionDir();
|
|
216
|
-
if (!sessionId || !sessionDir)
|
|
217
|
-
return null;
|
|
218
|
-
return { sessionId, sessionDir, usesDefaultSessionDir: manager.usesDefaultSessionDir() };
|
|
238
|
+
return this.#engine.currentSessionResumeMetadata();
|
|
219
239
|
}
|
|
220
240
|
get disposed() {
|
|
221
241
|
return this.#disposed;
|
|
222
242
|
}
|
|
223
243
|
suggestionReasoningPolicy() {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return "ordinary";
|
|
229
|
-
// Performance: the run's own level keeps the provider's thinking parameters, and the cached prefix, identical.
|
|
230
|
-
return readSuggestionReasoning(session.thinkingLevel);
|
|
231
|
-
}
|
|
232
|
-
async generate(request) {
|
|
233
|
-
assertOwnedUiPromptSuggestionRequest(request);
|
|
234
|
-
const identity = request.identity;
|
|
235
|
-
const session = this.#session;
|
|
236
|
-
const runtime = this.#runtime;
|
|
237
|
-
const activeModel = this.#activeModel;
|
|
238
|
-
if (this.#disposed || this.#overload !== undefined || this.#admissionStopped || session === undefined || runtime === undefined || request.signal.aborted
|
|
239
|
-
|| identity.sessionId !== this.#sessionId
|
|
240
|
-
|| identity.sessionGeneration !== this.#sessionGeneration
|
|
241
|
-
|| identity.runSequence !== this.#agentRunSequence
|
|
242
|
-
|| identity.responseSequence !== this.#assistantResponseSequence
|
|
243
|
-
|| activeModel === null
|
|
244
|
-
|| identity.model.providerId !== activeModel.providerId
|
|
245
|
-
|| identity.model.modelId !== activeModel.modelId) {
|
|
246
|
-
return { identity, outcome: request.signal.aborted ? "cancelled" : "unavailable", text: null };
|
|
247
|
-
}
|
|
248
|
-
const model = session.model;
|
|
249
|
-
const agent = session.agent;
|
|
250
|
-
const agentState = agent.state;
|
|
251
|
-
const policy = this.suggestionReasoningPolicy();
|
|
252
|
-
if (model === undefined || policy === "unavailable" || typeof runtime.services.modelRuntime.completeSimple !== "function") {
|
|
253
|
-
return { identity, outcome: "unavailable", text: null };
|
|
254
|
-
}
|
|
255
|
-
// Performance: mirror the primary loop's request shape so the provider serves the conversation prefix
|
|
256
|
-
// from the run's prompt cache. `onResponse` stays out: extensions must not see a suggestion as a response.
|
|
257
|
-
const reasoning = policy === "ordinary" || policy === "off" ? undefined : policy;
|
|
258
|
-
let response;
|
|
259
|
-
try {
|
|
260
|
-
const transformed = typeof agent.transformContext === "function"
|
|
261
|
-
? await agent.transformContext(agentState.messages, request.signal)
|
|
262
|
-
: agentState.messages;
|
|
263
|
-
const messages = typeof agent.convertToLlm === "function"
|
|
264
|
-
? await agent.convertToLlm(transformed)
|
|
265
|
-
: transformed.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
|
|
266
|
-
response = await runtime.services.modelRuntime.completeSimple(model, {
|
|
267
|
-
systemPrompt: agentState.systemPrompt,
|
|
268
|
-
messages: [
|
|
269
|
-
...messages,
|
|
270
|
-
{ role: "user", content: CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, timestamp: Date.now() },
|
|
271
|
-
],
|
|
272
|
-
tools: agentState.tools,
|
|
273
|
-
}, {
|
|
274
|
-
signal: request.signal,
|
|
275
|
-
...(reasoning === undefined ? {} : { reasoning }),
|
|
276
|
-
...(agent.sessionId === undefined ? {} : { sessionId: agent.sessionId }),
|
|
277
|
-
...(agent.thinkingBudgets === undefined ? {} : { thinkingBudgets: agent.thinkingBudgets }),
|
|
278
|
-
...(agent.transport === undefined ? {} : { transport: agent.transport }),
|
|
279
|
-
...(agent.onPayload === undefined ? {} : { onPayload: agent.onPayload }),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return { identity, outcome: request.signal.aborted ? "cancelled" : "provider-failure", text: null };
|
|
284
|
-
}
|
|
285
|
-
if (request.signal.aborted || (isRecord(response) && response.stopReason === "aborted")) {
|
|
286
|
-
return { identity, outcome: "cancelled", text: null };
|
|
287
|
-
}
|
|
288
|
-
if (!isRecord(response) || stringValue(response.errorMessage) !== undefined || response.stopReason === "error") {
|
|
289
|
-
return { identity, outcome: "provider-failure", text: null };
|
|
290
|
-
}
|
|
291
|
-
const content = Array.isArray(response.content) ? response.content : [];
|
|
292
|
-
if (response.stopReason === "length" || content.some(block => isRecord(block) && block.type === "toolCall")) {
|
|
293
|
-
return { identity, outcome: "rejected", text: null };
|
|
294
|
-
}
|
|
295
|
-
// Security: validate all text, not just the first block of a multi-part response.
|
|
296
|
-
const rawText = content.filter(block => isRecord(block) && block.type === "text")
|
|
297
|
-
.map(block => stringValue(block.text) ?? "").join("\n");
|
|
298
|
-
const text = normalizePromptSuggestionCandidate(rawText);
|
|
299
|
-
const result = text === null
|
|
300
|
-
? { identity, outcome: rawText.trim() ? "rejected" : "empty", text: null }
|
|
301
|
-
: { identity, outcome: "candidate", text };
|
|
302
|
-
assertOwnedUiPromptSuggestionResult(result);
|
|
303
|
-
return result;
|
|
244
|
+
return this.#suggestions.reasoningPolicy();
|
|
245
|
+
}
|
|
246
|
+
generate(request) {
|
|
247
|
+
return this.#suggestions.generate(request);
|
|
304
248
|
}
|
|
305
249
|
async start() {
|
|
306
|
-
if (this.#
|
|
250
|
+
if (this.#engine.started)
|
|
307
251
|
return this.view();
|
|
308
|
-
|
|
309
|
-
cwd: this.#cwd,
|
|
310
|
-
agentDir: this.#agentDir,
|
|
311
|
-
sessionId: this.#sessionId,
|
|
312
|
-
...(this.#sessionPath === undefined ? {} : { sessionPath: this.#sessionPath }),
|
|
313
|
-
...(this.#sessionSelection === undefined ? {} : { sessionSelection: this.#sessionSelection }),
|
|
314
|
-
...(this.#sessionForkPrompt === undefined ? {} : { sessionForkPrompt: this.#sessionForkPrompt }),
|
|
315
|
-
...(this.#projectTrustPrompt === undefined ? {} : { projectTrustPrompt: this.#projectTrustPrompt }),
|
|
316
|
-
}).catch(error => {
|
|
252
|
+
await this.#engine.start().catch(error => {
|
|
317
253
|
this.#lifecycle = "failed";
|
|
318
254
|
this.#addDiagnostic("error", "engine-startup", error instanceof Error ? error.message : String(error), false);
|
|
319
255
|
throw error;
|
|
320
256
|
});
|
|
321
|
-
this.#runtime = runtime;
|
|
322
|
-
this.#cwd = runtime.cwd ?? this.#cwd;
|
|
323
|
-
this.#gitBranch = await readGitBranch(this.#cwd);
|
|
324
|
-
this.#terminal = {
|
|
325
|
-
...this.#terminal,
|
|
326
|
-
hardwareCursor: runtime.services.settingsManager?.getShowHardwareCursor?.() ?? this.#terminal.hardwareCursor,
|
|
327
|
-
};
|
|
328
|
-
runtime.setRebindSession(async (session) => {
|
|
329
|
-
if (this.#overload !== undefined || this.#admissionStopped || this.#disposed)
|
|
330
|
-
return;
|
|
331
|
-
this.#bindSession(session);
|
|
332
|
-
this.#emitView();
|
|
333
|
-
});
|
|
334
|
-
for (const diagnostic of [...runtime.diagnostics, ...runtime.services.diagnostics]) {
|
|
335
|
-
this.#addDiagnostic(diagnostic.type === "error" ? "error" : diagnostic.type === "warning" ? "warning" : "info", "engine-startup", diagnostic.message, diagnostic.type !== "error");
|
|
336
|
-
}
|
|
337
|
-
this.#bindSession(runtime.session);
|
|
338
|
-
await this.#announceChangelog(runtime.services.settingsManager);
|
|
339
257
|
this.#lifecycle = "ready";
|
|
340
258
|
this.#editor = { ...this.#editor, submitEnabled: true };
|
|
341
259
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
342
260
|
this.#emitView();
|
|
343
|
-
void this.#announcePackageUpdates(
|
|
261
|
+
void this.#engine.announcePackageUpdates();
|
|
344
262
|
return this.view();
|
|
345
263
|
}
|
|
346
|
-
async #announceChangelog(settingsManager) {
|
|
347
|
-
if (!settingsManager || typeof settingsManager.getLastChangelogVersion !== "function"
|
|
348
|
-
|| typeof settingsManager.setLastChangelogVersion !== "function")
|
|
349
|
-
return;
|
|
350
|
-
if ((this.#session?.messages.length ?? 0) > 0)
|
|
351
|
-
return;
|
|
352
|
-
const lastVersion = settingsManager.getLastChangelogVersion();
|
|
353
|
-
if (lastVersion === VERSION)
|
|
354
|
-
return;
|
|
355
|
-
if (!lastVersion) {
|
|
356
|
-
settingsManager.setLastChangelogVersion(VERSION);
|
|
357
|
-
await settingsManager.flush();
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const markdown = await this.#workflowHost.readChangelog(lastVersion).catch(() => "");
|
|
361
|
-
if (markdown.trim().length > 0) {
|
|
362
|
-
this.#addDiagnostic("info", settingsManager.getCollapseChangelog() ? "changelog-collapsed" : "changelog-expanded", markdown, true);
|
|
363
|
-
settingsManager.setLastChangelogVersion(VERSION);
|
|
364
|
-
await settingsManager.flush();
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
async #announcePackageUpdates(settingsManager) {
|
|
368
|
-
if (process.env.PI_OFFLINE)
|
|
369
|
-
return;
|
|
370
|
-
let updates;
|
|
371
|
-
try {
|
|
372
|
-
updates = await this.#checkPackageUpdates(settingsManager);
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (this.#disposed || updates.length === 0)
|
|
378
|
-
return;
|
|
379
|
-
const packages = updates.map(name => `- ${name}`).join("\n");
|
|
380
|
-
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
|
|
381
|
-
this.#emitView();
|
|
382
|
-
}
|
|
383
264
|
onEvent(listener) {
|
|
384
|
-
|
|
385
|
-
this.#listeners.set(listener, initial.sequence);
|
|
386
|
-
listener(initial);
|
|
387
|
-
return () => this.#listeners.delete(listener);
|
|
265
|
+
return this.#delivery.subscribe(listener, this.#delivery.stamp({ type: "session-view", view: this.view() }));
|
|
388
266
|
}
|
|
389
267
|
/** Developer-only pressure evidence; never mirrored into visible diagnostic/status arrays. */
|
|
390
268
|
deliveryDiagnostics() {
|
|
391
|
-
return { ...this.#
|
|
392
|
-
pendingCommands: this.#pendingCommands.size + this.#pendingWorkflows.size, reservedOutcomes: this.#reservedOutcomes.size, invalidatedEvents: this.#invalidatedEvents };
|
|
269
|
+
return { ...this.#delivery.diagnostics(), pendingCommands: this.#commands.pendingCount + this.#workflows.pendingCount };
|
|
393
270
|
}
|
|
394
271
|
async flushEvents() {
|
|
395
|
-
const failed = this.#
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.#deliveryFailed = false;
|
|
272
|
+
const failed = this.#delivery.failed;
|
|
273
|
+
await this.#delivery.settle();
|
|
274
|
+
if (failed || this.#delivery.failed || this.#disposed && this.#lifecycle !== "stopped") {
|
|
275
|
+
this.#delivery.clearFailure();
|
|
400
276
|
throw new EngineDeliveryError();
|
|
401
277
|
}
|
|
402
278
|
}
|
|
403
279
|
nonVisualResources() {
|
|
404
|
-
|
|
405
|
-
if (!loader)
|
|
406
|
-
return [];
|
|
407
|
-
const resources = [];
|
|
408
|
-
const skills = collectionResult(loader.getSkills(), "skills");
|
|
409
|
-
for (const [index, skill] of skills.values.entries()) {
|
|
410
|
-
resources.push({
|
|
411
|
-
kind: "skill",
|
|
412
|
-
id: `skill-${index}-${stringProperty(skill, "name") ?? "unknown"}`,
|
|
413
|
-
label: stringProperty(skill, "name") ?? "Unnamed skill",
|
|
414
|
-
sourcePath: stringProperty(skill, "filePath") ?? stringProperty(skill, "path") ?? stringProperty(skill, "location") ?? null,
|
|
415
|
-
diagnostic: null,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
resources.push(...skills.diagnostics.map((diagnostic, index) => ({
|
|
419
|
-
kind: "skill",
|
|
420
|
-
id: `skill-diagnostic-${index}`,
|
|
421
|
-
label: "Skill diagnostic",
|
|
422
|
-
sourcePath: null,
|
|
423
|
-
diagnostic,
|
|
424
|
-
})));
|
|
425
|
-
const prompts = collectionResult(loader.getPrompts(), "prompts");
|
|
426
|
-
for (const [index, prompt] of prompts.values.entries()) {
|
|
427
|
-
resources.push({
|
|
428
|
-
kind: "prompt-template",
|
|
429
|
-
id: `prompt-${index}-${stringProperty(prompt, "name") ?? "unknown"}`,
|
|
430
|
-
label: stringProperty(prompt, "name") ?? "Prompt template",
|
|
431
|
-
sourcePath: stringProperty(prompt, "filePath") ?? stringProperty(prompt, "path") ?? null,
|
|
432
|
-
diagnostic: null,
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
resources.push(...prompts.diagnostics.map((diagnostic, index) => ({
|
|
436
|
-
kind: "prompt-template",
|
|
437
|
-
id: `prompt-diagnostic-${index}`,
|
|
438
|
-
label: "Prompt diagnostic",
|
|
439
|
-
sourcePath: null,
|
|
440
|
-
diagnostic,
|
|
441
|
-
})));
|
|
442
|
-
const agentsFilesResult = loader.getAgentsFiles();
|
|
443
|
-
const agentsFiles = unknownArray(isRecord(agentsFilesResult) ? agentsFilesResult.agentsFiles : undefined);
|
|
444
|
-
for (const [index, file] of agentsFiles.entries()) {
|
|
445
|
-
resources.push({
|
|
446
|
-
kind: "agent-context",
|
|
447
|
-
id: `agent-context-${index}`,
|
|
448
|
-
label: "Agent context",
|
|
449
|
-
sourcePath: stringProperty(file, "path") ?? null,
|
|
450
|
-
diagnostic: null,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
const systemPrompt = loader.getSystemPromptSource();
|
|
454
|
-
if (isRecord(systemPrompt) && typeof systemPrompt.path === "string") {
|
|
455
|
-
resources.push({
|
|
456
|
-
kind: "system-prompt",
|
|
457
|
-
id: "system-prompt",
|
|
458
|
-
label: "System prompt",
|
|
459
|
-
sourcePath: systemPrompt.path,
|
|
460
|
-
diagnostic: null,
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
for (const [index, source] of unknownArray(loader.getAppendSystemPromptSources()).entries()) {
|
|
464
|
-
resources.push({
|
|
465
|
-
kind: "system-prompt",
|
|
466
|
-
id: `append-system-prompt-${index}`,
|
|
467
|
-
label: "Append system prompt",
|
|
468
|
-
sourcePath: stringProperty(source, "path") ?? null,
|
|
469
|
-
diagnostic: null,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
if (loader.getThemes !== undefined) {
|
|
473
|
-
const themes = collectionResult(loader.getThemes(), "themes");
|
|
474
|
-
for (const [index, theme] of themes.values.entries()) {
|
|
475
|
-
const sourcePath = stringProperty(theme, "sourcePath");
|
|
476
|
-
if (!sourcePath)
|
|
477
|
-
continue;
|
|
478
|
-
resources.push({
|
|
479
|
-
kind: "theme",
|
|
480
|
-
id: `theme-${index}-${stringProperty(theme, "name") ?? "unknown"}`,
|
|
481
|
-
label: stringProperty(theme, "name") ?? compactResourceLabel(sourcePath),
|
|
482
|
-
sourcePath,
|
|
483
|
-
diagnostic: null,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
resources.push(...themes.diagnostics.map((diagnostic, index) => ({
|
|
487
|
-
kind: "theme",
|
|
488
|
-
id: `theme-diagnostic-${index}`,
|
|
489
|
-
label: "Theme diagnostic",
|
|
490
|
-
sourcePath: null,
|
|
491
|
-
diagnostic,
|
|
492
|
-
})));
|
|
493
|
-
}
|
|
494
|
-
return resources;
|
|
280
|
+
return this.#resources.nonVisualResources();
|
|
495
281
|
}
|
|
496
282
|
extensionResources() {
|
|
497
|
-
|
|
498
|
-
if (loader?.getExtensions === undefined)
|
|
499
|
-
return [];
|
|
500
|
-
let result;
|
|
501
|
-
try {
|
|
502
|
-
result = loader.getExtensions();
|
|
503
|
-
}
|
|
504
|
-
catch (error) {
|
|
505
|
-
return [extensionResourceDiagnostic(0, null, `Extension discovery failed: ${error instanceof Error ? error.message : String(error)}`)];
|
|
506
|
-
}
|
|
507
|
-
if (!isRecord(result))
|
|
508
|
-
return [extensionResourceDiagnostic(0, null, "Extension discovery returned a malformed result")];
|
|
509
|
-
const resources = [];
|
|
510
|
-
if (!Array.isArray(result.extensions)) {
|
|
511
|
-
resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned a malformed extensions collection"));
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
for (const extension of result.extensions) {
|
|
515
|
-
if (!isRecord(extension) || typeof extension.path !== "string" || extension.path.length === 0
|
|
516
|
-
|| typeof extension.resolvedPath !== "string" || extension.resolvedPath.length === 0
|
|
517
|
-
|| (extension.hidden !== undefined && typeof extension.hidden !== "boolean")) {
|
|
518
|
-
resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned malformed extension metadata"));
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
resources.push({
|
|
522
|
-
kind: "extension",
|
|
523
|
-
id: `extension-${resources.length}`,
|
|
524
|
-
sourcePath: extension.path,
|
|
525
|
-
resolvedPath: extension.resolvedPath,
|
|
526
|
-
sourceInfo: extensionSourceSummary(extension.sourceInfo),
|
|
527
|
-
loaded: true,
|
|
528
|
-
hidden: extension.hidden === true,
|
|
529
|
-
diagnostic: null,
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
if (!Array.isArray(result.errors)) {
|
|
534
|
-
resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned a malformed errors collection"));
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
for (const error of result.errors) {
|
|
538
|
-
if (!isRecord(error) || typeof error.path !== "string" || typeof error.error !== "string") {
|
|
539
|
-
resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned malformed error metadata"));
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
resources.push(extensionResourceDiagnostic(resources.length, error.path, error.error));
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return resources;
|
|
546
|
-
}
|
|
547
|
-
visualExtensionSupport() {
|
|
548
|
-
return {
|
|
549
|
-
available: this.#extensionBound,
|
|
550
|
-
contractComplete: true,
|
|
551
|
-
contractVersion: OWNED_UI_EXTENSION_CONTRACT_VERSION,
|
|
552
|
-
binding: this.#extensionBound ? "bound" : "unbound",
|
|
553
|
-
uiCallbacks: OWNED_UI_EXTENSION_UI_CALLBACKS,
|
|
554
|
-
uiProperties: OWNED_UI_EXTENSION_UI_PROPERTIES,
|
|
555
|
-
renderCallbacks: OWNED_UI_EXTENSION_RENDER_CALLBACKS,
|
|
556
|
-
diagnostic: this.#extensionBound
|
|
557
|
-
? `Pinned public extension UI lifecycle is bound through the ${PRODUCT_IDENTITY.displayName}-owned bridge.`
|
|
558
|
-
: `The complete ${PRODUCT_IDENTITY.displayName}-owned extension UI contract is available; the active session has not been bound to the owned UI bridge.`,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
async bindExtensionUi(ui, shutdown) {
|
|
562
|
-
assertPiExtensionUiContext(ui);
|
|
563
|
-
this.#extensionUi = ui;
|
|
564
|
-
this.#extensionShutdown = shutdown;
|
|
565
|
-
await this.#bindExtensionUiToSession();
|
|
566
|
-
}
|
|
567
|
-
async unbindExtensionUi() {
|
|
568
|
-
this.#extensionUi = undefined;
|
|
569
|
-
this.#extensionShutdown = undefined;
|
|
570
|
-
this.#extensionBound = false;
|
|
283
|
+
return this.#resources.extensionResources();
|
|
571
284
|
}
|
|
572
285
|
workflowAutocompleteCommands() {
|
|
573
|
-
|
|
574
|
-
{
|
|
575
|
-
name: "model",
|
|
576
|
-
description: "Select model (opens selector UI)",
|
|
577
|
-
argumentHint: "<provider/model>",
|
|
578
|
-
argumentOptions: this.#modelOptions(),
|
|
579
|
-
source: "builtin",
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
name: "login",
|
|
583
|
-
description: "Configure provider authentication",
|
|
584
|
-
argumentHint: "<provider>",
|
|
585
|
-
argumentOptions: this.#loginOptions().map(option => ({ ...option, id: option.id.split(":").at(-1) ?? option.id })),
|
|
586
|
-
source: "builtin",
|
|
587
|
-
},
|
|
588
|
-
];
|
|
589
|
-
const usedNames = new Set(PINNED_PI_WORKFLOW_COMMAND_NAMES);
|
|
590
|
-
const loader = this.#runtime?.services.resourceLoader;
|
|
591
|
-
if (!loader)
|
|
592
|
-
return commands;
|
|
593
|
-
const prompts = collectionResult(loader.getPrompts(), "prompts");
|
|
594
|
-
for (const prompt of prompts.values) {
|
|
595
|
-
const name = stringProperty(prompt, "name");
|
|
596
|
-
if (!name || usedNames.has(name))
|
|
597
|
-
continue;
|
|
598
|
-
const argumentHint = stringProperty(prompt, "argumentHint");
|
|
599
|
-
commands.push({
|
|
600
|
-
name,
|
|
601
|
-
description: stringProperty(prompt, "description") ?? "Prompt template",
|
|
602
|
-
...(argumentHint === undefined ? {} : { argumentHint }),
|
|
603
|
-
source: "prompt",
|
|
604
|
-
});
|
|
605
|
-
usedNames.add(name);
|
|
606
|
-
}
|
|
607
|
-
const settings = this.#runtime?.services.settingsManager;
|
|
608
|
-
const skillsEnabled = settings?.getEnableSkillCommands?.() !== false;
|
|
609
|
-
if (skillsEnabled) {
|
|
610
|
-
const skills = collectionResult(loader.getSkills(), "skills");
|
|
611
|
-
for (const skill of skills.values) {
|
|
612
|
-
const resourceName = stringProperty(skill, "name");
|
|
613
|
-
const name = resourceName ? `skill:${resourceName}` : undefined;
|
|
614
|
-
if (!name || usedNames.has(name))
|
|
615
|
-
continue;
|
|
616
|
-
commands.push({ name, description: stringProperty(skill, "description") ?? "Skill", source: "skill" });
|
|
617
|
-
usedNames.add(name);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
|
|
621
|
-
if (Array.isArray(extensionCommands)) {
|
|
622
|
-
const registered = extensionCommands.filter(isRecord);
|
|
623
|
-
for (const command of registered) {
|
|
624
|
-
const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
|
|
625
|
-
if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
|
|
626
|
-
continue;
|
|
627
|
-
commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
|
|
628
|
-
usedNames.add(name);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return commands;
|
|
632
|
-
}
|
|
633
|
-
async cycleModelWorkflow(direction) {
|
|
634
|
-
try {
|
|
635
|
-
const session = this.#requireWorkflowSession();
|
|
636
|
-
const result = await requireCapability(session.cycleModel, "cycleModel").call(session, direction);
|
|
637
|
-
if (!isRecord(result) || !isRecord(result.model)) {
|
|
638
|
-
const scoped = Array.isArray(session.scopedModels) && session.scopedModels.length > 0;
|
|
639
|
-
return workflowResult("model", "completed", scoped ? "Only one model in scope" : "Only one model available", undefined, "status");
|
|
640
|
-
}
|
|
641
|
-
const model = readModel(result.model);
|
|
642
|
-
if (model)
|
|
643
|
-
this.#activeModel = model;
|
|
644
|
-
if (result.thinkingLevel !== undefined)
|
|
645
|
-
this.#thinkingLevel = readThinkingLevel(result.thinkingLevel);
|
|
646
|
-
this.#emitView();
|
|
647
|
-
const modelName = stringProperty(result.model, "name") ?? stringProperty(result.model, "id") ?? "model";
|
|
648
|
-
const reasoning = result.model.reasoning === true;
|
|
649
|
-
const thinking = this.#thinkingLevel;
|
|
650
|
-
const suffix = reasoning && thinking !== "off" ? ` (thinking: ${thinking})` : "";
|
|
651
|
-
return workflowResult("model", "completed", `Switched to ${modelName}${suffix}`, undefined, "status");
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
return workflowResult("model", "failed", error instanceof Error ? error.message : String(error));
|
|
655
|
-
}
|
|
286
|
+
return this.#resources.workflowAutocompleteCommands();
|
|
656
287
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
* theme's name, or a `light/dark` pair meaning "follow the terminal".
|
|
660
|
-
*/
|
|
661
|
-
configuredTheme() {
|
|
662
|
-
return this.#runtime?.services.settingsManager.getThemeSetting();
|
|
288
|
+
visualExtensionSupport() {
|
|
289
|
+
return this.#extensions.support();
|
|
663
290
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const settings = this.#runtime?.services.settingsManager;
|
|
667
|
-
if (!settings || typeof settings.getCompactionEnabled !== "function")
|
|
668
|
-
return null;
|
|
669
|
-
if (this.#settingsIntegration === undefined || this.#settingsIntegrationManager !== settings) {
|
|
670
|
-
this.#settingsIntegrationManager = settings;
|
|
671
|
-
configureOwnedHttpDispatcher(settings.getHttpIdleTimeoutMs());
|
|
672
|
-
this.#settingsIntegration = new PiSettingsIntegration(settings, {
|
|
673
|
-
...(this.#availableThemes === null ? {} : { themes: this.#availableThemes }),
|
|
674
|
-
thinkingLevels: () => {
|
|
675
|
-
const levels = this.#runtime?.session?.getAvailableThinkingLevels?.();
|
|
676
|
-
return Array.isArray(levels) ? levels.map(level => String(level)) : [];
|
|
677
|
-
},
|
|
678
|
-
productMode: this.#settingsProductMode,
|
|
679
|
-
});
|
|
680
|
-
this.#settingsIntegration.bindOwner("shell", {
|
|
681
|
-
enableSkillCommands: { apply: value => {
|
|
682
|
-
if (typeof value !== "boolean")
|
|
683
|
-
throw new TypeError("Skill commands value is invalid");
|
|
684
|
-
settings.setEnableSkillCommands(value);
|
|
685
|
-
} },
|
|
686
|
-
doubleEscapeAction: { apply: value => {
|
|
687
|
-
if (value !== "tree" && value !== "fork" && value !== "none")
|
|
688
|
-
throw new TypeError("Double-Escape action is invalid");
|
|
689
|
-
settings.setDoubleEscapeAction(value);
|
|
690
|
-
} },
|
|
691
|
-
treeFilterMode: { apply: value => {
|
|
692
|
-
if (value !== "default" && value !== "no-tools" && value !== "user-only" && value !== "labeled-only" && value !== "all")
|
|
693
|
-
throw new TypeError("Tree filter mode is invalid");
|
|
694
|
-
settings.setTreeFilterMode(value);
|
|
695
|
-
} },
|
|
696
|
-
showCacheMissNotices: { apply: value => {
|
|
697
|
-
if (typeof value !== "boolean")
|
|
698
|
-
throw new TypeError("Cache-miss notice setting is invalid");
|
|
699
|
-
settings.setShowCacheMissNotices(value);
|
|
700
|
-
} },
|
|
701
|
-
});
|
|
702
|
-
this.#settingsIntegration.bindOwner("startup", {
|
|
703
|
-
// Invariant: deferred application is the owner operation: the next preflight reads
|
|
704
|
-
// the persisted default before constructing project-backed services.
|
|
705
|
-
defaultProjectTrust: { apply() { } },
|
|
706
|
-
collapseChangelog: { apply() { } },
|
|
707
|
-
});
|
|
708
|
-
this.#settingsIntegration.bindOwner("agent", {
|
|
709
|
-
autoCompact: { apply: value => {
|
|
710
|
-
if (typeof value !== "boolean")
|
|
711
|
-
throw new TypeError("Auto compact value is invalid");
|
|
712
|
-
this.#requireWorkflowSession().setAutoCompactionEnabled(value);
|
|
713
|
-
} },
|
|
714
|
-
autoResizeImages: { apply: value => {
|
|
715
|
-
if (typeof value !== "boolean")
|
|
716
|
-
throw new TypeError("Auto-resize images value is invalid");
|
|
717
|
-
settings.setImageAutoResize(value);
|
|
718
|
-
} },
|
|
719
|
-
blockImages: { apply: value => {
|
|
720
|
-
if (typeof value !== "boolean")
|
|
721
|
-
throw new TypeError("Block images value is invalid");
|
|
722
|
-
settings.setBlockImages(value);
|
|
723
|
-
} },
|
|
724
|
-
steeringMode: { apply: value => {
|
|
725
|
-
if (value !== "all" && value !== "one-at-a-time")
|
|
726
|
-
throw new TypeError("Steering mode is invalid");
|
|
727
|
-
this.#requireWorkflowSession().setSteeringMode(value);
|
|
728
|
-
} },
|
|
729
|
-
followUpMode: { apply: value => {
|
|
730
|
-
if (value !== "all" && value !== "one-at-a-time")
|
|
731
|
-
throw new TypeError("Follow-up mode is invalid");
|
|
732
|
-
this.#requireWorkflowSession().setFollowUpMode(value);
|
|
733
|
-
} },
|
|
734
|
-
transport: { apply: value => {
|
|
735
|
-
if (value !== "sse" && value !== "websocket" && value !== "websocket-cached" && value !== "auto")
|
|
736
|
-
throw new TypeError("Transport is invalid");
|
|
737
|
-
this.#requireWorkflowSession().agent.transport = value;
|
|
738
|
-
} },
|
|
739
|
-
httpIdleTimeoutMs: { apply: value => {
|
|
740
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
741
|
-
throw new TypeError("HTTP idle timeout is invalid");
|
|
742
|
-
// Invariant: provider streaming reads the manager per request; global fetch uses
|
|
743
|
-
// the matching owned dispatcher and zero maps to disabled semantics.
|
|
744
|
-
configureOwnedHttpDispatcher(value);
|
|
745
|
-
settings.setHttpIdleTimeoutMs(value);
|
|
746
|
-
} },
|
|
747
|
-
thinkingLevel: { apply: value => {
|
|
748
|
-
if (!isThinkingLevel(value))
|
|
749
|
-
throw new TypeError("Thinking level is invalid");
|
|
750
|
-
this.#requireWorkflowSession().setThinkingLevel(value);
|
|
751
|
-
this.#thinkingLevel = readThinkingLevel(this.#requireWorkflowSession().thinkingLevel);
|
|
752
|
-
this.#emitView();
|
|
753
|
-
} },
|
|
754
|
-
warnings: { apply: value => {
|
|
755
|
-
if (!isRecord(value) || Object.values(value).some(flag => typeof flag !== "boolean"))
|
|
756
|
-
throw new TypeError("Warnings setting is invalid");
|
|
757
|
-
settings.setWarnings(value);
|
|
758
|
-
} },
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
return this.#settingsIntegration;
|
|
291
|
+
bindExtensionUi(ui, shutdown) {
|
|
292
|
+
return this.#extensions.bind(ui, shutdown);
|
|
762
293
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
return this.#settingsProductMode;
|
|
294
|
+
unbindExtensionUi() {
|
|
295
|
+
return this.#extensions.unbind();
|
|
766
296
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (settings === null)
|
|
770
|
-
return () => { };
|
|
771
|
-
return settings.bindOwner(owner, handlers);
|
|
297
|
+
async cycleModelWorkflow(direction) {
|
|
298
|
+
return this.#workflows.cycleModelWorkflow(direction);
|
|
772
299
|
}
|
|
773
300
|
pinnedModelSelectorContext() {
|
|
774
|
-
|
|
775
|
-
const runtime = this.#runtime;
|
|
776
|
-
if (!runtime)
|
|
777
|
-
throw new Error("engine runtime is unavailable");
|
|
778
|
-
const scoped = session.scopedModels;
|
|
779
|
-
const modelRuntime = runtime.services.modelRuntime;
|
|
780
|
-
const selectorRuntime = typeof modelRuntime.getAvailableSnapshot === "function"
|
|
781
|
-
? modelRuntime
|
|
782
|
-
: {
|
|
783
|
-
getAvailableSnapshot: () => session.model === undefined ? [] : [session.model],
|
|
784
|
-
getModel: (providerId, modelId) => modelRuntime.getModel(providerId, modelId),
|
|
785
|
-
getError: () => undefined,
|
|
786
|
-
refresh: async () => undefined,
|
|
787
|
-
};
|
|
788
|
-
const settingsManager = runtime.services.settingsManager;
|
|
789
|
-
const selectorSettings = typeof settingsManager?.setDefaultModelAndProvider === "function"
|
|
790
|
-
? settingsManager
|
|
791
|
-
: { setDefaultModelAndProvider() { } };
|
|
792
|
-
return {
|
|
793
|
-
currentModel: this.#activeModel === null ? undefined : session.model,
|
|
794
|
-
settingsManager: selectorSettings,
|
|
795
|
-
modelRuntime: selectorRuntime,
|
|
796
|
-
scopedModels: Array.isArray(scoped) ? scoped : [],
|
|
797
|
-
};
|
|
301
|
+
return this.#contexts.pinnedModelSelectorContext();
|
|
798
302
|
}
|
|
799
303
|
pinnedProjectTrustContext() {
|
|
800
|
-
|
|
801
|
-
if (!runtime)
|
|
802
|
-
throw new Error("engine runtime is unavailable");
|
|
803
|
-
const cwd = resolve(this.#cwd);
|
|
804
|
-
let trustPath = cwd;
|
|
805
|
-
try {
|
|
806
|
-
trustPath = realpathSync(cwd);
|
|
807
|
-
}
|
|
808
|
-
catch {
|
|
809
|
-
// Compatibility: pinned Pi retains the resolved path when real-path lookup fails.
|
|
810
|
-
}
|
|
811
|
-
// Security: trust the target's parent, not the parent containing a directory alias.
|
|
812
|
-
const parent = dirname(trustPath);
|
|
813
|
-
const trustStore = new ProjectTrustStore(this.#agentDir);
|
|
814
|
-
const trustOptions = [
|
|
815
|
-
{ label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },
|
|
816
|
-
...(parent === trustPath ? [] : [{
|
|
817
|
-
label: `Trust parent folder (${parent})`,
|
|
818
|
-
trusted: true,
|
|
819
|
-
updates: [{ path: parent, decision: true }, { path: trustPath, decision: null }],
|
|
820
|
-
savedPath: parent,
|
|
821
|
-
}]),
|
|
822
|
-
{ label: "Do not trust", trusted: false, updates: [{ path: trustPath, decision: false }], savedPath: trustPath },
|
|
823
|
-
];
|
|
824
|
-
return {
|
|
825
|
-
cwd,
|
|
826
|
-
savedDecision: trustStore.getEntry(cwd),
|
|
827
|
-
projectTrusted: runtime.services.settingsManager?.isProjectTrusted?.() === true,
|
|
828
|
-
trustOptions,
|
|
829
|
-
};
|
|
304
|
+
return this.#contexts.pinnedProjectTrustContext();
|
|
830
305
|
}
|
|
831
306
|
persistProjectTrust(updates) {
|
|
832
|
-
|
|
307
|
+
this.#contexts.persistProjectTrust(updates);
|
|
833
308
|
}
|
|
834
309
|
pinnedSessionSelectorContext() {
|
|
835
|
-
|
|
836
|
-
const manager = session.sessionManager;
|
|
837
|
-
const cwd = manager?.getCwd?.();
|
|
838
|
-
const sessionDir = manager?.getSessionDir?.();
|
|
839
|
-
const currentSessionFilePath = manager?.getSessionFile?.();
|
|
840
|
-
const usesDefaultSessionDir = manager?.usesDefaultSessionDir?.() === true;
|
|
841
|
-
const resolvedCwd = typeof cwd === "string" ? cwd : this.#cwd;
|
|
842
|
-
const resolvedSessionDir = typeof sessionDir === "string" ? sessionDir : undefined;
|
|
843
|
-
return {
|
|
844
|
-
currentSessionFilePath: typeof currentSessionFilePath === "string" ? currentSessionFilePath : undefined,
|
|
845
|
-
loadCurrentSessions: onProgress => SessionManager.list(resolvedCwd, resolvedSessionDir, onProgress),
|
|
846
|
-
loadAllSessions: onProgress => usesDefaultSessionDir
|
|
847
|
-
? SessionManager.listAll(onProgress)
|
|
848
|
-
: resolvedSessionDir === undefined ? SessionManager.listAll(onProgress) : SessionManager.listAll(resolvedSessionDir, onProgress),
|
|
849
|
-
renameSession: async (sessionFilePath, nextName) => {
|
|
850
|
-
const next = (nextName ?? "").trim();
|
|
851
|
-
if (!next)
|
|
852
|
-
return;
|
|
853
|
-
SessionManager.open(sessionFilePath).appendSessionInfo(next);
|
|
854
|
-
},
|
|
855
|
-
};
|
|
310
|
+
return this.#contexts.pinnedSessionSelectorContext();
|
|
856
311
|
}
|
|
857
312
|
pinnedScopedModelsContext() {
|
|
858
|
-
|
|
859
|
-
const runtime = this.#runtime;
|
|
860
|
-
if (!runtime)
|
|
861
|
-
throw new Error("engine runtime is unavailable");
|
|
862
|
-
const models = scopedModelRecords(runtime.services.modelRuntime);
|
|
863
|
-
const scoped = session.scopedModels;
|
|
864
|
-
if (Array.isArray(scoped) && scoped.length > 0) {
|
|
865
|
-
return {
|
|
866
|
-
models: models.map(item => item.descriptor),
|
|
867
|
-
enabledModelIds: scoped.map(scopedModelReference).filter((id) => id !== undefined),
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
const patterns = runtime.services.settingsManager?.getEnabledModels?.();
|
|
871
|
-
return {
|
|
872
|
-
models: models.map(item => item.descriptor),
|
|
873
|
-
enabledModelIds: Array.isArray(patterns)
|
|
874
|
-
? resolveConfiguredModelIds(patterns.filter((value) => typeof value === "string"), models)
|
|
875
|
-
: null,
|
|
876
|
-
};
|
|
313
|
+
return this.#contexts.pinnedScopedModelsContext();
|
|
877
314
|
}
|
|
878
315
|
updateScopedModels(enabledModelIds) {
|
|
879
|
-
|
|
880
|
-
const runtime = this.#runtime;
|
|
881
|
-
if (!runtime)
|
|
882
|
-
throw new Error("engine runtime is unavailable");
|
|
883
|
-
const models = scopedModelRecords(runtime.services.modelRuntime);
|
|
884
|
-
const availableIds = new Set(models.map(item => `${item.descriptor.provider}/${item.descriptor.id}`));
|
|
885
|
-
const selected = enabledModelIds?.filter(id => availableIds.has(id)) ?? [];
|
|
886
|
-
const allAvailableEnabled = enabledModelIds !== null && availableIds.size > 0 && selected.length === availableIds.size;
|
|
887
|
-
const scoped = enabledModelIds !== null && selected.length > 0 && !allAvailableEnabled
|
|
888
|
-
? selected.flatMap(id => {
|
|
889
|
-
const item = models.find(candidate => `${candidate.descriptor.provider}/${candidate.descriptor.id}` === id);
|
|
890
|
-
return item === undefined ? [] : [{ model: item.model }];
|
|
891
|
-
})
|
|
892
|
-
: [];
|
|
893
|
-
requireCapability(session.setScopedModels, "setScopedModels").call(session, scoped);
|
|
894
|
-
this.#emitView();
|
|
316
|
+
this.#contexts.updateScopedModels(enabledModelIds);
|
|
895
317
|
}
|
|
896
318
|
persistScopedModels(enabledModelIds) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const patterns = enabledModelIds === null || enabledModelIds.length === availableCount
|
|
902
|
-
? undefined
|
|
903
|
-
: [...enabledModelIds];
|
|
904
|
-
requireCapability(runtime.services.settingsManager?.setEnabledModels, "setEnabledModels").call(runtime.services.settingsManager, patterns === undefined ? undefined : [...patterns]);
|
|
905
|
-
}
|
|
906
|
-
async refreshScopedModels(signal) {
|
|
907
|
-
const runtime = this.#runtime;
|
|
908
|
-
if (!runtime)
|
|
909
|
-
throw new Error("engine runtime is unavailable");
|
|
910
|
-
const result = await runtime.services.modelRuntime.refresh?.({ signal });
|
|
911
|
-
const context = this.pinnedScopedModelsContext();
|
|
912
|
-
if (isRecord(result) && result.aborted === true) {
|
|
913
|
-
return { ...context, status: "Model refresh timed out; showing cached models.", statusKind: "warning" };
|
|
914
|
-
}
|
|
915
|
-
const errors = isRecord(result) ? result.errors : undefined;
|
|
916
|
-
if (errors instanceof Map && errors.size > 0) {
|
|
917
|
-
return {
|
|
918
|
-
...context,
|
|
919
|
-
status: `Could not refresh ${[...errors.keys()].join(", ")}; showing cached models.`,
|
|
920
|
-
statusKind: "warning",
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
return { ...context, status: "Model catalogs refreshed.", statusKind: "success" };
|
|
319
|
+
this.#contexts.persistScopedModels(enabledModelIds);
|
|
320
|
+
}
|
|
321
|
+
refreshScopedModels(signal) {
|
|
322
|
+
return this.#contexts.refreshScopedModels(signal);
|
|
924
323
|
}
|
|
925
324
|
pinnedLoginOptions(authType) {
|
|
926
|
-
return this.#loginOptions(authType);
|
|
325
|
+
return this.#contexts.loginOptions(authType);
|
|
927
326
|
}
|
|
928
327
|
pinnedLoginMethodOptions(providerReference) {
|
|
929
|
-
|
|
930
|
-
if (!runtime)
|
|
931
|
-
throw new Error("engine runtime is unavailable");
|
|
932
|
-
const modelRuntime = runtime.services.modelRuntime;
|
|
933
|
-
const normalized = providerReference.trim().toLowerCase();
|
|
934
|
-
const providers = modelRuntime.getProviders?.();
|
|
935
|
-
const matches = Array.isArray(providers) ? providers.filter(isRecord).filter(candidate => stringProperty(candidate, "id")?.toLowerCase() === normalized
|
|
936
|
-
|| stringProperty(candidate, "name")?.toLowerCase() === normalized) : [];
|
|
937
|
-
const providerId = matches.length === 1 ? stringProperty(matches[0], "id") ?? providerReference : providerReference;
|
|
938
|
-
const provider = modelRuntime.getProvider?.(providerId);
|
|
939
|
-
const providerName = stringProperty(provider, "name") ?? providerId;
|
|
940
|
-
const oauth = dynamicObject(dynamicObject(provider, "auth"), "oauth");
|
|
941
|
-
const loginLabel = stringProperty(oauth, "loginLabel") ?? "Sign in with an account";
|
|
942
|
-
const options = this.#loginOptions().filter(option => option.id.endsWith(`:${providerId}`)).map(option => ({
|
|
943
|
-
id: option.id,
|
|
944
|
-
label: option.id.startsWith("api_key:") ? "Sign in with an API key" : loginLabel,
|
|
945
|
-
...(option.description === undefined ? {} : { description: option.description }),
|
|
946
|
-
}));
|
|
947
|
-
return { title: `Select authentication method for ${providerName}:`, options };
|
|
328
|
+
return this.#contexts.pinnedLoginMethodOptions(providerReference);
|
|
948
329
|
}
|
|
949
330
|
pinnedAmbientAuthentication(selection) {
|
|
950
|
-
|
|
951
|
-
return null;
|
|
952
|
-
const providerId = selection.slice("api_key:".length);
|
|
953
|
-
const provider = this.#runtime?.services.modelRuntime.getProvider?.(providerId);
|
|
954
|
-
const method = dynamicObject(dynamicObject(provider, "auth"), "apiKey");
|
|
955
|
-
if (!method || typeof method.login === "function")
|
|
956
|
-
return null;
|
|
957
|
-
const providerName = stringProperty(provider, "name") ?? providerId;
|
|
958
|
-
return { providerId, providerName, title: `${providerName} setup`, message: `${stringProperty(method, "name") ?? "Authentication"} is configured outside pi.` };
|
|
331
|
+
return this.#contexts.pinnedAmbientAuthentication(selection);
|
|
959
332
|
}
|
|
960
333
|
pinnedLogoutOptions() {
|
|
961
|
-
return this.#logoutOptions();
|
|
334
|
+
return this.#contexts.logoutOptions();
|
|
962
335
|
}
|
|
963
336
|
pinnedForkOptions() {
|
|
964
|
-
|
|
965
|
-
const messages = requireCapability(session.getUserMessagesForForking, "getUserMessagesForForking").call(session);
|
|
966
|
-
return workflowOptions(messages, "entryId", "text");
|
|
337
|
+
return this.#contexts.pinnedForkOptions();
|
|
967
338
|
}
|
|
968
339
|
pinnedTreeSelectorContext() {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
return
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
340
|
+
return this.#contexts.pinnedTreeSelectorContext();
|
|
341
|
+
}
|
|
342
|
+
/** Bind the owned UI's clipboard lifecycle without changing the comparison host. True means acknowledged delivery. */
|
|
343
|
+
bindClipboardWriter(writer) {
|
|
344
|
+
return this.#workflows.bindClipboardWriter(writer);
|
|
345
|
+
}
|
|
346
|
+
/** Workflow and tree copying share the active owner's write fence and delivery acknowledgment. */
|
|
347
|
+
copyWorkflowText(text) {
|
|
348
|
+
return this.#workflows.copyWorkflowText(text);
|
|
349
|
+
}
|
|
350
|
+
abortBashWorkflow() {
|
|
351
|
+
this.#workflows.abortBashWorkflow();
|
|
352
|
+
}
|
|
353
|
+
executeBashWorkflow(command, excludeFromContext) {
|
|
354
|
+
return this.#workflows.executeBashWorkflow(command, excludeFromContext);
|
|
355
|
+
}
|
|
356
|
+
reloadBlockedResult() {
|
|
357
|
+
return this.#workflows.reloadBlockedResult();
|
|
358
|
+
}
|
|
359
|
+
executeWorkflow(request) {
|
|
360
|
+
return this.#workflows.executeWorkflow(request);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The theme the engine is configured with, in the engine's own grammar: a
|
|
364
|
+
* theme's name, or a `light/dark` pair meaning "follow the terminal".
|
|
365
|
+
*/
|
|
366
|
+
configuredTheme() {
|
|
367
|
+
return this.#settings.configuredTheme();
|
|
368
|
+
}
|
|
369
|
+
/** Settings port for the live runtime, or null before the runtime is available. */
|
|
370
|
+
settingsPort() {
|
|
371
|
+
return this.#settings.settingsPort();
|
|
372
|
+
}
|
|
373
|
+
/** Which settings surface this adapter serves; hidden-in-bare effects are unbindable in bare mode. */
|
|
374
|
+
get settingsProductMode() {
|
|
375
|
+
return this.#settings.productMode;
|
|
376
|
+
}
|
|
377
|
+
bindSettingsOwner(owner, handlers) {
|
|
378
|
+
return this.#settings.bindSettingsOwner(owner, handlers);
|
|
986
379
|
}
|
|
987
380
|
pinnedSettingsSnapshot() {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
return value === undefined ? fallback : value;
|
|
993
|
-
};
|
|
994
|
-
const levels = session.getAvailableThinkingLevels?.();
|
|
995
|
-
const themes = collectionResult(this.#runtime?.services.resourceLoader?.getThemes?.(), "themes").values
|
|
996
|
-
.map(theme => stringProperty(theme, "name"))
|
|
997
|
-
.filter((name) => !!name);
|
|
998
|
-
return {
|
|
999
|
-
autoCompact: typeof session.autoCompactionEnabled === "boolean"
|
|
1000
|
-
? session.autoCompactionEnabled
|
|
1001
|
-
: setting(settings?.getCompactionEnabled, true),
|
|
1002
|
-
showImages: setting(settings?.getShowImages, true),
|
|
1003
|
-
imageWidthCells: setting(settings?.getImageWidthCells, 80),
|
|
1004
|
-
autoResizeImages: setting(settings?.getImageAutoResize, true),
|
|
1005
|
-
blockImages: setting(settings?.getBlockImages, false),
|
|
1006
|
-
enableSkillCommands: setting(settings?.getEnableSkillCommands, true),
|
|
1007
|
-
steeringMode: session.steeringMode ?? setting(settings?.getSteeringMode, "one-at-a-time"),
|
|
1008
|
-
followUpMode: session.followUpMode ?? setting(settings?.getFollowUpMode, "one-at-a-time"),
|
|
1009
|
-
transport: setting(settings?.getTransport, "sse"),
|
|
1010
|
-
httpIdleTimeoutMs: setting(settings?.getHttpIdleTimeoutMs, 300_000),
|
|
1011
|
-
thinkingLevel: readThinkingLevel(session.thinkingLevel),
|
|
1012
|
-
availableThinkingLevels: Array.isArray(levels) ? levels.map(readThinkingLevel) : ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
1013
|
-
currentTheme: setting(settings?.getThemeSetting, setting(settings?.getTheme, "dark")),
|
|
1014
|
-
terminalTheme: "dark",
|
|
1015
|
-
availableThemes: themes.length > 0 ? themes : ["dark", "light"],
|
|
1016
|
-
hideThinkingBlock: setting(settings?.getHideThinkingBlock, false),
|
|
1017
|
-
mermaidRenderingMode: setting(settings?.getMermaidRenderingMode, "off"),
|
|
1018
|
-
showCacheMissNotices: setting(settings?.getShowCacheMissNotices, false),
|
|
1019
|
-
collapseChangelog: setting(settings?.getCollapseChangelog, true),
|
|
1020
|
-
enableInstallTelemetry: setting(settings?.getEnableInstallTelemetry, true),
|
|
1021
|
-
doubleEscapeAction: setting(settings?.getDoubleEscapeAction, "tree"),
|
|
1022
|
-
treeFilterMode: setting(settings?.getTreeFilterMode, "default"),
|
|
1023
|
-
showHardwareCursor: setting(settings?.getShowHardwareCursor, false),
|
|
1024
|
-
editorPaddingX: setting(settings?.getEditorPaddingX, 0),
|
|
1025
|
-
outputPad: setting(settings?.getOutputPad, 1),
|
|
1026
|
-
autocompleteMaxVisible: setting(settings?.getAutocompleteMaxVisible, 5),
|
|
1027
|
-
quietStartup: setting(settings?.getQuietStartup, false),
|
|
1028
|
-
defaultProjectTrust: setting(settings?.getDefaultProjectTrust, "ask"),
|
|
1029
|
-
clearOnShrink: setting(settings?.getClearOnShrink, false),
|
|
1030
|
-
showTerminalProgress: setting(settings?.getShowTerminalProgress, false),
|
|
1031
|
-
tuiMode: setting(settings?.getTuiMode, "regular"),
|
|
1032
|
-
fullscreenExitOutput: setting(settings?.getFullscreenExitOutput, "transcript"),
|
|
1033
|
-
fullscreenScrollbar: setting(settings?.getFullscreenScrollbar, "auto"),
|
|
1034
|
-
warnings: setting(settings?.getWarnings, { anthropicExtraUsage: true }),
|
|
1035
|
-
};
|
|
381
|
+
return this.#settings.pinnedSettingsSnapshot();
|
|
382
|
+
}
|
|
383
|
+
applyPinnedSettingValue(callback, value) {
|
|
384
|
+
return this.#settings.applyPinnedSettingValue(callback, value);
|
|
1036
385
|
}
|
|
1037
386
|
pinnedMessageRenderer(customType) {
|
|
1038
387
|
return this.#requireWorkflowSession().extensionRunner?.getMessageRenderer?.(customType);
|
|
@@ -1044,121 +393,38 @@ export class PiEngineAdapter {
|
|
|
1044
393
|
pinnedToolDefinition(toolName) {
|
|
1045
394
|
return this.#requireWorkflowSession().extensionRunner?.getToolDefinition?.(toolName);
|
|
1046
395
|
}
|
|
1047
|
-
/** Bind the owned UI's clipboard lifecycle without changing the comparison host. True means acknowledged delivery. */
|
|
1048
|
-
bindClipboardWriter(writer) {
|
|
1049
|
-
const previous = this.#clipboardWriter;
|
|
1050
|
-
this.#clipboardWriter = writer;
|
|
1051
|
-
return () => { if (this.#clipboardWriter === writer)
|
|
1052
|
-
this.#clipboardWriter = previous; };
|
|
1053
|
-
}
|
|
1054
|
-
/** Workflow and tree copying share the active owner's write fence and delivery acknowledgment. */
|
|
1055
|
-
async copyWorkflowText(text) {
|
|
1056
|
-
if (this.#clipboardWriter !== undefined)
|
|
1057
|
-
return this.#clipboardWriter(text);
|
|
1058
|
-
await this.#workflowHost.copyText(text);
|
|
1059
|
-
return true;
|
|
1060
|
-
}
|
|
1061
396
|
clearQueuedWorkflows() {
|
|
1062
397
|
const session = this.#requireWorkflowSession();
|
|
1063
398
|
const result = session.clearQueue?.();
|
|
1064
|
-
this.#sessionCommands?.forgetQueuedImages();
|
|
399
|
+
this.#engine.sessionCommands?.forgetQueuedImages();
|
|
1065
400
|
if (!isRecord(result))
|
|
1066
401
|
return [];
|
|
1067
402
|
return [...readStringArray(result.steering), ...readStringArray(result.followUp)];
|
|
1068
403
|
}
|
|
1069
|
-
abortBashWorkflow() {
|
|
1070
|
-
this.#requireWorkflowSession().abortBash?.();
|
|
1071
|
-
}
|
|
1072
|
-
async executeBashWorkflow(command, excludeFromContext) {
|
|
1073
|
-
const session = this.#requireWorkflowSession();
|
|
1074
|
-
const result = await requireCapability(session.executeBash, "executeBash").call(session, command, undefined, { excludeFromContext });
|
|
1075
|
-
if (!isRecord(result))
|
|
1076
|
-
throw new Error("Pi bash workflow returned a malformed result");
|
|
1077
|
-
return {
|
|
1078
|
-
command,
|
|
1079
|
-
output: typeof result.output === "string" ? result.output : "",
|
|
1080
|
-
exitCode: typeof result.exitCode === "number" ? result.exitCode : undefined,
|
|
1081
|
-
cancelled: result.cancelled === true,
|
|
1082
|
-
truncated: result.truncated === true,
|
|
1083
|
-
excludeFromContext,
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
async applyPinnedSettingValue(callback, value) {
|
|
1087
|
-
try {
|
|
1088
|
-
return await this.#applyPinnedSetting(callback, value, true);
|
|
1089
|
-
}
|
|
1090
|
-
catch (error) {
|
|
1091
|
-
return workflowResult("settings", "failed", error instanceof Error ? error.message : String(error));
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
reloadBlockedResult() {
|
|
1095
|
-
const session = this.#requireWorkflowSession();
|
|
1096
|
-
if (session.isStreaming)
|
|
1097
|
-
return workflowResult("reload", "failed", "Wait for the current response to finish before reloading.", undefined, "warning");
|
|
1098
|
-
if (session.isCompacting)
|
|
1099
|
-
return workflowResult("reload", "failed", "Wait for compaction to finish before reloading.", undefined, "warning");
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
async executeWorkflow(request) {
|
|
1103
|
-
const cancelled = workflowResult(request.command, "cancelled", "", undefined, "silent");
|
|
1104
|
-
if (this.#overload !== undefined || this.#admissionStopped || this.#disposed
|
|
1105
|
-
|| this.#pendingCommands.size + this.#pendingWorkflows.size >= 32)
|
|
1106
|
-
return cancelled;
|
|
1107
|
-
let cancel;
|
|
1108
|
-
const cancellation = new Promise(resolve => { cancel = () => resolve(cancelled); });
|
|
1109
|
-
const pending = { command: request.command, cancel };
|
|
1110
|
-
this.#pendingWorkflows.add(pending);
|
|
1111
|
-
this.#runningCommands++;
|
|
1112
|
-
const operation = this.#runWorkflow(request).finally(() => { this.#runningCommands--; });
|
|
1113
|
-
try {
|
|
1114
|
-
return await Promise.race([operation, cancellation]);
|
|
1115
|
-
}
|
|
1116
|
-
finally {
|
|
1117
|
-
this.#pendingWorkflows.delete(pending);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
async #runWorkflow(request) {
|
|
1121
|
-
try {
|
|
1122
|
-
return await this.#performWorkflow(request);
|
|
1123
|
-
}
|
|
1124
|
-
catch (error) {
|
|
1125
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1126
|
-
const contextualMessage = request.command === "export"
|
|
1127
|
-
? `Failed to export session: ${errorMessage(error, "Unknown error")}`
|
|
1128
|
-
: request.command === "import"
|
|
1129
|
-
? `Failed to import session: ${message}`
|
|
1130
|
-
: request.command === "new"
|
|
1131
|
-
? `Failed to create session: ${message}`
|
|
1132
|
-
: request.command === "resume"
|
|
1133
|
-
? `Failed to resume session: ${message}`
|
|
1134
|
-
: request.command === "reload" ? `Reload failed: ${message}` : message;
|
|
1135
|
-
return workflowResult(request.command, "failed", contextualMessage);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
404
|
view() {
|
|
1139
405
|
return {
|
|
1140
406
|
contractVersion: 1,
|
|
1141
407
|
sessionId: this.#sessionId,
|
|
1142
408
|
revision: this.#viewRevision,
|
|
1143
409
|
lifecycle: this.#lifecycle,
|
|
1144
|
-
transcript: this.#
|
|
410
|
+
transcript: this.#projection.snapshot(),
|
|
1145
411
|
editor: { ...this.#editor, queuedSubmissions: [...this.#editor.queuedSubmissions] },
|
|
1146
412
|
status: {
|
|
1147
413
|
...this.#status,
|
|
1148
414
|
diagnostics: [...this.#status.diagnostics],
|
|
1149
415
|
badges: [...this.#status.badges],
|
|
1150
|
-
usage: this.#usageCache ??= this.#
|
|
416
|
+
usage: this.#usageCache ??= readUsageView(this.#engine.session, this.#engine.runtime, this.#activeModel),
|
|
1151
417
|
footer: {
|
|
1152
|
-
branch: this.#gitBranch,
|
|
1153
|
-
sessionName: this.#session?.sessionManager?.getSessionName() ?? null,
|
|
1154
|
-
availableProviderCount: new Set(this.#runtime?.services.modelRuntime.getAvailableSnapshot?.().map(model => model.provider).filter(provider => provider !== undefined) ?? []).size,
|
|
418
|
+
branch: this.#engine.gitBranch,
|
|
419
|
+
sessionName: this.#engine.session?.sessionManager?.getSessionName() ?? null,
|
|
420
|
+
availableProviderCount: new Set(this.#engine.runtime?.services.modelRuntime.getAvailableSnapshot?.().map(model => model.provider).filter(provider => provider !== undefined) ?? []).size,
|
|
1155
421
|
extensionStatuses: [],
|
|
1156
422
|
},
|
|
1157
423
|
},
|
|
1158
424
|
terminal: { ...this.#terminal },
|
|
1159
425
|
activeModel: this.#activeModel === null ? null : { ...this.#activeModel },
|
|
1160
426
|
thinkingLevel: this.#thinkingLevel,
|
|
1161
|
-
activeCommandIds: [...this.#activeCommandIds],
|
|
427
|
+
activeCommandIds: [...this.#commands.activeCommandIds],
|
|
1162
428
|
dialog: null,
|
|
1163
429
|
overlay: null,
|
|
1164
430
|
customizations: [],
|
|
@@ -1170,99 +436,27 @@ export class PiEngineAdapter {
|
|
|
1170
436
|
contractVersion: 1,
|
|
1171
437
|
snapshotId: `snapshot-${this.#viewRevision}`,
|
|
1172
438
|
sessionId: this.#sessionId,
|
|
1173
|
-
sequence: this.#sequence,
|
|
439
|
+
sequence: this.#delivery.sequence,
|
|
1174
440
|
view: this.view(),
|
|
1175
441
|
};
|
|
1176
442
|
assertOwnedUiSnapshot(snapshot);
|
|
1177
443
|
return snapshot;
|
|
1178
444
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
if (command.sessionId !== this.#sessionId) {
|
|
1182
|
-
return this.#finishCommand(command, "rejected", "command targets a different owned session");
|
|
1183
|
-
}
|
|
1184
|
-
if (this.#disposed || !this.#runtime || !this.#session) {
|
|
1185
|
-
return this.#finishCommand(command, "rejected", "engine adapter is not running");
|
|
1186
|
-
}
|
|
1187
|
-
// Invariant: the bounded out-of-band cancellation path has one slot per admitted command.
|
|
1188
|
-
if (this.#overload !== undefined || this.#admissionStopped || this.#pendingCommands.size + this.#pendingWorkflows.size >= 32)
|
|
1189
|
-
return { outcome: "rejected", diagnostic: null };
|
|
1190
|
-
const existing = this.#completedCommands.get(command.correlationId);
|
|
1191
|
-
if (existing)
|
|
1192
|
-
return existing;
|
|
1193
|
-
if (this.#activeCommandIds.includes(command.correlationId)) {
|
|
1194
|
-
return this.#finishCommand(command, "rejected", "duplicate engine command correlation id");
|
|
1195
|
-
}
|
|
1196
|
-
this.#activeCommandIds.push(command.correlationId);
|
|
1197
|
-
const generation = this.#sessionGeneration;
|
|
1198
|
-
let cancelled = false;
|
|
1199
|
-
const cancellation = new Promise(resolve => {
|
|
1200
|
-
this.#pendingCommands.set(command.correlationId, { type: command.type, cancel: () => {
|
|
1201
|
-
if (cancelled)
|
|
1202
|
-
return;
|
|
1203
|
-
cancelled = true;
|
|
1204
|
-
resolve(this.#recordCommand(command, "failed", null));
|
|
1205
|
-
} });
|
|
1206
|
-
});
|
|
1207
|
-
this.#emitEvent({ type: "command-outcome", correlationId: command.correlationId, outcome: "accepted", diagnostic: null });
|
|
1208
|
-
const operation = async () => {
|
|
1209
|
-
try {
|
|
1210
|
-
if (cancelled)
|
|
1211
|
-
return { outcome: "failed", diagnostic: null };
|
|
1212
|
-
this.#runningCommands++;
|
|
1213
|
-
try {
|
|
1214
|
-
await this.#perform(command);
|
|
1215
|
-
}
|
|
1216
|
-
finally {
|
|
1217
|
-
this.#runningCommands--;
|
|
1218
|
-
}
|
|
1219
|
-
if (cancelled)
|
|
1220
|
-
return { outcome: "failed", diagnostic: null };
|
|
1221
|
-
if (generation === this.#sessionGeneration)
|
|
1222
|
-
this.#emitView();
|
|
1223
|
-
if (cancelled)
|
|
1224
|
-
return { outcome: "failed", diagnostic: null };
|
|
1225
|
-
return this.#recordCommand(command, "completed", null);
|
|
1226
|
-
}
|
|
1227
|
-
catch (error) {
|
|
1228
|
-
if (cancelled)
|
|
1229
|
-
return { outcome: "failed", diagnostic: null };
|
|
1230
|
-
const diagnostic = error instanceof Error ? error.message : String(error);
|
|
1231
|
-
this.#addDiagnostic("error", "engine-command", diagnostic, true);
|
|
1232
|
-
if (generation === this.#sessionGeneration)
|
|
1233
|
-
this.#emitView();
|
|
1234
|
-
return this.#recordCommand(command, "failed", diagnostic);
|
|
1235
|
-
}
|
|
1236
|
-
};
|
|
1237
|
-
try {
|
|
1238
|
-
return await Promise.race([operation(), cancellation]);
|
|
1239
|
-
}
|
|
1240
|
-
finally {
|
|
1241
|
-
this.#pendingCommands.delete(command.correlationId);
|
|
1242
|
-
}
|
|
445
|
+
execute(command) {
|
|
446
|
+
return this.#commands.execute(command);
|
|
1243
447
|
}
|
|
1244
448
|
async dispose() {
|
|
1245
449
|
if (this.#disposed)
|
|
1246
450
|
return;
|
|
1247
451
|
this.#disposed = true;
|
|
1248
|
-
|
|
1249
|
-
if (pending.type !== "shutdown")
|
|
1250
|
-
pending.cancel();
|
|
452
|
+
this.#commands.cancelPending(["shutdown"]);
|
|
1251
453
|
// Compatibility: /quit owns normal disposal and must report its actual completion, not cancel itself.
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
this.#transcriptImageAssets.clear();
|
|
1256
|
-
this.#extensionBound = false;
|
|
1257
|
-
this.#extensionUi = undefined;
|
|
1258
|
-
this.#extensionShutdown = undefined;
|
|
454
|
+
this.#workflows.cancelPending("quit");
|
|
455
|
+
this.#projection.assets.clear();
|
|
456
|
+
await this.#extensions.unbind();
|
|
1259
457
|
this.#lifecycle = "stopping";
|
|
1260
458
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopping", reason: null });
|
|
1261
|
-
this.#
|
|
1262
|
-
this.#unsubscribe = undefined;
|
|
1263
|
-
this.#compactionProgress?.dispose();
|
|
1264
|
-
this.#compactionProgress = null;
|
|
1265
|
-
await this.#runtime?.dispose();
|
|
459
|
+
await this.#engine.dispose();
|
|
1266
460
|
this.#lifecycle = "stopped";
|
|
1267
461
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopped", reason: null });
|
|
1268
462
|
this.#emitView();
|
|
@@ -1274,716 +468,45 @@ export class PiEngineAdapter {
|
|
|
1274
468
|
throw error;
|
|
1275
469
|
}
|
|
1276
470
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
case "
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
?
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
: [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
|
|
1301
|
-
let model = findExactWorkflowModel(reference, availableModels);
|
|
1302
|
-
const messages = [];
|
|
1303
|
-
const generation = this.#sessionGeneration;
|
|
1304
|
-
const current = () => !this.#disposed && generation === this.#sessionGeneration;
|
|
1305
|
-
const publish = (message) => {
|
|
1306
|
-
if (!current())
|
|
1307
|
-
return;
|
|
1308
|
-
if (this.#workflowInteraction.publish)
|
|
1309
|
-
this.#workflowInteraction.publish(message);
|
|
1310
|
-
else
|
|
1311
|
-
messages.push(message);
|
|
1312
|
-
};
|
|
1313
|
-
if (model === undefined && scopedModels.length === 0) {
|
|
1314
|
-
publish({ kind: "status", message: "Refreshing model catalogs…" });
|
|
1315
|
-
const controller = new AbortController();
|
|
1316
|
-
let timedOut = false;
|
|
1317
|
-
const timeout = setTimeout(() => {
|
|
1318
|
-
timedOut = true;
|
|
1319
|
-
controller.abort();
|
|
1320
|
-
}, AUTH_REFRESH_TIMEOUT_MS);
|
|
1321
|
-
try {
|
|
1322
|
-
const refreshed = await runtime.services.modelRuntime.refresh?.({ signal: controller.signal });
|
|
1323
|
-
if (isRecord(refreshed) && refreshed.aborted === true && timedOut) {
|
|
1324
|
-
publish({ kind: "warning", message: "Model refresh timed out; searching cached models." });
|
|
1325
|
-
}
|
|
1326
|
-
else if (isRecord(refreshed) && refreshed.errors instanceof Map && refreshed.errors.size > 0) {
|
|
1327
|
-
publish({ kind: "warning", message: `Could not refresh ${[...refreshed.errors.keys()].join(", ")}; searching cached models.` });
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
catch (error) {
|
|
1331
|
-
publish({
|
|
1332
|
-
kind: "warning",
|
|
1333
|
-
message: timedOut
|
|
1334
|
-
? "Model refresh timed out; searching cached models."
|
|
1335
|
-
: `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`,
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
finally {
|
|
1339
|
-
clearTimeout(timeout);
|
|
1340
|
-
}
|
|
1341
|
-
availableModels = [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
|
|
1342
|
-
model = findExactWorkflowModel(reference, availableModels);
|
|
1343
|
-
}
|
|
1344
|
-
if (!current())
|
|
1345
|
-
return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
|
|
1346
|
-
if (model === undefined) {
|
|
1347
|
-
return {
|
|
1348
|
-
...workflowResult(request.command, "requires-selection", "Select a model", reference, "silent"),
|
|
1349
|
-
...(messages.length === 0 ? {} : { messages: Object.freeze(messages) }),
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1352
|
-
try {
|
|
1353
|
-
await session.setModel(model);
|
|
1354
|
-
}
|
|
1355
|
-
catch (error) {
|
|
1356
|
-
if (!current())
|
|
1357
|
-
return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
|
|
1358
|
-
const failure = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
1359
|
-
return workflowResult(request.command, "failed", failure.message, undefined, "error", messages.length === 0 ? undefined : [...messages, failure]);
|
|
1360
|
-
}
|
|
1361
|
-
if (!current())
|
|
1362
|
-
return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
|
|
1363
|
-
const providerId = stringProperty(model, "provider") ?? "unknown";
|
|
1364
|
-
const modelId = stringProperty(model, "id") ?? reference;
|
|
1365
|
-
this.#activeModel = { providerId, modelId, displayName: stringProperty(model, "name") ?? modelId };
|
|
1366
|
-
this.#emitView();
|
|
1367
|
-
const resultMessage = { kind: "status", message: `Model: ${modelId}` };
|
|
1368
|
-
return messages.length === 0
|
|
1369
|
-
? workflowResult(request.command, "completed", resultMessage.message)
|
|
1370
|
-
: workflowResult(request.command, "completed", resultMessage.message, undefined, "status", [...messages, resultMessage]);
|
|
1371
|
-
}
|
|
1372
|
-
case "scoped-models": {
|
|
1373
|
-
if (!selection)
|
|
1374
|
-
return workflowResult(request.command, "failed", "Scoped models requires the owned scoped-model controller");
|
|
1375
|
-
const [providerId, modelId] = selection.split("/", 2);
|
|
1376
|
-
const model = providerId && modelId ? runtime.services.modelRuntime.getModel(providerId, modelId) : undefined;
|
|
1377
|
-
if (!model)
|
|
1378
|
-
return workflowResult(request.command, "failed", `Model is unavailable: ${selection}`);
|
|
1379
|
-
requireCapability(session.setScopedModels, "setScopedModels").call(session, [{ model }]);
|
|
1380
|
-
return workflowResult(request.command, "completed", `Enabled scoped model ${selection}`);
|
|
1381
|
-
}
|
|
1382
|
-
case "export": {
|
|
1383
|
-
const path = pathArgument(argument);
|
|
1384
|
-
const file = path?.toLowerCase().endsWith(".jsonl")
|
|
1385
|
-
? requireCapability(session.exportToJsonl, "exportToJsonl").call(session, path)
|
|
1386
|
-
: await requireCapability(session.exportToHtml, "exportToHtml").call(session, path);
|
|
1387
|
-
return workflowResult(request.command, "completed", `Session exported to: ${String(file)}`);
|
|
1388
|
-
}
|
|
1389
|
-
case "import": {
|
|
1390
|
-
const path = pathArgument(argument);
|
|
1391
|
-
if (!path)
|
|
1392
|
-
return workflowResult(request.command, "failed", "Usage: /import <path.jsonl>");
|
|
1393
|
-
if (request.confirmed === undefined) {
|
|
1394
|
-
return workflowConfirmation(request.command, `Replace current session with ${path}?`);
|
|
1395
|
-
}
|
|
1396
|
-
if (!request.confirmed)
|
|
1397
|
-
return workflowResult(request.command, "cancelled", "Import cancelled", undefined, "status");
|
|
1398
|
-
try {
|
|
1399
|
-
const result = await requireCapability(runtime.importFromJsonl, "importFromJsonl").call(runtime, path, request.cwdOverride);
|
|
1400
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1401
|
-
return workflowResult(request.command, "cancelled", "Import cancelled", undefined, "status");
|
|
1402
|
-
return workflowResult(request.command, "completed", `Session imported from: ${path}`);
|
|
1403
|
-
}
|
|
1404
|
-
catch (error) {
|
|
1405
|
-
const issue = isRecord(error) && isRecord(error.issue) ? error.issue : undefined;
|
|
1406
|
-
const fallbackCwd = issue === undefined ? undefined : stringProperty(issue, "fallbackCwd");
|
|
1407
|
-
if (fallbackCwd !== undefined && request.cwdOverride === undefined) {
|
|
1408
|
-
const sessionCwd = stringProperty(issue, "sessionCwd") ?? "the session working directory";
|
|
1409
|
-
return workflowConfirmation(request.command, `cwd from session file does not exist\n${sessionCwd}\n\ncontinue in current cwd\n${fallbackCwd}`, fallbackCwd);
|
|
1410
|
-
}
|
|
1411
|
-
throw error;
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
case "share": {
|
|
1415
|
-
const signal = request.signal;
|
|
1416
|
-
if (signal?.aborted)
|
|
1417
|
-
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1418
|
-
try {
|
|
1419
|
-
await this.#workflowHost.runCommand("gh", ["auth", "status"], signal === undefined ? undefined : { signal });
|
|
1420
|
-
}
|
|
1421
|
-
catch (error) {
|
|
1422
|
-
if (signal?.aborted || isAbortError(error))
|
|
1423
|
-
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1424
|
-
const message = commandMissing(error)
|
|
1425
|
-
? "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/"
|
|
1426
|
-
: "GitHub CLI is not logged in. Run 'gh auth login' first.";
|
|
1427
|
-
return workflowResult(request.command, "failed", message);
|
|
1428
|
-
}
|
|
1429
|
-
const temporary = join(tmpdir(), `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}pi-session-${process.pid}.html`);
|
|
1430
|
-
try {
|
|
1431
|
-
try {
|
|
1432
|
-
await requireCapability(session.exportToHtml, "exportToHtml").call(session, temporary);
|
|
1433
|
-
}
|
|
1434
|
-
catch (error) {
|
|
1435
|
-
return workflowResult(request.command, "failed", `Failed to export session: ${errorMessage(error, "Unknown error")}`);
|
|
1436
|
-
}
|
|
1437
|
-
if (signal?.aborted)
|
|
1438
|
-
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1439
|
-
let gist;
|
|
1440
|
-
try {
|
|
1441
|
-
gist = await this.#workflowHost.runCommand("gh", ["gist", "create", "--public=false", temporary], signal === undefined ? undefined : { signal });
|
|
1442
|
-
}
|
|
1443
|
-
catch (error) {
|
|
1444
|
-
if (signal?.aborted || isAbortError(error))
|
|
1445
|
-
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1446
|
-
return workflowResult(request.command, "failed", `Failed to create gist: ${shareCommandFailureDetail(error)}`);
|
|
471
|
+
#requireWorkflowSession() {
|
|
472
|
+
if (this.#disposed || !this.#engine.runtime || !this.#engine.session)
|
|
473
|
+
throw new Error("engine adapter is not running");
|
|
474
|
+
return this.#engine.session;
|
|
475
|
+
}
|
|
476
|
+
async #perform(command) {
|
|
477
|
+
const runtime = this.#engine.runtime;
|
|
478
|
+
const session = this.#engine.session;
|
|
479
|
+
const generation = this.#engine.generation;
|
|
480
|
+
if (!runtime || !session)
|
|
481
|
+
throw new Error("engine session is unavailable");
|
|
482
|
+
switch (command.type) {
|
|
483
|
+
case "prompt":
|
|
484
|
+
case "steer":
|
|
485
|
+
case "follow-up":
|
|
486
|
+
case "abort":
|
|
487
|
+
case "retry":
|
|
488
|
+
case "compact": {
|
|
489
|
+
const result = await this.#engine.sessionCommands?.execute(command.type === "prompt" || command.type === "steer" || command.type === "follow-up"
|
|
490
|
+
? {
|
|
491
|
+
type: command.type,
|
|
492
|
+
text: command.text,
|
|
493
|
+
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
1447
494
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
const gistId = gistUrl.split("/").at(-1);
|
|
1452
|
-
if (!gistId)
|
|
1453
|
-
return workflowResult(request.command, "failed", "Failed to parse gist ID from gh output");
|
|
1454
|
-
return workflowResult(request.command, "completed", `Share URL: ${shareViewerUrl(gistId)}`, gistUrl);
|
|
1455
|
-
}
|
|
1456
|
-
finally {
|
|
1457
|
-
await rm(temporary, { force: true });
|
|
495
|
+
: { type: command.type });
|
|
496
|
+
if (!result || result.outcome === "rejected" || result.outcome === "failed") {
|
|
497
|
+
throw new Error(command.type === "retry" ? "no previous prompt is available to retry" : `Pi session command failed: ${command.type}`);
|
|
1458
498
|
}
|
|
499
|
+
return;
|
|
1459
500
|
}
|
|
1460
|
-
case "
|
|
1461
|
-
const
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1464
|
-
const acknowledged = await this.copyWorkflowText(text);
|
|
1465
|
-
return workflowResult(request.command, "completed", acknowledged ? "Copied last agent message to clipboard" : "Submitted last agent message to clipboard");
|
|
1466
|
-
}
|
|
1467
|
-
case "name": {
|
|
1468
|
-
const manager = session.sessionManager;
|
|
1469
|
-
if (!argument) {
|
|
1470
|
-
const current = manager?.getSessionName();
|
|
1471
|
-
return typeof current === "string"
|
|
1472
|
-
? workflowResult(request.command, "completed", `Session name: ${current}`)
|
|
1473
|
-
: workflowResult(request.command, "failed", "Usage: /name <name>", undefined, "warning");
|
|
501
|
+
case "set-model": {
|
|
502
|
+
const model = runtime.services.modelRuntime.getModel(command.model.providerId, command.model.modelId);
|
|
503
|
+
if (!model) {
|
|
504
|
+
throw new Error(`model is unavailable: ${command.model.providerId}/${command.model.modelId}`);
|
|
1474
505
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
return
|
|
1479
|
-
}
|
|
1480
|
-
case "session": {
|
|
1481
|
-
const manager = session.sessionManager;
|
|
1482
|
-
const stats = requireCapability(session.getSessionStats, "getSessionStats").call(session);
|
|
1483
|
-
const entries = manager?.getEntries();
|
|
1484
|
-
return {
|
|
1485
|
-
...workflowResult(request.command, "completed", "Session Info"),
|
|
1486
|
-
presentation: pinnedSessionInfoPresentation(stats, manager?.getSessionName(), Array.isArray(entries) ? entries : [], runtime.services.modelRuntime),
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
case "changelog": {
|
|
1490
|
-
const changelog = await this.#workflowHost.readChangelog();
|
|
1491
|
-
return workflowResult(request.command, "completed", "What's New", changelog);
|
|
1492
|
-
}
|
|
1493
|
-
case "hotkeys":
|
|
1494
|
-
return workflowResult(request.command, "completed", "Keyboard Shortcuts", pinnedHotkeySummary());
|
|
1495
|
-
case "fork": {
|
|
1496
|
-
if (!selection)
|
|
1497
|
-
return workflowResult(request.command, "failed", "Fork requires the owned user-message controller");
|
|
1498
|
-
const result = await requireCapability(runtime.fork, "fork").call(runtime, selection, { position: "before" });
|
|
1499
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1500
|
-
return workflowResult(request.command, "cancelled", "Fork cancelled", undefined, "silent");
|
|
1501
|
-
return workflowResult(request.command, "completed", "Forked to new session");
|
|
1502
|
-
}
|
|
1503
|
-
case "clone": {
|
|
1504
|
-
const manager = session.sessionManager;
|
|
1505
|
-
const leaf = manager?.getLeafId?.();
|
|
1506
|
-
if (typeof leaf !== "string")
|
|
1507
|
-
return workflowResult(request.command, "completed", "Nothing to clone yet", undefined, "status");
|
|
1508
|
-
const result = await requireCapability(runtime.fork, "fork").call(runtime, leaf, { position: "at" });
|
|
1509
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1510
|
-
return workflowResult(request.command, "cancelled", "Clone cancelled", undefined, "silent");
|
|
1511
|
-
return workflowResult(request.command, "completed", "Cloned to new session");
|
|
1512
|
-
}
|
|
1513
|
-
case "tree": {
|
|
1514
|
-
if (!selection)
|
|
1515
|
-
return workflowResult(request.command, "failed", "Tree navigation requires the owned tree controller");
|
|
1516
|
-
const navigateTree = requireCapability(session.navigateTree, "navigateTree");
|
|
1517
|
-
const result = request.treeSummary === undefined
|
|
1518
|
-
? await navigateTree.call(session, selection)
|
|
1519
|
-
: await navigateTree.call(session, selection, {
|
|
1520
|
-
summarize: request.treeSummary.summarize,
|
|
1521
|
-
...(request.treeSummary.customInstructions === undefined ? {} : { customInstructions: request.treeSummary.customInstructions }),
|
|
1522
|
-
});
|
|
1523
|
-
if (isRecord(result) && result.aborted === true)
|
|
1524
|
-
return workflowResult(request.command, "cancelled", "Branch summarization cancelled", undefined, "status");
|
|
1525
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1526
|
-
return workflowResult(request.command, "cancelled", "Navigation cancelled", undefined, "status");
|
|
1527
|
-
return workflowResult(request.command, "completed", "Navigated to selected point");
|
|
1528
|
-
}
|
|
1529
|
-
case "trust": {
|
|
1530
|
-
if (!selection)
|
|
1531
|
-
return workflowResult(request.command, "failed", "Trust requires the owned trust controller");
|
|
1532
|
-
requireCapability(runtime.services.settingsManager?.setProjectTrusted, "setProjectTrusted").call(runtime.services.settingsManager, selection === "trust");
|
|
1533
|
-
return workflowResult(request.command, "completed", selection === "trust" ? "Project trusted" : "Project trust removed");
|
|
1534
|
-
}
|
|
1535
|
-
case "login": {
|
|
1536
|
-
if (!selection && !argument)
|
|
1537
|
-
return workflowResult(request.command, "failed", "Login requires the owned authentication controller");
|
|
1538
|
-
const modelRuntime = runtime.services.modelRuntime;
|
|
1539
|
-
let loginSelection = selection ?? argument;
|
|
1540
|
-
if (!selection && argument && !argument.includes(":")) {
|
|
1541
|
-
const normalized = argument.toLowerCase();
|
|
1542
|
-
const providers = modelRuntime.getProviders?.();
|
|
1543
|
-
const providerMatches = Array.isArray(providers) ? providers.filter(isRecord).filter(candidate => stringProperty(candidate, "id")?.toLowerCase() === normalized
|
|
1544
|
-
|| stringProperty(candidate, "name")?.toLowerCase() === normalized) : [];
|
|
1545
|
-
const providerReference = providerMatches.length === 1 ? stringProperty(providerMatches[0], "id") ?? argument : argument;
|
|
1546
|
-
const matching = this.#loginOptions().filter(option => option.id.endsWith(`:${providerReference}`));
|
|
1547
|
-
if (matching.length > 1) {
|
|
1548
|
-
const provider = modelRuntime.getProvider?.(providerReference);
|
|
1549
|
-
const providerName = stringProperty(provider, "name") ?? providerReference;
|
|
1550
|
-
return workflowResult(request.command, "failed", `Authentication method for ${providerName} requires the owned authentication controller`);
|
|
1551
|
-
}
|
|
1552
|
-
if (matching[0])
|
|
1553
|
-
loginSelection = matching[0].id;
|
|
1554
|
-
}
|
|
1555
|
-
const [authTypeValue = "oauth", providerId = loginSelection] = loginSelection.includes(":")
|
|
1556
|
-
? loginSelection.split(":", 2)
|
|
1557
|
-
: ["oauth", loginSelection];
|
|
1558
|
-
const authType = authTypeValue === "api_key" ? "api_key" : "oauth";
|
|
1559
|
-
const provider = modelRuntime.getProvider?.(providerId);
|
|
1560
|
-
const providerName = stringProperty(provider, "name") ?? providerId;
|
|
1561
|
-
const previousModel = session.model;
|
|
1562
|
-
this.#workflowInteraction.startLogin?.({ providerId, providerName, authType });
|
|
1563
|
-
try {
|
|
1564
|
-
await requireCapability(modelRuntime.login, "login").call(modelRuntime, providerId, authType, {
|
|
1565
|
-
signal: AbortSignal.timeout(120_000),
|
|
1566
|
-
prompt: async (prompt) => {
|
|
1567
|
-
const promptType = stringProperty(prompt, "type");
|
|
1568
|
-
const options = isRecord(prompt) && Array.isArray(prompt.options)
|
|
1569
|
-
? prompt.options.filter(isRecord).flatMap(option => {
|
|
1570
|
-
const id = stringProperty(option, "id");
|
|
1571
|
-
const label = stringProperty(option, "label");
|
|
1572
|
-
return id && label ? [{ id, label }] : [];
|
|
1573
|
-
})
|
|
1574
|
-
: [];
|
|
1575
|
-
const response = await this.#workflowInteraction.prompt({
|
|
1576
|
-
type: promptType === "select"
|
|
1577
|
-
? "select"
|
|
1578
|
-
: promptType === "manual_code"
|
|
1579
|
-
? "manual-code"
|
|
1580
|
-
: authType === "api_key"
|
|
1581
|
-
? "secret"
|
|
1582
|
-
: "text",
|
|
1583
|
-
message: stringProperty(prompt, "message") ?? `Authenticate ${providerName}`,
|
|
1584
|
-
...(stringProperty(prompt, "placeholder") === undefined ? {} : { placeholder: stringProperty(prompt, "placeholder") }),
|
|
1585
|
-
...(options.length === 0 ? {} : { options }),
|
|
1586
|
-
});
|
|
1587
|
-
if (response === null)
|
|
1588
|
-
throw new Error("Login cancelled");
|
|
1589
|
-
return response;
|
|
1590
|
-
},
|
|
1591
|
-
notify: (event) => {
|
|
1592
|
-
const notification = workflowLoginNotification(event);
|
|
1593
|
-
if (notification)
|
|
1594
|
-
this.#workflowInteraction.notify(notification);
|
|
1595
|
-
},
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
catch (error) {
|
|
1599
|
-
if (error instanceof Error && error.message === "Login cancelled") {
|
|
1600
|
-
return workflowResult(request.command, "cancelled", "Login cancelled", undefined, "silent");
|
|
1601
|
-
}
|
|
1602
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1603
|
-
const actionLabel = authType === "api_key" ? `Saved API key for ${providerName}` : `Logged in to ${providerName}`;
|
|
1604
|
-
const message = error instanceof CredentialSynchronizationError
|
|
1605
|
-
? `${actionLabel}, but local model state could not be synchronized: ${detail}`
|
|
1606
|
-
: authType === "api_key"
|
|
1607
|
-
? `Failed to save API key for ${providerName}: ${detail}`
|
|
1608
|
-
: `Failed to login to ${providerName}: ${detail}`;
|
|
1609
|
-
return workflowResult(request.command, "failed", message);
|
|
1610
|
-
}
|
|
1611
|
-
finally {
|
|
1612
|
-
this.#workflowInteraction.finishLogin?.();
|
|
1613
|
-
}
|
|
1614
|
-
return await this.#completeProviderAuthentication(providerId, providerName, authType, previousModel);
|
|
1615
|
-
}
|
|
1616
|
-
case "logout": {
|
|
1617
|
-
if (!selection)
|
|
1618
|
-
return workflowResult(request.command, "failed", "Logout requires the owned authentication controller");
|
|
1619
|
-
const [credentialType = "oauth", providerId = selection] = selection.includes(":") ? selection.split(":", 2) : ["oauth", selection];
|
|
1620
|
-
const modelRuntime = runtime.services.modelRuntime;
|
|
1621
|
-
const provider = modelRuntime.getProvider?.(providerId);
|
|
1622
|
-
const providerName = stringProperty(provider, "name") ?? providerId;
|
|
1623
|
-
try {
|
|
1624
|
-
await requireCapability(modelRuntime.logout, "logout").call(modelRuntime, providerId, { signal: AbortSignal.timeout(15_000) });
|
|
1625
|
-
}
|
|
1626
|
-
catch (error) {
|
|
1627
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1628
|
-
const message = error instanceof CredentialSynchronizationError
|
|
1629
|
-
? `Credentials removed for ${providerName}, but local model state could not be synchronized: ${detail}`
|
|
1630
|
-
: `Logout failed: ${detail}`;
|
|
1631
|
-
return workflowResult(request.command, "failed", message);
|
|
1632
|
-
}
|
|
1633
|
-
this.#reconcileActiveModelAvailability();
|
|
1634
|
-
this.#emitView();
|
|
1635
|
-
return workflowResult(request.command, "completed", credentialType === "api_key"
|
|
1636
|
-
? `Removed stored API key for ${providerName}. Environment variables and models.json config are unchanged.`
|
|
1637
|
-
: `Logged out of ${providerName}`);
|
|
1638
|
-
}
|
|
1639
|
-
case "new": {
|
|
1640
|
-
const result = await runtime.newSession();
|
|
1641
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1642
|
-
return workflowResult(request.command, "cancelled", "New session cancelled", undefined, "silent");
|
|
1643
|
-
return workflowResult(request.command, "completed", "✓ New session started", undefined, "accent");
|
|
1644
|
-
}
|
|
1645
|
-
case "compact": {
|
|
1646
|
-
try {
|
|
1647
|
-
await session.compact(argument || undefined);
|
|
1648
|
-
return workflowResult(request.command, "completed", "Compaction requested", undefined, "silent");
|
|
1649
|
-
}
|
|
1650
|
-
catch (error) {
|
|
1651
|
-
return workflowResult(request.command, "failed", error instanceof Error ? error.message : String(error), undefined, "silent");
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
case "resume": {
|
|
1655
|
-
if (!selection && !argument)
|
|
1656
|
-
return workflowResult(request.command, "failed", "Resume requires the owned session controller");
|
|
1657
|
-
const sessionPath = selection ?? argument;
|
|
1658
|
-
try {
|
|
1659
|
-
const result = await runtime.switchSession(sessionPath);
|
|
1660
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1661
|
-
return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "silent");
|
|
1662
|
-
return workflowResult(request.command, "completed", "Resumed session");
|
|
1663
|
-
}
|
|
1664
|
-
catch (error) {
|
|
1665
|
-
const issue = isRecord(error) && isRecord(error.issue) ? error.issue : undefined;
|
|
1666
|
-
const fallbackCwd = issue === undefined ? undefined : stringProperty(issue, "fallbackCwd");
|
|
1667
|
-
if (fallbackCwd === undefined)
|
|
1668
|
-
throw error;
|
|
1669
|
-
if (request.confirmed === undefined) {
|
|
1670
|
-
const sessionCwd = stringProperty(issue, "sessionCwd") ?? "the session working directory";
|
|
1671
|
-
return workflowConfirmation(request.command, `cwd from session file does not exist\n${sessionCwd}\n\ncontinue in current cwd\n${fallbackCwd}`);
|
|
1672
|
-
}
|
|
1673
|
-
if (!request.confirmed)
|
|
1674
|
-
return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "status");
|
|
1675
|
-
const result = await runtime.switchSession(sessionPath, { cwdOverride: fallbackCwd });
|
|
1676
|
-
if (isRecord(result) && result.cancelled === true)
|
|
1677
|
-
return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "silent");
|
|
1678
|
-
return workflowResult(request.command, "completed", "Resumed session in current cwd");
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
case "reload": {
|
|
1682
|
-
const blocked = this.reloadBlockedResult();
|
|
1683
|
-
if (blocked)
|
|
1684
|
-
return blocked;
|
|
1685
|
-
await requireCapability(session.reload, "reload").call(session);
|
|
1686
|
-
await this.#bindExtensionUiToSession();
|
|
1687
|
-
const message = "Reloaded keybindings, extensions, skills, prompts, themes, and context files";
|
|
1688
|
-
const modelError = runtime.services.modelRuntime.getError?.();
|
|
1689
|
-
return workflowResult(request.command, "completed", message, undefined, "status", modelError ? [
|
|
1690
|
-
{ kind: "error", message: `models.json error: ${modelError}` },
|
|
1691
|
-
{ kind: "status", message },
|
|
1692
|
-
] : undefined);
|
|
1693
|
-
}
|
|
1694
|
-
case "quit": {
|
|
1695
|
-
await this.dispose();
|
|
1696
|
-
return workflowResult(request.command, "completed", "Shutdown complete");
|
|
1697
|
-
}
|
|
1698
|
-
case "debug": {
|
|
1699
|
-
const debugPath = join(this.#agentDir, "pi-debug.log");
|
|
1700
|
-
await mkdir(dirname(debugPath), { recursive: true });
|
|
1701
|
-
await writeFile(debugPath, `${JSON.stringify(this.snapshot(), null, 2)}\n`, "utf8");
|
|
1702
|
-
return workflowResult(request.command, "completed", "✓ Debug log written", debugPath);
|
|
1703
|
-
}
|
|
1704
|
-
case "arminsayshi":
|
|
1705
|
-
return workflowResult(request.command, "completed", "Armin says hi");
|
|
1706
|
-
case "dementedelves":
|
|
1707
|
-
return workflowResult(request.command, "completed", "Demented elves announcement");
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
async #completeProviderAuthentication(providerId, providerName, authType, previousModel) {
|
|
1711
|
-
const session = this.#requireWorkflowSession();
|
|
1712
|
-
const modelRuntime = this.#runtime?.services.modelRuntime;
|
|
1713
|
-
if (!modelRuntime)
|
|
1714
|
-
throw new Error("engine runtime is unavailable");
|
|
1715
|
-
const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
|
|
1716
|
-
let selectedModelId;
|
|
1717
|
-
let selectionError;
|
|
1718
|
-
if (isUnknownModel(previousModel)) {
|
|
1719
|
-
const available = modelRuntime.getAvailableSnapshot?.() ?? [];
|
|
1720
|
-
const providerModels = available.filter(model => stringProperty(model, "provider") === providerId);
|
|
1721
|
-
const defaultModelId = PINNED_DEFAULT_MODEL_BY_PROVIDER[providerId];
|
|
1722
|
-
if (defaultModelId === undefined) {
|
|
1723
|
-
selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
|
|
1724
|
-
}
|
|
1725
|
-
else if (providerModels.length === 0) {
|
|
1726
|
-
selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
|
|
1727
|
-
}
|
|
1728
|
-
else {
|
|
1729
|
-
const selectedModel = providerModels.find(model => stringProperty(model, "id") === defaultModelId);
|
|
1730
|
-
if (selectedModel === undefined) {
|
|
1731
|
-
selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
|
|
1732
|
-
}
|
|
1733
|
-
else {
|
|
1734
|
-
try {
|
|
1735
|
-
await session.setModel(selectedModel);
|
|
1736
|
-
selectedModelId = stringProperty(selectedModel, "id") ?? defaultModelId;
|
|
1737
|
-
this.#activeModel = {
|
|
1738
|
-
providerId,
|
|
1739
|
-
modelId: selectedModelId,
|
|
1740
|
-
displayName: stringProperty(selectedModel, "name") ?? selectedModelId,
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
catch (error) {
|
|
1744
|
-
selectionError = `${actionLabel}, but selecting its default model failed: ${error instanceof Error ? error.message : String(error)}. Use /model to select a model.`;
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
this.#reconcileActiveModelAvailability();
|
|
1750
|
-
this.#emitView();
|
|
1751
|
-
const status = `${actionLabel}.${selectedModelId ? ` Selected ${selectedModelId}.` : ""} Credentials saved to ${join(this.#agentDir, "auth.json")}`;
|
|
1752
|
-
const messages = [
|
|
1753
|
-
{ kind: "status", message: status },
|
|
1754
|
-
...(selectionError === undefined ? [] : [{ kind: "error", message: selectionError }]),
|
|
1755
|
-
];
|
|
1756
|
-
this.#scheduleAuthenticatedProviderRefresh(providerId, actionLabel);
|
|
1757
|
-
return workflowResult("login", "completed", status, undefined, "status", messages);
|
|
1758
|
-
}
|
|
1759
|
-
#scheduleAuthenticatedProviderRefresh(providerId, actionLabel) {
|
|
1760
|
-
const runtime = this.#runtime;
|
|
1761
|
-
const refresh = runtime?.services.modelRuntime.refresh;
|
|
1762
|
-
if (!runtime || typeof refresh !== "function")
|
|
1763
|
-
return;
|
|
1764
|
-
const generation = this.#sessionGeneration;
|
|
1765
|
-
const publish = this.#workflowInteraction.publish;
|
|
1766
|
-
const controller = new AbortController();
|
|
1767
|
-
const timeout = setTimeout(() => controller.abort(), AUTH_REFRESH_TIMEOUT_MS);
|
|
1768
|
-
// Compatibility: post-authentication refresh starts on the next turn so its notices follow the saved-credential status.
|
|
1769
|
-
void new Promise(resolve => setTimeout(resolve, 0))
|
|
1770
|
-
.then(() => refresh.call(runtime.services.modelRuntime, { providers: [providerId], signal: controller.signal }))
|
|
1771
|
-
.then(result => {
|
|
1772
|
-
if (this.#disposed || this.#sessionGeneration !== generation)
|
|
1773
|
-
return;
|
|
1774
|
-
if (isRecord(result) && result.aborted === true) {
|
|
1775
|
-
publish?.({ kind: "warning", message: `${actionLabel}, but its model catalog refresh timed out; using cached models.` });
|
|
1776
|
-
}
|
|
1777
|
-
else if (isRecord(result) && result.errors instanceof Map && result.errors.size > 0) {
|
|
1778
|
-
publish?.({ kind: "warning", message: `${actionLabel}, but its model catalog could not be refreshed; using cached models.` });
|
|
1779
|
-
}
|
|
1780
|
-
this.#reconcileActiveModelAvailability();
|
|
1781
|
-
this.#emitView();
|
|
1782
|
-
})
|
|
1783
|
-
.catch(error => {
|
|
1784
|
-
if (this.#disposed || this.#sessionGeneration !== generation)
|
|
1785
|
-
return;
|
|
1786
|
-
publish?.({
|
|
1787
|
-
kind: "warning",
|
|
1788
|
-
message: controller.signal.aborted
|
|
1789
|
-
? `${actionLabel}, but its model catalog refresh timed out; using cached models.`
|
|
1790
|
-
: `${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1791
|
-
});
|
|
1792
|
-
})
|
|
1793
|
-
.finally(() => clearTimeout(timeout));
|
|
1794
|
-
}
|
|
1795
|
-
#requireWorkflowSession() {
|
|
1796
|
-
if (this.#disposed || !this.#runtime || !this.#session)
|
|
1797
|
-
throw new Error("engine adapter is not running");
|
|
1798
|
-
return this.#session;
|
|
1799
|
-
}
|
|
1800
|
-
#modelOptions() {
|
|
1801
|
-
const runtime = this.#runtime;
|
|
1802
|
-
if (!runtime)
|
|
1803
|
-
return [];
|
|
1804
|
-
const models = runtime.services.modelRuntime.getAvailableSnapshot?.();
|
|
1805
|
-
if (!Array.isArray(models)) {
|
|
1806
|
-
const active = this.#activeModel;
|
|
1807
|
-
return active ? [{ id: `${active.providerId}/${active.modelId}`, label: active.displayName, description: `${active.providerId}/${active.modelId}` }] : [];
|
|
1808
|
-
}
|
|
1809
|
-
return models.filter(isRecord).flatMap(model => {
|
|
1810
|
-
const provider = stringProperty(model, "provider");
|
|
1811
|
-
const id = stringProperty(model, "id");
|
|
1812
|
-
if (!provider || !id)
|
|
1813
|
-
return [];
|
|
1814
|
-
return [{ id: `${provider}/${id}`, label: stringProperty(model, "name") ?? id, description: `${provider}/${id}` }];
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
#loginOptions(authType) {
|
|
1818
|
-
const runtime = this.#runtime;
|
|
1819
|
-
const modelRuntime = runtime?.services.modelRuntime;
|
|
1820
|
-
if (!modelRuntime)
|
|
1821
|
-
return [];
|
|
1822
|
-
const providers = modelRuntime.getProviders?.();
|
|
1823
|
-
if (!Array.isArray(providers))
|
|
1824
|
-
return [];
|
|
1825
|
-
return providers.filter(isRecord).flatMap(provider => {
|
|
1826
|
-
const id = stringProperty(provider, "id");
|
|
1827
|
-
if (!id)
|
|
1828
|
-
return [];
|
|
1829
|
-
const name = stringProperty(provider, "name") ?? id;
|
|
1830
|
-
const auth = isRecord(provider.auth) ? provider.auth : {};
|
|
1831
|
-
const authStatus = modelRuntime.getProviderAuthStatus?.(id);
|
|
1832
|
-
const source = authStatus?.label ?? authStatus?.source;
|
|
1833
|
-
const status = authStatus?.configured === true
|
|
1834
|
-
? {
|
|
1835
|
-
type: modelRuntime.isUsingOAuth?.(id) === true ? "oauth" : "api_key",
|
|
1836
|
-
...(source === undefined ? {} : { source }),
|
|
1837
|
-
}
|
|
1838
|
-
: undefined;
|
|
1839
|
-
return [
|
|
1840
|
-
...(authType !== "api_key" && auth.oauth ? [{
|
|
1841
|
-
id: `oauth:${id}`,
|
|
1842
|
-
providerId: id,
|
|
1843
|
-
label: name,
|
|
1844
|
-
description: "Account / OAuth",
|
|
1845
|
-
authType: "oauth",
|
|
1846
|
-
...(status === undefined ? {} : { status }),
|
|
1847
|
-
}] : []),
|
|
1848
|
-
...(authType !== "oauth" && auth.apiKey ? [{
|
|
1849
|
-
id: `api_key:${id}`,
|
|
1850
|
-
providerId: id,
|
|
1851
|
-
label: name,
|
|
1852
|
-
description: "API key",
|
|
1853
|
-
authType: "api_key",
|
|
1854
|
-
...(status === undefined ? {} : { status }),
|
|
1855
|
-
}] : []),
|
|
1856
|
-
];
|
|
1857
|
-
}).sort((left, right) => left.label.localeCompare(right.label));
|
|
1858
|
-
}
|
|
1859
|
-
async #logoutOptions() {
|
|
1860
|
-
const runtime = this.#runtime;
|
|
1861
|
-
if (!runtime)
|
|
1862
|
-
return [];
|
|
1863
|
-
const modelRuntime = runtime.services.modelRuntime;
|
|
1864
|
-
const credentials = await requireCapability(modelRuntime.listCredentials, "listCredentials").call(modelRuntime, { signal: AbortSignal.timeout(15_000) });
|
|
1865
|
-
if (!Array.isArray(credentials))
|
|
1866
|
-
return [];
|
|
1867
|
-
return credentials.filter(isRecord).flatMap(credential => {
|
|
1868
|
-
const providerId = stringProperty(credential, "providerId");
|
|
1869
|
-
if (!providerId)
|
|
1870
|
-
return [];
|
|
1871
|
-
const credentialType = stringProperty(credential, "type") === "api_key" ? "api_key" : "oauth";
|
|
1872
|
-
const provider = modelRuntime.getProvider?.(providerId);
|
|
1873
|
-
return [{
|
|
1874
|
-
id: `${credentialType}:${providerId}`,
|
|
1875
|
-
providerId,
|
|
1876
|
-
label: stringProperty(provider, "name") ?? providerId,
|
|
1877
|
-
description: credentialType,
|
|
1878
|
-
authType: credentialType,
|
|
1879
|
-
status: { type: credentialType, source: "stored credential" },
|
|
1880
|
-
}];
|
|
1881
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
async #sessionOptions() {
|
|
1884
|
-
const session = this.#requireWorkflowSession();
|
|
1885
|
-
const manager = session.sessionManager;
|
|
1886
|
-
const cwd = manager?.getCwd?.();
|
|
1887
|
-
const sessionDir = manager?.getSessionDir?.();
|
|
1888
|
-
if (typeof cwd !== "string")
|
|
1889
|
-
return [];
|
|
1890
|
-
return sessionInfoOptions(await SessionManager.list(cwd, typeof sessionDir === "string" ? sessionDir : undefined));
|
|
1891
|
-
}
|
|
1892
|
-
async #applyPinnedSetting(selection, selectedValue, hasSelectedValue = false) {
|
|
1893
|
-
if (!PINNED_PI_SETTINGS_CALLBACKS.includes(selection)) {
|
|
1894
|
-
return workflowResult("settings", "failed", `Unknown setting callback: ${selection}`);
|
|
1895
|
-
}
|
|
1896
|
-
const callback = selection;
|
|
1897
|
-
if (callback === "onCancel")
|
|
1898
|
-
return workflowResult("settings", "cancelled", "Settings cancelled");
|
|
1899
|
-
if (callback === "onThemePreview")
|
|
1900
|
-
return workflowResult("settings", "completed", "Theme preview refreshed");
|
|
1901
|
-
if (!hasSelectedValue) {
|
|
1902
|
-
const snapshot = this.pinnedSettingsSnapshot();
|
|
1903
|
-
const currentValues = {
|
|
1904
|
-
onAutoCompactChange: snapshot.autoCompact,
|
|
1905
|
-
onShowImagesChange: snapshot.showImages,
|
|
1906
|
-
onImageWidthCellsChange: snapshot.imageWidthCells,
|
|
1907
|
-
onAutoResizeImagesChange: snapshot.autoResizeImages,
|
|
1908
|
-
onBlockImagesChange: snapshot.blockImages,
|
|
1909
|
-
onEnableSkillCommandsChange: snapshot.enableSkillCommands,
|
|
1910
|
-
onSteeringModeChange: snapshot.steeringMode,
|
|
1911
|
-
onFollowUpModeChange: snapshot.followUpMode,
|
|
1912
|
-
onTransportChange: snapshot.transport,
|
|
1913
|
-
onHttpIdleTimeoutMsChange: snapshot.httpIdleTimeoutMs,
|
|
1914
|
-
onThinkingLevelChange: snapshot.thinkingLevel,
|
|
1915
|
-
onThemeChange: snapshot.currentTheme,
|
|
1916
|
-
onHideThinkingBlockChange: snapshot.hideThinkingBlock,
|
|
1917
|
-
onMermaidRenderingModeChange: snapshot.mermaidRenderingMode,
|
|
1918
|
-
onShowCacheMissNoticesChange: snapshot.showCacheMissNotices,
|
|
1919
|
-
onCollapseChangelogChange: snapshot.collapseChangelog,
|
|
1920
|
-
onEnableInstallTelemetryChange: snapshot.enableInstallTelemetry,
|
|
1921
|
-
onQuietStartupChange: snapshot.quietStartup,
|
|
1922
|
-
onDefaultProjectTrustChange: snapshot.defaultProjectTrust,
|
|
1923
|
-
onDoubleEscapeActionChange: snapshot.doubleEscapeAction,
|
|
1924
|
-
onTreeFilterModeChange: snapshot.treeFilterMode,
|
|
1925
|
-
onShowHardwareCursorChange: snapshot.showHardwareCursor,
|
|
1926
|
-
onEditorPaddingXChange: snapshot.editorPaddingX,
|
|
1927
|
-
onOutputPadChange: snapshot.outputPad,
|
|
1928
|
-
onAutocompleteMaxVisibleChange: snapshot.autocompleteMaxVisible,
|
|
1929
|
-
onClearOnShrinkChange: snapshot.clearOnShrink,
|
|
1930
|
-
onShowTerminalProgressChange: snapshot.showTerminalProgress,
|
|
1931
|
-
onTuiModeChange: snapshot.tuiMode,
|
|
1932
|
-
onFullscreenExitOutputChange: snapshot.fullscreenExitOutput,
|
|
1933
|
-
onFullscreenScrollbarChange: snapshot.fullscreenScrollbar,
|
|
1934
|
-
onWarningsChange: snapshot.warnings,
|
|
1935
|
-
};
|
|
1936
|
-
selectedValue = currentValues[callback];
|
|
1937
|
-
hasSelectedValue = true;
|
|
1938
|
-
}
|
|
1939
|
-
const port = this.settingsPort();
|
|
1940
|
-
if (port === null)
|
|
1941
|
-
return workflowResult("settings", "failed", "Settings are unavailable");
|
|
1942
|
-
const key = settingKeyForCallback(callback);
|
|
1943
|
-
if (key === null)
|
|
1944
|
-
return workflowResult("settings", "failed", `${settingLabel(callback)} is unavailable in this runtime`);
|
|
1945
|
-
const result = await port.writeSetting(key, agentJsonValue(selectedValue));
|
|
1946
|
-
if (result.status === "failed" || result.status === "unavailable") {
|
|
1947
|
-
return workflowResult("settings", "failed", result.failure ?? result.limitationReason ?? `${settingLabel(callback)} is unavailable`);
|
|
1948
|
-
}
|
|
1949
|
-
this.#emitView();
|
|
1950
|
-
const suffix = result.status === "deferred" ? ` (${result.application})` : "";
|
|
1951
|
-
return workflowResult("settings", "completed", `${settingLabel(callback)}: ${String(selectedValue)}${suffix}`);
|
|
1952
|
-
}
|
|
1953
|
-
async #perform(command) {
|
|
1954
|
-
const runtime = this.#runtime;
|
|
1955
|
-
const session = this.#session;
|
|
1956
|
-
const generation = this.#sessionGeneration;
|
|
1957
|
-
if (!runtime || !session)
|
|
1958
|
-
throw new Error("engine session is unavailable");
|
|
1959
|
-
switch (command.type) {
|
|
1960
|
-
case "prompt":
|
|
1961
|
-
case "steer":
|
|
1962
|
-
case "follow-up":
|
|
1963
|
-
case "abort":
|
|
1964
|
-
case "retry":
|
|
1965
|
-
case "compact": {
|
|
1966
|
-
const result = await this.#sessionCommands?.execute(command.type === "prompt" || command.type === "steer" || command.type === "follow-up"
|
|
1967
|
-
? {
|
|
1968
|
-
type: command.type,
|
|
1969
|
-
text: command.text,
|
|
1970
|
-
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
1971
|
-
}
|
|
1972
|
-
: { type: command.type });
|
|
1973
|
-
if (!result || result.outcome === "rejected" || result.outcome === "failed") {
|
|
1974
|
-
throw new Error(command.type === "retry" ? "no previous prompt is available to retry" : `Pi session command failed: ${command.type}`);
|
|
1975
|
-
}
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
case "set-model": {
|
|
1979
|
-
const model = runtime.services.modelRuntime.getModel(command.model.providerId, command.model.modelId);
|
|
1980
|
-
if (!model) {
|
|
1981
|
-
throw new Error(`model is unavailable: ${command.model.providerId}/${command.model.modelId}`);
|
|
1982
|
-
}
|
|
1983
|
-
await session.setModel(model);
|
|
1984
|
-
if (generation === this.#sessionGeneration && this.#overload === undefined && !this.#admissionStopped)
|
|
1985
|
-
this.#activeModel = { ...command.model };
|
|
1986
|
-
return;
|
|
506
|
+
await session.setModel(model);
|
|
507
|
+
if (generation === this.#engine.generation && !this.#delivery.overloaded && !this.#admissionStopped)
|
|
508
|
+
this.#activeModel = { ...command.model };
|
|
509
|
+
return;
|
|
1987
510
|
}
|
|
1988
511
|
case "set-thinking-level":
|
|
1989
512
|
session.setThinkingLevel(command.thinkingLevel);
|
|
@@ -2008,24 +531,10 @@ export class PiEngineAdapter {
|
|
|
2008
531
|
throw new Error("owned UI state commands belong to the owned UI layer, not the Pi engine adapter");
|
|
2009
532
|
}
|
|
2010
533
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
pending.cancel();
|
|
2016
|
-
}
|
|
2017
|
-
this.#sessionGeneration += 1;
|
|
2018
|
-
this.#sessionBindingGeneration += 1;
|
|
2019
|
-
this.#invalidatedEvents += this.#eventQueue.discardObsolete(this.#sessionGeneration);
|
|
2020
|
-
this.#session = session;
|
|
2021
|
-
this.#activeCommandIds = [];
|
|
2022
|
-
this.#completedCommands.clear();
|
|
2023
|
-
this.#sessionCommands = new PiSessionCommandIntegration(session);
|
|
2024
|
-
this.#compactionProgress?.dispose();
|
|
2025
|
-
this.#compactionProgress = observeCompactionProgress(session, percent => {
|
|
2026
|
-
if (this.#session === session && !this.#disposed)
|
|
2027
|
-
this.#publishCompactionProgress(percent);
|
|
2028
|
-
});
|
|
534
|
+
// Invariant: the runtime already advanced the generation and subscribed; this rebuilds what the view derives from a session.
|
|
535
|
+
#sessionReplaced(session) {
|
|
536
|
+
this.#delivery.discardObsolete(this.#engine.generation);
|
|
537
|
+
this.#commands.reset();
|
|
2029
538
|
this.#editor = {
|
|
2030
539
|
text: "",
|
|
2031
540
|
queuedSubmissions: [],
|
|
@@ -2035,33 +544,25 @@ export class PiEngineAdapter {
|
|
|
2035
544
|
submitEnabled: true,
|
|
2036
545
|
};
|
|
2037
546
|
this.#status = { ...this.#status, workingMessage: null, workingProgress: null, badges: [] };
|
|
2038
|
-
this.#
|
|
2039
|
-
this.#agentRunActive = false;
|
|
2040
|
-
this.#agentRunSequence = 0;
|
|
2041
|
-
this.#assistantResponseSequence = 0;
|
|
547
|
+
this.#events.reset();
|
|
2042
548
|
this.#activeModel = readModel(session.model);
|
|
2043
549
|
this.#reconcileActiveModelAvailability();
|
|
2044
550
|
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
2045
|
-
this.#
|
|
551
|
+
this.#projection.assets.clear();
|
|
2046
552
|
// Invariant: a new binding cannot inherit arguments/results from a reused invocation id.
|
|
2047
553
|
this.#setTranscript([]);
|
|
2048
|
-
this.#
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
if (generation === this.#sessionGeneration && !this.#disposed)
|
|
2052
|
-
this.#handlePiEvent(event);
|
|
2053
|
-
});
|
|
2054
|
-
if (this.#extensionUi !== undefined)
|
|
2055
|
-
void this.#bindExtensionUiToSession();
|
|
554
|
+
this.#setTranscript(this.#projection.rebuild(session.messages, "finalized"));
|
|
555
|
+
if (this.#extensions.attached)
|
|
556
|
+
void this.#extensions.rebind();
|
|
2056
557
|
}
|
|
2057
558
|
#reconcileActiveModelAvailability() {
|
|
2058
|
-
const modelRuntime = this.#runtime?.services.modelRuntime;
|
|
559
|
+
const modelRuntime = this.#engine.runtime?.services.modelRuntime;
|
|
2059
560
|
if (!modelRuntime) {
|
|
2060
561
|
this.#activeModel = null;
|
|
2061
562
|
return;
|
|
2062
563
|
}
|
|
2063
564
|
const available = modelRuntime.getAvailableSnapshot?.() ?? [];
|
|
2064
|
-
const sessionModel = readModel(this.#session?.model);
|
|
565
|
+
const sessionModel = readModel(this.#engine.session?.model);
|
|
2065
566
|
const authoritative = this.#activeModel ?? sessionModel;
|
|
2066
567
|
if (authoritative === null)
|
|
2067
568
|
return;
|
|
@@ -2069,694 +570,13 @@ export class PiEngineAdapter {
|
|
|
2069
570
|
&& stringProperty(model, "id") === authoritative.modelId);
|
|
2070
571
|
this.#activeModel = remainsAvailable ? authoritative : null;
|
|
2071
572
|
}
|
|
2072
|
-
async #bindExtensionUiToSession() {
|
|
2073
|
-
const session = this.#session;
|
|
2074
|
-
const ui = this.#extensionUi;
|
|
2075
|
-
if (session === undefined || ui === undefined || session.bindExtensions === undefined) {
|
|
2076
|
-
this.#extensionBound = false;
|
|
2077
|
-
return;
|
|
2078
|
-
}
|
|
2079
|
-
try {
|
|
2080
|
-
await session.bindExtensions({
|
|
2081
|
-
uiContext: ui,
|
|
2082
|
-
mode: "tui",
|
|
2083
|
-
shutdownHandler: () => this.#extensionShutdown?.(),
|
|
2084
|
-
onError: error => {
|
|
2085
|
-
const message = isRecord(error) && typeof error.error === "string"
|
|
2086
|
-
? error.error
|
|
2087
|
-
: error instanceof Error ? error.message : String(error);
|
|
2088
|
-
this.#addDiagnostic("warning", "extension-ui", message, true);
|
|
2089
|
-
this.#emitView();
|
|
2090
|
-
},
|
|
2091
|
-
});
|
|
2092
|
-
this.#extensionBound = true;
|
|
2093
|
-
this.#emitView();
|
|
2094
|
-
}
|
|
2095
|
-
catch (error) {
|
|
2096
|
-
this.#extensionBound = false;
|
|
2097
|
-
this.#addDiagnostic("error", "extension-ui-bind", error instanceof Error ? error.message : String(error), true);
|
|
2098
|
-
this.#emitView();
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
// Performance: replacing the transcript rebuilds its index so block lookup stays constant-time.
|
|
2102
573
|
#setTranscript(blocks) {
|
|
2103
574
|
// Invariant: lazy delivery snapshots must freeze before their authoritative source can be removed.
|
|
2104
|
-
if (!this.#
|
|
2105
|
-
this.#beginOverload();
|
|
2106
|
-
return;
|
|
2107
|
-
}
|
|
2108
|
-
for (const block of blocks)
|
|
2109
|
-
this.#transcriptImageAssets.retain(block);
|
|
2110
|
-
for (const block of this.#transcript)
|
|
2111
|
-
this.#transcriptImageAssets.release(block);
|
|
2112
|
-
this.#transcript = blocks;
|
|
2113
|
-
this.#transcriptIndex.clear();
|
|
2114
|
-
for (const [index, block] of blocks.entries())
|
|
2115
|
-
this.#transcriptIndex.set(block.id, index);
|
|
2116
|
-
this.#transcriptImageAssets.discardUnowned();
|
|
2117
|
-
this.#transcriptSnapshot = undefined;
|
|
2118
|
-
}
|
|
2119
|
-
#transcriptBlock(id) {
|
|
2120
|
-
const index = this.#transcriptIndex.get(id);
|
|
2121
|
-
return index === undefined ? undefined : this.#transcript[index];
|
|
2122
|
-
}
|
|
2123
|
-
// Invariant: the named work state is the only state a matching end may clear.
|
|
2124
|
-
#enterWorkState(kind, message) {
|
|
2125
|
-
const wasBusy = this.#lifecycle === "busy";
|
|
2126
|
-
this.#statusKind = kind;
|
|
2127
|
-
this.#lifecycle = "busy";
|
|
2128
|
-
this.#status = { ...this.#status, workingMessage: message, workingProgress: null };
|
|
2129
|
-
if (!wasBusy)
|
|
2130
|
-
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
2131
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
2132
|
-
}
|
|
2133
|
-
// Invariant: ending retry or compaction cannot clear a different active work state.
|
|
2134
|
-
#endWorkState(kind) {
|
|
2135
|
-
if (this.#statusKind !== kind)
|
|
2136
|
-
return;
|
|
2137
|
-
if (this.#agentRunActive) {
|
|
2138
|
-
this.#enterWorkState("working", "Working");
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
this.#leaveWorkStates();
|
|
2142
|
-
}
|
|
2143
|
-
#leaveWorkStates() {
|
|
2144
|
-
this.#statusKind = null;
|
|
2145
|
-
this.#lifecycle = "ready";
|
|
2146
|
-
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
2147
|
-
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
2148
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
2149
|
-
}
|
|
2150
|
-
// Invariant: progress belongs to the compaction state only and is published when the integer changes.
|
|
2151
|
-
#publishCompactionProgress(percent) {
|
|
2152
|
-
if (this.#statusKind !== "compaction" || this.#status.workingProgress === percent)
|
|
2153
|
-
return;
|
|
2154
|
-
this.#status = { ...this.#status, workingProgress: percent };
|
|
2155
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
2156
|
-
}
|
|
2157
|
-
// Rationale: a manual compaction ends with an idle session, so the engine would never consume
|
|
2158
|
-
// the messages queued during it; automatic compaction returns to a run or a pending prompt.
|
|
2159
|
-
#deliverQueuedAfterCompaction() {
|
|
2160
|
-
const commands = this.#sessionCommands;
|
|
2161
|
-
const generation = this.#sessionGeneration;
|
|
2162
|
-
if (commands === undefined)
|
|
575
|
+
if (!this.#delivery.seal()) {
|
|
576
|
+
this.#delivery.beginOverload();
|
|
2163
577
|
return;
|
|
2164
|
-
const report = (error) => {
|
|
2165
|
-
if (generation !== this.#sessionGeneration || this.#disposed)
|
|
2166
|
-
return;
|
|
2167
|
-
this.#addDiagnostic("error", "engine-command", `Queued input could not be sent after compaction: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
2168
|
-
this.#emitView();
|
|
2169
|
-
};
|
|
2170
|
-
commands.deliverQueuedAfterCompaction(report).catch(report);
|
|
2171
|
-
}
|
|
2172
|
-
#handlePiEvent(event) {
|
|
2173
|
-
try {
|
|
2174
|
-
this.#applyPiEvent(event);
|
|
2175
|
-
}
|
|
2176
|
-
finally {
|
|
2177
|
-
this.#transcriptImageAssets.discardUnowned();
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
#applyPiEvent(event) {
|
|
2181
|
-
if (!isRecord(event) || typeof event.type !== "string")
|
|
2182
|
-
return;
|
|
2183
|
-
// Invariant: usage moves at message and lifecycle boundaries, not with stream chunks, so the
|
|
2184
|
-
// two streaming event kinds keep the memo and everything else drops it.
|
|
2185
|
-
if (event.type !== "message_update" && event.type !== "tool_execution_update")
|
|
2186
|
-
this.#usageCache = undefined;
|
|
2187
|
-
switch (event.type) {
|
|
2188
|
-
case "agent_start":
|
|
2189
|
-
this.#agentRunActive = true;
|
|
2190
|
-
this.#agentRunSequence += 1;
|
|
2191
|
-
this.#emitEvent({ type: "agent-run-started" });
|
|
2192
|
-
this.#enterWorkState("working", "Working");
|
|
2193
|
-
return;
|
|
2194
|
-
case "message_start":
|
|
2195
|
-
this.#upsertMessageBlock(event.message, "live");
|
|
2196
|
-
return;
|
|
2197
|
-
case "message_update": {
|
|
2198
|
-
const delta = isRecord(event.assistantMessageEvent) && typeof event.assistantMessageEvent.delta === "string"
|
|
2199
|
-
? event.assistantMessageEvent.delta
|
|
2200
|
-
: undefined;
|
|
2201
|
-
// Invariant: the delta is folded in before the block is stored, so a chunk is one update to
|
|
2202
|
-
// one block rather than a store without the delta followed by a store with it.
|
|
2203
|
-
const blocks = this.#messageBlocks(event.message, "live", this.#transcript.length);
|
|
2204
|
-
for (const [index, block] of blocks.entries()) {
|
|
2205
|
-
this.#upsertTranscriptBlock(index === 0 && delta !== undefined && !block.text.endsWith(delta)
|
|
2206
|
-
? { ...block, text: `${block.text}${delta}` }
|
|
2207
|
-
: block);
|
|
2208
|
-
}
|
|
2209
|
-
return;
|
|
2210
|
-
}
|
|
2211
|
-
case "message_end":
|
|
2212
|
-
this.#upsertMessageBlock(event.message, "finalized");
|
|
2213
|
-
this.#settleFailedDeclarations(event.message);
|
|
2214
|
-
// Compatibility: preserve the same semantic boundary v2 counted. Transcript block
|
|
2215
|
-
// finalization is intentionally not a substitute: rebuilds, retries,
|
|
2216
|
-
// thinking parts, and tool rows can all finalize independently.
|
|
2217
|
-
if (isRecord(event.message) && event.message.role === "assistant") {
|
|
2218
|
-
this.#assistantResponseSequence += 1;
|
|
2219
|
-
const content = Array.isArray(event.message.content) ? event.message.content : [];
|
|
2220
|
-
const stopReason = stringValue(event.message.stopReason) ?? null;
|
|
2221
|
-
const toolContinuation = stopReason === "toolUse"
|
|
2222
|
-
|| content.some(item => isRecord(item) && item.type === "toolCall");
|
|
2223
|
-
const successful = stringValue(event.message.errorMessage) === undefined
|
|
2224
|
-
&& stopReason !== "error"
|
|
2225
|
-
&& stopReason !== "aborted"
|
|
2226
|
-
&& textFromContent(content).trim().length > 0;
|
|
2227
|
-
this.#emitEvent({
|
|
2228
|
-
type: "assistant-message-completed",
|
|
2229
|
-
sessionGeneration: this.#sessionGeneration,
|
|
2230
|
-
runSequence: this.#agentRunSequence,
|
|
2231
|
-
responseSequence: this.#assistantResponseSequence,
|
|
2232
|
-
model: this.#activeModel,
|
|
2233
|
-
assistantMessageCount: this.#transcript.filter(block => block.kind === "assistant").length,
|
|
2234
|
-
successful,
|
|
2235
|
-
stopReason,
|
|
2236
|
-
toolContinuation,
|
|
2237
|
-
});
|
|
2238
|
-
}
|
|
2239
|
-
return;
|
|
2240
|
-
case "turn_end":
|
|
2241
|
-
this.#upsertMessageBlock(event.message, "finalized");
|
|
2242
|
-
if (Array.isArray(event.toolResults)) {
|
|
2243
|
-
for (const result of event.toolResults)
|
|
2244
|
-
this.#upsertMessageBlock(result, "finalized");
|
|
2245
|
-
}
|
|
2246
|
-
return;
|
|
2247
|
-
case "tool_execution_start":
|
|
2248
|
-
case "tool_execution_end": {
|
|
2249
|
-
this.#upsertToolExecutionBlock(event);
|
|
2250
|
-
return;
|
|
2251
|
-
}
|
|
2252
|
-
case "tool_execution_update":
|
|
2253
|
-
this.#upsertToolExecutionBlock(event);
|
|
2254
|
-
return;
|
|
2255
|
-
case "agent_settled":
|
|
2256
|
-
case "agent_end": {
|
|
2257
|
-
if (event.type === "agent_end" && event.willRetry === true)
|
|
2258
|
-
return;
|
|
2259
|
-
// Protocol: agent_end is run-local; only settlement reads the complete session scope.
|
|
2260
|
-
const finalMessages = event.type === "agent_settled"
|
|
2261
|
-
? this.#session?.messages ?? []
|
|
2262
|
-
: Array.isArray(event.messages) ? event.messages : [];
|
|
2263
|
-
if (event.type === "agent_end")
|
|
2264
|
-
this.#mergeRunTranscript(finalMessages);
|
|
2265
|
-
else if (finalMessages.length > 0)
|
|
2266
|
-
this.#rebuildTranscript(finalMessages, "finalized");
|
|
2267
|
-
// Invariant: missing final messages must not erase accumulated content or invent tool outcomes.
|
|
2268
|
-
if (finalMessages.length === 0)
|
|
2269
|
-
this.#setTranscript(this.#transcript.map(block => block.status === "live" && transcriptToolState(block) === undefined
|
|
2270
|
-
? { ...block, status: "finalized", revision: block.revision + 1 } : block));
|
|
2271
|
-
// Compatibility: ending a turn leaves the working state, as the recorded pinned baseline does, but
|
|
2272
|
-
// it leaves only that state: a compaction or retry being shown outlives the turn
|
|
2273
|
-
// that ended under it. Settlement ends the run, and with it every state — the
|
|
2274
|
-
// engine ends a turn for each continuation it makes and settles once.
|
|
2275
|
-
if (event.type === "agent_settled") {
|
|
2276
|
-
this.#agentRunActive = false;
|
|
2277
|
-
this.#leaveWorkStates();
|
|
2278
|
-
}
|
|
2279
|
-
else if (this.#statusKind === null || this.#statusKind === "working") {
|
|
2280
|
-
this.#leaveWorkStates();
|
|
2281
|
-
}
|
|
2282
|
-
this.#emitView();
|
|
2283
|
-
if (event.type === "agent_settled") {
|
|
2284
|
-
const assistants = finalMessages.filter(message => isRecord(message) && message.role === "assistant");
|
|
2285
|
-
const lastAssistant = assistants.at(-1);
|
|
2286
|
-
const successful = lastAssistant !== undefined
|
|
2287
|
-
&& stringValue(lastAssistant.errorMessage) === undefined
|
|
2288
|
-
&& stringValue(lastAssistant.stopReason) !== "error"
|
|
2289
|
-
&& stringValue(lastAssistant.stopReason) !== "aborted";
|
|
2290
|
-
this.#emitEvent({
|
|
2291
|
-
type: "agent-run-settled",
|
|
2292
|
-
sessionGeneration: this.#sessionGeneration,
|
|
2293
|
-
runSequence: this.#agentRunSequence,
|
|
2294
|
-
responseSequence: this.#assistantResponseSequence,
|
|
2295
|
-
model: this.#activeModel,
|
|
2296
|
-
assistantMessageCount: assistants.length,
|
|
2297
|
-
successful,
|
|
2298
|
-
});
|
|
2299
|
-
}
|
|
2300
|
-
return;
|
|
2301
|
-
}
|
|
2302
|
-
case "queue_update": {
|
|
2303
|
-
const steering = readStringArray(event.steering);
|
|
2304
|
-
const followUp = readStringArray(event.followUp);
|
|
2305
|
-
this.#editor = {
|
|
2306
|
-
...this.#editor,
|
|
2307
|
-
queuedSubmissions: [...steering, ...followUp],
|
|
2308
|
-
historyRevision: this.#editor.historyRevision + 1,
|
|
2309
|
-
};
|
|
2310
|
-
this.#emitEvent({ type: "editor-state", editor: this.#editor });
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
case "auto_retry_start":
|
|
2314
|
-
this.#enterWorkState("retry", "Retrying");
|
|
2315
|
-
return;
|
|
2316
|
-
case "auto_retry_end":
|
|
2317
|
-
this.#endWorkState("retry");
|
|
2318
|
-
return;
|
|
2319
|
-
case "compaction_start":
|
|
2320
|
-
this.#enterWorkState("compaction", "Compacting");
|
|
2321
|
-
this.#compactionProgress?.begin();
|
|
2322
|
-
return;
|
|
2323
|
-
case "compaction_end":
|
|
2324
|
-
this.#compactionProgress?.end();
|
|
2325
|
-
this.#endWorkState("compaction");
|
|
2326
|
-
if (event.reason === "manual")
|
|
2327
|
-
this.#deliverQueuedAfterCompaction();
|
|
2328
|
-
return;
|
|
2329
|
-
case "thinking_level_changed":
|
|
2330
|
-
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
2331
|
-
return;
|
|
2332
|
-
default:
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
#readUsage() {
|
|
2337
|
-
let input = 0;
|
|
2338
|
-
let output = 0;
|
|
2339
|
-
let cacheRead = 0;
|
|
2340
|
-
let cacheWrite = 0;
|
|
2341
|
-
let cost = 0;
|
|
2342
|
-
let latestCacheHitRate = null;
|
|
2343
|
-
let latestPrompt = null;
|
|
2344
|
-
const entries = this.#session?.sessionManager?.getEntries?.()
|
|
2345
|
-
?? (this.#session?.messages ?? []).map(message => ({ type: "message", message }));
|
|
2346
|
-
for (const entry of entries) {
|
|
2347
|
-
if (!isRecord(entry))
|
|
2348
|
-
continue;
|
|
2349
|
-
const message = entry.type === "message" && isRecord(entry.message) ? entry.message : undefined;
|
|
2350
|
-
const usage = message !== undefined && isRecord(message.usage)
|
|
2351
|
-
? message.usage
|
|
2352
|
-
: (entry.type === "branch_summary" || entry.type === "compaction") && isRecord(entry.usage) ? entry.usage : undefined;
|
|
2353
|
-
if (usage === undefined)
|
|
2354
|
-
continue;
|
|
2355
|
-
input += finiteNumber(usage.input);
|
|
2356
|
-
output += finiteNumber(usage.output);
|
|
2357
|
-
cacheRead += finiteNumber(usage.cacheRead);
|
|
2358
|
-
cacheWrite += finiteNumber(usage.cacheWrite);
|
|
2359
|
-
cost += isRecord(usage.cost) ? finiteNumber(usage.cost.total) : 0;
|
|
2360
|
-
if (message?.role === "assistant") {
|
|
2361
|
-
latestPrompt = {
|
|
2362
|
-
input: finiteNumber(usage.input),
|
|
2363
|
-
cacheRead: finiteNumber(usage.cacheRead),
|
|
2364
|
-
cacheWrite: finiteNumber(usage.cacheWrite),
|
|
2365
|
-
};
|
|
2366
|
-
const promptTokens = latestPrompt.input + latestPrompt.cacheRead + latestPrompt.cacheWrite;
|
|
2367
|
-
latestCacheHitRate = promptTokens > 0 ? (latestPrompt.cacheRead / promptTokens) * 100 : null;
|
|
2368
|
-
}
|
|
2369
578
|
}
|
|
2370
|
-
|
|
2371
|
-
const providerId = this.#activeModel?.providerId;
|
|
2372
|
-
const usingSubscription = providerId === "kimi-coding"
|
|
2373
|
-
|| (providerId !== undefined && this.#runtime?.services.modelRuntime.isUsingSubscription?.(providerId) === true);
|
|
2374
|
-
return {
|
|
2375
|
-
input,
|
|
2376
|
-
output,
|
|
2377
|
-
cacheRead,
|
|
2378
|
-
cacheWrite,
|
|
2379
|
-
cost,
|
|
2380
|
-
latestCacheHitRate,
|
|
2381
|
-
latestPrompt,
|
|
2382
|
-
contextAvailable: context !== undefined,
|
|
2383
|
-
contextTokens: context?.tokens ?? null,
|
|
2384
|
-
contextWindow: context?.contextWindow ?? 0,
|
|
2385
|
-
contextPercent: context?.percent ?? null,
|
|
2386
|
-
usingSubscription,
|
|
2387
|
-
autoCompactEnabled: this.#runtime?.services.settingsManager?.getCompactionEnabled?.() ?? true,
|
|
2388
|
-
};
|
|
2389
|
-
}
|
|
2390
|
-
// Invariant: run-local completion may restate messages, but never owns transcript membership.
|
|
2391
|
-
#mergeRunTranscript(messages) {
|
|
2392
|
-
const positions = new Map();
|
|
2393
|
-
const sessionOccurrences = new Map();
|
|
2394
|
-
for (const [index, message] of (this.#session?.messages ?? []).entries()) {
|
|
2395
|
-
const key = messageFallbackKey(message, index);
|
|
2396
|
-
const occurrence = sessionOccurrences.get(key) ?? 0;
|
|
2397
|
-
sessionOccurrences.set(key, occurrence + 1);
|
|
2398
|
-
positions.set(message, { index, occurrence });
|
|
2399
|
-
}
|
|
2400
|
-
const runOccurrences = new Map();
|
|
2401
|
-
for (const [index, message] of messages.entries()) {
|
|
2402
|
-
const key = messageFallbackKey(message, index);
|
|
2403
|
-
const occurrence = runOccurrences.get(key) ?? 0;
|
|
2404
|
-
runOccurrences.set(key, occurrence + 1);
|
|
2405
|
-
const position = positions.get(message) ?? { index, occurrence };
|
|
2406
|
-
for (const block of this.#messageBlocks(message, "finalized", position.index, position.occurrence)) {
|
|
2407
|
-
this.#upsertTranscriptBlock(block);
|
|
2408
|
-
}
|
|
2409
|
-
this.#settleFailedDeclarations(message);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
// Invariant: full replacement is reserved for session-authoritative scope (bind/settlement).
|
|
2413
|
-
#rebuildTranscript(messages, status) {
|
|
2414
|
-
const blocks = [];
|
|
2415
|
-
const blockIndexes = new Map();
|
|
2416
|
-
const occurrences = new Map();
|
|
2417
|
-
for (const [index, message] of messages.entries()) {
|
|
2418
|
-
const key = messageFallbackKey(message, index);
|
|
2419
|
-
const occurrence = occurrences.get(key) ?? 0;
|
|
2420
|
-
occurrences.set(key, occurrence + 1);
|
|
2421
|
-
for (const block of this.#messageBlocks(message, status, index, occurrence, id => blocks[blockIndexes.get(id) ?? -1] ?? this.#transcriptBlock(id))) {
|
|
2422
|
-
const existingIndex = blockIndexes.get(block.id);
|
|
2423
|
-
if (existingIndex === undefined) {
|
|
2424
|
-
blockIndexes.set(block.id, blocks.length);
|
|
2425
|
-
blocks.push(block);
|
|
2426
|
-
continue;
|
|
2427
|
-
}
|
|
2428
|
-
const existing = blocks[existingIndex];
|
|
2429
|
-
if (existing !== undefined) {
|
|
2430
|
-
blocks[existingIndex] = {
|
|
2431
|
-
...block,
|
|
2432
|
-
payload: {
|
|
2433
|
-
...(isRecord(existing.payload) ? existing.payload : {}),
|
|
2434
|
-
...(isRecord(block.payload) ? block.payload : {}),
|
|
2435
|
-
},
|
|
2436
|
-
};
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
// Performance: an authoritative rebuild restates most of what is already there. Reusing the block
|
|
2441
|
-
// that already says it keeps its revision, and with it the rows the shell rendered for
|
|
2442
|
-
// it — otherwise every turn that ends re-renders the whole session.
|
|
2443
|
-
this.#setTranscript(blocks.map(block => {
|
|
2444
|
-
const existing = this.#transcriptBlock(block.id);
|
|
2445
|
-
const next = existing === undefined ? block : retainCompletedArguments(existing, block);
|
|
2446
|
-
return existing !== undefined && sameBlockContent(existing, next) ? existing : next;
|
|
2447
|
-
}));
|
|
2448
|
-
}
|
|
2449
|
-
#upsertMessageBlock(message, status) {
|
|
2450
|
-
const blocks = this.#messageBlocks(message, status, this.#transcript.length);
|
|
2451
|
-
const first = blocks[0];
|
|
2452
|
-
for (const block of blocks)
|
|
2453
|
-
this.#upsertTranscriptBlock(block);
|
|
2454
|
-
return first;
|
|
2455
|
-
}
|
|
2456
|
-
#messageBlocks(message, status, fallbackIndex, occurrence, currentBlock = id => this.#transcriptBlock(id)) {
|
|
2457
|
-
if (!isRecord(message) || typeof message.role !== "string")
|
|
2458
|
-
return [];
|
|
2459
|
-
const baseId = this.#messageBlockId(message, fallbackIndex, status, occurrence);
|
|
2460
|
-
if (message.role === "user") {
|
|
2461
|
-
return [{
|
|
2462
|
-
id: baseId,
|
|
2463
|
-
kind: "user",
|
|
2464
|
-
status,
|
|
2465
|
-
revision: this.#nextBlockRevision(baseId),
|
|
2466
|
-
title: "User",
|
|
2467
|
-
text: textFromContent(message.content),
|
|
2468
|
-
imageReferences: this.#imageReferences(message.content, "user"),
|
|
2469
|
-
payload: {
|
|
2470
|
-
role: "user",
|
|
2471
|
-
imageCount: contentImageCount(message.content),
|
|
2472
|
-
timestamp: typeof message.timestamp === "number" && Number.isFinite(message.timestamp) ? message.timestamp : null,
|
|
2473
|
-
},
|
|
2474
|
-
}];
|
|
2475
|
-
}
|
|
2476
|
-
if (message.role === "bashExecution") {
|
|
2477
|
-
return [{
|
|
2478
|
-
id: baseId,
|
|
2479
|
-
kind: "bash",
|
|
2480
|
-
status,
|
|
2481
|
-
revision: this.#nextBlockRevision(baseId),
|
|
2482
|
-
title: stringValue(message.command) ?? "Bash",
|
|
2483
|
-
text: stringValue(message.output) ?? "",
|
|
2484
|
-
payload: {
|
|
2485
|
-
role: "bashExecution",
|
|
2486
|
-
command: stringValue(message.command) ?? "",
|
|
2487
|
-
exitCode: typeof message.exitCode === "number" ? message.exitCode : null,
|
|
2488
|
-
cancelled: message.cancelled === true,
|
|
2489
|
-
truncated: message.truncated === true,
|
|
2490
|
-
fullOutputPath: stringValue(message.fullOutputPath) ?? null,
|
|
2491
|
-
excludeFromContext: message.excludeFromContext === true,
|
|
2492
|
-
},
|
|
2493
|
-
}];
|
|
2494
|
-
}
|
|
2495
|
-
if (message.role === "custom") {
|
|
2496
|
-
if (message.display === false)
|
|
2497
|
-
return [];
|
|
2498
|
-
return [{
|
|
2499
|
-
id: baseId,
|
|
2500
|
-
kind: "custom",
|
|
2501
|
-
status,
|
|
2502
|
-
revision: this.#nextBlockRevision(baseId),
|
|
2503
|
-
title: stringValue(message.customType) ?? "Custom",
|
|
2504
|
-
text: textFromContent(message.content),
|
|
2505
|
-
payload: {
|
|
2506
|
-
role: "custom",
|
|
2507
|
-
customType: stringValue(message.customType) ?? "custom",
|
|
2508
|
-
display: true,
|
|
2509
|
-
details: message.details,
|
|
2510
|
-
timestamp: typeof message.timestamp === "number" ? message.timestamp : 0,
|
|
2511
|
-
},
|
|
2512
|
-
}];
|
|
2513
|
-
}
|
|
2514
|
-
if (message.role === "compactionSummary" || message.role === "branchSummary") {
|
|
2515
|
-
return [{
|
|
2516
|
-
id: baseId,
|
|
2517
|
-
kind: "compaction",
|
|
2518
|
-
status,
|
|
2519
|
-
revision: this.#nextBlockRevision(baseId),
|
|
2520
|
-
title: message.role === "branchSummary" ? "Branch summary" : "Compaction summary",
|
|
2521
|
-
text: stringValue(message.summary) ?? "",
|
|
2522
|
-
payload: {
|
|
2523
|
-
role: message.role,
|
|
2524
|
-
tokensBefore: typeof message.tokensBefore === "number" ? message.tokensBefore : 0,
|
|
2525
|
-
fromId: stringValue(message.fromId) ?? null,
|
|
2526
|
-
timestamp: typeof message.timestamp === "number" ? message.timestamp : 0,
|
|
2527
|
-
},
|
|
2528
|
-
}];
|
|
2529
|
-
}
|
|
2530
|
-
if (message.role === "toolResult") {
|
|
2531
|
-
const toolCallId = stringValue(message.toolCallId);
|
|
2532
|
-
const blockId = toolCallId === undefined
|
|
2533
|
-
? baseId
|
|
2534
|
-
: this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
2535
|
-
if (toolCallId !== undefined)
|
|
2536
|
-
this.#toolBlockIds.set(toolCallId, blockId);
|
|
2537
|
-
const existing = currentBlock(blockId);
|
|
2538
|
-
const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
|
|
2539
|
-
const payload = {
|
|
2540
|
-
role: "toolResult", toolCallId: toolCallId ?? null,
|
|
2541
|
-
toolName: stringValue(message.toolName) ?? stringValue(existingPayload?.toolName) ?? "unknown",
|
|
2542
|
-
argsComplete: true, partialResult: status === "live", isError: message.isError === true,
|
|
2543
|
-
};
|
|
2544
|
-
const rendering = toolRenderingInput({ args: undefined, previousArgs: existing?.toolRendering,
|
|
2545
|
-
result: message, payload, image: part => this.#imageReferences([part], "tool-result")[0] });
|
|
2546
|
-
return [{
|
|
2547
|
-
id: blockId,
|
|
2548
|
-
kind: "tool-result",
|
|
2549
|
-
status,
|
|
2550
|
-
toolState: { argsComplete: true, execution: status === "live" ? "running" : message.isError === true ? "failed" : "succeeded" },
|
|
2551
|
-
revision: this.#nextBlockRevision(blockId),
|
|
2552
|
-
title: stringValue(message.toolName) ?? existing?.title ?? "Tool result",
|
|
2553
|
-
...rendering,
|
|
2554
|
-
payload,
|
|
2555
|
-
}];
|
|
2556
|
-
}
|
|
2557
|
-
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
2558
|
-
return [];
|
|
2559
|
-
const blocks = [{
|
|
2560
|
-
id: baseId,
|
|
2561
|
-
kind: "assistant",
|
|
2562
|
-
status,
|
|
2563
|
-
revision: this.#nextBlockRevision(baseId),
|
|
2564
|
-
title: "Assistant",
|
|
2565
|
-
text: textFromContent(message.content),
|
|
2566
|
-
payload: {
|
|
2567
|
-
role: "assistant",
|
|
2568
|
-
content: assistantContent(message.content),
|
|
2569
|
-
provider: stringValue(message.provider) ?? null,
|
|
2570
|
-
model: stringValue(message.model) ?? null,
|
|
2571
|
-
api: stringValue(message.api) ?? null,
|
|
2572
|
-
usage: sanitizeJson(message.usage),
|
|
2573
|
-
stopReason: stringValue(message.stopReason) ?? null,
|
|
2574
|
-
errorMessage: stringValue(message.errorMessage) ?? null,
|
|
2575
|
-
timestamp: typeof message.timestamp === "number" ? message.timestamp : 0,
|
|
2576
|
-
},
|
|
2577
|
-
}];
|
|
2578
|
-
for (const item of message.content) {
|
|
2579
|
-
if (!isRecord(item) || item.type !== "toolCall")
|
|
2580
|
-
continue;
|
|
2581
|
-
const toolCallId = stringValue(item.id) ?? `${baseId}:${blocks.length}`;
|
|
2582
|
-
const blockId = this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
2583
|
-
this.#toolBlockIds.set(toolCallId, blockId);
|
|
2584
|
-
const failure = status === "finalized" ? this.#declarationFailure(message) : undefined;
|
|
2585
|
-
const payload = { toolCallId, toolName: stringValue(item.name) ?? "unknown",
|
|
2586
|
-
argsComplete: failure === undefined && status === "finalized",
|
|
2587
|
-
...(failure === undefined ? {} : { isError: true }) };
|
|
2588
|
-
const rendering = toolRenderingInput({ args: item.arguments, payload,
|
|
2589
|
-
...(failure === undefined ? {} : { result: { content: [{ type: "text", text: failure.text }] } }),
|
|
2590
|
-
image: () => undefined });
|
|
2591
|
-
blocks.push({
|
|
2592
|
-
id: blockId,
|
|
2593
|
-
kind: failure === undefined ? "tool-call" : "tool-result",
|
|
2594
|
-
status: failure === undefined ? "live" : "finalized",
|
|
2595
|
-
toolState: {
|
|
2596
|
-
argsComplete: failure === undefined && status === "finalized",
|
|
2597
|
-
execution: failure?.execution ?? "pending",
|
|
2598
|
-
},
|
|
2599
|
-
revision: this.#nextBlockRevision(blockId),
|
|
2600
|
-
title: stringValue(item.name) ?? "Tool",
|
|
2601
|
-
...rendering,
|
|
2602
|
-
text: failure?.text ?? jsonSummary(item.arguments).summary,
|
|
2603
|
-
payload,
|
|
2604
|
-
});
|
|
2605
|
-
}
|
|
2606
|
-
return blocks;
|
|
2607
|
-
}
|
|
2608
|
-
#declarationFailure(message) {
|
|
2609
|
-
// Provenance: pinned interactive-mode message_end and history reconstruction settle pending tools.
|
|
2610
|
-
if (message.role !== "assistant")
|
|
2611
|
-
return undefined;
|
|
2612
|
-
if (message.stopReason === "aborted") {
|
|
2613
|
-
const attempts = this.#session?.retryAttempt ?? 0;
|
|
2614
|
-
return { execution: "aborted", text: attempts > 0
|
|
2615
|
-
? `Aborted after ${attempts} retry attempt${attempts > 1 ? "s" : ""}` : "Operation aborted" };
|
|
2616
|
-
}
|
|
2617
|
-
return message.stopReason === "error"
|
|
2618
|
-
? { execution: "failed", text: stringValue(message.errorMessage) || "Error" } : undefined;
|
|
2619
|
-
}
|
|
2620
|
-
#settleFailedDeclarations(message) {
|
|
2621
|
-
if (!isRecord(message))
|
|
2622
|
-
return;
|
|
2623
|
-
const failure = this.#declarationFailure(message);
|
|
2624
|
-
if (failure === undefined)
|
|
2625
|
-
return;
|
|
2626
|
-
for (const block of this.#transcript) {
|
|
2627
|
-
const state = transcriptToolState(block);
|
|
2628
|
-
if (state === undefined || state.execution !== "pending" && state.execution !== "running")
|
|
2629
|
-
continue;
|
|
2630
|
-
const payload = { ...(isRecord(block.payload) ? block.payload : {}), partialResult: false, isError: true };
|
|
2631
|
-
this.#upsertTranscriptBlock({
|
|
2632
|
-
...block, kind: "tool-result", status: "finalized", revision: block.revision + 1,
|
|
2633
|
-
...toolRenderingInput({ args: undefined, previousArgs: block.toolRendering, payload,
|
|
2634
|
-
result: { content: [{ type: "text", text: failure.text }] }, image: () => undefined }),
|
|
2635
|
-
toolState: { ...state, execution: failure.execution }, payload,
|
|
2636
|
-
});
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
#imageReferences(content, source) {
|
|
2640
|
-
if (!Array.isArray(content))
|
|
2641
|
-
return [];
|
|
2642
|
-
const references = [];
|
|
2643
|
-
for (const item of content) {
|
|
2644
|
-
if (references.length >= 16)
|
|
2645
|
-
break;
|
|
2646
|
-
const reference = this.#transcriptImageAssets.reference(item, source);
|
|
2647
|
-
if (reference !== undefined)
|
|
2648
|
-
references.push(reference);
|
|
2649
|
-
}
|
|
2650
|
-
return references;
|
|
2651
|
-
}
|
|
2652
|
-
#upsertToolExecutionBlock(event) {
|
|
2653
|
-
const toolCallId = stringValue(event.toolCallId);
|
|
2654
|
-
if (!toolCallId)
|
|
2655
|
-
return;
|
|
2656
|
-
const blockId = this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
2657
|
-
this.#toolBlockIds.set(toolCallId, blockId);
|
|
2658
|
-
const ended = event.type === "tool_execution_end";
|
|
2659
|
-
const existing = this.#transcriptBlock(blockId);
|
|
2660
|
-
const state = existing === undefined ? undefined : transcriptToolState(existing);
|
|
2661
|
-
// Invariant: duplicate starts cannot blank accumulated output; late events cannot reopen a settled invocation.
|
|
2662
|
-
if (state !== undefined && (state.execution !== "pending" && state.execution !== "running"
|
|
2663
|
-
|| event.type === "tool_execution_start" && state.execution === "running"))
|
|
2664
|
-
return;
|
|
2665
|
-
const source = ended ? event.result : event.partialResult;
|
|
2666
|
-
const payload = { toolCallId, toolName: stringValue(event.toolName) ?? "unknown",
|
|
2667
|
-
partialResult: event.type === "tool_execution_update", argsComplete: true, isError: event.isError === true };
|
|
2668
|
-
const rendering = toolRenderingInput({ args: event.args, previousArgs: existing?.toolRendering, result: source,
|
|
2669
|
-
payload, image: part => this.#imageReferences([part], "tool-result")[0] });
|
|
2670
|
-
this.#upsertTranscriptBlock({
|
|
2671
|
-
id: blockId,
|
|
2672
|
-
kind: ended ? "tool-result" : "tool-call",
|
|
2673
|
-
status: ended ? "finalized" : "live",
|
|
2674
|
-
toolState: { argsComplete: true, execution: ended ? event.isError === true ? "failed" : "succeeded" : "running" },
|
|
2675
|
-
revision: this.#nextBlockRevision(blockId),
|
|
2676
|
-
title: stringValue(event.toolName) ?? "Tool",
|
|
2677
|
-
...rendering,
|
|
2678
|
-
payload,
|
|
2679
|
-
});
|
|
2680
|
-
}
|
|
2681
|
-
#upsertTranscriptBlock(block) {
|
|
2682
|
-
const index = this.#transcriptIndex.get(block.id);
|
|
2683
|
-
if (index !== undefined) {
|
|
2684
|
-
const existing = this.#transcript[index];
|
|
2685
|
-
// Invariant: argument completion is not execution finality; stale phases still stay rejected.
|
|
2686
|
-
if (existing !== undefined && !acceptsTranscriptUpdate(existing, block))
|
|
2687
|
-
return;
|
|
2688
|
-
if (existing !== undefined)
|
|
2689
|
-
block = retainCompletedArguments(existing, block);
|
|
2690
|
-
// Performance: nothing is emitted for a block that repeats itself, and keeping the
|
|
2691
|
-
// revision keeps the rows it already rendered.
|
|
2692
|
-
if (existing !== undefined && sameBlockContent(existing, block))
|
|
2693
|
-
return;
|
|
2694
|
-
this.#transcriptImageAssets.retain(block);
|
|
2695
|
-
if (existing !== undefined)
|
|
2696
|
-
this.#transcriptImageAssets.release(existing);
|
|
2697
|
-
this.#transcript[index] = block;
|
|
2698
|
-
}
|
|
2699
|
-
else {
|
|
2700
|
-
this.#transcriptImageAssets.retain(block);
|
|
2701
|
-
this.#transcriptIndex.set(block.id, this.#transcript.length);
|
|
2702
|
-
this.#transcript.push(block);
|
|
2703
|
-
}
|
|
2704
|
-
this.#transcriptSnapshot = undefined;
|
|
2705
|
-
this.#emitEvent({ type: "transcript-block", block });
|
|
2706
|
-
}
|
|
2707
|
-
#messageBlockId(message, fallbackIndex, status, occurrence) {
|
|
2708
|
-
const existing = this.#messageBlockIds.get(message);
|
|
2709
|
-
if (existing)
|
|
2710
|
-
return existing;
|
|
2711
|
-
const fallback = messageFallbackKey(message, fallbackIndex);
|
|
2712
|
-
const cached = this.#messageFallbackIds.get(fallback) ?? [];
|
|
2713
|
-
let id;
|
|
2714
|
-
if (occurrence !== undefined) {
|
|
2715
|
-
id = cached[occurrence];
|
|
2716
|
-
if (id === undefined) {
|
|
2717
|
-
id = `${fallback}-${occurrence}`;
|
|
2718
|
-
cached[occurrence] = id;
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
else {
|
|
2722
|
-
id = [...cached].reverse().find(candidate => this.#transcriptBlock(candidate)?.status === "live");
|
|
2723
|
-
if (id === undefined && status === "finalized")
|
|
2724
|
-
id = cached.at(-1);
|
|
2725
|
-
if (id === undefined) {
|
|
2726
|
-
id = `${fallback}-${cached.length}`;
|
|
2727
|
-
cached.push(id);
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
this.#messageFallbackIds.set(fallback, cached);
|
|
2731
|
-
this.#messageBlockIds.set(message, id);
|
|
2732
|
-
return id;
|
|
2733
|
-
}
|
|
2734
|
-
#nextBlockRevision(id) {
|
|
2735
|
-
const existing = this.#transcriptBlock(id);
|
|
2736
|
-
if (existing)
|
|
2737
|
-
return existing.revision + 1;
|
|
2738
|
-
this.#nextBlockSequence += 1;
|
|
2739
|
-
return this.#nextBlockSequence;
|
|
2740
|
-
}
|
|
2741
|
-
#recordCommand(command, outcome, diagnostic) {
|
|
2742
|
-
const result = this.#finishCommand(command, outcome, diagnostic);
|
|
2743
|
-
this.#completedCommands.set(command.correlationId, result);
|
|
2744
|
-
if (this.#completedCommands.size > 256) {
|
|
2745
|
-
const oldest = this.#completedCommands.keys().next().value;
|
|
2746
|
-
if (oldest)
|
|
2747
|
-
this.#completedCommands.delete(oldest);
|
|
2748
|
-
}
|
|
2749
|
-
return result;
|
|
2750
|
-
}
|
|
2751
|
-
#finishCommand(command, outcome, diagnostic) {
|
|
2752
|
-
this.#activeCommandIds = this.#activeCommandIds.filter(id => id !== command.correlationId);
|
|
2753
|
-
this.#emitEvent({
|
|
2754
|
-
type: "command-outcome",
|
|
2755
|
-
correlationId: command.correlationId,
|
|
2756
|
-
outcome,
|
|
2757
|
-
diagnostic,
|
|
2758
|
-
});
|
|
2759
|
-
return { outcome, diagnostic };
|
|
579
|
+
this.#projection.replace(blocks);
|
|
2760
580
|
}
|
|
2761
581
|
#addDiagnostic(severity, code, message, recoverable) {
|
|
2762
582
|
const diagnostic = this.#recordDiagnostic(severity, code, message, recoverable);
|
|
@@ -2768,138 +588,40 @@ export class PiEngineAdapter {
|
|
|
2768
588
|
this.#emitEvent({ type: "session-view", view: this.view() });
|
|
2769
589
|
}
|
|
2770
590
|
#emitEvent(value) {
|
|
2771
|
-
|
|
2772
|
-
if (event.type !== "session-view")
|
|
591
|
+
if (value.type !== "session-view")
|
|
2773
592
|
this.#viewRevision += 1;
|
|
2774
|
-
this.#
|
|
2775
|
-
}
|
|
2776
|
-
#enqueueEvent(event) {
|
|
2777
|
-
if (this.#overload !== undefined) {
|
|
2778
|
-
if (event.type === "command-outcome" && event.outcome !== "accepted" && this.#pendingCommands.has(event.correlationId)) {
|
|
2779
|
-
this.#reservedOutcomes.set(event.correlationId, { outcome: event.outcome, diagnostic: event.diagnostic });
|
|
2780
|
-
}
|
|
2781
|
-
return;
|
|
2782
|
-
}
|
|
2783
|
-
if (!this.#eventQueue.push(event, this.#sessionGeneration, event.type === "transcript-block"
|
|
2784
|
-
? this.#blockReconciliation(event.block.id, event.sequence) : undefined)) {
|
|
2785
|
-
this.#beginOverload();
|
|
2786
|
-
if (event.type === "command-outcome" && event.outcome !== "accepted" && this.#pendingCommands.has(event.correlationId)
|
|
2787
|
-
&& !this.#reservedOutcomes.has(event.correlationId)) {
|
|
2788
|
-
this.#reservedOutcomes.set(event.correlationId, { outcome: event.outcome, diagnostic: event.diagnostic });
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
this.#eventQueueProcessing ??= Promise.resolve().then(() => this.#processEventQueue());
|
|
2792
|
-
}
|
|
2793
|
-
// Performance: capture only identity, not the complete block/event retained by an earlier revision.
|
|
2794
|
-
#blockReconciliation(id, sequence) {
|
|
2795
|
-
return () => ({ type: "transcript-block", sessionId: this.#sessionId, sequence, block: this.#transcriptBlock(id) });
|
|
2796
|
-
}
|
|
2797
|
-
#beginOverload() {
|
|
2798
|
-
if (this.#overload !== undefined)
|
|
2799
|
-
return;
|
|
2800
|
-
this.#overloads = Math.min(Number.MAX_SAFE_INTEGER, this.#overloads + 1);
|
|
2801
|
-
this.#deliveryFailed = true;
|
|
2802
|
-
// Concurrency: reserve before cancellation; outcomes produced reentrantly must not enter the saturated queue.
|
|
2803
|
-
this.#overload = Promise.resolve(false);
|
|
2804
|
-
for (const pending of this.#pendingCommands.values())
|
|
2805
|
-
pending.cancel();
|
|
2806
|
-
for (const pending of this.#pendingWorkflows)
|
|
2807
|
-
pending.cancel();
|
|
2808
|
-
const session = this.#session;
|
|
2809
|
-
this.#overload = new Promise(resolve => {
|
|
2810
|
-
const timer = setTimeout(() => resolve(false), 2000);
|
|
2811
|
-
void Promise.resolve().then(() => session?.abort()).then(() => {
|
|
2812
|
-
clearTimeout(timer);
|
|
2813
|
-
resolve(true);
|
|
2814
|
-
}, () => { clearTimeout(timer); resolve(false); });
|
|
2815
|
-
});
|
|
593
|
+
this.#delivery.emit(value);
|
|
2816
594
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
}
|
|
2824
|
-
catch (error) {
|
|
2825
|
-
this.#deliveryFailed = true;
|
|
2826
|
-
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
595
|
+
// Concurrency: cancellation runs synchronously so the overload reservation already guards reentrant outcomes.
|
|
596
|
+
#cancelForOverload() {
|
|
597
|
+
this.#commands.cancelPending();
|
|
598
|
+
this.#workflows.cancelPending();
|
|
599
|
+
const session = this.#engine.session;
|
|
600
|
+
return Promise.resolve().then(() => session?.abort()).then(() => undefined);
|
|
2829
601
|
}
|
|
2830
|
-
async #reconcileOverload() {
|
|
2831
|
-
const cancelled = await this.#overload;
|
|
602
|
+
async #reconcileOverload(cancelled) {
|
|
2832
603
|
const canResume = cancelled === true && this.#runningCommands === 0;
|
|
2833
604
|
this.#admissionStopped = !canResume;
|
|
2834
|
-
this.#
|
|
2835
|
-
this.#
|
|
2836
|
-
this.#compactionProgress?.dispose();
|
|
2837
|
-
this.#compactionProgress = null;
|
|
2838
|
-
++this.#sessionGeneration;
|
|
2839
|
-
this.#agentRunActive = false;
|
|
2840
|
-
this.#statusKind = null;
|
|
605
|
+
this.#engine.suspend();
|
|
606
|
+
this.#events.abandonRun();
|
|
2841
607
|
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
2842
608
|
this.#lifecycle = this.#disposed ? "stopped" : canResume ? "ready" : "failed";
|
|
2843
609
|
this.#editor = { ...this.#editor, submitEnabled: canResume && !this.#disposed };
|
|
2844
610
|
this.#viewRevision += 1;
|
|
2845
611
|
// Invariant: overload publishes one authoritative view; settle tool phases before finalizing other live blocks.
|
|
2846
|
-
this.#settleFailedDeclarations({ role: "assistant", stopReason: cancelled ? "aborted" : "error",
|
|
612
|
+
this.#projection.settleFailedDeclarations({ role: "assistant", stopReason: cancelled ? "aborted" : "error",
|
|
2847
613
|
errorMessage: "Tool result unavailable after UI delivery overload" });
|
|
2848
|
-
this.#setTranscript(this.#
|
|
2849
|
-
for (const [correlationId, result] of this.#
|
|
2850
|
-
this.#
|
|
614
|
+
this.#setTranscript(this.#projection.blocks.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
|
|
615
|
+
for (const [correlationId, result] of this.#delivery.takeReservedOutcomes()) {
|
|
616
|
+
this.#delivery.deliverNow(this.#delivery.stamp({ type: "command-outcome", correlationId, ...result }));
|
|
2851
617
|
await new Promise(resolve => setImmediate(resolve));
|
|
2852
618
|
}
|
|
2853
|
-
this.#reservedOutcomes.clear();
|
|
2854
619
|
// Invariant: one out-of-band authoritative reconciliation, not an emergency transcript/event log.
|
|
2855
|
-
this.#
|
|
620
|
+
this.#delivery.deliverNow(this.#delivery.stamp({ type: "session-view", view: this.view() }));
|
|
2856
621
|
if (this.#disposed)
|
|
2857
|
-
this.#
|
|
2858
|
-
if (canResume && !this.#disposed
|
|
2859
|
-
|
|
2860
|
-
this.#unsubscribe = this.#session.subscribe(event => {
|
|
2861
|
-
if (generation === this.#sessionGeneration && !this.#disposed)
|
|
2862
|
-
this.#handlePiEvent(event);
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
2865
|
-
this.#overload = undefined;
|
|
2866
|
-
}
|
|
2867
|
-
async #processEventQueue() {
|
|
2868
|
-
try {
|
|
2869
|
-
let deliveredSinceYield = 0;
|
|
2870
|
-
while (this.#eventQueue.size > 0) {
|
|
2871
|
-
const pending = this.#eventQueue.shift();
|
|
2872
|
-
if (pending === undefined)
|
|
2873
|
-
continue;
|
|
2874
|
-
if (pending.generation !== this.#sessionGeneration && pending.event.type !== "command-outcome") {
|
|
2875
|
-
this.#invalidatedEvents = Math.min(Number.MAX_SAFE_INTEGER, this.#invalidatedEvents + 1);
|
|
2876
|
-
pending.release?.();
|
|
2877
|
-
continue;
|
|
2878
|
-
}
|
|
2879
|
-
try {
|
|
2880
|
-
this.#deliver(pending.event);
|
|
2881
|
-
}
|
|
2882
|
-
finally {
|
|
2883
|
-
pending.release?.();
|
|
2884
|
-
}
|
|
2885
|
-
deliveredSinceYield += 1;
|
|
2886
|
-
// Concurrency: a microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
2887
|
-
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
2888
|
-
// drained. Yielding on a macrotask hands those their turn between batches.
|
|
2889
|
-
if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.size > 0) {
|
|
2890
|
-
deliveredSinceYield = 0;
|
|
2891
|
-
await new Promise(resolve => { setImmediate(resolve); });
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
if (this.#overload !== undefined)
|
|
2895
|
-
await this.#reconcileOverload();
|
|
2896
|
-
}
|
|
2897
|
-
finally {
|
|
2898
|
-
this.#eventQueueProcessing = undefined;
|
|
2899
|
-
if (this.#eventQueue.size > 0) {
|
|
2900
|
-
this.#eventQueueProcessing = Promise.resolve().then(() => this.#processEventQueue());
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
622
|
+
this.#delivery.deliverNow(this.#delivery.stamp({ type: "session-lifecycle", lifecycle: "stopped", reason: null }));
|
|
623
|
+
if (canResume && !this.#disposed)
|
|
624
|
+
this.#engine.resume();
|
|
2903
625
|
}
|
|
2904
626
|
#recordDiagnostic(severity, code, message, recoverable) {
|
|
2905
627
|
const diagnostic = {
|
|
@@ -2918,619 +640,9 @@ export class PiEngineAdapter {
|
|
|
2918
640
|
};
|
|
2919
641
|
return diagnostic;
|
|
2920
642
|
}
|
|
2921
|
-
#event(value) {
|
|
2922
|
-
this.#sequence += 1;
|
|
2923
|
-
return { ...value, sessionId: this.#sessionId, sequence: this.#sequence };
|
|
2924
|
-
}
|
|
2925
643
|
}
|
|
2926
644
|
export async function createPiEngineAdapter(options = {}) {
|
|
2927
645
|
const adapter = new PiEngineAdapter(options);
|
|
2928
646
|
await adapter.start();
|
|
2929
647
|
return adapter;
|
|
2930
648
|
}
|
|
2931
|
-
async function createDefaultPiRuntime(input) {
|
|
2932
|
-
return createPiRuntimeIntegration({
|
|
2933
|
-
cwd: input.cwd,
|
|
2934
|
-
agentDir: input.agentDir,
|
|
2935
|
-
...(input.sessionPath === undefined ? {} : { sessionPath: input.sessionPath }),
|
|
2936
|
-
...(input.sessionSelection === undefined ? {} : { sessionSelection: input.sessionSelection }),
|
|
2937
|
-
...(input.sessionForkPrompt === undefined ? {} : { sessionForkPrompt: input.sessionForkPrompt }),
|
|
2938
|
-
...(input.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: input.projectTrustPrompt }),
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
async function checkDefaultPiPackageUpdates(cwd, agentDir, settingsManager) {
|
|
2942
|
-
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
2943
|
-
const updates = await packageManager.checkForAvailableUpdates();
|
|
2944
|
-
return updates.map(update => update.displayName);
|
|
2945
|
-
}
|
|
2946
|
-
function pinnedSessionInfoPresentation(value, sessionName, entries, modelRuntime) {
|
|
2947
|
-
const stats = isRecord(value) ? value : {};
|
|
2948
|
-
const tokens = dynamicObject(stats, "tokens");
|
|
2949
|
-
return {
|
|
2950
|
-
kind: "session-info",
|
|
2951
|
-
...(sessionName === undefined ? {} : { sessionName }),
|
|
2952
|
-
stats: {
|
|
2953
|
-
...(stringProperty(stats, "sessionFile") === undefined ? {} : { sessionFile: stringProperty(stats, "sessionFile") }),
|
|
2954
|
-
sessionId: stringProperty(stats, "sessionId") ?? "unknown",
|
|
2955
|
-
userMessages: finiteNumber(stats.userMessages),
|
|
2956
|
-
assistantMessages: finiteNumber(stats.assistantMessages),
|
|
2957
|
-
toolCalls: finiteNumber(stats.toolCalls),
|
|
2958
|
-
toolResults: finiteNumber(stats.toolResults),
|
|
2959
|
-
totalMessages: finiteNumber(stats.totalMessages),
|
|
2960
|
-
tokens: {
|
|
2961
|
-
input: finiteNumber(tokens.input),
|
|
2962
|
-
output: finiteNumber(tokens.output),
|
|
2963
|
-
cacheRead: finiteNumber(tokens.cacheRead),
|
|
2964
|
-
cacheWrite: finiteNumber(tokens.cacheWrite),
|
|
2965
|
-
total: finiteNumber(tokens.total),
|
|
2966
|
-
},
|
|
2967
|
-
cost: finiteNumber(stats.cost),
|
|
2968
|
-
},
|
|
2969
|
-
cacheWaste: pinnedCacheWaste(entries, modelRuntime),
|
|
2970
|
-
usageBreakdown: pinnedUsageCostBreakdown(entries),
|
|
2971
|
-
};
|
|
2972
|
-
}
|
|
2973
|
-
function pinnedUsageCostBreakdown(entries) {
|
|
2974
|
-
const totals = new Map();
|
|
2975
|
-
for (const entry of entries) {
|
|
2976
|
-
if (!isRecord(entry))
|
|
2977
|
-
continue;
|
|
2978
|
-
let key;
|
|
2979
|
-
let usage;
|
|
2980
|
-
const message = dynamicObject(entry, "message");
|
|
2981
|
-
if (entry.type === "message" && message.role === "assistant") {
|
|
2982
|
-
const provider = stringProperty(message, "provider");
|
|
2983
|
-
const model = stringProperty(message, "responseModel") ?? stringProperty(message, "model");
|
|
2984
|
-
if (provider && model)
|
|
2985
|
-
key = `${provider}/${model}`;
|
|
2986
|
-
usage = dynamicObject(message, "usage");
|
|
2987
|
-
}
|
|
2988
|
-
else if (entry.type === "message" && message.role === "toolResult" && isRecord(message.usage)) {
|
|
2989
|
-
key = "Tools/summaries";
|
|
2990
|
-
usage = message.usage;
|
|
2991
|
-
}
|
|
2992
|
-
else if ((entry.type === "branch_summary" || entry.type === "compaction") && isRecord(entry.usage)) {
|
|
2993
|
-
key = "Tools/summaries";
|
|
2994
|
-
usage = entry.usage;
|
|
2995
|
-
}
|
|
2996
|
-
if (!key || !usage)
|
|
2997
|
-
continue;
|
|
2998
|
-
const cost = finiteNumber(dynamicObject(usage, "cost").total);
|
|
2999
|
-
const tokens = finiteNumber(usage.input) + finiteNumber(usage.output)
|
|
3000
|
-
+ finiteNumber(usage.cacheRead) + finiteNumber(usage.cacheWrite);
|
|
3001
|
-
const current = totals.get(key) ?? { cost: 0, tokens: 0 };
|
|
3002
|
-
current.cost += cost;
|
|
3003
|
-
current.tokens += tokens;
|
|
3004
|
-
totals.set(key, current);
|
|
3005
|
-
}
|
|
3006
|
-
return [...totals].map(([key, total]) => ({ key, ...total }))
|
|
3007
|
-
.filter(entry => entry.cost > 0 || entry.tokens > 0)
|
|
3008
|
-
.sort((a, b) => b.cost - a.cost);
|
|
3009
|
-
}
|
|
3010
|
-
function pinnedCacheWaste(entries, modelRuntime) {
|
|
3011
|
-
let previous;
|
|
3012
|
-
const totals = { missedTokens: 0, missedCost: 0, missCount: 0 };
|
|
3013
|
-
for (const entry of entries) {
|
|
3014
|
-
if (!isRecord(entry))
|
|
3015
|
-
continue;
|
|
3016
|
-
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
3017
|
-
previous = undefined;
|
|
3018
|
-
continue;
|
|
3019
|
-
}
|
|
3020
|
-
const message = dynamicObject(entry, "message");
|
|
3021
|
-
if (entry.type !== "message" || message.role !== "assistant")
|
|
3022
|
-
continue;
|
|
3023
|
-
const usage = dynamicObject(message, "usage");
|
|
3024
|
-
const input = finiteNumber(usage.input);
|
|
3025
|
-
const cacheRead = finiteNumber(usage.cacheRead);
|
|
3026
|
-
const cacheWrite = finiteNumber(usage.cacheWrite);
|
|
3027
|
-
const promptTokens = input + cacheRead + cacheWrite;
|
|
3028
|
-
if (previous && promptTokens > 0 && (cacheRead + cacheWrite > 0 || previous.reportedCache)) {
|
|
3029
|
-
const missedTokens = Math.min(previous.promptTokens, promptTokens) - cacheRead;
|
|
3030
|
-
if (missedTokens > 1024) {
|
|
3031
|
-
const cost = dynamicObject(usage, "cost");
|
|
3032
|
-
const paidTokens = input + cacheWrite;
|
|
3033
|
-
const paidRate = paidTokens > 0 ? (finiteNumber(cost.input) + finiteNumber(cost.cacheWrite)) / paidTokens : 0;
|
|
3034
|
-
const provider = stringProperty(message, "provider") ?? "";
|
|
3035
|
-
const modelId = stringProperty(message, "model") ?? "";
|
|
3036
|
-
const model = modelRuntime.getModel(provider, modelId);
|
|
3037
|
-
const modelCost = dynamicObject(dynamicObject(model, "cost"));
|
|
3038
|
-
const readRate = cacheRead > 0
|
|
3039
|
-
? finiteNumber(cost.cacheRead) / cacheRead
|
|
3040
|
-
: finiteNumber(modelCost.cacheRead) / 1_000_000;
|
|
3041
|
-
totals.missedTokens += missedTokens;
|
|
3042
|
-
totals.missedCost += missedTokens * Math.max(0, paidRate - readRate);
|
|
3043
|
-
totals.missCount += 1;
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
if (promptTokens > 0) {
|
|
3047
|
-
const provider = stringProperty(message, "provider") ?? "";
|
|
3048
|
-
const model = stringProperty(message, "model") ?? "";
|
|
3049
|
-
previous = {
|
|
3050
|
-
promptTokens,
|
|
3051
|
-
modelKey: `${provider}/${model}`,
|
|
3052
|
-
timestamp: finiteNumber(message.timestamp),
|
|
3053
|
-
reportedCache: (previous?.reportedCache ?? false) || cacheRead + cacheWrite > 0,
|
|
3054
|
-
};
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
return totals;
|
|
3058
|
-
}
|
|
3059
|
-
function defaultWorkflowHost() {
|
|
3060
|
-
return {
|
|
3061
|
-
copyText: copyToClipboard,
|
|
3062
|
-
async runCommand(command, arguments_, options) {
|
|
3063
|
-
const result = await execFileAsync(command, [...arguments_], { encoding: "utf8", signal: options?.signal });
|
|
3064
|
-
return { stdout: result.stdout, stderr: result.stderr };
|
|
3065
|
-
},
|
|
3066
|
-
readChangelog: readPinnedCommandChangelog,
|
|
3067
|
-
};
|
|
3068
|
-
}
|
|
3069
|
-
function workflowLoginNotification(event) {
|
|
3070
|
-
if (!isRecord(event))
|
|
3071
|
-
return undefined;
|
|
3072
|
-
if (event.type === "auth_url") {
|
|
3073
|
-
const url = stringProperty(event, "url");
|
|
3074
|
-
if (!url)
|
|
3075
|
-
return undefined;
|
|
3076
|
-
const instructions = stringProperty(event, "instructions");
|
|
3077
|
-
return { type: "auth_url", url, ...(instructions === undefined ? {} : { instructions }) };
|
|
3078
|
-
}
|
|
3079
|
-
if (event.type === "device_code") {
|
|
3080
|
-
const verificationUri = stringProperty(event, "verificationUri");
|
|
3081
|
-
const userCode = stringProperty(event, "userCode");
|
|
3082
|
-
return verificationUri && userCode ? { type: "device_code", verificationUri, userCode } : undefined;
|
|
3083
|
-
}
|
|
3084
|
-
const message = stringProperty(event, "message");
|
|
3085
|
-
if (!message)
|
|
3086
|
-
return undefined;
|
|
3087
|
-
if (event.type === "info") {
|
|
3088
|
-
const links = Array.isArray(event.links) ? event.links.filter(isRecord).flatMap(link => {
|
|
3089
|
-
const url = stringProperty(link, "url");
|
|
3090
|
-
if (!url)
|
|
3091
|
-
return [];
|
|
3092
|
-
const label = stringProperty(link, "label");
|
|
3093
|
-
return [{ ...(label === undefined ? {} : { label }), url }];
|
|
3094
|
-
}) : [];
|
|
3095
|
-
return { type: "info", message, ...(links.length === 0 ? {} : { links }) };
|
|
3096
|
-
}
|
|
3097
|
-
return { type: event.type === "waiting" ? "waiting" : "progress", message };
|
|
3098
|
-
}
|
|
3099
|
-
function workflowResult(command, outcome, message, detail, messageKind, messages) {
|
|
3100
|
-
return {
|
|
3101
|
-
command,
|
|
3102
|
-
outcome,
|
|
3103
|
-
message,
|
|
3104
|
-
...(detail === undefined ? {} : { detail }),
|
|
3105
|
-
...(messageKind === undefined ? {} : { messageKind }),
|
|
3106
|
-
...(messages === undefined ? {} : { messages: Object.freeze([...messages]) }),
|
|
3107
|
-
};
|
|
3108
|
-
}
|
|
3109
|
-
function workflowConfirmation(command, message, detail) {
|
|
3110
|
-
return {
|
|
3111
|
-
command,
|
|
3112
|
-
outcome: "requires-confirmation",
|
|
3113
|
-
message,
|
|
3114
|
-
...(detail === undefined ? {} : { detail }),
|
|
3115
|
-
selectorTitle: "Confirm",
|
|
3116
|
-
options: [
|
|
3117
|
-
{ id: "yes", label: "Yes" },
|
|
3118
|
-
{ id: "no", label: "No" },
|
|
3119
|
-
],
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
function errorMessage(error, fallback) {
|
|
3123
|
-
return error instanceof Error ? error.message : fallback;
|
|
3124
|
-
}
|
|
3125
|
-
function shareCommandFailureDetail(error) {
|
|
3126
|
-
// Compatibility: Pi reports a failed child's stderr, not execFile's command wrapper.
|
|
3127
|
-
if (isRecord(error) && (typeof error.code === "number" || typeof error.signal === "string") && typeof error.stderr === "string") {
|
|
3128
|
-
return error.stderr.trim() || "Unknown error";
|
|
3129
|
-
}
|
|
3130
|
-
return errorMessage(error, "Unknown error");
|
|
3131
|
-
}
|
|
3132
|
-
function commandMissing(error) {
|
|
3133
|
-
return isRecord(error) && error.code === "ENOENT";
|
|
3134
|
-
}
|
|
3135
|
-
function isAbortError(error) {
|
|
3136
|
-
return isRecord(error) && (error.name === "AbortError" || error.code === "ABORT_ERR");
|
|
3137
|
-
}
|
|
3138
|
-
function shareViewerUrl(gistId) {
|
|
3139
|
-
const baseUrl = process.env.PI_SHARE_VIEWER_URL || "https://pi.dev/session/";
|
|
3140
|
-
return `${baseUrl}#${gistId}`;
|
|
3141
|
-
}
|
|
3142
|
-
function workflowOptions(value, idKey, labelKey) {
|
|
3143
|
-
if (!Array.isArray(value))
|
|
3144
|
-
return [];
|
|
3145
|
-
return value.filter(isRecord).flatMap(item => {
|
|
3146
|
-
const id = stringProperty(item, idKey);
|
|
3147
|
-
if (!id)
|
|
3148
|
-
return [];
|
|
3149
|
-
return [{ id, label: stringProperty(item, labelKey) ?? id }];
|
|
3150
|
-
});
|
|
3151
|
-
}
|
|
3152
|
-
function sessionInfoOptions(value) {
|
|
3153
|
-
return value.filter(isRecord).flatMap(info => {
|
|
3154
|
-
const path = stringProperty(info, "path");
|
|
3155
|
-
if (!path)
|
|
3156
|
-
return [];
|
|
3157
|
-
const modified = info.modified instanceof Date ? info.modified.toISOString() : stringProperty(info, "modified") ?? "unknown time";
|
|
3158
|
-
const messageCount = typeof info.messageCount === "number" ? info.messageCount : 0;
|
|
3159
|
-
return [{
|
|
3160
|
-
id: path,
|
|
3161
|
-
label: stringProperty(info, "name") ?? stringProperty(info, "firstMessage") ?? stringProperty(info, "id") ?? path,
|
|
3162
|
-
description: `${messageCount} messages · ${modified}`,
|
|
3163
|
-
}];
|
|
3164
|
-
});
|
|
3165
|
-
}
|
|
3166
|
-
function assertPiExtensionUiContext(value) {
|
|
3167
|
-
assertOwnedUiExtensionUiPort(value);
|
|
3168
|
-
}
|
|
3169
|
-
function dynamicObject(value, key) {
|
|
3170
|
-
const candidate = key === undefined ? value : isRecord(value) ? value[key] : undefined;
|
|
3171
|
-
return isRecord(candidate) ? candidate : {};
|
|
3172
|
-
}
|
|
3173
|
-
function requireCapability(capability, name) {
|
|
3174
|
-
if (typeof capability !== "function")
|
|
3175
|
-
throw new Error(`Pi workflow capability is unavailable: ${name}`);
|
|
3176
|
-
return capability;
|
|
3177
|
-
}
|
|
3178
|
-
function pathArgument(value) {
|
|
3179
|
-
if (!value)
|
|
3180
|
-
return undefined;
|
|
3181
|
-
const quote = value[0];
|
|
3182
|
-
if (quote === '"' || quote === "'") {
|
|
3183
|
-
const closing = value.indexOf(quote, 1);
|
|
3184
|
-
return closing < 0 ? undefined : value.slice(1, closing);
|
|
3185
|
-
}
|
|
3186
|
-
return value.split(/\s/, 1)[0] || undefined;
|
|
3187
|
-
}
|
|
3188
|
-
function settingKeyForCallback(callback) {
|
|
3189
|
-
const keys = {
|
|
3190
|
-
onAutoCompactChange: "autoCompact", onShowImagesChange: "showImages", onImageWidthCellsChange: "imageWidthCells",
|
|
3191
|
-
onAutoResizeImagesChange: "autoResizeImages", onBlockImagesChange: "blockImages", onEnableSkillCommandsChange: "enableSkillCommands",
|
|
3192
|
-
onSteeringModeChange: "steeringMode", onFollowUpModeChange: "followUpMode", onTransportChange: "transport",
|
|
3193
|
-
onHttpIdleTimeoutMsChange: "httpIdleTimeoutMs", onThinkingLevelChange: "thinkingLevel", onThemeChange: "theme", onThemePreview: "theme",
|
|
3194
|
-
onHideThinkingBlockChange: "hideThinkingBlock", onMermaidRenderingModeChange: "mermaidRenderingMode",
|
|
3195
|
-
onShowCacheMissNoticesChange: "showCacheMissNotices", onCollapseChangelogChange: "collapseChangelog",
|
|
3196
|
-
onEnableInstallTelemetryChange: "enableInstallTelemetry", onQuietStartupChange: "quietStartup",
|
|
3197
|
-
onDefaultProjectTrustChange: "defaultProjectTrust", onDoubleEscapeActionChange: "doubleEscapeAction",
|
|
3198
|
-
onTreeFilterModeChange: "treeFilterMode", onShowHardwareCursorChange: "showHardwareCursor",
|
|
3199
|
-
onEditorPaddingXChange: "editorPaddingX", onOutputPadChange: "outputPad", onAutocompleteMaxVisibleChange: "autocompleteMaxVisible",
|
|
3200
|
-
onClearOnShrinkChange: "clearOnShrink", onShowTerminalProgressChange: "showTerminalProgress", onTuiModeChange: "tuiMode",
|
|
3201
|
-
onFullscreenExitOutputChange: "fullscreenExitOutput", onFullscreenScrollbarChange: "fullscreenScrollbar", onWarningsChange: "warnings",
|
|
3202
|
-
};
|
|
3203
|
-
return keys[callback] ?? null;
|
|
3204
|
-
}
|
|
3205
|
-
function agentJsonValue(value) {
|
|
3206
|
-
if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)))
|
|
3207
|
-
return value;
|
|
3208
|
-
if (Array.isArray(value))
|
|
3209
|
-
return value.map(agentJsonValue);
|
|
3210
|
-
if (isRecord(value))
|
|
3211
|
-
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => [key, agentJsonValue(item)]));
|
|
3212
|
-
throw new TypeError("setting value must be JSON serializable");
|
|
3213
|
-
}
|
|
3214
|
-
function isThinkingLevel(value) {
|
|
3215
|
-
return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh"].includes(value);
|
|
3216
|
-
}
|
|
3217
|
-
function settingLabel(callback) {
|
|
3218
|
-
return callback
|
|
3219
|
-
.replace(/^on/, "")
|
|
3220
|
-
.replace(/Change$|Preview$|Cancel$/, "")
|
|
3221
|
-
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
3222
|
-
.replace(/^./, character => character.toUpperCase());
|
|
3223
|
-
}
|
|
3224
|
-
function pinnedHotkeySummary() {
|
|
3225
|
-
return [
|
|
3226
|
-
"Enter: send message · Alt+Enter: queue follow-up",
|
|
3227
|
-
"Escape: cancel/abort · Ctrl+C: clear/exit · Ctrl+D: exit when empty",
|
|
3228
|
-
"Shift+Tab: cycle thinking · Ctrl+P/Shift+Ctrl+P: cycle models · Ctrl+L: select model",
|
|
3229
|
-
"Ctrl+O: expand tools · Ctrl+T: toggle thinking · Ctrl+X: copy message",
|
|
3230
|
-
"Alt+Up: restore queued messages · /: commands · !/!!: bash",
|
|
3231
|
-
].join("\n");
|
|
3232
|
-
}
|
|
3233
|
-
function scopedModelRecords(modelRuntime) {
|
|
3234
|
-
return modelRuntime.getAvailableSnapshot().map(model => ({
|
|
3235
|
-
descriptor: { provider: model.provider, id: model.id, name: model.name ?? model.id },
|
|
3236
|
-
model,
|
|
3237
|
-
}));
|
|
3238
|
-
}
|
|
3239
|
-
function scopedModelReference(value) {
|
|
3240
|
-
if (!isRecord(value) || !isRecord(value.model))
|
|
3241
|
-
return undefined;
|
|
3242
|
-
const provider = stringValue(value.model.provider);
|
|
3243
|
-
const id = stringValue(value.model.id);
|
|
3244
|
-
return provider && id ? `${provider}/${id}` : undefined;
|
|
3245
|
-
}
|
|
3246
|
-
function resolveConfiguredModelIds(patterns, models) {
|
|
3247
|
-
const references = models.map(item => `${item.descriptor.provider}/${item.descriptor.id}`);
|
|
3248
|
-
const resolved = [];
|
|
3249
|
-
for (const pattern of patterns) {
|
|
3250
|
-
const thinkingSuffix = /:(?:off|minimal|low|medium|high|xhigh)$/.exec(pattern);
|
|
3251
|
-
const modelPattern = thinkingSuffix === null ? pattern : pattern.slice(0, -thinkingSuffix[0].length);
|
|
3252
|
-
const matcher = wildcardMatcher(modelPattern);
|
|
3253
|
-
const matches = references.filter((reference, index) => matcher.test(reference) || matcher.test(models[index]?.descriptor.id ?? ""));
|
|
3254
|
-
if (matches.length === 0) {
|
|
3255
|
-
resolved.push(pattern);
|
|
3256
|
-
}
|
|
3257
|
-
else {
|
|
3258
|
-
for (const match of matches)
|
|
3259
|
-
if (!resolved.includes(match))
|
|
3260
|
-
resolved.push(match);
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
return resolved;
|
|
3264
|
-
}
|
|
3265
|
-
function wildcardMatcher(pattern) {
|
|
3266
|
-
let source = "";
|
|
3267
|
-
for (const character of pattern) {
|
|
3268
|
-
if (character === "*")
|
|
3269
|
-
source += ".*";
|
|
3270
|
-
else if (character === "?")
|
|
3271
|
-
source += ".";
|
|
3272
|
-
else
|
|
3273
|
-
source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
3274
|
-
}
|
|
3275
|
-
return new RegExp(`^${source}$`, "i");
|
|
3276
|
-
}
|
|
3277
|
-
function readModel(value) {
|
|
3278
|
-
if (!isRecord(value))
|
|
3279
|
-
return null;
|
|
3280
|
-
const providerId = stringValue(value.provider) ?? stringValue(value.providerId);
|
|
3281
|
-
const modelId = stringValue(value.id) ?? stringValue(value.modelId);
|
|
3282
|
-
if (!providerId || !modelId)
|
|
3283
|
-
return null;
|
|
3284
|
-
return {
|
|
3285
|
-
providerId,
|
|
3286
|
-
modelId,
|
|
3287
|
-
displayName: stringValue(value.name) ?? modelId,
|
|
3288
|
-
};
|
|
3289
|
-
}
|
|
3290
|
-
function readSuggestionReasoning(value) {
|
|
3291
|
-
return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max"
|
|
3292
|
-
? value
|
|
3293
|
-
: "off";
|
|
3294
|
-
}
|
|
3295
|
-
function readThinkingLevel(value) {
|
|
3296
|
-
return value === "off" || value === "minimal" || value === "low" || value === "medium"
|
|
3297
|
-
|| value === "high" || value === "xhigh"
|
|
3298
|
-
? value
|
|
3299
|
-
: "medium";
|
|
3300
|
-
}
|
|
3301
|
-
function readStringArray(value) {
|
|
3302
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
3303
|
-
}
|
|
3304
|
-
function stringValue(value) {
|
|
3305
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3306
|
-
}
|
|
3307
|
-
function messageFallbackKey(message, fallbackIndex) {
|
|
3308
|
-
if (!isRecord(message))
|
|
3309
|
-
return `message-unknown-${fallbackIndex}`;
|
|
3310
|
-
const timestamp = typeof message.timestamp === "number" && Number.isSafeInteger(message.timestamp)
|
|
3311
|
-
? message.timestamp
|
|
3312
|
-
: `index-${fallbackIndex}`;
|
|
3313
|
-
return `message-${stringValue(message.role) ?? "unknown"}-${timestamp}`;
|
|
3314
|
-
}
|
|
3315
|
-
function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
|
|
3316
|
-
return {
|
|
3317
|
-
kind: "extension",
|
|
3318
|
-
id: `extension-diagnostic-${index}`,
|
|
3319
|
-
sourcePath,
|
|
3320
|
-
resolvedPath: null,
|
|
3321
|
-
sourceInfo: null,
|
|
3322
|
-
loaded: false,
|
|
3323
|
-
hidden: false,
|
|
3324
|
-
diagnostic,
|
|
3325
|
-
};
|
|
3326
|
-
}
|
|
3327
|
-
function extensionSourceSummary(value) {
|
|
3328
|
-
if (!isRecord(value))
|
|
3329
|
-
return null;
|
|
3330
|
-
const source = stringProperty(value, "source");
|
|
3331
|
-
const scope = value.scope;
|
|
3332
|
-
const origin = value.origin;
|
|
3333
|
-
const baseDir = value.baseDir;
|
|
3334
|
-
if (!source
|
|
3335
|
-
|| (scope !== "user" && scope !== "project" && scope !== "temporary")
|
|
3336
|
-
|| (origin !== "package" && origin !== "top-level")
|
|
3337
|
-
|| (baseDir !== undefined && typeof baseDir !== "string"))
|
|
3338
|
-
return null;
|
|
3339
|
-
return { source, scope, origin, baseDir: baseDir ?? null };
|
|
3340
|
-
}
|
|
3341
|
-
function collectionResult(value, key) {
|
|
3342
|
-
if (!isRecord(value))
|
|
3343
|
-
return { values: [], diagnostics: [] };
|
|
3344
|
-
const diagnostics = unknownArray(value.diagnostics).map(diagnostic => {
|
|
3345
|
-
if (typeof diagnostic === "string")
|
|
3346
|
-
return diagnostic;
|
|
3347
|
-
if (isRecord(diagnostic)) {
|
|
3348
|
-
const message = stringProperty(diagnostic, "message") ?? String(diagnostic);
|
|
3349
|
-
const path = stringProperty(diagnostic, "path");
|
|
3350
|
-
return path ? `${path}: ${message}` : message;
|
|
3351
|
-
}
|
|
3352
|
-
return String(diagnostic);
|
|
3353
|
-
});
|
|
3354
|
-
return { values: unknownArray(value[key]), diagnostics };
|
|
3355
|
-
}
|
|
3356
|
-
function unknownArray(value) {
|
|
3357
|
-
return Array.isArray(value) ? value : [];
|
|
3358
|
-
}
|
|
3359
|
-
function stringProperty(value, key) {
|
|
3360
|
-
if (!isRecord(value))
|
|
3361
|
-
return undefined;
|
|
3362
|
-
const item = value[key];
|
|
3363
|
-
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
3364
|
-
}
|
|
3365
|
-
function isUnknownModel(value) {
|
|
3366
|
-
return isRecord(value)
|
|
3367
|
-
&& value.provider === "unknown"
|
|
3368
|
-
&& value.id === "unknown"
|
|
3369
|
-
&& value.api === "unknown";
|
|
3370
|
-
}
|
|
3371
|
-
function findExactWorkflowModel(reference, models) {
|
|
3372
|
-
const normalized = reference.trim().toLowerCase();
|
|
3373
|
-
const canonical = models.filter(model => {
|
|
3374
|
-
const provider = stringProperty(model, "provider");
|
|
3375
|
-
const id = stringProperty(model, "id");
|
|
3376
|
-
return provider !== undefined && id !== undefined && `${provider}/${id}`.toLowerCase() === normalized;
|
|
3377
|
-
});
|
|
3378
|
-
if (canonical.length === 1)
|
|
3379
|
-
return canonical[0];
|
|
3380
|
-
const bare = models.filter(model => stringProperty(model, "id")?.toLowerCase() === normalized);
|
|
3381
|
-
return bare.length === 1 ? bare[0] : undefined;
|
|
3382
|
-
}
|
|
3383
|
-
/**
|
|
3384
|
-
* Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
|
|
3385
|
-
* their unprefixed command. A1 presents the product-neutral command once while
|
|
3386
|
-
* leaving Pi's runner free to accept the compatibility alias when typed.
|
|
3387
|
-
*/
|
|
3388
|
-
function isPiPrefixedCompatibilityAlias(command, commands) {
|
|
3389
|
-
const name = stringProperty(command, "name");
|
|
3390
|
-
if (!name?.startsWith("pi-") || name.length === 3)
|
|
3391
|
-
return false;
|
|
3392
|
-
const canonicalName = name.slice(3);
|
|
3393
|
-
const description = stringProperty(command, "description");
|
|
3394
|
-
const sourcePath = extensionCommandSourcePath(command);
|
|
3395
|
-
return commands.some(candidate => candidate !== command
|
|
3396
|
-
&& stringProperty(candidate, "name") === canonicalName
|
|
3397
|
-
&& stringProperty(candidate, "description") === description
|
|
3398
|
-
&& extensionCommandSourcePath(candidate) === sourcePath);
|
|
3399
|
-
}
|
|
3400
|
-
function extensionCommandSourcePath(command) {
|
|
3401
|
-
return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
|
|
3402
|
-
}
|
|
3403
|
-
function compactResourceLabel(path) {
|
|
3404
|
-
const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
3405
|
-
return segments.at(-1) ?? path;
|
|
3406
|
-
}
|
|
3407
|
-
/** A later error may restate a declaration, but cannot make completed arguments incomplete. */
|
|
3408
|
-
function retainCompletedArguments(current, next) {
|
|
3409
|
-
if (current.toolState?.argsComplete !== true || next.toolState === undefined || next.toolState.argsComplete)
|
|
3410
|
-
return next;
|
|
3411
|
-
return { ...next, toolState: { ...next.toolState, argsComplete: true },
|
|
3412
|
-
...(next.toolRendering === undefined || current.toolRendering === undefined ? {} : {
|
|
3413
|
-
toolRendering: { ...next.toolRendering, arguments: current.toolRendering.arguments },
|
|
3414
|
-
}),
|
|
3415
|
-
payload: { ...(isRecord(next.payload) ? next.payload : {}), argsComplete: true } };
|
|
3416
|
-
}
|
|
3417
|
-
/**
|
|
3418
|
-
* Whether two blocks say the same thing. A block that says what it already said is not a
|
|
3419
|
-
* new revision: the shell renders a block once per revision, so bumping one it did not
|
|
3420
|
-
* need re-renders it for nothing.
|
|
3421
|
-
*/
|
|
3422
|
-
function sameBlockContent(left, right) {
|
|
3423
|
-
return left.kind === right.kind
|
|
3424
|
-
&& left.status === right.status
|
|
3425
|
-
&& left.title === right.title
|
|
3426
|
-
&& left.text === right.text
|
|
3427
|
-
&& sameValue(left.toolState, right.toolState)
|
|
3428
|
-
&& sameValue(left.toolRendering, right.toolRendering)
|
|
3429
|
-
&& sameValue(left.payload, right.payload)
|
|
3430
|
-
&& sameValue(left.imageReferences ?? [], right.imageReferences ?? []);
|
|
3431
|
-
}
|
|
3432
|
-
function sameValue(left, right) {
|
|
3433
|
-
if (left === right)
|
|
3434
|
-
return true;
|
|
3435
|
-
if (typeof left !== typeof right || left === null || right === null)
|
|
3436
|
-
return false;
|
|
3437
|
-
if (Array.isArray(left) || Array.isArray(right)) {
|
|
3438
|
-
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
3439
|
-
return false;
|
|
3440
|
-
return left.every((value, index) => sameValue(value, right[index]));
|
|
3441
|
-
}
|
|
3442
|
-
if (typeof left !== "object")
|
|
3443
|
-
return false;
|
|
3444
|
-
const leftKeys = Object.keys(left);
|
|
3445
|
-
const rightRecord = right;
|
|
3446
|
-
if (leftKeys.length !== Object.keys(rightRecord).length)
|
|
3447
|
-
return false;
|
|
3448
|
-
return leftKeys.every(key => Object.hasOwn(rightRecord, key) && sameValue(left[key], rightRecord[key]));
|
|
3449
|
-
}
|
|
3450
|
-
function textFromContent(content) {
|
|
3451
|
-
if (typeof content === "string")
|
|
3452
|
-
return content;
|
|
3453
|
-
if (!Array.isArray(content))
|
|
3454
|
-
return "";
|
|
3455
|
-
return content
|
|
3456
|
-
.map(item => isRecord(item) && item.type === "text" ? stringValue(item.text) ?? "" : "")
|
|
3457
|
-
.filter(text => text.length > 0)
|
|
3458
|
-
.join("\n");
|
|
3459
|
-
}
|
|
3460
|
-
function assistantContent(content) {
|
|
3461
|
-
const result = [];
|
|
3462
|
-
for (const item of content) {
|
|
3463
|
-
if (!isRecord(item))
|
|
3464
|
-
continue;
|
|
3465
|
-
if (item.type === "text")
|
|
3466
|
-
result.push({ type: "text", text: stringValue(item.text) ?? "" });
|
|
3467
|
-
else if (item.type === "thinking") {
|
|
3468
|
-
result.push({
|
|
3469
|
-
type: "thinking",
|
|
3470
|
-
thinking: stringValue(item.thinking) ?? "",
|
|
3471
|
-
...(item.redacted === true ? { redacted: true } : {}),
|
|
3472
|
-
});
|
|
3473
|
-
}
|
|
3474
|
-
else if (item.type === "toolCall") {
|
|
3475
|
-
result.push({
|
|
3476
|
-
type: "toolCall",
|
|
3477
|
-
id: stringValue(item.id) ?? "",
|
|
3478
|
-
name: stringValue(item.name) ?? "unknown",
|
|
3479
|
-
arguments: sanitizeJson(item.arguments),
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
return result;
|
|
3484
|
-
}
|
|
3485
|
-
function contentImageCount(content) {
|
|
3486
|
-
return Array.isArray(content)
|
|
3487
|
-
? content.filter(item => isRecord(item) && item.type === "image").length
|
|
3488
|
-
: 0;
|
|
3489
|
-
}
|
|
3490
|
-
function jsonSummary(value) {
|
|
3491
|
-
const json = sanitizeJson(value);
|
|
3492
|
-
let summary = "";
|
|
3493
|
-
try {
|
|
3494
|
-
summary = JSON.stringify(json);
|
|
3495
|
-
}
|
|
3496
|
-
catch {
|
|
3497
|
-
summary = String(value);
|
|
3498
|
-
}
|
|
3499
|
-
if (summary.length > 512)
|
|
3500
|
-
summary = `${summary.slice(0, 509)}...`;
|
|
3501
|
-
return { summary, json };
|
|
3502
|
-
}
|
|
3503
|
-
function sanitizeJson(value, depth = 0) {
|
|
3504
|
-
if (typeof value === "bigint")
|
|
3505
|
-
return value.toString();
|
|
3506
|
-
if (value === null || ["string", "number", "boolean"].includes(typeof value))
|
|
3507
|
-
return value;
|
|
3508
|
-
if (depth >= 8)
|
|
3509
|
-
return "[truncated]";
|
|
3510
|
-
if (Array.isArray(value))
|
|
3511
|
-
return value.slice(0, 100).map(item => sanitizeJson(item, depth + 1));
|
|
3512
|
-
if (isRecord(value)) {
|
|
3513
|
-
const output = {};
|
|
3514
|
-
for (const [key, item] of Object.entries(value).slice(0, 100)) {
|
|
3515
|
-
output[key] = sanitizeJson(item, depth + 1);
|
|
3516
|
-
}
|
|
3517
|
-
return output;
|
|
3518
|
-
}
|
|
3519
|
-
return String(value);
|
|
3520
|
-
}
|
|
3521
|
-
async function readGitBranch(cwd) {
|
|
3522
|
-
try {
|
|
3523
|
-
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], { cwd, windowsHide: true });
|
|
3524
|
-
const branch = stdout.trim();
|
|
3525
|
-
return branch.length > 0 ? branch : null;
|
|
3526
|
-
}
|
|
3527
|
-
catch {
|
|
3528
|
-
return null;
|
|
3529
|
-
}
|
|
3530
|
-
}
|
|
3531
|
-
function finiteNumber(value) {
|
|
3532
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
3533
|
-
}
|
|
3534
|
-
function isRecord(value) {
|
|
3535
|
-
return typeof value === "object" && value !== null;
|
|
3536
|
-
}
|