@selesai/code 0.5.28 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/credential-print.d.ts +23 -0
- package/dist/cli/credential-print.d.ts.map +1 -0
- package/dist/cli/credential-print.js +117 -0
- package/dist/cli/credential-print.js.map +1 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +3 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +20 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +11 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +10 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/llama/provider.d.ts.map +1 -1
- package/dist/core/llama/provider.js +8 -3
- package/dist/core/llama/provider.js.map +1 -1
- package/dist/core/model-config.d.ts +30 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +6 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +20 -3
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +2 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +5 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +13 -6
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +24 -11
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts +15 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +66 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +19 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/core/tools/bash.d.ts +2 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +34 -5
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +3 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
- package/dist/extensions/pi-intercom/README.md +102 -39
- package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
- package/dist/extensions/pi-intercom/broker/client.ts +315 -12
- package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
- package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
- package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
- package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
- package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
- package/dist/extensions/pi-intercom/config.test.ts +93 -0
- package/dist/extensions/pi-intercom/config.ts +55 -6
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
- package/dist/extensions/pi-intercom/cwd.ts +31 -0
- package/dist/extensions/pi-intercom/extension-api.ts +44 -0
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
- package/dist/extensions/pi-intercom/format-context.ts +32 -0
- package/dist/extensions/pi-intercom/index.ts +742 -145
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
- package/dist/extensions/pi-intercom/package.json +15 -5
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
- package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
- package/dist/extensions/pi-intercom/types.ts +94 -4
- package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
- package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
- package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +88 -0
- package/dist/extensions/pi-subagents/LICENSE +21 -0
- package/dist/extensions/pi-subagents/README.md +158 -69
- package/dist/extensions/pi-subagents/agents/architect.md +5 -4
- package/dist/extensions/pi-subagents/agents/builder.md +6 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +3 -3
- package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +22 -988
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +431 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +281 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +125 -37
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
- package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +17 -13
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +22 -8
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +33 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +32 -14
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +86 -21
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +56 -5
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +92 -11
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +388 -29
- package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +214 -60
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +546 -252
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +553 -154
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +19 -13
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
- package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +22 -1
- package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
- package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +196 -12
- package/dist/extensions/pi-subagents/src/shared/utils.ts +102 -5
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +10 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
- package/dist/extensions/pi-subagents/src/tui/render.ts +55 -34
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +113 -8
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +419 -47
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +62 -22
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +8 -6
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +231 -19
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +48 -7
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +199 -17
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +898 -16
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +198 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +136 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +17 -12
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +39 -2
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +46 -2
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +61 -10
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +43 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +3 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +10 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +16 -46
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +4 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts +12 -0
- package/dist/modes/interactive/external-editor.d.ts.map +1 -0
- package/dist/modes/interactive/external-editor.js +37 -0
- package/dist/modes/interactive/external-editor.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +72 -64
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +14 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +14 -5
- package/docs/environment-variables.md +86 -0
- package/docs/extensions.md +15 -5
- package/docs/index.md +1 -0
- package/docs/models.md +9 -2
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/docs/providers.md +21 -2
- package/docs/rpc.md +23 -6
- package/docs/session-format.md +2 -0
- package/docs/settings.md +1 -1
- package/docs/usage.md +0 -13
- package/examples/extensions/custom-compaction.ts +5 -2
- package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/handoff.ts +11 -3
- package/examples/extensions/message-renderer.ts +3 -3
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/summarize.ts +5 -2
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +3 -1
- package/package.json +16 -7
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@selesai/code";
|
|
2
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
3
|
import { randomUUID } from "crypto";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -7,24 +8,45 @@ import { spawnBrokerIfNeeded } from "./broker/spawn.ts";
|
|
|
7
8
|
import { SessionListOverlay } from "./ui/session-list.ts";
|
|
8
9
|
import { ComposeOverlay, type ComposeResult } from "./ui/compose.ts";
|
|
9
10
|
import { InlineMessageComponent } from "./ui/inline-message.ts";
|
|
10
|
-
import { loadConfig, type IntercomConfig } from "./config.ts";
|
|
11
|
-
import
|
|
11
|
+
import { getAskTimeoutMs, loadConfig, type IntercomConfig } from "./config.ts";
|
|
12
|
+
import { EXTENSION_BUS_FEATURE } from "./types.ts";
|
|
13
|
+
import type { Attachment, BrokerMessage, Message, MessageControl, MessageReceiptStatus, SessionInfo, SessionRegistration } from "./types.ts";
|
|
14
|
+
import {
|
|
15
|
+
INTERCOM_EXTENSION_REGISTER_EVENT,
|
|
16
|
+
INTERCOM_EXTENSION_REGISTRY_READY_EVENT,
|
|
17
|
+
type IntercomExtensionChannel,
|
|
18
|
+
type IntercomExtensionEvent,
|
|
19
|
+
type IntercomExtensionOwner,
|
|
20
|
+
type IntercomExtensionRegistration,
|
|
21
|
+
type IntercomExtensionState,
|
|
22
|
+
} from "./extension-api.ts";
|
|
12
23
|
import { ReplyTracker } from "./reply-tracker.ts";
|
|
24
|
+
import { resolve as resolvePath } from "node:path";
|
|
25
|
+
import { sameCwd } from "./cwd.ts";
|
|
26
|
+
import { formatContextUsage } from "./format-context.ts";
|
|
13
27
|
|
|
14
28
|
const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
|
|
15
29
|
const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
|
|
16
30
|
const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
|
|
17
31
|
const INBOUND_FLUSH_DELAY_MS = 200;
|
|
18
32
|
const INBOUND_IDLE_RETRY_MS = 500;
|
|
33
|
+
const INBOUND_MESSAGE_DEDUPE_MAX = 1000;
|
|
34
|
+
const INBOUND_MESSAGE_DEDUPE_RETENTION_MS = 60 * 60 * 1000;
|
|
19
35
|
const DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX = "subagent-chat";
|
|
20
36
|
const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "SELESAI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
37
|
+
const SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV = "SELESAI_SUBAGENT_ORCHESTRATOR_SESSION_ID";
|
|
38
|
+
const INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
39
|
+
const STABLE_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_STABLE_ID";
|
|
40
|
+
const NAME_POLL_MS_ENV = "PI_INTERCOM_NAME_POLL_MS";
|
|
21
41
|
const SUBAGENT_RUN_ID_ENV = "SELESAI_SUBAGENT_RUN_ID";
|
|
22
42
|
const SUBAGENT_CHILD_AGENT_ENV = "SELESAI_SUBAGENT_CHILD_AGENT";
|
|
23
43
|
const SUBAGENT_CHILD_INDEX_ENV = "SELESAI_SUBAGENT_CHILD_INDEX";
|
|
24
44
|
const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "SELESAI_SUBAGENT_INTERCOM_SESSION_NAME";
|
|
45
|
+
const SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV = "SELESAI_SUBAGENT_SUPERVISOR_CHANNEL_DIR";
|
|
25
46
|
|
|
26
47
|
interface ChildOrchestratorMetadata {
|
|
27
48
|
orchestratorTarget: string;
|
|
49
|
+
orchestratorSessionId?: string;
|
|
28
50
|
runId: string;
|
|
29
51
|
agent: string;
|
|
30
52
|
index: string;
|
|
@@ -78,6 +100,8 @@ function formatAttachments(attachments: Attachment[]): string {
|
|
|
78
100
|
}
|
|
79
101
|
function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null {
|
|
80
102
|
const orchestratorTarget = process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV]?.trim();
|
|
103
|
+
const orchestratorSessionId = process.env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV]?.trim()
|
|
104
|
+
|| process.env[INTERCOM_SESSION_ID_ENV]?.trim();
|
|
81
105
|
const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();
|
|
82
106
|
const agent = process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim();
|
|
83
107
|
const index = process.env[SUBAGENT_CHILD_INDEX_ENV]?.trim();
|
|
@@ -87,6 +111,7 @@ function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null {
|
|
|
87
111
|
const sessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
|
|
88
112
|
return {
|
|
89
113
|
orchestratorTarget,
|
|
114
|
+
...(orchestratorSessionId ? { orchestratorSessionId } : {}),
|
|
90
115
|
runId,
|
|
91
116
|
agent,
|
|
92
117
|
index,
|
|
@@ -381,6 +406,12 @@ function buildPresenceIdentity(pi: ExtensionAPI, sessionId: string): { name: str
|
|
|
381
406
|
name: resolveIntercomPresenceName(pi.getSessionName(), sessionId),
|
|
382
407
|
};
|
|
383
408
|
}
|
|
409
|
+
function resolveConfiguredIntercomSessionId(piSessionId: string, config: IntercomConfig): string {
|
|
410
|
+
return process.env[STABLE_INTERCOM_SESSION_ID_ENV]?.trim() || config.stableId || piSessionId;
|
|
411
|
+
}
|
|
412
|
+
function formatIntercomContactSnippet(sessionId: string): string {
|
|
413
|
+
return `Use pi-intercom: intercom({ action: "send", to: "${sessionId}", message: "..." })`;
|
|
414
|
+
}
|
|
384
415
|
function formatSessionLabel(session: SessionInfo, duplicates: Set<string>): string {
|
|
385
416
|
if (!session.name) {
|
|
386
417
|
return session.id;
|
|
@@ -394,7 +425,7 @@ function formatSessionListRow(session: SessionInfo, currentCwd: string, isSelf:
|
|
|
394
425
|
const tags = [isSelf ? "self" : session.cwd === currentCwd ? "same cwd" : undefined, session.status]
|
|
395
426
|
.filter((tag): tag is string => Boolean(tag));
|
|
396
427
|
const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
397
|
-
return `• ${name} (${shortSessionId(session.id)}) — ${session.cwd} (${session.model})${suffix}`;
|
|
428
|
+
return `• ${name} (${shortSessionId(session.id)}) — ${session.cwd} (${session.model}${formatContextUsage(session)})${suffix}`;
|
|
398
429
|
}
|
|
399
430
|
function previewText(value: unknown, maxLength = 72): string | undefined {
|
|
400
431
|
if (typeof value !== "string") {
|
|
@@ -409,14 +440,53 @@ function previewText(value: unknown, maxLength = 72): string | undefined {
|
|
|
409
440
|
function firstTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
410
441
|
return result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text?.replace(/\*\*/g, "") ?? "";
|
|
411
442
|
}
|
|
443
|
+
function formatMessageTimestamp(timestamp: number | undefined): string | undefined {
|
|
444
|
+
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
|
|
445
|
+
}
|
|
446
|
+
function formatInboundDeliveryMetadata(message: Message): string {
|
|
447
|
+
const parts = [`id ${message.id}`];
|
|
448
|
+
if (typeof message.senderSequence === "number") parts.push(`seq ${message.senderSequence}`);
|
|
449
|
+
if (message.supersedes) parts.push(`supersedes ${message.supersedes}`);
|
|
450
|
+
if (message.retryOf) parts.push(`retry of ${message.retryOf}`);
|
|
451
|
+
const sentAt = formatMessageTimestamp(message.timestamp);
|
|
452
|
+
if (sentAt) parts.push(`sent ${sentAt}`);
|
|
453
|
+
const brokerDeliveredAt = formatMessageTimestamp(message.brokerDeliveredAt);
|
|
454
|
+
if (brokerDeliveredAt) parts.push(`broker delivered ${brokerDeliveredAt}`);
|
|
455
|
+
const receiverReceivedAt = formatMessageTimestamp(message.receiverReceivedAt);
|
|
456
|
+
if (receiverReceivedAt) parts.push(`receiver received ${receiverReceivedAt}`);
|
|
457
|
+
const injectedAt = formatMessageTimestamp(message.injectedAt);
|
|
458
|
+
if (injectedAt) parts.push(`injected ${injectedAt}`);
|
|
459
|
+
return parts.join(" · ");
|
|
460
|
+
}
|
|
461
|
+
function getNamePollMs(): number {
|
|
462
|
+
const configured = process.env[NAME_POLL_MS_ENV];
|
|
463
|
+
if (configured !== undefined) {
|
|
464
|
+
const value = Number(configured);
|
|
465
|
+
if (Number.isFinite(value) && value > 0) {
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return 1000;
|
|
470
|
+
}
|
|
412
471
|
export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
413
472
|
let client: IntercomClient | null = null;
|
|
414
473
|
const config: IntercomConfig = loadConfig();
|
|
474
|
+
const askTimeoutMs = getAskTimeoutMs();
|
|
475
|
+
const localExtensions = new Map<string, {
|
|
476
|
+
registration: IntercomExtensionRegistration;
|
|
477
|
+
channel: IntercomExtensionChannel;
|
|
478
|
+
owner?: IntercomExtensionOwner;
|
|
479
|
+
state?: IntercomExtensionState;
|
|
480
|
+
}>();
|
|
415
481
|
let runtimeContext: ExtensionContext | null = null;
|
|
416
482
|
let currentSessionId: string | null = null;
|
|
483
|
+
let currentIntercomSessionId: string | null = null;
|
|
417
484
|
let currentModel = "unknown";
|
|
418
485
|
let sessionStartedAt: number | null = null;
|
|
419
486
|
let reconnectTimer: NodeJS.Timeout | null = null;
|
|
487
|
+
let namePollTimer: NodeJS.Timeout | null = null;
|
|
488
|
+
let lastPresenceName: string | null = null;
|
|
489
|
+
const previousIntercomSessionId = process.env[INTERCOM_SESSION_ID_ENV];
|
|
420
490
|
let reconnectPromise: Promise<IntercomClient> | null = null;
|
|
421
491
|
let reconnectPromiseGeneration: number | null = null;
|
|
422
492
|
let startupConnectTimer: NodeJS.Timeout | null = null;
|
|
@@ -429,6 +499,75 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
429
499
|
const activeTools = new Map<string, string>();
|
|
430
500
|
const replyTracker = new ReplyTracker();
|
|
431
501
|
const pendingIdleMessages: InboundMessageEntry[] = [];
|
|
502
|
+
const seenInboundMessages = new Map<string, number>();
|
|
503
|
+
const latestOutboundReceipts = new Map<string, { status: MessageReceiptStatus; timestamp: number; detail?: string }>();
|
|
504
|
+
function dismissIncomingAsk(messageId: string): void {
|
|
505
|
+
replyTracker.dismissPendingAsk(messageId);
|
|
506
|
+
const queuedIndex = pendingIdleMessages.findIndex((entry) => entry.message.id === messageId);
|
|
507
|
+
if (queuedIndex >= 0) pendingIdleMessages.splice(queuedIndex, 1);
|
|
508
|
+
}
|
|
509
|
+
function expirePendingIdleMessages(detail: string): void {
|
|
510
|
+
for (const entry of pendingIdleMessages) {
|
|
511
|
+
emitMessageReceipt(entry.message.id, "expired", detail);
|
|
512
|
+
}
|
|
513
|
+
pendingIdleMessages.length = 0;
|
|
514
|
+
}
|
|
515
|
+
function hasSeenInboundMessage(from: SessionInfo, message: Message, now = Date.now()): boolean {
|
|
516
|
+
for (const [key, seenAt] of seenInboundMessages) {
|
|
517
|
+
if (now - seenAt > INBOUND_MESSAGE_DEDUPE_RETENTION_MS) {
|
|
518
|
+
seenInboundMessages.delete(key);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const key = `${from.id}\0${message.id}`;
|
|
522
|
+
if (seenInboundMessages.has(key)) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
seenInboundMessages.set(key, now);
|
|
526
|
+
while (seenInboundMessages.size > INBOUND_MESSAGE_DEDUPE_MAX) {
|
|
527
|
+
const oldestKey = seenInboundMessages.keys().next().value;
|
|
528
|
+
if (typeof oldestKey !== "string") break;
|
|
529
|
+
seenInboundMessages.delete(oldestKey);
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
function emitMessageReceipt(messageId: string, status: MessageReceiptStatus, detail?: string): void {
|
|
534
|
+
try {
|
|
535
|
+
client?.sendMessageReceipt({
|
|
536
|
+
messageId,
|
|
537
|
+
status,
|
|
538
|
+
timestamp: Date.now(),
|
|
539
|
+
...(detail ? { detail } : {}),
|
|
540
|
+
});
|
|
541
|
+
} catch {
|
|
542
|
+
// Receipts are diagnostics; message handling should not fail when the sender disconnects.
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function handleMessageControl(control: MessageControl): void {
|
|
546
|
+
const queuedIndex = pendingIdleMessages.findIndex((entry) => entry.message.id === control.messageId);
|
|
547
|
+
if (control.action === "cancel") {
|
|
548
|
+
if (queuedIndex >= 0) {
|
|
549
|
+
pendingIdleMessages.splice(queuedIndex, 1);
|
|
550
|
+
replyTracker.dismissPendingAsk(control.messageId);
|
|
551
|
+
emitMessageReceipt(control.messageId, "cancelled", "cancelled before injection");
|
|
552
|
+
} else {
|
|
553
|
+
emitMessageReceipt(control.messageId, "cancellation_requested", "message was not queued; it may already be injected or processed");
|
|
554
|
+
}
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (queuedIndex >= 0) {
|
|
559
|
+
pendingIdleMessages.splice(queuedIndex, 1);
|
|
560
|
+
}
|
|
561
|
+
replyTracker.dismissPendingAsk(control.messageId);
|
|
562
|
+
emitMessageReceipt(control.messageId, "superseded", control.supersededBy ? `superseded by ${control.supersededBy}` : undefined);
|
|
563
|
+
}
|
|
564
|
+
function latestDeliveryState(messageId: string | null, fallback: string): string {
|
|
565
|
+
if (!messageId) {
|
|
566
|
+
return fallback;
|
|
567
|
+
}
|
|
568
|
+
const receipt = latestOutboundReceipts.get(messageId);
|
|
569
|
+
return receipt ? receipt.status : fallback;
|
|
570
|
+
}
|
|
432
571
|
let inboundFlushTimer: NodeJS.Timeout | null = null;
|
|
433
572
|
let replyWaiter: {
|
|
434
573
|
from: string;
|
|
@@ -436,7 +575,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
436
575
|
resolve: (message: Message) => void;
|
|
437
576
|
reject: (error: Error) => void;
|
|
438
577
|
} | null = null;
|
|
439
|
-
function waitForReply(from: string, replyTo: string, signal?: AbortSignal): Promise<Message> {
|
|
578
|
+
function waitForReply(from: string, replyTo: string, signal?: AbortSignal, cancelOnAbort?: () => void, getDeliveryState: () => string = () => "unknown"): Promise<Message> {
|
|
440
579
|
if (replyWaiter) {
|
|
441
580
|
return Promise.reject(new Error("Already waiting for a reply"));
|
|
442
581
|
}
|
|
@@ -445,8 +584,9 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
445
584
|
}
|
|
446
585
|
return new Promise((resolve, reject) => {
|
|
447
586
|
const timeout = setTimeout(() => {
|
|
448
|
-
|
|
449
|
-
|
|
587
|
+
const timeoutDescription = askTimeoutMs % 60000 === 0 ? `${askTimeoutMs / 60000} minutes` : `${askTimeoutMs}ms`;
|
|
588
|
+
rejectReplyWaiter(new Error(`No reply from "${from}" for message ${replyTo} within ${timeoutDescription}. Last known delivery state: ${getDeliveryState()}. This waiter timeout is not cancellation; the delivered message may still be queued or actionable in the recipient session.`));
|
|
589
|
+
}, askTimeoutMs);
|
|
450
590
|
const cleanup = () => {
|
|
451
591
|
clearTimeout(timeout);
|
|
452
592
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -455,6 +595,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
455
595
|
}
|
|
456
596
|
};
|
|
457
597
|
const onAbort = () => {
|
|
598
|
+
cancelOnAbort?.();
|
|
458
599
|
cleanup();
|
|
459
600
|
reject(new Error("Cancelled"));
|
|
460
601
|
};
|
|
@@ -490,6 +631,13 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
490
631
|
clearTimeout(startupConnectTimer);
|
|
491
632
|
startupConnectTimer = null;
|
|
492
633
|
}
|
|
634
|
+
function clearNamePollTimer(): void {
|
|
635
|
+
if (!namePollTimer) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
clearInterval(namePollTimer);
|
|
639
|
+
namePollTimer = null;
|
|
640
|
+
}
|
|
493
641
|
function clearInboundFlushTimer(): void {
|
|
494
642
|
if (!inboundFlushTimer) {
|
|
495
643
|
return;
|
|
@@ -532,34 +680,171 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
532
680
|
const lifecycleStatus = activeToolName ? `tool:${activeToolName}` : agentRunning ? "thinking" : "idle";
|
|
533
681
|
return config.status ? `${lifecycleStatus} · ${config.status}` : lifecycleStatus;
|
|
534
682
|
}
|
|
535
|
-
function
|
|
683
|
+
function emitLocalExtensionEvent(namespace: string, event: IntercomExtensionEvent): void {
|
|
684
|
+
try {
|
|
685
|
+
localExtensions.get(namespace)?.registration.onEvent(event);
|
|
686
|
+
} catch {
|
|
687
|
+
// One local extension must not break intercom or other extension channels.
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function createExtensionChannel(namespace: string): IntercomExtensionChannel {
|
|
691
|
+
return {
|
|
692
|
+
namespace,
|
|
693
|
+
snapshot() {
|
|
694
|
+
const extension = localExtensions.get(namespace);
|
|
695
|
+
return {
|
|
696
|
+
connected: Boolean(client?.isConnected()),
|
|
697
|
+
supported: Boolean(client?.supportsFeature(EXTENSION_BUS_FEATURE)),
|
|
698
|
+
...(extension?.owner ? { owner: extension.owner } : {}),
|
|
699
|
+
...(extension?.state ? { state: extension.state } : {}),
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
publish(payload, options = {}) {
|
|
703
|
+
const activeClient = client;
|
|
704
|
+
if (!activeClient?.isConnected()) throw new Error("Intercom is not connected");
|
|
705
|
+
const extension = localExtensions.get(namespace);
|
|
706
|
+
const ownerOnly = options.ownerOnly ?? false;
|
|
707
|
+
const ownerEpoch = ownerOnly ? extension?.owner?.epoch : undefined;
|
|
708
|
+
if (ownerOnly && !ownerEpoch) throw new Error(`No owner is available for ${namespace}`);
|
|
709
|
+
activeClient.sendExtensionMessage({
|
|
710
|
+
type: "extension_publish",
|
|
711
|
+
namespace,
|
|
712
|
+
audience: options.audience ?? "owner",
|
|
713
|
+
...(ownerOnly ? { ownerOnly: true, ownerEpoch } : {}),
|
|
714
|
+
payload,
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
commitState(payload, expectedRevision) {
|
|
718
|
+
const activeClient = client;
|
|
719
|
+
if (!activeClient?.isConnected()) throw new Error("Intercom is not connected");
|
|
720
|
+
const extension = localExtensions.get(namespace);
|
|
721
|
+
const ownerEpoch = extension?.owner?.epoch;
|
|
722
|
+
if (!ownerEpoch || extension.owner?.sessionId !== activeClient.sessionId) {
|
|
723
|
+
throw new Error(`Current session is not the owner of ${namespace}`);
|
|
724
|
+
}
|
|
725
|
+
activeClient.sendExtensionMessage({
|
|
726
|
+
type: "extension_state_commit",
|
|
727
|
+
namespace,
|
|
728
|
+
ownerEpoch,
|
|
729
|
+
expectedRevision: expectedRevision ?? extension.state?.revision ?? 0,
|
|
730
|
+
payload,
|
|
731
|
+
});
|
|
732
|
+
},
|
|
733
|
+
async listSessions() {
|
|
734
|
+
const activeClient = client;
|
|
735
|
+
if (!activeClient?.isConnected()) throw new Error("Intercom is not connected");
|
|
736
|
+
return activeClient.listSessions();
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function currentExtensionCapabilities() {
|
|
741
|
+
return [...localExtensions.values()].map(({ registration }) => ({
|
|
742
|
+
namespace: registration.namespace,
|
|
743
|
+
ownerEligible: registration.ownerEligible,
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
function registerLocalExtension(registration: IntercomExtensionRegistration): void {
|
|
747
|
+
if (!/^[a-z0-9][a-z0-9._/-]{0,63}$/.test(registration.namespace)) {
|
|
748
|
+
throw new Error(`Invalid intercom extension namespace: ${registration.namespace}`);
|
|
749
|
+
}
|
|
750
|
+
if (localExtensions.has(registration.namespace)) {
|
|
751
|
+
throw new Error(`Intercom extension namespace already registered: ${registration.namespace}`);
|
|
752
|
+
}
|
|
753
|
+
const channel = createExtensionChannel(registration.namespace);
|
|
754
|
+
localExtensions.set(registration.namespace, { registration, channel });
|
|
755
|
+
const activeClient = client;
|
|
756
|
+
const connected = Boolean(activeClient?.isConnected());
|
|
757
|
+
const supported = Boolean(activeClient?.supportsFeature(EXTENSION_BUS_FEATURE));
|
|
758
|
+
// Write the capability update before exposing a connected channel. Socket
|
|
759
|
+
// framing preserves this order if onReady publishes synchronously.
|
|
760
|
+
if (activeClient && connected && supported) {
|
|
761
|
+
activeClient.updateExtensionCapabilities(currentExtensionCapabilities());
|
|
762
|
+
}
|
|
763
|
+
registration.onReady(channel);
|
|
764
|
+
if (connected) {
|
|
765
|
+
emitLocalExtensionEvent(registration.namespace, { type: "connection", connected: true, supported });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function buildRegistration(): SessionRegistration {
|
|
536
769
|
const liveContext = getLiveContext();
|
|
537
770
|
if (!liveContext || !currentSessionId || sessionStartedAt === null) {
|
|
538
771
|
throw new Error("Intercom runtime not initialized");
|
|
539
772
|
}
|
|
540
773
|
|
|
541
|
-
const identity = buildPresenceIdentity(pi, currentSessionId);
|
|
774
|
+
const identity = buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId);
|
|
542
775
|
return {
|
|
543
776
|
name: identity.name,
|
|
544
|
-
cwd: liveContext.cwd
|
|
777
|
+
cwd: liveContext.cwd,
|
|
545
778
|
model: currentModel,
|
|
546
779
|
pid: process.pid,
|
|
547
780
|
startedAt: sessionStartedAt,
|
|
548
781
|
lastActivity: Date.now(),
|
|
549
782
|
status: currentStatus(),
|
|
783
|
+
...(localExtensions.size > 0
|
|
784
|
+
? {
|
|
785
|
+
extensions: currentExtensionCapabilities(),
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
// Snapshot the live session's context-window usage for presence. getContextUsage()
|
|
791
|
+
// (stock SDK) reports { tokens, contextWindow, percent }, with tokens/percent null
|
|
792
|
+
// right after a compaction (before the next assistant response). We emit null in
|
|
793
|
+
// that case to CLEAR a peer's stale value rather than freeze the old percentage.
|
|
794
|
+
// Feature-detected so an older runtime without getContextUsage() just omits it.
|
|
795
|
+
function currentContextUsage(): { contextPct?: number | null; contextTokens?: number | null; contextWindow?: number } {
|
|
796
|
+
const usage = getLiveContext()?.getContextUsage?.();
|
|
797
|
+
if (!usage) {
|
|
798
|
+
return {};
|
|
799
|
+
}
|
|
800
|
+
const result: { contextPct?: number | null; contextTokens?: number | null; contextWindow?: number } = {
|
|
801
|
+
contextPct: typeof usage.percent === "number" && Number.isFinite(usage.percent) ? Math.round(usage.percent) : null,
|
|
802
|
+
contextTokens: typeof usage.tokens === "number" && Number.isFinite(usage.tokens) ? usage.tokens : null,
|
|
550
803
|
};
|
|
804
|
+
if (typeof usage.contextWindow === "number" && usage.contextWindow > 0) {
|
|
805
|
+
result.contextWindow = usage.contextWindow;
|
|
806
|
+
}
|
|
807
|
+
return result;
|
|
551
808
|
}
|
|
809
|
+
|
|
552
810
|
function syncPresenceIdentity(sessionId: string): void {
|
|
553
811
|
if (!client || !getLiveContext()) {
|
|
554
812
|
return;
|
|
555
813
|
}
|
|
556
|
-
|
|
814
|
+
const identity = buildPresenceIdentity(pi, currentIntercomSessionId ?? sessionId);
|
|
815
|
+
lastPresenceName = identity.name;
|
|
816
|
+
client.updatePresence({ ...identity, status: currentStatus(), ...currentContextUsage() });
|
|
817
|
+
}
|
|
818
|
+
function startNamePoll(): void {
|
|
819
|
+
clearNamePollTimer();
|
|
820
|
+
lastPresenceName = currentSessionId ? buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId).name : null;
|
|
821
|
+
namePollTimer = setInterval(() => {
|
|
822
|
+
if (!currentSessionId || !getLiveContext()) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const identity = buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId);
|
|
826
|
+
if (identity.name !== lastPresenceName) {
|
|
827
|
+
syncPresenceIdentity(currentSessionId);
|
|
828
|
+
}
|
|
829
|
+
}, getNamePollMs());
|
|
830
|
+
namePollTimer.unref?.();
|
|
831
|
+
}
|
|
832
|
+
function publishIntercomSessionId(sessionId: string): void {
|
|
833
|
+
process.env[INTERCOM_SESSION_ID_ENV] = sessionId;
|
|
834
|
+
}
|
|
835
|
+
function restoreIntercomSessionId(): void {
|
|
836
|
+
if (previousIntercomSessionId === undefined) {
|
|
837
|
+
delete process.env[INTERCOM_SESSION_ID_ENV];
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
process.env[INTERCOM_SESSION_ID_ENV] = previousIntercomSessionId;
|
|
557
841
|
}
|
|
558
842
|
function syncPresenceStatus(): void {
|
|
559
843
|
if (!client || !currentSessionId || !getLiveContext()) {
|
|
560
844
|
return;
|
|
561
845
|
}
|
|
562
|
-
|
|
846
|
+
// context% rides the status heartbeat so peers see live usage at turn boundaries.
|
|
847
|
+
client.updatePresence({ status: currentStatus(), ...currentContextUsage() });
|
|
563
848
|
}
|
|
564
849
|
function currentSessionTargetMatches(to: string, resolvedTo?: string | null, activeClient?: IntercomClient): boolean {
|
|
565
850
|
const targets = new Set<string>();
|
|
@@ -568,29 +853,46 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
568
853
|
if (trimmed) targets.add(trimmed.toLowerCase());
|
|
569
854
|
};
|
|
570
855
|
addTarget(currentSessionId);
|
|
856
|
+
addTarget(currentIntercomSessionId);
|
|
571
857
|
addTarget(activeClient?.sessionId);
|
|
572
858
|
addTarget(pi.getSessionName());
|
|
573
|
-
if (currentSessionId) addTarget(buildPresenceIdentity(pi, currentSessionId).name);
|
|
859
|
+
if (currentSessionId) addTarget(buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId).name);
|
|
574
860
|
return Boolean(resolvedTo && activeClient?.sessionId && resolvedTo === activeClient.sessionId)
|
|
575
861
|
|| targets.has(to.trim().toLowerCase());
|
|
576
862
|
}
|
|
577
|
-
function
|
|
863
|
+
function shouldTriggerInboundMessage(entry: InboundMessageEntry, forceTrigger = false): boolean {
|
|
864
|
+
if (forceTrigger) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
if (config.inboundTrigger === "always") {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
if (config.inboundTrigger === "replies") {
|
|
871
|
+
return Boolean(entry.message.replyTo);
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
function sendIncomingMessage(entry: InboundMessageEntry, delivery: "trigger" | "followUp", generation = runtimeGeneration, forceTrigger = false): void {
|
|
578
876
|
if (runtimeStarted && !getLiveContext(runtimeContext, generation)) {
|
|
579
877
|
return;
|
|
580
878
|
}
|
|
879
|
+
const injectedMessage = { ...entry.message, injectedAt: Date.now() };
|
|
880
|
+
emitMessageReceipt(injectedMessage.id, "injected");
|
|
881
|
+
const deliveredEntry = { ...entry, message: injectedMessage };
|
|
581
882
|
if (delivery !== "followUp") {
|
|
582
|
-
replyTracker.queueTurnContext({ from: entry.from, message:
|
|
883
|
+
replyTracker.queueTurnContext({ from: entry.from, message: injectedMessage, receivedAt: Date.now() });
|
|
583
884
|
}
|
|
584
885
|
const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
|
|
585
886
|
const replyInstruction = entry.replyCommand ? `\n\nTo reply, use the intercom tool: ${entry.replyCommand}` : "";
|
|
887
|
+
const deliveryMetadata = formatInboundDeliveryMetadata(injectedMessage);
|
|
586
888
|
pi.sendMessage(
|
|
587
889
|
{
|
|
588
890
|
customType: "intercom_message",
|
|
589
|
-
content: `**📨 From ${senderDisplay}** (${entry.from.cwd})${replyInstruction}\n\n${entry.bodyText}`,
|
|
891
|
+
content: `**📨 From ${senderDisplay}** (${entry.from.cwd})${replyInstruction}\n\n_${deliveryMetadata}_\n\n${entry.bodyText}`,
|
|
590
892
|
display: true,
|
|
591
|
-
details:
|
|
893
|
+
details: deliveredEntry,
|
|
592
894
|
},
|
|
593
|
-
delivery === "trigger"
|
|
895
|
+
delivery === "trigger" && shouldTriggerInboundMessage(entry, forceTrigger)
|
|
594
896
|
? { triggerTurn: true }
|
|
595
897
|
: { deliverAs: "followUp" }
|
|
596
898
|
);
|
|
@@ -634,6 +936,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
634
936
|
}
|
|
635
937
|
function queueIdleMessage(entry: InboundMessageEntry): void {
|
|
636
938
|
pendingIdleMessages.push(entry);
|
|
939
|
+
emitMessageReceipt(entry.message.id, "queued");
|
|
637
940
|
scheduleInboundFlush();
|
|
638
941
|
}
|
|
639
942
|
function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message): void {
|
|
@@ -642,25 +945,34 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
642
945
|
if (!liveContext) {
|
|
643
946
|
return;
|
|
644
947
|
}
|
|
948
|
+
const receiverReceivedAt = Date.now();
|
|
949
|
+
if (hasSeenInboundMessage(from, message, receiverReceivedAt)) {
|
|
950
|
+
emitMessageReceipt(message.id, "acknowledged", "duplicate message id suppressed");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const receivedMessage = { ...message, receiverReceivedAt };
|
|
954
|
+
emitMessageReceipt(receivedMessage.id, "receiver_received");
|
|
645
955
|
if (replyWaiter) {
|
|
646
956
|
const senderTarget = from.name || from.id;
|
|
647
957
|
const fromMatches = senderTarget.toLowerCase() === replyWaiter.from.toLowerCase()
|
|
648
958
|
|| from.id === replyWaiter.from;
|
|
649
|
-
const replyMatches =
|
|
959
|
+
const replyMatches = receivedMessage.replyTo === replyWaiter.replyTo;
|
|
650
960
|
if (fromMatches && replyMatches) {
|
|
651
|
-
|
|
961
|
+
emitMessageReceipt(receivedMessage.id, "acknowledged", "matched reply waiter");
|
|
962
|
+
replyWaiter.resolve(receivedMessage);
|
|
652
963
|
return;
|
|
653
964
|
}
|
|
654
965
|
}
|
|
655
|
-
const attachmentText =
|
|
656
|
-
? formatAttachments(
|
|
966
|
+
const attachmentText = receivedMessage.content.attachments?.length
|
|
967
|
+
? formatAttachments(receivedMessage.content.attachments)
|
|
657
968
|
: "";
|
|
658
|
-
const bodyText = `${
|
|
659
|
-
const replyCommand = config.replyHint &&
|
|
969
|
+
const bodyText = `${receivedMessage.content.text}${attachmentText}`;
|
|
970
|
+
const replyCommand = config.replyHint && receivedMessage.expectsReply
|
|
660
971
|
? `intercom({ action: "reply", message: "..." })`
|
|
661
972
|
: undefined;
|
|
662
|
-
replyTracker.recordIncomingMessage(from,
|
|
663
|
-
|
|
973
|
+
replyTracker.recordIncomingMessage(from, receivedMessage, receiverReceivedAt);
|
|
974
|
+
emitMessageReceipt(receivedMessage.id, "acknowledged", "accepted by receiver");
|
|
975
|
+
const entry = { from, message: receivedMessage, replyCommand, bodyText };
|
|
664
976
|
void (async () => {
|
|
665
977
|
const activeContext = getLiveContext(liveContext, messageGeneration);
|
|
666
978
|
if (!activeContext) {
|
|
@@ -676,7 +988,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
676
988
|
replyTo: message.id,
|
|
677
989
|
});
|
|
678
990
|
if (result.delivered && getLiveContext(liveContext, messageGeneration)) {
|
|
679
|
-
|
|
991
|
+
dismissIncomingAsk(message.id);
|
|
680
992
|
}
|
|
681
993
|
} catch {
|
|
682
994
|
// Best-effort reply; keep the busy non-interactive session running either way.
|
|
@@ -693,6 +1005,83 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
693
1005
|
})();
|
|
694
1006
|
}
|
|
695
1007
|
function attachClientHandlers(nextClient: IntercomClient): void {
|
|
1008
|
+
nextClient.onBrokerMessage((message: BrokerMessage) => {
|
|
1009
|
+
if (client !== nextClient) return;
|
|
1010
|
+
switch (message.type) {
|
|
1011
|
+
case "registered": {
|
|
1012
|
+
const supported = message.features?.includes(EXTENSION_BUS_FEATURE) ?? false;
|
|
1013
|
+
if (supported && localExtensions.size > 0) {
|
|
1014
|
+
nextClient.updateExtensionCapabilities(currentExtensionCapabilities());
|
|
1015
|
+
}
|
|
1016
|
+
for (const namespace of localExtensions.keys()) {
|
|
1017
|
+
emitLocalExtensionEvent(namespace, { type: "connection", connected: true, supported });
|
|
1018
|
+
}
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
case "extension_owner": {
|
|
1022
|
+
const extension = localExtensions.get(message.namespace);
|
|
1023
|
+
if (!extension) break;
|
|
1024
|
+
extension.owner = message.ownerId && message.ownerEpoch
|
|
1025
|
+
? { sessionId: message.ownerId, epoch: message.ownerEpoch }
|
|
1026
|
+
: undefined;
|
|
1027
|
+
emitLocalExtensionEvent(message.namespace, { type: "owner", ...(extension.owner ? { owner: extension.owner } : {}) });
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
case "extension_message": {
|
|
1031
|
+
const extension = localExtensions.get(message.namespace);
|
|
1032
|
+
if (!extension) break;
|
|
1033
|
+
emitLocalExtensionEvent(message.namespace, {
|
|
1034
|
+
type: "message",
|
|
1035
|
+
fromSessionId: message.fromSessionId,
|
|
1036
|
+
...(message.ownerId && message.ownerEpoch
|
|
1037
|
+
? { owner: { sessionId: message.ownerId, epoch: message.ownerEpoch } }
|
|
1038
|
+
: {}),
|
|
1039
|
+
payload: message.payload,
|
|
1040
|
+
});
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case "extension_state": {
|
|
1044
|
+
const extension = localExtensions.get(message.namespace);
|
|
1045
|
+
if (!extension) break;
|
|
1046
|
+
extension.state = { revision: message.revision, payload: message.payload };
|
|
1047
|
+
emitLocalExtensionEvent(message.namespace, { type: "state", state: extension.state });
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
case "extension_state_result":
|
|
1051
|
+
emitLocalExtensionEvent(message.namespace, {
|
|
1052
|
+
type: "state_result",
|
|
1053
|
+
committed: message.committed,
|
|
1054
|
+
revision: message.revision,
|
|
1055
|
+
...(message.reason ? { reason: message.reason } : {}),
|
|
1056
|
+
});
|
|
1057
|
+
break;
|
|
1058
|
+
case "message_receipt":
|
|
1059
|
+
latestOutboundReceipts.set(message.receipt.messageId, {
|
|
1060
|
+
status: message.receipt.status,
|
|
1061
|
+
timestamp: message.receipt.timestamp,
|
|
1062
|
+
...(message.receipt.detail ? { detail: message.receipt.detail } : {}),
|
|
1063
|
+
});
|
|
1064
|
+
break;
|
|
1065
|
+
case "message_control":
|
|
1066
|
+
handleMessageControl(message.control);
|
|
1067
|
+
break;
|
|
1068
|
+
case "session_joined":
|
|
1069
|
+
for (const namespace of localExtensions.keys()) {
|
|
1070
|
+
emitLocalExtensionEvent(namespace, { type: "session_joined", session: message.session });
|
|
1071
|
+
}
|
|
1072
|
+
break;
|
|
1073
|
+
case "session_left":
|
|
1074
|
+
for (const namespace of localExtensions.keys()) {
|
|
1075
|
+
emitLocalExtensionEvent(namespace, { type: "session_left", sessionId: message.sessionId });
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
case "presence_update":
|
|
1079
|
+
for (const namespace of localExtensions.keys()) {
|
|
1080
|
+
emitLocalExtensionEvent(namespace, { type: "presence_update", session: message.session });
|
|
1081
|
+
}
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
696
1085
|
nextClient.on("message", (from, message) => {
|
|
697
1086
|
const liveContext = getLiveContext();
|
|
698
1087
|
if (client !== nextClient || !liveContext) {
|
|
@@ -705,6 +1094,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
705
1094
|
return;
|
|
706
1095
|
}
|
|
707
1096
|
rejectReplyWaiter(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
|
|
1097
|
+
for (const [namespace, extension] of localExtensions) {
|
|
1098
|
+
extension.owner = undefined;
|
|
1099
|
+
emitLocalExtensionEvent(namespace, { type: "connection", connected: false, supported: false });
|
|
1100
|
+
emitLocalExtensionEvent(namespace, { type: "owner" });
|
|
1101
|
+
}
|
|
708
1102
|
client = null;
|
|
709
1103
|
if (!shuttingDown && !disposed) {
|
|
710
1104
|
clearReconnectTimer();
|
|
@@ -756,7 +1150,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
756
1150
|
attachClientHandlers(nextClient);
|
|
757
1151
|
try {
|
|
758
1152
|
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
759
|
-
await nextClient.connect(buildRegistration());
|
|
1153
|
+
await nextClient.connect(buildRegistration(), currentIntercomSessionId ?? currentSessionId);
|
|
760
1154
|
if (!getLiveContext(contextAtStart, generationAtStart)) {
|
|
761
1155
|
await nextClient.disconnect();
|
|
762
1156
|
throw new Error("Intercom runtime no longer active");
|
|
@@ -792,17 +1186,39 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
792
1186
|
const lowerName = nameOrId.toLowerCase();
|
|
793
1187
|
const byName = sessions.filter(s => s.name?.toLowerCase() === lowerName);
|
|
794
1188
|
if (byName.length > 1) {
|
|
795
|
-
|
|
1189
|
+
const ids = byName.map(s => shortSessionId(s.id)).join(", ");
|
|
1190
|
+
throw new Error(`Multiple sessions named "${nameOrId}" are connected. Address one by the id shown in parentheses by "list" (${ids}).`);
|
|
796
1191
|
}
|
|
797
|
-
|
|
1192
|
+
if (byName.length === 1) {
|
|
1193
|
+
return byName[0]!.id;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const byIdPrefix = sessions.filter(s => s.id.startsWith(nameOrId));
|
|
1197
|
+
if (byIdPrefix.length === 1) {
|
|
1198
|
+
return byIdPrefix[0]!.id;
|
|
1199
|
+
}
|
|
1200
|
+
if (byIdPrefix.length > 1) {
|
|
1201
|
+
throw new Error(`Multiple sessions match ID prefix "${nameOrId}". Use a longer session ID prefix.`);
|
|
1202
|
+
}
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
async function resolveSupervisorTarget(activeClient: IntercomClient, metadata: ChildOrchestratorMetadata): Promise<string> {
|
|
1206
|
+
if (metadata.orchestratorSessionId) {
|
|
1207
|
+
const bySessionId = await resolveSessionTarget(activeClient, metadata.orchestratorSessionId);
|
|
1208
|
+
if (bySessionId) {
|
|
1209
|
+
return bySessionId;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return await resolveSessionTarget(activeClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
|
|
798
1213
|
}
|
|
799
1214
|
function deliverLocalSubagentRelayMessage(sender: "subagent-control" | "subagent-result", status: string, messageText: string): void {
|
|
1215
|
+
const liveContext = getLiveContext();
|
|
800
1216
|
const now = Date.now();
|
|
801
1217
|
sendIncomingMessage({
|
|
802
1218
|
from: {
|
|
803
1219
|
id: sender,
|
|
804
1220
|
name: sender,
|
|
805
|
-
cwd:
|
|
1221
|
+
cwd: liveContext?.cwd ?? "",
|
|
806
1222
|
model: sender,
|
|
807
1223
|
pid: process.pid,
|
|
808
1224
|
startedAt: now,
|
|
@@ -815,7 +1231,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
815
1231
|
content: { text: messageText },
|
|
816
1232
|
},
|
|
817
1233
|
bodyText: messageText,
|
|
818
|
-
}, "trigger");
|
|
1234
|
+
}, "trigger", runtimeGeneration, true);
|
|
819
1235
|
}
|
|
820
1236
|
function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void {
|
|
821
1237
|
pi.appendEntry(entryType, {
|
|
@@ -825,6 +1241,49 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
825
1241
|
timestamp: Date.now(),
|
|
826
1242
|
});
|
|
827
1243
|
}
|
|
1244
|
+
function startSessionRuntime(ctx: ExtensionContext): void {
|
|
1245
|
+
const previousClient = client;
|
|
1246
|
+
shuttingDown = false;
|
|
1247
|
+
disposed = false;
|
|
1248
|
+
runtimeStarted = true;
|
|
1249
|
+
runtimeGeneration += 1;
|
|
1250
|
+
reconnectAttempt = 0;
|
|
1251
|
+
clearReconnectTimer();
|
|
1252
|
+
clearStartupConnectTimer();
|
|
1253
|
+
clearNamePollTimer();
|
|
1254
|
+
clearInboundFlushTimer();
|
|
1255
|
+
rejectReplyWaiter(new Error("Session replaced"));
|
|
1256
|
+
replyTracker.reset();
|
|
1257
|
+
expirePendingIdleMessages("receiver session replaced before injection");
|
|
1258
|
+
if (previousClient) {
|
|
1259
|
+
client = null;
|
|
1260
|
+
void previousClient.disconnect().catch(() => undefined);
|
|
1261
|
+
}
|
|
1262
|
+
runtimeContext = ctx;
|
|
1263
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
1264
|
+
currentIntercomSessionId = resolveConfiguredIntercomSessionId(currentSessionId, config);
|
|
1265
|
+
publishIntercomSessionId(currentIntercomSessionId);
|
|
1266
|
+
currentModel = ctx.model?.id ?? "unknown";
|
|
1267
|
+
sessionStartedAt = Date.now();
|
|
1268
|
+
lastPresenceName = buildPresenceIdentity(pi, currentIntercomSessionId).name;
|
|
1269
|
+
agentRunning = false;
|
|
1270
|
+
activeTools.clear();
|
|
1271
|
+
startNamePoll();
|
|
1272
|
+
const startupGeneration = runtimeGeneration;
|
|
1273
|
+
startupConnectTimer = setTimeout(() => {
|
|
1274
|
+
startupConnectTimer = null;
|
|
1275
|
+
if (!getLiveContext(ctx, startupGeneration)) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
void ensureConnected("startup").catch(() => {
|
|
1279
|
+
if (!getLiveContext(ctx, startupGeneration)) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
client = null;
|
|
1283
|
+
scheduleReconnect();
|
|
1284
|
+
});
|
|
1285
|
+
}, 0);
|
|
1286
|
+
}
|
|
828
1287
|
function emitResultDelivery(requestId: string | undefined, delivered: boolean, error?: unknown): void {
|
|
829
1288
|
if (!requestId) return;
|
|
830
1289
|
pi.events.emit(SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT, {
|
|
@@ -892,14 +1351,28 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
892
1351
|
}
|
|
893
1352
|
})();
|
|
894
1353
|
}
|
|
895
|
-
pi.events.on(
|
|
1354
|
+
const unsubscribeExtensionRegister = pi.events.on(INTERCOM_EXTENSION_REGISTER_EVENT, (payload) => {
|
|
1355
|
+
if (!payload || typeof payload !== "object") return;
|
|
1356
|
+
const registration = payload as Partial<IntercomExtensionRegistration>;
|
|
1357
|
+
if (
|
|
1358
|
+
typeof registration.namespace !== "string"
|
|
1359
|
+
|| typeof registration.ownerEligible !== "boolean"
|
|
1360
|
+
|| typeof registration.onEvent !== "function"
|
|
1361
|
+
|| typeof registration.onReady !== "function"
|
|
1362
|
+
) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
registerLocalExtension(registration as IntercomExtensionRegistration);
|
|
1366
|
+
});
|
|
1367
|
+
pi.events.emit(INTERCOM_EXTENSION_REGISTRY_READY_EVENT, { version: 1 });
|
|
1368
|
+
const unsubscribeSubagentControlIntercom = pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => {
|
|
896
1369
|
relaySubagentIntercomPayload(payload, {
|
|
897
1370
|
sender: "subagent-control",
|
|
898
1371
|
status: "needs_attention",
|
|
899
1372
|
errorEntryType: "intercom_control_error",
|
|
900
1373
|
});
|
|
901
1374
|
});
|
|
902
|
-
pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => {
|
|
1375
|
+
const unsubscribeSubagentResultIntercom = pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => {
|
|
903
1376
|
relaySubagentIntercomPayload(payload, {
|
|
904
1377
|
sender: "subagent-result",
|
|
905
1378
|
status: "result",
|
|
@@ -911,44 +1384,23 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
911
1384
|
if (!config.enabled) {
|
|
912
1385
|
return;
|
|
913
1386
|
}
|
|
914
|
-
|
|
915
|
-
disposed = false;
|
|
916
|
-
runtimeStarted = true;
|
|
917
|
-
runtimeGeneration += 1;
|
|
918
|
-
reconnectAttempt = 0;
|
|
919
|
-
clearReconnectTimer();
|
|
920
|
-
clearStartupConnectTimer();
|
|
921
|
-
runtimeContext = ctx;
|
|
922
|
-
currentSessionId = ctx.sessionManager.getSessionId();
|
|
923
|
-
currentModel = ctx.model?.id ?? "unknown";
|
|
924
|
-
sessionStartedAt = Date.now();
|
|
925
|
-
agentRunning = false;
|
|
926
|
-
activeTools.clear();
|
|
927
|
-
const startupGeneration = runtimeGeneration;
|
|
928
|
-
startupConnectTimer = setTimeout(() => {
|
|
929
|
-
startupConnectTimer = null;
|
|
930
|
-
if (!getLiveContext(ctx, startupGeneration)) {
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
void ensureConnected("startup").catch(() => {
|
|
934
|
-
if (!getLiveContext(ctx, startupGeneration)) {
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
client = null;
|
|
938
|
-
scheduleReconnect();
|
|
939
|
-
});
|
|
940
|
-
}, 0);
|
|
1387
|
+
startSessionRuntime(ctx);
|
|
941
1388
|
});
|
|
942
1389
|
|
|
943
1390
|
pi.on("session_shutdown", async () => {
|
|
1391
|
+
unsubscribeExtensionRegister();
|
|
1392
|
+
unsubscribeSubagentControlIntercom();
|
|
1393
|
+
unsubscribeSubagentResultIntercom();
|
|
944
1394
|
shuttingDown = true;
|
|
945
1395
|
disposed = true;
|
|
946
1396
|
runtimeGeneration += 1;
|
|
947
1397
|
clearStartupConnectTimer();
|
|
948
1398
|
clearReconnectTimer();
|
|
1399
|
+
clearNamePollTimer();
|
|
1400
|
+
restoreIntercomSessionId();
|
|
949
1401
|
rejectReplyWaiter(new Error("Session shutting down"));
|
|
950
1402
|
replyTracker.reset();
|
|
951
|
-
|
|
1403
|
+
expirePendingIdleMessages("receiver session shut down before injection");
|
|
952
1404
|
clearInboundFlushTimer();
|
|
953
1405
|
agentRunning = false;
|
|
954
1406
|
activeTools.clear();
|
|
@@ -958,6 +1410,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
958
1410
|
}
|
|
959
1411
|
runtimeContext = null;
|
|
960
1412
|
currentSessionId = null;
|
|
1413
|
+
currentIntercomSessionId = null;
|
|
961
1414
|
sessionStartedAt = null;
|
|
962
1415
|
});
|
|
963
1416
|
pi.on("turn_end", () => {
|
|
@@ -999,19 +1452,28 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
999
1452
|
scheduleInboundFlush(0);
|
|
1000
1453
|
});
|
|
1001
1454
|
pi.on("turn_start", (_event, ctx) => {
|
|
1455
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1456
|
+
if (!currentSessionId || sessionId !== currentSessionId) {
|
|
1457
|
+
if (!config.enabled) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
startSessionRuntime(ctx);
|
|
1461
|
+
replyTracker.beginTurn();
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1002
1464
|
if (!getLiveContext(ctx)) {
|
|
1003
1465
|
return;
|
|
1004
1466
|
}
|
|
1005
|
-
|
|
1006
|
-
syncPresenceIdentity(ctx.sessionManager.getSessionId());
|
|
1467
|
+
syncPresenceIdentity(sessionId);
|
|
1007
1468
|
replyTracker.beginTurn();
|
|
1008
1469
|
});
|
|
1009
1470
|
pi.on("session_info_changed", (_event, ctx) => {
|
|
1010
1471
|
if (!getLiveContext(ctx)) {
|
|
1011
1472
|
return;
|
|
1012
1473
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1474
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1475
|
+
currentSessionId = sessionId;
|
|
1476
|
+
syncPresenceIdentity(sessionId);
|
|
1015
1477
|
});
|
|
1016
1478
|
pi.on("model_select", (event, ctx) => {
|
|
1017
1479
|
if (!getLiveContext(ctx)) {
|
|
@@ -1020,21 +1482,36 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1020
1482
|
currentModel = event.model.id;
|
|
1021
1483
|
if (client) {
|
|
1022
1484
|
client.updatePresence({
|
|
1023
|
-
...buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()),
|
|
1485
|
+
...buildPresenceIdentity(pi, currentIntercomSessionId ?? ctx.sessionManager.getSessionId()),
|
|
1024
1486
|
model: event.model.id,
|
|
1025
1487
|
status: currentStatus(),
|
|
1026
1488
|
});
|
|
1027
1489
|
}
|
|
1028
1490
|
});
|
|
1029
1491
|
|
|
1030
|
-
pi.registerMessageRenderer("intercom_message", (message,
|
|
1492
|
+
pi.registerMessageRenderer("intercom_message", (message, options, theme) => {
|
|
1031
1493
|
const details = message.details as { from: SessionInfo; message: Message; replyCommand?: string; bodyText?: string } | undefined;
|
|
1032
1494
|
if (!details) return undefined;
|
|
1033
|
-
return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText);
|
|
1495
|
+
return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText, !options.expanded);
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
pi.on("tool_result", (event) => {
|
|
1499
|
+
if (event.toolName !== "intercom" && event.toolName !== "contact_supervisor") {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (!event.details || typeof event.details !== "object") {
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const details = event.details as { error?: unknown; delivered?: unknown };
|
|
1507
|
+
if (details.error === true || details.delivered === false) {
|
|
1508
|
+
return { isError: true };
|
|
1509
|
+
}
|
|
1034
1510
|
});
|
|
1035
1511
|
|
|
1036
1512
|
const childOrchestratorMetadata = readChildOrchestratorMetadata();
|
|
1037
|
-
|
|
1513
|
+
const nativeSupervisorChannelAvailable = Boolean(process.env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV]?.trim());
|
|
1514
|
+
if (childOrchestratorMetadata && !nativeSupervisorChannelAvailable) {
|
|
1038
1515
|
pi.registerTool({
|
|
1039
1516
|
name: "contact_supervisor",
|
|
1040
1517
|
label: "Contact Supervisor",
|
|
@@ -1047,8 +1524,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1047
1524
|
"Do not use contact_supervisor for routine completion handoffs; return the final subagent result normally.",
|
|
1048
1525
|
],
|
|
1049
1526
|
parameters: Type.Object({
|
|
1050
|
-
reason:
|
|
1051
|
-
enum: ["need_decision", "progress_update", "interview_request"],
|
|
1527
|
+
reason: StringEnum(["need_decision", "progress_update", "interview_request"] as const, {
|
|
1052
1528
|
description: "Contact reason: 'need_decision' waits for a reply; 'interview_request' sends structured questions and waits for a reply; 'progress_update' sends a non-blocking update",
|
|
1053
1529
|
}),
|
|
1054
1530
|
message: Type.Optional(Type.String({
|
|
@@ -1059,7 +1535,9 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1059
1535
|
description: Type.Optional(Type.String()),
|
|
1060
1536
|
questions: Type.Array(Type.Object({
|
|
1061
1537
|
id: Type.String(),
|
|
1062
|
-
type:
|
|
1538
|
+
type: StringEnum(["single", "multi", "text", "image", "info"] as const, {
|
|
1539
|
+
description: "Question type: single, multi, text, image, or info",
|
|
1540
|
+
}),
|
|
1063
1541
|
question: Type.String(),
|
|
1064
1542
|
options: Type.Optional(Type.Array(Type.Any())),
|
|
1065
1543
|
context: Type.Optional(Type.String()),
|
|
@@ -1071,14 +1549,12 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1071
1549
|
if (reason !== "need_decision" && reason !== "progress_update" && reason !== "interview_request") {
|
|
1072
1550
|
return {
|
|
1073
1551
|
content: [{ type: "text", text: "Invalid reason. Use 'need_decision', 'interview_request', or 'progress_update'." }],
|
|
1074
|
-
isError: true,
|
|
1075
1552
|
details: { error: true },
|
|
1076
1553
|
};
|
|
1077
1554
|
}
|
|
1078
1555
|
if ((reason === "need_decision" || reason === "progress_update") && typeof params.message !== "string") {
|
|
1079
1556
|
return {
|
|
1080
1557
|
content: [{ type: "text", text: `Missing 'message' parameter for reason '${reason}'.` }],
|
|
1081
|
-
isError: true,
|
|
1082
1558
|
details: { error: true },
|
|
1083
1559
|
};
|
|
1084
1560
|
}
|
|
@@ -1088,7 +1564,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1088
1564
|
if (interviewValidation?.ok === false) {
|
|
1089
1565
|
return {
|
|
1090
1566
|
content: [{ type: "text", text: `Invalid interview request: ${interviewValidation.error}` }],
|
|
1091
|
-
isError: true,
|
|
1092
1567
|
details: { error: true },
|
|
1093
1568
|
};
|
|
1094
1569
|
}
|
|
@@ -1100,7 +1575,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1100
1575
|
} catch (error) {
|
|
1101
1576
|
return {
|
|
1102
1577
|
content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
|
|
1103
|
-
isError: true,
|
|
1104
1578
|
details: { error: true },
|
|
1105
1579
|
};
|
|
1106
1580
|
}
|
|
@@ -1110,7 +1584,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1110
1584
|
if (signal?.aborted) {
|
|
1111
1585
|
return {
|
|
1112
1586
|
content: [{ type: "text", text: "Cancelled" }],
|
|
1113
|
-
isError: true,
|
|
1114
1587
|
details: { error: true },
|
|
1115
1588
|
};
|
|
1116
1589
|
}
|
|
@@ -1118,25 +1591,22 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1118
1591
|
const metadata = childOrchestratorMetadata;
|
|
1119
1592
|
let sendTo: string;
|
|
1120
1593
|
try {
|
|
1121
|
-
sendTo = await
|
|
1594
|
+
sendTo = await resolveSupervisorTarget(connectedClient, metadata);
|
|
1122
1595
|
} catch (error) {
|
|
1123
1596
|
return {
|
|
1124
1597
|
content: [{ type: "text", text: `Failed to resolve supervisor target: ${getErrorMessage(error)}` }],
|
|
1125
|
-
isError: true,
|
|
1126
1598
|
details: { error: true },
|
|
1127
1599
|
};
|
|
1128
1600
|
}
|
|
1129
1601
|
if (signal?.aborted) {
|
|
1130
1602
|
return {
|
|
1131
1603
|
content: [{ type: "text", text: "Cancelled" }],
|
|
1132
|
-
isError: true,
|
|
1133
1604
|
details: { error: true },
|
|
1134
1605
|
};
|
|
1135
1606
|
}
|
|
1136
1607
|
if (sendTo === connectedClient.sessionId) {
|
|
1137
1608
|
return {
|
|
1138
1609
|
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
1139
|
-
isError: true,
|
|
1140
1610
|
details: { error: true },
|
|
1141
1611
|
};
|
|
1142
1612
|
}
|
|
@@ -1151,7 +1621,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1151
1621
|
const errorText = result.reason ?? "Session may not exist or has disconnected.";
|
|
1152
1622
|
return {
|
|
1153
1623
|
content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }],
|
|
1154
|
-
isError: true,
|
|
1155
1624
|
details: { messageId: result.id, delivered: false, reason: result.reason },
|
|
1156
1625
|
};
|
|
1157
1626
|
}
|
|
@@ -1164,13 +1633,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1164
1633
|
});
|
|
1165
1634
|
return {
|
|
1166
1635
|
content: [{ type: "text", text: `Progress update sent to supervisor ${metadata.orchestratorTarget}` }],
|
|
1167
|
-
isError: false,
|
|
1168
1636
|
details: { messageId: result.id, delivered: true },
|
|
1169
1637
|
};
|
|
1170
1638
|
} catch (error) {
|
|
1171
1639
|
return {
|
|
1172
1640
|
content: [{ type: "text", text: `Failed to send progress update: ${getErrorMessage(error)}` }],
|
|
1173
|
-
isError: true,
|
|
1174
1641
|
details: { error: true },
|
|
1175
1642
|
};
|
|
1176
1643
|
}
|
|
@@ -1179,15 +1646,16 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1179
1646
|
if (replyWaiter) {
|
|
1180
1647
|
return {
|
|
1181
1648
|
content: [{ type: "text", text: "Already waiting for a reply" }],
|
|
1182
|
-
isError: true,
|
|
1183
1649
|
details: { error: true },
|
|
1184
1650
|
};
|
|
1185
1651
|
}
|
|
1186
1652
|
|
|
1187
1653
|
let replyPromise: Promise<Message> | null = null;
|
|
1654
|
+
let deliveryState = "created";
|
|
1655
|
+
let questionId: string | null = null;
|
|
1188
1656
|
try {
|
|
1189
|
-
|
|
1190
|
-
replyPromise = waitForReply(sendTo, questionId, signal);
|
|
1657
|
+
questionId = randomUUID();
|
|
1658
|
+
replyPromise = waitForReply(sendTo, questionId, signal, () => connectedClient.cancelAsk(questionId!), () => latestDeliveryState(questionId, deliveryState));
|
|
1191
1659
|
replyPromise.catch(() => undefined);
|
|
1192
1660
|
if (signal?.aborted) {
|
|
1193
1661
|
rejectReplyWaiter(new Error("Cancelled"));
|
|
@@ -1198,7 +1666,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1198
1666
|
}
|
|
1199
1667
|
return {
|
|
1200
1668
|
content: [{ type: "text", text: "Cancelled" }],
|
|
1201
|
-
isError: true,
|
|
1202
1669
|
details: { error: true },
|
|
1203
1670
|
};
|
|
1204
1671
|
}
|
|
@@ -1210,6 +1677,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1210
1677
|
text: requestText,
|
|
1211
1678
|
expectsReply: true,
|
|
1212
1679
|
});
|
|
1680
|
+
deliveryState = sendResult.delivered ? "socket_delivered" : "delivery_failed";
|
|
1213
1681
|
if (!sendResult.delivered) {
|
|
1214
1682
|
const errorText = sendResult.reason ?? "Session may not exist or has disconnected.";
|
|
1215
1683
|
rejectReplyWaiter(new Error(`Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}`));
|
|
@@ -1222,7 +1690,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1222
1690
|
}
|
|
1223
1691
|
return {
|
|
1224
1692
|
content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }],
|
|
1225
|
-
isError: true,
|
|
1226
1693
|
details: { error: true },
|
|
1227
1694
|
};
|
|
1228
1695
|
}
|
|
@@ -1252,10 +1719,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1252
1719
|
});
|
|
1253
1720
|
return {
|
|
1254
1721
|
content: [{ type: "text", text: `**Reply from supervisor:**\n${replyText}${replyAttachments}` }],
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1722
|
+
details: structuredReply
|
|
1723
|
+
? structuredReply.value !== undefined
|
|
1724
|
+
? { structuredReply: structuredReply.value }
|
|
1725
|
+
: { structuredReplyParseError: structuredReply.error }
|
|
1726
|
+
: {},
|
|
1259
1727
|
};
|
|
1260
1728
|
} catch (error) {
|
|
1261
1729
|
rejectReplyWaiter(toError(error));
|
|
@@ -1268,8 +1736,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1268
1736
|
}
|
|
1269
1737
|
return {
|
|
1270
1738
|
content: [{ type: "text", text: `Failed: ${getErrorMessage(error)}` }],
|
|
1271
|
-
|
|
1272
|
-
details: { error: true },
|
|
1739
|
+
details: { error: true, ...(questionId ? { messageId: questionId, deliveryState: latestDeliveryState(questionId, deliveryState) } : {}) },
|
|
1273
1740
|
};
|
|
1274
1741
|
}
|
|
1275
1742
|
},
|
|
@@ -1306,37 +1773,44 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1306
1773
|
}
|
|
1307
1774
|
return new Text(text, 0, 0);
|
|
1308
1775
|
},
|
|
1309
|
-
});
|
|
1776
|
+
} as any);
|
|
1310
1777
|
}
|
|
1311
1778
|
|
|
1312
1779
|
pi.registerTool({
|
|
1313
1780
|
name: "intercom",
|
|
1314
1781
|
label: "Intercom",
|
|
1315
|
-
description: `Send a message to another
|
|
1782
|
+
description: `Send a message to another Selesai session running on this machine.
|
|
1316
1783
|
Use this to communicate findings, request help, or coordinate work with other sessions.
|
|
1317
1784
|
|
|
1785
|
+
Target a session by name, full session ID, or the short id shown in parentheses
|
|
1786
|
+
by "list" (a leading prefix of the ID is enough). Prefer the short id when two
|
|
1787
|
+
sessions share a name.
|
|
1788
|
+
|
|
1318
1789
|
Usage:
|
|
1319
1790
|
intercom({ action: "list" }) → List active sessions
|
|
1320
|
-
intercom({ action: "
|
|
1321
|
-
intercom({ action: "
|
|
1791
|
+
intercom({ action: "list-cwd" }) → List sessions in the current working directory
|
|
1792
|
+
intercom({ action: "list-cwd", cwd: "/path" }) → List sessions in a specific directory
|
|
1793
|
+
intercom({ action: "send", to: "name-or-id", message: "..." }) → Send message
|
|
1794
|
+
intercom({ action: "ask", to: "name-or-id", message: "..." }) → Ask and wait for reply
|
|
1795
|
+
intercom({ action: "cancel", messageId: "..." }) → Request cancellation of a sent message
|
|
1322
1796
|
intercom({ action: "reply", message: "..." }) → Reply to the active/single pending ask
|
|
1323
1797
|
intercom({ action: "pending" }) → List unresolved inbound asks
|
|
1324
1798
|
intercom({ action: "status" }) → Show connection status`,
|
|
1325
1799
|
promptSnippet:
|
|
1326
|
-
"Use to coordinate with other local
|
|
1800
|
+
"Use to coordinate with other local Selesai sessions: list peers, send updates, ask for help, or check intercom connectivity.",
|
|
1327
1801
|
|
|
1328
1802
|
parameters: Type.Object({
|
|
1329
|
-
action:
|
|
1330
|
-
description: "Action: 'list', 'send', 'ask', 'reply', 'pending', or '
|
|
1803
|
+
action: StringEnum(["list", "list-cwd", "send", "ask", "reply", "pending", "status", "cancel"] as const, {
|
|
1804
|
+
description: "Action: 'list', 'list-cwd', 'send', 'ask', 'reply', 'pending', 'status', or 'cancel'",
|
|
1331
1805
|
}),
|
|
1332
1806
|
to: Type.Optional(Type.String({
|
|
1333
|
-
description: "Target session name
|
|
1807
|
+
description: "Target session: name, full session ID, or the short id shown in parentheses by 'list' (a leading ID prefix resolves). For 'send', 'ask', or disambiguating 'reply'.",
|
|
1334
1808
|
})),
|
|
1335
1809
|
message: Type.Optional(Type.String({
|
|
1336
1810
|
description: "Message to send (for 'send', 'ask', or 'reply' action)",
|
|
1337
1811
|
})),
|
|
1338
1812
|
attachments: Type.Optional(Type.Array(Type.Object({
|
|
1339
|
-
type:
|
|
1813
|
+
type: StringEnum(["file", "snippet", "context"] as const),
|
|
1340
1814
|
name: Type.String(),
|
|
1341
1815
|
content: Type.String(),
|
|
1342
1816
|
language: Type.Optional(Type.String()),
|
|
@@ -1344,6 +1818,18 @@ Usage:
|
|
|
1344
1818
|
replyTo: Type.Optional(Type.String({
|
|
1345
1819
|
description: "Message ID to reply to (for threading or responding to an 'ask')",
|
|
1346
1820
|
})),
|
|
1821
|
+
messageId: Type.Optional(Type.String({
|
|
1822
|
+
description: "Message ID for actions that operate on an existing message, such as 'cancel'.",
|
|
1823
|
+
})),
|
|
1824
|
+
supersedes: Type.Optional(Type.String({
|
|
1825
|
+
description: "Previous message ID this send/ask explicitly supersedes. Only works for the same sender and receiver.",
|
|
1826
|
+
})),
|
|
1827
|
+
retryOf: Type.Optional(Type.String({
|
|
1828
|
+
description: "Previous message ID this send/ask is a user-authored retry of. Retries always send a new message ID.",
|
|
1829
|
+
})),
|
|
1830
|
+
cwd: Type.Optional(Type.String({
|
|
1831
|
+
description: "Working directory filter for 'list-cwd' (absolute, or relative to the current session's cwd; '.' means the current cwd). Defaults to the current session's cwd.",
|
|
1832
|
+
})),
|
|
1347
1833
|
}),
|
|
1348
1834
|
|
|
1349
1835
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -1353,14 +1839,13 @@ Usage:
|
|
|
1353
1839
|
} catch (error) {
|
|
1354
1840
|
return {
|
|
1355
1841
|
content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
|
|
1356
|
-
isError: true,
|
|
1357
1842
|
details: { error: true },
|
|
1358
1843
|
};
|
|
1359
1844
|
}
|
|
1360
1845
|
|
|
1361
1846
|
syncPresenceIdentity(ctx.sessionManager.getSessionId());
|
|
1362
1847
|
|
|
1363
|
-
const { action, to, message, attachments, replyTo } = params;
|
|
1848
|
+
const { action, to, message, attachments, replyTo, messageId, supersedes, retryOf, cwd } = params;
|
|
1364
1849
|
|
|
1365
1850
|
switch (action) {
|
|
1366
1851
|
case "list": {
|
|
@@ -1373,7 +1858,6 @@ Usage:
|
|
|
1373
1858
|
if (!currentSession) {
|
|
1374
1859
|
return {
|
|
1375
1860
|
content: [{ type: "text", text: "Current session is missing from intercom session list." }],
|
|
1376
|
-
isError: true,
|
|
1377
1861
|
details: { error: true },
|
|
1378
1862
|
};
|
|
1379
1863
|
}
|
|
@@ -1385,22 +1869,101 @@ Usage:
|
|
|
1385
1869
|
|
|
1386
1870
|
return {
|
|
1387
1871
|
content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }],
|
|
1388
|
-
|
|
1872
|
+
details: {},
|
|
1389
1873
|
};
|
|
1390
1874
|
} catch (error) {
|
|
1391
1875
|
return {
|
|
1392
1876
|
content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }],
|
|
1393
|
-
isError: true,
|
|
1394
1877
|
details: { error: true },
|
|
1395
1878
|
};
|
|
1396
1879
|
}
|
|
1397
1880
|
}
|
|
1398
1881
|
|
|
1882
|
+
case "list-cwd": {
|
|
1883
|
+
try {
|
|
1884
|
+
const mySessionId = connectedClient.sessionId;
|
|
1885
|
+
const sessions = await connectedClient.listSessions();
|
|
1886
|
+
const currentSession = sessions.find(s => s.id === mySessionId);
|
|
1887
|
+
|
|
1888
|
+
if (!currentSession) {
|
|
1889
|
+
return {
|
|
1890
|
+
content: [{ type: "text", text: "Current session is missing from intercom session list." }],
|
|
1891
|
+
details: { error: true },
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Default to the current session's cwd; an explicit `cwd` overrides
|
|
1896
|
+
// (relative paths resolved against it, "." meaning the current cwd).
|
|
1897
|
+
const filterCwd = cwd && cwd !== "."
|
|
1898
|
+
? resolvePath(currentSession.cwd, cwd)
|
|
1899
|
+
: currentSession.cwd;
|
|
1900
|
+
|
|
1901
|
+
const otherSessions = sessions.filter(
|
|
1902
|
+
s => s.id !== mySessionId && sameCwd(s.cwd, filterCwd),
|
|
1903
|
+
);
|
|
1904
|
+
|
|
1905
|
+
// Fail loud: filtering by a directory with no peers while the
|
|
1906
|
+
// session's OWN cwd has some otherwise reads as a misleading empty
|
|
1907
|
+
// result (common when a caller passes a guessed parent cwd).
|
|
1908
|
+
let emptyNote = "No other sessions in this directory.";
|
|
1909
|
+
if (otherSessions.length === 0 && !sameCwd(filterCwd, currentSession.cwd)) {
|
|
1910
|
+
const here = sessions.filter(
|
|
1911
|
+
s => s.id !== mySessionId && sameCwd(s.cwd, currentSession.cwd),
|
|
1912
|
+
).length;
|
|
1913
|
+
if (here > 0) {
|
|
1914
|
+
emptyNote += ` Your session's cwd is ${currentSession.cwd} (${here} peer${here === 1 ? "" : "s"} there) — call list-cwd without a cwd argument to list them.`;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const currentSection = `**Current session:**\n${formatSessionListRow(currentSession, currentSession.cwd, true)}`;
|
|
1919
|
+
const otherSection = otherSessions.length === 0
|
|
1920
|
+
? `**Other sessions (cwd: ${filterCwd}):**\n${emptyNote}`
|
|
1921
|
+
: `**Other sessions (cwd: ${filterCwd}):**\n${otherSessions.map(s => formatSessionListRow(s, currentSession.cwd, false)).join("\n")}`;
|
|
1922
|
+
|
|
1923
|
+
return {
|
|
1924
|
+
content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }],
|
|
1925
|
+
details: {},
|
|
1926
|
+
};
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
return {
|
|
1929
|
+
content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }],
|
|
1930
|
+
details: { error: true },
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
case "cancel": {
|
|
1936
|
+
if (!messageId) {
|
|
1937
|
+
return {
|
|
1938
|
+
content: [{ type: "text", text: "Missing 'messageId' parameter" }],
|
|
1939
|
+
details: { error: true },
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
try {
|
|
1943
|
+
const result = await connectedClient.cancelMessage(messageId);
|
|
1944
|
+
if (!result.delivered) {
|
|
1945
|
+
const errorText = result.reason ?? "Message may not exist or may belong to another sender.";
|
|
1946
|
+
return {
|
|
1947
|
+
content: [{ type: "text", text: `Cancellation for ${messageId} was not delivered: ${errorText}` }],
|
|
1948
|
+
details: { messageId, delivered: false, reason: result.reason },
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
return {
|
|
1952
|
+
content: [{ type: "text", text: `Cancellation requested for ${messageId}` }],
|
|
1953
|
+
details: { messageId, delivered: true },
|
|
1954
|
+
};
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
return {
|
|
1957
|
+
content: [{ type: "text", text: `Failed to cancel message: ${getErrorMessage(error)}` }],
|
|
1958
|
+
details: { error: true, messageId },
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1399
1963
|
case "send": {
|
|
1400
1964
|
if (!to || !message) {
|
|
1401
1965
|
return {
|
|
1402
1966
|
content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
|
|
1403
|
-
isError: true,
|
|
1404
1967
|
details: { error: true },
|
|
1405
1968
|
};
|
|
1406
1969
|
}
|
|
@@ -1409,7 +1972,6 @@ Usage:
|
|
|
1409
1972
|
if (sendTo === connectedClient.sessionId) {
|
|
1410
1973
|
return {
|
|
1411
1974
|
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
1412
|
-
isError: true,
|
|
1413
1975
|
details: { error: true },
|
|
1414
1976
|
};
|
|
1415
1977
|
}
|
|
@@ -1422,7 +1984,7 @@ Usage:
|
|
|
1422
1984
|
if (!confirmed) {
|
|
1423
1985
|
return {
|
|
1424
1986
|
content: [{ type: "text", text: "Message cancelled by user" }],
|
|
1425
|
-
|
|
1987
|
+
details: {},
|
|
1426
1988
|
};
|
|
1427
1989
|
}
|
|
1428
1990
|
}
|
|
@@ -1430,33 +1992,32 @@ Usage:
|
|
|
1430
1992
|
text: message,
|
|
1431
1993
|
attachments,
|
|
1432
1994
|
replyTo,
|
|
1995
|
+
supersedes,
|
|
1996
|
+
retryOf,
|
|
1433
1997
|
});
|
|
1434
1998
|
if (!result.delivered) {
|
|
1435
1999
|
const errorText = result.reason ?? "Session may not exist or has disconnected.";
|
|
1436
2000
|
return {
|
|
1437
2001
|
content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }],
|
|
1438
|
-
isError: true,
|
|
1439
2002
|
details: { messageId: result.id, delivered: false, reason: result.reason },
|
|
1440
2003
|
};
|
|
1441
2004
|
}
|
|
1442
2005
|
pi.appendEntry("intercom_sent", {
|
|
1443
2006
|
to,
|
|
1444
|
-
message: { text: message, attachments, replyTo },
|
|
2007
|
+
message: { text: message, attachments, replyTo, supersedes, retryOf },
|
|
1445
2008
|
messageId: result.id,
|
|
1446
2009
|
timestamp: Date.now(),
|
|
1447
2010
|
});
|
|
1448
2011
|
if (replyTo) {
|
|
1449
|
-
|
|
2012
|
+
dismissIncomingAsk(replyTo);
|
|
1450
2013
|
}
|
|
1451
2014
|
return {
|
|
1452
2015
|
content: [{ type: "text", text: `Message sent to ${to}` }],
|
|
1453
|
-
isError: false,
|
|
1454
2016
|
details: { messageId: result.id, delivered: true },
|
|
1455
2017
|
};
|
|
1456
2018
|
} catch (error) {
|
|
1457
2019
|
return {
|
|
1458
2020
|
content: [{ type: "text", text: `Failed to send: ${getErrorMessage(error)}` }],
|
|
1459
|
-
isError: true,
|
|
1460
2021
|
details: { error: true },
|
|
1461
2022
|
};
|
|
1462
2023
|
}
|
|
@@ -1466,7 +2027,6 @@ Usage:
|
|
|
1466
2027
|
if (!to || !message) {
|
|
1467
2028
|
return {
|
|
1468
2029
|
content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
|
|
1469
|
-
isError: true,
|
|
1470
2030
|
details: { error: true },
|
|
1471
2031
|
};
|
|
1472
2032
|
}
|
|
@@ -1474,7 +2034,6 @@ Usage:
|
|
|
1474
2034
|
if (replyWaiter) {
|
|
1475
2035
|
return {
|
|
1476
2036
|
content: [{ type: "text", text: "Already waiting for a reply" }],
|
|
1477
|
-
isError: true,
|
|
1478
2037
|
details: { error: true },
|
|
1479
2038
|
};
|
|
1480
2039
|
}
|
|
@@ -1482,38 +2041,47 @@ Usage:
|
|
|
1482
2041
|
if (_signal?.aborted) {
|
|
1483
2042
|
return {
|
|
1484
2043
|
content: [{ type: "text", text: "Cancelled" }],
|
|
1485
|
-
isError: true,
|
|
1486
2044
|
details: { error: true },
|
|
1487
2045
|
};
|
|
1488
2046
|
}
|
|
1489
2047
|
let replyPromise: Promise<Message> | null = null;
|
|
2048
|
+
let deliveryState = "created";
|
|
2049
|
+
let questionId: string | null = null;
|
|
1490
2050
|
|
|
1491
2051
|
try {
|
|
1492
2052
|
const sendTo = await resolveSessionTarget(connectedClient, to) ?? to;
|
|
1493
2053
|
if (_signal?.aborted) {
|
|
1494
2054
|
return {
|
|
1495
2055
|
content: [{ type: "text", text: "Cancelled" }],
|
|
1496
|
-
isError: true,
|
|
1497
2056
|
details: { error: true },
|
|
1498
2057
|
};
|
|
1499
2058
|
}
|
|
1500
2059
|
if (sendTo === connectedClient.sessionId) {
|
|
1501
2060
|
return {
|
|
1502
2061
|
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
1503
|
-
isError: true,
|
|
1504
2062
|
details: { error: true },
|
|
1505
2063
|
};
|
|
1506
2064
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
2065
|
+
if (replyWaiter) {
|
|
2066
|
+
return {
|
|
2067
|
+
content: [{ type: "text", text: "Already waiting for a reply" }],
|
|
2068
|
+
details: { error: true },
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
questionId = randomUUID();
|
|
2072
|
+
replyPromise = waitForReply(sendTo, questionId, _signal, () => connectedClient.cancelAsk(questionId!), () => latestDeliveryState(questionId, deliveryState));
|
|
2073
|
+
replyPromise.catch(() => undefined);
|
|
1509
2074
|
const sendResult = await connectedClient.send(sendTo, {
|
|
1510
2075
|
messageId: questionId,
|
|
1511
2076
|
text: message,
|
|
1512
2077
|
attachments,
|
|
1513
2078
|
replyTo,
|
|
1514
2079
|
expectsReply: true,
|
|
2080
|
+
supersedes,
|
|
2081
|
+
retryOf,
|
|
1515
2082
|
});
|
|
1516
2083
|
|
|
2084
|
+
deliveryState = sendResult.delivered ? "socket_delivered" : "delivery_failed";
|
|
1517
2085
|
if (!sendResult.delivered) {
|
|
1518
2086
|
const errorText = sendResult.reason ?? "Session may not exist or has disconnected.";
|
|
1519
2087
|
rejectReplyWaiter(new Error(`Message to "${to}" was not delivered: ${errorText}`));
|
|
@@ -1526,13 +2094,12 @@ Usage:
|
|
|
1526
2094
|
}
|
|
1527
2095
|
return {
|
|
1528
2096
|
content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }],
|
|
1529
|
-
isError: true,
|
|
1530
2097
|
details: { error: true },
|
|
1531
2098
|
};
|
|
1532
2099
|
}
|
|
1533
2100
|
pi.appendEntry("intercom_sent", {
|
|
1534
2101
|
to,
|
|
1535
|
-
message: { text: message, attachments, replyTo },
|
|
2102
|
+
message: { text: message, attachments, replyTo, supersedes, retryOf },
|
|
1536
2103
|
messageId: sendResult.id,
|
|
1537
2104
|
timestamp: Date.now(),
|
|
1538
2105
|
});
|
|
@@ -1549,7 +2116,7 @@ Usage:
|
|
|
1549
2116
|
});
|
|
1550
2117
|
return {
|
|
1551
2118
|
content: [{ type: "text", text: `**Reply from ${to}:**\n${replyText}${replyAttachments}` }],
|
|
1552
|
-
|
|
2119
|
+
details: {},
|
|
1553
2120
|
};
|
|
1554
2121
|
} catch (error) {
|
|
1555
2122
|
rejectReplyWaiter(toError(error));
|
|
@@ -1562,8 +2129,7 @@ Usage:
|
|
|
1562
2129
|
}
|
|
1563
2130
|
return {
|
|
1564
2131
|
content: [{ type: "text", text: `Failed: ${getErrorMessage(error)}` }],
|
|
1565
|
-
|
|
1566
|
-
details: { error: true },
|
|
2132
|
+
details: { error: true, ...(questionId ? { messageId: questionId, deliveryState: latestDeliveryState(questionId, deliveryState) } : {}) },
|
|
1567
2133
|
};
|
|
1568
2134
|
}
|
|
1569
2135
|
}
|
|
@@ -1572,17 +2138,15 @@ Usage:
|
|
|
1572
2138
|
if (!message) {
|
|
1573
2139
|
return {
|
|
1574
2140
|
content: [{ type: "text", text: "Missing 'message' parameter" }],
|
|
1575
|
-
isError: true,
|
|
1576
2141
|
details: { error: true },
|
|
1577
2142
|
};
|
|
1578
2143
|
}
|
|
1579
2144
|
|
|
1580
2145
|
try {
|
|
1581
|
-
const target = replyTracker.resolveReplyTarget({ to });
|
|
2146
|
+
const target = replyTracker.resolveReplyTarget({ to, replyTo });
|
|
1582
2147
|
if (target.from.id === connectedClient.sessionId) {
|
|
1583
2148
|
return {
|
|
1584
2149
|
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
1585
|
-
isError: true,
|
|
1586
2150
|
details: { error: true },
|
|
1587
2151
|
};
|
|
1588
2152
|
}
|
|
@@ -1592,13 +2156,15 @@ Usage:
|
|
|
1592
2156
|
});
|
|
1593
2157
|
if (!result.delivered) {
|
|
1594
2158
|
const errorText = result.reason ?? "Session may not exist or has disconnected.";
|
|
2159
|
+
if (result.reason === "Session not found") {
|
|
2160
|
+
dismissIncomingAsk(target.message.id);
|
|
2161
|
+
}
|
|
1595
2162
|
return {
|
|
1596
2163
|
content: [{ type: "text", text: `Reply to "${target.from.name || target.from.id}" was not delivered: ${errorText}` }],
|
|
1597
|
-
isError: true,
|
|
1598
2164
|
details: { messageId: result.id, delivered: false, reason: result.reason },
|
|
1599
2165
|
};
|
|
1600
2166
|
}
|
|
1601
|
-
|
|
2167
|
+
dismissIncomingAsk(target.message.id);
|
|
1602
2168
|
pi.appendEntry("intercom_sent", {
|
|
1603
2169
|
to: target.from.name || target.from.id,
|
|
1604
2170
|
message: { text: message, replyTo: target.message.id },
|
|
@@ -1607,13 +2173,11 @@ Usage:
|
|
|
1607
2173
|
});
|
|
1608
2174
|
return {
|
|
1609
2175
|
content: [{ type: "text", text: `Reply sent to ${target.from.name || target.from.id}` }],
|
|
1610
|
-
isError: false,
|
|
1611
2176
|
details: { messageId: result.id, delivered: true, replyTo: target.message.id },
|
|
1612
2177
|
};
|
|
1613
2178
|
} catch (error) {
|
|
1614
2179
|
return {
|
|
1615
2180
|
content: [{ type: "text", text: `Failed to reply: ${getErrorMessage(error)}` }],
|
|
1616
|
-
isError: true,
|
|
1617
2181
|
details: { error: true },
|
|
1618
2182
|
};
|
|
1619
2183
|
}
|
|
@@ -1624,7 +2188,7 @@ Usage:
|
|
|
1624
2188
|
if (pendingAsks.length === 0) {
|
|
1625
2189
|
return {
|
|
1626
2190
|
content: [{ type: "text", text: "No unresolved inbound asks." }],
|
|
1627
|
-
|
|
2191
|
+
details: {},
|
|
1628
2192
|
};
|
|
1629
2193
|
}
|
|
1630
2194
|
|
|
@@ -1636,7 +2200,7 @@ Usage:
|
|
|
1636
2200
|
});
|
|
1637
2201
|
return {
|
|
1638
2202
|
content: [{ type: "text", text: `**Pending asks:**\n${lines.join("\n")}` }],
|
|
1639
|
-
|
|
2203
|
+
details: {},
|
|
1640
2204
|
};
|
|
1641
2205
|
}
|
|
1642
2206
|
|
|
@@ -1649,12 +2213,11 @@ Usage:
|
|
|
1649
2213
|
type: "text",
|
|
1650
2214
|
text: `**Intercom Status:**\nConnected: Yes\nSession ID: ${mySessionId}\nActive sessions: ${sessions.length}`,
|
|
1651
2215
|
}],
|
|
1652
|
-
|
|
2216
|
+
details: {},
|
|
1653
2217
|
};
|
|
1654
2218
|
} catch (error) {
|
|
1655
2219
|
return {
|
|
1656
2220
|
content: [{ type: "text", text: `Failed to get status: ${getErrorMessage(error)}` }],
|
|
1657
|
-
isError: true,
|
|
1658
2221
|
details: { error: true },
|
|
1659
2222
|
};
|
|
1660
2223
|
}
|
|
@@ -1663,7 +2226,6 @@ Usage:
|
|
|
1663
2226
|
default:
|
|
1664
2227
|
return {
|
|
1665
2228
|
content: [{ type: "text", text: `Unknown action: ${action}` }],
|
|
1666
|
-
isError: true,
|
|
1667
2229
|
details: { error: true },
|
|
1668
2230
|
};
|
|
1669
2231
|
}
|
|
@@ -1702,12 +2264,42 @@ Usage:
|
|
|
1702
2264
|
}
|
|
1703
2265
|
return new Text(text, 0, 0);
|
|
1704
2266
|
},
|
|
1705
|
-
});
|
|
2267
|
+
} as any);
|
|
2268
|
+
|
|
2269
|
+
function insertIntoEditor(ctx: ExtensionContext, text: string): boolean {
|
|
2270
|
+
if (!ctx.hasUI) return false;
|
|
2271
|
+
const ui = ctx.ui as { getEditorText?: () => string; setEditorText?: (text: string) => void };
|
|
2272
|
+
if (typeof ui.setEditorText !== "function") return false;
|
|
2273
|
+
const existing = typeof ui.getEditorText === "function" ? ui.getEditorText() : "";
|
|
2274
|
+
ui.setEditorText(existing.trim() ? `${existing.trimEnd()}\n\n${text}` : text);
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
async function insertIntercomId(ctx: ExtensionContext): Promise<void> {
|
|
2279
|
+
const commandGeneration = runtimeGeneration;
|
|
2280
|
+
const liveContext = getLiveContext(ctx, commandGeneration);
|
|
2281
|
+
if (!liveContext) return;
|
|
2282
|
+
let contactClient: IntercomClient;
|
|
2283
|
+
try {
|
|
2284
|
+
contactClient = await ensureConnected("tool");
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", commandGeneration);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const sessionId = contactClient.sessionId;
|
|
2290
|
+
if (!sessionId || !getLiveContext(liveContext, commandGeneration)) return;
|
|
2291
|
+
const snippet = formatIntercomContactSnippet(sessionId);
|
|
2292
|
+
if (insertIntoEditor(liveContext, snippet)) {
|
|
2293
|
+
notifyIfLive(liveContext, `Inserted intercom contact target: ${sessionId}`, "info", commandGeneration);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
notifyIfLive(liveContext, `Intercom contact target: ${sessionId}`, "info", commandGeneration);
|
|
2297
|
+
}
|
|
1706
2298
|
|
|
1707
2299
|
async function openIntercomOverlay(ctx: ExtensionContext): Promise<void> {
|
|
1708
2300
|
const overlayGeneration = runtimeGeneration;
|
|
1709
2301
|
const liveContext = getLiveContext(ctx, overlayGeneration);
|
|
1710
|
-
if (!liveContext?.hasUI) return;
|
|
2302
|
+
if (!liveContext?.hasUI || (liveContext as ExtensionContext & { mode?: string }).mode !== "tui") return;
|
|
1711
2303
|
|
|
1712
2304
|
let overlayClient: IntercomClient;
|
|
1713
2305
|
try {
|
|
@@ -1742,7 +2334,7 @@ Usage:
|
|
|
1742
2334
|
|
|
1743
2335
|
const selectedSession = await ctx.ui.custom<SessionInfo | undefined>(
|
|
1744
2336
|
(_tui, theme, keybindings, done) => new SessionListOverlay(theme, keybindings, currentSession, sessions, done),
|
|
1745
|
-
{ overlay: true }
|
|
2337
|
+
{ overlay: true, overlayOptions: { width: 88 } }
|
|
1746
2338
|
).catch(() => undefined);
|
|
1747
2339
|
|
|
1748
2340
|
if (!selectedSession || !getLiveContext(ctx, overlayGeneration)) return;
|
|
@@ -1759,7 +2351,7 @@ Usage:
|
|
|
1759
2351
|
|
|
1760
2352
|
const result = await ctx.ui.custom<ComposeResult>(
|
|
1761
2353
|
(tui, theme, keybindings, done) => new ComposeOverlay(tui, theme, keybindings, selectedSession, targetLabel, overlayClient, done),
|
|
1762
|
-
{ overlay: true }
|
|
2354
|
+
{ overlay: true, overlayOptions: { width: 72 } }
|
|
1763
2355
|
).catch(() => undefined);
|
|
1764
2356
|
|
|
1765
2357
|
if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) {
|
|
@@ -1778,6 +2370,11 @@ Usage:
|
|
|
1778
2370
|
handler: async (_args, ctx) => openIntercomOverlay(ctx),
|
|
1779
2371
|
});
|
|
1780
2372
|
|
|
2373
|
+
pi.registerCommand("intercom-id", {
|
|
2374
|
+
description: "Insert a stable pi-intercom handoff snippet for this session into the editor",
|
|
2375
|
+
handler: async (_args, ctx) => insertIntercomId(ctx),
|
|
2376
|
+
});
|
|
2377
|
+
|
|
1781
2378
|
pi.registerShortcut("alt+m", {
|
|
1782
2379
|
description: "Open session intercom",
|
|
1783
2380
|
handler: async (ctx) => openIntercomOverlay(ctx),
|