@gajae-code/coding-agent 0.11.1 → 0.11.3
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 +64 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +141 -11
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +4 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +5 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +214 -12
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +327 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +127 -30
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +782 -94
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +44 -2
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +462 -480
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +19 -7
- package/src/tools/read.ts +219 -64
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/subagent.ts +43 -2
- package/src/utils/event-bus.ts +9 -4
- package/src/utils/file-mentions.ts +4 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -1,34 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
* Interactive mode for the coding agent.
|
|
3
|
-
* Handles TUI rendering and user interaction, delegating business logic to AgentSession.
|
|
4
|
-
*/
|
|
5
|
-
import * as fs from "node:fs/promises";
|
|
6
|
-
import * as path from "node:path";
|
|
7
|
-
import { type Agent, type AgentMessage, type AgentToolResult, ThinkingLevel } from "@gajae-code/agent-core";
|
|
1
|
+
import { type Agent, type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
8
2
|
import type { CompactionOutcome } from "@gajae-code/agent-core/compaction";
|
|
9
|
-
import {
|
|
10
|
-
type AssistantMessage,
|
|
11
|
-
type ImageContent,
|
|
12
|
-
type Message,
|
|
13
|
-
type Model,
|
|
14
|
-
modelsAreEqual,
|
|
15
|
-
type UsageReport,
|
|
16
|
-
} from "@gajae-code/ai";
|
|
3
|
+
import type { AssistantMessage, ImageContent, Message, UsageReport } from "@gajae-code/ai";
|
|
17
4
|
import type { Component, EditorTheme, SlashCommand } from "@gajae-code/tui";
|
|
18
5
|
import {
|
|
19
6
|
Container,
|
|
20
7
|
clearRenderCache,
|
|
21
8
|
getRenderCacheRetainedBytes,
|
|
22
9
|
Loader,
|
|
23
|
-
Markdown,
|
|
24
10
|
onImageProtocolChanged,
|
|
25
11
|
ProcessTerminal,
|
|
26
12
|
Spacer,
|
|
27
13
|
Text,
|
|
28
14
|
TUI,
|
|
29
|
-
visibleWidth,
|
|
30
15
|
} from "@gajae-code/tui";
|
|
31
|
-
import { APP_NAME, adjustHsv, getProjectDir,
|
|
16
|
+
import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem } from "@gajae-code/utils";
|
|
32
17
|
import chalk from "chalk";
|
|
33
18
|
import { AsyncJobManager } from "../async";
|
|
34
19
|
import { type AppKeybinding, KeybindingsManager } from "../config/keybindings";
|
|
@@ -43,45 +28,26 @@ import type {
|
|
|
43
28
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
44
29
|
import { resolveSkillSlashCommands, type Skill } from "../extensibility/skills";
|
|
45
30
|
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
|
46
|
-
import { consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
|
|
47
|
-
import { type Goal, type GoalModeState, normalizeGoal } from "../goals/state";
|
|
48
|
-
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
49
31
|
import { getLspStartupWarningMessage, LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
50
|
-
import {
|
|
51
|
-
humanizePlanTitle,
|
|
52
|
-
type PlanApprovalDetails,
|
|
53
|
-
renameApprovedPlanFile,
|
|
54
|
-
resolvePlanTitle,
|
|
55
|
-
} from "../plan-mode/approved-plan";
|
|
56
|
-
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
57
|
-
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
58
|
-
type: "text",
|
|
59
|
-
};
|
|
60
32
|
import {
|
|
61
33
|
createStarReminderBeforeAgentStartContributor,
|
|
62
34
|
scheduleLaunchStarReminderAfterFirstRender,
|
|
63
35
|
starReminderLaunchGate,
|
|
64
36
|
} from "../reminders/star-reminder";
|
|
65
37
|
import type { NotificationSessionReconcileResult, NotificationSessionStatus } from "../sdk/bus/session-control";
|
|
66
|
-
import type { AgentSession, AgentSessionEvent
|
|
38
|
+
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
67
39
|
import { HistoryStorage } from "../session/history-storage";
|
|
68
40
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
69
|
-
import { getRecentSessions } from "../session/session-manager";
|
|
70
|
-
import { formatDuration } from "../slash-commands/helpers/format";
|
|
71
|
-
import { STTController, type SttState } from "../stt";
|
|
41
|
+
import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
|
|
72
42
|
import type { LspStartupServerInfo } from "../tools";
|
|
73
|
-
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
74
|
-
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
75
43
|
import { formatPhaseDisplayName } from "../tools/todo-write";
|
|
76
|
-
import { ToolError } from "../tools/tool-errors";
|
|
77
44
|
import type { EventBus } from "../utils/event-bus";
|
|
78
|
-
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
79
45
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
80
46
|
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
|
|
81
47
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
82
48
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
49
|
+
import type { CommandPaletteAction } from "./components/command-palette";
|
|
83
50
|
import { CustomEditor } from "./components/custom-editor";
|
|
84
|
-
import { DynamicBorder } from "./components/dynamic-border";
|
|
85
51
|
import type { EvalExecutionComponent } from "./components/eval-execution";
|
|
86
52
|
import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
|
|
87
53
|
import type { HookEditorComponent } from "./components/hook-editor";
|
|
@@ -94,8 +60,8 @@ import {
|
|
|
94
60
|
isPetCapabilityProbePending,
|
|
95
61
|
warnWhenPetCapabilitySettled,
|
|
96
62
|
} from "./components/pet-capability";
|
|
97
|
-
import { StatusLineComponent } from "./components/status-line";
|
|
98
63
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
64
|
+
import { StatusLineComponent } from "./components/tool-status-header";
|
|
99
65
|
import {
|
|
100
66
|
WelcomeComponent,
|
|
101
67
|
type WelcomeLogoMode,
|
|
@@ -105,9 +71,13 @@ import { BtwController } from "./controllers/btw-controller";
|
|
|
105
71
|
import { CommandController } from "./controllers/command-controller";
|
|
106
72
|
import { EventController } from "./controllers/event-controller";
|
|
107
73
|
import { ExtensionUiController } from "./controllers/extension-ui-controller";
|
|
74
|
+
import { GoalModeController } from "./controllers/goal-mode-controller";
|
|
108
75
|
import { InputController } from "./controllers/input-controller";
|
|
76
|
+
import { ModeGate } from "./controllers/mode-gate";
|
|
77
|
+
import { PlanModeController } from "./controllers/plan-mode-controller";
|
|
109
78
|
import { SelectorController } from "./controllers/selector-controller";
|
|
110
79
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
80
|
+
import { SttModeController } from "./controllers/stt-controller";
|
|
111
81
|
import { TodoCommandController } from "./controllers/todo-command-controller";
|
|
112
82
|
import { IrcObservationLedger } from "./irc-observation-ledger";
|
|
113
83
|
import { JobsObserver } from "./jobs-observer";
|
|
@@ -115,29 +85,25 @@ import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
|
115
85
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
116
86
|
import { interruptHint } from "./shared";
|
|
117
87
|
import { shouldShowExtensionCommand } from "./slash-command-visibility";
|
|
88
|
+
import { TasksAggregator } from "./tasks-aggregator";
|
|
118
89
|
import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
119
90
|
import type { Theme } from "./theme/theme";
|
|
91
|
+
import { getEditorTheme, getSymbolTheme, onTerminalAppearanceChange, onThemeChange, theme } from "./theme/theme";
|
|
92
|
+
import { type RegisterTranscriptItem, TranscriptItemRegistry, transcriptItemId } from "./transcript-item-registry";
|
|
120
93
|
import {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
InteractiveModeContext,
|
|
131
|
-
IrcArrivalSnapshot,
|
|
132
|
-
SubmittedUserInput,
|
|
133
|
-
TodoItem,
|
|
134
|
-
TodoPhase,
|
|
135
|
-
TranscriptRebuildPolicy,
|
|
94
|
+
type CompactionQueuedMessage,
|
|
95
|
+
type ComposerSubmissionOptions,
|
|
96
|
+
canApplyComposerSubmission,
|
|
97
|
+
type InteractiveModeContext,
|
|
98
|
+
type IrcArrivalSnapshot,
|
|
99
|
+
type SubmittedUserInput,
|
|
100
|
+
type TodoItem,
|
|
101
|
+
type TodoPhase,
|
|
102
|
+
type TranscriptRebuildPolicy,
|
|
136
103
|
} from "./types";
|
|
137
104
|
import type { ParsedIrcMessage } from "./utils/irc-message";
|
|
138
105
|
import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
|
|
139
106
|
|
|
140
|
-
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
141
107
|
const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
|
|
142
108
|
export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
|
|
143
109
|
const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
|
|
@@ -268,22 +234,6 @@ function formatHudNoteMarker(count: number): string {
|
|
|
268
234
|
return theme.fg("dim", chalk.italic(` \u207a${sub}`));
|
|
269
235
|
}
|
|
270
236
|
|
|
271
|
-
type GoalSubcommand = "set" | "show" | "pause" | "resume" | "drop";
|
|
272
|
-
|
|
273
|
-
const GOAL_SUBCOMMANDS = new Set<GoalSubcommand>(["set", "show", "pause", "resume", "drop"]);
|
|
274
|
-
|
|
275
|
-
function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; rest: string } {
|
|
276
|
-
const trimmed = args.trim();
|
|
277
|
-
if (!trimmed) return { sub: undefined, rest: "" };
|
|
278
|
-
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
279
|
-
if (!match) return { sub: undefined, rest: trimmed };
|
|
280
|
-
const first = match[1].toLowerCase();
|
|
281
|
-
if (GOAL_SUBCOMMANDS.has(first as GoalSubcommand)) {
|
|
282
|
-
return { sub: first as GoalSubcommand, rest: match[2]?.trim() ?? "" };
|
|
283
|
-
}
|
|
284
|
-
return { sub: undefined, rest: trimmed };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
237
|
export type WelcomeBannerSettingMode = "auto" | "unicode" | "square" | "ascii";
|
|
288
238
|
|
|
289
239
|
export function resolveWelcomeLogoMode(
|
|
@@ -342,11 +292,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
342
292
|
isBashNoContext = false;
|
|
343
293
|
toolOutputExpanded = false;
|
|
344
294
|
todoExpanded = false;
|
|
345
|
-
planModeEnabled = false;
|
|
346
|
-
planModePaused = false;
|
|
347
|
-
goalModeEnabled = false;
|
|
348
|
-
goalModePaused = false;
|
|
349
|
-
planModePlanFilePath: string | undefined = undefined;
|
|
350
295
|
todoPhases: TodoPhase[] = [];
|
|
351
296
|
hideThinkingBlock = false;
|
|
352
297
|
pendingImages: ImageContent[] = [];
|
|
@@ -392,6 +337,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
392
337
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
393
338
|
|
|
394
339
|
#baseSlashCommands: SlashCommand[] = [];
|
|
340
|
+
#resolvedSlashCommands: SlashCommand[] = [];
|
|
395
341
|
#baseReservedSlashCommandNames: Set<string> = new Set();
|
|
396
342
|
#cleanupUnsubscribe?: () => void;
|
|
397
343
|
#subprocessTeardownUnsubscribe?: () => void;
|
|
@@ -400,17 +346,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
400
346
|
#petUnavailableWarningDisposer?: () => void;
|
|
401
347
|
readonly #version: string;
|
|
402
348
|
readonly #changelogMarkdown: string | undefined;
|
|
403
|
-
|
|
404
|
-
#goalModePreviousTools: string[] | undefined;
|
|
405
|
-
#goalContinuationTimer: NodeJS.Timeout | undefined;
|
|
406
|
-
#goalTurnHadToolCalls = false;
|
|
407
|
-
#goalContinuationTurnInFlight = false;
|
|
408
|
-
#goalSuppressNextContinuation = false;
|
|
409
|
-
#planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
410
|
-
#pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
411
|
-
#planModeProviderSessionScope: TemporaryProviderSessionScope | undefined;
|
|
412
|
-
#planModeHasEntered = false;
|
|
413
|
-
#planReviewContainer: Container | undefined;
|
|
349
|
+
|
|
414
350
|
readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
|
|
415
351
|
mcpManager?: import("../runtime-mcp").MCPManager;
|
|
416
352
|
readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
|
|
@@ -423,14 +359,25 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
423
359
|
readonly #inputController: InputController;
|
|
424
360
|
readonly #selectorController: SelectorController;
|
|
425
361
|
readonly #uiHelpers: UiHelpers;
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
#
|
|
430
|
-
#voicePreviousUseTerminalCursor: boolean | null = null;
|
|
362
|
+
readonly #modeGate = new ModeGate();
|
|
363
|
+
readonly #goalModeController: GoalModeController;
|
|
364
|
+
readonly #planModeController: PlanModeController;
|
|
365
|
+
#sttController: SttModeController | undefined;
|
|
431
366
|
#resizeHandler?: () => void;
|
|
432
367
|
#observerRegistry: SessionObserverRegistry;
|
|
368
|
+
#transcriptRegistry = new TranscriptItemRegistry();
|
|
369
|
+
|
|
370
|
+
/** Direct controller capabilities for consumers that coordinate mode transitions. */
|
|
371
|
+
get planModeController(): PlanModeController {
|
|
372
|
+
return this.#planModeController;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
get goalModeController(): GoalModeController {
|
|
376
|
+
return this.#goalModeController;
|
|
377
|
+
}
|
|
378
|
+
|
|
433
379
|
#jobsObserver?: JobsObserver;
|
|
380
|
+
#tasksAggregator?: TasksAggregator;
|
|
434
381
|
#eventBus?: EventBus;
|
|
435
382
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
436
383
|
#welcomeComponent?: WelcomeComponent;
|
|
@@ -449,6 +396,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
449
396
|
) {
|
|
450
397
|
this.session = session;
|
|
451
398
|
this.sessionManager = session.sessionManager;
|
|
399
|
+
this.session.setSdkPlanModeHandler(async on => {
|
|
400
|
+
if (on && (this.#goalModeController.enabled || this.#goalModeController.paused)) {
|
|
401
|
+
throw Object.assign(new Error("mode.plan.set could not enter plan mode while goal mode is active."), {
|
|
402
|
+
code: "conflict",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (on) await this.#planModeController.enter();
|
|
406
|
+
else await this.#planModeController.exit();
|
|
407
|
+
const state = this.session.getPlanModeState();
|
|
408
|
+
const applied = on
|
|
409
|
+
? this.#planModeController.enabled && state?.enabled === true
|
|
410
|
+
: !this.#planModeController.enabled && !this.#planModeController.paused && state === undefined;
|
|
411
|
+
if (!applied) {
|
|
412
|
+
throw Object.assign(
|
|
413
|
+
new Error(`mode.plan.set could not ${on ? "enter" : "exit"} plan mode in the current lifecycle state.`),
|
|
414
|
+
{
|
|
415
|
+
code: "conflict",
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return state;
|
|
420
|
+
});
|
|
452
421
|
this.session.setRetainedMemorySampler(() => ({
|
|
453
422
|
tuiChatChildren: this.chatContainer.children.length,
|
|
454
423
|
tuiCachedRenderBytes: getRenderCacheRetainedBytes(),
|
|
@@ -462,6 +431,67 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
462
431
|
this.lspServers = lspServers;
|
|
463
432
|
this.mcpManager = mcpManager;
|
|
464
433
|
this.#eventBus = eventBus;
|
|
434
|
+
const thisMode = this;
|
|
435
|
+
this.#goalModeController = new GoalModeController({
|
|
436
|
+
session: this.session,
|
|
437
|
+
sessionManager: this.sessionManager,
|
|
438
|
+
modeGate: this.#modeGate,
|
|
439
|
+
get planModeActive() {
|
|
440
|
+
return thisMode.#planModeController.enabled || thisMode.#planModeController.paused;
|
|
441
|
+
},
|
|
442
|
+
get inputCallback() {
|
|
443
|
+
return thisMode.onInputCallback;
|
|
444
|
+
},
|
|
445
|
+
get hasPendingSubmission() {
|
|
446
|
+
return thisMode.#pendingSubmittedInput !== undefined;
|
|
447
|
+
},
|
|
448
|
+
get hasPendingImages() {
|
|
449
|
+
return thisMode.pendingImages.length > 0;
|
|
450
|
+
},
|
|
451
|
+
get editorText() {
|
|
452
|
+
return thisMode.editor.getText();
|
|
453
|
+
},
|
|
454
|
+
startPendingSubmission: input => thisMode.startPendingSubmission(input),
|
|
455
|
+
showStatus: message => this.showStatus(message),
|
|
456
|
+
showWarning: message => this.showWarning(message),
|
|
457
|
+
showError: message => this.showError(message),
|
|
458
|
+
showHookConfirm: (title, message) => this.showHookConfirm(title, message),
|
|
459
|
+
showHookSelector: (title, options) => this.showHookSelector(title, options),
|
|
460
|
+
showHookEditor: (title, options) => this.showHookEditor(title, undefined, undefined, options),
|
|
461
|
+
updateGoalModeStatus: () => this.#updateGoalModeStatus(),
|
|
462
|
+
});
|
|
463
|
+
this.#planModeController = new PlanModeController({
|
|
464
|
+
session: this.session,
|
|
465
|
+
sessionManager: this.sessionManager,
|
|
466
|
+
modeGate: this.#modeGate,
|
|
467
|
+
get chatContainer() {
|
|
468
|
+
return thisMode.chatContainer;
|
|
469
|
+
},
|
|
470
|
+
get inputCallback() {
|
|
471
|
+
return thisMode.onInputCallback;
|
|
472
|
+
},
|
|
473
|
+
get externalEditorKey() {
|
|
474
|
+
return thisMode.keybindings.getDisplayString("app.editor.external");
|
|
475
|
+
},
|
|
476
|
+
get externalEditorKeys() {
|
|
477
|
+
return thisMode.keybindings.getKeys("app.editor.external");
|
|
478
|
+
},
|
|
479
|
+
startPendingSubmission: input => thisMode.startPendingSubmission(input),
|
|
480
|
+
addChatChild: child => addChatChild(thisMode, child),
|
|
481
|
+
requestRender: full => thisMode.ui.requestRender(full),
|
|
482
|
+
stopUi: () => thisMode.ui.stop(),
|
|
483
|
+
startUi: () => thisMode.ui.start(),
|
|
484
|
+
showStatus: message => thisMode.showStatus(message),
|
|
485
|
+
showWarning: message => thisMode.showWarning(message),
|
|
486
|
+
showError: message => thisMode.showError(message),
|
|
487
|
+
showHookConfirm: (title, message) => thisMode.showHookConfirm(title, message),
|
|
488
|
+
showPlanPreview: (content, options) => thisMode.#selectorController.showPlanPreview(content, options),
|
|
489
|
+
flushCompactionQueue: options => thisMode.flushCompactionQueue(options),
|
|
490
|
+
updatePlanModeStatus: status => thisMode.#updatePlanModeStatus(status),
|
|
491
|
+
handleClearCommand: () => thisMode.handleClearCommand(),
|
|
492
|
+
handleCompactCommand: instructions => thisMode.handleCompactCommand(instructions),
|
|
493
|
+
updateEditorChrome: () => thisMode.updateEditorChrome(),
|
|
494
|
+
});
|
|
465
495
|
if (eventBus) {
|
|
466
496
|
this.#eventBusUnsubscribers.push(
|
|
467
497
|
eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
|
|
@@ -470,7 +500,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
470
500
|
);
|
|
471
501
|
}
|
|
472
502
|
|
|
473
|
-
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor")
|
|
503
|
+
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"), {
|
|
504
|
+
enableMouse: settings.get("mouse.enabled"),
|
|
505
|
+
});
|
|
474
506
|
this.ui.setClearOnShrink(settings.get("clearOnShrink"));
|
|
475
507
|
this.chatContainer = new Container();
|
|
476
508
|
this.#ircSplitView = new IrcSplitViewComponent(this.chatContainer, this.ircLedger, () => theme);
|
|
@@ -507,7 +539,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
507
539
|
this.hookWidgetContainerBelow = new Container();
|
|
508
540
|
this.editorContainer = new Container();
|
|
509
541
|
this.editorContainer.addChild(this.editor);
|
|
510
|
-
this.statusLine = new StatusLineComponent(session, { version: this.#version });
|
|
542
|
+
this.statusLine = new StatusLineComponent(session, { version: this.#version, focusDomain: "composer" });
|
|
511
543
|
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
512
544
|
|
|
513
545
|
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
|
@@ -532,7 +564,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
532
564
|
|
|
533
565
|
this.#baseSlashCommands = [...BUILTIN_SLASH_COMMANDS, ...hookCommands, ...customCommands];
|
|
534
566
|
this.#baseReservedSlashCommandNames = new Set(this.#baseSlashCommands.map(command => command.name));
|
|
535
|
-
this.#rebuildSkillSlashCommands();
|
|
567
|
+
this.#resolvedSlashCommands = [...this.#baseSlashCommands, ...this.#rebuildSkillSlashCommands()];
|
|
536
568
|
|
|
537
569
|
this.#uiHelpers = new UiHelpers(this);
|
|
538
570
|
this.#btwController = new BtwController(this);
|
|
@@ -542,6 +574,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
542
574
|
this.#todoCommandController = new TodoCommandController(this);
|
|
543
575
|
this.#selectorController = new SelectorController(this);
|
|
544
576
|
this.#inputController = new InputController(this);
|
|
577
|
+
this.statusLine.setActionRegistry(this.#inputController.actionRegistry, () => this.keybindings);
|
|
545
578
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
546
579
|
}
|
|
547
580
|
|
|
@@ -694,6 +727,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
694
727
|
this.statusLine.setJobs(jobsObserver.getSnapshot());
|
|
695
728
|
this.ui.requestRender();
|
|
696
729
|
});
|
|
730
|
+
this.#tasksAggregator = new TasksAggregator(
|
|
731
|
+
jobManager,
|
|
732
|
+
jobsObserver,
|
|
733
|
+
this.#observerRegistry,
|
|
734
|
+
this.session.getAgentId(),
|
|
735
|
+
);
|
|
697
736
|
}
|
|
698
737
|
|
|
699
738
|
// Load initial todos
|
|
@@ -706,6 +745,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
706
745
|
this.updateEditorChrome();
|
|
707
746
|
this.#syncEditorMaxHeight();
|
|
708
747
|
this.isInitialized = true;
|
|
748
|
+
if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
|
|
709
749
|
this.#syncIrcSidebarAvailabilityFromSettings();
|
|
710
750
|
this.ui.requestRender(true);
|
|
711
751
|
|
|
@@ -784,6 +824,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
784
824
|
this.updateEditorChrome();
|
|
785
825
|
}
|
|
786
826
|
|
|
827
|
+
getSlashCommands(): readonly SlashCommand[] {
|
|
828
|
+
return this.#resolvedSlashCommands;
|
|
829
|
+
}
|
|
830
|
+
|
|
787
831
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
788
832
|
async refreshSlashCommandState(cwd?: string): Promise<void> {
|
|
789
833
|
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
@@ -795,9 +839,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
795
839
|
description: cmd.description,
|
|
796
840
|
}));
|
|
797
841
|
const skillCommands = this.#rebuildSkillSlashCommands(fileCommandNames);
|
|
798
|
-
|
|
842
|
+
this.#resolvedSlashCommands = [...this.#baseSlashCommands, ...skillCommands, ...fileSlashCommands];
|
|
799
843
|
const autocompleteProvider = this.#inputController.createAutocompleteProvider(
|
|
800
|
-
|
|
844
|
+
this.#resolvedSlashCommands,
|
|
801
845
|
basePath,
|
|
802
846
|
);
|
|
803
847
|
this.editor.setAutocompleteProvider(autocompleteProvider);
|
|
@@ -828,68 +872,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
828
872
|
|
|
829
873
|
async getUserInput(): Promise<SubmittedUserInput> {
|
|
830
874
|
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
831
|
-
await this.#
|
|
875
|
+
await this.#goalModeController.beforeGetUserInput();
|
|
832
876
|
}
|
|
833
877
|
const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
|
|
834
878
|
this.onInputCallback = input => {
|
|
835
879
|
this.onInputCallback = undefined;
|
|
836
880
|
resolve(input);
|
|
837
881
|
};
|
|
838
|
-
this.#
|
|
882
|
+
this.#goalModeController.scheduleContinuation();
|
|
839
883
|
return promise;
|
|
840
884
|
}
|
|
841
885
|
|
|
842
|
-
#scheduleGoalContinuation(): void {
|
|
843
|
-
this.#cancelGoalContinuation();
|
|
844
|
-
if (!this.onInputCallback) return;
|
|
845
|
-
if (!this.session.settings.get("goal.continuationModes").includes("interactive")) return;
|
|
846
|
-
if (this.planModeEnabled || this.planModePaused) return;
|
|
847
|
-
if (!this.goalModeEnabled || this.goalModePaused) return;
|
|
848
|
-
if (this.#goalSuppressNextContinuation) return;
|
|
849
|
-
if (this.#pendingSubmittedInput) return;
|
|
850
|
-
if (this.editor.getText().trim().length > 0) return;
|
|
851
|
-
if ((this.pendingImages?.length ?? 0) > 0) return;
|
|
852
|
-
const state = this.session.getGoalModeState();
|
|
853
|
-
if (!state?.enabled || state.goal.status !== "active") return;
|
|
854
|
-
const prompt = this.session.goalRuntime.buildContinuationPrompt();
|
|
855
|
-
if (!prompt) return;
|
|
856
|
-
this.#goalContinuationTimer = setTimeout(() => {
|
|
857
|
-
this.#goalContinuationTimer = undefined;
|
|
858
|
-
if (!this.onInputCallback) return;
|
|
859
|
-
if (!this.goalModeEnabled || this.goalModePaused) return;
|
|
860
|
-
if (this.#pendingSubmittedInput) return;
|
|
861
|
-
if (this.editor.getText().trim().length > 0) return;
|
|
862
|
-
if ((this.pendingImages?.length ?? 0) > 0) return;
|
|
863
|
-
// Never fire an autonomous continuation prompt() while the session is
|
|
864
|
-
// busy. A wedged/orphaned subagent turn can leave isStreaming stuck true;
|
|
865
|
-
// firing prompt() here throws AgentBusyError, which submitInteractiveInput
|
|
866
|
-
// surfaces as a red "Error: Agent is already processing…" and then loops
|
|
867
|
-
// back to getUserInput(), re-arming this timer — an infinite error spam.
|
|
868
|
-
// Re-arm and only fire once the session returns to idle.
|
|
869
|
-
if (this.session.isStreaming || this.session.isCompacting) {
|
|
870
|
-
this.#scheduleGoalContinuation();
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
const latestState = this.session.getGoalModeState();
|
|
874
|
-
if (!latestState?.enabled || latestState.goal.status !== "active") return;
|
|
875
|
-
this.#goalContinuationTurnInFlight = true;
|
|
876
|
-
this.onInputCallback(
|
|
877
|
-
this.startPendingSubmission({
|
|
878
|
-
text: prompt,
|
|
879
|
-
customType: "goal-continuation",
|
|
880
|
-
display: false,
|
|
881
|
-
}),
|
|
882
|
-
);
|
|
883
|
-
}, 800);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
#cancelGoalContinuation(): void {
|
|
887
|
-
if (this.#goalContinuationTimer) {
|
|
888
|
-
clearTimeout(this.#goalContinuationTimer);
|
|
889
|
-
this.#goalContinuationTimer = undefined;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
886
|
recordLocalSubmission(text: string, imageCount = 0): () => void {
|
|
894
887
|
if (this.isKnownSlashCommand(text)) {
|
|
895
888
|
return () => {};
|
|
@@ -914,12 +907,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
914
907
|
}
|
|
915
908
|
}
|
|
916
909
|
|
|
917
|
-
startPendingSubmission(
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
910
|
+
startPendingSubmission(
|
|
911
|
+
input: {
|
|
912
|
+
text: string;
|
|
913
|
+
images?: ImageContent[];
|
|
914
|
+
customType?: string;
|
|
915
|
+
display?: boolean;
|
|
916
|
+
},
|
|
917
|
+
options?: ComposerSubmissionOptions,
|
|
918
|
+
): SubmittedUserInput {
|
|
923
919
|
const submission: SubmittedUserInput = {
|
|
924
920
|
text: input.text,
|
|
925
921
|
images: input.images,
|
|
@@ -930,7 +926,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
930
926
|
};
|
|
931
927
|
this.#pendingSubmittedInput = submission;
|
|
932
928
|
if (!submission.customType) {
|
|
933
|
-
this.#
|
|
929
|
+
this.#goalModeController.onUserSubmission();
|
|
934
930
|
const imageCount = submission.images?.length ?? 0;
|
|
935
931
|
this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
|
|
936
932
|
this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
|
|
@@ -944,7 +940,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
944
940
|
this.optimisticUserMessageSignature = undefined;
|
|
945
941
|
this.#pendingSubmissionDispose = undefined;
|
|
946
942
|
}
|
|
947
|
-
this.editor
|
|
943
|
+
if (canApplyComposerSubmission(options, this.editor)) {
|
|
944
|
+
this.editor.setText("");
|
|
945
|
+
}
|
|
948
946
|
this.ensureLoadingAnimation();
|
|
949
947
|
this.ui.requestRender();
|
|
950
948
|
return submission;
|
|
@@ -962,9 +960,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
962
960
|
this.#pendingSubmissionDispose?.();
|
|
963
961
|
this.#pendingSubmissionDispose = undefined;
|
|
964
962
|
this.#pendingWorkingMessage = undefined;
|
|
965
|
-
|
|
966
|
-
this.#goalContinuationTurnInFlight = false;
|
|
967
|
-
}
|
|
963
|
+
this.#goalModeController.onPendingSubmissionFinished(submission.customType);
|
|
968
964
|
if (this.loadingAnimation) {
|
|
969
965
|
this.loadingAnimation.stop();
|
|
970
966
|
this.loadingAnimation = undefined;
|
|
@@ -995,9 +991,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
995
991
|
this.#pendingSubmittedInput = undefined;
|
|
996
992
|
this.#pendingSubmissionDispose = undefined;
|
|
997
993
|
}
|
|
998
|
-
|
|
999
|
-
this.#goalContinuationTurnInFlight = false;
|
|
1000
|
-
}
|
|
994
|
+
this.#goalModeController.onPendingSubmissionFinished(input.customType);
|
|
1001
995
|
|
|
1002
996
|
if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
|
|
1003
997
|
this.optimisticUserMessageSignature = undefined;
|
|
@@ -1247,29 +1241,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1247
1241
|
this.#renderTodoList();
|
|
1248
1242
|
}
|
|
1249
1243
|
|
|
1250
|
-
|
|
1251
|
-
return "local://PLAN.md";
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
#resolvePlanFilePath(planFilePath: string): string {
|
|
1255
|
-
if (planFilePath.startsWith("local:")) {
|
|
1256
|
-
const normalized = normalizeLocalScheme(planFilePath);
|
|
1257
|
-
return resolveLocalUrlToPath(normalized, {
|
|
1258
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1259
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
return path.resolve(this.sessionManager.getCwd(), planFilePath);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
#updatePlanModeStatus(): void {
|
|
1266
|
-
const status =
|
|
1267
|
-
this.planModeEnabled || this.planModePaused
|
|
1268
|
-
? {
|
|
1269
|
-
enabled: this.planModeEnabled,
|
|
1270
|
-
paused: this.planModePaused,
|
|
1271
|
-
}
|
|
1272
|
-
: undefined;
|
|
1244
|
+
#updatePlanModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
|
|
1273
1245
|
this.statusLine.setPlanModeStatus(status);
|
|
1274
1246
|
this.updateEditorChrome();
|
|
1275
1247
|
this.ui.requestRender();
|
|
@@ -1277,903 +1249,23 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1277
1249
|
|
|
1278
1250
|
#updateGoalModeStatus(): void {
|
|
1279
1251
|
const status =
|
|
1280
|
-
this.
|
|
1281
|
-
? { enabled: this.
|
|
1252
|
+
this.#goalModeController.enabled || this.#goalModeController.paused
|
|
1253
|
+
? { enabled: this.#goalModeController.enabled, paused: this.#goalModeController.paused }
|
|
1282
1254
|
: undefined;
|
|
1283
1255
|
this.statusLine.setGoalModeStatus(status);
|
|
1284
1256
|
this.updateEditorChrome();
|
|
1285
1257
|
this.ui.requestRender();
|
|
1286
1258
|
}
|
|
1287
1259
|
|
|
1288
|
-
#resetGoalContinuationSuppression(): void {
|
|
1289
|
-
this.#goalSuppressNextContinuation = false;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
#getPausedGoalState(): GoalModeState | undefined {
|
|
1293
|
-
const state = this.session.getGoalModeState();
|
|
1294
|
-
if (!state?.goal || state.enabled || state.goal.status !== "paused") {
|
|
1295
|
-
return undefined;
|
|
1296
|
-
}
|
|
1297
|
-
return state;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
#goalFromModeData(modeData: SessionContext["modeData"]): Goal | undefined {
|
|
1301
|
-
return normalizeGoal(modeData?.goal) ?? undefined;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
1260
|
async #handleGoalSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
1305
|
-
|
|
1306
|
-
this.#goalTurnHadToolCalls = false;
|
|
1307
|
-
this.#cancelGoalContinuation();
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
if (event.type === "tool_execution_start") {
|
|
1311
|
-
this.#goalTurnHadToolCalls = true;
|
|
1312
|
-
if (!this.#goalContinuationTurnInFlight) {
|
|
1313
|
-
this.#resetGoalContinuationSuppression();
|
|
1314
|
-
}
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
if (event.type === "message_start" && event.message.role === "user" && !event.message.synthetic) {
|
|
1318
|
-
this.#resetGoalContinuationSuppression();
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
if (event.type === "goal_updated") {
|
|
1322
|
-
// Handle drop before clearing goalModeEnabled so #exitGoalMode can
|
|
1323
|
-
// still restore the previous tool set while the flag is true.
|
|
1324
|
-
if (event.state?.goal?.status === "dropped") {
|
|
1325
|
-
await this.#exitGoalMode({ reason: "dropped", silent: true });
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
if (event.state?.enabled === true && !this.#goalModePreviousTools) {
|
|
1329
|
-
this.#goalModePreviousTools = this.session.getActiveToolNames();
|
|
1330
|
-
}
|
|
1331
|
-
this.goalModeEnabled = event.state?.enabled === true;
|
|
1332
|
-
this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
|
|
1333
|
-
if (!event.state?.enabled) {
|
|
1334
|
-
this.#cancelGoalContinuation();
|
|
1335
|
-
}
|
|
1336
|
-
this.#updateGoalModeStatus();
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
if (event.type !== "agent_end") {
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (this.#goalContinuationTurnInFlight) {
|
|
1343
|
-
this.#goalSuppressNextContinuation = !this.#goalTurnHadToolCalls;
|
|
1344
|
-
this.#goalContinuationTurnInFlight = false;
|
|
1345
|
-
}
|
|
1346
|
-
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
1347
|
-
await this.#exitGoalMode({ reason: "completed", silent: true });
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
this.#scheduleGoalContinuation();
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
async #applyPlanModeModel(): Promise<void> {
|
|
1354
|
-
const resolved = this.session.resolveRoleModelWithThinking("plan");
|
|
1355
|
-
if (!resolved.model) return;
|
|
1356
|
-
|
|
1357
|
-
const currentModel = this.session.model;
|
|
1358
|
-
const sameModel = modelsAreEqual(currentModel, resolved.model);
|
|
1359
|
-
const planThinkingLevel = resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined;
|
|
1360
|
-
|
|
1361
|
-
this.#planModePreviousModelState = currentModel
|
|
1362
|
-
? { model: currentModel, thinkingLevel: this.session.thinkingLevel }
|
|
1363
|
-
: undefined;
|
|
1364
|
-
|
|
1365
|
-
if (!sameModel) {
|
|
1366
|
-
if (this.session.isStreaming) {
|
|
1367
|
-
this.#pendingModelSwitch = { model: resolved.model, thinkingLevel: planThinkingLevel };
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
try {
|
|
1371
|
-
this.#planModeProviderSessionScope = this.session.beginTemporaryProviderSessionScope("plan-mode");
|
|
1372
|
-
await this.session.setModelTemporary(resolved.model, planThinkingLevel, {
|
|
1373
|
-
cause: "temporary-operation",
|
|
1374
|
-
reason: "plan-mode",
|
|
1375
|
-
providerSessionScope: this.#planModeProviderSessionScope,
|
|
1376
|
-
});
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
const scope = this.#planModeProviderSessionScope;
|
|
1379
|
-
if (scope) {
|
|
1380
|
-
this.session.restoreTemporaryProviderSessionScope(scope);
|
|
1381
|
-
this.#planModeProviderSessionScope = undefined;
|
|
1382
|
-
}
|
|
1383
|
-
this.showWarning(
|
|
1384
|
-
`Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
} else if (planThinkingLevel) {
|
|
1388
|
-
this.session.setThinkingLevel(planThinkingLevel);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
/** Apply any deferred model switch after the current stream ends. */
|
|
1393
|
-
async flushPendingModelSwitch(): Promise<void> {
|
|
1394
|
-
const pending = this.#pendingModelSwitch;
|
|
1395
|
-
if (!pending) return;
|
|
1396
|
-
this.#pendingModelSwitch = undefined;
|
|
1397
|
-
try {
|
|
1398
|
-
this.#planModeProviderSessionScope ??= this.session.beginTemporaryProviderSessionScope("plan-mode");
|
|
1399
|
-
await this.session.setModelTemporary(pending.model, pending.thinkingLevel, {
|
|
1400
|
-
cause: "temporary-operation",
|
|
1401
|
-
reason: "plan-mode",
|
|
1402
|
-
providerSessionScope: this.#planModeProviderSessionScope,
|
|
1403
|
-
});
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
const scope = this.#planModeProviderSessionScope;
|
|
1406
|
-
if (scope) {
|
|
1407
|
-
this.session.restoreTemporaryProviderSessionScope(scope);
|
|
1408
|
-
this.#planModeProviderSessionScope = undefined;
|
|
1409
|
-
}
|
|
1410
|
-
this.showWarning(
|
|
1411
|
-
`Failed to switch model after streaming: ${error instanceof Error ? error.message : String(error)}`,
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1261
|
+
await this.#goalModeController.handleSessionEvent(event);
|
|
1414
1262
|
}
|
|
1415
1263
|
|
|
1416
|
-
/** Restore mode state from session entries on resume
|
|
1264
|
+
/** Restore mode state from session entries on resume. */
|
|
1417
1265
|
async #restoreModeFromSession(): Promise<void> {
|
|
1418
1266
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
this.sessionManager.appendModeChange("none");
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
if (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused") {
|
|
1425
|
-
const goal = this.#goalFromModeData(sessionContext.modeData);
|
|
1426
|
-
if (!goal) {
|
|
1427
|
-
this.sessionManager.appendModeChange("none");
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
this.session.setGoalModeState({
|
|
1431
|
-
enabled: sessionContext.mode === "goal",
|
|
1432
|
-
mode: "active",
|
|
1433
|
-
goal,
|
|
1434
|
-
});
|
|
1435
|
-
const restored = await this.session.goalRuntime.onThreadResumed();
|
|
1436
|
-
this.goalModeEnabled = restored?.enabled === true;
|
|
1437
|
-
this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
|
|
1438
|
-
// Keep `goal` armed on resumed threads; it is part of the default active tool set.
|
|
1439
|
-
if (restored?.goal) {
|
|
1440
|
-
const previousTools = this.session.getActiveToolNames();
|
|
1441
|
-
this.#goalModePreviousTools = previousTools;
|
|
1442
|
-
await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
|
|
1443
|
-
}
|
|
1444
|
-
this.#updateGoalModeStatus();
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
const pendingGoal = goalEnabled
|
|
1448
|
-
? await consumePendingGoalModeRequest(this.sessionManager.getCwd(), this.sessionManager.getSessionId())
|
|
1449
|
-
: null;
|
|
1450
|
-
if (pendingGoal) {
|
|
1451
|
-
await this.#enterGoalMode({ objective: pendingGoal.objective, silent: true });
|
|
1452
|
-
this.#scheduleGoalContinuation();
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
if (!this.session.settings.get("plan.enabled")) {
|
|
1456
|
-
// Clear stale plan/plan_paused mode so re-enabling the setting
|
|
1457
|
-
// later doesn't unexpectedly restore an old plan session.
|
|
1458
|
-
if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused") {
|
|
1459
|
-
this.sessionManager.appendModeChange("none");
|
|
1460
|
-
}
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
if (sessionContext.mode === "plan") {
|
|
1464
|
-
const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
|
|
1465
|
-
await this.#enterPlanMode({ planFilePath });
|
|
1466
|
-
} else if (sessionContext.mode === "plan_paused") {
|
|
1467
|
-
this.planModePaused = true;
|
|
1468
|
-
this.#planModeHasEntered = true;
|
|
1469
|
-
this.#updatePlanModeStatus();
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
async #enterPlanMode(options?: { planFilePath?: string; workflow?: "parallel" | "iterative" }): Promise<void> {
|
|
1474
|
-
if (this.planModeEnabled) {
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
|
-
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1478
|
-
this.showWarning("Exit goal mode first.");
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
this.planModePaused = false;
|
|
1483
|
-
|
|
1484
|
-
const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
|
|
1485
|
-
const previousTools = this.session.getActiveToolNames();
|
|
1486
|
-
const hasResolveTool = this.session.getToolByName("resolve") !== undefined;
|
|
1487
|
-
const planTools = hasResolveTool ? [...previousTools, "resolve"] : previousTools;
|
|
1488
|
-
const uniquePlanTools = [...new Set(planTools)];
|
|
1489
|
-
|
|
1490
|
-
this.#planModePreviousTools = previousTools;
|
|
1491
|
-
this.planModePlanFilePath = planFilePath;
|
|
1492
|
-
this.planModeEnabled = true;
|
|
1493
|
-
|
|
1494
|
-
await this.session.setActiveToolsByName(uniquePlanTools);
|
|
1495
|
-
this.session.setPlanModeState({
|
|
1496
|
-
enabled: true,
|
|
1497
|
-
planFilePath,
|
|
1498
|
-
workflow: options?.workflow ?? "parallel",
|
|
1499
|
-
reentry: this.#planModeHasEntered,
|
|
1500
|
-
});
|
|
1501
|
-
this.session.setStandingResolveHandler?.(input => this.#runPlanApprovalResolve(input));
|
|
1502
|
-
if (this.session.isStreaming) {
|
|
1503
|
-
await this.session.sendPlanModeContext({ deliverAs: "steer" });
|
|
1504
|
-
}
|
|
1505
|
-
this.#planModeHasEntered = true;
|
|
1506
|
-
await this.#applyPlanModeModel();
|
|
1507
|
-
this.#updatePlanModeStatus();
|
|
1508
|
-
this.sessionManager.appendModeChange("plan", { planFilePath });
|
|
1509
|
-
this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
/** Standing resolve dispatcher registered while plan mode is active. The agent
|
|
1513
|
-
* submits the finalized plan by calling `resolve { action: "apply", extra: { title } }`;
|
|
1514
|
-
* this handler validates the plan file exists, normalizes the title, and shapes the
|
|
1515
|
-
* payload that `event-controller` forwards to `handlePlanApproval`. */
|
|
1516
|
-
#runPlanApprovalResolve(input: unknown): Promise<AgentToolResult<ResolveToolDetails>> {
|
|
1517
|
-
return runResolveInvocation(input as Parameters<typeof runResolveInvocation>[0], {
|
|
1518
|
-
sourceToolName: "plan_approval",
|
|
1519
|
-
label: "Plan ready for approval",
|
|
1520
|
-
apply: async (_reason, extra) => {
|
|
1521
|
-
const state = this.session.getPlanModeState?.();
|
|
1522
|
-
if (!state?.enabled) {
|
|
1523
|
-
throw new ToolError("Plan mode is not active.");
|
|
1524
|
-
}
|
|
1525
|
-
const planFilePath = state.planFilePath;
|
|
1526
|
-
const planContent = await this.#readPlanFile(planFilePath);
|
|
1527
|
-
if (planContent === null) {
|
|
1528
|
-
throw new ToolError(
|
|
1529
|
-
`Plan file not found at ${planFilePath}. Write the finalized plan to ${planFilePath} before requesting approval.`,
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
const normalized = resolvePlanTitle({
|
|
1533
|
-
suppliedTitle: extra?.title,
|
|
1534
|
-
planContent,
|
|
1535
|
-
planFilePath,
|
|
1536
|
-
});
|
|
1537
|
-
const details: PlanApprovalDetails = {
|
|
1538
|
-
planFilePath,
|
|
1539
|
-
finalPlanFilePath: `local://${normalized.fileName}`,
|
|
1540
|
-
title: normalized.title,
|
|
1541
|
-
planExists: true,
|
|
1542
|
-
};
|
|
1543
|
-
return {
|
|
1544
|
-
content: [{ type: "text" as const, text: "Plan ready for approval." }],
|
|
1545
|
-
details,
|
|
1546
|
-
};
|
|
1547
|
-
},
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
async #exitPlanMode(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
|
|
1552
|
-
if (!this.planModeEnabled) {
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// Leaving plan mode invalidates the plan-mode steering and standing
|
|
1557
|
-
// resolve handler that the in-flight turn may still be acting on. Abort
|
|
1558
|
-
// before clearing that state so the model cannot continue from stale
|
|
1559
|
-
// plan-mode context and retry `resolve` against a retired handler.
|
|
1560
|
-
await this.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
|
|
1561
|
-
|
|
1562
|
-
const previousTools = this.#planModePreviousTools;
|
|
1563
|
-
if (previousTools && previousTools.length > 0) {
|
|
1564
|
-
await this.session.setActiveToolsByName(previousTools);
|
|
1565
|
-
}
|
|
1566
|
-
const scope = this.#planModeProviderSessionScope;
|
|
1567
|
-
if (scope && !this.session.isStreaming) {
|
|
1568
|
-
const restored = this.session.restoreTemporaryProviderSessionScope(scope);
|
|
1569
|
-
if (restored) this.#planModeProviderSessionScope = undefined;
|
|
1570
|
-
} else if (this.#planModePreviousModelState) {
|
|
1571
|
-
const prev = this.#planModePreviousModelState;
|
|
1572
|
-
if (modelsAreEqual(this.session.model, prev.model)) {
|
|
1573
|
-
this.session.setThinkingLevel(prev.thinkingLevel);
|
|
1574
|
-
} else if (this.session.isStreaming) {
|
|
1575
|
-
this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
|
|
1576
|
-
} else {
|
|
1577
|
-
await this.session.setModelTemporary(prev.model, prev.thinkingLevel, {
|
|
1578
|
-
cause: "restore",
|
|
1579
|
-
reason: "plan-mode",
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
// If #applyPlanModeModel queued a deferred switch to the plan-role model
|
|
1584
|
-
// (because the session was streaming on entry), drop it now: we are
|
|
1585
|
-
// leaving plan mode, so flushing it on the next agent_end would land the
|
|
1586
|
-
// session on the plan-role model after the user has exited plan mode
|
|
1587
|
-
// (issue #816). Only clear when the pending target matches the plan-role
|
|
1588
|
-
// model — leave any unrelated user-queued switch intact.
|
|
1589
|
-
const pending = this.#pendingModelSwitch;
|
|
1590
|
-
if (pending) {
|
|
1591
|
-
const planResolution = this.session.resolveRoleModelWithThinking("plan");
|
|
1592
|
-
if (planResolution.model && modelsAreEqual(pending.model, planResolution.model)) {
|
|
1593
|
-
this.#pendingModelSwitch = undefined;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
this.session.setStandingResolveHandler?.(null);
|
|
1597
|
-
this.session.setPlanModeState(undefined);
|
|
1598
|
-
this.planModeEnabled = false;
|
|
1599
|
-
this.planModePaused = options?.paused ?? false;
|
|
1600
|
-
this.planModePlanFilePath = undefined;
|
|
1601
|
-
this.#planModePreviousTools = undefined;
|
|
1602
|
-
this.#planModePreviousModelState = undefined;
|
|
1603
|
-
this.#updatePlanModeStatus();
|
|
1604
|
-
const paused = options?.paused ?? false;
|
|
1605
|
-
this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
|
|
1606
|
-
if (!options?.silent) {
|
|
1607
|
-
this.showStatus(paused ? "Plan mode paused." : "Plan mode disabled.");
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
async #enterGoalMode(options: { objective?: string; resume?: boolean; silent?: boolean }): Promise<void> {
|
|
1612
|
-
if (this.goalModeEnabled) {
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
if (this.planModeEnabled || this.planModePaused) {
|
|
1616
|
-
this.showWarning("Exit plan mode first.");
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
const previousTools = this.session.getActiveToolNames();
|
|
1620
|
-
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
1621
|
-
this.#goalModePreviousTools = previousTools;
|
|
1622
|
-
this.goalModePaused = false;
|
|
1623
|
-
const state = options.resume
|
|
1624
|
-
? await this.session.goalRuntime.resumeGoal()
|
|
1625
|
-
: await this.session.goalRuntime.createGoal({ objective: options.objective ?? "" });
|
|
1626
|
-
await this.session.setActiveToolsByName(goalTools);
|
|
1627
|
-
this.session.setGoalModeState(state);
|
|
1628
|
-
this.goalModeEnabled = true;
|
|
1629
|
-
this.#resetGoalContinuationSuppression();
|
|
1630
|
-
this.#updateGoalModeStatus();
|
|
1631
|
-
if (this.session.isStreaming) {
|
|
1632
|
-
await this.session.sendGoalModeContext({ deliverAs: "steer" });
|
|
1633
|
-
}
|
|
1634
|
-
if (!options.silent) {
|
|
1635
|
-
this.showStatus(options.resume ? "Goal mode resumed." : "Goal mode enabled.");
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
async #exitGoalMode(options?: {
|
|
1640
|
-
silent?: boolean;
|
|
1641
|
-
paused?: boolean;
|
|
1642
|
-
reason?: "completed" | "paused" | "dropped";
|
|
1643
|
-
}): Promise<void> {
|
|
1644
|
-
const previousTools = this.#goalModePreviousTools;
|
|
1645
|
-
// Drop keeps the `goal` tool callable so the agent can immediately create a new
|
|
1646
|
-
// goal in the same session without a leader-side cleanup. Complete (and pause)
|
|
1647
|
-
// exit goal mode and restore the pre-goal tool set even when the goal_updated
|
|
1648
|
-
// event has already cleared goalModeEnabled (see #completeGoalFromTool emitting
|
|
1649
|
-
// state.enabled = false before #exitGoalMode runs). Spec: deep-interview-ultragoal-goal-tool-wiring AC1+AC2.
|
|
1650
|
-
const shouldRestoreTools =
|
|
1651
|
-
previousTools &&
|
|
1652
|
-
options?.reason !== "dropped" &&
|
|
1653
|
-
(this.goalModeEnabled || options?.reason === "completed" || options?.paused === true);
|
|
1654
|
-
if (shouldRestoreTools) {
|
|
1655
|
-
await this.session.setActiveToolsByName(previousTools);
|
|
1656
|
-
}
|
|
1657
|
-
const currentState = this.session.getGoalModeState();
|
|
1658
|
-
if (options?.reason === "completed") {
|
|
1659
|
-
this.session.setGoalModeState(undefined);
|
|
1660
|
-
this.sessionManager.appendModeChange("none");
|
|
1661
|
-
this.sessionManager.appendCustomEntry("goal-completed", {
|
|
1662
|
-
objective: currentState?.goal?.objective,
|
|
1663
|
-
tokensUsed: currentState?.goal?.tokensUsed,
|
|
1664
|
-
timeUsedSeconds: currentState?.goal?.timeUsedSeconds,
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
this.goalModeEnabled = false;
|
|
1668
|
-
this.goalModePaused = options?.paused ?? false;
|
|
1669
|
-
this.#goalModePreviousTools = undefined;
|
|
1670
|
-
this.#goalContinuationTurnInFlight = false;
|
|
1671
|
-
this.#cancelGoalContinuation();
|
|
1672
|
-
this.#updateGoalModeStatus();
|
|
1673
|
-
if (!options?.silent) {
|
|
1674
|
-
if (options?.reason === "completed") {
|
|
1675
|
-
this.showStatus("Goal mode completed.");
|
|
1676
|
-
} else if (options?.reason === "dropped") {
|
|
1677
|
-
this.showStatus("Goal dropped.");
|
|
1678
|
-
} else if (options?.paused) {
|
|
1679
|
-
this.showStatus("Goal mode paused.");
|
|
1680
|
-
} else {
|
|
1681
|
-
this.showStatus("Goal mode disabled.");
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
async #readPlanFile(planFilePath: string): Promise<string | null> {
|
|
1687
|
-
const resolvedPath = this.#resolvePlanFilePath(planFilePath);
|
|
1688
|
-
try {
|
|
1689
|
-
return await Bun.file(resolvedPath).text();
|
|
1690
|
-
} catch (error) {
|
|
1691
|
-
if (isEnoent(error)) {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
throw error;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
#renderPlanPreview(planContent: string, options?: { append?: boolean }): void {
|
|
1699
|
-
const existingContainer = this.#planReviewContainer;
|
|
1700
|
-
// Only reuse the existing preview when it is still attached to the chat container;
|
|
1701
|
-
// a collapsed/disposed container must be re-added, not mutated in place.
|
|
1702
|
-
const stillAttached = existingContainer !== undefined && this.chatContainer.children.includes(existingContainer);
|
|
1703
|
-
const replaceExisting = options?.append !== true && stillAttached;
|
|
1704
|
-
const planReviewContainer = replaceExisting && existingContainer ? existingContainer : new Container();
|
|
1705
|
-
planReviewContainer.clear();
|
|
1706
|
-
planReviewContainer.addChild(new Spacer(1));
|
|
1707
|
-
planReviewContainer.addChild(new DynamicBorder());
|
|
1708
|
-
planReviewContainer.addChild(new Text(theme.bold(theme.fg("accent", "Plan Review")), 1, 1));
|
|
1709
|
-
planReviewContainer.addChild(new Spacer(1));
|
|
1710
|
-
planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
|
|
1711
|
-
planReviewContainer.addChild(new DynamicBorder());
|
|
1712
|
-
if (!replaceExisting) {
|
|
1713
|
-
addChatChild(this, planReviewContainer);
|
|
1714
|
-
}
|
|
1715
|
-
this.#planReviewContainer = planReviewContainer;
|
|
1716
|
-
this.ui.requestRender();
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
#getEditorTerminalPath(): string | null {
|
|
1720
|
-
if (process.platform === "win32") {
|
|
1721
|
-
return null;
|
|
1722
|
-
}
|
|
1723
|
-
return "/dev/tty";
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
async #openEditorTerminalHandle(): Promise<fs.FileHandle | null> {
|
|
1727
|
-
const terminalPath = this.#getEditorTerminalPath();
|
|
1728
|
-
if (!terminalPath) {
|
|
1729
|
-
return null;
|
|
1730
|
-
}
|
|
1731
|
-
try {
|
|
1732
|
-
return await fs.open(terminalPath, "r+");
|
|
1733
|
-
} catch {
|
|
1734
|
-
return null;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
#getPlanReviewHelpText(): string {
|
|
1739
|
-
const externalEditorKey = this.keybindings.getDisplayString("app.editor.external");
|
|
1740
|
-
if (!externalEditorKey) {
|
|
1741
|
-
return "up/down navigate enter select esc cancel";
|
|
1742
|
-
}
|
|
1743
|
-
return `up/down navigate enter select ${externalEditorKey.toLowerCase()} open in editor esc cancel`;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
|
|
1747
|
-
const editorCmd = getEditorCommand();
|
|
1748
|
-
if (!editorCmd) {
|
|
1749
|
-
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
const resolvedPath = this.#resolvePlanFilePath(planFilePath);
|
|
1754
|
-
let currentText: string;
|
|
1755
|
-
try {
|
|
1756
|
-
currentText = await Bun.file(resolvedPath).text();
|
|
1757
|
-
} catch (error) {
|
|
1758
|
-
if (isEnoent(error)) {
|
|
1759
|
-
this.showError(`Plan file not found at ${planFilePath}`);
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
1763
|
-
return;
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
let ttyHandle: fs.FileHandle | null = null;
|
|
1767
|
-
try {
|
|
1768
|
-
ttyHandle = await this.#openEditorTerminalHandle();
|
|
1769
|
-
this.ui.stop();
|
|
1770
|
-
|
|
1771
|
-
const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
|
|
1772
|
-
? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
|
|
1773
|
-
: ["inherit", "inherit", "inherit"];
|
|
1774
|
-
|
|
1775
|
-
const result = await openInEditor(editorCmd, currentText, {
|
|
1776
|
-
extension: path.extname(resolvedPath) || ".md",
|
|
1777
|
-
stdio,
|
|
1778
|
-
trimTrailingNewline: false,
|
|
1779
|
-
});
|
|
1780
|
-
if (result !== null) {
|
|
1781
|
-
await Bun.write(resolvedPath, result);
|
|
1782
|
-
this.#renderPlanPreview(result);
|
|
1783
|
-
this.showStatus("Plan updated in external editor.");
|
|
1784
|
-
}
|
|
1785
|
-
} catch (error) {
|
|
1786
|
-
this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
1787
|
-
} finally {
|
|
1788
|
-
if (ttyHandle) {
|
|
1789
|
-
await ttyHandle.close();
|
|
1790
|
-
}
|
|
1791
|
-
this.ui.start();
|
|
1792
|
-
this.ui.requestRender(true);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
async #approvePlan(
|
|
1797
|
-
planContent: string,
|
|
1798
|
-
options: {
|
|
1799
|
-
planFilePath: string;
|
|
1800
|
-
finalPlanFilePath: string;
|
|
1801
|
-
title: string;
|
|
1802
|
-
preserveContext?: boolean;
|
|
1803
|
-
compactBeforeExecute?: boolean;
|
|
1804
|
-
},
|
|
1805
|
-
): Promise<void> {
|
|
1806
|
-
await renameApprovedPlanFile({
|
|
1807
|
-
planFilePath: options.planFilePath,
|
|
1808
|
-
finalPlanFilePath: options.finalPlanFilePath,
|
|
1809
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1810
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1811
|
-
});
|
|
1812
|
-
const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
|
|
1813
|
-
|
|
1814
|
-
// Mark the pending abort caused by the plan-mode → compaction transition as
|
|
1815
|
-
// silent BEFORE #exitPlanMode raises it. The `finally` below clears the
|
|
1816
|
-
// flag on every terminal compaction outcome (ok / cancelled / failed /
|
|
1817
|
-
// throw) so a leaked flag cannot silence a later unrelated abort.
|
|
1818
|
-
// Branchless mark+clear when !compactBeforeExecute: mark is gated; clear
|
|
1819
|
-
// is unconditional and idempotent.
|
|
1820
|
-
if (options.compactBeforeExecute) {
|
|
1821
|
-
this.session.markPlanCompactAbortPending();
|
|
1822
|
-
}
|
|
1823
|
-
let sessionSwitchCompleted = true;
|
|
1824
|
-
let compactOutcome: CompactionOutcome | undefined;
|
|
1825
|
-
try {
|
|
1826
|
-
await this.#exitPlanMode({ silent: true, paused: false });
|
|
1827
|
-
|
|
1828
|
-
if (!options.preserveContext) {
|
|
1829
|
-
sessionSwitchCompleted = await this.handleClearCommand();
|
|
1830
|
-
if (sessionSwitchCompleted) {
|
|
1831
|
-
// The new session has a fresh local:// root — persist the approved plan there
|
|
1832
|
-
// so `local://<title>.md` resolves correctly in the execution session.
|
|
1833
|
-
const newLocalPath = resolveLocalUrlToPath(options.finalPlanFilePath, {
|
|
1834
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1835
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1836
|
-
});
|
|
1837
|
-
await Bun.write(newLocalPath, planContent);
|
|
1838
|
-
}
|
|
1839
|
-
} else if (options.compactBeforeExecute) {
|
|
1840
|
-
// Distill the plan-mode transcript before the execution turn is queued so
|
|
1841
|
-
// the plan-approved synthetic prompt lands as a fresh cache anchor.
|
|
1842
|
-
// Outcome is consumed after tool-restoration and plan-reference-path
|
|
1843
|
-
// bookkeeping below; `markPlanReferenceSent` is intentionally deferred
|
|
1844
|
-
// past the cancel guard — see the comment at the cancel branch.
|
|
1845
|
-
// Cancellation skips the synthetic-prompt dispatch (operator's explicit
|
|
1846
|
-
// abort is honored); failure proceeds best-effort — approval intent stands.
|
|
1847
|
-
const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
|
|
1848
|
-
planFilePath: options.finalPlanFilePath,
|
|
1849
|
-
});
|
|
1850
|
-
// Pin the plan reference path BEFORE compaction so any user messages
|
|
1851
|
-
// queued during the compaction await (which `handleCompactCommand`
|
|
1852
|
-
// flushes via `flushCompactionQueue` before returning) see the
|
|
1853
|
-
// approved plan in `#buildPlanReferenceMessage`. Reassignment after
|
|
1854
|
-
// the try/finally is idempotent and kept for the !compactBeforeExecute
|
|
1855
|
-
// branch.
|
|
1856
|
-
this.session.setPlanReferencePath(options.finalPlanFilePath);
|
|
1857
|
-
compactOutcome = await this.handleCompactCommand(compactionPrompt);
|
|
1858
|
-
}
|
|
1859
|
-
} finally {
|
|
1860
|
-
// Unconditional clear. Idempotent: a no-op when the flag was never set
|
|
1861
|
-
// (i.e., the !compactBeforeExecute branch), and a no-op when the flag
|
|
1862
|
-
// was already consumed by AgentSession.#handleAgentEvent's aborted
|
|
1863
|
-
// message_end stamping. Guarantees the flag is dead at every exit.
|
|
1864
|
-
this.session.clearPlanCompactAbortPending();
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
// Tool restoration runs on every path — the plan mode tools must be
|
|
1868
|
-
// retired regardless of whether the synthetic prompt fires.
|
|
1869
|
-
if (previousTools.length > 0) {
|
|
1870
|
-
await this.session.setActiveToolsByName(previousTools);
|
|
1871
|
-
}
|
|
1872
|
-
if (!sessionSwitchCompleted) {
|
|
1873
|
-
this.showWarning("Plan approved, but the new session could not be created — execution was not dispatched.");
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
this.session.setPlanReferencePath(options.finalPlanFilePath);
|
|
1877
|
-
|
|
1878
|
-
if (compactOutcome === "cancelled") {
|
|
1879
|
-
// Explicit abort: honor it. `executeCompaction` already surfaced
|
|
1880
|
-
// `showError("Compaction cancelled")` to the operator; we add the
|
|
1881
|
-
// deferred-dispatch warning and exit. `markPlanReferenceSent` is
|
|
1882
|
-
// intentionally skipped here: `#planReferenceSent` stays false, so
|
|
1883
|
-
// `AgentSession.#buildPlanReferenceMessage` will inject the plan
|
|
1884
|
-
// reference on the operator's next `prompt()` call. If we marked it
|
|
1885
|
-
// sent here, the executor's first turn would have no plan context.
|
|
1886
|
-
this.showWarning(
|
|
1887
|
-
"Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
|
|
1888
|
-
);
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// Approved plans land in a fresh (or compacted) session whose first user-visible
|
|
1893
|
-
// turn is the synthetic plan-approved prompt — that path bypasses the
|
|
1894
|
-
// input-controller's title generation. Seed an auto-name from the plan title
|
|
1895
|
-
// so the session is not left unnamed. `setSessionName("auto")` is a no-op
|
|
1896
|
-
// when the user has already chosen a name (preserveContext paths).
|
|
1897
|
-
const seededName = humanizePlanTitle(options.title);
|
|
1898
|
-
if (seededName && !this.sessionManager.getSessionName()) {
|
|
1899
|
-
const applied = await this.sessionManager.setSessionName(seededName, "auto");
|
|
1900
|
-
if (applied) {
|
|
1901
|
-
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
1902
|
-
this.updateEditorChrome();
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
// markPlanReferenceSent fires only on the dispatch path so the synthetic
|
|
1907
|
-
// plan-approved prompt is the source of the reference injection.
|
|
1908
|
-
this.session.markPlanReferenceSent();
|
|
1909
|
-
const planModePrompt = prompt.render(planModeApprovedPrompt, {
|
|
1910
|
-
planContent,
|
|
1911
|
-
finalPlanFilePath: options.finalPlanFilePath,
|
|
1912
|
-
contextPreserved: options.preserveContext === true,
|
|
1913
|
-
tools: this.session.getActiveToolNames(),
|
|
1914
|
-
});
|
|
1915
|
-
await this.session.prompt(planModePrompt, { synthetic: true });
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
|
|
1919
|
-
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1920
|
-
this.showWarning("Exit goal mode first.");
|
|
1921
|
-
return;
|
|
1922
|
-
}
|
|
1923
|
-
if (this.planModeEnabled) {
|
|
1924
|
-
const confirmed = await this.showHookConfirm(
|
|
1925
|
-
"Exit plan mode?",
|
|
1926
|
-
"This exits plan mode without approving a plan.",
|
|
1927
|
-
);
|
|
1928
|
-
if (!confirmed) return;
|
|
1929
|
-
await this.#exitPlanMode({ paused: true });
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
if (!this.session.settings.get("plan.enabled")) {
|
|
1933
|
-
this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
await this.#enterPlanMode();
|
|
1937
|
-
if (initialPrompt && this.onInputCallback) {
|
|
1938
|
-
this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
async handleGoalModeCommand(rest?: string): Promise<void> {
|
|
1943
|
-
try {
|
|
1944
|
-
if (this.planModeEnabled || this.planModePaused) {
|
|
1945
|
-
this.showWarning("Exit plan mode first.");
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
if (!this.session.settings.get("goal.enabled")) {
|
|
1949
|
-
this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
|
|
1953
|
-
if (sub) {
|
|
1954
|
-
await this.#dispatchGoalSubcommand(sub, subRest);
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
if (this.goalModeEnabled) {
|
|
1958
|
-
if (subRest) {
|
|
1959
|
-
this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
|
|
1960
|
-
return;
|
|
1961
|
-
}
|
|
1962
|
-
await this.#openGoalMenu("active");
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const pausedState = this.#getPausedGoalState();
|
|
1966
|
-
if (pausedState) {
|
|
1967
|
-
if (subRest) {
|
|
1968
|
-
this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
await this.#openGoalMenu("paused");
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
if (subRest) {
|
|
1975
|
-
await this.#startGoalFromObjective(subRest);
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
const objective = (
|
|
1979
|
-
await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
|
|
1980
|
-
)?.trim();
|
|
1981
|
-
if (!objective) return;
|
|
1982
|
-
await this.#startGoalFromObjective(objective);
|
|
1983
|
-
} catch (error) {
|
|
1984
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
|
|
1989
|
-
switch (sub) {
|
|
1990
|
-
case "set":
|
|
1991
|
-
await this.#handleGoalSetSubcommand(rest);
|
|
1992
|
-
return;
|
|
1993
|
-
case "show":
|
|
1994
|
-
this.#showGoalDetails();
|
|
1995
|
-
return;
|
|
1996
|
-
case "pause":
|
|
1997
|
-
await this.#pauseGoalAction();
|
|
1998
|
-
return;
|
|
1999
|
-
case "resume":
|
|
2000
|
-
await this.#resumeGoalAction();
|
|
2001
|
-
return;
|
|
2002
|
-
case "drop":
|
|
2003
|
-
await this.#confirmAndDropGoal();
|
|
2004
|
-
return;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
async #openGoalMenu(state: "active" | "paused"): Promise<void> {
|
|
2009
|
-
const goal = this.session.getGoalModeState()?.goal;
|
|
2010
|
-
if (!goal) return;
|
|
2011
|
-
const summary = goal.objective.length > 48 ? `${goal.objective.slice(0, 47)}…` : goal.objective;
|
|
2012
|
-
const title = state === "active" ? `Goal: ${summary} (${goal.status})` : `Goal paused: ${summary}`;
|
|
2013
|
-
const items = state === "active" ? ["Show details", "Pause", "Drop"] : ["Resume", "Show details", "Drop"];
|
|
2014
|
-
const choice = await this.showHookSelector(title, items);
|
|
2015
|
-
if (!choice) return;
|
|
2016
|
-
switch (choice) {
|
|
2017
|
-
case "Show details":
|
|
2018
|
-
this.#showGoalDetails();
|
|
2019
|
-
return;
|
|
2020
|
-
case "Pause":
|
|
2021
|
-
await this.#pauseGoalAction();
|
|
2022
|
-
return;
|
|
2023
|
-
case "Resume":
|
|
2024
|
-
await this.#resumeGoalAction();
|
|
2025
|
-
return;
|
|
2026
|
-
case "Drop":
|
|
2027
|
-
await this.#confirmAndDropGoal();
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
#showGoalDetails(): void {
|
|
2033
|
-
const state = this.session.getGoalModeState();
|
|
2034
|
-
const goal = state?.goal;
|
|
2035
|
-
if (!goal) {
|
|
2036
|
-
this.showStatus("No goal set.");
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
const used = goal.tokensUsed.toLocaleString();
|
|
2040
|
-
const lines = [
|
|
2041
|
-
`Objective: ${goal.objective}`,
|
|
2042
|
-
`Status: ${goal.status}${state?.enabled ? "" : " (paused)"}`,
|
|
2043
|
-
`Tokens used: ${used}`,
|
|
2044
|
-
`Time spent: ${formatDuration(goal.timeUsedSeconds * 1000)}`,
|
|
2045
|
-
];
|
|
2046
|
-
this.showStatus(lines.join("\n"));
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
async #pauseGoalAction(): Promise<void> {
|
|
2050
|
-
if (!this.goalModeEnabled) {
|
|
2051
|
-
this.showWarning("No active goal to pause.");
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
await this.session.goalRuntime.pauseGoal();
|
|
2055
|
-
await this.#exitGoalMode({ paused: true, reason: "paused" });
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
async #resumeGoalAction(): Promise<void> {
|
|
2059
|
-
if (!this.#getPausedGoalState()) {
|
|
2060
|
-
this.showWarning("No paused goal to resume.");
|
|
2061
|
-
return;
|
|
2062
|
-
}
|
|
2063
|
-
await this.#enterGoalMode({ resume: true, silent: true });
|
|
2064
|
-
this.showStatus("Goal mode resumed.");
|
|
2065
|
-
this.#scheduleGoalContinuation();
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
async #confirmAndDropGoal(): Promise<void> {
|
|
2069
|
-
if (!this.goalModeEnabled && !this.#getPausedGoalState()) {
|
|
2070
|
-
this.showWarning("No goal to drop.");
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
const confirmed = await this.showHookConfirm(
|
|
2074
|
-
"Drop goal?",
|
|
2075
|
-
"This removes the goal record. Accumulated usage stays in the session log.",
|
|
2076
|
-
);
|
|
2077
|
-
if (!confirmed) return;
|
|
2078
|
-
await this.session.goalRuntime.dropGoal();
|
|
2079
|
-
await this.#exitGoalMode({ reason: "dropped" });
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
async #startGoalFromObjective(objective: string): Promise<void> {
|
|
2083
|
-
await this.#enterGoalMode({ objective, silent: true });
|
|
2084
|
-
this.#resetGoalContinuationSuppression();
|
|
2085
|
-
if (this.onInputCallback) {
|
|
2086
|
-
this.onInputCallback(this.startPendingSubmission({ text: objective }));
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
async #replaceGoalFromObjective(objective: string): Promise<void> {
|
|
2091
|
-
const state = await this.session.goalRuntime.replaceGoal({ objective });
|
|
2092
|
-
this.session.setGoalModeState(state);
|
|
2093
|
-
this.goalModeEnabled = true;
|
|
2094
|
-
this.goalModePaused = false;
|
|
2095
|
-
this.#resetGoalContinuationSuppression();
|
|
2096
|
-
this.#updateGoalModeStatus();
|
|
2097
|
-
if (this.session.isStreaming) {
|
|
2098
|
-
await this.session.sendGoalModeContext({ deliverAs: "steer" });
|
|
2099
|
-
}
|
|
2100
|
-
if (this.onInputCallback) {
|
|
2101
|
-
this.onInputCallback(this.startPendingSubmission({ text: objective }));
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
async #handleGoalSetSubcommand(rest: string): Promise<void> {
|
|
2106
|
-
if (!this.goalModeEnabled && this.#getPausedGoalState()) {
|
|
2107
|
-
this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
|
|
2108
|
-
return;
|
|
2109
|
-
}
|
|
2110
|
-
const objective = rest.trim()
|
|
2111
|
-
? rest.trim()
|
|
2112
|
-
: (await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true }))?.trim();
|
|
2113
|
-
if (!objective) return;
|
|
2114
|
-
if (this.goalModeEnabled) {
|
|
2115
|
-
await this.#replaceGoalFromObjective(objective);
|
|
2116
|
-
return;
|
|
2117
|
-
}
|
|
2118
|
-
await this.#startGoalFromObjective(objective);
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
|
|
2122
|
-
if (!this.planModeEnabled) {
|
|
2123
|
-
this.showWarning("Plan mode is not active.");
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// Abort the agent to prevent it from continuing (e.g., re-submitting the
|
|
2128
|
-
// plan) while the popup is showing. The event listener fires asynchronously
|
|
2129
|
-
// (agent's #emit is fire-and-forget), so without this the model sees
|
|
2130
|
-
// "Plan ready for approval." and immediately re-invokes `resolve` in a loop.
|
|
2131
|
-
await this.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
|
|
2132
|
-
|
|
2133
|
-
const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
|
|
2134
|
-
this.planModePlanFilePath = planFilePath;
|
|
2135
|
-
const planContent = await this.#readPlanFile(planFilePath);
|
|
2136
|
-
if (!planContent) {
|
|
2137
|
-
this.showError(`Plan file not found at ${planFilePath}`);
|
|
2138
|
-
return;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
this.#renderPlanPreview(planContent, { append: true });
|
|
2142
|
-
const choice = await this.showHookSelector(
|
|
2143
|
-
"Plan mode - next step",
|
|
2144
|
-
["Approve and execute", "Approve and compact context", "Approve and keep context", "Refine plan"],
|
|
2145
|
-
{
|
|
2146
|
-
helpText: this.#getPlanReviewHelpText(),
|
|
2147
|
-
onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
|
|
2148
|
-
},
|
|
2149
|
-
);
|
|
2150
|
-
|
|
2151
|
-
if (
|
|
2152
|
-
choice === "Approve and execute" ||
|
|
2153
|
-
choice === "Approve and compact context" ||
|
|
2154
|
-
choice === "Approve and keep context"
|
|
2155
|
-
) {
|
|
2156
|
-
const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
|
|
2157
|
-
try {
|
|
2158
|
-
const latestPlanContent = await this.#readPlanFile(planFilePath);
|
|
2159
|
-
if (!latestPlanContent) {
|
|
2160
|
-
this.showError(`Plan file not found at ${planFilePath}`);
|
|
2161
|
-
return;
|
|
2162
|
-
}
|
|
2163
|
-
await this.#approvePlan(latestPlanContent, {
|
|
2164
|
-
planFilePath,
|
|
2165
|
-
finalPlanFilePath,
|
|
2166
|
-
title: details.title,
|
|
2167
|
-
preserveContext: choice !== "Approve and execute",
|
|
2168
|
-
compactBeforeExecute: choice === "Approve and compact context",
|
|
2169
|
-
});
|
|
2170
|
-
} catch (error) {
|
|
2171
|
-
this.showError(
|
|
2172
|
-
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
2173
|
-
);
|
|
2174
|
-
}
|
|
2175
|
-
return;
|
|
2176
|
-
}
|
|
1267
|
+
if (await this.#goalModeController.restoreFromSession(sessionContext)) return;
|
|
1268
|
+
await this.#planModeController.restoreFromSession(sessionContext);
|
|
2177
1269
|
}
|
|
2178
1270
|
|
|
2179
1271
|
stop(): void {
|
|
@@ -2189,18 +1281,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2189
1281
|
}
|
|
2190
1282
|
this.#welcomeComponent?.dispose();
|
|
2191
1283
|
this.#welcomeComponent = undefined;
|
|
2192
|
-
this.#cleanupMicAnimation();
|
|
2193
|
-
this.#cancelGoalContinuation();
|
|
2194
1284
|
if (this.#sttController) {
|
|
2195
|
-
this.#sttController.dispose();
|
|
1285
|
+
this.#sttController.dispose(this);
|
|
2196
1286
|
this.#sttController = undefined;
|
|
2197
1287
|
}
|
|
1288
|
+
this.#goalModeController.cancelContinuation();
|
|
2198
1289
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
2199
1290
|
this.#extensionUiController.clearHookWidgets();
|
|
2200
1291
|
for (const unsubscribe of this.#eventBusUnsubscribers) {
|
|
2201
1292
|
unsubscribe();
|
|
2202
1293
|
}
|
|
2203
1294
|
this.#eventBusUnsubscribers = [];
|
|
1295
|
+
this.#tasksAggregator?.dispose();
|
|
2204
1296
|
this.#observerRegistry.dispose();
|
|
2205
1297
|
this.#eventController.dispose();
|
|
2206
1298
|
this.statusLine.dispose();
|
|
@@ -2244,6 +1336,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2244
1336
|
this.#btwController.dispose();
|
|
2245
1337
|
|
|
2246
1338
|
// Emit shutdown event to hooks
|
|
1339
|
+
this.session.setSdkPlanModeHandler(null);
|
|
2247
1340
|
await this.session.dispose();
|
|
2248
1341
|
|
|
2249
1342
|
if (this.isInitialized) {
|
|
@@ -2461,8 +1554,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2461
1554
|
this.#uiHelpers.updatePendingMessagesDisplay();
|
|
2462
1555
|
}
|
|
2463
1556
|
|
|
2464
|
-
queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
|
|
2465
|
-
this.#uiHelpers.queueCompactionMessage(text, mode);
|
|
1557
|
+
queueCompactionMessage(text: string, mode: "steer" | "followUp", options?: ComposerSubmissionOptions): void {
|
|
1558
|
+
this.#uiHelpers.queueCompactionMessage(text, mode, options);
|
|
2466
1559
|
}
|
|
2467
1560
|
|
|
2468
1561
|
flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
|
|
@@ -2588,7 +1681,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2588
1681
|
this.#btwController.dispose();
|
|
2589
1682
|
if (cleanupPreviousSessionUi) cleanupPreviousSessionUi();
|
|
2590
1683
|
else this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
2591
|
-
this.#
|
|
1684
|
+
this.#planModeController.clearReview();
|
|
2592
1685
|
}
|
|
2593
1686
|
|
|
2594
1687
|
async handleClearCommand(): Promise<boolean> {
|
|
@@ -2632,75 +1725,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2632
1725
|
this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
|
|
2633
1726
|
return;
|
|
2634
1727
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
}
|
|
2638
|
-
await this.#sttController.toggle(this.editor, {
|
|
2639
|
-
showWarning: (msg: string) => this.showWarning(msg),
|
|
2640
|
-
showStatus: (msg: string) => this.showStatus(msg),
|
|
2641
|
-
onStateChange: (state: SttState) => {
|
|
2642
|
-
if (state === "recording") {
|
|
2643
|
-
this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
|
|
2644
|
-
this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
|
|
2645
|
-
this.ui.setShowHardwareCursor(false);
|
|
2646
|
-
this.editor.setUseTerminalCursor(false);
|
|
2647
|
-
this.#startMicAnimation();
|
|
2648
|
-
} else if (state === "transcribing") {
|
|
2649
|
-
this.#stopMicAnimation();
|
|
2650
|
-
this.#setMicCursor({ r: 200, g: 200, b: 200 });
|
|
2651
|
-
} else {
|
|
2652
|
-
this.#cleanupMicAnimation();
|
|
2653
|
-
}
|
|
2654
|
-
this.updateEditorChrome();
|
|
2655
|
-
this.ui.requestRender();
|
|
2656
|
-
},
|
|
2657
|
-
});
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
#setMicCursor(color: { r: number; g: number; b: number }): void {
|
|
2661
|
-
this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
|
|
2662
|
-
// Theme symbols can be wide (for example, 🎤), so measure the rendered override.
|
|
2663
|
-
this.editor.cursorOverrideWidth = visibleWidth(this.editor.cursorOverride);
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
#updateMicIcon(): void {
|
|
2667
|
-
const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
|
|
2668
|
-
this.#setMicCursor({ r, g, b });
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
#startMicAnimation(): void {
|
|
2672
|
-
if (this.#voiceAnimationInterval) return;
|
|
2673
|
-
this.#voiceHue = 0;
|
|
2674
|
-
this.#updateMicIcon();
|
|
2675
|
-
this.#voiceAnimationInterval = setInterval(() => {
|
|
2676
|
-
this.#voiceHue = (this.#voiceHue + 8) % 360;
|
|
2677
|
-
this.#updateMicIcon();
|
|
2678
|
-
this.ui.requestRender();
|
|
2679
|
-
}, 60);
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
#stopMicAnimation(): void {
|
|
2683
|
-
if (this.#voiceAnimationInterval) {
|
|
2684
|
-
clearInterval(this.#voiceAnimationInterval);
|
|
2685
|
-
this.#voiceAnimationInterval = undefined;
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
#cleanupMicAnimation(): void {
|
|
2690
|
-
if (this.#voiceAnimationInterval) {
|
|
2691
|
-
clearInterval(this.#voiceAnimationInterval);
|
|
2692
|
-
this.#voiceAnimationInterval = undefined;
|
|
2693
|
-
}
|
|
2694
|
-
this.editor.cursorOverride = undefined;
|
|
2695
|
-
this.editor.cursorOverrideWidth = undefined;
|
|
2696
|
-
if (this.#voicePreviousShowHardwareCursor !== null) {
|
|
2697
|
-
this.ui.setShowHardwareCursor(this.#voicePreviousShowHardwareCursor);
|
|
2698
|
-
this.#voicePreviousShowHardwareCursor = null;
|
|
2699
|
-
}
|
|
2700
|
-
if (this.#voicePreviousUseTerminalCursor !== null) {
|
|
2701
|
-
this.editor.setUseTerminalCursor(this.#voicePreviousUseTerminalCursor);
|
|
2702
|
-
this.#voicePreviousUseTerminalCursor = null;
|
|
2703
|
-
}
|
|
1728
|
+
this.#sttController ??= new SttModeController();
|
|
1729
|
+
await this.#sttController.toggle(this);
|
|
2704
1730
|
}
|
|
2705
1731
|
|
|
2706
1732
|
showDebugSelector(): void {
|
|
@@ -2716,6 +1742,103 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2716
1742
|
this.#selectorController.showSessionObserver(this.#observerRegistry);
|
|
2717
1743
|
}
|
|
2718
1744
|
|
|
1745
|
+
showSessionsDashboard(): void {
|
|
1746
|
+
void this.#selectorController.showSessionsDashboard();
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
isTranscriptViewerOpen(): boolean {
|
|
1750
|
+
return this.#selectorController.isTranscriptViewerOpen();
|
|
1751
|
+
}
|
|
1752
|
+
refreshTranscriptViewer(): void {
|
|
1753
|
+
if (!this.isTranscriptViewerOpen()) return;
|
|
1754
|
+
const identityMap = this.#rebuildTranscriptRegistry();
|
|
1755
|
+
this.#selectorController.refreshTranscriptViewer(identityMap);
|
|
1756
|
+
}
|
|
1757
|
+
showTranscriptViewer(): void {
|
|
1758
|
+
this.#rebuildTranscriptRegistry();
|
|
1759
|
+
this.#selectorController.showTranscriptViewer(this.#transcriptRegistry);
|
|
1760
|
+
}
|
|
1761
|
+
#rebuildTranscriptRegistry(): ReadonlyMap<string, string> {
|
|
1762
|
+
const toolResults = new Map<string, Extract<AgentMessage, { role: "toolResult" }>>();
|
|
1763
|
+
for (const message of this.session.messages) {
|
|
1764
|
+
if (message.role === "toolResult") toolResults.set(message.toolCallId, message);
|
|
1765
|
+
}
|
|
1766
|
+
const items: RegisterTranscriptItem[] = [];
|
|
1767
|
+
const identityMap = new Map<string, string>();
|
|
1768
|
+
for (const [messageIndex, message] of this.session.messages.entries()) {
|
|
1769
|
+
const provisionalEntryId = transcriptItemId.stream(this.session.transcriptPromptGeneration, messageIndex);
|
|
1770
|
+
const durableEntryId = getSessionMessageEntryId(message);
|
|
1771
|
+
const entryId = durableEntryId ?? provisionalEntryId;
|
|
1772
|
+
if (durableEntryId) {
|
|
1773
|
+
if (message.role === "user" || message.role === "developer") {
|
|
1774
|
+
identityMap.set(transcriptItemId.entry(provisionalEntryId), transcriptItemId.entry(durableEntryId));
|
|
1775
|
+
} else if (message.role === "assistant") {
|
|
1776
|
+
for (const [contentIndex] of message.content.entries()) {
|
|
1777
|
+
identityMap.set(
|
|
1778
|
+
transcriptItemId.assistantContent(provisionalEntryId, contentIndex),
|
|
1779
|
+
transcriptItemId.assistantContent(durableEntryId, contentIndex),
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (message.role === "user" || message.role === "developer") {
|
|
1785
|
+
const text =
|
|
1786
|
+
typeof message.content === "string"
|
|
1787
|
+
? message.content
|
|
1788
|
+
: message.content
|
|
1789
|
+
.filter(part => part.type === "text")
|
|
1790
|
+
.map(part => part.text)
|
|
1791
|
+
.join("\n");
|
|
1792
|
+
if (text.trim())
|
|
1793
|
+
items.push({
|
|
1794
|
+
kind: "user",
|
|
1795
|
+
source: { entryId, message },
|
|
1796
|
+
getPayload: () => ({ text, metadata: { role: message.role }, source: message }),
|
|
1797
|
+
});
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (message.role !== "assistant") continue;
|
|
1801
|
+
for (const [contentIndex, content] of message.content.entries()) {
|
|
1802
|
+
if (content.type === "thinking" && content.thinking.trim())
|
|
1803
|
+
items.push({
|
|
1804
|
+
kind: "assistant-thinking",
|
|
1805
|
+
source: { entryId, contentIndex, content },
|
|
1806
|
+
getPayload: () => ({ text: content.thinking, metadata: { entryId, contentIndex }, source: content }),
|
|
1807
|
+
});
|
|
1808
|
+
if (content.type === "text" && content.text.trim())
|
|
1809
|
+
items.push({
|
|
1810
|
+
kind: "assistant-text",
|
|
1811
|
+
source: { entryId, contentIndex, content },
|
|
1812
|
+
getPayload: () => ({ text: content.text, metadata: { entryId, contentIndex }, source: content }),
|
|
1813
|
+
});
|
|
1814
|
+
if (content.type === "toolCall") {
|
|
1815
|
+
const result = toolResults.get(content.id);
|
|
1816
|
+
const resultText =
|
|
1817
|
+
result?.content
|
|
1818
|
+
.filter(part => part.type === "text")
|
|
1819
|
+
.map(part => part.text)
|
|
1820
|
+
.join("\n") ?? "";
|
|
1821
|
+
items.push({
|
|
1822
|
+
kind: "tool",
|
|
1823
|
+
source: { toolCallId: content.id, content, result },
|
|
1824
|
+
getPayload: () => ({
|
|
1825
|
+
text: resultText || JSON.stringify(content.arguments, null, 2),
|
|
1826
|
+
metadata: {
|
|
1827
|
+
name: content.name,
|
|
1828
|
+
arguments: content.arguments,
|
|
1829
|
+
intent: content.intent,
|
|
1830
|
+
isError: result?.isError ?? false,
|
|
1831
|
+
},
|
|
1832
|
+
source: { content, result },
|
|
1833
|
+
}),
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
this.#transcriptRegistry.rebuild(items);
|
|
1839
|
+
return identityMap;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
2719
1842
|
showJobsOverlay(): void {
|
|
2720
1843
|
if (!this.#jobsObserver) {
|
|
2721
1844
|
this.showStatus("Background jobs are unavailable in this session");
|
|
@@ -2724,6 +1847,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2724
1847
|
this.#selectorController.showJobsOverlay(this.#jobsObserver);
|
|
2725
1848
|
}
|
|
2726
1849
|
|
|
1850
|
+
showTasksPane(): void {
|
|
1851
|
+
if (!this.#tasksAggregator) {
|
|
1852
|
+
this.showStatus("Tasks are unavailable in this session");
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
this.#selectorController.showTasksPane(this.#tasksAggregator);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
2727
1858
|
resetObserverRegistry(): void {
|
|
2728
1859
|
this.#observerRegistry.resetSessions();
|
|
2729
1860
|
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
|
@@ -2770,6 +1901,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2770
1901
|
}
|
|
2771
1902
|
|
|
2772
1903
|
// Selector handling
|
|
1904
|
+
showCommandPalette(
|
|
1905
|
+
commands: SlashCommand[],
|
|
1906
|
+
actions: CommandPaletteAction[],
|
|
1907
|
+
executeSlashCommand: (name: string) => Promise<void>,
|
|
1908
|
+
): void {
|
|
1909
|
+
this.#selectorController.showCommandPalette(commands, actions, executeSlashCommand);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
2773
1912
|
showSettingsSelector(): void {
|
|
2774
1913
|
this.#selectorController.showSettingsSelector();
|
|
2775
1914
|
}
|