@gajae-code/coding-agent 0.11.0 → 0.11.2

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.
Files changed (418) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/async/job-manager.d.ts +46 -7
  3. package/dist/types/cli/config-cli.d.ts +11 -1
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +56 -1
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  54. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  55. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  56. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  57. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  58. package/dist/types/goals/runtime.d.ts +1 -0
  59. package/dist/types/goals/state.d.ts +8 -0
  60. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  61. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  62. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  63. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  64. package/dist/types/hindsight/state.d.ts +11 -4
  65. package/dist/types/lsp/lspmux.d.ts +5 -3
  66. package/dist/types/lsp/path-trust.d.ts +1 -0
  67. package/dist/types/main.d.ts +3 -2
  68. package/dist/types/memories/storage.d.ts +1 -0
  69. package/dist/types/modes/action-registry.d.ts +29 -0
  70. package/dist/types/modes/components/chrome.d.ts +14 -0
  71. package/dist/types/modes/components/command-palette.d.ts +27 -0
  72. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +2 -1
  74. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  75. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  76. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  77. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  78. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  79. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  80. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  81. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  82. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  83. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  84. package/dist/types/modes/components/status-line.d.ts +1 -94
  85. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  86. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  87. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  88. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  89. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  90. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  91. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  92. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  93. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  94. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  95. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  96. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  97. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  98. package/dist/types/modes/interactive-mode.d.ts +21 -19
  99. package/dist/types/modes/jobs-observer.d.ts +2 -0
  100. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  101. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +21 -2
  102. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  103. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  104. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  105. package/dist/types/modes/types.d.ts +22 -15
  106. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  107. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  108. package/dist/types/sdk/broker/broker.d.ts +78 -12
  109. package/dist/types/sdk/broker/ensure.d.ts +9 -0
  110. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  111. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  112. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  113. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  114. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  115. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  116. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  117. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  120. package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
  121. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  122. package/dist/types/sdk/bus/index.d.ts +27 -0
  123. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  124. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  125. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  126. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  127. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  128. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  129. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  130. package/dist/types/sdk/bus/telegram-daemon.d.ts +69 -3
  131. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  132. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  133. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  134. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  135. package/dist/types/sdk/host/host.d.ts +2 -0
  136. package/dist/types/sdk/index.d.ts +1 -0
  137. package/dist/types/sdk/session-directory.d.ts +56 -0
  138. package/dist/types/session/agent-session.d.ts +82 -47
  139. package/dist/types/session/agent-storage.d.ts +7 -0
  140. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  141. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  142. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  143. package/dist/types/session/messages.d.ts +16 -0
  144. package/dist/types/session/session-manager-internal.d.ts +14 -0
  145. package/dist/types/session/session-manager.d.ts +82 -12
  146. package/dist/types/session/session-storage.d.ts +43 -3
  147. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  148. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  149. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  150. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  151. package/dist/types/system-prompt.d.ts +2 -11
  152. package/dist/types/task/executor.d.ts +3 -1
  153. package/dist/types/thinking.d.ts +7 -0
  154. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  155. package/dist/types/tools/ask.d.ts +64 -2
  156. package/dist/types/tools/bash.d.ts +3 -0
  157. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  158. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  159. package/dist/types/tools/cron.d.ts +2 -0
  160. package/dist/types/tools/fetch.d.ts +1 -0
  161. package/dist/types/tools/index.d.ts +30 -15
  162. package/dist/types/tools/output-meta.d.ts +1 -0
  163. package/dist/types/tools/read.d.ts +3 -0
  164. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  165. package/dist/types/tools/subagent-render.d.ts +1 -0
  166. package/dist/types/tools/subagent.d.ts +6 -1
  167. package/dist/types/utils/event-bus.d.ts +1 -1
  168. package/dist/types/web/search/index.d.ts +3 -1
  169. package/package.json +21 -9
  170. package/scripts/build-sdk-package-smoke.ts +39 -1
  171. package/scripts/generate-hotkeys-docs.ts +74 -0
  172. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  173. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  174. package/src/async/job-manager.ts +455 -69
  175. package/src/cli/config-cli.ts +75 -2
  176. package/src/cli/notify-cli.ts +92 -26
  177. package/src/cli/session-picker.ts +20 -3
  178. package/src/cli/setup-cli.ts +1 -1
  179. package/src/cli.ts +1 -1
  180. package/src/commands/config.ts +1 -1
  181. package/src/commands/harness.ts +34 -106
  182. package/src/commands/launch.ts +6 -4
  183. package/src/commands/mcp-serve.ts +85 -8
  184. package/src/commands/mcp.ts +2 -2
  185. package/src/commands/notify.ts +8 -4
  186. package/src/commands/sdk.ts +61 -40
  187. package/src/commands/ultragoal.ts +4 -1
  188. package/src/config/atomic-yaml-patch.ts +80 -13
  189. package/src/config/file-lock.ts +105 -43
  190. package/src/config/keybindings.ts +64 -20
  191. package/src/config/model-auth.ts +9 -0
  192. package/src/config/model-bindings-applier.ts +139 -0
  193. package/src/config/model-discovery-manager.ts +224 -0
  194. package/src/config/model-equivalence.ts +66 -0
  195. package/src/config/model-registry.ts +169 -302
  196. package/src/config/model-resolver.ts +361 -369
  197. package/src/config/models-config-schema.ts +5 -4
  198. package/src/config/settings-schema.ts +159 -2
  199. package/src/config/settings.ts +458 -482
  200. package/src/coordinator-mcp/policy.ts +76 -11
  201. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  202. package/src/coordinator-mcp/question-state.ts +778 -0
  203. package/src/coordinator-mcp/server.ts +1702 -238
  204. package/src/debug/index.ts +1 -1
  205. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  206. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  207. package/src/discovery/helpers.ts +1 -50
  208. package/src/edit/index.ts +64 -0
  209. package/src/eval/py/env.ts +26 -0
  210. package/src/eval/py/kernel.ts +5 -3
  211. package/src/extensibility/extensions/loader.ts +9 -1
  212. package/src/extensibility/extensions/runner.ts +12 -3
  213. package/src/extensibility/extensions/types.ts +39 -1
  214. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  215. package/src/extensibility/hooks/runner.ts +2 -2
  216. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  217. package/src/extensibility/shared-events.ts +2 -0
  218. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  219. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  220. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  221. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  222. package/src/gjc-runtime/launch-tmux.ts +2 -5
  223. package/src/gjc-runtime/launch-worktree.ts +5 -5
  224. package/src/gjc-runtime/linux-proc.ts +71 -0
  225. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  226. package/src/gjc-runtime/session-resolution.ts +18 -3
  227. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  228. package/src/gjc-runtime/state-migrations.ts +1 -1
  229. package/src/gjc-runtime/state-runtime.ts +320 -249
  230. package/src/gjc-runtime/state-writer.ts +36 -40
  231. package/src/gjc-runtime/team-launch.ts +319 -0
  232. package/src/gjc-runtime/team-notify.ts +326 -0
  233. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  234. package/src/gjc-runtime/team-store.ts +828 -0
  235. package/src/gjc-runtime/team-workers.ts +962 -0
  236. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  237. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  238. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  239. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  240. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  241. package/src/gjc-runtime/ultragoal-guard.ts +83 -12
  242. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  243. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  244. package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
  245. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  246. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  247. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  248. package/src/goals/continuation-timeout-guard.ts +79 -0
  249. package/src/goals/runtime.ts +7 -5
  250. package/src/goals/state.ts +18 -0
  251. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  252. package/src/harness-control-plane/phase-rollup.ts +4 -7
  253. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  254. package/src/harness-control-plane/receipt-spool.ts +51 -7
  255. package/src/harness-control-plane/session-lease.ts +12 -130
  256. package/src/hindsight/backend.ts +26 -27
  257. package/src/hindsight/state.ts +101 -32
  258. package/src/hooks/skill-state.ts +17 -11
  259. package/src/internal-urls/docs-index.generated.ts +21 -18
  260. package/src/lsp/client.ts +1 -1
  261. package/src/lsp/config.ts +145 -26
  262. package/src/lsp/index.ts +1 -1
  263. package/src/lsp/lspmux.ts +39 -10
  264. package/src/lsp/path-trust.ts +81 -0
  265. package/src/main.ts +28 -8
  266. package/src/memories/index.ts +1 -0
  267. package/src/memories/storage.ts +6 -2
  268. package/src/modes/action-registry.ts +165 -0
  269. package/src/modes/components/chrome.ts +40 -0
  270. package/src/modes/components/command-palette.ts +168 -0
  271. package/src/modes/components/custom-editor.ts +1 -1
  272. package/src/modes/components/custom-provider-wizard.ts +51 -14
  273. package/src/modes/components/index.ts +2 -1
  274. package/src/modes/components/pet-selector.ts +11 -17
  275. package/src/modes/components/plan-preview-overlay.ts +295 -0
  276. package/src/modes/components/queue-mode-selector.ts +10 -27
  277. package/src/modes/components/queue-pane.ts +79 -0
  278. package/src/modes/components/read-tool-group.ts +13 -0
  279. package/src/modes/components/session-observer-overlay.ts +207 -772
  280. package/src/modes/components/sessions-dashboard.ts +181 -0
  281. package/src/modes/components/settings-selector.ts +1 -1
  282. package/src/modes/components/show-images-selector.ts +10 -24
  283. package/src/modes/components/status-line/types.ts +1 -1
  284. package/src/modes/components/status-line.ts +1 -969
  285. package/src/modes/components/tasks-pane.ts +73 -0
  286. package/src/modes/components/theme-selector.ts +11 -31
  287. package/src/modes/components/tool-execution.ts +22 -0
  288. package/src/modes/components/tool-status-header.ts +1056 -0
  289. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  290. package/src/modes/controllers/btw-controller.ts +1 -0
  291. package/src/modes/controllers/command-controller.ts +9 -15
  292. package/src/modes/controllers/event-controller.ts +3 -2
  293. package/src/modes/controllers/extension-ui-controller.ts +72 -25
  294. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  295. package/src/modes/controllers/input-controller.ts +764 -229
  296. package/src/modes/controllers/mode-gate.ts +24 -0
  297. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  298. package/src/modes/controllers/selector-controller.ts +225 -10
  299. package/src/modes/controllers/stt-controller.ts +125 -0
  300. package/src/modes/interactive-mode.ts +302 -1151
  301. package/src/modes/jobs-observer.ts +3 -0
  302. package/src/modes/runtime-init.ts +8 -0
  303. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  304. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +78 -19
  305. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  306. package/src/modes/tasks-aggregator.ts +199 -0
  307. package/src/modes/transcript-item-registry.ts +229 -0
  308. package/src/modes/types.ts +47 -20
  309. package/src/modes/utils/ui-helpers.ts +17 -9
  310. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  311. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  312. package/src/prompts/agents/architect.md +2 -8
  313. package/src/prompts/agents/critic.md +2 -8
  314. package/src/prompts/agents/executor.md +3 -0
  315. package/src/prompts/agents/planner.md +4 -12
  316. package/src/prompts/system/custom-system-prompt.md +1 -23
  317. package/src/prompts/system/plan-mode-approved.md +8 -0
  318. package/src/prompts/system/system-prompt.md +17 -96
  319. package/src/prompts/tools/bash.md +4 -4
  320. package/src/prompts/tools/browser.md +15 -52
  321. package/src/prompts/tools/find.md +2 -2
  322. package/src/prompts/tools/hashline.md +13 -55
  323. package/src/prompts/tools/read.md +3 -6
  324. package/src/prompts/tools/search-tool-bm25.md +0 -2
  325. package/src/prompts/tools/search.md +2 -4
  326. package/src/prompts/tools/skill-discovery.md +1 -2
  327. package/src/prompts/tools/task.md +1 -1
  328. package/src/runtime-mcp/manager.ts +1 -1
  329. package/src/sdk/broker/broker.ts +206 -78
  330. package/src/sdk/broker/discovery.ts +21 -2
  331. package/src/sdk/broker/ensure.ts +85 -25
  332. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  333. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  334. package/src/sdk/broker/lifecycle.ts +1399 -142
  335. package/src/sdk/broker/process-incarnation.ts +5 -13
  336. package/src/sdk/broker/session-index.ts +145 -56
  337. package/src/sdk/broker/state-version.ts +21 -0
  338. package/src/sdk/broker/transport.ts +1 -1
  339. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  340. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  341. package/src/sdk/bus/chat-effect-journal.ts +14 -4
  342. package/src/sdk/bus/config-commands.ts +5 -4
  343. package/src/sdk/bus/config.ts +98 -0
  344. package/src/sdk/bus/conversation-store.ts +5 -29
  345. package/src/sdk/bus/daemon-paths.ts +2 -0
  346. package/src/sdk/bus/discord-daemon.ts +25 -8
  347. package/src/sdk/bus/html-format.ts +20 -3
  348. package/src/sdk/bus/index.ts +834 -113
  349. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  350. package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
  351. package/src/sdk/bus/natives-augment.d.ts +26 -1
  352. package/src/sdk/bus/notification-service.ts +168 -20
  353. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  354. package/src/sdk/bus/recent-activity.ts +188 -74
  355. package/src/sdk/bus/rich-document.ts +258 -0
  356. package/src/sdk/bus/rich-render.ts +62 -70
  357. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  358. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  359. package/src/sdk/bus/telegram-daemon.ts +1491 -247
  360. package/src/sdk/bus/threaded-inbound.ts +8 -3
  361. package/src/sdk/bus/threaded-render.ts +57 -1
  362. package/src/sdk/bus/topic-registry.ts +120 -13
  363. package/src/sdk/host/control/dispatch.ts +14 -2
  364. package/src/sdk/host/control/operations.ts +6 -1
  365. package/src/sdk/host/host.ts +31 -1
  366. package/src/sdk/host/query/handlers.ts +20 -22
  367. package/src/sdk/host/query/revision-store.ts +80 -57
  368. package/src/sdk/index.ts +1 -0
  369. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  370. package/src/sdk/session-directory.ts +141 -0
  371. package/src/sdk/session.ts +458 -479
  372. package/src/session/agent-session.ts +1901 -637
  373. package/src/session/agent-storage.ts +18 -0
  374. package/src/session/cache-economics.ts +25 -8
  375. package/src/session/fallback-chain-controller.ts +53 -3
  376. package/src/session/internal/managed-session-scope.ts +2226 -0
  377. package/src/session/internal/managed-session-storage.ts +453 -0
  378. package/src/session/messages.ts +91 -2
  379. package/src/session/session-manager-internal.ts +33 -0
  380. package/src/session/session-manager.ts +1112 -333
  381. package/src/session/session-storage.ts +444 -39
  382. package/src/session/volatile-context-pruning.ts +78 -0
  383. package/src/setup/credential-auto-import.ts +2 -1
  384. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  385. package/src/setup/provider-onboarding.ts +36 -61
  386. package/src/setup/provider-presets.json +5 -0
  387. package/src/skill-state/active-state.ts +3 -13
  388. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  389. package/src/skill-state/workflow-state-contract.ts +6 -41
  390. package/src/slash-commands/builtin-registry.ts +46 -11
  391. package/src/system-prompt.ts +37 -44
  392. package/src/task/agents.ts +19 -1
  393. package/src/task/executor.ts +90 -6
  394. package/src/task/index.ts +62 -37
  395. package/src/thinking.ts +21 -6
  396. package/src/tool-discovery/tool-index.ts +2 -134
  397. package/src/tools/ask.ts +157 -21
  398. package/src/tools/ast-edit.ts +1 -1
  399. package/src/tools/bash.ts +38 -0
  400. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  401. package/src/tools/browser/tab-supervisor.ts +282 -36
  402. package/src/tools/computer.ts +10 -3
  403. package/src/tools/cron.ts +30 -16
  404. package/src/tools/eval.ts +1 -2
  405. package/src/tools/fetch.ts +11 -4
  406. package/src/tools/index.ts +76 -24
  407. package/src/tools/output-meta.ts +13 -5
  408. package/src/tools/read.ts +37 -1
  409. package/src/tools/resource-gc.ts +2 -2
  410. package/src/tools/search-tool-bm25.ts +11 -74
  411. package/src/tools/subagent-render.ts +19 -8
  412. package/src/tools/subagent.ts +84 -32
  413. package/src/utils/event-bus.ts +9 -4
  414. package/src/web/search/index.ts +3 -3
  415. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  416. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  417. package/src/discovery/plugin-dir-roots.ts +0 -28
  418. 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, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@gajae-code/utils";
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, TemporaryProviderSessionScope } from "../session/agent-session";
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
- getEditorTheme,
122
- getMarkdownTheme,
123
- getSymbolTheme,
124
- onTerminalAppearanceChange,
125
- onThemeChange,
126
- theme,
127
- } from "./theme/theme";
128
- import type {
129
- CompactionQueuedMessage,
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
- #planModePreviousTools: string[] | undefined;
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
- #sttController: STTController | undefined;
427
- #voiceAnimationInterval: NodeJS.Timeout | undefined;
428
- #voiceHue = 0;
429
- #voicePreviousShowHardwareCursor: boolean | null = null;
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
- const slashCommands = [...this.#baseSlashCommands, ...skillCommands];
842
+ this.#resolvedSlashCommands = [...this.#baseSlashCommands, ...skillCommands, ...fileSlashCommands];
799
843
  const autocompleteProvider = this.#inputController.createAutocompleteProvider(
800
- [...slashCommands, ...fileSlashCommands],
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.#exitGoalMode({ reason: "completed", silent: true });
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.#scheduleGoalContinuation();
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(input: {
918
- text: string;
919
- images?: ImageContent[];
920
- customType?: string;
921
- display?: boolean;
922
- }): SubmittedUserInput {
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.#resetGoalContinuationSuppression();
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.setText("");
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
- if (submission.customType === "goal-continuation") {
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
- if (input.customType === "goal-continuation") {
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
- async #getPlanFilePath(): Promise<string> {
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,896 +1249,23 @@ export class InteractiveMode implements InteractiveModeContext {
1277
1249
 
1278
1250
  #updateGoalModeStatus(): void {
1279
1251
  const status =
1280
- this.goalModeEnabled || this.goalModePaused
1281
- ? { enabled: this.goalModeEnabled, paused: this.goalModePaused }
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
- if (event.type === "agent_start") {
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 (e.g. plan mode). */
1264
+ /** Restore mode state from session entries on resume. */
1417
1265
  async #restoreModeFromSession(): Promise<void> {
1418
1266
  const sessionContext = this.sessionManager.buildSessionContext();
1419
- const goalEnabled = this.session.settings.get("goal.enabled");
1420
- if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
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 compactOutcome: CompactionOutcome | undefined;
1824
- try {
1825
- await this.#exitPlanMode({ silent: true, paused: false });
1826
-
1827
- if (!options.preserveContext) {
1828
- await this.handleClearCommand();
1829
- // The new session has a fresh local:// root — persist the approved plan there
1830
- // so `local://<title>.md` resolves correctly in the execution session.
1831
- const newLocalPath = resolveLocalUrlToPath(options.finalPlanFilePath, {
1832
- getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
1833
- getSessionId: () => this.sessionManager.getSessionId(),
1834
- });
1835
- await Bun.write(newLocalPath, planContent);
1836
- } else if (options.compactBeforeExecute) {
1837
- // Distill the plan-mode transcript before the execution turn is queued so
1838
- // the plan-approved synthetic prompt lands as a fresh cache anchor.
1839
- // Outcome is consumed after tool-restoration and plan-reference-path
1840
- // bookkeeping below; `markPlanReferenceSent` is intentionally deferred
1841
- // past the cancel guard — see the comment at the cancel branch.
1842
- // Cancellation skips the synthetic-prompt dispatch (operator's explicit
1843
- // abort is honored); failure proceeds best-effort — approval intent stands.
1844
- const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
1845
- planFilePath: options.finalPlanFilePath,
1846
- });
1847
- // Pin the plan reference path BEFORE compaction so any user messages
1848
- // queued during the compaction await (which `handleCompactCommand`
1849
- // flushes via `flushCompactionQueue` before returning) see the
1850
- // approved plan in `#buildPlanReferenceMessage`. Reassignment after
1851
- // the try/finally is idempotent and kept for the !compactBeforeExecute
1852
- // branch.
1853
- this.session.setPlanReferencePath(options.finalPlanFilePath);
1854
- compactOutcome = await this.handleCompactCommand(compactionPrompt);
1855
- }
1856
- } finally {
1857
- // Unconditional clear. Idempotent: a no-op when the flag was never set
1858
- // (i.e., the !compactBeforeExecute branch), and a no-op when the flag
1859
- // was already consumed by AgentSession.#handleAgentEvent's aborted
1860
- // message_end stamping. Guarantees the flag is dead at every exit.
1861
- this.session.clearPlanCompactAbortPending();
1862
- }
1863
-
1864
- // Tool restoration runs on every path — the plan mode tools must be
1865
- // retired regardless of whether the synthetic prompt fires.
1866
- if (previousTools.length > 0) {
1867
- await this.session.setActiveToolsByName(previousTools);
1868
- }
1869
- this.session.setPlanReferencePath(options.finalPlanFilePath);
1870
-
1871
- if (compactOutcome === "cancelled") {
1872
- // Explicit abort: honor it. `executeCompaction` already surfaced
1873
- // `showError("Compaction cancelled")` to the operator; we add the
1874
- // deferred-dispatch warning and exit. `markPlanReferenceSent` is
1875
- // intentionally skipped here: `#planReferenceSent` stays false, so
1876
- // `AgentSession.#buildPlanReferenceMessage` will inject the plan
1877
- // reference on the operator's next `prompt()` call. If we marked it
1878
- // sent here, the executor's first turn would have no plan context.
1879
- this.showWarning(
1880
- "Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
1881
- );
1882
- return;
1883
- }
1884
-
1885
- // Approved plans land in a fresh (or compacted) session whose first user-visible
1886
- // turn is the synthetic plan-approved prompt — that path bypasses the
1887
- // input-controller's title generation. Seed an auto-name from the plan title
1888
- // so the session is not left unnamed. `setSessionName("auto")` is a no-op
1889
- // when the user has already chosen a name (preserveContext paths).
1890
- const seededName = humanizePlanTitle(options.title);
1891
- if (seededName && !this.sessionManager.getSessionName()) {
1892
- const applied = await this.sessionManager.setSessionName(seededName, "auto");
1893
- if (applied) {
1894
- setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
1895
- this.updateEditorChrome();
1896
- }
1897
- }
1898
-
1899
- // markPlanReferenceSent fires only on the dispatch path so the synthetic
1900
- // plan-approved prompt is the source of the reference injection.
1901
- this.session.markPlanReferenceSent();
1902
- const planModePrompt = prompt.render(planModeApprovedPrompt, {
1903
- planContent,
1904
- finalPlanFilePath: options.finalPlanFilePath,
1905
- contextPreserved: options.preserveContext === true,
1906
- tools: this.session.getActiveToolNames(),
1907
- });
1908
- await this.session.prompt(planModePrompt, { synthetic: true });
1909
- }
1910
-
1911
- async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
1912
- if (this.goalModeEnabled || this.goalModePaused) {
1913
- this.showWarning("Exit goal mode first.");
1914
- return;
1915
- }
1916
- if (this.planModeEnabled) {
1917
- const confirmed = await this.showHookConfirm(
1918
- "Exit plan mode?",
1919
- "This exits plan mode without approving a plan.",
1920
- );
1921
- if (!confirmed) return;
1922
- await this.#exitPlanMode({ paused: true });
1923
- return;
1924
- }
1925
- if (!this.session.settings.get("plan.enabled")) {
1926
- this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
1927
- return;
1928
- }
1929
- await this.#enterPlanMode();
1930
- if (initialPrompt && this.onInputCallback) {
1931
- this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
1932
- }
1933
- }
1934
-
1935
- async handleGoalModeCommand(rest?: string): Promise<void> {
1936
- try {
1937
- if (this.planModeEnabled || this.planModePaused) {
1938
- this.showWarning("Exit plan mode first.");
1939
- return;
1940
- }
1941
- if (!this.session.settings.get("goal.enabled")) {
1942
- this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
1943
- return;
1944
- }
1945
- const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
1946
- if (sub) {
1947
- await this.#dispatchGoalSubcommand(sub, subRest);
1948
- return;
1949
- }
1950
- if (this.goalModeEnabled) {
1951
- if (subRest) {
1952
- this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
1953
- return;
1954
- }
1955
- await this.#openGoalMenu("active");
1956
- return;
1957
- }
1958
- const pausedState = this.#getPausedGoalState();
1959
- if (pausedState) {
1960
- if (subRest) {
1961
- this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
1962
- return;
1963
- }
1964
- await this.#openGoalMenu("paused");
1965
- return;
1966
- }
1967
- if (subRest) {
1968
- await this.#startGoalFromObjective(subRest);
1969
- return;
1970
- }
1971
- const objective = (
1972
- await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
1973
- )?.trim();
1974
- if (!objective) return;
1975
- await this.#startGoalFromObjective(objective);
1976
- } catch (error) {
1977
- this.showError(error instanceof Error ? error.message : String(error));
1978
- }
1979
- }
1980
-
1981
- async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
1982
- switch (sub) {
1983
- case "set":
1984
- await this.#handleGoalSetSubcommand(rest);
1985
- return;
1986
- case "show":
1987
- this.#showGoalDetails();
1988
- return;
1989
- case "pause":
1990
- await this.#pauseGoalAction();
1991
- return;
1992
- case "resume":
1993
- await this.#resumeGoalAction();
1994
- return;
1995
- case "drop":
1996
- await this.#confirmAndDropGoal();
1997
- return;
1998
- }
1999
- }
2000
-
2001
- async #openGoalMenu(state: "active" | "paused"): Promise<void> {
2002
- const goal = this.session.getGoalModeState()?.goal;
2003
- if (!goal) return;
2004
- const summary = goal.objective.length > 48 ? `${goal.objective.slice(0, 47)}…` : goal.objective;
2005
- const title = state === "active" ? `Goal: ${summary} (${goal.status})` : `Goal paused: ${summary}`;
2006
- const items = state === "active" ? ["Show details", "Pause", "Drop"] : ["Resume", "Show details", "Drop"];
2007
- const choice = await this.showHookSelector(title, items);
2008
- if (!choice) return;
2009
- switch (choice) {
2010
- case "Show details":
2011
- this.#showGoalDetails();
2012
- return;
2013
- case "Pause":
2014
- await this.#pauseGoalAction();
2015
- return;
2016
- case "Resume":
2017
- await this.#resumeGoalAction();
2018
- return;
2019
- case "Drop":
2020
- await this.#confirmAndDropGoal();
2021
- return;
2022
- }
2023
- }
2024
-
2025
- #showGoalDetails(): void {
2026
- const state = this.session.getGoalModeState();
2027
- const goal = state?.goal;
2028
- if (!goal) {
2029
- this.showStatus("No goal set.");
2030
- return;
2031
- }
2032
- const used = goal.tokensUsed.toLocaleString();
2033
- const lines = [
2034
- `Objective: ${goal.objective}`,
2035
- `Status: ${goal.status}${state?.enabled ? "" : " (paused)"}`,
2036
- `Tokens used: ${used}`,
2037
- `Time spent: ${formatDuration(goal.timeUsedSeconds * 1000)}`,
2038
- ];
2039
- this.showStatus(lines.join("\n"));
2040
- }
2041
-
2042
- async #pauseGoalAction(): Promise<void> {
2043
- if (!this.goalModeEnabled) {
2044
- this.showWarning("No active goal to pause.");
2045
- return;
2046
- }
2047
- await this.session.goalRuntime.pauseGoal();
2048
- await this.#exitGoalMode({ paused: true, reason: "paused" });
2049
- }
2050
-
2051
- async #resumeGoalAction(): Promise<void> {
2052
- if (!this.#getPausedGoalState()) {
2053
- this.showWarning("No paused goal to resume.");
2054
- return;
2055
- }
2056
- await this.#enterGoalMode({ resume: true, silent: true });
2057
- this.showStatus("Goal mode resumed.");
2058
- this.#scheduleGoalContinuation();
2059
- }
2060
-
2061
- async #confirmAndDropGoal(): Promise<void> {
2062
- if (!this.goalModeEnabled && !this.#getPausedGoalState()) {
2063
- this.showWarning("No goal to drop.");
2064
- return;
2065
- }
2066
- const confirmed = await this.showHookConfirm(
2067
- "Drop goal?",
2068
- "This removes the goal record. Accumulated usage stays in the session log.",
2069
- );
2070
- if (!confirmed) return;
2071
- await this.session.goalRuntime.dropGoal();
2072
- await this.#exitGoalMode({ reason: "dropped" });
2073
- }
2074
-
2075
- async #startGoalFromObjective(objective: string): Promise<void> {
2076
- await this.#enterGoalMode({ objective, silent: true });
2077
- this.#resetGoalContinuationSuppression();
2078
- if (this.onInputCallback) {
2079
- this.onInputCallback(this.startPendingSubmission({ text: objective }));
2080
- }
2081
- }
2082
-
2083
- async #replaceGoalFromObjective(objective: string): Promise<void> {
2084
- const state = await this.session.goalRuntime.replaceGoal({ objective });
2085
- this.session.setGoalModeState(state);
2086
- this.goalModeEnabled = true;
2087
- this.goalModePaused = false;
2088
- this.#resetGoalContinuationSuppression();
2089
- this.#updateGoalModeStatus();
2090
- if (this.session.isStreaming) {
2091
- await this.session.sendGoalModeContext({ deliverAs: "steer" });
2092
- }
2093
- if (this.onInputCallback) {
2094
- this.onInputCallback(this.startPendingSubmission({ text: objective }));
2095
- }
2096
- }
2097
-
2098
- async #handleGoalSetSubcommand(rest: string): Promise<void> {
2099
- if (!this.goalModeEnabled && this.#getPausedGoalState()) {
2100
- this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
2101
- return;
2102
- }
2103
- const objective = rest.trim()
2104
- ? rest.trim()
2105
- : (await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true }))?.trim();
2106
- if (!objective) return;
2107
- if (this.goalModeEnabled) {
2108
- await this.#replaceGoalFromObjective(objective);
2109
- return;
2110
- }
2111
- await this.#startGoalFromObjective(objective);
2112
- }
2113
-
2114
- async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
2115
- if (!this.planModeEnabled) {
2116
- this.showWarning("Plan mode is not active.");
2117
- return;
2118
- }
2119
-
2120
- // Abort the agent to prevent it from continuing (e.g., re-submitting the
2121
- // plan) while the popup is showing. The event listener fires asynchronously
2122
- // (agent's #emit is fire-and-forget), so without this the model sees
2123
- // "Plan ready for approval." and immediately re-invokes `resolve` in a loop.
2124
- await this.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
2125
-
2126
- const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
2127
- this.planModePlanFilePath = planFilePath;
2128
- const planContent = await this.#readPlanFile(planFilePath);
2129
- if (!planContent) {
2130
- this.showError(`Plan file not found at ${planFilePath}`);
2131
- return;
2132
- }
2133
-
2134
- this.#renderPlanPreview(planContent, { append: true });
2135
- const choice = await this.showHookSelector(
2136
- "Plan mode - next step",
2137
- ["Approve and execute", "Approve and compact context", "Approve and keep context", "Refine plan"],
2138
- {
2139
- helpText: this.#getPlanReviewHelpText(),
2140
- onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
2141
- },
2142
- );
2143
-
2144
- if (
2145
- choice === "Approve and execute" ||
2146
- choice === "Approve and compact context" ||
2147
- choice === "Approve and keep context"
2148
- ) {
2149
- const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
2150
- try {
2151
- const latestPlanContent = await this.#readPlanFile(planFilePath);
2152
- if (!latestPlanContent) {
2153
- this.showError(`Plan file not found at ${planFilePath}`);
2154
- return;
2155
- }
2156
- await this.#approvePlan(latestPlanContent, {
2157
- planFilePath,
2158
- finalPlanFilePath,
2159
- title: details.title,
2160
- preserveContext: choice !== "Approve and execute",
2161
- compactBeforeExecute: choice === "Approve and compact context",
2162
- });
2163
- } catch (error) {
2164
- this.showError(
2165
- `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
2166
- );
2167
- }
2168
- return;
2169
- }
1267
+ if (await this.#goalModeController.restoreFromSession(sessionContext)) return;
1268
+ await this.#planModeController.restoreFromSession(sessionContext);
2170
1269
  }
2171
1270
 
2172
1271
  stop(): void {
@@ -2182,18 +1281,18 @@ export class InteractiveMode implements InteractiveModeContext {
2182
1281
  }
2183
1282
  this.#welcomeComponent?.dispose();
2184
1283
  this.#welcomeComponent = undefined;
2185
- this.#cleanupMicAnimation();
2186
- this.#cancelGoalContinuation();
2187
1284
  if (this.#sttController) {
2188
- this.#sttController.dispose();
1285
+ this.#sttController.dispose(this);
2189
1286
  this.#sttController = undefined;
2190
1287
  }
1288
+ this.#goalModeController.cancelContinuation();
2191
1289
  this.#extensionUiController.clearExtensionTerminalInputListeners();
2192
1290
  this.#extensionUiController.clearHookWidgets();
2193
1291
  for (const unsubscribe of this.#eventBusUnsubscribers) {
2194
1292
  unsubscribe();
2195
1293
  }
2196
1294
  this.#eventBusUnsubscribers = [];
1295
+ this.#tasksAggregator?.dispose();
2197
1296
  this.#observerRegistry.dispose();
2198
1297
  this.#eventController.dispose();
2199
1298
  this.statusLine.dispose();
@@ -2237,6 +1336,7 @@ export class InteractiveMode implements InteractiveModeContext {
2237
1336
  this.#btwController.dispose();
2238
1337
 
2239
1338
  // Emit shutdown event to hooks
1339
+ this.session.setSdkPlanModeHandler(null);
2240
1340
  await this.session.dispose();
2241
1341
 
2242
1342
  if (this.isInitialized) {
@@ -2454,8 +1554,8 @@ export class InteractiveMode implements InteractiveModeContext {
2454
1554
  this.#uiHelpers.updatePendingMessagesDisplay();
2455
1555
  }
2456
1556
 
2457
- queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
2458
- this.#uiHelpers.queueCompactionMessage(text, mode);
1557
+ queueCompactionMessage(text: string, mode: "steer" | "followUp", options?: ComposerSubmissionOptions): void {
1558
+ this.#uiHelpers.queueCompactionMessage(text, mode, options);
2459
1559
  }
2460
1560
 
2461
1561
  flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
@@ -2577,15 +1677,18 @@ export class InteractiveMode implements InteractiveModeContext {
2577
1677
  this.#commandController.handleContextCommand();
2578
1678
  }
2579
1679
 
2580
- #prepareSessionSwitch(): void {
1680
+ #prepareSessionSwitch(cleanupPreviousSessionUi?: () => void): void {
2581
1681
  this.#btwController.dispose();
2582
- this.#extensionUiController.clearExtensionTerminalInputListeners();
2583
- this.#planReviewContainer = undefined;
1682
+ if (cleanupPreviousSessionUi) cleanupPreviousSessionUi();
1683
+ else this.#extensionUiController.clearExtensionTerminalInputListeners();
1684
+ this.#planModeController.clearReview();
2584
1685
  }
2585
1686
 
2586
- handleClearCommand(): Promise<void> {
2587
- this.#prepareSessionSwitch();
2588
- return this.#commandController.handleClearCommand();
1687
+ async handleClearCommand(): Promise<boolean> {
1688
+ const cleanupPreviousSessionUi = this.#extensionUiController.captureSessionUiCleanup();
1689
+ const switched = await this.#commandController.handleClearCommand();
1690
+ if (switched) this.#prepareSessionSwitch(cleanupPreviousSessionUi);
1691
+ return switched;
2589
1692
  }
2590
1693
 
2591
1694
  handleContextClearCommand(): Promise<void> {
@@ -2593,9 +1696,11 @@ export class InteractiveMode implements InteractiveModeContext {
2593
1696
  return this.#commandController.handleContextClearCommand();
2594
1697
  }
2595
1698
 
2596
- handleDropCommand(): Promise<void> {
2597
- this.#prepareSessionSwitch();
2598
- return this.#commandController.handleDropCommand();
1699
+ async handleDropCommand(): Promise<boolean> {
1700
+ const cleanupPreviousSessionUi = this.#extensionUiController.captureSessionUiCleanup();
1701
+ const switched = await this.#commandController.handleDropCommand();
1702
+ if (switched) this.#prepareSessionSwitch(cleanupPreviousSessionUi);
1703
+ return switched;
2599
1704
  }
2600
1705
 
2601
1706
  handleForkCommand(): Promise<void> {
@@ -2620,75 +1725,8 @@ export class InteractiveMode implements InteractiveModeContext {
2620
1725
  this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
2621
1726
  return;
2622
1727
  }
2623
- if (!this.#sttController) {
2624
- this.#sttController = new STTController();
2625
- }
2626
- await this.#sttController.toggle(this.editor, {
2627
- showWarning: (msg: string) => this.showWarning(msg),
2628
- showStatus: (msg: string) => this.showStatus(msg),
2629
- onStateChange: (state: SttState) => {
2630
- if (state === "recording") {
2631
- this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
2632
- this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
2633
- this.ui.setShowHardwareCursor(false);
2634
- this.editor.setUseTerminalCursor(false);
2635
- this.#startMicAnimation();
2636
- } else if (state === "transcribing") {
2637
- this.#stopMicAnimation();
2638
- this.#setMicCursor({ r: 200, g: 200, b: 200 });
2639
- } else {
2640
- this.#cleanupMicAnimation();
2641
- }
2642
- this.updateEditorChrome();
2643
- this.ui.requestRender();
2644
- },
2645
- });
2646
- }
2647
-
2648
- #setMicCursor(color: { r: number; g: number; b: number }): void {
2649
- this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
2650
- // Theme symbols can be wide (for example, 🎤), so measure the rendered override.
2651
- this.editor.cursorOverrideWidth = visibleWidth(this.editor.cursorOverride);
2652
- }
2653
-
2654
- #updateMicIcon(): void {
2655
- const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
2656
- this.#setMicCursor({ r, g, b });
2657
- }
2658
-
2659
- #startMicAnimation(): void {
2660
- if (this.#voiceAnimationInterval) return;
2661
- this.#voiceHue = 0;
2662
- this.#updateMicIcon();
2663
- this.#voiceAnimationInterval = setInterval(() => {
2664
- this.#voiceHue = (this.#voiceHue + 8) % 360;
2665
- this.#updateMicIcon();
2666
- this.ui.requestRender();
2667
- }, 60);
2668
- }
2669
-
2670
- #stopMicAnimation(): void {
2671
- if (this.#voiceAnimationInterval) {
2672
- clearInterval(this.#voiceAnimationInterval);
2673
- this.#voiceAnimationInterval = undefined;
2674
- }
2675
- }
2676
-
2677
- #cleanupMicAnimation(): void {
2678
- if (this.#voiceAnimationInterval) {
2679
- clearInterval(this.#voiceAnimationInterval);
2680
- this.#voiceAnimationInterval = undefined;
2681
- }
2682
- this.editor.cursorOverride = undefined;
2683
- this.editor.cursorOverrideWidth = undefined;
2684
- if (this.#voicePreviousShowHardwareCursor !== null) {
2685
- this.ui.setShowHardwareCursor(this.#voicePreviousShowHardwareCursor);
2686
- this.#voicePreviousShowHardwareCursor = null;
2687
- }
2688
- if (this.#voicePreviousUseTerminalCursor !== null) {
2689
- this.editor.setUseTerminalCursor(this.#voicePreviousUseTerminalCursor);
2690
- this.#voicePreviousUseTerminalCursor = null;
2691
- }
1728
+ this.#sttController ??= new SttModeController();
1729
+ await this.#sttController.toggle(this);
2692
1730
  }
2693
1731
 
2694
1732
  showDebugSelector(): void {
@@ -2704,6 +1742,103 @@ export class InteractiveMode implements InteractiveModeContext {
2704
1742
  this.#selectorController.showSessionObserver(this.#observerRegistry);
2705
1743
  }
2706
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
+
2707
1842
  showJobsOverlay(): void {
2708
1843
  if (!this.#jobsObserver) {
2709
1844
  this.showStatus("Background jobs are unavailable in this session");
@@ -2712,6 +1847,14 @@ export class InteractiveMode implements InteractiveModeContext {
2712
1847
  this.#selectorController.showJobsOverlay(this.#jobsObserver);
2713
1848
  }
2714
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
+
2715
1858
  resetObserverRegistry(): void {
2716
1859
  this.#observerRegistry.resetSessions();
2717
1860
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
@@ -2758,6 +1901,14 @@ export class InteractiveMode implements InteractiveModeContext {
2758
1901
  }
2759
1902
 
2760
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
+
2761
1912
  showSettingsSelector(): void {
2762
1913
  this.#selectorController.showSettingsSelector();
2763
1914
  }