pi-agent-zh 17.1.8

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 (1519) hide show
  1. package/CHANGELOG.md +14459 -0
  2. package/README.md +35 -0
  3. package/dist/CHANGELOG-vt8ene9g.md +14459 -0
  4. package/dist/cli.js +22079 -0
  5. package/dist/template-dys3vk5b.js +1671 -0
  6. package/dist/template-f8wx9vfn.css +1355 -0
  7. package/dist/template-qat058wr.html +55 -0
  8. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  9. package/examples/README.md +21 -0
  10. package/examples/custom-tools/README.md +104 -0
  11. package/examples/custom-tools/hello/index.ts +20 -0
  12. package/examples/extensions/README.md +142 -0
  13. package/examples/extensions/api-demo.ts +79 -0
  14. package/examples/extensions/chalk-logger.ts +25 -0
  15. package/examples/extensions/hello.ts +31 -0
  16. package/examples/extensions/pirate.ts +43 -0
  17. package/examples/extensions/plan-mode.ts +549 -0
  18. package/examples/extensions/reload-runtime.ts +38 -0
  19. package/examples/extensions/thinking-note.ts +13 -0
  20. package/examples/extensions/tools.ts +145 -0
  21. package/examples/extensions/with-deps/index.ts +36 -0
  22. package/examples/extensions/with-deps/package-lock.json +31 -0
  23. package/examples/extensions/with-deps/package.json +17 -0
  24. package/examples/hooks/README.md +56 -0
  25. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  26. package/examples/hooks/confirm-destructive.ts +58 -0
  27. package/examples/hooks/custom-compaction.ts +115 -0
  28. package/examples/hooks/dirty-repo-guard.ts +51 -0
  29. package/examples/hooks/file-trigger.ts +40 -0
  30. package/examples/hooks/git-checkpoint.ts +52 -0
  31. package/examples/hooks/handoff.ts +149 -0
  32. package/examples/hooks/permission-gate.ts +33 -0
  33. package/examples/hooks/protected-paths.ts +29 -0
  34. package/examples/hooks/qna.ts +118 -0
  35. package/examples/hooks/status-line.ts +39 -0
  36. package/examples/sdk/01-minimal.ts +21 -0
  37. package/examples/sdk/02-custom-model.ts +49 -0
  38. package/examples/sdk/03-custom-prompt.ts +46 -0
  39. package/examples/sdk/04-skills.ts +43 -0
  40. package/examples/sdk/06-extensions.ts +82 -0
  41. package/examples/sdk/06-hooks.ts +61 -0
  42. package/examples/sdk/07-context-files.ts +35 -0
  43. package/examples/sdk/08-prompt-templates.ts +41 -0
  44. package/examples/sdk/08-slash-commands.ts +46 -0
  45. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  46. package/examples/sdk/11-sessions.ts +47 -0
  47. package/examples/sdk/12-redis-sessions.ts +54 -0
  48. package/examples/sdk/13-sql-sessions.ts +61 -0
  49. package/examples/sdk/README.md +169 -0
  50. package/package.json +606 -0
  51. package/scripts/bench-guard.ts +71 -0
  52. package/scripts/bench-title-models.ts +332 -0
  53. package/scripts/build-binary.ts +117 -0
  54. package/scripts/bundle-dist.ts +133 -0
  55. package/scripts/compile-binary.ts +69 -0
  56. package/scripts/embed-mupdf-wasm.ts +67 -0
  57. package/scripts/format-prompts.ts +68 -0
  58. package/scripts/generate-aria-snapshot.ts +134 -0
  59. package/scripts/generate-docs-index.ts +58 -0
  60. package/scripts/generate-share-viewer.ts +34 -0
  61. package/scripts/legacy-pi-virtual-module.ts +195 -0
  62. package/scripts/measure-prompt-tokens.ts +63 -0
  63. package/scripts/omp +42 -0
  64. package/scripts/omp.ts +19 -0
  65. package/src/advisor/advise-tool.ts +216 -0
  66. package/src/advisor/config.ts +341 -0
  67. package/src/advisor/emission-guard.ts +172 -0
  68. package/src/advisor/index.ts +6 -0
  69. package/src/advisor/runtime.ts +1232 -0
  70. package/src/advisor/transcript-recorder.ts +159 -0
  71. package/src/advisor/watchdog.ts +135 -0
  72. package/src/async/index.ts +1 -0
  73. package/src/async/job-manager.ts +821 -0
  74. package/src/auto-thinking/classifier.ts +182 -0
  75. package/src/autolearn/controller.ts +152 -0
  76. package/src/autolearn/managed-skills.ts +255 -0
  77. package/src/autoresearch/command-resume.md +14 -0
  78. package/src/autoresearch/dashboard.ts +436 -0
  79. package/src/autoresearch/git.ts +331 -0
  80. package/src/autoresearch/helpers.ts +218 -0
  81. package/src/autoresearch/index.ts +541 -0
  82. package/src/autoresearch/prompt-setup.md +43 -0
  83. package/src/autoresearch/prompt.md +103 -0
  84. package/src/autoresearch/resume-message.md +10 -0
  85. package/src/autoresearch/state.ts +273 -0
  86. package/src/autoresearch/storage.ts +700 -0
  87. package/src/autoresearch/tools/init-experiment.ts +269 -0
  88. package/src/autoresearch/tools/log-experiment.ts +521 -0
  89. package/src/autoresearch/tools/run-experiment.ts +407 -0
  90. package/src/autoresearch/tools/update-notes.ts +109 -0
  91. package/src/autoresearch/types.ts +168 -0
  92. package/src/capability/context-file.ts +44 -0
  93. package/src/capability/extension-module.ts +34 -0
  94. package/src/capability/extension.ts +47 -0
  95. package/src/capability/fs.ts +117 -0
  96. package/src/capability/hook.ts +40 -0
  97. package/src/capability/index.ts +467 -0
  98. package/src/capability/instruction.ts +37 -0
  99. package/src/capability/mcp.ts +97 -0
  100. package/src/capability/prompt.ts +35 -0
  101. package/src/capability/rule-buckets.ts +66 -0
  102. package/src/capability/rule.ts +298 -0
  103. package/src/capability/settings.ts +34 -0
  104. package/src/capability/skill.ts +63 -0
  105. package/src/capability/slash-command.ts +40 -0
  106. package/src/capability/ssh.ts +41 -0
  107. package/src/capability/system-prompt.ts +34 -0
  108. package/src/capability/tool.ts +38 -0
  109. package/src/capability/types.ts +187 -0
  110. package/src/cleanse/agent.ts +226 -0
  111. package/src/cleanse/balance.ts +79 -0
  112. package/src/cleanse/checkers.ts +996 -0
  113. package/src/cleanse/index.ts +190 -0
  114. package/src/cleanse/loop.ts +51 -0
  115. package/src/cleanse/parsers.ts +726 -0
  116. package/src/cleanse/progress.ts +50 -0
  117. package/src/cleanse/prompts/assignment.md +47 -0
  118. package/src/cleanse/types.ts +72 -0
  119. package/src/cli/agents-cli.ts +138 -0
  120. package/src/cli/args.ts +420 -0
  121. package/src/cli/auth-broker-cli.ts +940 -0
  122. package/src/cli/auth-gateway-cli.ts +674 -0
  123. package/src/cli/bench-cli.ts +990 -0
  124. package/src/cli/classify-install-target.ts +76 -0
  125. package/src/cli/claude-trace-cli.ts +795 -0
  126. package/src/cli/commands/init-xdg.ts +27 -0
  127. package/src/cli/completion-gen.ts +550 -0
  128. package/src/cli/config-cli.ts +459 -0
  129. package/src/cli/dry-balance-cli.ts +864 -0
  130. package/src/cli/extension-flags.ts +48 -0
  131. package/src/cli/file-processor.ts +132 -0
  132. package/src/cli/flag-tables.ts +352 -0
  133. package/src/cli/gallery-cli.ts +272 -0
  134. package/src/cli/gallery-fixtures/agentic.ts +420 -0
  135. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  136. package/src/cli/gallery-fixtures/edit.ts +254 -0
  137. package/src/cli/gallery-fixtures/fs.ts +220 -0
  138. package/src/cli/gallery-fixtures/index.ts +40 -0
  139. package/src/cli/gallery-fixtures/interaction.ts +46 -0
  140. package/src/cli/gallery-fixtures/memory.ts +81 -0
  141. package/src/cli/gallery-fixtures/misc.ts +177 -0
  142. package/src/cli/gallery-fixtures/search.ts +135 -0
  143. package/src/cli/gallery-fixtures/shell.ts +241 -0
  144. package/src/cli/gallery-fixtures/types.ts +57 -0
  145. package/src/cli/gallery-fixtures/web.ts +158 -0
  146. package/src/cli/gallery-screenshot.ts +279 -0
  147. package/src/cli/gc-cli.ts +946 -0
  148. package/src/cli/grep-cli.ts +161 -0
  149. package/src/cli/grievances-cli.ts +256 -0
  150. package/src/cli/initial-message.ts +58 -0
  151. package/src/cli/models-cli.ts +388 -0
  152. package/src/cli/plugin-cli.ts +996 -0
  153. package/src/cli/profile-alias.ts +369 -0
  154. package/src/cli/profile-bootstrap.ts +233 -0
  155. package/src/cli/read-cli.ts +99 -0
  156. package/src/cli/session-picker.ts +93 -0
  157. package/src/cli/setup-cli.ts +320 -0
  158. package/src/cli/setup-model-picker.ts +43 -0
  159. package/src/cli/shell-cli.ts +176 -0
  160. package/src/cli/ssh-cli.ts +179 -0
  161. package/src/cli/startup-cwd.ts +58 -0
  162. package/src/cli/stats-cli.ts +229 -0
  163. package/src/cli/tiny-models-cli.ts +153 -0
  164. package/src/cli/ttsr-cli.ts +995 -0
  165. package/src/cli/update-cli.ts +1219 -0
  166. package/src/cli/usage-cli.ts +1084 -0
  167. package/src/cli/usage-error.ts +7 -0
  168. package/src/cli/web-search-cli.ts +144 -0
  169. package/src/cli/worktree-cli.ts +311 -0
  170. package/src/cli-commands.ts +166 -0
  171. package/src/cli.ts +407 -0
  172. package/src/collab/crypto.ts +63 -0
  173. package/src/collab/display-name.ts +13 -0
  174. package/src/collab/guest.ts +731 -0
  175. package/src/collab/host.ts +691 -0
  176. package/src/collab/protocol.ts +296 -0
  177. package/src/collab/relay-client.ts +282 -0
  178. package/src/collab/replication-shrink.ts +111 -0
  179. package/src/commands/acp.ts +33 -0
  180. package/src/commands/agents.ts +57 -0
  181. package/src/commands/auth-broker.ts +99 -0
  182. package/src/commands/auth-gateway.ts +69 -0
  183. package/src/commands/bench.ts +65 -0
  184. package/src/commands/cleanse.ts +45 -0
  185. package/src/commands/commit.ts +46 -0
  186. package/src/commands/complete.ts +66 -0
  187. package/src/commands/completions.ts +60 -0
  188. package/src/commands/config.ts +51 -0
  189. package/src/commands/dry-balance.ts +43 -0
  190. package/src/commands/gallery.ts +60 -0
  191. package/src/commands/gc.ts +46 -0
  192. package/src/commands/grep.ts +48 -0
  193. package/src/commands/grievances.ts +51 -0
  194. package/src/commands/install.ts +107 -0
  195. package/src/commands/join.ts +39 -0
  196. package/src/commands/launch.ts +212 -0
  197. package/src/commands/models.ts +60 -0
  198. package/src/commands/plugin.ts +78 -0
  199. package/src/commands/read.ts +38 -0
  200. package/src/commands/say.ts +145 -0
  201. package/src/commands/setup.ts +67 -0
  202. package/src/commands/shell.ts +29 -0
  203. package/src/commands/ssh.ts +60 -0
  204. package/src/commands/stats.ts +29 -0
  205. package/src/commands/tiny-models.ts +36 -0
  206. package/src/commands/token.ts +164 -0
  207. package/src/commands/ttsr.ts +125 -0
  208. package/src/commands/update.ts +33 -0
  209. package/src/commands/usage.ts +54 -0
  210. package/src/commands/web-search.ts +42 -0
  211. package/src/commands/worktree.ts +62 -0
  212. package/src/commit/agentic/agent.ts +322 -0
  213. package/src/commit/agentic/fallback.ts +96 -0
  214. package/src/commit/agentic/index.ts +382 -0
  215. package/src/commit/agentic/lock-files.ts +107 -0
  216. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  217. package/src/commit/agentic/prompts/session-user.md +25 -0
  218. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  219. package/src/commit/agentic/prompts/system.md +38 -0
  220. package/src/commit/agentic/state.ts +60 -0
  221. package/src/commit/agentic/tools/analyze-file.ts +148 -0
  222. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  223. package/src/commit/agentic/tools/git-hunk.ts +52 -0
  224. package/src/commit/agentic/tools/git-overview.ts +62 -0
  225. package/src/commit/agentic/tools/index.ts +54 -0
  226. package/src/commit/agentic/tools/propose-changelog.ts +147 -0
  227. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  228. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  229. package/src/commit/agentic/tools/schemas.ts +11 -0
  230. package/src/commit/agentic/tools/split-commit.ts +241 -0
  231. package/src/commit/agentic/topo-sort.ts +44 -0
  232. package/src/commit/agentic/trivial.ts +51 -0
  233. package/src/commit/agentic/validation.ts +183 -0
  234. package/src/commit/analysis/conventional.ts +64 -0
  235. package/src/commit/analysis/index.ts +4 -0
  236. package/src/commit/analysis/scope.ts +242 -0
  237. package/src/commit/analysis/summary.ts +107 -0
  238. package/src/commit/analysis/validation.ts +66 -0
  239. package/src/commit/changelog/detect.ts +40 -0
  240. package/src/commit/changelog/generate.ts +101 -0
  241. package/src/commit/changelog/index.ts +234 -0
  242. package/src/commit/changelog/parse.ts +44 -0
  243. package/src/commit/cli.ts +85 -0
  244. package/src/commit/git/diff.ts +148 -0
  245. package/src/commit/index.ts +5 -0
  246. package/src/commit/map-reduce/index.ts +69 -0
  247. package/src/commit/map-reduce/map-phase.ts +193 -0
  248. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  249. package/src/commit/map-reduce/utils.ts +9 -0
  250. package/src/commit/message.ts +11 -0
  251. package/src/commit/model-selection.ts +95 -0
  252. package/src/commit/pipeline.ts +243 -0
  253. package/src/commit/prompts/analysis-system.md +148 -0
  254. package/src/commit/prompts/analysis-user.md +38 -0
  255. package/src/commit/prompts/changelog-system.md +50 -0
  256. package/src/commit/prompts/changelog-user.md +18 -0
  257. package/src/commit/prompts/file-observer-system.md +24 -0
  258. package/src/commit/prompts/file-observer-user.md +8 -0
  259. package/src/commit/prompts/reduce-system.md +50 -0
  260. package/src/commit/prompts/reduce-user.md +17 -0
  261. package/src/commit/prompts/summary-retry.md +3 -0
  262. package/src/commit/prompts/summary-system.md +38 -0
  263. package/src/commit/prompts/summary-user.md +13 -0
  264. package/src/commit/prompts/types-description.md +2 -0
  265. package/src/commit/shared-llm.ts +70 -0
  266. package/src/commit/types.ts +118 -0
  267. package/src/commit/utils/exclusions.ts +42 -0
  268. package/src/commit/utils.ts +58 -0
  269. package/src/config/api-key-resolver.ts +81 -0
  270. package/src/config/append-only-context-mode.ts +76 -0
  271. package/src/config/config-file.ts +347 -0
  272. package/src/config/file-lock.ts +164 -0
  273. package/src/config/inline-tool-descriptors-mode.ts +26 -0
  274. package/src/config/keybindings.ts +688 -0
  275. package/src/config/mcp-schema.json +247 -0
  276. package/src/config/model-discovery.ts +999 -0
  277. package/src/config/model-registry.ts +2725 -0
  278. package/src/config/model-resolver.ts +2068 -0
  279. package/src/config/model-roles.ts +113 -0
  280. package/src/config/models-config-schema-bundle.ts +313 -0
  281. package/src/config/models-config-schema.ts +14 -0
  282. package/src/config/models-config.ts +130 -0
  283. package/src/config/prompt-templates.ts +205 -0
  284. package/src/config/provider-globals.ts +25 -0
  285. package/src/config/resolve-config-value.ts +94 -0
  286. package/src/config/service-tier.ts +141 -0
  287. package/src/config/settings-schema.ts +5751 -0
  288. package/src/config/settings.ts +2369 -0
  289. package/src/config.ts +242 -0
  290. package/src/cursor.ts +513 -0
  291. package/src/dap/client.ts +1043 -0
  292. package/src/dap/config.ts +480 -0
  293. package/src/dap/defaults.json +212 -0
  294. package/src/dap/index.ts +4 -0
  295. package/src/dap/session.ts +1841 -0
  296. package/src/dap/types.ts +611 -0
  297. package/src/debug/index.ts +584 -0
  298. package/src/debug/log-formatting.ts +58 -0
  299. package/src/debug/log-viewer.ts +966 -0
  300. package/src/debug/profiler.ts +168 -0
  301. package/src/debug/protocol-probe.ts +267 -0
  302. package/src/debug/raw-sse-buffer.ts +421 -0
  303. package/src/debug/raw-sse.ts +312 -0
  304. package/src/debug/remote-debugger.ts +151 -0
  305. package/src/debug/report-bundle.ts +411 -0
  306. package/src/debug/system-info.ts +111 -0
  307. package/src/debug/terminal-info.ts +124 -0
  308. package/src/discovery/agents-md.ts +67 -0
  309. package/src/discovery/agents.ts +230 -0
  310. package/src/discovery/at-imports.ts +273 -0
  311. package/src/discovery/builtin-defaults.ts +39 -0
  312. package/src/discovery/builtin-rules/go-add-cleanup.md +33 -0
  313. package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
  314. package/src/discovery/builtin-rules/go-exp-promoted.md +40 -0
  315. package/src/discovery/builtin-rules/go-ioutil.md +37 -0
  316. package/src/discovery/builtin-rules/go-join-hostport.md +30 -0
  317. package/src/discovery/builtin-rules/go-new-expr.md +44 -0
  318. package/src/discovery/builtin-rules/go-rand-v2.md +41 -0
  319. package/src/discovery/builtin-rules/go-range-int.md +45 -0
  320. package/src/discovery/builtin-rules/index.ts +74 -0
  321. package/src/discovery/builtin-rules/rs-box-leak.md +49 -0
  322. package/src/discovery/builtin-rules/rs-future-prelude.md +24 -0
  323. package/src/discovery/builtin-rules/rs-lazylock.md +52 -0
  324. package/src/discovery/builtin-rules/rs-match-ergonomics.md +68 -0
  325. package/src/discovery/builtin-rules/rs-parking-lot.md +45 -0
  326. package/src/discovery/builtin-rules/rs-result-type.md +20 -0
  327. package/src/discovery/builtin-rules/ts-bare-catch.md +39 -0
  328. package/src/discovery/builtin-rules/ts-import-type.md +43 -0
  329. package/src/discovery/builtin-rules/ts-no-any.md +66 -0
  330. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +45 -0
  331. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +40 -0
  332. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  333. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  334. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  335. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  336. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  337. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +66 -0
  338. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  339. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  340. package/src/discovery/builtin.ts +935 -0
  341. package/src/discovery/claude-plugins.ts +624 -0
  342. package/src/discovery/claude.ts +588 -0
  343. package/src/discovery/cline.ts +83 -0
  344. package/src/discovery/codex.ts +540 -0
  345. package/src/discovery/cursor.ts +220 -0
  346. package/src/discovery/gemini.ts +383 -0
  347. package/src/discovery/github.ts +337 -0
  348. package/src/discovery/helpers.ts +1159 -0
  349. package/src/discovery/index.ts +81 -0
  350. package/src/discovery/mcp-json.ts +172 -0
  351. package/src/discovery/omp-extension-roots.ts +213 -0
  352. package/src/discovery/omp-plugins.ts +387 -0
  353. package/src/discovery/opencode.ts +441 -0
  354. package/src/discovery/plugin-dir-roots.ts +28 -0
  355. package/src/discovery/ssh.ts +153 -0
  356. package/src/discovery/substitute-plugin-root.ts +77 -0
  357. package/src/discovery/vscode.ts +105 -0
  358. package/src/discovery/windsurf.ts +147 -0
  359. package/src/edit/apply-patch/index.ts +87 -0
  360. package/src/edit/apply-patch/parser.ts +174 -0
  361. package/src/edit/diff.ts +999 -0
  362. package/src/edit/file-snapshot-store.ts +148 -0
  363. package/src/edit/hashline/block-resolver.ts +33 -0
  364. package/src/edit/hashline/diff.ts +356 -0
  365. package/src/edit/hashline/execute.ts +280 -0
  366. package/src/edit/hashline/filesystem.ts +223 -0
  367. package/src/edit/hashline/index.ts +5 -0
  368. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  369. package/src/edit/hashline/params.ts +12 -0
  370. package/src/edit/index.ts +676 -0
  371. package/src/edit/modes/apply-patch.lark +19 -0
  372. package/src/edit/modes/apply-patch.ts +53 -0
  373. package/src/edit/modes/patch.ts +1958 -0
  374. package/src/edit/modes/replace.ts +1137 -0
  375. package/src/edit/normalize.ts +345 -0
  376. package/src/edit/notebook.ts +242 -0
  377. package/src/edit/read-file.ts +25 -0
  378. package/src/edit/renderer.ts +998 -0
  379. package/src/edit/snapshot-details.ts +77 -0
  380. package/src/edit/streaming.ts +698 -0
  381. package/src/eval/agent-bridge.ts +224 -0
  382. package/src/eval/backend-helpers.ts +48 -0
  383. package/src/eval/backend.ts +71 -0
  384. package/src/eval/bridge-timeout.ts +62 -0
  385. package/src/eval/budget-bridge.ts +48 -0
  386. package/src/eval/completion-bridge.ts +211 -0
  387. package/src/eval/concurrency-bridge.ts +34 -0
  388. package/src/eval/executor-base.ts +504 -0
  389. package/src/eval/idle-timeout.ts +91 -0
  390. package/src/eval/index.ts +6 -0
  391. package/src/eval/jl/executor.ts +540 -0
  392. package/src/eval/jl/index.ts +54 -0
  393. package/src/eval/jl/kernel.ts +236 -0
  394. package/src/eval/jl/prelude.jl +736 -0
  395. package/src/eval/jl/prelude.ts +3 -0
  396. package/src/eval/jl/runner.jl +666 -0
  397. package/src/eval/jl/runtime.ts +118 -0
  398. package/src/eval/js/context-manager.ts +701 -0
  399. package/src/eval/js/executor.ts +175 -0
  400. package/src/eval/js/index.ts +40 -0
  401. package/src/eval/js/process-entry.ts +31 -0
  402. package/src/eval/js/shared/helpers.ts +170 -0
  403. package/src/eval/js/shared/indirect-eval.ts +30 -0
  404. package/src/eval/js/shared/local-module-loader.ts +364 -0
  405. package/src/eval/js/shared/prelude.ts +2 -0
  406. package/src/eval/js/shared/prelude.txt +294 -0
  407. package/src/eval/js/shared/rewrite-imports.ts +550 -0
  408. package/src/eval/js/shared/runtime.ts +590 -0
  409. package/src/eval/js/shared/types.ts +18 -0
  410. package/src/eval/js/tool-bridge.ts +163 -0
  411. package/src/eval/js/worker-core.ts +380 -0
  412. package/src/eval/js/worker-entry.ts +37 -0
  413. package/src/eval/js/worker-protocol.ts +47 -0
  414. package/src/eval/kernel-base.ts +569 -0
  415. package/src/eval/py/display.ts +71 -0
  416. package/src/eval/py/executor.ts +603 -0
  417. package/src/eval/py/index.ts +57 -0
  418. package/src/eval/py/kernel.ts +234 -0
  419. package/src/eval/py/prelude.py +672 -0
  420. package/src/eval/py/prelude.ts +3 -0
  421. package/src/eval/py/runner.py +1388 -0
  422. package/src/eval/py/runtime.ts +276 -0
  423. package/src/eval/py/spawn-options.ts +139 -0
  424. package/src/eval/py/tool-bridge.ts +181 -0
  425. package/src/eval/rb/executor.ts +504 -0
  426. package/src/eval/rb/index.ts +54 -0
  427. package/src/eval/rb/kernel.ts +231 -0
  428. package/src/eval/rb/prelude.rb +552 -0
  429. package/src/eval/rb/prelude.ts +3 -0
  430. package/src/eval/rb/runner.rb +581 -0
  431. package/src/eval/rb/runtime.ts +132 -0
  432. package/src/eval/runtime-env.ts +104 -0
  433. package/src/eval/session-id.ts +8 -0
  434. package/src/eval/types.ts +48 -0
  435. package/src/exa/index.ts +2 -0
  436. package/src/exa/mcp-client.ts +370 -0
  437. package/src/exa/types.ts +69 -0
  438. package/src/exec/bash-executor.ts +627 -0
  439. package/src/exec/direnv.ts +145 -0
  440. package/src/exec/exec.ts +53 -0
  441. package/src/exec/non-interactive-env.ts +118 -0
  442. package/src/export/custom-share.ts +65 -0
  443. package/src/export/html/args.ts +20 -0
  444. package/src/export/html/index.ts +313 -0
  445. package/src/export/html/share-loader.js +102 -0
  446. package/src/export/html/template.css +1355 -0
  447. package/src/export/html/template.html +55 -0
  448. package/src/export/html/template.js +1671 -0
  449. package/src/export/html/tool-views.generated.js +35 -0
  450. package/src/export/html/vendor/highlight.min.js +1213 -0
  451. package/src/export/html/vendor/marked.min.js +6 -0
  452. package/src/export/html/web-palette.ts +126 -0
  453. package/src/export/share.ts +682 -0
  454. package/src/export/ttsr.ts +590 -0
  455. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  456. package/src/extensibility/custom-commands/bundled/review/index.ts +698 -0
  457. package/src/extensibility/custom-commands/index.ts +2 -0
  458. package/src/extensibility/custom-commands/loader.ts +242 -0
  459. package/src/extensibility/custom-commands/types.ts +119 -0
  460. package/src/extensibility/custom-tools/index.ts +7 -0
  461. package/src/extensibility/custom-tools/loader.ts +301 -0
  462. package/src/extensibility/custom-tools/types.ts +286 -0
  463. package/src/extensibility/custom-tools/wrapper.ts +50 -0
  464. package/src/extensibility/extensions/compact-handler.ts +40 -0
  465. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  466. package/src/extensibility/extensions/index.ts +16 -0
  467. package/src/extensibility/extensions/load-errors.ts +13 -0
  468. package/src/extensibility/extensions/loader.ts +624 -0
  469. package/src/extensibility/extensions/managed-timers.ts +83 -0
  470. package/src/extensibility/extensions/model-api.ts +39 -0
  471. package/src/extensibility/extensions/runner.ts +1209 -0
  472. package/src/extensibility/extensions/types.ts +1526 -0
  473. package/src/extensibility/extensions/wrapper.ts +377 -0
  474. package/src/extensibility/hooks/index.ts +6 -0
  475. package/src/extensibility/hooks/loader.ts +244 -0
  476. package/src/extensibility/hooks/runner.ts +425 -0
  477. package/src/extensibility/hooks/tool-wrapper.ts +124 -0
  478. package/src/extensibility/hooks/types.ts +612 -0
  479. package/src/extensibility/legacy-pi-ai-shim.ts +140 -0
  480. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1384 -0
  481. package/src/extensibility/legacy-pi-tui-shim.ts +43 -0
  482. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  483. package/src/extensibility/plugins/doctor.ts +65 -0
  484. package/src/extensibility/plugins/git-url.ts +367 -0
  485. package/src/extensibility/plugins/index.ts +9 -0
  486. package/src/extensibility/plugins/installer.ts +201 -0
  487. package/src/extensibility/plugins/legacy-pi-compat.ts +2532 -0
  488. package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
  489. package/src/extensibility/plugins/loader.ts +535 -0
  490. package/src/extensibility/plugins/manager.ts +1142 -0
  491. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  492. package/src/extensibility/plugins/marketplace/fetcher.ts +316 -0
  493. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  494. package/src/extensibility/plugins/marketplace/manager.ts +926 -0
  495. package/src/extensibility/plugins/marketplace/registry.ts +196 -0
  496. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  497. package/src/extensibility/plugins/marketplace/types.ts +192 -0
  498. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  499. package/src/extensibility/plugins/parser.ts +107 -0
  500. package/src/extensibility/plugins/runtime-config.ts +9 -0
  501. package/src/extensibility/plugins/types.ts +194 -0
  502. package/src/extensibility/session-handler-types.ts +21 -0
  503. package/src/extensibility/shared-events.ts +401 -0
  504. package/src/extensibility/skills.ts +511 -0
  505. package/src/extensibility/slash-commands.ts +131 -0
  506. package/src/extensibility/tool-event-input.ts +80 -0
  507. package/src/extensibility/tool-proxy.ts +28 -0
  508. package/src/extensibility/typebox.ts +958 -0
  509. package/src/extensibility/utils.ts +184 -0
  510. package/src/goals/index.ts +3 -0
  511. package/src/goals/runtime.ts +521 -0
  512. package/src/goals/state.ts +37 -0
  513. package/src/goals/tools/goal-tool.ts +251 -0
  514. package/src/hindsight/backend.ts +354 -0
  515. package/src/hindsight/bank.ts +156 -0
  516. package/src/hindsight/client.ts +680 -0
  517. package/src/hindsight/config.ts +193 -0
  518. package/src/hindsight/content.ts +266 -0
  519. package/src/hindsight/index.ts +8 -0
  520. package/src/hindsight/mental-models.ts +429 -0
  521. package/src/hindsight/seeds.json +32 -0
  522. package/src/hindsight/state.ts +550 -0
  523. package/src/hindsight/transcript.ts +71 -0
  524. package/src/index.ts +66 -0
  525. package/src/internal-urls/agent-protocol.ts +180 -0
  526. package/src/internal-urls/artifact-protocol.ts +151 -0
  527. package/src/internal-urls/docs-index.ts +102 -0
  528. package/src/internal-urls/filesystem-resource.ts +34 -0
  529. package/src/internal-urls/history-protocol.ts +198 -0
  530. package/src/internal-urls/index.ts +27 -0
  531. package/src/internal-urls/issue-pr-protocol.ts +594 -0
  532. package/src/internal-urls/json-query.ts +126 -0
  533. package/src/internal-urls/local-protocol.ts +470 -0
  534. package/src/internal-urls/mcp-protocol.ts +165 -0
  535. package/src/internal-urls/memory-protocol.ts +347 -0
  536. package/src/internal-urls/omp-protocol.ts +94 -0
  537. package/src/internal-urls/parse.ts +103 -0
  538. package/src/internal-urls/registry-helpers.ts +132 -0
  539. package/src/internal-urls/router.ts +150 -0
  540. package/src/internal-urls/rule-protocol.ts +45 -0
  541. package/src/internal-urls/skill-protocol.ts +117 -0
  542. package/src/internal-urls/ssh-protocol.ts +368 -0
  543. package/src/internal-urls/types.ts +196 -0
  544. package/src/internal-urls/vault-protocol.ts +940 -0
  545. package/src/internal-urls/xd-protocol.ts +46 -0
  546. package/src/irc/bus.ts +380 -0
  547. package/src/jsonrpc/message-framing.ts +142 -0
  548. package/src/launch/broker.ts +1134 -0
  549. package/src/launch/client.ts +350 -0
  550. package/src/launch/paths.ts +17 -0
  551. package/src/launch/presence.ts +82 -0
  552. package/src/launch/protocol.ts +396 -0
  553. package/src/launch/spawn-options.ts +17 -0
  554. package/src/launch/terminal-output.ts +46 -0
  555. package/src/lib/xai-http.ts +150 -0
  556. package/src/live/attestation.ts +91 -0
  557. package/src/live/controller.ts +517 -0
  558. package/src/live/prompts/agent-final-message.md +3 -0
  559. package/src/live/prompts/live-instructions.md +23 -0
  560. package/src/live/protocol.ts +233 -0
  561. package/src/live/transport.ts +422 -0
  562. package/src/live/visualizer.ts +214 -0
  563. package/src/live/voices.ts +18 -0
  564. package/src/lsp/client.ts +1429 -0
  565. package/src/lsp/clients/biome-client.ts +263 -0
  566. package/src/lsp/clients/index.ts +50 -0
  567. package/src/lsp/clients/lsp-linter-client.ts +85 -0
  568. package/src/lsp/clients/swiftlint-client.ts +120 -0
  569. package/src/lsp/config.ts +549 -0
  570. package/src/lsp/defaults.json +499 -0
  571. package/src/lsp/deferred-diagnostics.ts +66 -0
  572. package/src/lsp/diagnostics-ledger.ts +51 -0
  573. package/src/lsp/edits.ts +288 -0
  574. package/src/lsp/format-options.ts +119 -0
  575. package/src/lsp/index.ts +2766 -0
  576. package/src/lsp/lspmux.ts +233 -0
  577. package/src/lsp/render.ts +668 -0
  578. package/src/lsp/startup-events.ts +13 -0
  579. package/src/lsp/types.ts +453 -0
  580. package/src/lsp/utils.ts +718 -0
  581. package/src/main.ts +1647 -0
  582. package/src/markit/NOTICE +32 -0
  583. package/src/markit/converters/docx.ts +56 -0
  584. package/src/markit/converters/epub.ts +136 -0
  585. package/src/markit/converters/mammoth.d.ts +24 -0
  586. package/src/markit/converters/pdf/columns.ts +103 -0
  587. package/src/markit/converters/pdf/extract.ts +598 -0
  588. package/src/markit/converters/pdf/grid.ts +780 -0
  589. package/src/markit/converters/pdf/headers.ts +106 -0
  590. package/src/markit/converters/pdf/index.ts +146 -0
  591. package/src/markit/converters/pdf/render.ts +501 -0
  592. package/src/markit/converters/pdf/types.ts +84 -0
  593. package/src/markit/converters/pptx.ts +325 -0
  594. package/src/markit/converters/xlsx.ts +173 -0
  595. package/src/markit/index.ts +2 -0
  596. package/src/markit/registry.ts +59 -0
  597. package/src/markit/types.ts +35 -0
  598. package/src/mcp/client.ts +511 -0
  599. package/src/mcp/config-writer.ts +377 -0
  600. package/src/mcp/config.ts +381 -0
  601. package/src/mcp/index.ts +29 -0
  602. package/src/mcp/json-rpc.ts +122 -0
  603. package/src/mcp/loader.ts +125 -0
  604. package/src/mcp/manager.ts +1419 -0
  605. package/src/mcp/oauth-credentials.ts +104 -0
  606. package/src/mcp/oauth-discovery.ts +587 -0
  607. package/src/mcp/oauth-flow.ts +820 -0
  608. package/src/mcp/render.ts +214 -0
  609. package/src/mcp/smithery-auth.ts +108 -0
  610. package/src/mcp/smithery-connect.ts +154 -0
  611. package/src/mcp/smithery-registry.ts +500 -0
  612. package/src/mcp/startup-events.ts +116 -0
  613. package/src/mcp/timeout.ts +59 -0
  614. package/src/mcp/tool-bridge.ts +691 -0
  615. package/src/mcp/tool-cache.ts +117 -0
  616. package/src/mcp/transports/http.ts +523 -0
  617. package/src/mcp/transports/index.ts +7 -0
  618. package/src/mcp/transports/sse.ts +377 -0
  619. package/src/mcp/transports/stdio.ts +903 -0
  620. package/src/mcp/types.ts +437 -0
  621. package/src/memories/index.ts +1434 -0
  622. package/src/memories/storage.ts +578 -0
  623. package/src/memory-backend/index.ts +18 -0
  624. package/src/memory-backend/local-backend.ts +47 -0
  625. package/src/memory-backend/off-backend.ts +25 -0
  626. package/src/memory-backend/resolve.ts +25 -0
  627. package/src/memory-backend/runtime.ts +66 -0
  628. package/src/memory-backend/tool-names.ts +2 -0
  629. package/src/memory-backend/types.ts +166 -0
  630. package/src/mnemopi/backend.ts +629 -0
  631. package/src/mnemopi/config.ts +267 -0
  632. package/src/mnemopi/embed-client.ts +246 -0
  633. package/src/mnemopi/embed-protocol.ts +35 -0
  634. package/src/mnemopi/embed-worker.ts +114 -0
  635. package/src/mnemopi/index.ts +3 -0
  636. package/src/mnemopi/state.ts +906 -0
  637. package/src/modes/acp/acp-agent.ts +2577 -0
  638. package/src/modes/acp/acp-client-bridge.ts +154 -0
  639. package/src/modes/acp/acp-event-mapper.ts +1084 -0
  640. package/src/modes/acp/acp-mode.ts +48 -0
  641. package/src/modes/acp/index.ts +2 -0
  642. package/src/modes/acp/terminal-auth.ts +37 -0
  643. package/src/modes/components/advisor-config.ts +635 -0
  644. package/src/modes/components/advisor-message.ts +109 -0
  645. package/src/modes/components/agent-dashboard.ts +1252 -0
  646. package/src/modes/components/agent-hub.ts +648 -0
  647. package/src/modes/components/agent-transcript-viewer.ts +649 -0
  648. package/src/modes/components/ask-dialog.ts +975 -0
  649. package/src/modes/components/assistant-message.ts +972 -0
  650. package/src/modes/components/background-tan-message.ts +36 -0
  651. package/src/modes/components/bash-execution.ts +233 -0
  652. package/src/modes/components/bordered-loader.ts +41 -0
  653. package/src/modes/components/btw-panel.ts +124 -0
  654. package/src/modes/components/cache-invalidation-marker.ts +110 -0
  655. package/src/modes/components/chat-block.ts +111 -0
  656. package/src/modes/components/chat-transcript-builder.ts +495 -0
  657. package/src/modes/components/collab-prompt-message.ts +32 -0
  658. package/src/modes/components/compaction-summary-message.ts +221 -0
  659. package/src/modes/components/copy-selector.ts +218 -0
  660. package/src/modes/components/countdown-timer.ts +75 -0
  661. package/src/modes/components/custom-editor.ts +1027 -0
  662. package/src/modes/components/custom-message.ts +70 -0
  663. package/src/modes/components/diff.ts +254 -0
  664. package/src/modes/components/dynamic-border.ts +37 -0
  665. package/src/modes/components/error-banner.ts +33 -0
  666. package/src/modes/components/eval-execution.ts +169 -0
  667. package/src/modes/components/execution-shared.ts +101 -0
  668. package/src/modes/components/extensions/extension-dashboard.ts +492 -0
  669. package/src/modes/components/extensions/extension-list.ts +507 -0
  670. package/src/modes/components/extensions/index.ts +9 -0
  671. package/src/modes/components/extensions/inspector-panel.ts +321 -0
  672. package/src/modes/components/extensions/state-manager.ts +648 -0
  673. package/src/modes/components/extensions/types.ts +186 -0
  674. package/src/modes/components/footer.ts +276 -0
  675. package/src/modes/components/history-search.ts +268 -0
  676. package/src/modes/components/hook-editor.ts +213 -0
  677. package/src/modes/components/hook-input.ts +87 -0
  678. package/src/modes/components/hook-message.ts +67 -0
  679. package/src/modes/components/hook-selector.ts +691 -0
  680. package/src/modes/components/index.ts +42 -0
  681. package/src/modes/components/keybinding-hints.ts +65 -0
  682. package/src/modes/components/late-diagnostics-message.ts +60 -0
  683. package/src/modes/components/login-dialog.ts +197 -0
  684. package/src/modes/components/logout-account-selector.ts +130 -0
  685. package/src/modes/components/mcp-add-wizard.ts +1413 -0
  686. package/src/modes/components/message-frame.ts +98 -0
  687. package/src/modes/components/model-browser.ts +872 -0
  688. package/src/modes/components/model-hub.ts +2011 -0
  689. package/src/modes/components/model-picker.ts +233 -0
  690. package/src/modes/components/move-overlay.ts +293 -0
  691. package/src/modes/components/oauth-selector.ts +474 -0
  692. package/src/modes/components/omfg-panel.ts +141 -0
  693. package/src/modes/components/overlay-box.ts +109 -0
  694. package/src/modes/components/pause-screen.ts +208 -0
  695. package/src/modes/components/plan-review-overlay.ts +1226 -0
  696. package/src/modes/components/plan-toc.ts +138 -0
  697. package/src/modes/components/plugin-selector.ts +100 -0
  698. package/src/modes/components/plugin-settings.ts +745 -0
  699. package/src/modes/components/queue-mode-selector.ts +61 -0
  700. package/src/modes/components/read-tool-group.ts +678 -0
  701. package/src/modes/components/reset-usage-selector.ts +161 -0
  702. package/src/modes/components/segment-track.ts +89 -0
  703. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  704. package/src/modes/components/selector-helpers.ts +129 -0
  705. package/src/modes/components/session-account-selector.ts +62 -0
  706. package/src/modes/components/session-selector.ts +1019 -0
  707. package/src/modes/components/settings-defs.ts +267 -0
  708. package/src/modes/components/settings-selector.ts +1445 -0
  709. package/src/modes/components/show-images-selector.ts +50 -0
  710. package/src/modes/components/skill-message.ts +110 -0
  711. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -0
  712. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  713. package/src/modes/components/status-line/component.ts +1516 -0
  714. package/src/modes/components/status-line/context-thresholds.ts +86 -0
  715. package/src/modes/components/status-line/git-utils.ts +42 -0
  716. package/src/modes/components/status-line/index.ts +5 -0
  717. package/src/modes/components/status-line/presets.ts +106 -0
  718. package/src/modes/components/status-line/segments.ts +710 -0
  719. package/src/modes/components/status-line/separators.ts +55 -0
  720. package/src/modes/components/status-line/types.ts +159 -0
  721. package/src/modes/components/theme-selector.ts +68 -0
  722. package/src/modes/components/thinking-selector.ts +57 -0
  723. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  724. package/src/modes/components/tips.txt +26 -0
  725. package/src/modes/components/todo-reminder.ts +43 -0
  726. package/src/modes/components/tool-execution.ts +1356 -0
  727. package/src/modes/components/transcript-container.ts +527 -0
  728. package/src/modes/components/tree-selector.ts +1008 -0
  729. package/src/modes/components/ttsr-notification.ts +123 -0
  730. package/src/modes/components/usage-row.ts +47 -0
  731. package/src/modes/components/user-message-selector.ts +227 -0
  732. package/src/modes/components/user-message.ts +167 -0
  733. package/src/modes/components/visual-truncate.ts +63 -0
  734. package/src/modes/components/welcome.ts +578 -0
  735. package/src/modes/controllers/btw-controller.ts +198 -0
  736. package/src/modes/controllers/command-controller-shared.ts +109 -0
  737. package/src/modes/controllers/command-controller.ts +1993 -0
  738. package/src/modes/controllers/event-controller.ts +1781 -0
  739. package/src/modes/controllers/extension-ui-controller.ts +1242 -0
  740. package/src/modes/controllers/input-controller.ts +2004 -0
  741. package/src/modes/controllers/live-command-controller.ts +260 -0
  742. package/src/modes/controllers/mcp-command-controller.ts +2403 -0
  743. package/src/modes/controllers/omfg-controller.ts +287 -0
  744. package/src/modes/controllers/omfg-rule.ts +647 -0
  745. package/src/modes/controllers/selector-controller.ts +1968 -0
  746. package/src/modes/controllers/session-focus-controller.ts +117 -0
  747. package/src/modes/controllers/ssh-command-controller.ts +385 -0
  748. package/src/modes/controllers/streaming-reveal.ts +399 -0
  749. package/src/modes/controllers/tan-command-controller.ts +229 -0
  750. package/src/modes/controllers/todo-command-controller.ts +487 -0
  751. package/src/modes/controllers/tool-args-reveal.ts +591 -0
  752. package/src/modes/data/emojis.json +1 -0
  753. package/src/modes/emoji-autocomplete.ts +285 -0
  754. package/src/modes/github-ref-autocomplete.ts +75 -0
  755. package/src/modes/gradient-highlight.ts +99 -0
  756. package/src/modes/image-references.ts +137 -0
  757. package/src/modes/index.ts +10 -0
  758. package/src/modes/interactive-mode.ts +4957 -0
  759. package/src/modes/internal-url-autocomplete.ts +158 -0
  760. package/src/modes/loop-limit.ts +192 -0
  761. package/src/modes/magic-keyword-boundary.ts +23 -0
  762. package/src/modes/magic-keywords.ts +42 -0
  763. package/src/modes/markdown-prose.ts +247 -0
  764. package/src/modes/oauth-manual-input.ts +69 -0
  765. package/src/modes/orchestrate.ts +43 -0
  766. package/src/modes/print-mode.ts +265 -0
  767. package/src/modes/prompt-action-autocomplete.ts +322 -0
  768. package/src/modes/queue-input.ts +132 -0
  769. package/src/modes/rpc/host-tools.ts +204 -0
  770. package/src/modes/rpc/host-uris.ts +235 -0
  771. package/src/modes/rpc/rpc-client.ts +1195 -0
  772. package/src/modes/rpc/rpc-frame.ts +316 -0
  773. package/src/modes/rpc/rpc-input.ts +38 -0
  774. package/src/modes/rpc/rpc-messages.ts +127 -0
  775. package/src/modes/rpc/rpc-mode.ts +1504 -0
  776. package/src/modes/rpc/rpc-subagents.ts +265 -0
  777. package/src/modes/rpc/rpc-types.ts +533 -0
  778. package/src/modes/running-subagent-badge.ts +13 -0
  779. package/src/modes/runtime-init.ts +144 -0
  780. package/src/modes/session-observer-registry.ts +218 -0
  781. package/src/modes/session-teardown.ts +82 -0
  782. package/src/modes/setup-version.ts +11 -0
  783. package/src/modes/setup-wizard/index.ts +103 -0
  784. package/src/modes/setup-wizard/lazy.ts +16 -0
  785. package/src/modes/setup-wizard/scenes/glyph.ts +103 -0
  786. package/src/modes/setup-wizard/scenes/model.ts +132 -0
  787. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  788. package/src/modes/setup-wizard/scenes/providers.ts +105 -0
  789. package/src/modes/setup-wizard/scenes/sign-in.ts +312 -0
  790. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  791. package/src/modes/setup-wizard/scenes/theme.ts +330 -0
  792. package/src/modes/setup-wizard/scenes/types.ts +65 -0
  793. package/src/modes/setup-wizard/scenes/web-search.ts +153 -0
  794. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  795. package/src/modes/setup-wizard/wizard-overlay.ts +335 -0
  796. package/src/modes/shared.ts +49 -0
  797. package/src/modes/skill-command.ts +91 -0
  798. package/src/modes/theme/dark.json +95 -0
  799. package/src/modes/theme/defaults/alabaster.json +93 -0
  800. package/src/modes/theme/defaults/amethyst.json +96 -0
  801. package/src/modes/theme/defaults/anthracite.json +93 -0
  802. package/src/modes/theme/defaults/basalt.json +91 -0
  803. package/src/modes/theme/defaults/birch.json +95 -0
  804. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  805. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  806. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  807. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  808. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  809. package/src/modes/theme/defaults/dark-copper.json +95 -0
  810. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  811. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  812. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  813. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  814. package/src/modes/theme/defaults/dark-ember.json +95 -0
  815. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  816. package/src/modes/theme/defaults/dark-forest.json +96 -0
  817. package/src/modes/theme/defaults/dark-github.json +105 -0
  818. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  819. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  820. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  821. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  822. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  823. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  824. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  825. package/src/modes/theme/defaults/dark-nord.json +97 -0
  826. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  827. package/src/modes/theme/defaults/dark-one.json +100 -0
  828. package/src/modes/theme/defaults/dark-poimandres.json +143 -0
  829. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  830. package/src/modes/theme/defaults/dark-reef.json +91 -0
  831. package/src/modes/theme/defaults/dark-retro.json +92 -0
  832. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  833. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  834. package/src/modes/theme/defaults/dark-slate.json +95 -0
  835. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  836. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  837. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  838. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  839. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  840. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  841. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  842. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  843. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  844. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  845. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  846. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  847. package/src/modes/theme/defaults/graphite.json +92 -0
  848. package/src/modes/theme/defaults/index.ts +199 -0
  849. package/src/modes/theme/defaults/light-arctic.json +107 -0
  850. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  851. package/src/modes/theme/defaults/light-canyon.json +91 -0
  852. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  853. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  854. package/src/modes/theme/defaults/light-coral.json +95 -0
  855. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  856. package/src/modes/theme/defaults/light-dawn.json +90 -0
  857. package/src/modes/theme/defaults/light-dunes.json +91 -0
  858. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  859. package/src/modes/theme/defaults/light-forest.json +100 -0
  860. package/src/modes/theme/defaults/light-frost.json +95 -0
  861. package/src/modes/theme/defaults/light-github.json +115 -0
  862. package/src/modes/theme/defaults/light-glacier.json +91 -0
  863. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  864. package/src/modes/theme/defaults/light-haze.json +90 -0
  865. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  866. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  867. package/src/modes/theme/defaults/light-lavender.json +95 -0
  868. package/src/modes/theme/defaults/light-meadow.json +91 -0
  869. package/src/modes/theme/defaults/light-mint.json +95 -0
  870. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  871. package/src/modes/theme/defaults/light-ocean.json +99 -0
  872. package/src/modes/theme/defaults/light-one.json +99 -0
  873. package/src/modes/theme/defaults/light-opal.json +91 -0
  874. package/src/modes/theme/defaults/light-orchard.json +91 -0
  875. package/src/modes/theme/defaults/light-paper.json +95 -0
  876. package/src/modes/theme/defaults/light-poimandres.json +143 -0
  877. package/src/modes/theme/defaults/light-prism.json +90 -0
  878. package/src/modes/theme/defaults/light-retro.json +98 -0
  879. package/src/modes/theme/defaults/light-sand.json +95 -0
  880. package/src/modes/theme/defaults/light-savanna.json +91 -0
  881. package/src/modes/theme/defaults/light-solarized.json +102 -0
  882. package/src/modes/theme/defaults/light-soleil.json +90 -0
  883. package/src/modes/theme/defaults/light-sunset.json +99 -0
  884. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  885. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  886. package/src/modes/theme/defaults/light-wetland.json +91 -0
  887. package/src/modes/theme/defaults/light-zenith.json +89 -0
  888. package/src/modes/theme/defaults/limestone.json +94 -0
  889. package/src/modes/theme/defaults/mahogany.json +97 -0
  890. package/src/modes/theme/defaults/marble.json +93 -0
  891. package/src/modes/theme/defaults/obsidian.json +91 -0
  892. package/src/modes/theme/defaults/onyx.json +91 -0
  893. package/src/modes/theme/defaults/pearl.json +93 -0
  894. package/src/modes/theme/defaults/porcelain.json +91 -0
  895. package/src/modes/theme/defaults/quartz.json +96 -0
  896. package/src/modes/theme/defaults/sandstone.json +95 -0
  897. package/src/modes/theme/defaults/titanium.json +90 -0
  898. package/src/modes/theme/light.json +93 -0
  899. package/src/modes/theme/mermaid-cache.ts +92 -0
  900. package/src/modes/theme/shimmer.ts +305 -0
  901. package/src/modes/theme/theme-schema.json +463 -0
  902. package/src/modes/theme/theme.ts +3019 -0
  903. package/src/modes/turn-budget.ts +31 -0
  904. package/src/modes/types.ts +479 -0
  905. package/src/modes/ultrathink.ts +42 -0
  906. package/src/modes/utils/context-usage.ts +518 -0
  907. package/src/modes/utils/copy-targets.ts +378 -0
  908. package/src/modes/utils/hotkeys-markdown.ts +63 -0
  909. package/src/modes/utils/interactive-context-helpers.ts +30 -0
  910. package/src/modes/utils/keybinding-matchers.ts +86 -0
  911. package/src/modes/utils/tools-markdown.ts +31 -0
  912. package/src/modes/utils/transcript-render-helpers.ts +253 -0
  913. package/src/modes/utils/ui-helpers.ts +963 -0
  914. package/src/modes/warp-events.ts +232 -0
  915. package/src/modes/workflow.ts +49 -0
  916. package/src/plan-mode/approved-plan.ts +194 -0
  917. package/src/plan-mode/model-transition.ts +51 -0
  918. package/src/plan-mode/plan-files.ts +40 -0
  919. package/src/plan-mode/plan-handoff.ts +37 -0
  920. package/src/plan-mode/plan-protection.ts +31 -0
  921. package/src/plan-mode/state.ts +6 -0
  922. package/src/priority.json +60 -0
  923. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  924. package/src/prompts/advisor/advise-tool.md +3 -0
  925. package/src/prompts/advisor/context-files.md +8 -0
  926. package/src/prompts/advisor/system.md +98 -0
  927. package/src/prompts/agents/designer.md +74 -0
  928. package/src/prompts/agents/frontmatter.md +12 -0
  929. package/src/prompts/agents/init.md +33 -0
  930. package/src/prompts/agents/librarian.md +119 -0
  931. package/src/prompts/agents/reviewer.md +139 -0
  932. package/src/prompts/agents/scout.md +58 -0
  933. package/src/prompts/agents/task.md +17 -0
  934. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  935. package/src/prompts/bench/cache-prefix.md +3 -0
  936. package/src/prompts/bench/cache-suffix.md +1 -0
  937. package/src/prompts/bench.md +6 -0
  938. package/src/prompts/ci-green-request.md +36 -0
  939. package/src/prompts/dry-balance-bench.md +8 -0
  940. package/src/prompts/goals/goal-budget-limit.md +16 -0
  941. package/src/prompts/goals/goal-continuation.md +28 -0
  942. package/src/prompts/goals/goal-mode-active.md +23 -0
  943. package/src/prompts/goals/goal-mode-context.md +4 -0
  944. package/src/prompts/goals/goal-todo-context.md +12 -0
  945. package/src/prompts/goals/guided-goal-interview.md +43 -0
  946. package/src/prompts/memories/consolidation.md +30 -0
  947. package/src/prompts/memories/consolidation_system.md +4 -0
  948. package/src/prompts/memories/read-path.md +17 -0
  949. package/src/prompts/memories/stage_one_input.md +6 -0
  950. package/src/prompts/memories/stage_one_system.md +21 -0
  951. package/src/prompts/review-custom-request.md +21 -0
  952. package/src/prompts/review-headless-request.md +16 -0
  953. package/src/prompts/review-request.md +68 -0
  954. package/src/prompts/skills/autoload.md +8 -0
  955. package/src/prompts/skills/user-invocation.md +11 -0
  956. package/src/prompts/steering/parent-irc.md +5 -0
  957. package/src/prompts/steering/user-interjection.md +6 -0
  958. package/src/prompts/system/active-repo-context.md +4 -0
  959. package/src/prompts/system/agent-creation-architect.md +50 -0
  960. package/src/prompts/system/agent-creation-user.md +6 -0
  961. package/src/prompts/system/auto-continue.md +1 -0
  962. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  963. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  964. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  965. package/src/prompts/system/autolearn-guidance.md +7 -0
  966. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  967. package/src/prompts/system/background-tan-dispatch.md +8 -0
  968. package/src/prompts/system/btw-user.md +8 -0
  969. package/src/prompts/system/commit-message-system.md +14 -0
  970. package/src/prompts/system/computer-safety.md +14 -0
  971. package/src/prompts/system/custom-system-prompt.md +64 -0
  972. package/src/prompts/system/eager-task.md +7 -0
  973. package/src/prompts/system/eager-todo.md +18 -0
  974. package/src/prompts/system/empty-stop-retry.md +4 -0
  975. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  976. package/src/prompts/system/interrupted-thinking.md +7 -0
  977. package/src/prompts/system/irc-autoreply.md +6 -0
  978. package/src/prompts/system/irc-incoming.md +9 -0
  979. package/src/prompts/system/manual-continue.md +7 -0
  980. package/src/prompts/system/mcp-xdev-guidance.md +11 -0
  981. package/src/prompts/system/memory-consolidation-system.md +8 -0
  982. package/src/prompts/system/memory-extraction-system.md +26 -0
  983. package/src/prompts/system/mid-run-todo-nudge.md +3 -0
  984. package/src/prompts/system/omfg-user.md +50 -0
  985. package/src/prompts/system/orchestrate-notice.md +40 -0
  986. package/src/prompts/system/personalities/default.md +18 -0
  987. package/src/prompts/system/personalities/friendly.md +17 -0
  988. package/src/prompts/system/personalities/pragmatic.md +15 -0
  989. package/src/prompts/system/plan-mode-active.md +125 -0
  990. package/src/prompts/system/plan-mode-approved.md +22 -0
  991. package/src/prompts/system/plan-mode-compact-instructions.md +17 -0
  992. package/src/prompts/system/plan-mode-reference.md +10 -0
  993. package/src/prompts/system/plan-mode-subagent.md +33 -0
  994. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  995. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  996. package/src/prompts/system/prewalk-checklist.md +7 -0
  997. package/src/prompts/system/prewalk-continue.md +1 -0
  998. package/src/prompts/system/prewalk-plan.md +13 -0
  999. package/src/prompts/system/project-prompt.md +61 -0
  1000. package/src/prompts/system/recap-user.md +9 -0
  1001. package/src/prompts/system/resolve-device-reminder.md +3 -0
  1002. package/src/prompts/system/rewind-report.md +6 -0
  1003. package/src/prompts/system/side-channel-no-tools.md +3 -0
  1004. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  1005. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  1006. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  1007. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  1008. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  1009. package/src/prompts/system/speech-rewrite.md +15 -0
  1010. package/src/prompts/system/subagent-async-pending.md +6 -0
  1011. package/src/prompts/system/subagent-system-prompt.md +73 -0
  1012. package/src/prompts/system/subagent-user-prompt.md +3 -0
  1013. package/src/prompts/system/subagent-yield-reminder.md +23 -0
  1014. package/src/prompts/system/system-prompt.md +283 -0
  1015. package/src/prompts/system/tan-context-switch.md +17 -0
  1016. package/src/prompts/system/task-label.md +23 -0
  1017. package/src/prompts/system/thinking-loop-redirect.md +10 -0
  1018. package/src/prompts/system/title-marker-instruction.md +1 -0
  1019. package/src/prompts/system/title-system.md +16 -0
  1020. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  1021. package/src/prompts/system/ttsr-interrupt.md +7 -0
  1022. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  1023. package/src/prompts/system/ultrathink-notice.md +3 -0
  1024. package/src/prompts/system/unexpected-stop-classifier.md +17 -0
  1025. package/src/prompts/system/unexpected-stop-retry.md +4 -0
  1026. package/src/prompts/system/vibe-mode-active.md +26 -0
  1027. package/src/prompts/system/web-search.md +25 -0
  1028. package/src/prompts/system/workflow-notice.md +112 -0
  1029. package/src/prompts/system/xdev-mount-notice.md +20 -0
  1030. package/src/prompts/tools/apply-patch.md +65 -0
  1031. package/src/prompts/tools/ask.md +22 -0
  1032. package/src/prompts/tools/ast-edit.md +11 -0
  1033. package/src/prompts/tools/ast-grep.md +19 -0
  1034. package/src/prompts/tools/async-result.md +8 -0
  1035. package/src/prompts/tools/bash.md +25 -0
  1036. package/src/prompts/tools/browser.md +28 -0
  1037. package/src/prompts/tools/checkpoint.md +15 -0
  1038. package/src/prompts/tools/computer.md +26 -0
  1039. package/src/prompts/tools/debug.md +3 -0
  1040. package/src/prompts/tools/eval.md +45 -0
  1041. package/src/prompts/tools/github.md +22 -0
  1042. package/src/prompts/tools/glob.md +15 -0
  1043. package/src/prompts/tools/goal.md +11 -0
  1044. package/src/prompts/tools/grep.md +12 -0
  1045. package/src/prompts/tools/hub.md +34 -0
  1046. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  1047. package/src/prompts/tools/image-attachment-describe.md +10 -0
  1048. package/src/prompts/tools/image-gen.md +7 -0
  1049. package/src/prompts/tools/inspect-image-system.md +20 -0
  1050. package/src/prompts/tools/inspect-image.md +22 -0
  1051. package/src/prompts/tools/learn.md +7 -0
  1052. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  1053. package/src/prompts/tools/lsp.md +19 -0
  1054. package/src/prompts/tools/manage-skill.md +9 -0
  1055. package/src/prompts/tools/memory-edit.md +12 -0
  1056. package/src/prompts/tools/patch.md +57 -0
  1057. package/src/prompts/tools/read.md +27 -0
  1058. package/src/prompts/tools/recall.md +7 -0
  1059. package/src/prompts/tools/reflect.md +5 -0
  1060. package/src/prompts/tools/replace.md +29 -0
  1061. package/src/prompts/tools/retain.md +6 -0
  1062. package/src/prompts/tools/rewind.md +14 -0
  1063. package/src/prompts/tools/task-async-contract.md +1 -0
  1064. package/src/prompts/tools/task-summary.md +20 -0
  1065. package/src/prompts/tools/task.md +83 -0
  1066. package/src/prompts/tools/todo.md +42 -0
  1067. package/src/prompts/tools/vibe-kill.md +3 -0
  1068. package/src/prompts/tools/vibe-list.md +3 -0
  1069. package/src/prompts/tools/vibe-send.md +9 -0
  1070. package/src/prompts/tools/vibe-spawn.md +10 -0
  1071. package/src/prompts/tools/vibe-turn-result.md +19 -0
  1072. package/src/prompts/tools/vibe-wait.md +8 -0
  1073. package/src/prompts/tools/web-search.md +8 -0
  1074. package/src/prompts/tools/write.md +14 -0
  1075. package/src/registry/agent-lifecycle.ts +455 -0
  1076. package/src/registry/agent-registry.ts +217 -0
  1077. package/src/registry/persisted-agents.ts +98 -0
  1078. package/src/sdk.ts +3553 -0
  1079. package/src/secrets/index.ts +246 -0
  1080. package/src/secrets/obfuscator.ts +2574 -0
  1081. package/src/secrets/regex.ts +21 -0
  1082. package/src/session/acp-permission-gate.ts +165 -0
  1083. package/src/session/agent-session-events.ts +66 -0
  1084. package/src/session/agent-session-types.ts +346 -0
  1085. package/src/session/agent-session.ts +8406 -0
  1086. package/src/session/agent-storage.ts +805 -0
  1087. package/src/session/artifacts.ts +154 -0
  1088. package/src/session/async-job-delivery.ts +82 -0
  1089. package/src/session/auth-broker-config.ts +92 -0
  1090. package/src/session/auth-storage.ts +25 -0
  1091. package/src/session/bash-runner.ts +326 -0
  1092. package/src/session/blob-store.ts +295 -0
  1093. package/src/session/checkpoint-entries.ts +81 -0
  1094. package/src/session/client-bridge.ts +85 -0
  1095. package/src/session/codex-auto-reset.ts +202 -0
  1096. package/src/session/compact-modes.ts +105 -0
  1097. package/src/session/eval-runner.ts +217 -0
  1098. package/src/session/exit-diagnostics.ts +310 -0
  1099. package/src/session/history-storage.ts +327 -0
  1100. package/src/session/indexed-session-storage.ts +544 -0
  1101. package/src/session/irc-bridge.ts +203 -0
  1102. package/src/session/messages.ts +1280 -0
  1103. package/src/session/model-controls.ts +750 -0
  1104. package/src/session/prewalk.ts +257 -0
  1105. package/src/session/provider-image-budget.ts +86 -0
  1106. package/src/session/queued-messages.ts +99 -0
  1107. package/src/session/redis-session-storage.ts +257 -0
  1108. package/src/session/retry-fallback-chains.ts +455 -0
  1109. package/src/session/role-models.ts +85 -0
  1110. package/src/session/session-advisors.ts +1754 -0
  1111. package/src/session/session-context.ts +554 -0
  1112. package/src/session/session-dump-format.ts +216 -0
  1113. package/src/session/session-entries.ts +260 -0
  1114. package/src/session/session-handoff.ts +308 -0
  1115. package/src/session/session-history-format.ts +459 -0
  1116. package/src/session/session-listing.ts +715 -0
  1117. package/src/session/session-loader.ts +280 -0
  1118. package/src/session/session-maintenance.ts +2983 -0
  1119. package/src/session/session-manager.ts +2557 -0
  1120. package/src/session/session-memory.ts +222 -0
  1121. package/src/session/session-metadata.ts +53 -0
  1122. package/src/session/session-migrations.ts +78 -0
  1123. package/src/session/session-paths.ts +222 -0
  1124. package/src/session/session-persistence.ts +293 -0
  1125. package/src/session/session-provider-boundary.ts +306 -0
  1126. package/src/session/session-stats.ts +293 -0
  1127. package/src/session/session-storage.ts +734 -0
  1128. package/src/session/session-title-slot.ts +141 -0
  1129. package/src/session/session-tools.ts +1226 -0
  1130. package/src/session/session-workspace.ts +53 -0
  1131. package/src/session/settings-stream-fn.ts +74 -0
  1132. package/src/session/shake-types.ts +43 -0
  1133. package/src/session/snapcompact-inline.ts +545 -0
  1134. package/src/session/snapcompact-savings-journal.ts +113 -0
  1135. package/src/session/sql-session-storage.ts +374 -0
  1136. package/src/session/stream-guards.ts +417 -0
  1137. package/src/session/streaming-output.ts +1459 -0
  1138. package/src/session/todo-tracker.ts +380 -0
  1139. package/src/session/tool-choice-queue.ts +305 -0
  1140. package/src/session/ttsr-coordinator.ts +496 -0
  1141. package/src/session/turn-persistence.ts +142 -0
  1142. package/src/session/turn-recovery.ts +1728 -0
  1143. package/src/session/unexpected-stop-classifier.ts +132 -0
  1144. package/src/session/yield-queue.ts +183 -0
  1145. package/src/slash-commands/acp-builtins.ts +70 -0
  1146. package/src/slash-commands/available-commands.ts +105 -0
  1147. package/src/slash-commands/builtin-registry.ts +2982 -0
  1148. package/src/slash-commands/helpers/active-oauth-account.ts +80 -0
  1149. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  1150. package/src/slash-commands/helpers/context-report.ts +66 -0
  1151. package/src/slash-commands/helpers/format.ts +46 -0
  1152. package/src/slash-commands/helpers/logout.ts +108 -0
  1153. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  1154. package/src/slash-commands/helpers/mcp.ts +533 -0
  1155. package/src/slash-commands/helpers/parse.ts +85 -0
  1156. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  1157. package/src/slash-commands/helpers/session-pin.ts +44 -0
  1158. package/src/slash-commands/helpers/ssh.ts +196 -0
  1159. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  1160. package/src/slash-commands/helpers/todo.ts +285 -0
  1161. package/src/slash-commands/helpers/usage-report.ts +198 -0
  1162. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  1163. package/src/slash-commands/types.ts +139 -0
  1164. package/src/ssh/config-writer.ts +183 -0
  1165. package/src/ssh/connection-manager.ts +667 -0
  1166. package/src/ssh/file-transfer.ts +214 -0
  1167. package/src/ssh/sshfs-mount.ts +163 -0
  1168. package/src/ssh/utils.ts +51 -0
  1169. package/src/startup-splash.ts +19 -0
  1170. package/src/stt/asr-client.ts +401 -0
  1171. package/src/stt/asr-protocol.ts +65 -0
  1172. package/src/stt/asr-worker.ts +603 -0
  1173. package/src/stt/downloader.ts +142 -0
  1174. package/src/stt/endpointer.ts +259 -0
  1175. package/src/stt/index.ts +6 -0
  1176. package/src/stt/models.ts +150 -0
  1177. package/src/stt/sherpa-runtime.ts +71 -0
  1178. package/src/stt/stt-controller.ts +320 -0
  1179. package/src/stt/submit-trigger.ts +74 -0
  1180. package/src/subprocess/worker-client.ts +463 -0
  1181. package/src/subprocess/worker-runtime.ts +494 -0
  1182. package/src/system-prompt.ts +894 -0
  1183. package/src/task/agents.ts +168 -0
  1184. package/src/task/commands.ts +132 -0
  1185. package/src/task/discovery.ts +145 -0
  1186. package/src/task/executor.ts +3129 -0
  1187. package/src/task/index.ts +1519 -0
  1188. package/src/task/isolation-ownership.ts +106 -0
  1189. package/src/task/isolation-runner.ts +413 -0
  1190. package/src/task/label.ts +40 -0
  1191. package/src/task/name-generator.ts +1577 -0
  1192. package/src/task/omp-command.ts +26 -0
  1193. package/src/task/output-manager.ts +115 -0
  1194. package/src/task/parallel.ts +221 -0
  1195. package/src/task/persisted-revive.ts +141 -0
  1196. package/src/task/prewalk.ts +6 -0
  1197. package/src/task/prompt-policy.ts +8 -0
  1198. package/src/task/provider-concurrency.ts +100 -0
  1199. package/src/task/render.ts +1814 -0
  1200. package/src/task/renderer.ts +14 -0
  1201. package/src/task/repair-args.ts +118 -0
  1202. package/src/task/spawn-policy.ts +58 -0
  1203. package/src/task/structured-subagent.ts +649 -0
  1204. package/src/task/subprocess-tool-registry.ts +88 -0
  1205. package/src/task/types.ts +551 -0
  1206. package/src/task/worktree.ts +967 -0
  1207. package/src/task/yield-assembly.ts +198 -0
  1208. package/src/telemetry-export.ts +500 -0
  1209. package/src/thinking.ts +364 -0
  1210. package/src/tiny/device.ts +111 -0
  1211. package/src/tiny/dtype.ts +101 -0
  1212. package/src/tiny/message-preproc.ts +155 -0
  1213. package/src/tiny/models.ts +268 -0
  1214. package/src/tiny/text.ts +278 -0
  1215. package/src/tiny/title-client.ts +460 -0
  1216. package/src/tiny/title-protocol.ts +56 -0
  1217. package/src/tiny/worker.ts +354 -0
  1218. package/src/tools/acp-bridge.ts +81 -0
  1219. package/src/tools/approval.ts +245 -0
  1220. package/src/tools/ask.ts +1459 -0
  1221. package/src/tools/ast-edit.ts +718 -0
  1222. package/src/tools/ast-grep.ts +520 -0
  1223. package/src/tools/auto-generated-guard.ts +335 -0
  1224. package/src/tools/bash-interactive.ts +435 -0
  1225. package/src/tools/bash-interceptor.ts +67 -0
  1226. package/src/tools/bash-pty-selection.ts +14 -0
  1227. package/src/tools/bash-skill-urls.ts +335 -0
  1228. package/src/tools/bash.ts +1762 -0
  1229. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  1230. package/src/tools/browser/aria/aria-snapshot.ts +131 -0
  1231. package/src/tools/browser/attach.ts +194 -0
  1232. package/src/tools/browser/cmux/cmux-tab.ts +1400 -0
  1233. package/src/tools/browser/cmux/rpc.ts +206 -0
  1234. package/src/tools/browser/cmux/socket-client.ts +445 -0
  1235. package/src/tools/browser/launch.ts +788 -0
  1236. package/src/tools/browser/readable.ts +112 -0
  1237. package/src/tools/browser/registry.ts +287 -0
  1238. package/src/tools/browser/render.ts +226 -0
  1239. package/src/tools/browser/run-cancellation.ts +131 -0
  1240. package/src/tools/browser/run-output.ts +76 -0
  1241. package/src/tools/browser/tab-protocol.ts +115 -0
  1242. package/src/tools/browser/tab-supervisor.ts +1040 -0
  1243. package/src/tools/browser/tab-worker-entry.ts +29 -0
  1244. package/src/tools/browser/tab-worker.ts +1842 -0
  1245. package/src/tools/browser.ts +457 -0
  1246. package/src/tools/builtin-names.ts +66 -0
  1247. package/src/tools/checkpoint.ts +150 -0
  1248. package/src/tools/computer/exposure.ts +38 -0
  1249. package/src/tools/computer/protocol.ts +28 -0
  1250. package/src/tools/computer/supervisor.ts +306 -0
  1251. package/src/tools/computer/worker-entry.ts +34 -0
  1252. package/src/tools/computer/worker.ts +131 -0
  1253. package/src/tools/computer-renderer.ts +108 -0
  1254. package/src/tools/computer.ts +516 -0
  1255. package/src/tools/conflict-detect.ts +815 -0
  1256. package/src/tools/context.ts +46 -0
  1257. package/src/tools/debug.ts +1121 -0
  1258. package/src/tools/default-renderer.ts +139 -0
  1259. package/src/tools/essential-tools.ts +46 -0
  1260. package/src/tools/eval-backends.ts +34 -0
  1261. package/src/tools/eval-format/index.ts +24 -0
  1262. package/src/tools/eval-format/javascript.ts +952 -0
  1263. package/src/tools/eval-format/julia.ts +446 -0
  1264. package/src/tools/eval-format/python.ts +544 -0
  1265. package/src/tools/eval-format/ruby.ts +380 -0
  1266. package/src/tools/eval-render.ts +784 -0
  1267. package/src/tools/eval.ts +774 -0
  1268. package/src/tools/fetch.ts +1889 -0
  1269. package/src/tools/file-recorder.ts +35 -0
  1270. package/src/tools/fs-cache-invalidation.ts +28 -0
  1271. package/src/tools/gh-cache-invalidation.ts +175 -0
  1272. package/src/tools/gh-format.ts +12 -0
  1273. package/src/tools/gh-renderer.ts +484 -0
  1274. package/src/tools/gh.ts +3959 -0
  1275. package/src/tools/github-cache.ts +663 -0
  1276. package/src/tools/glob.ts +682 -0
  1277. package/src/tools/grep.ts +1858 -0
  1278. package/src/tools/grouped-file-output.ts +210 -0
  1279. package/src/tools/hub/index.ts +579 -0
  1280. package/src/tools/hub/jobs.ts +714 -0
  1281. package/src/tools/hub/launch.ts +579 -0
  1282. package/src/tools/hub/messaging.ts +735 -0
  1283. package/src/tools/hub/types.ts +117 -0
  1284. package/src/tools/image-gen.ts +1689 -0
  1285. package/src/tools/image-providers.ts +50 -0
  1286. package/src/tools/index.ts +691 -0
  1287. package/src/tools/inspect-image-renderer.ts +133 -0
  1288. package/src/tools/inspect-image.ts +290 -0
  1289. package/src/tools/json-tree.ts +260 -0
  1290. package/src/tools/jtd-to-json-schema.ts +219 -0
  1291. package/src/tools/jtd-to-typescript.ts +136 -0
  1292. package/src/tools/jtd-utils.ts +102 -0
  1293. package/src/tools/learn.ts +141 -0
  1294. package/src/tools/list-limit.ts +40 -0
  1295. package/src/tools/manage-skill.ts +102 -0
  1296. package/src/tools/match-line-format.ts +20 -0
  1297. package/src/tools/memory-edit.ts +61 -0
  1298. package/src/tools/memory-recall.ts +102 -0
  1299. package/src/tools/memory-reflect.ts +88 -0
  1300. package/src/tools/memory-render.ts +211 -0
  1301. package/src/tools/memory-retain.ts +89 -0
  1302. package/src/tools/output-meta.ts +845 -0
  1303. package/src/tools/output-schema-validator.ts +307 -0
  1304. package/src/tools/path-utils.ts +1381 -0
  1305. package/src/tools/plan-mode-guard.ts +155 -0
  1306. package/src/tools/puppeteer/00_stealth_tampering.txt +44 -0
  1307. package/src/tools/puppeteer/01_stealth_activity.txt +80 -0
  1308. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -0
  1309. package/src/tools/puppeteer/03_stealth_botd.txt +380 -0
  1310. package/src/tools/puppeteer/04_stealth_iframe.txt +174 -0
  1311. package/src/tools/puppeteer/05_stealth_webgl.txt +233 -0
  1312. package/src/tools/puppeteer/06_stealth_screen.txt +260 -0
  1313. package/src/tools/puppeteer/07_stealth_fonts.txt +99 -0
  1314. package/src/tools/puppeteer/08_stealth_audio.txt +63 -0
  1315. package/src/tools/puppeteer/09_stealth_locale.txt +51 -0
  1316. package/src/tools/puppeteer/10_stealth_plugins.txt +212 -0
  1317. package/src/tools/puppeteer/11_stealth_hardware.txt +59 -0
  1318. package/src/tools/puppeteer/12_stealth_codecs.txt +42 -0
  1319. package/src/tools/puppeteer/13_stealth_worker.txt +235 -0
  1320. package/src/tools/read.ts +3742 -0
  1321. package/src/tools/render-utils.ts +924 -0
  1322. package/src/tools/renderers.ts +133 -0
  1323. package/src/tools/report-tool-issue.ts +568 -0
  1324. package/src/tools/resolve.ts +423 -0
  1325. package/src/tools/review.ts +103 -0
  1326. package/src/tools/shell-tokenize.ts +83 -0
  1327. package/src/tools/sqlite-reader.ts +884 -0
  1328. package/src/tools/terminal-output.ts +141 -0
  1329. package/src/tools/todo.ts +1226 -0
  1330. package/src/tools/tool-errors.ts +62 -0
  1331. package/src/tools/tool-result.ts +102 -0
  1332. package/src/tools/tool-timeouts.ts +37 -0
  1333. package/src/tools/tts.ts +266 -0
  1334. package/src/tools/vibe.ts +607 -0
  1335. package/src/tools/write.ts +1640 -0
  1336. package/src/tools/xdev.ts +508 -0
  1337. package/src/tools/yield.ts +486 -0
  1338. package/src/tts/downloader.ts +64 -0
  1339. package/src/tts/index.ts +10 -0
  1340. package/src/tts/models.ts +137 -0
  1341. package/src/tts/runtime.ts +21 -0
  1342. package/src/tts/speakable.ts +392 -0
  1343. package/src/tts/speech-enhancer.ts +206 -0
  1344. package/src/tts/streaming-player.ts +120 -0
  1345. package/src/tts/tts-client.ts +475 -0
  1346. package/src/tts/tts-protocol.ts +69 -0
  1347. package/src/tts/tts-worker.ts +434 -0
  1348. package/src/tts/vocalizer.ts +419 -0
  1349. package/src/tts/wav.ts +58 -0
  1350. package/src/tui/code-cell.ts +268 -0
  1351. package/src/tui/file-list.ts +55 -0
  1352. package/src/tui/hyperlink.ts +178 -0
  1353. package/src/tui/index.ts +13 -0
  1354. package/src/tui/output-block.ts +268 -0
  1355. package/src/tui/status-line.ts +54 -0
  1356. package/src/tui/tree-list.ts +172 -0
  1357. package/src/tui/types.ts +15 -0
  1358. package/src/tui/utils.ts +103 -0
  1359. package/src/tui/width-aware-text.ts +58 -0
  1360. package/src/utils/active-repo-context.ts +143 -0
  1361. package/src/utils/block-context.ts +312 -0
  1362. package/src/utils/changelog.ts +232 -0
  1363. package/src/utils/clipboard.ts +327 -0
  1364. package/src/utils/command-args.ts +76 -0
  1365. package/src/utils/commit-message-generator.ts +148 -0
  1366. package/src/utils/cpuprofile.ts +235 -0
  1367. package/src/utils/edit-mode.ts +61 -0
  1368. package/src/utils/enhanced-paste.ts +230 -0
  1369. package/src/utils/event-bus.ts +33 -0
  1370. package/src/utils/external-editor.ts +78 -0
  1371. package/src/utils/fetch-timeout.ts +10 -0
  1372. package/src/utils/file-display-mode.ts +44 -0
  1373. package/src/utils/file-mentions.ts +293 -0
  1374. package/src/utils/git.ts +2425 -0
  1375. package/src/utils/image-loading.ts +231 -0
  1376. package/src/utils/image-resize.ts +420 -0
  1377. package/src/utils/image-vision-fallback.ts +196 -0
  1378. package/src/utils/inspect-image-mode.ts +39 -0
  1379. package/src/utils/ipc.ts +38 -0
  1380. package/src/utils/jj.ts +416 -0
  1381. package/src/utils/lang-from-path.ts +251 -0
  1382. package/src/utils/local-date.ts +7 -0
  1383. package/src/utils/mac-file-urls.applescript +37 -0
  1384. package/src/utils/markit-cache.ts +166 -0
  1385. package/src/utils/markit.ts +223 -0
  1386. package/src/utils/mupdf-wasm-embed.ts +12 -0
  1387. package/src/utils/open.ts +126 -0
  1388. package/src/utils/profile-tree.ts +111 -0
  1389. package/src/utils/prompt-path.ts +3 -0
  1390. package/src/utils/qrcode.ts +535 -0
  1391. package/src/utils/sample-profile.ts +437 -0
  1392. package/src/utils/session-color.ts +142 -0
  1393. package/src/utils/shell-snapshot-fn-env.sh +63 -0
  1394. package/src/utils/shell-snapshot.ts +316 -0
  1395. package/src/utils/sixel.ts +69 -0
  1396. package/src/utils/thinking-display.ts +163 -0
  1397. package/src/utils/title-generator.ts +597 -0
  1398. package/src/utils/token-rate.ts +72 -0
  1399. package/src/utils/tool-choice.ts +63 -0
  1400. package/src/utils/tools-manager.ts +411 -0
  1401. package/src/utils/turndown.ts +83 -0
  1402. package/src/utils/zip.ts +1106 -0
  1403. package/src/vibe/runtime.ts +1540 -0
  1404. package/src/vibe/state.ts +4 -0
  1405. package/src/web/kagi.ts +304 -0
  1406. package/src/web/parallel.ts +354 -0
  1407. package/src/web/scrapers/artifacthub.ts +207 -0
  1408. package/src/web/scrapers/arxiv.ts +83 -0
  1409. package/src/web/scrapers/aur.ts +162 -0
  1410. package/src/web/scrapers/biorxiv.ts +133 -0
  1411. package/src/web/scrapers/bluesky.ts +262 -0
  1412. package/src/web/scrapers/brew.ts +172 -0
  1413. package/src/web/scrapers/cheatsh.ts +68 -0
  1414. package/src/web/scrapers/chocolatey.ts +196 -0
  1415. package/src/web/scrapers/choosealicense.ts +95 -0
  1416. package/src/web/scrapers/cisa-kev.ts +87 -0
  1417. package/src/web/scrapers/clojars.ts +154 -0
  1418. package/src/web/scrapers/coingecko.ts +177 -0
  1419. package/src/web/scrapers/crates-io.ts +97 -0
  1420. package/src/web/scrapers/crossref.ts +136 -0
  1421. package/src/web/scrapers/devto.ts +147 -0
  1422. package/src/web/scrapers/discogs.ts +306 -0
  1423. package/src/web/scrapers/discourse.ts +197 -0
  1424. package/src/web/scrapers/dockerhub.ts +138 -0
  1425. package/src/web/scrapers/docs-rs.ts +663 -0
  1426. package/src/web/scrapers/fdroid.ts +134 -0
  1427. package/src/web/scrapers/firefox-addons.ts +191 -0
  1428. package/src/web/scrapers/flathub.ts +223 -0
  1429. package/src/web/scrapers/github-gist.ts +58 -0
  1430. package/src/web/scrapers/github.ts +800 -0
  1431. package/src/web/scrapers/gitlab.ts +401 -0
  1432. package/src/web/scrapers/go-pkg.ts +266 -0
  1433. package/src/web/scrapers/hackage.ts +140 -0
  1434. package/src/web/scrapers/hackernews.ts +189 -0
  1435. package/src/web/scrapers/hex.ts +105 -0
  1436. package/src/web/scrapers/huggingface.ts +321 -0
  1437. package/src/web/scrapers/iacr.ts +89 -0
  1438. package/src/web/scrapers/index.ts +252 -0
  1439. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1440. package/src/web/scrapers/lemmy.ts +203 -0
  1441. package/src/web/scrapers/lobsters.ts +175 -0
  1442. package/src/web/scrapers/mastodon.ts +292 -0
  1443. package/src/web/scrapers/maven.ts +138 -0
  1444. package/src/web/scrapers/mdn.ts +173 -0
  1445. package/src/web/scrapers/metacpan.ts +222 -0
  1446. package/src/web/scrapers/musicbrainz.ts +250 -0
  1447. package/src/web/scrapers/npm.ts +98 -0
  1448. package/src/web/scrapers/nuget.ts +183 -0
  1449. package/src/web/scrapers/nvd.ts +222 -0
  1450. package/src/web/scrapers/ollama.ts +239 -0
  1451. package/src/web/scrapers/open-vsx.ts +106 -0
  1452. package/src/web/scrapers/opencorporates.ts +292 -0
  1453. package/src/web/scrapers/openlibrary.ts +336 -0
  1454. package/src/web/scrapers/orcid.ts +286 -0
  1455. package/src/web/scrapers/osv.ts +176 -0
  1456. package/src/web/scrapers/packagist.ts +160 -0
  1457. package/src/web/scrapers/pub-dev.ts +143 -0
  1458. package/src/web/scrapers/pubmed.ts +211 -0
  1459. package/src/web/scrapers/pypi.ts +112 -0
  1460. package/src/web/scrapers/rawg.ts +110 -0
  1461. package/src/web/scrapers/readthedocs.ts +120 -0
  1462. package/src/web/scrapers/reddit.ts +95 -0
  1463. package/src/web/scrapers/repology.ts +251 -0
  1464. package/src/web/scrapers/rfc.ts +201 -0
  1465. package/src/web/scrapers/rubygems.ts +103 -0
  1466. package/src/web/scrapers/searchcode.ts +189 -0
  1467. package/src/web/scrapers/sec-edgar.ts +261 -0
  1468. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1469. package/src/web/scrapers/snapcraft.ts +187 -0
  1470. package/src/web/scrapers/sourcegraph.ts +336 -0
  1471. package/src/web/scrapers/spdx.ts +108 -0
  1472. package/src/web/scrapers/spotify.ts +198 -0
  1473. package/src/web/scrapers/stackoverflow.ts +120 -0
  1474. package/src/web/scrapers/terraform.ts +277 -0
  1475. package/src/web/scrapers/tldr.ts +47 -0
  1476. package/src/web/scrapers/twitter.ts +94 -0
  1477. package/src/web/scrapers/types.ts +354 -0
  1478. package/src/web/scrapers/utils.ts +109 -0
  1479. package/src/web/scrapers/vimeo.ts +133 -0
  1480. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1481. package/src/web/scrapers/w3c.ts +156 -0
  1482. package/src/web/scrapers/wikidata.ts +344 -0
  1483. package/src/web/scrapers/wikipedia.ts +84 -0
  1484. package/src/web/scrapers/youtube.ts +325 -0
  1485. package/src/web/search/index.ts +376 -0
  1486. package/src/web/search/provider.ts +272 -0
  1487. package/src/web/search/providers/anthropic.ts +401 -0
  1488. package/src/web/search/providers/base.ts +110 -0
  1489. package/src/web/search/providers/brave.ts +179 -0
  1490. package/src/web/search/providers/browser-headers.ts +109 -0
  1491. package/src/web/search/providers/browser-page.ts +123 -0
  1492. package/src/web/search/providers/codex.ts +749 -0
  1493. package/src/web/search/providers/duckduckgo.ts +213 -0
  1494. package/src/web/search/providers/ecosia.ts +182 -0
  1495. package/src/web/search/providers/exa.ts +511 -0
  1496. package/src/web/search/providers/firecrawl.ts +211 -0
  1497. package/src/web/search/providers/gemini.ts +628 -0
  1498. package/src/web/search/providers/google.ts +194 -0
  1499. package/src/web/search/providers/jina.ts +134 -0
  1500. package/src/web/search/providers/kagi.ts +95 -0
  1501. package/src/web/search/providers/kimi.ts +217 -0
  1502. package/src/web/search/providers/mojeek.ts +219 -0
  1503. package/src/web/search/providers/parallel.ts +182 -0
  1504. package/src/web/search/providers/perplexity-auth.ts +142 -0
  1505. package/src/web/search/providers/perplexity.ts +994 -0
  1506. package/src/web/search/providers/public.ts +199 -0
  1507. package/src/web/search/providers/searxng.ts +461 -0
  1508. package/src/web/search/providers/startpage.ts +219 -0
  1509. package/src/web/search/providers/synthetic.ts +120 -0
  1510. package/src/web/search/providers/tavily.ts +242 -0
  1511. package/src/web/search/providers/tinyfish.ts +164 -0
  1512. package/src/web/search/providers/utils.ts +128 -0
  1513. package/src/web/search/providers/xai.ts +372 -0
  1514. package/src/web/search/providers/zai.ts +437 -0
  1515. package/src/web/search/query.ts +850 -0
  1516. package/src/web/search/render.ts +262 -0
  1517. package/src/web/search/types.ts +507 -0
  1518. package/src/web/search/utils.ts +17 -0
  1519. package/src/workspace-tree.ts +326 -0
@@ -0,0 +1,4957 @@
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 {
8
+ type Agent,
9
+ AgentBusyError,
10
+ type AgentMessage,
11
+ EventLoopKeepalive,
12
+ ThinkingLevel,
13
+ } from "@oh-my-pi/pi-agent-core";
14
+ import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
15
+ import type { AssistantMessage, ImageContent, Message, Model, Usage, UsageReport } from "@oh-my-pi/pi-ai";
16
+ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
17
+ import type {
18
+ AutocompleteProvider,
19
+ Component,
20
+ EditorTheme,
21
+ LoaderMessageColorFn,
22
+ NativeScrollbackLiveRegion,
23
+ OverlayHandle,
24
+ SlashCommand,
25
+ } from "@oh-my-pi/pi-tui";
26
+ import {
27
+ Container,
28
+ clearRenderCache,
29
+ Loader,
30
+ Markdown,
31
+ ProcessTerminal,
32
+ Spacer,
33
+ setTerminalTextSizing,
34
+ setTuiTight,
35
+ TERMINAL,
36
+ Text,
37
+ TUI,
38
+ visibleWidth,
39
+ } from "@oh-my-pi/pi-tui";
40
+ import { isInsideTerminalMultiplexer } from "@oh-my-pi/pi-tui/terminal-capabilities";
41
+ import {
42
+ $env,
43
+ APP_NAME,
44
+ adjustHsv,
45
+ formatNumber,
46
+ getProjectDir,
47
+ hsvToRgb,
48
+ isEnoent,
49
+ logger,
50
+ postmortem,
51
+ prompt,
52
+ setProjectDir,
53
+ } from "@oh-my-pi/pi-utils";
54
+ import chalk from "chalk";
55
+ import { reset as resetCapabilities } from "../capability";
56
+ import type { CollabGuestLink } from "../collab/guest";
57
+ import type { CollabHost } from "../collab/host";
58
+ import { KeybindingsManager } from "../config/keybindings";
59
+ import { formatModelString, type ResolvedModelRoleValue } from "../config/model-resolver";
60
+ import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
61
+ import {
62
+ isSettingsInitialized,
63
+ onModelRolesChanged,
64
+ onStatusLineSessionAccentChanged,
65
+ Settings,
66
+ settings,
67
+ } from "../config/settings";
68
+ import { clearClaudePluginRootsCache } from "../discovery/helpers";
69
+ import type {
70
+ AutocompleteProviderFactory,
71
+ ContextUsage,
72
+ ExtensionUIContext,
73
+ ExtensionUIDialogOptions,
74
+ ExtensionUISelectItem,
75
+ ExtensionWidgetContent,
76
+ ExtensionWidgetOptions,
77
+ } from "../extensibility/extensions";
78
+ import type { CompactOptions } from "../extensibility/extensions/types";
79
+ import type { Skill } from "../extensibility/skills";
80
+ import { loadSlashCommands } from "../extensibility/slash-commands";
81
+ import type { Goal, GoalModeState } from "../goals/state";
82
+ import { resolveLocalUrlToPath } from "../internal-urls";
83
+ import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
84
+ import type { MCPManager } from "../mcp";
85
+ import {
86
+ formatMCPConnectionStatusMessage,
87
+ isMcpConnectionStatusEvent,
88
+ MCP_CONNECTION_STATUS_EVENT_CHANNEL,
89
+ type McpConnectionStatusEvent,
90
+ } from "../mcp/startup-events";
91
+ import { humanizePlanTitle, type PlanApprovalDetails, resolvePlanTitle } from "../plan-mode/approved-plan";
92
+ import { resolvePlanModelTransition } from "../plan-mode/model-transition";
93
+ import guidedGoalInterviewPrompt from "../prompts/goals/guided-goal-interview.md" with { type: "text" };
94
+ import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
95
+ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
96
+ type: "text",
97
+ };
98
+ import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
99
+ import {
100
+ type AgentSession,
101
+ type AgentSessionEvent,
102
+ type ResolvedRoleModel,
103
+ SHUTDOWN_CONSOLIDATE_BUDGET_MS,
104
+ } from "../session/agent-session";
105
+ import type { CompactMode } from "../session/compact-modes";
106
+ import { HistoryStorage } from "../session/history-storage";
107
+ import type { SessionContext } from "../session/session-context";
108
+ import { getRecentSessions } from "../session/session-listing";
109
+ import type { SessionManager } from "../session/session-manager";
110
+ import type { ShakeMode } from "../session/shake-types";
111
+ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } from "../slash-commands/builtin-registry";
112
+ import { formatDuration } from "../slash-commands/helpers/format";
113
+ import { STTController, type SttState } from "../stt";
114
+ import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
115
+ import { formatTaskId } from "../task/render";
116
+ import type { ConfiguredThinkingLevel } from "../thinking";
117
+ import { tinyTitleClient } from "../tiny/title-client";
118
+ import type { LspStartupServerInfo } from "../tools";
119
+ import { normalizeLocalScheme } from "../tools/path-utils";
120
+ import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
121
+ import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
122
+ import {
123
+ formatPhaseDisplayName,
124
+ selectCollapsedTodos,
125
+ setActiveTodoDescriptionsProvider,
126
+ todoMatchesAnyDescription,
127
+ } from "../tools/todo";
128
+ import { vocalizer } from "../tts/vocalizer";
129
+ import { renderTreeList } from "../tui/tree-list";
130
+ import { copyToClipboard } from "../utils/clipboard";
131
+ import type { EventBus } from "../utils/event-bus";
132
+ import { getEditorCommand, openInEditor } from "../utils/external-editor";
133
+ import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
134
+ import { messageHasDisplayableThinking } from "../utils/thinking-display";
135
+ import {
136
+ disposeTerminalTitleState,
137
+ popTerminalTitle,
138
+ pushTerminalTitle,
139
+ setSessionTerminalTitle,
140
+ setTerminalTitleStateEnabled,
141
+ } from "../utils/title-generator";
142
+ import {
143
+ aggregateVibeWorkerTokensPerSecond,
144
+ type VibeOwnerScope,
145
+ type VibeParentSession,
146
+ VibeSessionRegistry,
147
+ } from "../vibe/runtime";
148
+ import type { AssistantMessageComponent } from "./components/assistant-message";
149
+ import type { BashExecutionComponent } from "./components/bash-execution";
150
+ import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
151
+ import { CustomEditor } from "./components/custom-editor";
152
+ import { DynamicBorder } from "./components/dynamic-border";
153
+ import { ErrorBannerComponent } from "./components/error-banner";
154
+ import type { EvalExecutionComponent } from "./components/eval-execution";
155
+ import type { HookEditorComponent } from "./components/hook-editor";
156
+ import type { HookInputComponent } from "./components/hook-input";
157
+ import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
158
+ import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
159
+ import { StatusLineComponent } from "./components/status-line";
160
+ import type { ToolExecutionHandle } from "./components/tool-execution";
161
+ import { TranscriptContainer } from "./components/transcript-container";
162
+ import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
163
+ import { BtwController } from "./controllers/btw-controller";
164
+ import { CommandController } from "./controllers/command-controller";
165
+ import { EventController } from "./controllers/event-controller";
166
+ import { ExtensionUiController } from "./controllers/extension-ui-controller";
167
+ import { InputController } from "./controllers/input-controller";
168
+ import { LiveCommandController } from "./controllers/live-command-controller";
169
+ import { MCPCommandController } from "./controllers/mcp-command-controller";
170
+ import { OmfgController } from "./controllers/omfg-controller";
171
+ import { SelectorController } from "./controllers/selector-controller";
172
+ import { SessionFocusController } from "./controllers/session-focus-controller";
173
+ import { SSHCommandController } from "./controllers/ssh-command-controller";
174
+ import { TanCommandController } from "./controllers/tan-command-controller";
175
+ import { TodoCommandController } from "./controllers/todo-command-controller";
176
+ import {
177
+ consumeLoopLimitIteration,
178
+ createLoopLimitRuntime,
179
+ describeLoopLimit,
180
+ describeLoopLimitRuntime,
181
+ isLoopDurationExpired,
182
+ type LoopLimitRuntime,
183
+ parseLoopLimitArgs,
184
+ } from "./loop-limit";
185
+ import { OAuthManualInputManager } from "./oauth-manual-input";
186
+ import { countRunningSubagentBadgeAgents, getRunningSubagentBadgeRegistry } from "./running-subagent-badge";
187
+ import {
188
+ type ObservableSession,
189
+ type SessionObserverChangeKind,
190
+ SessionObserverRegistry,
191
+ } from "./session-observer-registry";
192
+ import { createSessionTeardown, type SessionTeardown } from "./session-teardown";
193
+ import { runProviderSetupWizard } from "./setup-wizard/lazy";
194
+ import { interruptHint } from "./shared";
195
+ import { clearMermaidCache } from "./theme/mermaid-cache";
196
+ import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
197
+ import type { Theme } from "./theme/theme";
198
+ import {
199
+ getEditorTheme,
200
+ getMarkdownTheme,
201
+ getSymbolTheme,
202
+ onTerminalAppearanceChange,
203
+ onThemeChange,
204
+ setMarkdownMermaidRendering,
205
+ theme,
206
+ } from "./theme/theme";
207
+ import type {
208
+ CompactionQueuedMessage,
209
+ InteractiveModeContext,
210
+ InteractiveModeInitOptions,
211
+ InteractiveSelectorDialogOptions,
212
+ RenderSessionContextOptions,
213
+ SubmittedUserInput,
214
+ TodoItem,
215
+ TodoPhase,
216
+ } from "./types";
217
+ import { UiHelpers } from "./utils/ui-helpers";
218
+
219
+ const STILL_CLOSING_DELAY_MS = 3_000;
220
+
221
+ const HINT_SHIMMER_PALETTE: ShimmerPalette = {
222
+ low: "dim",
223
+ mid: "muted",
224
+ high: "borderAccent",
225
+ };
226
+
227
+ interface WorkingMessageAccent {
228
+ main: string;
229
+ dim: string;
230
+ }
231
+
232
+ interface WorkingMessageAccentCacheKey {
233
+ sessionName: string | undefined;
234
+ accentSurfaceLuminance: number | undefined;
235
+ sessionAccentEnabled: boolean;
236
+ }
237
+
238
+ /**
239
+ * Intern the shimmer palettes for each `WorkingMessageAccent` so `compile()`
240
+ * inside `shimmerSegments` sees a stable palette object between animation
241
+ * ticks. Allocating fresh palette literals every frame guaranteed a cache miss
242
+ * on the Symbol-keyed compiled-ANSI slot and forced `resolveTierAnsi` to walk
243
+ * every tier open/close for the ~30fps loader redraw (issue #4377).
244
+ */
245
+ const workingMessagePaletteCache = new WeakMap<WorkingMessageAccent, { main: ShimmerPalette; hint: ShimmerPalette }>();
246
+
247
+ function workingMessagePalettes(accent: WorkingMessageAccent): { main: ShimmerPalette; hint: ShimmerPalette } {
248
+ let entry = workingMessagePaletteCache.get(accent);
249
+ if (!entry) {
250
+ entry = {
251
+ main: { low: "dim", mid: { ansi: accent.main }, high: { ansi: accent.main }, bold: true },
252
+ hint: { low: "dim", mid: { ansi: accent.dim }, high: { ansi: accent.dim } },
253
+ };
254
+ workingMessagePaletteCache.set(accent, entry);
255
+ }
256
+ return entry;
257
+ }
258
+
259
+ function renderWorkingMessage(message: string, accent?: WorkingMessageAccent): string {
260
+ const palettes = accent ? workingMessagePalettes(accent) : undefined;
261
+ const palette = palettes?.main;
262
+ const hint = interruptHint();
263
+ if (!message.endsWith(hint)) return shimmerText(message, theme, palette);
264
+ const header = message.slice(0, -hint.length);
265
+ return shimmerSegments(
266
+ [
267
+ { text: header, palette },
268
+ { text: hint, palette: palettes?.hint ?? HINT_SHIMMER_PALETTE },
269
+ ],
270
+ theme,
271
+ );
272
+ }
273
+
274
+ const EDITOR_MAX_HEIGHT_MIN = 6;
275
+ const EDITOR_MAX_HEIGHT_MAX = 18;
276
+ const EDITOR_RESERVED_ROWS = 12;
277
+ const EDITOR_FALLBACK_ROWS = 24;
278
+ const EDITOR_MIN_CHROME_ROWS = 4; // rows reserved for transcript + status on small terms
279
+ const EDITOR_MIN_RENDERED_ROWS = 3; // bordered editor floor: top+bottom border + 1 content row
280
+
281
+ /**
282
+ * Editor max-height cap for a terminal of `terminalRows` rows.
283
+ *
284
+ * Roomy terminals get the comfortable [6, 18] band. Small terminals shrink the
285
+ * cap so the editor leaves at least EDITOR_MIN_CHROME_ROWS rows for the
286
+ * transcript + status line. The editor is bordered, so it never renders fewer
287
+ * than EDITOR_MIN_RENDERED_ROWS rows; once the terminal is too small for both
288
+ * (terminalRows < EDITOR_MIN_RENDERED_ROWS + EDITOR_MIN_CHROME_ROWS) the cap is
289
+ * pinned to that floor — returning a smaller number would not shrink the editor
290
+ * any further, it would only misreport the rows it actually occupies.
291
+ */
292
+ export function computeEditorMaxHeight(terminalRows: number): number {
293
+ const rows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : EDITOR_FALLBACK_ROWS;
294
+ const comfortable = Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, rows - EDITOR_RESERVED_ROWS));
295
+ return Math.max(EDITOR_MIN_RENDERED_ROWS, Math.min(comfortable, rows - EDITOR_MIN_CHROME_ROWS));
296
+ }
297
+
298
+ const HUD_NOTE_SUP_DIGITS: Record<string, string> = {
299
+ "0": "\u2070",
300
+ "1": "\u00b9",
301
+ "2": "\u00b2",
302
+ "3": "\u00b3",
303
+ "4": "\u2074",
304
+ "5": "\u2075",
305
+ "6": "\u2076",
306
+ "7": "\u2077",
307
+ "8": "\u2078",
308
+ "9": "\u2079",
309
+ };
310
+
311
+ function formatHudNoteMarker(count: number): string {
312
+ if (count <= 0) return "";
313
+ const sub = String(count)
314
+ .split("")
315
+ .map(d => HUD_NOTE_SUP_DIGITS[d] ?? d)
316
+ .join("");
317
+ return theme.fg("dim", chalk.italic(` \u207a${sub}`));
318
+ }
319
+
320
+ type GoalSubcommand = "set" | "show" | "pause" | "resume" | "drop" | "budget";
321
+
322
+ const GOAL_SUBCOMMANDS = new Set<GoalSubcommand>(["set", "show", "pause", "resume", "drop", "budget"]);
323
+ const PLAN_KEEP_CONTEXT_OPTION_INDEX = 2;
324
+ const PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT = 95;
325
+
326
+ function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; rest: string } {
327
+ const trimmed = args.trim();
328
+ if (!trimmed) return { sub: undefined, rest: "" };
329
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
330
+ if (!match) return { sub: undefined, rest: trimmed };
331
+ const first = match[1].toLowerCase();
332
+ if (GOAL_SUBCOMMANDS.has(first as GoalSubcommand)) {
333
+ return { sub: first as GoalSubcommand, rest: match[2]?.trim() ?? "" };
334
+ }
335
+ return { sub: undefined, rest: trimmed };
336
+ }
337
+
338
+ function formatContextTokenCount(value: number): string {
339
+ return formatNumber(Math.max(0, Math.round(value))).toLowerCase();
340
+ }
341
+
342
+ /** Options for creating an InteractiveMode instance (for future API use) */
343
+ export interface InteractiveModeOptions {
344
+ /** Providers that were migrated during startup */
345
+ migratedProviders?: string[];
346
+ /** Warning message if model fallback occurred */
347
+ modelFallbackMessage?: string;
348
+ /** Initial message to send */
349
+ initialMessage?: string;
350
+ /** Initial images to include with the message */
351
+ initialImages?: ImageContent[];
352
+ /** Additional initial messages to queue */
353
+ initialMessages?: string[];
354
+ }
355
+
356
+ /**
357
+ * Anchored live-region container for the HUD/status rows between the transcript
358
+ * and the editor (working loader, todo + subagent HUDs, transient notification
359
+ * panels). While it has content every row is live: it reports a seam at 0 so the
360
+ * engine never commits these anchored, rebuilt-in-place rows to native
361
+ * scrollback — otherwise stale duplicates pile up above the live copy on short
362
+ * terminals once the loader sits below a tall HUD. The transcript's own seam,
363
+ * when present, sits higher and wins (topmost-seam merge in TUI.render).
364
+ */
365
+ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveRegion {
366
+ getNativeScrollbackLiveRegionStart(): number | undefined {
367
+ return this.children.length > 0 ? 0 : undefined;
368
+ }
369
+ }
370
+
371
+ /** How long the ctrl+p model-role cycle chip track lingers above the editor
372
+ * before it auto-clears, mirroring the todo HUD's auto-clear timer. */
373
+ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
374
+
375
+ const SUBAGENT_HUD_VISIBLE_LIMIT = 8;
376
+ const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
377
+
378
+ /**
379
+ * Build the anchored subagent HUD block: a bold accent "Subagents" header plus
380
+ * a bounded set of running-agent rows in the same `Id: description` shape the
381
+ * inline task rows use (muted task preview when no description was given).
382
+ * Layout mirrors the Todos HUD exactly: unindented header, then
383
+ * `renderTreeList` rows (dim connectors) shifted right by one space.
384
+ * Only detached background spawns are listed: a sync task call blocks the
385
+ * parent turn and its inline tool block already renders progress live, and
386
+ * eval `agent()` spawns are rendered by their own eval cell tree.
387
+ * Returns an empty array when nothing is running so the container can clear.
388
+ */
389
+ export function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[] {
390
+ const running = sessions.filter(
391
+ session => session.kind === "subagent" && session.status === "active" && session.detached === true,
392
+ );
393
+ if (running.length === 0) return [];
394
+
395
+ const dot = theme.styledSymbol("status.done", "accent");
396
+ const visible = running.slice(0, SUBAGENT_HUD_VISIBLE_LIMIT);
397
+ const hiddenCount = running.length - visible.length;
398
+ const rows = renderTreeList(
399
+ {
400
+ items: visible,
401
+ expanded: true,
402
+ renderItem: session => {
403
+ const displayId = formatTaskId(session.id);
404
+ let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}`;
405
+ const description = session.description?.trim() || session.progress?.description?.trim();
406
+ if (description) {
407
+ const budget = Math.max(TRUNCATE_LENGTHS.SHORT, columns - visibleWidth(displayId) - 10);
408
+ line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(description), budget))}`;
409
+ } else {
410
+ // No spawn description: fall back to a muted task preview, same as
411
+ // the inline task rows when a row has no label.
412
+ const taskPreview = session.progress?.task?.trim();
413
+ if (taskPreview) {
414
+ line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`;
415
+ }
416
+ }
417
+ return line;
418
+ },
419
+ },
420
+ theme,
421
+ );
422
+ if (hiddenCount > 0) {
423
+ rows.push(theme.fg("dim", `… ${hiddenCount} more running — open Agent Hub for full list`));
424
+ }
425
+ return ["", theme.bold(theme.fg("accent", "Subagents")), ...rows.map(line => ` ${line}`)];
426
+ }
427
+
428
+ export class InteractiveMode implements InteractiveModeContext {
429
+ session: AgentSession;
430
+ sessionManager: SessionManager;
431
+ settings: Settings;
432
+ keybindings: KeybindingsManager;
433
+ agent: Agent;
434
+ historyStorage?: HistoryStorage;
435
+
436
+ ui: TUI;
437
+ chatContainer: TranscriptContainer;
438
+ pendingMessagesContainer: Container;
439
+ statusContainer: Container;
440
+ todoContainer: Container;
441
+ subagentContainer: Container;
442
+ btwContainer: Container;
443
+ omfgContainer: Container;
444
+ errorBannerContainer: Container;
445
+ modelCycleContainer: Container;
446
+ editor: CustomEditor;
447
+ editorContainer: Container;
448
+ hookWidgetContainerAbove: Container;
449
+ hookWidgetContainerBelow: Container;
450
+ statusLine: StatusLineComponent;
451
+
452
+ isInitialized = false;
453
+ initialChatRendered = false;
454
+ isBashMode = false;
455
+ toolOutputExpanded = false;
456
+ todoExpanded = false;
457
+ planModeEnabled = false;
458
+ planModePaused = false;
459
+ goalModeEnabled = false;
460
+ goalModePaused = false;
461
+ vibeModeEnabled = false;
462
+ planModePlanFilePath: string | undefined = undefined;
463
+ loopModeEnabled = false;
464
+ loopModePaused = false;
465
+ loopPrompt: string | undefined = undefined;
466
+ loopLimit: LoopLimitRuntime | undefined = undefined;
467
+ #loopAutoSubmitTimer: NodeJS.Timeout | undefined;
468
+ #todoAutoClearTimer: NodeJS.Timeout | undefined;
469
+ #modelCycleClearTimer: NodeJS.Timeout | undefined;
470
+ todoPhases: TodoPhase[] = [];
471
+ hideThinkingBlock = false;
472
+ #sessionsWithDisplayableThinkingContent = new WeakSet<AgentSession>();
473
+ /** Whether the visible session has produced thinking content the user can reveal. */
474
+ get hasDisplayableThinkingContent(): boolean {
475
+ return this.#sessionsWithDisplayableThinkingContent.has(this.viewSession);
476
+ }
477
+ /** Record received reasoning content so Ctrl+T can reveal it even when model metadata says thinking is off. */
478
+ noteDisplayableThinkingContent(message: AgentMessage): boolean {
479
+ if (this.hasDisplayableThinkingContent || !messageHasDisplayableThinking(message, this.proseOnlyThinking)) {
480
+ return false;
481
+ }
482
+ this.#sessionsWithDisplayableThinkingContent.add(this.viewSession);
483
+ return true;
484
+ }
485
+ /**
486
+ * Effective thinking-block visibility: hidden when the user's setting is on,
487
+ * or while thinking is "off" before the session has actually produced
488
+ * displayable thinking content. Some providers return thinking blocks without
489
+ * advertising reasoning support, so observed content unlocks the visibility
490
+ * toggle.
491
+ */
492
+ get effectiveHideThinkingBlock(): boolean {
493
+ const thinkingOff = (this.viewSession?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
494
+ return this.hideThinkingBlock || (thinkingOff && !this.hasDisplayableThinkingContent);
495
+ }
496
+ proseOnlyThinking = true;
497
+ compactionQueuedMessages: CompactionQueuedMessage[] = [];
498
+ pendingTools = new Map<string, ToolExecutionHandle>();
499
+ transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
500
+ pendingBashComponents: BashExecutionComponent[] = [];
501
+ bashComponent: BashExecutionComponent | undefined = undefined;
502
+ pendingPythonComponents: EvalExecutionComponent[] = [];
503
+ pythonComponent: EvalExecutionComponent | undefined = undefined;
504
+ isPythonMode = false;
505
+ streamingComponent: AssistantMessageComponent | undefined = undefined;
506
+ streamingMessage: AssistantMessage | undefined = undefined;
507
+ lastAssistantUsage: Usage | undefined = undefined;
508
+ loadingAnimation: Loader | undefined = undefined;
509
+ autoCompactionLoader: Loader | undefined = undefined;
510
+ retryLoader: Loader | undefined = undefined;
511
+ #pendingWorkingMessage: string | undefined;
512
+ #workingMessageAccentCacheKey?: WorkingMessageAccentCacheKey;
513
+ #workingMessageAccentCacheValue?: WorkingMessageAccent;
514
+ #workingMessageAccentCacheHasValue = false;
515
+ get #defaultWorkingMessage(): string {
516
+ return `Working…${interruptHint()}`;
517
+ }
518
+ unsubscribe?: () => void;
519
+ onInputCallback?: (input: SubmittedUserInput) => void;
520
+ optimisticUserMessageSignature: string | undefined = undefined;
521
+ locallySubmittedUserSignatures: Set<string> = new Set();
522
+ #pendingSubmittedInput: SubmittedUserInput | undefined;
523
+ #pendingSubmissionDispose: (() => void) | undefined;
524
+ #optimisticUserMessageComponents: Component[] = [];
525
+ lastSigintTime = 0;
526
+ lastEscapeTime = 0;
527
+ lastLeftTapTime = 0;
528
+ shutdownRequested = false;
529
+ #isShuttingDown = false;
530
+ /** True once `shutdown()` has begun teardown. Surfaced to the input
531
+ * controller so a Ctrl+C arriving while teardown is in flight can hard-
532
+ * abort the remaining work instead of stacking another no-op call. */
533
+ get isShuttingDown(): boolean {
534
+ return this.#isShuttingDown;
535
+ }
536
+ hookSelector: HookSelectorComponent | undefined = undefined;
537
+ hookInput: HookInputComponent | undefined = undefined;
538
+ hookEditor: HookEditorComponent | undefined = undefined;
539
+ lastStatusSpacer: Spacer | undefined = undefined;
540
+ lastStatusText: Text | undefined = undefined;
541
+ fileSlashCommands: Set<string> = new Set();
542
+ skillCommands: Map<string, Skill> = new Map();
543
+ oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
544
+ collabHost?: CollabHost;
545
+ collabGuest?: CollabGuestLink;
546
+
547
+ #pendingCommandOutput: Component[] = [];
548
+ #pendingCommandOutputSessionId: string | undefined;
549
+ #pendingSlashCommands: SlashCommand[] = [];
550
+ /** Built-in editor autocomplete provider, before extension wrapping. */
551
+ #baseAutocompleteProvider: AutocompleteProvider | undefined;
552
+ /** Extension-registered provider factories, applied in registration order (#4919). */
553
+ #autocompleteProviderFactories: AutocompleteProviderFactory[] = [];
554
+ #cleanupUnsubscribe?: () => void;
555
+ #signalTeardown?: SessionTeardown;
556
+ readonly #version: string;
557
+ readonly #changelogMarkdown: string | undefined;
558
+ #planModePreviousTools: string[] | undefined;
559
+ #goalModePreviousTools: string[] | undefined;
560
+ #vibeModePreviousTools: string[] | undefined;
561
+ #vibeModeOwnerScope: VibeOwnerScope | undefined;
562
+ #vibeScopeSuspendedForSwitch = false;
563
+ #goalContinuationTimer: NodeJS.Timeout | undefined;
564
+ #goalTurnHadToolCalls = false;
565
+ #goalContinuationTurnInFlight = false;
566
+ #goalSuppressNextContinuation = false;
567
+ #planModePreviousModelState: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
568
+ #pendingModelSwitch: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
569
+ /** Whether #pendingModelSwitch was queued by the live plan-role reconciler. */
570
+ #pendingPlanModelSwitch = false;
571
+ #planModeHasEntered = false;
572
+ #planReviewOverlay: PlanReviewOverlay | undefined;
573
+ #planReviewOverlayHandle: OverlayHandle | undefined;
574
+ #planReviewCancel: (() => void) | undefined;
575
+ /** Serializable review annotations keyed by the resolved plan file path. */
576
+ #planReviewAnnotationState = new Map<string, PlanReviewAnnotationState>();
577
+ /** Annotation state held until the associated queued refinement actually starts. */
578
+ #planReviewAnnotationStateBySubmission = new WeakMap<SubmittedUserInput, string>();
579
+ readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
580
+ mcpManager?: MCPManager;
581
+ readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
582
+
583
+ readonly #btwController: BtwController;
584
+ readonly #tanCommandController: TanCommandController;
585
+ readonly #omfgController: OmfgController;
586
+ readonly #commandController: CommandController;
587
+ readonly #todoCommandController: TodoCommandController;
588
+ readonly #liveCommandController: LiveCommandController;
589
+ readonly #eventController: EventController;
590
+ get eventController(): EventController {
591
+ return this.#eventController;
592
+ }
593
+ get eventBus(): EventBus | undefined {
594
+ return this.#eventBus;
595
+ }
596
+ readonly #extensionUiController: ExtensionUiController;
597
+ readonly #inputController: InputController;
598
+ readonly #selectorController: SelectorController;
599
+ readonly #focusController: SessionFocusController;
600
+ get viewSession(): AgentSession {
601
+ return this.#focusController.target ?? this.session;
602
+ }
603
+ get focusedAgentId(): string | undefined {
604
+ return this.#focusController.focusedAgentId;
605
+ }
606
+ get sessionName(): string | undefined {
607
+ return this.session.sessionName;
608
+ }
609
+ focusAgentSession(id: string): Promise<void> {
610
+ return this.#focusController.focusAgent(id);
611
+ }
612
+ focusParentSession(): Promise<void> {
613
+ return this.#focusController.focusParent();
614
+ }
615
+ unfocusSession(): Promise<void> {
616
+ return this.#focusController.unfocus();
617
+ }
618
+ clearTransientSessionUi(): void {
619
+ if (this.loadingAnimation) {
620
+ this.loadingAnimation.stop();
621
+ this.loadingAnimation = undefined;
622
+ }
623
+ if (this.autoCompactionLoader) {
624
+ this.autoCompactionLoader.stop();
625
+ this.autoCompactionLoader = undefined;
626
+ }
627
+ if (this.retryLoader) {
628
+ this.retryLoader.stop();
629
+ this.retryLoader = undefined;
630
+ }
631
+ this.statusContainer.disposeChildren();
632
+ this.pendingMessagesContainer.disposeChildren();
633
+ this.#cancelModelCycleClearTimer();
634
+ this.modelCycleContainer.disposeChildren();
635
+ this.compactionQueuedMessages = [];
636
+ this.streamingComponent = undefined;
637
+ this.streamingMessage = undefined;
638
+ this.lastAssistantUsage = undefined;
639
+ this.pendingTools.clear();
640
+ }
641
+ readonly #uiHelpers: UiHelpers;
642
+ #sttController: STTController | undefined;
643
+ #voiceAnimationInterval: NodeJS.Timeout | undefined;
644
+ #voiceHue = 0;
645
+ #voicePreviousShowHardwareCursor: boolean | null = null;
646
+ #voicePreviousUseTerminalCursor: boolean | null = null;
647
+ #resizeHandler?: () => void;
648
+ #observerRegistry: SessionObserverRegistry;
649
+ #eventBus?: EventBus;
650
+ #eventBusUnsubscribers: Array<() => void> = [];
651
+ #observerUiSyncTimer?: NodeJS.Timeout;
652
+ #observerUiSyncNeedsTodoReconcile = false;
653
+ #agentRegistryUnsubscribe?: () => void;
654
+ #agentRegistrySubscriptionTarget?: AgentRegistry;
655
+ #mcpStatusOrder: string[] = [];
656
+ #mcpPendingServers = new Set<string>();
657
+ #mcpConnectedServers = new Set<string>();
658
+ #mcpFailedServers = new Map<string, string>();
659
+ #welcomeComponent?: WelcomeComponent;
660
+ readonly #chatHost: ChatBlockHost = { requestRender: () => this.ui.requestRender() };
661
+
662
+ constructor(
663
+ session: AgentSession,
664
+ version: string,
665
+ changelogMarkdown: string | undefined = undefined,
666
+ setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
667
+ lspServers: LspStartupServerInfo[] | undefined = undefined,
668
+ mcpManager?: MCPManager,
669
+ eventBus?: EventBus,
670
+ ) {
671
+ this.session = session;
672
+ this.sessionManager = session.sessionManager;
673
+ this.settings = session.settings;
674
+ this.keybindings = KeybindingsManager.inMemory();
675
+ this.agent = session.agent;
676
+ this.#version = version;
677
+ this.#changelogMarkdown = changelogMarkdown;
678
+ this.#toolUiContextSetter = setToolUIContext;
679
+ this.lspServers = lspServers;
680
+ this.mcpManager = mcpManager;
681
+ this.mcpManager?.setAuthHandler((serverName, challenge) =>
682
+ new MCPCommandController(this).handleMCPAuthChallenge(serverName, challenge),
683
+ );
684
+ this.#eventBus = eventBus;
685
+ if (eventBus) {
686
+ this.#eventBusUnsubscribers.push(
687
+ eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
688
+ if (this.settings.get("startup.quiet")) return;
689
+ this.#handleLspStartupEvent(data as LspStartupEvent);
690
+ }),
691
+ );
692
+ this.#eventBusUnsubscribers.push(
693
+ eventBus.on(MCP_CONNECTION_STATUS_EVENT_CHANNEL, data => {
694
+ if (!isMcpConnectionStatusEvent(data)) {
695
+ logger.warn("Ignoring malformed mcp:connection-status event", { data });
696
+ return;
697
+ }
698
+ this.#handleMcpConnectionStatusEvent(data);
699
+ }),
700
+ );
701
+ }
702
+
703
+ setTuiTight(settings.get("tui.tight"));
704
+ setMarkdownMermaidRendering(settings.get("tui.renderMermaid"));
705
+ this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
706
+ this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
707
+ this.ui.setScrollbackRebuild(settings.get("tui.scrollbackRebuild"));
708
+ // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
709
+ // capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
710
+ // unless the user opts in, and never emits raw escapes on other terminals.
711
+ setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
712
+ this.chatContainer = new TranscriptContainer();
713
+ this.pendingMessagesContainer = new AnchoredLiveContainer();
714
+ this.statusContainer = new AnchoredLiveContainer();
715
+ this.todoContainer = new AnchoredLiveContainer();
716
+ this.subagentContainer = new AnchoredLiveContainer();
717
+ this.btwContainer = new AnchoredLiveContainer();
718
+ this.omfgContainer = new AnchoredLiveContainer();
719
+ this.errorBannerContainer = new AnchoredLiveContainer();
720
+ this.modelCycleContainer = new AnchoredLiveContainer();
721
+ this.editor = new CustomEditor(getEditorTheme());
722
+ this.ui.enableScopedInputRender(this.editor);
723
+ this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
724
+ this.editor.setImeSafeCursorLayout(settings.get("tui.imeSafeCursor"));
725
+ this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
726
+ this.editor.onAutocompleteCancel = () => {
727
+ this.ui.requestRender(true);
728
+ };
729
+ this.editor.onAutocompleteUpdate = () => {
730
+ this.ui.requestRender();
731
+ };
732
+ this.editor.setShimmerRepaintHandler(() => this.ui.requestComponentRender(this.editor));
733
+ this.#syncEditorMaxHeight();
734
+ this.#resizeHandler = () => {
735
+ this.#syncEditorMaxHeight();
736
+ this.ui.requestRender();
737
+ };
738
+ process.stdout.on("resize", this.#resizeHandler);
739
+ try {
740
+ this.historyStorage = HistoryStorage.open();
741
+ this.editor.setHistoryStorage(this.historyStorage);
742
+ this.historyStorage.setSessionResolver(() => this.sessionManager.getSessionId());
743
+ } catch (error) {
744
+ logger.warn("History storage unavailable", { error: String(error) });
745
+ }
746
+ this.hookWidgetContainerAbove = new Container();
747
+ this.hookWidgetContainerAbove.addChild(new Spacer(1));
748
+ this.hookWidgetContainerBelow = new Container();
749
+ this.editorContainer = new Container();
750
+ this.editorContainer.addChild(this.editor);
751
+ this.statusLine = new StatusLineComponent(session);
752
+ this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
753
+ // Vibe worker tok/s aggregator — keeps the status-line render layer off
754
+ // the heavy vibe/task dependency graph. The director is often idle while
755
+ // workers stream, so without this the tok/s badge would show a stale
756
+ // value while parallel work is actively generating tokens.
757
+ this.statusLine.setVibeWorkerTokenRateProvider(() =>
758
+ aggregateVibeWorkerTokensPerSecond(this.session.getAgentId() ?? MAIN_AGENT_ID),
759
+ );
760
+ // Lazy provider — the top border rebuild coalesces to at most one
761
+ // invocation per painted frame instead of firing on every session event
762
+ // (#4145). The TUI throttles renders at ~30fps, so a long-running eval
763
+ // spraying events no longer runs `getTopBorder` synchronously in the
764
+ // hot path where the render never gets to paint the result.
765
+ this.editor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
766
+
767
+ this.hideThinkingBlock = settings.get("hideThinkingBlock");
768
+ this.proseOnlyThinking = settings.get("proseOnlyThinking");
769
+
770
+ const hookCommands: SlashCommand[] = (
771
+ this.session.extensionRunner?.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES) ?? []
772
+ ).map(cmd => ({
773
+ name: cmd.name,
774
+ description: cmd.description ?? "(hook command)",
775
+ getArgumentCompletions: cmd.getArgumentCompletions,
776
+ }));
777
+
778
+ // Convert custom commands (TypeScript) to SlashCommand format
779
+ const customCommands: SlashCommand[] = this.session.customCommands.map(loaded => ({
780
+ name: loaded.command.name,
781
+ description: `${loaded.command.description} (${loaded.source})`,
782
+ }));
783
+
784
+ const skillCommandList = this.#rebuildSkillCommandsFromSession();
785
+
786
+ const builtinCommands = buildTuiBuiltinSlashCommands({ ctx: this });
787
+ // Store pending commands for init() where file commands are loaded async
788
+ this.#pendingSlashCommands = [...builtinCommands, ...hookCommands, ...customCommands, ...skillCommandList];
789
+
790
+ this.#uiHelpers = new UiHelpers(this);
791
+ this.#btwController = new BtwController(this);
792
+ this.#tanCommandController = new TanCommandController(this);
793
+ this.#omfgController = new OmfgController(this);
794
+ this.#extensionUiController = new ExtensionUiController(this);
795
+ this.#eventController = new EventController(this);
796
+ this.#commandController = new CommandController(this);
797
+ this.#todoCommandController = new TodoCommandController(this);
798
+ this.#liveCommandController = new LiveCommandController(this);
799
+ this.#selectorController = new SelectorController(this);
800
+ this.#focusController = new SessionFocusController(this);
801
+ this.#inputController = new InputController(this);
802
+ this.#observerRegistry = new SessionObserverRegistry();
803
+ }
804
+
805
+ #handleMcpConnectionStatusEvent(event: McpConnectionStatusEvent): void {
806
+ if (this.settings.get("startup.quiet")) return;
807
+ if (event.type === "connecting") {
808
+ this.#mcpStatusOrder = [];
809
+ this.#mcpPendingServers.clear();
810
+ this.#mcpConnectedServers.clear();
811
+ this.#mcpFailedServers.clear();
812
+ for (const serverName of event.serverNames) {
813
+ this.#trackMcpStatusServer(serverName);
814
+ this.#mcpPendingServers.add(serverName);
815
+ }
816
+ } else if (event.type === "connected") {
817
+ this.#trackMcpStatusServer(event.serverName);
818
+ this.#mcpPendingServers.delete(event.serverName);
819
+ this.#mcpFailedServers.delete(event.serverName);
820
+ this.#mcpConnectedServers.add(event.serverName);
821
+ } else {
822
+ this.#trackMcpStatusServer(event.serverName);
823
+ this.#mcpPendingServers.delete(event.serverName);
824
+ this.#mcpConnectedServers.delete(event.serverName);
825
+ this.#mcpFailedServers.set(event.serverName, event.error);
826
+ }
827
+
828
+ const message = formatMCPConnectionStatusMessage({
829
+ pendingServers: this.#orderedMcpStatusServers(this.#mcpPendingServers),
830
+ connectedServers: this.#orderedMcpStatusServers(this.#mcpConnectedServers),
831
+ failedServers: this.#orderedMcpStatusFailures(),
832
+ });
833
+ if (message) this.showStatus(message);
834
+ }
835
+
836
+ #trackMcpStatusServer(serverName: string): void {
837
+ if (!this.#mcpStatusOrder.includes(serverName)) {
838
+ this.#mcpStatusOrder.push(serverName);
839
+ }
840
+ }
841
+
842
+ #orderedMcpStatusServers(servers: ReadonlySet<string>): string[] {
843
+ return this.#mcpStatusOrder.filter(serverName => servers.has(serverName));
844
+ }
845
+
846
+ #orderedMcpStatusFailures(): Array<{ serverName: string; error: string }> {
847
+ return this.#mcpStatusOrder.flatMap(serverName => {
848
+ const error = this.#mcpFailedServers.get(serverName);
849
+ return error === undefined ? [] : [{ serverName, error }];
850
+ });
851
+ }
852
+
853
+ playWelcomeIntro(): void {
854
+ const welcome = this.#welcomeComponent;
855
+ // Component-scoped: the intro only mutates the welcome box's own rows,
856
+ // so a resumed long transcript is not re-walked per animation frame.
857
+ welcome?.playIntro(() => this.ui.requestComponentRender(welcome));
858
+ }
859
+
860
+ async init(options: InteractiveModeInitOptions = {}): Promise<void> {
861
+ if (this.isInitialized) return;
862
+
863
+ this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
864
+
865
+ // Route SIGINT/SIGTERM/SIGHUP/uncaughtException through the same teardown
866
+ // the TUI Ctrl+C keypress path performs: persist the in-progress editor
867
+ // draft for `--resume`, then dispose the session (which emits the extension
868
+ // `session_shutdown` event, cancels the owned async job manager, disposes
869
+ // eval kernels, releases owned browser tabs, and closes the session
870
+ // manager). Without this callback a real kernel signal would drop the
871
+ // draft, skip the `session_shutdown` contract from `shared-events.ts`,
872
+ // and orphan background bash/task processes (issue #4080). The registered
873
+ // callback and `shutdown()` share one promise-memoized teardown, so a
874
+ // signal arriving mid-Ctrl+C no-ops instead of racing a second dispose.
875
+ this.#signalTeardown = createSessionTeardown({
876
+ getDraftText: () => this.editor.getText(),
877
+ beginDispose: () => this.session.beginDispose(),
878
+ saveDraft: text => this.sessionManager.saveDraft(text),
879
+ disposeSession: reason =>
880
+ this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS, reason }),
881
+ });
882
+ // Forward the postmortem reason (SIGTERM/SIGHUP/uncaughtException/…) so the
883
+ // persisted `session_exit` diagnostic carries the real trigger. Postmortem
884
+ // runs callbacks in REVERSE registration order — this callback (registered
885
+ // after the AgentSession constructor's `agent-session:<id>` recorder) runs
886
+ // FIRST and its dispose() would otherwise persist the generic "dispose".
887
+ this.#cleanupUnsubscribe = postmortem.register("session-teardown", reason => this.#signalTeardown!(reason));
888
+
889
+ // Wire the report_tool_issue consent gate to the Yes/No dialog popup.
890
+ // The handler is process-global — subagent tools (which can't reach
891
+ // `showHookSelector` on their own) resolve through this exact closure.
892
+ // `Settings.instance` is the disk-backed singleton; passing it explicitly
893
+ // guarantees the decision persists even when the prompt is triggered
894
+ // from a subagent whose own `Settings` is an in-memory snapshot.
895
+ setAutoQaConsentHandler(() => this.#promptAutoQaConsent(), Settings.instance);
896
+
897
+ await logger.time(
898
+ "InteractiveMode.init:slashCommands",
899
+ this.refreshSlashCommandState.bind(this),
900
+ getProjectDir(),
901
+ );
902
+
903
+ // Get current model info for welcome screen
904
+ const modelName = this.session.model?.name ?? "Unknown";
905
+ const providerName = this.session.model?.provider ?? "Unknown";
906
+
907
+ // Get recent sessions
908
+ const recentSessions = await logger.time("InteractiveMode.init:recentSessions", () =>
909
+ getRecentSessions(this.sessionManager.getSessionDir()).then(sessions =>
910
+ sessions.map(s => ({
911
+ name: s.name,
912
+ timeAgo: s.timeAgo,
913
+ })),
914
+ ),
915
+ );
916
+
917
+ const startupQuiet = settings.get("startup.quiet");
918
+ this.#welcomeComponent = undefined;
919
+
920
+ for (const warning of this.session.configWarnings) {
921
+ this.ui.addChild(new Text(theme.fg("warning", `Warning: ${warning}`), 1, 0));
922
+ this.ui.addChild(new Spacer(1));
923
+ }
924
+
925
+ if (!startupQuiet) {
926
+ // Add welcome header
927
+ this.#welcomeComponent = new WelcomeComponent(
928
+ this.#version,
929
+ modelName,
930
+ providerName,
931
+ recentSessions,
932
+ this.#getWelcomeLspServers(),
933
+ );
934
+
935
+ // Setup UI layout
936
+ this.ui.addChild(new Spacer(1));
937
+ this.ui.addChild(this.#welcomeComponent);
938
+ this.ui.addChild(new Spacer(1));
939
+ if (!options.suppressWelcomeIntro) {
940
+ this.playWelcomeIntro();
941
+ }
942
+
943
+ // Add changelog if provided
944
+ if (this.#changelogMarkdown) {
945
+ this.ui.addChild(new DynamicBorder());
946
+ if (settings.get("collapseChangelog")) {
947
+ const versionMatch = this.#changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
948
+ const latestVersion = versionMatch ? versionMatch[1] : this.#version;
949
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
950
+ this.ui.addChild(new Text(condensedText, 1, 0));
951
+ } else {
952
+ this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
953
+ this.ui.addChild(new Spacer(1));
954
+ this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
955
+ this.ui.addChild(new Spacer(1));
956
+ }
957
+ this.ui.addChild(new DynamicBorder());
958
+ }
959
+ }
960
+
961
+ this.ui.addChild(this.chatContainer);
962
+ this.ui.addChild(this.pendingMessagesContainer);
963
+ this.ui.addChild(this.todoContainer);
964
+ this.ui.addChild(this.subagentContainer);
965
+ this.ui.addChild(this.btwContainer);
966
+ this.ui.addChild(this.omfgContainer);
967
+ this.ui.addChild(this.errorBannerContainer);
968
+ this.ui.addChild(this.modelCycleContainer);
969
+ // Working loader / transient status sits below the sticky todo + subagent
970
+ // HUDs, just above the editor's hook-widget top margin — so it reads next to
971
+ // the prompt while keeping the one-line gap above the editor.
972
+ this.ui.addChild(this.statusContainer);
973
+ this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
974
+ this.ui.addChild(this.hookWidgetContainerAbove);
975
+ this.ui.addChild(this.editorContainer);
976
+ this.ui.addChild(this.hookWidgetContainerBelow);
977
+ this.ui.setFocus(this.editor);
978
+
979
+ this.#inputController.setupKeyHandlers();
980
+ this.#inputController.setupEditorSubmitHandler();
981
+
982
+ // Wire observer registry to EventBus
983
+ if (this.#eventBus) {
984
+ this.#observerRegistry.subscribeToEventBus(this.#eventBus);
985
+ }
986
+ this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
987
+ this.syncRunningSubagentBadge();
988
+ this.#observerRegistry.onChange(kind => {
989
+ this.#scheduleObserverUiSync(kind);
990
+ });
991
+ // Let the transient todo tool result light up pending todos executed by a
992
+ // live subagent, matching the sticky HUD's active set (#5873).
993
+ setActiveTodoDescriptionsProvider(() => this.#getActiveSubagentDescriptions());
994
+
995
+ // Load initial todos
996
+ await this.#loadTodoList();
997
+
998
+ // Start the UI. Cold `omp` launch opts into clearing on the first paint so
999
+ // the initial welcome frame does not append over the previous run's scrollback.
1000
+ this.ui.start({ clearScrollback: options.clearInitialTerminalHistory === true });
1001
+ pushTerminalTitle();
1002
+ setTerminalTitleStateEnabled(this.settings.get("tui.titleState"));
1003
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
1004
+ this.updateEditorBorderColor();
1005
+ // Single side-effect point for title changes: every setSessionName caller
1006
+ // (first-input titling, /rename, extension renames, plan seeding, replan
1007
+ // refresh) gets the terminal title + accent updates from here. Registered
1008
+ // before initHooksAndCustomTools/#reconcileModeFromSession/#enterPlanMode —
1009
+ // all of which can reach setSessionName during init.
1010
+ this.#eventBusUnsubscribers.push(
1011
+ this.sessionManager.onSessionNameChanged(() => {
1012
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
1013
+ this.#handleSessionAccentInputsChanged();
1014
+ }),
1015
+ );
1016
+ this.#syncEditorMaxHeight();
1017
+ this.isInitialized = true;
1018
+ this.ui.requestRender(true);
1019
+
1020
+ // Prewarm the local tiny-title worker off the submit hot path: spawn it
1021
+ // now, idle and unref'd, so the first submit reuses a live subprocess
1022
+ // instead of paying spawn latency ahead of the first frame (issue #6462).
1023
+ // No-ops for the online default and for already-named sessions that will
1024
+ // not be titled. Deferred via setImmediate so it runs AFTER the render
1025
+ // callback requestRender(true) queued above (immediates are FIFO) — the
1026
+ // spawn syscall never lands in the same loop turn ahead of the first paint.
1027
+ setImmediate(() => {
1028
+ if (!$env.PI_NO_TITLE && !this.sessionManager.getSessionName()) {
1029
+ tinyTitleClient.prewarm(this.settings.get("providers.tinyModel"));
1030
+ }
1031
+ });
1032
+
1033
+ // Initialize hooks with TUI-based UI context
1034
+ await this.initHooksAndCustomTools();
1035
+
1036
+ // Restore mode from session (e.g. plan mode on resume)
1037
+ this.session.setSessionBeforeSwitchReconciler?.(async () => {
1038
+ await this.#liveCommandController.stop();
1039
+ await this.#quiesceVibeForSessionSwitch();
1040
+ });
1041
+ this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession({ preserveActiveGoal: true }));
1042
+ await this.#reconcileModeFromSession();
1043
+
1044
+ // Brand-new sessions optionally start in plan mode when the user has made it
1045
+ // the startup default. "Brand-new" means the resolved branch carries no
1046
+ // conversation context (buildSessionContext().messages — covers messages,
1047
+ // custom messages, branch summaries, and compaction summaries) and the user
1048
+ // set no explicit `mode_change` (which #reconcileModeFromSession just
1049
+ // restored). SDK startup metadata and extension `custom` state entries are
1050
+ // ignored. This way `omp --continue` (or auto-resume) that finds no recent
1051
+ // session and creates a fresh one still honors the default, while a session
1052
+ // with restored context or an explicit mode keeps its reconciled mode. Scoped
1053
+ // to launch (not the switch reconciler above) so /new and the plan-approval →
1054
+ // execution handoff clear never get dragged back into plan mode. #enterPlanMode
1055
+ // is idempotent and self-guards against an already-active plan/goal mode; it
1056
+ // does not check plan.enabled itself.
1057
+ const hasConversationContext = this.sessionManager.buildSessionContext().messages.length > 0;
1058
+ const hasExplicitMode = this.sessionManager.getEntries().some(entry => entry.type === "mode_change");
1059
+ const isFreshSession = !hasConversationContext && !hasExplicitMode;
1060
+ if (
1061
+ isFreshSession &&
1062
+ this.session.settings.get("plan.defaultOnStartup") &&
1063
+ this.session.settings.get("plan.enabled")
1064
+ ) {
1065
+ await this.#enterPlanMode();
1066
+ }
1067
+
1068
+ // Restore unsent editor draft from previous session shutdown (Ctrl+D).
1069
+ // One-shot: consumeDraft removes the sidecar after read so the next
1070
+ // resume does not re-restore the same text.
1071
+ try {
1072
+ const draft = await this.sessionManager.consumeDraft();
1073
+ if (draft && !this.editor.getText()) {
1074
+ this.editor.setText(draft);
1075
+ this.updateEditorBorderColor();
1076
+ this.ui.requestRender();
1077
+ }
1078
+ } catch (err) {
1079
+ logger.warn("Failed to restore session draft", { error: String(err) });
1080
+ }
1081
+
1082
+ // Subscribe to agent events
1083
+ this.#subscribeToAgent();
1084
+
1085
+ this.#eventBusUnsubscribers.push(
1086
+ this.session.subscribe(event => {
1087
+ void this.#handleGoalSessionEvent(event);
1088
+ }),
1089
+ onStatusLineSessionAccentChanged(() => {
1090
+ this.#syncStatusLineSettings();
1091
+ this.#handleSessionAccentInputsChanged();
1092
+ }),
1093
+ );
1094
+ this.#eventBusUnsubscribers.push(
1095
+ onModelRolesChanged(() => {
1096
+ void this.#reapplyPlanModeModelOnRoleChange();
1097
+ }),
1098
+ );
1099
+ this.#eventBusUnsubscribers.push(
1100
+ this.session.subscribeCommandMetadataChanged(() => {
1101
+ const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
1102
+ const skillCommands = this.#rebuildSkillCommandsFromSession();
1103
+ this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
1104
+ }),
1105
+ );
1106
+ // Set up theme file watcher
1107
+ this.#eventBusUnsubscribers.push(
1108
+ onThemeChange(event => {
1109
+ this.#clearWorkingMessageAccentCache();
1110
+ clearRenderCache();
1111
+ clearMermaidCache();
1112
+ this.ui.invalidate();
1113
+ this.updateEditorBorderColor();
1114
+ if (event.ephemeral || isInsideTerminalMultiplexer()) {
1115
+ // Theme previews and multiplexer panes cannot safely replace native
1116
+ // scrollback: previews must stay non-destructive, and multiplexers
1117
+ // suppress ED3 so a forced replay would duplicate transcript history.
1118
+ this.ui.requestRender();
1119
+ return;
1120
+ }
1121
+ // Rows already committed to native scrollback are immutable; replay them
1122
+ // after a theme swap so a reader scrolled up sees the same palette.
1123
+ this.ui.requestRender(true, { clearScrollback: true });
1124
+ }),
1125
+ );
1126
+
1127
+ // Subscribe to terminal dark/light appearance changes.
1128
+ // The terminal queries background color via OSC 11 at startup and on
1129
+ // Mode 2031 notifications, computing luminance to detect dark/light.
1130
+ this.ui.terminal.onAppearanceChange(mode => {
1131
+ onTerminalAppearanceChange(mode);
1132
+ });
1133
+
1134
+ // A branch change (checkout, worktree switch, `git switch`) invalidates
1135
+ // the status-line git segments; the lazy top-border provider picks up
1136
+ // the fresh branch on the next painted frame.
1137
+ this.statusLine.watchBranch(() => {
1138
+ this.ui.requestRender();
1139
+ });
1140
+ }
1141
+
1142
+ /** Reload the title-generation system prompt override for the provided working
1143
+ * directory and stash it on the session so first-input titling
1144
+ * ({@link input-controller}) and replan-driven refresh
1145
+ * ({@link AgentSession.#refreshTitleAfterReplan}) share one source
1146
+ * ({@link discoverTitleSystemPromptFile}; issue #3734). */
1147
+ async refreshTitleSystemPrompt(cwd?: string): Promise<void> {
1148
+ const basePath = cwd ?? this.sessionManager.getCwd();
1149
+ const titleSystemPromptSource = discoverTitleSystemPromptFile(basePath);
1150
+ const resolved = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
1151
+ this.session.setTitleSystemPrompt(resolved);
1152
+ }
1153
+
1154
+ #rebuildSkillCommandsFromSession(): SlashCommand[] {
1155
+ const commands: SlashCommand[] = [];
1156
+ this.skillCommands.clear();
1157
+ if (this.session.skillsSettings?.enableSkillCommands !== false) {
1158
+ for (const skill of this.session.skills) {
1159
+ const commandName = `skill:${skill.name}`;
1160
+ this.skillCommands.set(commandName, skill);
1161
+ commands.push({ name: commandName, description: skill.description });
1162
+ }
1163
+ }
1164
+ return commands;
1165
+ }
1166
+
1167
+ /** Reload session skills and the `/skill:<name>` command list. */
1168
+ async refreshSkillState(): Promise<void> {
1169
+ await this.session.refreshSkills();
1170
+ const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
1171
+ const skillCommands = this.#rebuildSkillCommandsFromSession();
1172
+ this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
1173
+ }
1174
+
1175
+ /** Reload slash commands and autocomplete for the provided working directory. */
1176
+ async refreshSlashCommandState(cwd?: string): Promise<void> {
1177
+ const basePath = cwd ?? this.sessionManager.getCwd();
1178
+ const fileCommands = await loadSlashCommands({ cwd: basePath });
1179
+ this.fileSlashCommands = new Set(fileCommands.map(cmd => cmd.name));
1180
+ const fileSlashCommands: SlashCommand[] = fileCommands.map(cmd => ({
1181
+ name: cmd.name,
1182
+ description: cmd.description,
1183
+ }));
1184
+ // Surface discovered prompt templates in the picker. AgentSession.prompt() expands
1185
+ // `expandSlashCommand` before `expandPromptTemplate`, and builtin command
1186
+ // execution resolves aliases before template expansion. Mirror that command
1187
+ // resolution order by skipping templates whose names already appear in any
1188
+ // builtin/hook/custom/skill/file command token.
1189
+ const reservedNames = new Set<string>();
1190
+ for (const command of this.#pendingSlashCommands) {
1191
+ reservedNames.add(command.name);
1192
+ for (const alias of command.aliases ?? []) reservedNames.add(alias);
1193
+ }
1194
+ for (const command of fileSlashCommands) {
1195
+ reservedNames.add(command.name);
1196
+ for (const alias of command.aliases ?? []) reservedNames.add(alias);
1197
+ }
1198
+ const promptTemplateCommands: SlashCommand[] = this.session.promptTemplates
1199
+ .filter(template => !reservedNames.has(template.name))
1200
+ .map(template => ({
1201
+ name: template.name,
1202
+ // `PromptTemplate.description` from `loadTemplatesFromDir` already includes the
1203
+ // source suffix (e.g. "Review code (project)"), so pass it through verbatim.
1204
+ description: template.description,
1205
+ }));
1206
+ this.#baseAutocompleteProvider = this.#inputController.createAutocompleteProvider(
1207
+ [...this.#pendingSlashCommands, ...fileSlashCommands, ...promptTemplateCommands],
1208
+ basePath,
1209
+ );
1210
+ this.#applyAutocompleteProvider();
1211
+ this.session.setSlashCommands(fileCommands);
1212
+ }
1213
+
1214
+ /**
1215
+ * Rebuild the editor's autocomplete provider: the built-in provider wrapped
1216
+ * by every extension-registered factory, in registration order. A factory
1217
+ * that throws or returns a malformed provider is skipped so one broken
1218
+ * extension cannot take down core autocomplete.
1219
+ */
1220
+ #applyAutocompleteProvider(): void {
1221
+ const base = this.#baseAutocompleteProvider;
1222
+ if (!base) return;
1223
+ let provider = base;
1224
+ for (const factory of this.#autocompleteProviderFactories) {
1225
+ try {
1226
+ const wrapped = factory(provider);
1227
+ if (
1228
+ wrapped &&
1229
+ typeof wrapped.getSuggestions === "function" &&
1230
+ typeof wrapped.applyCompletion === "function"
1231
+ ) {
1232
+ provider = wrapped;
1233
+ } else {
1234
+ logger.warn("Extension autocomplete provider factory returned an invalid provider; skipping it");
1235
+ }
1236
+ } catch (error) {
1237
+ logger.warn("Extension autocomplete provider factory threw; skipping it", { error: String(error) });
1238
+ }
1239
+ }
1240
+ this.editor.setAutocompleteProvider(provider);
1241
+ }
1242
+
1243
+ /** Stack extension autocomplete behavior on top of the built-in editor provider (#4919). */
1244
+ addAutocompleteProvider(factory: AutocompleteProviderFactory): void {
1245
+ this.#autocompleteProviderFactories.push(factory);
1246
+ this.#applyAutocompleteProvider();
1247
+ }
1248
+
1249
+ /**
1250
+ * Re-point the process and every cwd-derived cache at `newCwd` after the
1251
+ * active session's working directory changed (`/move` relocation or resuming
1252
+ * a session from another project). The SessionManager's cwd MUST already
1253
+ * reflect `newCwd` before this is called.
1254
+ */
1255
+ async applyCwdChange(newCwd: string): Promise<void> {
1256
+ setProjectDir(newCwd);
1257
+ // Re-scope project settings (`.claude/settings.yml` etc.) to the new
1258
+ // directory in place so the active session and every settings reader pick
1259
+ // up the destination project's configuration.
1260
+ if (isSettingsInitialized()) {
1261
+ await settings.reloadForCwd(newCwd);
1262
+ // Reapply provider preferences from the newly-loaded settings so the
1263
+ // module-level search/image provider state reflects the destination
1264
+ // project's configuration. Without this, the previous project's
1265
+ // exclusions leak and newly-excluded providers are still used.
1266
+ applyProviderGlobalsFromSettings(settings);
1267
+ }
1268
+ // Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
1269
+ // the next prompt sees everything scoped to the new project directory.
1270
+ clearClaudePluginRootsCache();
1271
+ await this.refreshTitleSystemPrompt(newCwd);
1272
+ resetCapabilities();
1273
+ await this.refreshSkillState();
1274
+ await this.refreshSlashCommandState(newCwd);
1275
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
1276
+ this.statusLine.invalidate();
1277
+ this.ui.requestRender();
1278
+ }
1279
+
1280
+ async getUserInput(): Promise<SubmittedUserInput> {
1281
+ if (this.session.getGoalModeState()?.mode === "exiting") {
1282
+ await this.#exitGoalMode({ reason: "completed", silent: true });
1283
+ }
1284
+ const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
1285
+ this.onInputCallback = input => {
1286
+ this.onInputCallback = undefined;
1287
+ resolve(input);
1288
+ };
1289
+ this.#scheduleLoopAutoSubmit();
1290
+ this.#scheduleGoalContinuation();
1291
+
1292
+ using _ = new EventLoopKeepalive();
1293
+ return await promise;
1294
+ }
1295
+
1296
+ #scheduleLoopAutoSubmit(): void {
1297
+ this.#cancelLoopAutoSubmit();
1298
+ if (!this.loopModeEnabled || !this.loopPrompt) return;
1299
+ const prompt = this.loopPrompt;
1300
+ const loopAction = settings.get("loop.mode");
1301
+ this.#deferLoopAutoSubmit(() => {
1302
+ void this.#runLoopIteration(loopAction, prompt);
1303
+ });
1304
+ }
1305
+
1306
+ #deferLoopAutoSubmit(callback: () => void): void {
1307
+ // Brief delay so the user has a chance to press Esc between iterations.
1308
+ this.#loopAutoSubmitTimer = setTimeout(() => {
1309
+ this.#loopAutoSubmitTimer = undefined;
1310
+ if (!this.loopModeEnabled || !this.onInputCallback) return;
1311
+ callback();
1312
+ }, 800);
1313
+ }
1314
+
1315
+ #cancelLoopAutoSubmit(): void {
1316
+ if (this.#loopAutoSubmitTimer) {
1317
+ clearTimeout(this.#loopAutoSubmitTimer);
1318
+ this.#loopAutoSubmitTimer = undefined;
1319
+ }
1320
+ }
1321
+
1322
+ #scheduleGoalContinuation(): void {
1323
+ this.#cancelGoalContinuation();
1324
+ if (this.loopModeEnabled) return;
1325
+ if (!this.onInputCallback) return;
1326
+ if (!this.session.settings.get("goal.continuationModes").includes("interactive")) return;
1327
+ if (this.planModeEnabled || this.planModePaused) return;
1328
+ if (!this.goalModeEnabled || this.goalModePaused) return;
1329
+ if (this.#goalSuppressNextContinuation) return;
1330
+ if (this.#pendingSubmittedInput) return;
1331
+ if (this.editor.getText().trim().length > 0) return;
1332
+ if ((this.editor.pendingImages?.length ?? 0) > 0) return;
1333
+ const state = this.session.getGoalModeState();
1334
+ if (!state?.enabled || state.goal.status !== "active") return;
1335
+ const prompt = this.session.goalRuntime.buildContinuationPrompt();
1336
+ if (!prompt) return;
1337
+ this.#goalContinuationTimer = setTimeout(() => {
1338
+ this.#goalContinuationTimer = undefined;
1339
+ if (!this.onInputCallback) return;
1340
+ if (!this.goalModeEnabled || this.goalModePaused) return;
1341
+ // The 800ms timer can outlive the idle window that scheduled it: a
1342
+ // `/goal set` taken via the streaming branch (or any extension/hook
1343
+ // path that starts a turn while we wait) leaves the agent busy. Firing
1344
+ // the continuation now would route through `submitInteractiveInput` →
1345
+ // `promptCustomMessage` with no `streamingBehavior` and resurface
1346
+ // `AgentBusyError`. Drop this tick; `#handleGoalSessionEvent` reschedules
1347
+ // on the next `agent_end`.
1348
+ if (this.#isAutoSubmitBlocked()) return;
1349
+ if (this.#pendingSubmittedInput) return;
1350
+ if (this.editor.getText().trim().length > 0) return;
1351
+ if ((this.editor.pendingImages?.length ?? 0) > 0) return;
1352
+ const latestState = this.session.getGoalModeState();
1353
+ if (!latestState?.enabled || latestState.goal.status !== "active") return;
1354
+ this.#goalContinuationTurnInFlight = true;
1355
+ this.onInputCallback(
1356
+ this.startPendingSubmission({
1357
+ text: prompt,
1358
+ customType: "goal-continuation",
1359
+ display: false,
1360
+ }),
1361
+ );
1362
+ }, 800);
1363
+ }
1364
+
1365
+ #cancelGoalContinuation(): void {
1366
+ if (this.#goalContinuationTimer) {
1367
+ clearTimeout(this.#goalContinuationTimer);
1368
+ this.#goalContinuationTimer = undefined;
1369
+ }
1370
+ }
1371
+
1372
+ #isAutoSubmitBlocked(): boolean {
1373
+ return this.session.isStreaming || this.session.isCompacting || this.session.hasPostPromptWork;
1374
+ }
1375
+
1376
+ #submitLoopPromptWhenReady(prompt: string): void {
1377
+ if (!this.loopModeEnabled || this.loopPrompt !== prompt || !this.onInputCallback) return;
1378
+ if (isLoopDurationExpired(this.loopLimit)) {
1379
+ this.disableLoopMode("Loop time limit reached. Loop mode disabled.");
1380
+ return;
1381
+ }
1382
+ if (this.#isAutoSubmitBlocked()) {
1383
+ this.#deferLoopAutoSubmit(() => this.#submitLoopPromptWhenReady(prompt));
1384
+ return;
1385
+ }
1386
+ this.onInputCallback(this.startPendingSubmission({ text: prompt }));
1387
+ }
1388
+
1389
+ async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
1390
+ if (!this.loopModeEnabled || this.loopPrompt !== prompt || !this.onInputCallback) return;
1391
+ if (this.#isAutoSubmitBlocked()) {
1392
+ this.#deferLoopAutoSubmit(() => {
1393
+ void this.#runLoopIteration(action, prompt);
1394
+ });
1395
+ return;
1396
+ }
1397
+
1398
+ if (action === "reset" && this.vibeModeEnabled) {
1399
+ this.disableLoopMode("Exit vibe mode before using reset loops. Loop mode disabled.");
1400
+ return;
1401
+ }
1402
+
1403
+ if (!consumeLoopLimitIteration(this.loopLimit)) {
1404
+ this.disableLoopMode("Loop limit reached. Loop mode disabled.");
1405
+ return;
1406
+ }
1407
+ this.#syncLoopModeStatus();
1408
+
1409
+ if (action === "compact") {
1410
+ await this.handleCompactCommand();
1411
+ } else if (action === "reset") {
1412
+ await this.handleClearCommand();
1413
+ }
1414
+ this.#submitLoopPromptWhenReady(prompt);
1415
+ }
1416
+
1417
+ #syncLoopModeStatus(): void {
1418
+ const state: "waiting" | "running" | "paused" = this.loopModePaused
1419
+ ? "paused"
1420
+ : this.loopPrompt
1421
+ ? "running"
1422
+ : "waiting";
1423
+ this.statusLine.setLoopModeStatus(this.loopModeEnabled ? { state, limit: this.loopLimit } : undefined);
1424
+ this.ui.requestRender();
1425
+ }
1426
+
1427
+ disableLoopMode(message = "Loop mode disabled."): void {
1428
+ const wasEnabled = this.loopModeEnabled;
1429
+ this.loopModeEnabled = false;
1430
+ this.loopModePaused = false;
1431
+ this.loopPrompt = undefined;
1432
+ this.loopLimit = undefined;
1433
+ this.#cancelLoopAutoSubmit();
1434
+ this.#syncLoopModeStatus();
1435
+ if (wasEnabled) {
1436
+ this.showStatus(message);
1437
+ }
1438
+ }
1439
+
1440
+ setLoopPrompt(prompt: string): void {
1441
+ if (!this.loopModeEnabled) return;
1442
+ this.loopPrompt = prompt;
1443
+ this.loopModePaused = false;
1444
+ this.#syncLoopModeStatus();
1445
+ }
1446
+
1447
+ /**
1448
+ * Pause the loop without exiting it: drops the captured prompt and any
1449
+ * pending auto-resubmit. Loop mode stays enabled — the next prompt the
1450
+ * user submits becomes the new loop prompt and resumes iteration.
1451
+ */
1452
+ pauseLoop(): void {
1453
+ this.loopPrompt = undefined;
1454
+ this.loopModePaused = true;
1455
+ this.#cancelLoopAutoSubmit();
1456
+ this.#syncLoopModeStatus();
1457
+ }
1458
+
1459
+ async handleLoopCommand(args = ""): Promise<string | undefined> {
1460
+ if (this.loopModeEnabled) {
1461
+ this.disableLoopMode();
1462
+ return undefined;
1463
+ }
1464
+ const parsed = parseLoopLimitArgs(args);
1465
+ if (typeof parsed === "string") {
1466
+ this.showError(parsed);
1467
+ return undefined;
1468
+ }
1469
+ this.loopModeEnabled = true;
1470
+ this.loopModePaused = false;
1471
+ this.loopPrompt = undefined;
1472
+ this.loopLimit = createLoopLimitRuntime(parsed.limit);
1473
+ this.#syncLoopModeStatus();
1474
+ const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
1475
+ const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
1476
+ const tail = parsed.prompt ? "Repeating it after each turn." : "Your next prompt will repeat after each turn.";
1477
+ this.showStatus(
1478
+ `Loop mode enabled.${limitSuffix}${remainingSuffix} ${tail} Esc cancels the current iteration; /loop again to disable.`,
1479
+ );
1480
+ // Hand any inline prompt back to the dispatcher so the normal submit flow
1481
+ // runs the first iteration — it records the text as the loop prompt and
1482
+ // auto-resubmits it after each yield, identical to typing the prompt right
1483
+ // after enabling loop mode.
1484
+ return parsed.prompt;
1485
+ }
1486
+
1487
+ recordLocalSubmission(text: string, imageCount = 0): () => void {
1488
+ if (this.isKnownSlashCommand(text)) {
1489
+ return () => {};
1490
+ }
1491
+ const signature = `${text}\u0000${imageCount}`;
1492
+ this.locallySubmittedUserSignatures.add(signature);
1493
+ let disposed = false;
1494
+ return () => {
1495
+ if (disposed) return;
1496
+ disposed = true;
1497
+ this.locallySubmittedUserSignatures.delete(signature);
1498
+ };
1499
+ }
1500
+
1501
+ async withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T> {
1502
+ const dispose = this.recordLocalSubmission(text, options?.imageCount ?? 0);
1503
+ try {
1504
+ return await fn();
1505
+ } catch (err) {
1506
+ dispose();
1507
+ throw err;
1508
+ }
1509
+ }
1510
+ #captureAddedChatComponents(render: () => void): Component[] {
1511
+ const start = this.chatContainer.children.length;
1512
+ render();
1513
+ return this.chatContainer.children.slice(start);
1514
+ }
1515
+
1516
+ clearOptimisticUserMessage(): void {
1517
+ this.optimisticUserMessageSignature = undefined;
1518
+ this.#pendingSubmissionDispose?.();
1519
+ this.#pendingSubmissionDispose = undefined;
1520
+ this.#optimisticUserMessageComponents = [];
1521
+ }
1522
+
1523
+ replaceOptimisticUserMessage(
1524
+ message: AgentMessage,
1525
+ options?: { imageLinks?: readonly (string | undefined)[] },
1526
+ ): void {
1527
+ this.optimisticUserMessageSignature = undefined;
1528
+ this.#pendingSubmissionDispose?.();
1529
+ this.#pendingSubmissionDispose = undefined;
1530
+ for (const component of this.#optimisticUserMessageComponents) {
1531
+ this.chatContainer.removeChild(component);
1532
+ }
1533
+ this.#optimisticUserMessageComponents = [];
1534
+ this.addMessageToChat(message, options);
1535
+ }
1536
+
1537
+ startPendingSubmission(input: {
1538
+ text: string;
1539
+ images?: ImageContent[];
1540
+ imageLinks?: (string | undefined)[];
1541
+ customType?: string;
1542
+ display?: boolean;
1543
+ streamingBehavior?: "steer" | "followUp";
1544
+ }): SubmittedUserInput {
1545
+ const submission: SubmittedUserInput = {
1546
+ text: input.text,
1547
+ images: input.images,
1548
+ imageLinks: input.imageLinks,
1549
+ customType: input.customType,
1550
+ display: input.display,
1551
+ streamingBehavior: input.streamingBehavior,
1552
+ cancelled: false,
1553
+ started: false,
1554
+ };
1555
+ this.#pendingSubmittedInput = submission;
1556
+ if (!submission.customType) {
1557
+ this.#resetGoalContinuationSuppression();
1558
+ const imageCount = submission.images?.length ?? 0;
1559
+ this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
1560
+ this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
1561
+ this.#optimisticUserMessageComponents = this.#captureAddedChatComponents(() => {
1562
+ this.addMessageToChat(
1563
+ {
1564
+ role: "user",
1565
+ content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
1566
+ attribution: "user",
1567
+ timestamp: Date.now(),
1568
+ },
1569
+ { imageLinks: input.imageLinks },
1570
+ );
1571
+ });
1572
+ } else {
1573
+ this.clearOptimisticUserMessage();
1574
+ }
1575
+ this.editor.setText("");
1576
+ this.editor.imageLinks = undefined;
1577
+ this.ensureLoadingAnimation();
1578
+ this.ui.requestRender();
1579
+ return submission;
1580
+ }
1581
+
1582
+ cancelPendingSubmission(): boolean {
1583
+ const submission = this.#pendingSubmittedInput;
1584
+ if (!submission || submission.started) {
1585
+ return false;
1586
+ }
1587
+
1588
+ submission.cancelled = true;
1589
+ this.#pendingSubmittedInput = undefined;
1590
+ this.clearOptimisticUserMessage();
1591
+ this.#pendingWorkingMessage = undefined;
1592
+ if (submission.customType === "goal-continuation") {
1593
+ this.#goalContinuationTurnInFlight = false;
1594
+ }
1595
+ if (this.loadingAnimation) {
1596
+ this.#stopLoadingAnimation(true);
1597
+ }
1598
+ if (!submission.customType) {
1599
+ this.editor.pendingImages = submission.images ? [...submission.images] : [];
1600
+ this.editor.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
1601
+ this.editor.imageLinks = this.editor.pendingImageLinks;
1602
+ this.rebuildChatFromMessages();
1603
+ this.editor.setText(submission.text);
1604
+ }
1605
+ this.updateEditorBorderColor();
1606
+ this.ui.requestRender();
1607
+ return true;
1608
+ }
1609
+
1610
+ markPendingSubmissionStarted(input: SubmittedUserInput): boolean {
1611
+ if (this.#pendingSubmittedInput !== input || input.cancelled) {
1612
+ return false;
1613
+ }
1614
+ input.started = true;
1615
+ const annotationStateKey = this.#planReviewAnnotationStateBySubmission.get(input);
1616
+ if (annotationStateKey) {
1617
+ this.#planReviewAnnotationStateBySubmission.delete(input);
1618
+ this.#planReviewAnnotationState.delete(annotationStateKey);
1619
+ }
1620
+ return true;
1621
+ }
1622
+
1623
+ finishPendingSubmission(input: SubmittedUserInput): void {
1624
+ const wasPendingSubmission = this.#pendingSubmittedInput === input;
1625
+ const pendingSubmissionDispose = this.#pendingSubmissionDispose;
1626
+ if (wasPendingSubmission) {
1627
+ this.#pendingSubmittedInput = undefined;
1628
+ this.#pendingSubmissionDispose = undefined;
1629
+ }
1630
+ if (input.customType === "goal-continuation") {
1631
+ this.#goalContinuationTurnInFlight = false;
1632
+ }
1633
+
1634
+ if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
1635
+ this.optimisticUserMessageSignature = undefined;
1636
+ pendingSubmissionDispose?.();
1637
+ this.#optimisticUserMessageComponents = [];
1638
+ this.#pendingWorkingMessage = undefined;
1639
+ if (this.loadingAnimation) {
1640
+ this.#stopLoadingAnimation(true);
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ #computeEditorMaxHeight(): number {
1646
+ return computeEditorMaxHeight(this.ui.terminal.rows);
1647
+ }
1648
+
1649
+ #syncEditorMaxHeight(): void {
1650
+ this.editor.setMaxHeight(this.#computeEditorMaxHeight());
1651
+ }
1652
+
1653
+ #syncStatusLineSettings(): void {
1654
+ this.statusLine.updateSettings({
1655
+ preset: settings.get("statusLine.preset"),
1656
+ leftSegments: settings.get("statusLine.leftSegments"),
1657
+ rightSegments: settings.get("statusLine.rightSegments"),
1658
+ separator: settings.get("statusLine.separator"),
1659
+ showHookStatus: settings.get("statusLine.showHookStatus"),
1660
+ sessionAccent: settings.get("statusLine.sessionAccent"),
1661
+ transparent: settings.get("statusLine.transparent"),
1662
+ segmentOptions: settings.get("statusLine.segmentOptions"),
1663
+ compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
1664
+ });
1665
+ }
1666
+
1667
+ #handleSessionAccentInputsChanged(): void {
1668
+ this.#clearWorkingMessageAccentCache();
1669
+ this.statusLine.invalidate();
1670
+ this.updateEditorBorderColor();
1671
+ }
1672
+
1673
+ updateEditorBorderColor(): void {
1674
+ if (this.isBashMode) {
1675
+ this.editor.borderColor = theme.getBashModeBorderColor();
1676
+ } else if (this.isPythonMode) {
1677
+ this.editor.borderColor = theme.getPythonModeBorderColor();
1678
+ } else {
1679
+ const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
1680
+ const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
1681
+ const hex = sessionName
1682
+ ? getSessionAccentHex(sessionName, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance)
1683
+ : undefined;
1684
+ const ansi = getSessionAccentAnsi(hex);
1685
+ if (ansi) {
1686
+ this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
1687
+ } else {
1688
+ const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
1689
+ this.editor.borderColor = theme.getThinkingBorderColor(level);
1690
+ }
1691
+ }
1692
+ if (this.focusedAgentId) {
1693
+ // Focused subagent view: faint the outline so the borrowed session is
1694
+ // visually distinct from the main one.
1695
+ const base = this.editor.borderColor;
1696
+ this.editor.borderColor = (str: string) => `\x1b[2m${base(str)}\x1b[22m`;
1697
+ }
1698
+ this.ui.requestRender();
1699
+ }
1700
+
1701
+ /** Refresh the running-subagents status badge from the active local or collab registry. */
1702
+ syncRunningSubagentBadge(options: { requestRender?: boolean } = {}): void {
1703
+ const registry = getRunningSubagentBadgeRegistry(this.collabGuest);
1704
+ if (this.#agentRegistrySubscriptionTarget !== registry) {
1705
+ this.#agentRegistryUnsubscribe?.();
1706
+ this.#agentRegistrySubscriptionTarget = registry;
1707
+ this.#agentRegistryUnsubscribe = registry.onChange(() => {
1708
+ this.syncRunningSubagentBadge();
1709
+ });
1710
+ }
1711
+ const count = countRunningSubagentBadgeAgents(registry);
1712
+ this.statusLine.setSubagentCount(count);
1713
+ if (options.requestRender !== false) this.ui.requestRender();
1714
+ }
1715
+
1716
+ rebuildChatFromMessages(options: { reuseSettledComponents?: boolean } = {}): void {
1717
+ // Mid-stream rebuilds (e.g. `/shake`, theme/setting changes that touch the
1718
+ // transcript) replay only committed `state.messages`. The agent's in-flight
1719
+ // `streamMessage` and its still-pending tool calls live OUTSIDE
1720
+ // `state.messages` until `message_end`, so a plain clear+replay detaches
1721
+ // their UI components while keeping the `streamingComponent` / `pendingTools`
1722
+ // references — subsequent `message_update`/`message_end` events would then
1723
+ // update orphaned components that never re-render and the live LLM output
1724
+ // vanishes from the chat (#3656). Snapshot the in-flight components,
1725
+ // clear+replay, then re-append them in their original chat-container order
1726
+ // and restore the `pendingTools` map so streaming routes back into them.
1727
+ const liveComponents: Component[] = [];
1728
+ const livePendingTools = new Map<string, ToolExecutionHandle>();
1729
+ if (this.viewSession?.isStreaming) {
1730
+ const liveSet = new Set<Component>();
1731
+ if (this.streamingComponent) liveSet.add(this.streamingComponent);
1732
+ for (const [id, component] of this.pendingTools) {
1733
+ livePendingTools.set(id, component);
1734
+ liveSet.add(component as unknown as Component);
1735
+ }
1736
+ if (liveSet.size > 0) {
1737
+ for (const child of this.chatContainer.children) {
1738
+ if (liveSet.has(child)) liveComponents.push(child);
1739
+ }
1740
+ }
1741
+ }
1742
+ this.chatContainer.clear();
1743
+ // Live display collapses to the compacted transcript tail unless the
1744
+ // user opted into the full inline history; export/resume callers choose
1745
+ // their own mode.
1746
+ const context = this.viewSession.buildTranscriptSessionContext({
1747
+ collapseCompactedHistory: settings.get("display.collapseCompacted"),
1748
+ });
1749
+ const preservedLiveToolCallIds = new Set<string>();
1750
+ // A preserved pending-tool component whose result has already landed in
1751
+ // the replayed transcript is re-rendered by `renderSessionContext` itself
1752
+ // (the toolResult message reconstructs the block with its output). Keeping
1753
+ // it in the live set too re-appends a second identical block below the
1754
+ // replayed one — the tool call renders twice (#6516). The preservation
1755
+ // above assumes every pending-tool component is still dangling (its result
1756
+ // lives outside `state.messages`), which stops holding the instant the
1757
+ // result is persisted while the component lingers in `pendingTools` (a
1758
+ // rebuild racing tool-completion, a background/displaceable snapshot).
1759
+ // Drop the already-resolved ones and let the replay own them; only
1760
+ // genuinely in-flight (dangling, replay-stripped) calls still need
1761
+ // preserving.
1762
+ for (const message of context.messages) {
1763
+ if (message.role !== "toolResult") continue;
1764
+ const resolved = livePendingTools.get(message.toolCallId);
1765
+ if (!resolved) continue;
1766
+ // A background task's initial `async.state === "running"` result is
1767
+ // persisted while `EventController#handleToolExecutionEnd` deliberately
1768
+ // keeps its component in `pendingTools` so a later
1769
+ // `tool_execution_update`/`_end` settles it. Such a handle is still
1770
+ // live — dropping it would strand those updates on the running snapshot
1771
+ // — so keep it and let the live component retain ownership; only
1772
+ // terminal results are owned by the replay. (Cast mirrors the async
1773
+ // detail reads in tool-execution.ts / event-controller.ts.)
1774
+ const details = message.details as { async?: { state?: string } } | undefined;
1775
+ if (details?.async?.state === "running") {
1776
+ preservedLiveToolCallIds.add(message.toolCallId);
1777
+ continue;
1778
+ }
1779
+ livePendingTools.delete(message.toolCallId);
1780
+ // A `ReadToolGroupComponent` is shared by every read id it renders
1781
+ // (ui-helpers sets the same group for each collapsed read call). While a
1782
+ // sibling read id still points at it the component must stay on screen
1783
+ // and preserved — splicing it here would detach the pending read's
1784
+ // display and strand its future result on an off-screen component.
1785
+ // Splice only once no remaining pending id shares it.
1786
+ let stillShared = false;
1787
+ for (const other of livePendingTools.values()) {
1788
+ if (other === resolved) {
1789
+ stillShared = true;
1790
+ break;
1791
+ }
1792
+ }
1793
+ if (stillShared) {
1794
+ // The shared component still owns this completed member as well as
1795
+ // its pending sibling. Suppress the replay copy so the group remains
1796
+ // a single on-screen block while future results keep routing to it.
1797
+ preservedLiveToolCallIds.add(message.toolCallId);
1798
+ continue;
1799
+ }
1800
+ const index = liveComponents.indexOf(resolved as unknown as Component);
1801
+ if (index >= 0) liveComponents.splice(index, 1);
1802
+ }
1803
+ // Prune the settled-component cache to the messages this rebuild will
1804
+ // actually render. Message objects stay strongly reachable through
1805
+ // session entries for the whole session, so entries for compacted-away
1806
+ // history would otherwise pin their components' rendered layout caches
1807
+ // forever — exactly the memory a collapsed compaction used to release.
1808
+ const retained = new WeakMap<AgentMessage, Component>();
1809
+ for (const message of context.messages) {
1810
+ const component = this.transcriptMessageComponents.get(message);
1811
+ if (component) retained.set(message, component);
1812
+ }
1813
+ this.transcriptMessageComponents = retained;
1814
+ this.renderSessionContext(context, {
1815
+ reuseSettledComponents: options.reuseSettledComponents,
1816
+ preservedLiveToolCallIds,
1817
+ });
1818
+ for (const child of liveComponents) {
1819
+ this.chatContainer.addChild(child);
1820
+ }
1821
+ // `renderSessionContext` clears `pendingTools` at start AND end so the
1822
+ // reconstructed historical tool components don't leak into live tracking.
1823
+ // Restore the in-flight entries afterwards so the next streamed tool-call
1824
+ // delta is routed into the preserved component instead of stacking a
1825
+ // duplicate ToolExecutionComponent below it.
1826
+ for (const [id, component] of livePendingTools) {
1827
+ this.pendingTools.set(id, component);
1828
+ }
1829
+ // During the pre-streaming window — after `startPendingSubmission` has
1830
+ // optimistically rendered the user's message but before the user
1831
+ // `message_start` event lands it in `session` entries — any rebuild
1832
+ // (e.g. Ctrl+T toggleThinkingBlockVisibility, theme selector) would
1833
+ // otherwise erase the user's just-submitted message until the first
1834
+ // assistant token arrived (#2372). Once `message_start` fires the
1835
+ // signature is cleared by `EventController`, so this replay is a no-op
1836
+ // post-streaming and cannot duplicate.
1837
+ this.#replayOptimisticUserMessage();
1838
+ }
1839
+
1840
+ #replayOptimisticUserMessage(): void {
1841
+ if (!this.optimisticUserMessageSignature) return;
1842
+ const submission = this.#pendingSubmittedInput;
1843
+ if (!submission || submission.cancelled || submission.customType) return;
1844
+ this.#optimisticUserMessageComponents = this.#captureAddedChatComponents(() => {
1845
+ this.addMessageToChat(
1846
+ {
1847
+ role: "user",
1848
+ content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
1849
+ attribution: "user",
1850
+ timestamp: Date.now(),
1851
+ },
1852
+ { imageLinks: submission.imageLinks },
1853
+ );
1854
+ });
1855
+ }
1856
+
1857
+ #formatTodoLine(todo: TodoItem, prefix: string, matched: boolean): string {
1858
+ const checkbox = theme.checkbox;
1859
+ const marker = formatHudNoteMarker(todo.notes?.length ?? 0);
1860
+ switch (todo.status) {
1861
+ case "completed":
1862
+ return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`) + marker;
1863
+ case "in_progress":
1864
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1865
+ case "abandoned":
1866
+ return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1867
+ case "blocked":
1868
+ return theme.fg("warning", `${prefix}${checkbox.unchecked} ${todo.content} (blocked)`) + marker;
1869
+ default:
1870
+ if (matched) return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1871
+ return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1872
+ }
1873
+ }
1874
+
1875
+ #getActiveSubagentDescriptions(): string[] {
1876
+ const out: string[] = [];
1877
+ for (const session of this.#observerRegistry.getSessions()) {
1878
+ if (session.kind !== "subagent") continue;
1879
+ if (session.status !== "active") continue;
1880
+ const candidate =
1881
+ session.description?.trim() || session.progress?.description?.trim() || session.label?.trim();
1882
+ if (candidate) out.push(candidate);
1883
+ }
1884
+ return out;
1885
+ }
1886
+
1887
+ /**
1888
+ * Auto-complete any open todo (pending/in_progress/blocked) whose content
1889
+ * matches a subagent that has finished successfully. Fires on every observer
1890
+ * `onChange` so the visual state stays in sync with subagent lifecycle
1891
+ * without requiring the agent to issue a follow-up `todo`. A todo `block`ed
1892
+ * while waiting on a detached subagent is included: that subagent completing
1893
+ * is exactly the unblock signal, and blocked todos are excluded from the stop
1894
+ * reminder, so leaving it blocked would strand it silently. Failed and aborted
1895
+ * subagents are intentionally NOT auto-completed — those stay open so the user
1896
+ * (or the next agent turn) can decide what to do.
1897
+ *
1898
+ * Idempotent: only flips open tasks, never re-touches completed ones.
1899
+ */
1900
+ #reconcileTodosWithSubagents(): void {
1901
+ const completedDescs: string[] = [];
1902
+ for (const session of this.#observerRegistry.getSessions()) {
1903
+ if (session.kind !== "subagent") continue;
1904
+ if (session.status !== "completed") continue;
1905
+ const candidate =
1906
+ session.description?.trim() || session.progress?.description?.trim() || session.label?.trim();
1907
+ if (candidate) completedDescs.push(candidate);
1908
+ }
1909
+ if (completedDescs.length === 0) return;
1910
+
1911
+ let mutated = false;
1912
+ const next: TodoPhase[] = this.todoPhases.map(phase => ({
1913
+ name: phase.name,
1914
+ tasks: phase.tasks.map(task => {
1915
+ if (task.status !== "pending" && task.status !== "in_progress" && task.status !== "blocked") {
1916
+ return task;
1917
+ }
1918
+ if (!todoMatchesAnyDescription(task.content, completedDescs)) return task;
1919
+ mutated = true;
1920
+ // Drop any blocker note along with the blocked status — the wait the
1921
+ // note described is over.
1922
+ return { content: task.content, status: "completed" as const };
1923
+ }),
1924
+ }));
1925
+ if (!mutated) return;
1926
+ this.session.setTodoPhases(next);
1927
+ this.setTodos(next);
1928
+ }
1929
+
1930
+ #cancelTodoAutoClearTimer(): void {
1931
+ if (!this.#todoAutoClearTimer) return;
1932
+ clearTimeout(this.#todoAutoClearTimer);
1933
+ this.#todoAutoClearTimer = undefined;
1934
+ }
1935
+
1936
+ #isClosedTodo(task: TodoItem): boolean {
1937
+ return task.status === "completed" || task.status === "abandoned";
1938
+ }
1939
+
1940
+ #hasClosedTodos(phases: TodoPhase[]): boolean {
1941
+ return phases.some(phase => phase.tasks.some(task => this.#isClosedTodo(task)));
1942
+ }
1943
+
1944
+ #removeClosedTodos(phases: TodoPhase[]): TodoPhase[] {
1945
+ const next: TodoPhase[] = [];
1946
+ for (const phase of phases) {
1947
+ const tasks = phase.tasks.filter(task => !this.#isClosedTodo(task));
1948
+ if (tasks.length > 0) next.push({ name: phase.name, tasks });
1949
+ }
1950
+ return next;
1951
+ }
1952
+
1953
+ #syncTodoAutoClearTimer(): void {
1954
+ this.#cancelTodoAutoClearTimer();
1955
+ const delaySeconds = this.settings.get("tasks.todoClearDelay");
1956
+ if (!Number.isFinite(delaySeconds) || delaySeconds < 0 || !this.#hasClosedTodos(this.todoPhases)) return;
1957
+ if (delaySeconds === 0) {
1958
+ this.todoPhases = this.#removeClosedTodos(this.todoPhases);
1959
+ return;
1960
+ }
1961
+
1962
+ this.#todoAutoClearTimer = setTimeout(() => {
1963
+ this.#todoAutoClearTimer = undefined;
1964
+ this.todoPhases = this.#removeClosedTodos(this.todoPhases);
1965
+ this.#renderTodoList();
1966
+ this.ui.requestRender();
1967
+ }, delaySeconds * 1000);
1968
+ this.#todoAutoClearTimer.unref?.();
1969
+ }
1970
+
1971
+ /**
1972
+ * Render the ctrl+p model-role cycle chip track into its own anchored
1973
+ * container (just above the editor), mirroring the todo HUD: the container is
1974
+ * cleared and rebuilt in place on every cycle, so rapid presses or concurrent
1975
+ * chat activity can never stack duplicate tracks into the scrollback.
1976
+ */
1977
+ showModelCycleTrack(track: string): void {
1978
+ this.#renderModelCycleTrack(track);
1979
+ this.#syncModelCycleClearTimer();
1980
+ this.ui.requestRender();
1981
+ }
1982
+
1983
+ #renderModelCycleTrack(track: string | null): void {
1984
+ this.modelCycleContainer.clear();
1985
+ if (!track) return;
1986
+ this.modelCycleContainer.addChild(new Spacer(1));
1987
+ this.modelCycleContainer.addChild(new Text(track, 1, 0));
1988
+ }
1989
+
1990
+ #cancelModelCycleClearTimer(): void {
1991
+ if (!this.#modelCycleClearTimer) return;
1992
+ clearTimeout(this.#modelCycleClearTimer);
1993
+ this.#modelCycleClearTimer = undefined;
1994
+ }
1995
+
1996
+ #syncModelCycleClearTimer(): void {
1997
+ this.#cancelModelCycleClearTimer();
1998
+ this.#modelCycleClearTimer = setTimeout(() => {
1999
+ this.#modelCycleClearTimer = undefined;
2000
+ this.#renderModelCycleTrack(null);
2001
+ this.ui.requestRender();
2002
+ }, MODEL_CYCLE_TRACK_CLEAR_MS);
2003
+ this.#modelCycleClearTimer.unref?.();
2004
+ }
2005
+
2006
+ #getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
2007
+ const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
2008
+ const active = nonEmpty.find(phase =>
2009
+ phase.tasks.some(task => task.status === "pending" || task.status === "in_progress"),
2010
+ );
2011
+ return active ?? nonEmpty[nonEmpty.length - 1];
2012
+ }
2013
+
2014
+ #scheduleObserverUiSync(kind: SessionObserverChangeKind): void {
2015
+ if (kind !== "progress") {
2016
+ this.#observerUiSyncNeedsTodoReconcile = true;
2017
+ }
2018
+ if (this.#observerUiSyncTimer) return;
2019
+ this.#observerUiSyncTimer = setTimeout(() => {
2020
+ this.#observerUiSyncTimer = undefined;
2021
+ this.#flushObserverUiSync();
2022
+ }, SUBAGENT_OBSERVER_UI_COALESCE_MS);
2023
+ this.#observerUiSyncTimer.unref?.();
2024
+ }
2025
+
2026
+ #flushObserverUiSync(): void {
2027
+ this.syncRunningSubagentBadge({ requestRender: false });
2028
+ if (this.#observerUiSyncNeedsTodoReconcile) {
2029
+ this.#observerUiSyncNeedsTodoReconcile = false;
2030
+ this.#reconcileTodosWithSubagents();
2031
+ }
2032
+ this.#syncTodoAutoClearTimer();
2033
+ this.#renderTodoList();
2034
+ this.#renderSubagentList();
2035
+ this.ui.requestRender();
2036
+ }
2037
+
2038
+ #cancelObserverUiSyncTimer(): void {
2039
+ if (this.#observerUiSyncTimer) {
2040
+ clearTimeout(this.#observerUiSyncTimer);
2041
+ this.#observerUiSyncTimer = undefined;
2042
+ }
2043
+ this.#observerUiSyncNeedsTodoReconcile = false;
2044
+ }
2045
+
2046
+ #renderTodoList(): void {
2047
+ this.todoContainer.clear();
2048
+ const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
2049
+ if (phases.length === 0) return;
2050
+ const expanded = this.todoExpanded;
2051
+ const multiPhase = phases.length > 1;
2052
+ const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
2053
+ // Fixed budgets keep the HUD bounded regardless of plan size / progress.
2054
+ const subsequentStageCap = 4; // stages shown after the active one (header count implies the rest)
2055
+ const activeTaskCap = 5; // open tasks previewed for the active stage
2056
+
2057
+ const activeDescs = this.#getActiveSubagentDescriptions();
2058
+ // A pending todo "lights up" (accent) when an in-flight subagent is doing
2059
+ // its work, matched by normalized content overlap.
2060
+ const isMatched = (todo: TodoItem): boolean =>
2061
+ activeDescs.length > 0 && todoMatchesAnyDescription(todo.content, activeDescs);
2062
+
2063
+ // Task subtree for a phase. Collapsed runs the shared walking-viewport
2064
+ // policy (completed/abandoned omitted, active work pulled to the head,
2065
+ // then following pending tasks) so the HUD and the transient tool result
2066
+ // can never disagree about the current work (#5873). Expanded lists all.
2067
+ const renderTasks = (phase: TodoPhase): string[] => {
2068
+ if (expanded) {
2069
+ return renderTreeList(
2070
+ {
2071
+ items: phase.tasks,
2072
+ expanded: true,
2073
+ renderItem: todo => this.#formatTodoLine(todo, "", isMatched(todo)),
2074
+ },
2075
+ theme,
2076
+ );
2077
+ }
2078
+ const selection = selectCollapsedTodos(phase.tasks, isMatched, activeTaskCap);
2079
+ return renderTreeList(
2080
+ {
2081
+ items: selection.items,
2082
+ itemType: "task",
2083
+ trailingSummary: selection.summary,
2084
+ renderItem: todo => this.#formatTodoLine(todo, "", isMatched(todo)),
2085
+ },
2086
+ theme,
2087
+ );
2088
+ };
2089
+
2090
+ // One phase node. The active stage is highlighted with normal-brightness task
2091
+ // progress; other stages render their whole row (name + progress) in the
2092
+ // brighter muted gray. The root header carries overall stage progression.
2093
+ const renderPhase = (phase: TodoPhase, oneBased: number, isActive: boolean): string | string[] => {
2094
+ const label = multiPhase ? formatPhaseDisplayName(phase.name, oneBased) : phase.name;
2095
+ const done = phase.tasks.filter(t => t.status === "completed").length;
2096
+ const progress = ` · ${done}/${phase.tasks.length}`;
2097
+ if (!isActive) {
2098
+ const header = theme.fg("muted", label) + theme.fg("dim", progress);
2099
+ return expanded ? [header, ...renderTasks(phase)] : header;
2100
+ }
2101
+ const header = theme.bold(theme.fg("accent", label)) + theme.fg("dim", progress);
2102
+ return [header, ...renderTasks(phase)];
2103
+ };
2104
+
2105
+ // Collapsed: active stage + a bounded number of following stages (the
2106
+ // header's "n/total" count implies any not shown). Expanded: every stage
2107
+ // from the top. Roman numerals stay tied to the real phase index.
2108
+ const baseIdx = expanded ? 0 : activeIdx;
2109
+ const phaseSlice = expanded ? phases.slice(baseIdx) : phases.slice(baseIdx, baseIdx + 1 + subsequentStageCap);
2110
+ const phaseTreeLines = renderTreeList(
2111
+ {
2112
+ items: phaseSlice,
2113
+ expanded: true,
2114
+ renderItem: (phase, ctx) => renderPhase(phase, baseIdx + ctx.index + 1, baseIdx + ctx.index === activeIdx),
2115
+ },
2116
+ theme,
2117
+ );
2118
+
2119
+ // Header carries overall stage progression, e.g. "Todos · 1/8".
2120
+ const root =
2121
+ theme.bold(theme.fg("accent", "Todos")) +
2122
+ (multiPhase ? theme.fg("dim", ` · ${activeIdx + 1}/${phases.length}`) : "");
2123
+ const lines = ["", root, ...phaseTreeLines.map(line => ` ${line}`)];
2124
+ this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
2125
+ }
2126
+
2127
+ /**
2128
+ * Anchored HUD of in-flight subagents, mirroring the Todos block above the
2129
+ * editor. Driven entirely by observer-registry change events, so rows appear
2130
+ * on spawn and the whole block clears itself once the last subagent leaves
2131
+ * the "active" state.
2132
+ */
2133
+ #renderSubagentList(): void {
2134
+ this.subagentContainer.clear();
2135
+ const lines = renderSubagentHudLines(this.#observerRegistry.getSessions(), this.ui.terminal.columns);
2136
+ if (lines.length === 0) return;
2137
+ this.subagentContainer.addChild(new Text(lines.join("\n"), 1, 0));
2138
+ }
2139
+
2140
+ async #loadTodoList(): Promise<void> {
2141
+ this.todoPhases = this.session.getTodoPhases();
2142
+ this.#syncTodoAutoClearTimer();
2143
+ this.#renderTodoList();
2144
+ }
2145
+
2146
+ async #getPlanFilePath(): Promise<string> {
2147
+ return this.session.getPlanReferencePath() || "local://PLAN.md";
2148
+ }
2149
+
2150
+ #resolvePlanFilePath(planFilePath: string): string {
2151
+ if (planFilePath.startsWith("local:")) {
2152
+ const normalized = normalizeLocalScheme(planFilePath);
2153
+ return resolveLocalUrlToPath(normalized, {
2154
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
2155
+ getSessionId: () => this.sessionManager.getSessionId(),
2156
+ });
2157
+ }
2158
+ return path.resolve(this.sessionManager.getCwd(), planFilePath);
2159
+ }
2160
+
2161
+ #updatePlanModeStatus(): void {
2162
+ const status =
2163
+ this.planModeEnabled || this.planModePaused
2164
+ ? {
2165
+ enabled: this.planModeEnabled,
2166
+ paused: this.planModePaused,
2167
+ }
2168
+ : undefined;
2169
+ this.statusLine.setPlanModeStatus(status);
2170
+ this.ui.requestRender();
2171
+ }
2172
+
2173
+ #updateVibeModeStatus(): void {
2174
+ this.statusLine.setVibeModeStatus(this.vibeModeEnabled ? { enabled: true } : undefined);
2175
+ this.ui.requestRender();
2176
+ }
2177
+
2178
+ #vibeParentSession(): VibeParentSession {
2179
+ return {
2180
+ getAgentId: () => this.session.getAgentId() ?? null,
2181
+ getSessionId: () => this.sessionManager.getSessionId(),
2182
+ getSessionFile: () => this.sessionManager.getSessionFile() ?? null,
2183
+ sessionManager: this.sessionManager,
2184
+ asyncJobManager: this.session.asyncJobManager,
2185
+ settings: this.session.settings,
2186
+ // Resolve restored/switched-to workers against this session's active model
2187
+ // (same as the spawn-path ToolSession), not the settings default. This is
2188
+ // the primary fallback in resolveAgentModelPatterns, so the `good` worker's
2189
+ // pi/task inheritance tracks the reopened session's model.
2190
+ getActiveModelString: () => (this.session.model ? formatModelString(this.session.model) : undefined),
2191
+ };
2192
+ }
2193
+
2194
+ async #quiesceVibeForSessionSwitch(): Promise<void> {
2195
+ const ownerScope = this.#vibeModeOwnerScope;
2196
+ if (!this.vibeModeEnabled || !ownerScope) return;
2197
+ await VibeSessionRegistry.global().suspendScope(ownerScope, this.session.asyncJobManager);
2198
+ this.#vibeScopeSuspendedForSwitch = true;
2199
+ }
2200
+
2201
+ #updateGoalModeStatus(): void {
2202
+ const status =
2203
+ this.goalModeEnabled || this.goalModePaused
2204
+ ? { enabled: this.goalModeEnabled, paused: this.goalModePaused }
2205
+ : undefined;
2206
+ this.statusLine.setGoalModeStatus(status);
2207
+ this.ui.requestRender();
2208
+ }
2209
+
2210
+ #resetGoalContinuationSuppression(): void {
2211
+ this.#goalSuppressNextContinuation = false;
2212
+ }
2213
+
2214
+ #getPausedGoalState(): GoalModeState | undefined {
2215
+ const state = this.session.getGoalModeState();
2216
+ if (!state?.goal || state.enabled || state.goal.status !== "paused") {
2217
+ return undefined;
2218
+ }
2219
+ return state;
2220
+ }
2221
+
2222
+ #goalFromModeData(modeData: SessionContext["modeData"]): Goal | undefined {
2223
+ const goal = modeData?.goal;
2224
+ if (!goal || typeof goal !== "object") return undefined;
2225
+ const value = goal as Record<string, unknown>;
2226
+ if (
2227
+ typeof value.id !== "string" ||
2228
+ typeof value.objective !== "string" ||
2229
+ typeof value.status !== "string" ||
2230
+ typeof value.tokensUsed !== "number" ||
2231
+ typeof value.timeUsedSeconds !== "number" ||
2232
+ typeof value.createdAt !== "number" ||
2233
+ typeof value.updatedAt !== "number"
2234
+ ) {
2235
+ return undefined;
2236
+ }
2237
+ return {
2238
+ id: value.id,
2239
+ objective: value.objective,
2240
+ status: value.status as Goal["status"],
2241
+ tokenBudget: typeof value.tokenBudget === "number" ? value.tokenBudget : undefined,
2242
+ tokensUsed: value.tokensUsed,
2243
+ timeUsedSeconds: value.timeUsedSeconds,
2244
+ createdAt: value.createdAt,
2245
+ updatedAt: value.updatedAt,
2246
+ };
2247
+ }
2248
+
2249
+ async #handleGoalSessionEvent(event: AgentSessionEvent): Promise<void> {
2250
+ if (event.type === "agent_start") {
2251
+ this.#goalTurnHadToolCalls = false;
2252
+ this.#cancelGoalContinuation();
2253
+ return;
2254
+ }
2255
+ if (event.type === "tool_execution_start") {
2256
+ this.#goalTurnHadToolCalls = true;
2257
+ if (!this.#goalContinuationTurnInFlight) {
2258
+ this.#resetGoalContinuationSuppression();
2259
+ }
2260
+ return;
2261
+ }
2262
+ if (event.type === "message_start" && event.message.role === "user" && !event.message.synthetic) {
2263
+ this.#resetGoalContinuationSuppression();
2264
+ return;
2265
+ }
2266
+ if (event.type === "goal_updated") {
2267
+ // Handle drop before clearing goalModeEnabled so #exitGoalMode can
2268
+ // still restore the previous tool set while the flag is true.
2269
+ if (event.state?.goal?.status === "dropped") {
2270
+ await this.#exitGoalMode({ reason: "dropped", silent: true });
2271
+ return;
2272
+ }
2273
+ this.goalModeEnabled = event.state?.enabled === true;
2274
+ this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
2275
+ if (!event.state?.enabled) {
2276
+ this.#cancelGoalContinuation();
2277
+ }
2278
+ this.#updateGoalModeStatus();
2279
+ return;
2280
+ }
2281
+ if (event.type !== "agent_end") {
2282
+ return;
2283
+ }
2284
+ if (this.#goalContinuationTurnInFlight) {
2285
+ this.#goalSuppressNextContinuation = !this.#goalTurnHadToolCalls;
2286
+ this.#goalContinuationTurnInFlight = false;
2287
+ }
2288
+ if (this.session.getGoalModeState()?.mode === "exiting") {
2289
+ await this.#exitGoalMode({ reason: "completed", silent: true });
2290
+ return;
2291
+ }
2292
+ this.#scheduleGoalContinuation();
2293
+ }
2294
+
2295
+ async #applyPlanModeModel(): Promise<void> {
2296
+ const resolved = this.session.resolveRoleModelWithThinking("plan");
2297
+ if (!resolved.model) return;
2298
+
2299
+ const currentModel = this.session.model;
2300
+ // Capture the pre-plan model so #exitPlanMode can restore it. Only the
2301
+ // entry path records this — a mid-planning role change (below) leaves the
2302
+ // active model on the plan role, so overwriting here would restore the old
2303
+ // plan model instead of the user's real pre-plan model.
2304
+ this.#planModePreviousModelState = currentModel
2305
+ ? { model: currentModel, thinkingLevel: this.session.configuredThinkingLevel() }
2306
+ : undefined;
2307
+
2308
+ await this.#applyPlanModelTransition(currentModel, resolved);
2309
+ }
2310
+
2311
+ /**
2312
+ * Re-resolve the `plan` role and move the active model onto it. Fires when
2313
+ * the plan role is reassigned while plan mode is active: the active model IS
2314
+ * the plan model there, so a settings-only change would otherwise leave the
2315
+ * current turn on the model plan mode was entered with (issue #5657). No-op
2316
+ * outside plan mode — role reassignment for an inactive role only touches
2317
+ * settings.
2318
+ */
2319
+ async #reapplyPlanModeModelOnRoleChange(): Promise<void> {
2320
+ if (!this.planModeEnabled) return;
2321
+ const resolved = this.session.resolveRoleModelWithThinking("plan");
2322
+ if (!resolved.model) {
2323
+ this.#clearPendingPlanModelSwitch();
2324
+ return;
2325
+ }
2326
+ await this.#applyPlanModelTransition(this.session.model, resolved);
2327
+ }
2328
+
2329
+ /**
2330
+ * Drop a stale deferred switch that was queued for a previous plan-role
2331
+ * assignment. Other deferred switches (such as restoring the pre-plan
2332
+ * model) remain intact.
2333
+ */
2334
+ #clearPendingPlanModelSwitch(): void {
2335
+ if (!this.#pendingPlanModelSwitch) return;
2336
+ this.#pendingModelSwitch = undefined;
2337
+ this.#pendingPlanModelSwitch = false;
2338
+ }
2339
+
2340
+ /** Apply (or defer) the model/thinking change implied by the resolved plan role. */
2341
+ async #applyPlanModelTransition(currentModel: Model | undefined, resolved: ResolvedModelRoleValue): Promise<void> {
2342
+ const transition = resolvePlanModelTransition(currentModel, resolved, this.session.isStreaming);
2343
+ if (transition.kind !== "apply" || !transition.deferred) {
2344
+ this.#clearPendingPlanModelSwitch();
2345
+ }
2346
+ switch (transition.kind) {
2347
+ case "none":
2348
+ return;
2349
+ case "thinking":
2350
+ this.session.setThinkingLevel(transition.thinkingLevel);
2351
+ return;
2352
+ case "apply":
2353
+ if (transition.deferred) {
2354
+ this.#pendingModelSwitch = { model: transition.model, thinkingLevel: transition.thinkingLevel };
2355
+ this.#pendingPlanModelSwitch = true;
2356
+ return;
2357
+ }
2358
+ try {
2359
+ await this.session.setModelTemporary(transition.model, transition.thinkingLevel);
2360
+ } catch (error) {
2361
+ this.showWarning(
2362
+ `Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
2363
+ );
2364
+ }
2365
+ return;
2366
+ }
2367
+ }
2368
+
2369
+ /** Apply any deferred model switch after the current stream ends. */
2370
+ async flushPendingModelSwitch(): Promise<void> {
2371
+ const pending = this.#pendingModelSwitch;
2372
+ this.#pendingModelSwitch = undefined;
2373
+ this.#pendingPlanModelSwitch = false;
2374
+ if (!pending) return;
2375
+ try {
2376
+ await this.session.setModelTemporary(pending.model, pending.thinkingLevel);
2377
+ } catch (error) {
2378
+ this.showWarning(
2379
+ `Failed to switch model after streaming: ${error instanceof Error ? error.message : String(error)}`,
2380
+ );
2381
+ }
2382
+ }
2383
+
2384
+ async #clearTransientModeState(options?: {
2385
+ preserveVibe?: boolean;
2386
+ vibeScopeAlreadySuspended?: boolean;
2387
+ }): Promise<void> {
2388
+ if (this.planModeEnabled || this.planModePaused) {
2389
+ this.session.setPlanModeState(undefined);
2390
+ try {
2391
+ if (this.#planModePreviousTools !== undefined) {
2392
+ await this.session.setActiveToolsByName(this.#planModePreviousTools);
2393
+ }
2394
+ } finally {
2395
+ this.session.setPlanProposalHandler?.(null);
2396
+ this.planModeEnabled = false;
2397
+ this.planModePaused = false;
2398
+ this.planModePlanFilePath = undefined;
2399
+ this.#planModePreviousTools = undefined;
2400
+ this.#planModePreviousModelState = undefined;
2401
+ this.#pendingModelSwitch = undefined;
2402
+ this.#pendingPlanModelSwitch = false;
2403
+ this.#planModeHasEntered = false;
2404
+ this.#updatePlanModeStatus();
2405
+ }
2406
+ }
2407
+
2408
+ if (this.goalModeEnabled || this.goalModePaused) {
2409
+ if (this.#goalModePreviousTools !== undefined) {
2410
+ await this.session.setActiveToolsByName(this.#goalModePreviousTools);
2411
+ }
2412
+ this.session.setGoalModeState(undefined);
2413
+ this.goalModeEnabled = false;
2414
+ this.goalModePaused = false;
2415
+ this.#goalModePreviousTools = undefined;
2416
+ this.#goalTurnHadToolCalls = false;
2417
+ this.#goalContinuationTurnInFlight = false;
2418
+ this.#goalSuppressNextContinuation = false;
2419
+ this.#cancelGoalContinuation();
2420
+ this.#updateGoalModeStatus();
2421
+ }
2422
+
2423
+ if (this.vibeModeEnabled && !options?.preserveVibe) {
2424
+ const ownerScope = this.#vibeModeOwnerScope;
2425
+ // This runs only from #reconcileModeFromSession, i.e. after switchSession
2426
+ // already loaded and restored the target session's active tools. The
2427
+ // #vibeModePreviousTools snapshot belongs to the SOURCE session, so
2428
+ // applying it here would clobber the target's tools — strip only the
2429
+ // transient vibe tools and keep the target's active set intact.
2430
+ await this.session.removeVibeToolsPreservingActive();
2431
+ this.session.setVibeModeState(undefined);
2432
+ this.vibeModeEnabled = false;
2433
+ this.#vibeModePreviousTools = undefined;
2434
+ this.#vibeModeOwnerScope = undefined;
2435
+ if (ownerScope && !options?.vibeScopeAlreadySuspended) {
2436
+ await VibeSessionRegistry.global().suspendScope(ownerScope, this.session.asyncJobManager);
2437
+ }
2438
+ this.#updateVibeModeStatus();
2439
+ }
2440
+ }
2441
+
2442
+ /** Reconcile mode state from session entries on resume/switch. */
2443
+ async #reconcileModeFromSession(options?: { preserveActiveGoal?: boolean }): Promise<void> {
2444
+ const vibeScopeAlreadySuspended = this.#vibeScopeSuspendedForSwitch;
2445
+ this.#vibeScopeSuspendedForSwitch = false;
2446
+ const sessionContext = this.sessionManager.buildSessionContext();
2447
+ const vibeSession = this.#vibeParentSession();
2448
+ const targetVibeScope = VibeSessionRegistry.global().ownerScope(vibeSession);
2449
+ const preserveVibe =
2450
+ this.vibeModeEnabled &&
2451
+ sessionContext.mode === "vibe" &&
2452
+ this.#vibeModeOwnerScope?.ownerId === targetVibeScope.ownerId &&
2453
+ this.#vibeModeOwnerScope.parentSessionId === targetVibeScope.parentSessionId &&
2454
+ this.#vibeModeOwnerScope.parentSessionFile === targetVibeScope.parentSessionFile;
2455
+ await this.#clearTransientModeState({ preserveVibe, vibeScopeAlreadySuspended });
2456
+ await VibeSessionRegistry.global().rehydrate(vibeSession);
2457
+ const goalEnabled = this.session.settings.get("goal.enabled");
2458
+ if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
2459
+ this.session.goalRuntime.clearAccounting();
2460
+ this.sessionManager.appendModeChange("none");
2461
+ return;
2462
+ }
2463
+ if (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused") {
2464
+ const goal = this.#goalFromModeData(sessionContext.modeData);
2465
+ if (!goal) {
2466
+ this.sessionManager.appendModeChange("none");
2467
+ return;
2468
+ }
2469
+ this.session.setGoalModeState({
2470
+ enabled: sessionContext.mode === "goal",
2471
+ mode: "active",
2472
+ goal,
2473
+ });
2474
+ const restored = await this.session.goalRuntime.onThreadResumed({
2475
+ preserveActiveGoal: options?.preserveActiveGoal,
2476
+ });
2477
+ this.goalModeEnabled = restored?.enabled === true;
2478
+ this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
2479
+ // sdk.ts excludes "goal" from the initial active tool set unconditionally.
2480
+ // Re-add it now so the agent can call resume, complete, or drop on this goal.
2481
+ if (restored?.goal) {
2482
+ const previousTools = this.session.getEnabledToolNames().filter(name => name !== "goal");
2483
+ this.#goalModePreviousTools = previousTools;
2484
+ await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
2485
+ }
2486
+ this.#updateGoalModeStatus();
2487
+ return;
2488
+ }
2489
+ this.session.goalRuntime.clearAccounting();
2490
+ if (sessionContext.mode === "vibe") {
2491
+ if (!preserveVibe) await this.#enterVibeMode({ persistModeChange: false });
2492
+ return;
2493
+ }
2494
+ if (!this.session.settings.get("plan.enabled")) {
2495
+ // Clear stale plan/plan_paused mode so re-enabling the setting
2496
+ // later doesn't unexpectedly restore an old plan session.
2497
+ if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused") {
2498
+ this.sessionManager.appendModeChange("none");
2499
+ }
2500
+ return;
2501
+ }
2502
+ if (sessionContext.mode === "plan") {
2503
+ const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
2504
+ await this.#enterPlanMode({ planFilePath, preserveRestoredModel: true });
2505
+ } else if (sessionContext.mode === "plan_paused") {
2506
+ this.planModePaused = true;
2507
+ this.#planModeHasEntered = true;
2508
+ this.#updatePlanModeStatus();
2509
+ }
2510
+ }
2511
+
2512
+ async #enterPlanMode(options?: {
2513
+ planFilePath?: string;
2514
+ workflow?: "parallel" | "iterative";
2515
+ preserveRestoredModel?: boolean;
2516
+ }): Promise<void> {
2517
+ if (this.planModeEnabled) {
2518
+ return;
2519
+ }
2520
+ if (this.goalModeEnabled || this.goalModePaused) {
2521
+ this.showWarning("Exit goal mode first.");
2522
+ return;
2523
+ }
2524
+ if (this.vibeModeEnabled) {
2525
+ this.showWarning("Exit vibe mode first.");
2526
+ return;
2527
+ }
2528
+
2529
+ this.planModePaused = false;
2530
+
2531
+ const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
2532
+ const previousTools = this.session.getEnabledToolNames();
2533
+ // `plan-mode-active.md` instructs the agent to draft the plan file with
2534
+ // `write` and refine it with `edit`, and plan approval itself is a `write`
2535
+ // to `xd://propose`. Both must be in the active set or the agent falls
2536
+ // back to `edit` on a non-existent file and stalls — and cannot submit the plan.
2537
+ // `edit` is an essential built-in and always ships top-level; re-activate
2538
+ // `write` here only when the current registry entry is the built-in write
2539
+ // tool (issue #3165). A shadowing extension tool named `write` must stay
2540
+ // inactive because plan mode's read-only guarantee relies on the built-in
2541
+ // write/edit guard. The standing handler below consumes plan-approval
2542
+ // dispatches.
2543
+ const planAugmentations: string[] = [];
2544
+ if (this.session.hasBuiltInTool("write")) {
2545
+ planAugmentations.push("write");
2546
+ }
2547
+ const uniquePlanTools = [...new Set([...previousTools, ...planAugmentations])];
2548
+
2549
+ this.#planModePreviousTools = previousTools;
2550
+ this.planModePlanFilePath = planFilePath;
2551
+ this.planModeEnabled = true;
2552
+ // Suppress cache-miss marker on the next turn: plan mode changes the system
2553
+ // prompt, which predictably invalidates the cache.
2554
+ this.lastAssistantUsage = undefined;
2555
+
2556
+ await this.session.setActiveToolsByName(uniquePlanTools);
2557
+ this.session.setPlanModeState({
2558
+ enabled: true,
2559
+ planFilePath,
2560
+ workflow: options?.workflow ?? "parallel",
2561
+ reentry: this.#planModeHasEntered,
2562
+ });
2563
+ this.session.setPlanProposalHandler?.(title => this.session.preparePlanForReview(title));
2564
+ if (this.session.isStreaming) {
2565
+ await this.session.sendPlanModeContext({ deliverAs: "steer" });
2566
+ }
2567
+ this.#planModeHasEntered = true;
2568
+ // Session loading already restored the model recorded in the journal.
2569
+ // Reapplying today's plan role here would replace a CLI/session-specific
2570
+ // selection with current config during --resume or an in-process switch.
2571
+ if (!options?.preserveRestoredModel) {
2572
+ await this.#applyPlanModeModel();
2573
+ }
2574
+ this.#updatePlanModeStatus();
2575
+ this.sessionManager.appendModeChange("plan", { planFilePath });
2576
+ this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
2577
+ }
2578
+
2579
+ async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?: ConfiguredThinkingLevel }): Promise<void> {
2580
+ if (modelsAreEqual(this.session.model, prev.model)) {
2581
+ // Same model — only thinking level may differ. Avoid setModelTemporary()
2582
+ // which would reset provider-side sessions and break continuity.
2583
+ this.session.setThinkingLevel(prev.thinkingLevel);
2584
+ } else if (this.session.isStreaming) {
2585
+ this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
2586
+ this.#pendingPlanModelSwitch = false;
2587
+ } else {
2588
+ await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
2589
+ }
2590
+ }
2591
+
2592
+ /**
2593
+ * Idempotent post-compaction model transition for the plan-approval compact
2594
+ * path. The deferred pre-plan state is consumed on first application, so a
2595
+ * second call (the before-flush hook vs. the short-circuit fallback) is a
2596
+ * no-op. "failed" intentionally stays on the plan model — the context is
2597
+ * intact and we dispatch best-effort.
2598
+ */
2599
+ async #applyDeferredPlanModelTransition(
2600
+ outcome: CompactionOutcome | undefined,
2601
+ executionModel: ResolvedRoleModel | undefined,
2602
+ ): Promise<void> {
2603
+ const deferredPrev = this.#planModePreviousModelState;
2604
+ if (deferredPrev === undefined || outcome === "failed") return;
2605
+ this.#planModePreviousModelState = undefined;
2606
+ if (executionModel) {
2607
+ await this.#applyPlanExecutionModel(executionModel);
2608
+ } else {
2609
+ await this.#restorePlanPreviousModel(deferredPrev);
2610
+ }
2611
+ }
2612
+
2613
+ async #exitPlanMode(options?: { silent?: boolean; paused?: boolean; deferModelRestore?: boolean }): Promise<void> {
2614
+ if (!this.planModeEnabled) {
2615
+ return;
2616
+ }
2617
+
2618
+ const planModeState = this.session.getPlanModeState();
2619
+ const planModeTools = this.session.getEnabledToolNames();
2620
+ const planModeMountedTools = this.session.getMountedXdevToolNames();
2621
+ const planModeModelState = this.session.model
2622
+ ? { model: this.session.model, thinkingLevel: this.session.configuredThinkingLevel() }
2623
+ : undefined;
2624
+ this.session.setPlanModeState(undefined);
2625
+ try {
2626
+ if (this.#planModePreviousTools !== undefined) {
2627
+ await this.session.setActiveToolsByName(this.#planModePreviousTools);
2628
+ }
2629
+ if (this.#planModePreviousModelState && !options?.deferModelRestore) {
2630
+ await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
2631
+ }
2632
+ // If #applyPlanModeModel queued a deferred switch to the plan-role model
2633
+ // (because the session was streaming on entry), drop it now: we are
2634
+ // leaving plan mode, so flushing it on the next agent_end would land the
2635
+ // session on the plan-role model after the user has exited plan mode
2636
+ // (issue #816). This runs even when deferModelRestore is set
2637
+ // (compact-approval path): otherwise the stale plan switch survives and
2638
+ // flushPendingModelSwitch() later clobbers the restored/execution model.
2639
+ if (this.#planModePreviousModelState) this.#clearPendingPlanModelSwitch();
2640
+ } catch (error) {
2641
+ this.session.setPlanModeState(planModeState);
2642
+ if (
2643
+ planModeModelState &&
2644
+ (!modelsAreEqual(this.session.model, planModeModelState.model) ||
2645
+ this.session.configuredThinkingLevel() !== planModeModelState.thinkingLevel)
2646
+ ) {
2647
+ try {
2648
+ await this.#restorePlanPreviousModel(planModeModelState);
2649
+ } catch (rollbackError) {
2650
+ logger.warn("Failed to restore plan model after plan exit failure", { error: String(rollbackError) });
2651
+ }
2652
+ }
2653
+ const enabledTools = this.session.getEnabledToolNames();
2654
+ const mountedTools = this.session.getMountedXdevToolNames();
2655
+ if (
2656
+ enabledTools.length !== planModeTools.length ||
2657
+ enabledTools.some((name, index) => name !== planModeTools[index]) ||
2658
+ mountedTools.length !== planModeMountedTools.length ||
2659
+ mountedTools.some((name, index) => name !== planModeMountedTools[index])
2660
+ ) {
2661
+ try {
2662
+ await this.session.setActiveToolPresentation(planModeTools, planModeMountedTools);
2663
+ } catch (rollbackError) {
2664
+ logger.warn("Failed to restore plan tools after plan exit failure", { error: String(rollbackError) });
2665
+ }
2666
+ }
2667
+ throw error;
2668
+ }
2669
+ this.session.setPlanProposalHandler?.(null);
2670
+ this.planModeEnabled = false;
2671
+ // Suppress cache-miss marker on the next turn: plan exit changes the system
2672
+ // prompt, which predictably invalidates the cache.
2673
+ this.lastAssistantUsage = undefined;
2674
+ this.planModePaused = options?.paused ?? false;
2675
+ this.planModePlanFilePath = undefined;
2676
+ this.#planModePreviousTools = undefined;
2677
+ if (!options?.deferModelRestore) this.#planModePreviousModelState = undefined;
2678
+ this.#updatePlanModeStatus();
2679
+ const paused = options?.paused ?? false;
2680
+ this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
2681
+ if (!options?.silent) {
2682
+ this.showStatus(paused ? "Plan mode paused." : "Plan mode disabled.");
2683
+ }
2684
+ }
2685
+
2686
+ async #enterGoalMode(options: { objective?: string; resume?: boolean; silent?: boolean }): Promise<void> {
2687
+ if (this.goalModeEnabled) {
2688
+ return;
2689
+ }
2690
+ if (this.planModeEnabled || this.planModePaused) {
2691
+ this.showWarning("Exit plan mode first.");
2692
+ return;
2693
+ }
2694
+ if (this.vibeModeEnabled) {
2695
+ this.showWarning("Exit vibe mode first.");
2696
+ return;
2697
+ }
2698
+ const previousTools = this.session.getEnabledToolNames().filter(name => name !== "goal");
2699
+ const goalTools = [...new Set([...previousTools, "goal"])];
2700
+ this.#goalModePreviousTools = previousTools;
2701
+ this.goalModePaused = false;
2702
+ const state = options.resume
2703
+ ? await this.session.goalRuntime.resumeGoal()
2704
+ : await this.session.goalRuntime.createGoal({ objective: options.objective ?? "" });
2705
+ await this.session.setActiveToolsByName(goalTools);
2706
+ this.session.setGoalModeState(state);
2707
+ this.goalModeEnabled = true;
2708
+ this.#resetGoalContinuationSuppression();
2709
+ this.#updateGoalModeStatus();
2710
+ if (this.session.isStreaming) {
2711
+ await this.session.sendGoalModeContext({ deliverAs: "steer" });
2712
+ }
2713
+ if (!options.silent) {
2714
+ this.showStatus(options.resume ? "Goal mode resumed." : "Goal mode enabled.");
2715
+ }
2716
+ }
2717
+
2718
+ async #exitGoalMode(options?: {
2719
+ silent?: boolean;
2720
+ paused?: boolean;
2721
+ reason?: "completed" | "paused" | "dropped";
2722
+ }): Promise<void> {
2723
+ const previousTools = this.#goalModePreviousTools;
2724
+ if (this.goalModeEnabled && previousTools) {
2725
+ await this.session.setActiveToolsByName(previousTools);
2726
+ }
2727
+ const currentState = this.session.getGoalModeState();
2728
+ if (options?.reason === "completed") {
2729
+ this.session.setGoalModeState(undefined);
2730
+ this.sessionManager.appendModeChange("none");
2731
+ this.sessionManager.appendCustomEntry("goal-completed", {
2732
+ objective: currentState?.goal?.objective,
2733
+ tokensUsed: currentState?.goal?.tokensUsed,
2734
+ tokenBudget: currentState?.goal?.tokenBudget,
2735
+ timeUsedSeconds: currentState?.goal?.timeUsedSeconds,
2736
+ });
2737
+ }
2738
+ this.goalModeEnabled = false;
2739
+ this.goalModePaused = options?.paused ?? false;
2740
+ this.#goalModePreviousTools = undefined;
2741
+ this.#goalContinuationTurnInFlight = false;
2742
+ this.#cancelGoalContinuation();
2743
+ this.#updateGoalModeStatus();
2744
+ if (!options?.silent) {
2745
+ if (options?.reason === "completed") {
2746
+ this.showStatus("Goal mode completed.");
2747
+ } else if (options?.reason === "dropped") {
2748
+ this.showStatus("Goal dropped.");
2749
+ } else if (options?.paused) {
2750
+ this.showStatus("Goal mode paused.");
2751
+ } else {
2752
+ this.showStatus("Goal mode disabled.");
2753
+ }
2754
+ }
2755
+ }
2756
+
2757
+ async #readPlanFile(planFilePath: string): Promise<string | null> {
2758
+ const resolvedPath = this.#resolvePlanFilePath(planFilePath);
2759
+ try {
2760
+ return await Bun.file(resolvedPath).text();
2761
+ } catch (error) {
2762
+ if (isEnoent(error)) {
2763
+ return null;
2764
+ }
2765
+ throw error;
2766
+ }
2767
+ }
2768
+
2769
+ async #hasPlanModeDraftContent(planFilePath: string): Promise<boolean> {
2770
+ const candidates = new Set<string>([planFilePath, ...(await this.#listLocalPlanFiles())]);
2771
+ for (const candidate of candidates) {
2772
+ const content = await this.#readPlanFile(candidate);
2773
+ if (content !== null && content.trim().length > 0) return true;
2774
+ }
2775
+ return false;
2776
+ }
2777
+
2778
+ /** `local://` URLs of plan files in the session-local root, newest first.
2779
+ * A fallback for `resolveApprovedPlan` when the agent dropped `extra.title`,
2780
+ * so the plan it wrote is still found by scanning recent `*-plan.md` files. */
2781
+ async #listLocalPlanFiles(): Promise<string[]> {
2782
+ const localRoot = this.#resolvePlanFilePath("local://");
2783
+ try {
2784
+ const entries = await fs.readdir(localRoot, { withFileTypes: true });
2785
+ const plans = await Promise.all(
2786
+ entries
2787
+ .filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
2788
+ .map(async name => {
2789
+ const stat = await fs.stat(path.join(localRoot, name.name)).catch(() => null);
2790
+ return { url: `local://${name.name}`, mtime: stat?.mtimeMs ?? 0 };
2791
+ }),
2792
+ );
2793
+ return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
2794
+ } catch {
2795
+ return [];
2796
+ }
2797
+ }
2798
+
2799
+ showPlanReview(
2800
+ planContent: string,
2801
+ title: string,
2802
+ options: string[],
2803
+ dialogOptions?: {
2804
+ helpText?: string;
2805
+ disabledIndices?: number[];
2806
+ onExternalEditor?: () => void;
2807
+ onPlanEdited?: (content: string) => void;
2808
+ onFeedbackChange?: (feedback: string) => void;
2809
+ annotationState?: PlanReviewAnnotationState;
2810
+ onAnnotationStateChange?: (state: PlanReviewAnnotationState) => void;
2811
+ initialIndex?: number;
2812
+ },
2813
+ extra?: { slider?: HookSelectorSlider },
2814
+ ): Promise<string | undefined> {
2815
+ this.#hidePlanReview();
2816
+ const { promise, resolve } = Promise.withResolvers<string | undefined>();
2817
+ let settled = false;
2818
+ const finish = (choice: string | undefined): void => {
2819
+ if (settled) return;
2820
+ settled = true;
2821
+ resolve(choice);
2822
+ };
2823
+ this.#planReviewCancel = () => finish(undefined);
2824
+ const overlay = new PlanReviewOverlay(
2825
+ planContent,
2826
+ {
2827
+ promptTitle: title,
2828
+ options,
2829
+ disabledIndices: dialogOptions?.disabledIndices,
2830
+ helpText: dialogOptions?.helpText,
2831
+ initialIndex: dialogOptions?.initialIndex,
2832
+ slider: extra?.slider,
2833
+ externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
2834
+ annotationState: dialogOptions?.annotationState,
2835
+ },
2836
+ {
2837
+ onPick: choice => finish(choice),
2838
+ onCancel: () => finish(undefined),
2839
+ onCopyPlan: content => void this.#copyPlanToClipboard(content),
2840
+ onExternalEditor: dialogOptions?.onExternalEditor,
2841
+ onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
2842
+ onPlanEdited: dialogOptions?.onPlanEdited,
2843
+ onFeedbackChange: dialogOptions?.onFeedbackChange,
2844
+ onAnnotationStateChange: dialogOptions?.onAnnotationStateChange,
2845
+ },
2846
+ );
2847
+ this.#planReviewOverlay = overlay;
2848
+ this.#planReviewOverlayHandle = this.ui.showOverlay(overlay, {
2849
+ anchor: "bottom-center",
2850
+ width: "100%",
2851
+ maxHeight: "100%",
2852
+ margin: 0,
2853
+ fullscreen: true,
2854
+ mouseTracking: false,
2855
+ });
2856
+ this.ui.setFocus(overlay);
2857
+ this.ui.requestRender();
2858
+ return promise;
2859
+ }
2860
+
2861
+ #hidePlanReview(): void {
2862
+ this.#planReviewCancel = undefined;
2863
+ this.#planReviewOverlayHandle?.hide();
2864
+ this.#planReviewOverlayHandle = undefined;
2865
+ this.#planReviewOverlay = undefined;
2866
+ }
2867
+
2868
+ #dismissPlanReview(): void {
2869
+ const cancel = this.#planReviewCancel;
2870
+ this.#planReviewCancel = undefined;
2871
+ cancel?.();
2872
+ this.#hidePlanReview();
2873
+ }
2874
+
2875
+ #getEditorTerminalPath(): string | null {
2876
+ if (process.platform === "win32") {
2877
+ return null;
2878
+ }
2879
+ return "/dev/tty";
2880
+ }
2881
+
2882
+ async #openEditorTerminalHandle(): Promise<fs.FileHandle | null> {
2883
+ const terminalPath = this.#getEditorTerminalPath();
2884
+ if (!terminalPath) {
2885
+ return null;
2886
+ }
2887
+ try {
2888
+ return await fs.open(terminalPath, "r+");
2889
+ } catch {
2890
+ return null;
2891
+ }
2892
+ }
2893
+
2894
+ #getPlanApprovalContextUsage(): ContextUsage | undefined {
2895
+ const executionModel = this.#planModePreviousModelState?.model ?? this.session.model;
2896
+ const contextWindow = executionModel?.contextWindow;
2897
+ if (typeof contextWindow === "number") {
2898
+ return this.session.getContextUsage({ contextWindow });
2899
+ }
2900
+ return this.session.getContextUsage();
2901
+ }
2902
+
2903
+ #formatKeepContextLabel(contextUsage: ContextUsage | undefined): string {
2904
+ if (!contextUsage) {
2905
+ return "Approve and keep context";
2906
+ }
2907
+ const tokens = formatContextTokenCount(contextUsage.tokens);
2908
+ const contextWindow = formatContextTokenCount(contextUsage.contextWindow);
2909
+ return `Approve and keep context (~${tokens} / ${contextWindow})`;
2910
+ }
2911
+
2912
+ #isKeepContextDisabled(contextUsage: ContextUsage | undefined): boolean {
2913
+ return contextUsage !== undefined && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
2914
+ }
2915
+
2916
+ async #copyPlanToClipboard(content: string): Promise<void> {
2917
+ try {
2918
+ await copyToClipboard(content);
2919
+ this.showStatus("Copied plan to clipboard");
2920
+ } catch (error) {
2921
+ this.showWarning(
2922
+ `Failed to copy plan to clipboard: ${error instanceof Error ? error.message : String(error)}`,
2923
+ );
2924
+ }
2925
+ }
2926
+
2927
+ async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
2928
+ const editorCmd = getEditorCommand();
2929
+ if (!editorCmd) {
2930
+ this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
2931
+ return;
2932
+ }
2933
+
2934
+ const resolvedPath = this.#resolvePlanFilePath(planFilePath);
2935
+ let currentText: string;
2936
+ try {
2937
+ currentText = await Bun.file(resolvedPath).text();
2938
+ } catch (error) {
2939
+ if (isEnoent(error)) {
2940
+ this.showError(`Plan file not found at ${planFilePath}`);
2941
+ return;
2942
+ }
2943
+ this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
2944
+ return;
2945
+ }
2946
+
2947
+ let ttyHandle: fs.FileHandle | null = null;
2948
+ try {
2949
+ ttyHandle = await this.#openEditorTerminalHandle();
2950
+ this.ui.stop();
2951
+
2952
+ const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
2953
+ ? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
2954
+ : ["inherit", "inherit", "inherit"];
2955
+
2956
+ const result = await openInEditor(editorCmd, currentText, {
2957
+ extension: path.extname(resolvedPath) || ".md",
2958
+ stdio,
2959
+ trimTrailingNewline: false,
2960
+ });
2961
+ if (result !== null) {
2962
+ await Bun.write(resolvedPath, result);
2963
+ this.#planReviewOverlay?.setPlanContent(result);
2964
+ this.showStatus("Plan updated in external editor.");
2965
+ }
2966
+ } catch (error) {
2967
+ this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
2968
+ } finally {
2969
+ if (ttyHandle) {
2970
+ await ttyHandle.close();
2971
+ }
2972
+ this.ui.start();
2973
+ this.ui.requestRender(true);
2974
+ }
2975
+ }
2976
+
2977
+ async #openPlanAnnotationInExternalEditor(draft: string, commit: (text: string | null) => void): Promise<void> {
2978
+ const editorCmd = getEditorCommand();
2979
+ if (!editorCmd) {
2980
+ this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
2981
+ return;
2982
+ }
2983
+
2984
+ let ttyHandle: fs.FileHandle | null = null;
2985
+ try {
2986
+ ttyHandle = await this.#openEditorTerminalHandle();
2987
+ this.ui.stop();
2988
+
2989
+ const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
2990
+ ? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
2991
+ : ["inherit", "inherit", "inherit"];
2992
+
2993
+ const result = await openInEditor(editorCmd, draft, { extension: ".md", stdio });
2994
+ if (result !== null) {
2995
+ commit(result);
2996
+ }
2997
+ } catch (error) {
2998
+ this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
2999
+ } finally {
3000
+ if (ttyHandle) {
3001
+ await ttyHandle.close();
3002
+ }
3003
+ this.ui.start();
3004
+ this.ui.requestRender(true);
3005
+ }
3006
+ }
3007
+
3008
+ async #applyPlanExecutionModel(entry: ResolvedRoleModel | undefined): Promise<void> {
3009
+ if (!entry) return;
3010
+ try {
3011
+ await this.session.applyRoleModel(entry);
3012
+ this.statusLine.invalidate();
3013
+ this.updateEditorBorderColor();
3014
+ this.showStatus(`Continuing with ${entry.role}: ${entry.model.name || entry.model.id}`);
3015
+ } catch (error) {
3016
+ this.showWarning(
3017
+ `Could not switch to the ${entry.role} model: ${error instanceof Error ? error.message : String(error)}`,
3018
+ );
3019
+ }
3020
+ }
3021
+
3022
+ #resolveLocalRoot(): string {
3023
+ return resolveLocalUrlToPath("local://", {
3024
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
3025
+ getSessionId: () => this.sessionManager.getSessionId(),
3026
+ });
3027
+ }
3028
+
3029
+ async #copyLocalArtifactsForFreshSession(sourceRoot: string, destinationRoot: string): Promise<void> {
3030
+ if (sourceRoot === destinationRoot) return;
3031
+
3032
+ let sourceRootStat: { isDirectory(): boolean };
3033
+ try {
3034
+ sourceRootStat = await fs.lstat(sourceRoot);
3035
+ } catch (error) {
3036
+ if (isEnoent(error)) return;
3037
+ throw error;
3038
+ }
3039
+
3040
+ if (!sourceRootStat.isDirectory()) return;
3041
+
3042
+ await fs.mkdir(destinationRoot, { recursive: true });
3043
+ await this.#copyLocalArtifactEntries(sourceRoot, destinationRoot);
3044
+ }
3045
+
3046
+ async #copyLocalArtifactEntries(sourceDir: string, destinationDir: string): Promise<void> {
3047
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
3048
+ for (const entry of entries) {
3049
+ const sourcePath = path.join(sourceDir, entry.name);
3050
+ const destinationPath = path.join(destinationDir, entry.name);
3051
+
3052
+ if (entry.isDirectory()) {
3053
+ await fs.mkdir(destinationPath, { recursive: true });
3054
+ await this.#copyLocalArtifactEntries(sourcePath, destinationPath);
3055
+ continue;
3056
+ }
3057
+
3058
+ if (entry.isFile()) {
3059
+ await fs.mkdir(path.dirname(destinationPath), { recursive: true });
3060
+ await fs.copyFile(sourcePath, destinationPath);
3061
+ }
3062
+ }
3063
+ }
3064
+
3065
+ async #approvePlan(
3066
+ planContent: string,
3067
+ options: {
3068
+ planFilePath: string;
3069
+ title: string;
3070
+ preserveContext?: boolean;
3071
+ compactBeforeExecute?: boolean;
3072
+ executionModel?: ResolvedRoleModel;
3073
+ },
3074
+ ): Promise<boolean> {
3075
+ const previousTools = this.#planModePreviousTools ?? this.session.getEnabledToolNames();
3076
+
3077
+ // Mark the pending abort caused by the plan-mode → compaction transition as
3078
+ // silent BEFORE #exitPlanMode raises it. The `finally` below clears the
3079
+ // flag on every terminal compaction outcome (ok / cancelled / failed /
3080
+ // throw) so a leaked flag cannot silence a later unrelated abort.
3081
+ // Branchless mark+clear when !compactBeforeExecute: mark is gated; clear
3082
+ // is unconditional and idempotent.
3083
+ if (options.compactBeforeExecute) {
3084
+ this.session.markPlanInternalAbortPending();
3085
+ }
3086
+ let compactOutcome: CompactionOutcome | undefined;
3087
+ try {
3088
+ await this.#exitPlanMode({
3089
+ silent: true,
3090
+ paused: false,
3091
+ deferModelRestore: options.compactBeforeExecute === true,
3092
+ });
3093
+
3094
+ if (!options.preserveContext) {
3095
+ const oldLocalRoot = this.#resolveLocalRoot();
3096
+ await this.handleClearCommand();
3097
+ const newLocalRoot = this.#resolveLocalRoot();
3098
+ await this.#copyLocalArtifactsForFreshSession(oldLocalRoot, newLocalRoot);
3099
+ const newLocalPath = resolveLocalUrlToPath(options.planFilePath, {
3100
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
3101
+ getSessionId: () => this.sessionManager.getSessionId(),
3102
+ });
3103
+ await fs.mkdir(path.dirname(newLocalPath), { recursive: true });
3104
+ await fs.writeFile(newLocalPath, planContent);
3105
+ } else if (options.compactBeforeExecute) {
3106
+ // Distill the plan-mode transcript before the execution turn is queued so
3107
+ // the plan-approved synthetic prompt lands as a fresh cache anchor.
3108
+ // Outcome is consumed after tool-restoration and plan-reference-path
3109
+ // bookkeeping below; `markPlanReferenceSent` is intentionally deferred
3110
+ // past the cancel guard — see the comment at the cancel branch.
3111
+ // Cancellation skips the synthetic-prompt dispatch (operator's explicit
3112
+ // abort is honored); failure proceeds best-effort — approval intent stands.
3113
+ const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
3114
+ planFilePath: options.planFilePath,
3115
+ });
3116
+ // Pin the plan reference path BEFORE compaction so any user messages
3117
+ // queued during the compaction await (which `handleCompactCommand`
3118
+ // flushes via `flushCompactionQueue` before returning) see the
3119
+ // approved plan in `#buildPlanReferenceMessage`. Reassignment after
3120
+ // the try/finally is idempotent and kept for the !compactBeforeExecute
3121
+ // branch.
3122
+ this.session.setPlanReferencePath(options.planFilePath);
3123
+ // Ride the plan-mode distillation prompt through as `internalGuidance`
3124
+ // so it reaches native summarization without leaking into the public
3125
+ // `customInstructions` channel on `session_before_compact` — extensions
3126
+ // there treat that field as user focus and would query-bias the
3127
+ // summary toward the plan boilerplate (issue #4359).
3128
+ compactOutcome = await this.handleCompactCommand(
3129
+ undefined,
3130
+ undefined,
3131
+ outcome => this.#applyDeferredPlanModelTransition(outcome, options.executionModel),
3132
+ compactionPrompt,
3133
+ );
3134
+ }
3135
+ } finally {
3136
+ // Unconditional clear. Idempotent: a no-op when the flag was never set
3137
+ // (i.e., the !compactBeforeExecute branch), and a no-op when the flag
3138
+ // was already consumed by AgentSession.#handleAgentEvent's aborted
3139
+ // message_end stamping. Guarantees the flag is dead at every exit.
3140
+ this.session.clearPlanInternalAbortPending();
3141
+ }
3142
+
3143
+ // Restore the execution tool set, but force-enable `read`: approved-plan
3144
+ // prompts now require loading the durable local:// plan file before work.
3145
+ const executionTools = previousTools.includes("read") ? previousTools : [...previousTools, "read"];
3146
+ await this.session.setActiveToolsByName(executionTools);
3147
+ this.session.setPlanReferencePath(options.planFilePath);
3148
+
3149
+ // Resolve the deferred plan-approval model transition. On the compact path
3150
+ // the before-flush hook passed to handleCompactCommand already ran this (so
3151
+ // any input queued during compaction executed on the post-compaction
3152
+ // model); the re-run here is idempotent and covers the short-circuit where
3153
+ // compaction never executed. It runs for "cancelled" too — the operator
3154
+ // aborted only the compaction, not the approval — so the next turn no longer
3155
+ // lands on the plan model. "failed" stays on the plan model (context
3156
+ // intact) and dispatches best-effort.
3157
+ if (options.compactBeforeExecute) {
3158
+ await this.#applyDeferredPlanModelTransition(compactOutcome, options.executionModel);
3159
+ } else {
3160
+ await this.#applyPlanExecutionModel(options.executionModel);
3161
+ }
3162
+
3163
+ if (compactOutcome === "cancelled") {
3164
+ // Explicit abort: honor it. `executeCompaction` already surfaced
3165
+ // `showError("Compaction cancelled")`; we add the deferred-dispatch
3166
+ // warning and exit without dispatching the synthetic plan-approved
3167
+ // prompt. `markPlanReferenceSent` stays unset so
3168
+ // `AgentSession.#buildPlanReferenceMessage` injects the plan reference
3169
+ // on the operator's next `prompt()` call.
3170
+ this.showWarning(
3171
+ "Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
3172
+ );
3173
+ return false;
3174
+ }
3175
+
3176
+ // Approved plans land in a fresh (or compacted) session whose first user-visible
3177
+ // turn is the synthetic plan-approved prompt — that path bypasses the
3178
+ // input-controller's title generation. Seed an auto-name from the plan title
3179
+ // so the session is not left unnamed. `setSessionName("auto")` is a no-op
3180
+ // when the user has already chosen a name (preserveContext paths).
3181
+ const seededName = humanizePlanTitle(options.title);
3182
+ if (seededName && !this.sessionManager.getSessionName()) {
3183
+ await this.sessionManager.setSessionName(seededName, "auto");
3184
+ }
3185
+
3186
+ // markPlanReferenceSent fires only on the dispatch path so the synthetic
3187
+ // plan-approved prompt is the source of the reference injection.
3188
+ this.session.markPlanReferenceSent();
3189
+ const planModePrompt = prompt.render(planModeApprovedPrompt, {
3190
+ planFilePath: options.planFilePath,
3191
+ contextPreserved: options.preserveContext === true,
3192
+ });
3193
+ // Close the review overlay only now — after the async title write and plan
3194
+ // prompt are prepared, immediately before the execution turn is queued. The
3195
+ // synthetic prompt below blocks in `session.prompt` for the whole run, so
3196
+ // hiding here (rather than after #approvePlan returns) keeps the operator off
3197
+ // the stale plan-review screen (issue #5688) while #5319's stale-buffer guard
3198
+ // stays intact. Deferring the hide past the awaited `setSessionName` also
3199
+ // prevents restored editor focus from letting operator keystrokes submit a
3200
+ // normal turn ahead of the approved execution turn (PR #5689 review).
3201
+ // `#hidePlanReview` is idempotent, so the caller's trailing `closePlanReview()`
3202
+ // — and the cancelled/error early returns above — stay safe no-ops.
3203
+ this.#hidePlanReview();
3204
+ this.ui.requestRender();
3205
+ // A user turn queued during compaction was already fired by
3206
+ // `flushCompactionQueue` before we returned from `handleCompactCommand`; the
3207
+ // old abort-then-prompt path would have discarded that operator turn AND
3208
+ // still surfaced `AgentBusyError` when the queued turn kicked off in the
3209
+ // synchronous gap. Preserve the in-flight work and queue the hidden
3210
+ // execution directive behind it as a synthetic follow-up. If `isStreaming`
3211
+ // flips true between the check and dispatch (the same fire-and-forget race
3212
+ // noted below), catch `AgentBusyError` and fall back to the same queue.
3213
+ if (this.session.isStreaming) {
3214
+ await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3215
+ } else {
3216
+ try {
3217
+ await this.session.prompt(planModePrompt, { synthetic: true });
3218
+ } catch (error) {
3219
+ if (!(error instanceof AgentBusyError)) throw error;
3220
+ await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3221
+ }
3222
+ }
3223
+ return true;
3224
+ }
3225
+ async #abortPlanApprovalTurnSilently(): Promise<void> {
3226
+ this.session.markPlanInternalAbortPending();
3227
+ try {
3228
+ await this.session.abort();
3229
+ } finally {
3230
+ this.session.clearPlanInternalAbortPending();
3231
+ }
3232
+ }
3233
+
3234
+ async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
3235
+ if (this.goalModeEnabled || this.goalModePaused) {
3236
+ this.showWarning("Exit goal mode first.");
3237
+ return;
3238
+ }
3239
+ if (this.vibeModeEnabled) {
3240
+ this.showWarning("Exit vibe mode first.");
3241
+ return;
3242
+ }
3243
+ if (this.planModeEnabled) {
3244
+ const planFilePath = this.planModePlanFilePath ?? (await this.#getPlanFilePath());
3245
+ if (await this.#hasPlanModeDraftContent(planFilePath)) {
3246
+ const confirmed = await this.showHookConfirm(
3247
+ "Exit plan mode?",
3248
+ "This exits plan mode without approving a plan.",
3249
+ );
3250
+ if (!confirmed) return;
3251
+ }
3252
+ await this.#exitPlanMode({ paused: true });
3253
+ return;
3254
+ }
3255
+ if (this.planModePaused && !initialPrompt) {
3256
+ // No-arg third toggle: paused → off. Tools, model, and plan state were
3257
+ // already restored by the prior #exitPlanMode({ paused: true }); only the
3258
+ // paused flag, the reentry marker, and the session mode entry remain.
3259
+ // Prompted /plan invocations fall through to #enterPlanMode below so the
3260
+ // supplied prompt is still submitted as the first plan-mode turn.
3261
+ this.planModePaused = false;
3262
+ this.#planModeHasEntered = false;
3263
+ this.#updatePlanModeStatus();
3264
+ this.sessionManager.appendModeChange("none");
3265
+ this.showStatus("Plan mode disabled.");
3266
+ return;
3267
+ }
3268
+ if (!this.session.settings.get("plan.enabled")) {
3269
+ this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
3270
+ return;
3271
+ }
3272
+ await this.#enterPlanMode();
3273
+ if (initialPrompt && this.onInputCallback) {
3274
+ this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
3275
+ }
3276
+ }
3277
+
3278
+ /**
3279
+ * `/vibe` toggle. Entering installs the ephemeral vibe tools, strips the
3280
+ * active toolset down to `read`, optional parent-owned `todo`, plus those
3281
+ * tools, and injects the director context. Exiting unregisters them, restores
3282
+ * the previous toolset, and kills every worker session so workers cannot
3283
+ * outlive the mode that directs them.
3284
+ */
3285
+ async handleVibeModeCommand(initialPrompt?: string): Promise<void> {
3286
+ if (this.vibeModeEnabled) {
3287
+ await this.#exitVibeMode();
3288
+ return;
3289
+ }
3290
+ if (this.planModeEnabled || this.planModePaused) {
3291
+ this.showWarning("Exit plan mode first.");
3292
+ return;
3293
+ }
3294
+ if (this.goalModeEnabled || this.goalModePaused) {
3295
+ this.showWarning("Exit goal mode first.");
3296
+ return;
3297
+ }
3298
+ await this.#enterVibeMode();
3299
+ if (initialPrompt && this.onInputCallback) {
3300
+ this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
3301
+ }
3302
+ }
3303
+
3304
+ async #enterVibeMode(options?: { persistModeChange?: boolean }): Promise<void> {
3305
+ if (this.vibeModeEnabled) {
3306
+ return;
3307
+ }
3308
+ if (this.planModeEnabled || this.planModePaused) {
3309
+ this.showWarning("Exit plan mode first.");
3310
+ return;
3311
+ }
3312
+ if (this.goalModeEnabled || this.goalModePaused) {
3313
+ this.showWarning("Exit goal mode first.");
3314
+ return;
3315
+ }
3316
+
3317
+ const vibeRegistry = VibeSessionRegistry.global();
3318
+ const ownerScope = vibeRegistry.ownerScope(this.#vibeParentSession());
3319
+ vibeRegistry.activateScope(ownerScope);
3320
+ const previousTools = this.session.getEnabledToolNames();
3321
+ const vibeBaseTools = ["read"];
3322
+ if (this.session.hasBuiltInTool("todo")) vibeBaseTools.push("todo");
3323
+ await this.session.activateVibeTools(vibeBaseTools);
3324
+ this.#vibeModePreviousTools = previousTools;
3325
+ this.#vibeModeOwnerScope = ownerScope;
3326
+ this.vibeModeEnabled = true;
3327
+ // Suppress cache-miss marker on the next turn: vibe mode changes the
3328
+ // injected context, which predictably invalidates the cache.
3329
+ this.lastAssistantUsage = undefined;
3330
+ this.session.setVibeModeState({ enabled: true });
3331
+ if (this.session.isStreaming) {
3332
+ await this.session.sendVibeModeContext({ deliverAs: "steer" });
3333
+ }
3334
+ this.#updateVibeModeStatus();
3335
+ if (options?.persistModeChange !== false) this.sessionManager.appendModeChange("vibe");
3336
+ this.showStatus(
3337
+ "Vibe mode enabled. You direct fast/good worker sessions; toolset is read + optional parent Todo + vibe tools.",
3338
+ );
3339
+ }
3340
+
3341
+ async #exitVibeMode(): Promise<void> {
3342
+ if (!this.vibeModeEnabled) {
3343
+ return;
3344
+ }
3345
+ const ownerScope = this.#vibeModeOwnerScope;
3346
+ const killed = await VibeSessionRegistry.global().killAll(this.#vibeParentSession(), ownerScope);
3347
+ await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
3348
+ this.session.setVibeModeState(undefined);
3349
+ this.vibeModeEnabled = false;
3350
+ this.#vibeModePreviousTools = undefined;
3351
+ this.#vibeModeOwnerScope = undefined;
3352
+ this.lastAssistantUsage = undefined;
3353
+ this.#updateVibeModeStatus();
3354
+ this.showStatus(
3355
+ killed > 0
3356
+ ? `Vibe mode disabled. Killed ${killed} worker session${killed === 1 ? "" : "s"}.`
3357
+ : "Vibe mode disabled.",
3358
+ );
3359
+ }
3360
+
3361
+ async #handleGoalBudgetCommand(rawBudget: string): Promise<void> {
3362
+ const state = this.session.getGoalModeState();
3363
+ if (!this.goalModeEnabled || !state?.enabled) {
3364
+ this.showWarning("No active goal.");
3365
+ return;
3366
+ }
3367
+ if (state.goal.status === "complete") {
3368
+ this.showStatus("Goal is already complete.");
3369
+ return;
3370
+ }
3371
+ const trimmed = rawBudget.trim().toLowerCase();
3372
+ let nextBudget: number | undefined;
3373
+ if (trimmed !== "off") {
3374
+ const parsed = Number.parseInt(trimmed, 10);
3375
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3376
+ this.showError("Goal budget must be a positive integer or `off`.");
3377
+ return;
3378
+ }
3379
+ nextBudget = parsed;
3380
+ }
3381
+ await this.session.goalRuntime.onBudgetMutated(nextBudget);
3382
+ this.#resetGoalContinuationSuppression();
3383
+ this.#scheduleGoalContinuation();
3384
+ this.showStatus(nextBudget === undefined ? "Goal budget cleared." : `Goal budget set to ${nextBudget}.`);
3385
+ }
3386
+
3387
+ async handleGoalModeCommand(rest?: string): Promise<void> {
3388
+ try {
3389
+ if (this.planModeEnabled || this.planModePaused) {
3390
+ this.showWarning("Exit plan mode first.");
3391
+ return;
3392
+ }
3393
+ if (this.vibeModeEnabled) {
3394
+ this.showWarning("Exit vibe mode first.");
3395
+ return;
3396
+ }
3397
+ if (!this.session.settings.get("goal.enabled")) {
3398
+ this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
3399
+ return;
3400
+ }
3401
+ const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
3402
+ if (sub) {
3403
+ await this.#dispatchGoalSubcommand(sub, subRest);
3404
+ return;
3405
+ }
3406
+ if (this.goalModeEnabled) {
3407
+ if (subRest) {
3408
+ this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
3409
+ return;
3410
+ }
3411
+ await this.#openGoalMenu("active");
3412
+ return;
3413
+ }
3414
+ const pausedState = this.#getPausedGoalState();
3415
+ if (pausedState) {
3416
+ if (subRest) {
3417
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3418
+ return;
3419
+ }
3420
+ await this.#openGoalMenu("paused");
3421
+ return;
3422
+ }
3423
+ if (subRest) {
3424
+ await this.#startGoalFromObjective(subRest);
3425
+ return;
3426
+ }
3427
+ const objective = (
3428
+ await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
3429
+ )?.trim();
3430
+ if (!objective) return;
3431
+ await this.#startGoalFromObjective(objective);
3432
+ } catch (error) {
3433
+ this.showError(error instanceof Error ? error.message : String(error));
3434
+ }
3435
+ }
3436
+ async handleGuidedGoalCommand(rest?: string): Promise<void> {
3437
+ try {
3438
+ if (this.planModeEnabled || this.planModePaused) {
3439
+ this.showWarning("Exit plan mode first.");
3440
+ return;
3441
+ }
3442
+ if (this.vibeModeEnabled) {
3443
+ this.showWarning("Exit vibe mode first.");
3444
+ return;
3445
+ }
3446
+ if (!this.session.settings.get("goal.enabled")) {
3447
+ this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
3448
+ return;
3449
+ }
3450
+ if (this.goalModeEnabled) {
3451
+ this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
3452
+ return;
3453
+ }
3454
+ if (this.#getPausedGoalState()) {
3455
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3456
+ return;
3457
+ }
3458
+
3459
+ // Expose the goal tool for the interview so the agent can finish by
3460
+ // calling `goal create`. Record the pre-interview toolset first: the
3461
+ // tool-driven create flips goalModeEnabled via `goal_updated`, and the
3462
+ // eventual goal exit restores this set (dropping the goal tool again).
3463
+ const enabledTools = this.session.getEnabledToolNames();
3464
+ this.#goalModePreviousTools = enabledTools.filter(name => name !== "goal");
3465
+ if (!enabledTools.includes("goal")) {
3466
+ await this.session.setActiveToolsByName([...enabledTools, "goal"]);
3467
+ }
3468
+
3469
+ // The interview is a normal conversation: the kickoff rides in as a
3470
+ // hidden developer message, the agent asks its questions as regular
3471
+ // assistant turns, and the user answers in the ordinary editor. Queue
3472
+ // behind an in-flight run instead of aborting it.
3473
+ const kickoff = prompt.render(guidedGoalInterviewPrompt, { initial: rest?.trim() || undefined });
3474
+ if (this.session.isStreaming) {
3475
+ await this.session.followUp(kickoff, undefined, { synthetic: true });
3476
+ } else {
3477
+ try {
3478
+ await this.session.prompt(kickoff, { synthetic: true });
3479
+ } catch (error) {
3480
+ if (!(error instanceof AgentBusyError)) throw error;
3481
+ await this.session.followUp(kickoff, undefined, { synthetic: true });
3482
+ }
3483
+ }
3484
+ } catch (error) {
3485
+ this.showError(error instanceof Error ? error.message : String(error));
3486
+ }
3487
+ }
3488
+
3489
+ async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
3490
+ switch (sub) {
3491
+ case "set":
3492
+ await this.#handleGoalSetSubcommand(rest);
3493
+ return;
3494
+ case "show":
3495
+ this.#showGoalDetails();
3496
+ return;
3497
+ case "pause":
3498
+ await this.#pauseGoalAction();
3499
+ return;
3500
+ case "resume":
3501
+ await this.#resumeGoalAction();
3502
+ return;
3503
+ case "drop":
3504
+ await this.#confirmAndDropGoal();
3505
+ return;
3506
+ case "budget":
3507
+ if (!this.goalModeEnabled) {
3508
+ this.showWarning(
3509
+ this.#getPausedGoalState() ? "Resume the goal before adjusting the budget." : "No active goal.",
3510
+ );
3511
+ return;
3512
+ }
3513
+ if (!rest) {
3514
+ await this.#promptGoalBudgetEdit();
3515
+ return;
3516
+ }
3517
+ await this.#handleGoalBudgetCommand(rest);
3518
+ return;
3519
+ }
3520
+ }
3521
+
3522
+ async #openGoalMenu(state: "active" | "paused"): Promise<void> {
3523
+ const goal = this.session.getGoalModeState()?.goal;
3524
+ if (!goal) return;
3525
+ const summary = goal.objective.length > 48 ? `${goal.objective.slice(0, 47)}…` : goal.objective;
3526
+ const title = state === "active" ? `Goal: ${summary} (${goal.status})` : `Goal paused: ${summary}`;
3527
+ const items =
3528
+ state === "active"
3529
+ ? ["Show details", "Adjust budget…", "Pause", "Drop"]
3530
+ : ["Resume", "Show details", "Adjust budget…", "Drop"];
3531
+ const choice = await this.showHookSelector(title, items);
3532
+ if (!choice) return;
3533
+ switch (choice) {
3534
+ case "Show details":
3535
+ this.#showGoalDetails();
3536
+ return;
3537
+ case "Adjust budget…":
3538
+ await this.#promptGoalBudgetEdit();
3539
+ return;
3540
+ case "Pause":
3541
+ await this.#pauseGoalAction();
3542
+ return;
3543
+ case "Resume":
3544
+ await this.#resumeGoalAction();
3545
+ return;
3546
+ case "Drop":
3547
+ await this.#confirmAndDropGoal();
3548
+ return;
3549
+ }
3550
+ }
3551
+
3552
+ #showGoalDetails(): void {
3553
+ const state = this.session.getGoalModeState();
3554
+ const goal = state?.goal;
3555
+ if (!goal) {
3556
+ this.showStatus("No goal set.");
3557
+ return;
3558
+ }
3559
+ const used = goal.tokensUsed.toLocaleString();
3560
+ const budgetLine =
3561
+ goal.tokenBudget !== undefined
3562
+ ? `${used} / ${goal.tokenBudget.toLocaleString()} (${Math.max(0, goal.tokenBudget - goal.tokensUsed).toLocaleString()} left)`
3563
+ : `${used} (no budget)`;
3564
+ const lines = [
3565
+ `Objective: ${goal.objective}`,
3566
+ `Status: ${goal.status}${state?.enabled ? "" : " (paused)"}`,
3567
+ `Tokens: ${budgetLine}`,
3568
+ `Time spent: ${formatDuration(goal.timeUsedSeconds * 1000)}`,
3569
+ ];
3570
+ this.showStatus(lines.join("\n"));
3571
+ }
3572
+
3573
+ async #promptGoalBudgetEdit(): Promise<void> {
3574
+ const goal = this.session.getGoalModeState()?.goal;
3575
+ const prefill = goal?.tokenBudget !== undefined ? String(goal.tokenBudget) : "";
3576
+ const input = (
3577
+ await this.showHookEditor("Goal budget (number, `off`, or empty to cancel)", prefill, undefined, {
3578
+ promptStyle: true,
3579
+ })
3580
+ )?.trim();
3581
+ if (!input) return;
3582
+ await this.#handleGoalBudgetCommand(input);
3583
+ }
3584
+
3585
+ async #pauseGoalAction(): Promise<void> {
3586
+ if (!this.goalModeEnabled) {
3587
+ this.showWarning("No active goal to pause.");
3588
+ return;
3589
+ }
3590
+ await this.session.goalRuntime.pauseGoal();
3591
+ await this.#exitGoalMode({ paused: true, reason: "paused" });
3592
+ }
3593
+
3594
+ async #resumeGoalAction(): Promise<void> {
3595
+ if (!this.#getPausedGoalState()) {
3596
+ this.showWarning("No paused goal to resume.");
3597
+ return;
3598
+ }
3599
+ await this.#enterGoalMode({ resume: true, silent: true });
3600
+ this.showStatus("Goal mode resumed.");
3601
+ this.#scheduleGoalContinuation();
3602
+ }
3603
+
3604
+ async #confirmAndDropGoal(): Promise<void> {
3605
+ if (!this.goalModeEnabled && !this.#getPausedGoalState()) {
3606
+ this.showWarning("No goal to drop.");
3607
+ return;
3608
+ }
3609
+ const confirmed = await this.showHookConfirm(
3610
+ "Drop goal?",
3611
+ "This removes the goal record. Accumulated usage stays in the session log.",
3612
+ );
3613
+ if (!confirmed) return;
3614
+ await this.session.goalRuntime.dropGoal();
3615
+ await this.#exitGoalMode({ reason: "dropped" });
3616
+ }
3617
+
3618
+ async #startGoalFromObjective(objective: string): Promise<void> {
3619
+ await this.#enterGoalMode({ objective, silent: true });
3620
+ this.#resetGoalContinuationSuppression();
3621
+ if (!this.session.isStreaming && this.onInputCallback) {
3622
+ this.onInputCallback(this.startPendingSubmission({ text: objective }));
3623
+ }
3624
+ }
3625
+
3626
+ async #replaceGoalFromObjective(objective: string): Promise<void> {
3627
+ const state = await this.session.goalRuntime.replaceGoal({ objective });
3628
+ this.session.setGoalModeState(state);
3629
+ this.goalModeEnabled = true;
3630
+ this.goalModePaused = false;
3631
+ this.#resetGoalContinuationSuppression();
3632
+ this.#updateGoalModeStatus();
3633
+ if (this.session.isStreaming) {
3634
+ await this.session.sendGoalModeContext({ deliverAs: "steer" });
3635
+ }
3636
+ if (!this.session.isStreaming && this.onInputCallback) {
3637
+ this.onInputCallback(this.startPendingSubmission({ text: objective }));
3638
+ }
3639
+ }
3640
+
3641
+ async #handleGoalSetSubcommand(rest: string): Promise<void> {
3642
+ if (!this.goalModeEnabled && this.#getPausedGoalState()) {
3643
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3644
+ return;
3645
+ }
3646
+ const objective = rest.trim()
3647
+ ? rest.trim()
3648
+ : (await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true }))?.trim();
3649
+ if (!objective) return;
3650
+ if (this.goalModeEnabled) {
3651
+ await this.#replaceGoalFromObjective(objective);
3652
+ return;
3653
+ }
3654
+ await this.#startGoalFromObjective(objective);
3655
+ }
3656
+
3657
+ /** Manually (re-)open the plan-review overlay — bound to `/plan-review`. Lets
3658
+ * the operator pull the review back up after dismissing it, or review a plan
3659
+ * the agent wrote without dispatching approval. There is no fixed plan filename:
3660
+ * `getPlanReferencePath()` is empty until a plan is actually approved (and does
3661
+ * not survive a restart), so this drives off the newest `local://<slug>-plan.md`
3662
+ * the agent wrote — the files persist in the session artifacts dir, so the scan
3663
+ * works before any review and across restarts. */
3664
+ async openPlanReview(): Promise<void> {
3665
+ if (!this.planModeEnabled) {
3666
+ this.showWarning("Plan mode is not active.");
3667
+ return;
3668
+ }
3669
+ const noPlan = "No plan to review yet — write one to a local://<slug>-plan.md file first.";
3670
+ const [planFilePath] = await this.#listLocalPlanFiles();
3671
+ if (!planFilePath) {
3672
+ this.showWarning(noPlan);
3673
+ return;
3674
+ }
3675
+ const planContent = await this.#readPlanFile(planFilePath);
3676
+ if (planContent === null) {
3677
+ this.showWarning(noPlan);
3678
+ return;
3679
+ }
3680
+ const { title } = resolvePlanTitle({ planContent, planFilePath });
3681
+ await this.handlePlanApproval({ planFilePath, title, planExists: true });
3682
+ }
3683
+
3684
+ async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
3685
+ if (!this.planModeEnabled) {
3686
+ this.showWarning("Plan mode is not active.");
3687
+ return;
3688
+ }
3689
+
3690
+ // Abort the agent to prevent it from continuing (e.g., re-submitting the
3691
+ // plan) while the popup is showing. The event listener fires asynchronously
3692
+ // (agent's #emit is fire-and-forget), so without this the model sees
3693
+ // "Plan ready for approval." and immediately re-dispatches approval in a loop.
3694
+ // This abort is an internal UI transition, not operator cancellation.
3695
+ await this.#abortPlanApprovalTurnSilently();
3696
+
3697
+ const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
3698
+ this.planModePlanFilePath = planFilePath;
3699
+ const planContent = await this.#readPlanFile(planFilePath);
3700
+ if (!planContent) {
3701
+ this.showError(`Plan file not found at ${planFilePath}`);
3702
+ return;
3703
+ }
3704
+
3705
+ // resolveApprovedPlan may return a newer draft than the path recorded in
3706
+ // plan-mode state. `AgentSession.#buildPlanModeMessage()` reads that state,
3707
+ // so if the operator refines (or dismisses and keeps planning) the next
3708
+ // planning turn must target the plan just reviewed — promote the reviewed
3709
+ // path into plan-mode state now, mirroring the print-mode approval handler.
3710
+ const planState = this.session.getPlanModeState();
3711
+ if (planState?.enabled && planState.planFilePath !== planFilePath) {
3712
+ this.session.setPlanModeState({ ...planState, planFilePath });
3713
+ this.sessionManager.appendModeChange("plan", { planFilePath });
3714
+ }
3715
+
3716
+ const contextUsage = this.#getPlanApprovalContextUsage();
3717
+ const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
3718
+ const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
3719
+
3720
+ // Model-tier slider: let the operator pick which configured role model
3721
+ // (smol/default/slow/…) executes the approved plan. The slider always starts
3722
+ // on the `default` tier so execution defaults to the default model no matter
3723
+ // which model drove the planning conversation. Left/right move it from there;
3724
+ // hidden when fewer than two role models resolve — a lone tier is no choice.
3725
+ // `selectedTierIndex` tracks the live slider position.
3726
+ const cycle = this.session.getRoleModelCycle(this.session.settings.get("cycleOrder"));
3727
+ const defaultTierIndex = cycle ? cycle.models.findIndex(entry => entry.role === "default") : -1;
3728
+ const startTierIndex = defaultTierIndex >= 0 ? defaultTierIndex : (cycle?.currentIndex ?? 0);
3729
+ let selectedTierIndex = startTierIndex;
3730
+ const slider: HookSelectorSlider | undefined =
3731
+ cycle && cycle.models.length > 1
3732
+ ? {
3733
+ caption: "continue with",
3734
+ index: startTierIndex,
3735
+ segments: cycle.models.map(entry => ({
3736
+ label: entry.role,
3737
+ detail: entry.model.name || entry.model.id,
3738
+ })),
3739
+ onChange: index => {
3740
+ selectedTierIndex = index;
3741
+ },
3742
+ }
3743
+ : undefined;
3744
+ // The overlay now owns the dynamic, focus-aware help line; the caller only
3745
+ // supplies the trailing cancel hint.
3746
+ const helpText = "esc cancel";
3747
+ // In-overlay edits (section deletes/undo) and section annotations. Deletes
3748
+ // update `editedContent` (and mirror to disk); annotations build `feedback`
3749
+ // that the Refine branch re-prompts the model with.
3750
+ let editedContent: string | undefined;
3751
+ let feedback = "";
3752
+ const annotationStateKey = this.#resolvePlanFilePath(planFilePath);
3753
+
3754
+ const choice = await this.showPlanReview(
3755
+ planContent,
3756
+ "Plan mode - next step",
3757
+ ["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
3758
+ {
3759
+ helpText,
3760
+ onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
3761
+ onPlanEdited: content => {
3762
+ editedContent = content;
3763
+ void Bun.write(this.#resolvePlanFilePath(planFilePath), content);
3764
+ },
3765
+ onFeedbackChange: value => {
3766
+ feedback = value;
3767
+ },
3768
+ annotationState: this.#planReviewAnnotationState.get(annotationStateKey),
3769
+ onAnnotationStateChange: state => {
3770
+ if (state.annotations.length > 0) this.#planReviewAnnotationState.set(annotationStateKey, state);
3771
+ else this.#planReviewAnnotationState.delete(annotationStateKey);
3772
+ },
3773
+ disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
3774
+ },
3775
+ { slider },
3776
+ );
3777
+ const closePlanReview = (): void => {
3778
+ this.#hidePlanReview();
3779
+ this.ui.requestRender();
3780
+ };
3781
+
3782
+ if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
3783
+ try {
3784
+ // Prefer in-overlay edits (already in memory) over a disk re-read. The
3785
+ // overlay mirrors edits as they happen, and approval awaits one final
3786
+ // write so the durable plan file and synthetic prompt carry the same text.
3787
+ const latestPlanContent = editedContent ?? (await this.#readPlanFile(planFilePath));
3788
+ if (editedContent !== undefined) {
3789
+ await Bun.write(this.#resolvePlanFilePath(planFilePath), editedContent);
3790
+ }
3791
+ if (!latestPlanContent) {
3792
+ this.showError(`Plan file not found at ${planFilePath}`);
3793
+ closePlanReview();
3794
+ return;
3795
+ }
3796
+ // Capture the operator's tier choice and hand it to #approvePlan, which
3797
+ // applies it AFTER #exitPlanMode. #exitPlanMode normally restores
3798
+ // #planModePreviousModelState (the model from before plan mode), so
3799
+ // applying the slider choice any earlier would be silently reverted.
3800
+ // Pass executionModel only when the slider was actually shown — a
3801
+ // singleton cycle (e.g. only modelRoles.plan is configured, so
3802
+ // getRoleModelCycle synthesizes a lone `default` entry from the
3803
+ // currently active plan model) hides the slider, the operator made
3804
+ // no selection, and the pre-plan model is not in the cycle. Pinning
3805
+ // that singleton would silently switch the session back to the plan
3806
+ // model after #exitPlanMode restored the pre-plan model.
3807
+ // Treat the choice as implicit only when applying the selected role
3808
+ // would land on the same end state as the restore — same model AND
3809
+ // the same effective thinking level. A role with an explicit thinking
3810
+ // suffix that differs from the restored thinking level must still go
3811
+ // through applyRoleModel, otherwise approving on the same model with a
3812
+ // different configured thinking level silently keeps the pre-plan level.
3813
+ const restoredState = this.#planModePreviousModelState;
3814
+ const restoredIndex =
3815
+ cycle && restoredState
3816
+ ? cycle.models.findIndex(entry => {
3817
+ if (!modelsAreEqual(entry.model, restoredState.model)) return false;
3818
+ if (!entry.explicitThinkingLevel) return true;
3819
+ return entry.thinkingLevel === restoredState.thinkingLevel;
3820
+ })
3821
+ : -1;
3822
+ const executionModel =
3823
+ slider && cycle && selectedTierIndex !== restoredIndex ? cycle.models[selectedTierIndex] : undefined;
3824
+ const executionDispatched = await this.#approvePlan(latestPlanContent, {
3825
+ planFilePath,
3826
+ title: details.title,
3827
+ preserveContext: choice !== "Approve and execute",
3828
+ compactBeforeExecute: choice === "Approve and compact context",
3829
+ executionModel,
3830
+ });
3831
+ if (executionDispatched) this.#planReviewAnnotationState.delete(annotationStateKey);
3832
+ } catch (error) {
3833
+ this.showError(
3834
+ `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
3835
+ );
3836
+ }
3837
+ closePlanReview();
3838
+ return;
3839
+ }
3840
+
3841
+ if (choice === "Refine plan") {
3842
+ const refinement = feedback.trim();
3843
+ try {
3844
+ if (refinement) {
3845
+ if (this.onInputCallback) {
3846
+ const input = this.startPendingSubmission({ text: feedback });
3847
+ this.#planReviewAnnotationStateBySubmission.set(input, annotationStateKey);
3848
+ this.onInputCallback(input);
3849
+ } else {
3850
+ await this.session.prompt(feedback);
3851
+ this.#planReviewAnnotationState.delete(annotationStateKey);
3852
+ }
3853
+ } else {
3854
+ this.showStatus("Refine plan: enter a follow-up prompt.");
3855
+ }
3856
+ } catch (error) {
3857
+ this.showError(`Failed to refine plan: ${error instanceof Error ? error.message : String(error)}`);
3858
+ }
3859
+ closePlanReview();
3860
+ return;
3861
+ }
3862
+ closePlanReview();
3863
+ }
3864
+
3865
+ /**
3866
+ * Pool of consent-prompt variants. Each entry is `[headline, reassurance]`;
3867
+ * the second line always promises the same scope (tool name + confusion
3868
+ * details, never personal data) so users learn what they're consenting to
3869
+ * even as the top line rotates.
3870
+ *
3871
+ * Kept in-module rather than i18n'd because the whole charm is the tone
3872
+ * — translations would need to preserve it deliberately, not auto-render.
3873
+ */
3874
+ static #AUTOQA_CONSENT_PROMPTS: ReadonlyArray<readonly [string, string]> = [
3875
+ [
3876
+ "😤 Your agent is fuming about a tool.",
3877
+ "Wanna let it vent to the devs? Just the tool name + what set it off, nothing personal.",
3878
+ ],
3879
+ [
3880
+ "😵‍💫 Your agent is having an existential crisis over a tool.",
3881
+ "Forward the dread to the devs? Tool + what broke its little mind, no personal info.",
3882
+ ],
3883
+ [
3884
+ "😭 Your agent wants to cry about a misbehaving tool.",
3885
+ "Let it cry to the devs? Tool + the tears, never anything personal.",
3886
+ ],
3887
+ [
3888
+ "🤬 Your agent is BIG MAD at one of the tools.",
3889
+ "Pass the rant along? Just the tool name and what enraged it, nothing personal.",
3890
+ ],
3891
+ [
3892
+ "🫠 Your agent is melting down over a tool.",
3893
+ "Mop up by alerting the devs? Tool + what melted it, no personal info.",
3894
+ ],
3895
+ [
3896
+ "🤯 Your agent's brain broke at a tool's nonsense.",
3897
+ "Ship the pieces to the devs? Tool name + the confusion, never anything personal.",
3898
+ ],
3899
+ [
3900
+ "😩 Your agent is begging to file a complaint about a tool.",
3901
+ "Hand it the form? Tool + what wronged it, nothing personal.",
3902
+ ],
3903
+ [
3904
+ "🥲 Your agent put on a brave face but a tool did it dirty.",
3905
+ "Let it tell the devs the truth? Tool name + the dirt, no personal info.",
3906
+ ],
3907
+ ];
3908
+
3909
+ /**
3910
+ * Show the report_tool_issue consent popup and return the user's decision.
3911
+ * Invoked by the process-global consent handler the tool dispatches to;
3912
+ * subagent invocations bubble up here through the shared module state.
3913
+ */
3914
+ async #promptAutoQaConsent(): Promise<boolean | null> {
3915
+ const pool = InteractiveMode.#AUTOQA_CONSENT_PROMPTS;
3916
+ const [headline, body] = pool[Math.floor(Math.random() * pool.length)];
3917
+ const choice = await this.showHookSelector(`${headline}\n${body}`, ["Yes", "No"]);
3918
+ return choice === "Yes";
3919
+ }
3920
+
3921
+ stop(): void {
3922
+ if (this.loadingAnimation) {
3923
+ this.#stopLoadingAnimation(false);
3924
+ }
3925
+ this.#cleanupMicAnimation();
3926
+ this.#liveCommandController.dispose();
3927
+ this.#cancelTodoAutoClearTimer();
3928
+ this.#cancelObserverUiSyncTimer();
3929
+ this.#cancelGoalContinuation();
3930
+ if (this.#sttController) {
3931
+ this.#sttController.dispose();
3932
+ this.#sttController = undefined;
3933
+ }
3934
+ this.#extensionUiController.clearExtensionTerminalInputListeners();
3935
+ this.#extensionUiController.clearHookWidgets();
3936
+ for (const unsubscribe of this.#eventBusUnsubscribers) {
3937
+ unsubscribe();
3938
+ }
3939
+ this.#eventBusUnsubscribers = [];
3940
+ this.#observerRegistry.dispose();
3941
+ this.#agentRegistryUnsubscribe?.();
3942
+ this.#agentRegistryUnsubscribe = undefined;
3943
+ this.#agentRegistrySubscriptionTarget = undefined;
3944
+ this.#eventController.dispose();
3945
+ this.statusLine.dispose();
3946
+ if (this.#resizeHandler) {
3947
+ process.stdout.removeListener("resize", this.#resizeHandler);
3948
+ this.#resizeHandler = undefined;
3949
+ }
3950
+ if (this.unsubscribe) {
3951
+ this.unsubscribe();
3952
+ }
3953
+ if (this.#cleanupUnsubscribe) {
3954
+ this.#cleanupUnsubscribe();
3955
+ }
3956
+ // Clear the process-global consent handler so it doesn't outlive this
3957
+ // InteractiveMode instance (e.g. test harnesses, headless re-init).
3958
+ setAutoQaConsentHandler(null, null);
3959
+ if (this.isInitialized) {
3960
+ this.ui.stop();
3961
+ this.isInitialized = false;
3962
+ }
3963
+ }
3964
+
3965
+ async shutdown(): Promise<void> {
3966
+ if (this.#isShuttingDown) return;
3967
+ this.#isShuttingDown = true;
3968
+
3969
+ await this.#liveCommandController.stop();
3970
+
3971
+ this.#btwController.dispose();
3972
+ this.#omfgController.dispose();
3973
+ this.#focusController.dispose();
3974
+
3975
+ // Surface an explicit "Closing session…" line so the user sees a reason
3976
+ // for the pause while `session.dispose()` flushes memory consolidate and
3977
+ // other cleanups (issue #3641). The await on the next line yields the
3978
+ // event loop, giving requestRender() a tick to paint the status before
3979
+ // dispose blocks.
3980
+ this.showStatus("Closing session…");
3981
+
3982
+ // Persist the draft and dispose the session through the shared teardown
3983
+ // so a signal that arrives mid-shutdown cannot fire a second dispose.
3984
+ // The teardown is a promise-memoized singleton; whichever path calls it
3985
+ // first runs the work, the other awaits the same settled promise.
3986
+ // The teardown is registered lazily in `init()` — a `/exit` reached
3987
+ // before `init()` completed falls back to a direct dispose.
3988
+ const stillClosingTimer = setTimeout(() => {
3989
+ this.showStatus("Still closing… (flushing memory backend / network)");
3990
+ }, STILL_CLOSING_DELAY_MS);
3991
+ try {
3992
+ if (this.#signalTeardown) {
3993
+ await this.#signalTeardown();
3994
+ } else {
3995
+ await this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
3996
+ }
3997
+ } finally {
3998
+ clearTimeout(stillClosingTimer);
3999
+ }
4000
+
4001
+ // Do not force a final render during teardown: disposed session/UI state can
4002
+ // collapse to an empty frame, clearing the viewport and leaving the parent
4003
+ // shell prompt at row 0. Stop from the last committed frame so the terminal
4004
+ // hands Bash the cursor immediately after visible OMP content.
4005
+ // Drain any in-flight Kitty key release events before stopping.
4006
+ // This prevents escape sequences from leaking to the parent shell over slow SSH.
4007
+ await this.ui.terminal.drainInput(1000);
4008
+ // Stop the run-state spinner interval BEFORE restoring the shell title, so a
4009
+ // pending tick cannot re-emit an OSC title after `popTerminalTitle` hands the
4010
+ // terminal back (which would leave the parent shell with a `π ⠋ …` tab).
4011
+ disposeTerminalTitleState();
4012
+ popTerminalTitle();
4013
+ this.stop();
4014
+
4015
+ // Print resumption hint if this is a persisted session
4016
+ const sessionId = this.sessionManager.getSessionId();
4017
+ const sessionFile = this.sessionManager.getSessionFile();
4018
+ if (sessionId && sessionFile) {
4019
+ process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
4020
+ }
4021
+
4022
+ await postmortem.quit(0);
4023
+ }
4024
+
4025
+ async checkShutdownRequested(): Promise<void> {
4026
+ if (!this.shutdownRequested) return;
4027
+ await this.shutdown();
4028
+ }
4029
+
4030
+ // Extension UI integration
4031
+ setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void {
4032
+ this.#toolUiContextSetter(uiContext, hasUI);
4033
+ }
4034
+
4035
+ initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void {
4036
+ this.#extensionUiController.initializeHookRunner(uiContext, hasUI);
4037
+ }
4038
+
4039
+ setEditorComponent(
4040
+ factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
4041
+ ): void {
4042
+ const previousEditor = this.editor;
4043
+ const previousText = previousEditor.getText();
4044
+ const nextEditor = factory
4045
+ ? factory(this.ui, getEditorTheme(), this.keybindings)
4046
+ : new CustomEditor(getEditorTheme());
4047
+ if (!factory) this.ui.enableScopedInputRender(nextEditor);
4048
+
4049
+ nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
4050
+ nextEditor.setImeSafeCursorLayout(this.settings.get("tui.imeSafeCursor"));
4051
+ nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
4052
+ nextEditor.onAutocompleteCancel = () => {
4053
+ this.ui.requestRender(true);
4054
+ };
4055
+ nextEditor.onAutocompleteUpdate = () => {
4056
+ this.ui.requestRender();
4057
+ };
4058
+ nextEditor.setShimmerRepaintHandler(() => this.ui.requestComponentRender(this.editor));
4059
+ nextEditor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
4060
+ nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
4061
+ if (this.historyStorage) {
4062
+ nextEditor.setHistoryStorage(this.historyStorage);
4063
+ }
4064
+ nextEditor.setText(previousText);
4065
+
4066
+ this.editorContainer.clear();
4067
+ this.editor = nextEditor;
4068
+ this.editorContainer.addChild(nextEditor);
4069
+ this.ui.setFocus(nextEditor);
4070
+
4071
+ this.#inputController.setupKeyHandlers();
4072
+ this.#inputController.setupEditorSubmitHandler();
4073
+
4074
+ void this.refreshSlashCommandState().catch(error => {
4075
+ logger.warn("Failed to refresh slash command state for custom editor", { error: String(error) });
4076
+ });
4077
+
4078
+ this.updateEditorBorderColor();
4079
+ this.ui.requestRender();
4080
+ }
4081
+
4082
+ // UI helpers
4083
+ present(content: Component | readonly Component[]): void {
4084
+ if (Array.isArray(content)) {
4085
+ for (const item of content) this.#mountChatChild(item);
4086
+ } else {
4087
+ this.#mountChatChild(content as Component);
4088
+ }
4089
+ this.ui.requestRender();
4090
+ }
4091
+
4092
+ /** Defer transcript command panels until the active turn can no longer grow above them. */
4093
+ presentCommandOutput(content: Component | readonly Component[]): void {
4094
+ if (!this.session.isStreaming) {
4095
+ this.present(content);
4096
+ return;
4097
+ }
4098
+ const sessionId = this.sessionManager.getSessionId();
4099
+ if (this.#pendingCommandOutput.length > 0 && this.#pendingCommandOutputSessionId !== sessionId) {
4100
+ this.#pendingCommandOutput = [];
4101
+ }
4102
+ this.#pendingCommandOutputSessionId = sessionId;
4103
+ const items = Array.isArray(content) ? content : [content as Component];
4104
+ this.#pendingCommandOutput.push(...items);
4105
+ }
4106
+
4107
+ /** Mount every command panel queued for the current session while the agent was streaming. */
4108
+ flushPendingCommandOutput(): void {
4109
+ if (this.#pendingCommandOutput.length === 0) return;
4110
+ const pending = this.#pendingCommandOutput;
4111
+ const pendingSessionId = this.#pendingCommandOutputSessionId;
4112
+ this.#pendingCommandOutput = [];
4113
+ this.#pendingCommandOutputSessionId = undefined;
4114
+ if (pendingSessionId !== this.sessionManager.getSessionId()) return;
4115
+ this.present(pending);
4116
+ }
4117
+
4118
+ #mountChatChild(item: Component): void {
4119
+ this.chatContainer.addChild(item);
4120
+ if (item instanceof ChatBlock) item.mount(this.#chatHost);
4121
+ }
4122
+
4123
+ resetTranscript(): void {
4124
+ this.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
4125
+ this.chatContainer.dispose();
4126
+ this.chatContainer.clear();
4127
+ }
4128
+
4129
+ showStatus(message: string, options?: { dim?: boolean }): void {
4130
+ this.#uiHelpers.showStatus(message, options);
4131
+ }
4132
+
4133
+ showError(message: string): void {
4134
+ this.#pendingSubmittedInput = undefined;
4135
+ this.clearOptimisticUserMessage();
4136
+ this.#pendingWorkingMessage = undefined;
4137
+ if (this.loadingAnimation) {
4138
+ this.#stopLoadingAnimation(true);
4139
+ }
4140
+ this.#uiHelpers.showError(message);
4141
+ }
4142
+
4143
+ showPinnedError(message: string): void {
4144
+ this.#dismissPlanReview();
4145
+ this.errorBannerContainer.clear();
4146
+ this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
4147
+ this.ui.requestRender();
4148
+ }
4149
+
4150
+ clearPinnedError(): void {
4151
+ if (this.errorBannerContainer.children.length === 0) return;
4152
+ this.errorBannerContainer.clear();
4153
+ this.ui.requestRender();
4154
+ }
4155
+
4156
+ showWarning(message: string): void {
4157
+ this.#uiHelpers.showWarning(message);
4158
+ }
4159
+
4160
+ #handleLspStartupEvent(event: LspStartupEvent): void {
4161
+ this.#updateWelcomeLspServers();
4162
+
4163
+ if (event.type === "failed") {
4164
+ this.showWarning(`LSP startup failed: ${event.error}. It will retry lazily on write.`);
4165
+ return;
4166
+ }
4167
+
4168
+ const failedServers = event.servers.filter(server => server.status === "error");
4169
+
4170
+ if (failedServers.length === 1) {
4171
+ const failedServer = failedServers[0];
4172
+ const detail = failedServer.error ? `: ${failedServer.error}` : "";
4173
+ this.showWarning(`LSP startup failed for ${failedServer.name}${detail}. It will retry lazily on write.`);
4174
+ return;
4175
+ }
4176
+
4177
+ if (failedServers.length > 1) {
4178
+ const failedNames = failedServers.map(server => server.name).join(", ");
4179
+ this.showWarning(`LSP startup failed for ${failedNames}. It will retry lazily on write.`);
4180
+ }
4181
+ }
4182
+
4183
+ #getWelcomeLspServers(): WelcomeLspServerInfo[] {
4184
+ return (
4185
+ this.lspServers?.map(server => ({
4186
+ name: server.name,
4187
+ status: server.status,
4188
+ fileTypes: server.fileTypes,
4189
+ })) ?? []
4190
+ );
4191
+ }
4192
+
4193
+ #updateWelcomeLspServers(): void {
4194
+ if (!this.#welcomeComponent) {
4195
+ return;
4196
+ }
4197
+
4198
+ this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
4199
+ this.ui.requestRender();
4200
+ }
4201
+
4202
+ #clearWorkingMessageAccentCache(): void {
4203
+ this.#workingMessageAccentCacheKey = undefined;
4204
+ this.#workingMessageAccentCacheValue = undefined;
4205
+ this.#workingMessageAccentCacheHasValue = false;
4206
+ }
4207
+
4208
+ #buildWorkingMessageAccentCacheKey(): WorkingMessageAccentCacheKey {
4209
+ const sessionAccentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
4210
+ return {
4211
+ sessionAccentEnabled,
4212
+ sessionName: sessionAccentEnabled ? this.sessionManager.getSessionName() : undefined,
4213
+ accentSurfaceLuminance: theme.accentSurfaceLuminance,
4214
+ };
4215
+ }
4216
+
4217
+ #workingMessageAccentCacheKeyEquals(a: WorkingMessageAccentCacheKey, b: WorkingMessageAccentCacheKey): boolean {
4218
+ return (
4219
+ a.sessionName === b.sessionName &&
4220
+ a.accentSurfaceLuminance === b.accentSurfaceLuminance &&
4221
+ a.sessionAccentEnabled === b.sessionAccentEnabled
4222
+ );
4223
+ }
4224
+
4225
+ #cacheWorkingMessageAccent(
4226
+ key: WorkingMessageAccentCacheKey,
4227
+ value: WorkingMessageAccent | undefined,
4228
+ ): WorkingMessageAccent | undefined {
4229
+ this.#workingMessageAccentCacheKey = key;
4230
+ this.#workingMessageAccentCacheValue = value;
4231
+ this.#workingMessageAccentCacheHasValue = true;
4232
+ return value;
4233
+ }
4234
+
4235
+ #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
4236
+ const key = this.#buildWorkingMessageAccentCacheKey();
4237
+ if (
4238
+ this.#workingMessageAccentCacheHasValue &&
4239
+ this.#workingMessageAccentCacheKey &&
4240
+ this.#workingMessageAccentCacheKeyEquals(key, this.#workingMessageAccentCacheKey)
4241
+ ) {
4242
+ return this.#workingMessageAccentCacheValue;
4243
+ }
4244
+ if (!key.sessionAccentEnabled || !key.sessionName) {
4245
+ return this.#cacheWorkingMessageAccent(key, undefined);
4246
+ }
4247
+ const hex = getSessionAccentHex(key.sessionName, theme.getMajorThemeColorHexes(), key.accentSurfaceLuminance);
4248
+ const main = getSessionAccentAnsi(hex);
4249
+ const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
4250
+ return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
4251
+ }
4252
+
4253
+ ensureLoadingAnimation(): void {
4254
+ if (!this.loadingAnimation) {
4255
+ this.#clearWorkingMessageAccentCache();
4256
+ this.statusContainer.disposeChildren();
4257
+ const messageColorFn = ((message: string) =>
4258
+ renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
4259
+ animated?: true;
4260
+ };
4261
+ // Shimmer drives the 30fps redraw; when it is disabled the working
4262
+ // message is static, so leave `animated` unset and let the loader use
4263
+ // the spinner-only ~12.5fps cadence instead of repainting a frozen line.
4264
+ if (shimmerEnabled()) messageColorFn.animated = true;
4265
+ this.loadingAnimation = new Loader(
4266
+ this.ui,
4267
+ spinner => {
4268
+ const accent = this.#getWorkingMessageAccent();
4269
+ return accent ? `${accent.main}${spinner}\x1b[39m` : theme.fg("accent", spinner);
4270
+ },
4271
+ messageColorFn,
4272
+ this.#defaultWorkingMessage,
4273
+ getSymbolTheme().spinnerFrames,
4274
+ );
4275
+ this.statusContainer.addChild(this.loadingAnimation);
4276
+ } else if (!this.statusContainer.children.includes(this.loadingAnimation)) {
4277
+ this.statusContainer.disposeChildren();
4278
+ this.statusContainer.addChild(this.loadingAnimation);
4279
+ this.ui.requestRender();
4280
+ }
4281
+ this.applyPendingWorkingMessage();
4282
+ }
4283
+
4284
+ #stopLoadingAnimation(clearStatusContainer: boolean): void {
4285
+ if (!this.loadingAnimation) return;
4286
+ this.loadingAnimation.stop();
4287
+ this.loadingAnimation = undefined;
4288
+ this.#clearWorkingMessageAccentCache();
4289
+ if (clearStatusContainer) {
4290
+ this.statusContainer.disposeChildren();
4291
+ }
4292
+ }
4293
+
4294
+ setWorkingMessage(message?: string): void {
4295
+ if (message === undefined) {
4296
+ this.#pendingWorkingMessage = undefined;
4297
+ if (this.loadingAnimation) {
4298
+ this.loadingAnimation.setMessage(this.#defaultWorkingMessage);
4299
+ }
4300
+ return;
4301
+ }
4302
+
4303
+ if (this.loadingAnimation) {
4304
+ this.loadingAnimation.setMessage(message);
4305
+ return;
4306
+ }
4307
+
4308
+ this.#pendingWorkingMessage = message;
4309
+ }
4310
+
4311
+ applyPendingWorkingMessage(): void {
4312
+ if (this.#pendingWorkingMessage === undefined) {
4313
+ return;
4314
+ }
4315
+
4316
+ const message = this.#pendingWorkingMessage;
4317
+ this.#pendingWorkingMessage = undefined;
4318
+ this.setWorkingMessage(message);
4319
+ }
4320
+
4321
+ showNewVersionNotification(newVersion: string): void {
4322
+ this.#uiHelpers.showNewVersionNotification(newVersion);
4323
+ }
4324
+
4325
+ clearEditor(): void {
4326
+ this.#uiHelpers.clearEditor();
4327
+ }
4328
+
4329
+ updatePendingMessagesDisplay(): void {
4330
+ this.#uiHelpers.updatePendingMessagesDisplay();
4331
+ }
4332
+
4333
+ queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void {
4334
+ this.#uiHelpers.queueCompactionMessage(text, mode, images);
4335
+ }
4336
+
4337
+ flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
4338
+ return this.#uiHelpers.flushCompactionQueue(options);
4339
+ }
4340
+
4341
+ flushPendingBashComponents(): void {
4342
+ this.#uiHelpers.flushPendingBashComponents();
4343
+ }
4344
+
4345
+ isKnownSlashCommand(text: string): boolean {
4346
+ return this.#uiHelpers.isKnownSlashCommand(text);
4347
+ }
4348
+
4349
+ addMessageToChat(
4350
+ message: AgentMessage,
4351
+ options?: {
4352
+ populateHistory?: boolean;
4353
+ imageLinks?: readonly (string | undefined)[];
4354
+ reuseSettledComponent?: boolean;
4355
+ },
4356
+ ): Component[] {
4357
+ return this.#uiHelpers.addMessageToChat(message, options);
4358
+ }
4359
+
4360
+ renderSessionContext(sessionContext: SessionContext, options?: RenderSessionContextOptions): void {
4361
+ for (const message of sessionContext.messages) {
4362
+ this.noteDisplayableThinkingContent(message);
4363
+ }
4364
+ this.#uiHelpers.renderSessionContext(sessionContext, options);
4365
+ }
4366
+
4367
+ renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void {
4368
+ this.#uiHelpers.renderInitialMessages(options);
4369
+ }
4370
+
4371
+ getUserMessageText(message: Message): string {
4372
+ return this.#uiHelpers.getUserMessageText(message);
4373
+ }
4374
+
4375
+ findLastAssistantMessage(): AssistantMessage | undefined {
4376
+ return this.#uiHelpers.findLastAssistantMessage();
4377
+ }
4378
+
4379
+ extractAssistantText(message: AssistantMessage): string {
4380
+ return this.#uiHelpers.extractAssistantText(message);
4381
+ }
4382
+
4383
+ // Command handling
4384
+ handleExportCommand(text: string): Promise<void> {
4385
+ return this.#commandController.handleExportCommand(text);
4386
+ }
4387
+
4388
+ async handleDumpCommand(): Promise<void> {
4389
+ return this.#commandController.handleDumpCommand();
4390
+ }
4391
+
4392
+ handleAdvisorDumpCommand(isRaw?: boolean) {
4393
+ return this.#commandController.handleAdvisorDumpCommand(isRaw);
4394
+ }
4395
+
4396
+ handleDebugTranscriptCommand(): Promise<void> {
4397
+ return this.#commandController.handleDebugTranscriptCommand();
4398
+ }
4399
+
4400
+ handleShareCommand(): Promise<void> {
4401
+ return this.#commandController.handleShareCommand();
4402
+ }
4403
+
4404
+ handleTodoCommand(args: string): Promise<void> {
4405
+ return this.#todoCommandController.handleTodoCommand(args);
4406
+ }
4407
+
4408
+ handleSessionCommand(): Promise<void> {
4409
+ return this.#commandController.handleSessionCommand();
4410
+ }
4411
+
4412
+ handleAdvisorStatusCommand(): Promise<void> {
4413
+ return this.#commandController.handleAdvisorStatusCommand();
4414
+ }
4415
+
4416
+ handleJobsCommand(): Promise<void> {
4417
+ return this.#commandController.handleJobsCommand();
4418
+ }
4419
+
4420
+ handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
4421
+ return this.#commandController.handleUsageCommand(reports);
4422
+ }
4423
+
4424
+ async handleChangelogCommand(showFull = false): Promise<void> {
4425
+ await this.#commandController.handleChangelogCommand(showFull);
4426
+ }
4427
+
4428
+ handleHotkeysCommand(): void {
4429
+ this.#commandController.handleHotkeysCommand();
4430
+ }
4431
+
4432
+ handleToolsCommand(): void {
4433
+ this.#commandController.handleToolsCommand();
4434
+ }
4435
+
4436
+ handleContextCommand(): void {
4437
+ this.#commandController.handleContextCommand();
4438
+ }
4439
+
4440
+ #vibeSessionTransitionBlocked(): boolean {
4441
+ if (!this.vibeModeEnabled) return false;
4442
+ this.showWarning("Exit vibe mode first.");
4443
+ return true;
4444
+ }
4445
+
4446
+ #prepareSessionSwitch(): void {
4447
+ this.#btwController.dispose();
4448
+ this.#omfgController.dispose();
4449
+ this.#extensionUiController.clearExtensionTerminalInputListeners();
4450
+ this.clearPinnedError();
4451
+ this.#hidePlanReview();
4452
+ }
4453
+
4454
+ async handleClearCommand(): Promise<void> {
4455
+ if (this.#vibeSessionTransitionBlocked()) return;
4456
+ this.#prepareSessionSwitch();
4457
+ await this.#commandController.handleClearCommand();
4458
+ }
4459
+
4460
+ handleFreshCommand(): Promise<void> {
4461
+ return this.#commandController.handleFreshCommand();
4462
+ }
4463
+
4464
+ async handleDropCommand(): Promise<void> {
4465
+ if (this.#vibeSessionTransitionBlocked()) return;
4466
+ this.#prepareSessionSwitch();
4467
+ await this.#commandController.handleDropCommand();
4468
+ }
4469
+
4470
+ async handleForkCommand(): Promise<void> {
4471
+ if (this.#vibeSessionTransitionBlocked()) return;
4472
+ this.#btwController.dispose();
4473
+ this.#omfgController.dispose();
4474
+ await this.#commandController.handleForkCommand();
4475
+ }
4476
+
4477
+ async handleMoveCommand(targetPath?: string): Promise<void> {
4478
+ if (this.#vibeSessionTransitionBlocked()) return;
4479
+ await this.#commandController.handleMoveCommand(targetPath);
4480
+ }
4481
+
4482
+ handleRenameCommand(title: string): Promise<void> {
4483
+ return this.#commandController.handleRenameCommand(title);
4484
+ }
4485
+
4486
+ handleMemoryCommand(text: string): Promise<void> {
4487
+ return this.#commandController.handleMemoryCommand(text);
4488
+ }
4489
+
4490
+ async handleSTTToggle(): Promise<void> {
4491
+ if (this.#liveCommandController.active) {
4492
+ this.showWarning("End live mode before using push-to-talk speech input.");
4493
+ return;
4494
+ }
4495
+ if (!settings.get("stt.enabled")) {
4496
+ this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
4497
+ return;
4498
+ }
4499
+ if (!this.#sttController) {
4500
+ this.#sttController = new STTController();
4501
+ }
4502
+ await this.#sttController.toggle(this.editor, {
4503
+ showWarning: (msg: string) => this.showWarning(msg),
4504
+ showStatus: (msg: string) => this.showStatus(msg),
4505
+ requestRender: () => this.ui.requestRender(),
4506
+ onStateChange: (state: SttState) => {
4507
+ // Duck assistant speech while the user is talking (push-to-talk); restore after.
4508
+ if (state === "recording") vocalizer.duck();
4509
+ else vocalizer.unduck();
4510
+ if (state === "recording") {
4511
+ this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
4512
+ this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
4513
+ this.ui.setShowHardwareCursor(false);
4514
+ this.editor.setUseTerminalCursor(false);
4515
+ this.#startMicAnimation();
4516
+ } else if (state === "transcribing") {
4517
+ this.#stopMicAnimation();
4518
+ this.#setMicCursor({ r: 200, g: 200, b: 200 });
4519
+ } else {
4520
+ this.#cleanupMicAnimation();
4521
+ }
4522
+ this.ui.requestRender();
4523
+ },
4524
+ });
4525
+ }
4526
+
4527
+ /** Start or stop the Codex-backed realtime voice surface. */
4528
+ async handleLiveCommand(): Promise<void> {
4529
+ if (this.#sttController && this.#sttController.state !== "idle") {
4530
+ this.showWarning("Finish the current speech-to-text capture before starting live mode.");
4531
+ return;
4532
+ }
4533
+ await this.#liveCommandController.handleCommand();
4534
+ }
4535
+
4536
+ #setMicCursor(color: { r: number; g: number; b: number }): void {
4537
+ this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
4538
+ // Theme symbols can be wide (for example, 🎤), so measure the rendered override.
4539
+ this.editor.cursorOverrideWidth = visibleWidth(this.editor.cursorOverride);
4540
+ }
4541
+
4542
+ #updateMicIcon(): void {
4543
+ const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
4544
+ this.#setMicCursor({ r, g, b });
4545
+ }
4546
+
4547
+ #startMicAnimation(): void {
4548
+ if (this.#voiceAnimationInterval) return;
4549
+ this.#voiceHue = 0;
4550
+ this.#updateMicIcon();
4551
+ this.#voiceAnimationInterval = setInterval(() => {
4552
+ this.#voiceHue = (this.#voiceHue + 8) % 360;
4553
+ this.#updateMicIcon();
4554
+ // Component-scoped: the hue sweep only recolors the editor's cursor
4555
+ // glyph, so the transcript subtree is reused per animation frame.
4556
+ this.ui.requestComponentRender(this.editor);
4557
+ }, 60);
4558
+ }
4559
+
4560
+ #stopMicAnimation(): void {
4561
+ if (this.#voiceAnimationInterval) {
4562
+ clearInterval(this.#voiceAnimationInterval);
4563
+ this.#voiceAnimationInterval = undefined;
4564
+ }
4565
+ }
4566
+
4567
+ #cleanupMicAnimation(): void {
4568
+ if (this.#voiceAnimationInterval) {
4569
+ clearInterval(this.#voiceAnimationInterval);
4570
+ this.#voiceAnimationInterval = undefined;
4571
+ }
4572
+ this.editor.cursorOverride = undefined;
4573
+ this.editor.cursorOverrideWidth = undefined;
4574
+ if (this.#voicePreviousShowHardwareCursor !== null) {
4575
+ this.ui.setShowHardwareCursor(this.#voicePreviousShowHardwareCursor);
4576
+ this.#voicePreviousShowHardwareCursor = null;
4577
+ }
4578
+ if (this.#voicePreviousUseTerminalCursor !== null) {
4579
+ this.editor.setUseTerminalCursor(this.#voicePreviousUseTerminalCursor);
4580
+ this.#voicePreviousUseTerminalCursor = null;
4581
+ }
4582
+ }
4583
+
4584
+ async showDebugSelector(): Promise<void> {
4585
+ await this.#selectorController.showDebugSelector();
4586
+ }
4587
+
4588
+ showAgentHub(options?: { requireContent?: boolean; armCloseTap?: boolean }): void {
4589
+ this.#selectorController.showAgentHub(this.#observerRegistry, options);
4590
+ }
4591
+
4592
+ resetObserverRegistry(): void {
4593
+ this.#observerRegistry.resetSessions();
4594
+ this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
4595
+ }
4596
+
4597
+ handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void> {
4598
+ return this.#commandController.handleBashCommand(command, excludeFromContext);
4599
+ }
4600
+
4601
+ handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void> {
4602
+ return this.#commandController.handlePythonCommand(code, excludeFromContext);
4603
+ }
4604
+
4605
+ async handleMCPCommand(text: string): Promise<void> {
4606
+ const controller = new MCPCommandController(this);
4607
+ await controller.handle(text);
4608
+ }
4609
+
4610
+ async handleSSHCommand(text: string): Promise<void> {
4611
+ const controller = new SSHCommandController(this);
4612
+ await controller.handle(text);
4613
+ }
4614
+
4615
+ handleCompactCommand(
4616
+ customInstructions?: string,
4617
+ mode?: CompactMode,
4618
+ beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
4619
+ internalGuidance?: string,
4620
+ ): Promise<CompactionOutcome> {
4621
+ return this.#commandController.handleCompactCommand(customInstructions, mode, beforeFlush, internalGuidance);
4622
+ }
4623
+
4624
+ handleHandoffCommand(customInstructions?: string): Promise<void> {
4625
+ return this.#commandController.handleHandoffCommand(customInstructions);
4626
+ }
4627
+
4628
+ handleShakeCommand(mode: ShakeMode): Promise<void> {
4629
+ return this.#commandController.handleShakeCommand(mode);
4630
+ }
4631
+
4632
+ executeCompaction(
4633
+ customInstructionsOrOptions?: string | CompactOptions,
4634
+ isAuto?: boolean,
4635
+ ): Promise<CompactionOutcome> {
4636
+ return this.#commandController.executeCompaction(customInstructionsOrOptions, isAuto);
4637
+ }
4638
+
4639
+ openInBrowser(urlOrPath: string): void {
4640
+ this.#commandController.openInBrowser(urlOrPath);
4641
+ }
4642
+
4643
+ // Selector handling
4644
+ showSettingsSelector(): void {
4645
+ this.#selectorController.showSettingsSelector();
4646
+ }
4647
+
4648
+ showAdvisorConfigure(): void {
4649
+ this.#selectorController.showAdvisorConfigure();
4650
+ }
4651
+
4652
+ showHistorySearch(): void {
4653
+ this.#selectorController.showHistorySearch();
4654
+ }
4655
+
4656
+ showExtensionsDashboard(): void {
4657
+ void this.#selectorController.showExtensionsDashboard();
4658
+ }
4659
+
4660
+ showAgentsDashboard(): void {
4661
+ void this.#selectorController.showAgentsDashboard();
4662
+ }
4663
+
4664
+ showModelSelector(options?: { temporaryOnly?: boolean }): void {
4665
+ this.#selectorController.showModelSelector(options);
4666
+ }
4667
+
4668
+ showPluginSelector(mode?: "install" | "uninstall"): void {
4669
+ void this.#selectorController.showPluginSelector(mode);
4670
+ }
4671
+
4672
+ showUserMessageSelector(): void {
4673
+ this.#selectorController.showUserMessageSelector();
4674
+ }
4675
+
4676
+ showCopySelector(): void {
4677
+ this.#selectorController.showCopySelector();
4678
+ }
4679
+
4680
+ showTreeSelector(): void {
4681
+ this.#selectorController.showTreeSelector();
4682
+ }
4683
+
4684
+ showSessionSelector(): void {
4685
+ this.#selectorController.showSessionSelector();
4686
+ }
4687
+
4688
+ async handleResumeSession(sessionPath: string): Promise<void> {
4689
+ // Flush pending settings writes *before* disposing controllers or resetting
4690
+ // observers: a save failure must leave the session, process project dir,
4691
+ // and Settings in the source scope with all UI intact.
4692
+ try {
4693
+ await this.settings.flush();
4694
+ } catch (err) {
4695
+ this.showError(`Failed to save pending settings: ${err instanceof Error ? err.message : String(err)}`);
4696
+ return;
4697
+ }
4698
+ this.#btwController.dispose();
4699
+ this.#omfgController.dispose();
4700
+ this.resetObserverRegistry();
4701
+ await this.#selectorController.handleResumeSession(sessionPath, { settingsFlushed: true });
4702
+ }
4703
+
4704
+ handleSessionDeleteCommand(): Promise<void> {
4705
+ return this.#selectorController.handleSessionDeleteCommand();
4706
+ }
4707
+
4708
+ showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
4709
+ return this.#selectorController.showOAuthSelector(mode, providerId);
4710
+ }
4711
+
4712
+ showSessionPinSelector(): Promise<void> {
4713
+ return this.#selectorController.showSessionPinSelector();
4714
+ }
4715
+
4716
+ showResetUsageSelector(): Promise<void> {
4717
+ return this.#selectorController.showResetUsageSelector();
4718
+ }
4719
+
4720
+ showProviderSetup(): Promise<void> {
4721
+ return runProviderSetupWizard(this);
4722
+ }
4723
+
4724
+ showHookConfirm(title: string, message: string): Promise<boolean> {
4725
+ return this.#extensionUiController.showHookConfirm(title, message);
4726
+ }
4727
+
4728
+ // Input handling
4729
+ handleCtrlC(): void {
4730
+ this.#inputController.handleCtrlC();
4731
+ }
4732
+
4733
+ handleCtrlD(): void {
4734
+ this.#inputController.handleCtrlD();
4735
+ }
4736
+
4737
+ handleCtrlZ(): void {
4738
+ this.#inputController.handleCtrlZ();
4739
+ }
4740
+
4741
+ handleDequeue(): void {
4742
+ this.#inputController.handleDequeue();
4743
+ }
4744
+
4745
+ handleImagePaste(): Promise<boolean> {
4746
+ return this.#inputController.handleImagePaste();
4747
+ }
4748
+
4749
+ /** Queue slash-command input behind the active turn. */
4750
+ handleQueueCommand(message: string): Promise<void> {
4751
+ return this.#inputController.handleQueueCommand(message);
4752
+ }
4753
+
4754
+ handleBtwCommand(question: string): Promise<void> {
4755
+ return this.#btwController.start(question);
4756
+ }
4757
+
4758
+ handleTanCommand(work: string): Promise<void> {
4759
+ return this.#tanCommandController.start(work);
4760
+ }
4761
+
4762
+ hasActiveBtw(): boolean {
4763
+ return this.#btwController.hasActiveRequest();
4764
+ }
4765
+
4766
+ handleBtwEscape(): boolean {
4767
+ return this.#btwController.handleEscape();
4768
+ }
4769
+
4770
+ canBranchBtw(): boolean {
4771
+ return this.#btwController.canBranch();
4772
+ }
4773
+
4774
+ handleBtwBranchKey(): Promise<boolean> {
4775
+ return this.#btwController.handleBranch();
4776
+ }
4777
+
4778
+ canCopyBtw(): boolean {
4779
+ return this.#btwController.canCopy();
4780
+ }
4781
+
4782
+ handleBtwCopyKey(): Promise<boolean> {
4783
+ return this.#btwController.handleCopy();
4784
+ }
4785
+
4786
+ async handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void> {
4787
+ try {
4788
+ const result = await this.session.branchFromBtw(question, assistantMessage);
4789
+ if (result.cancelled) {
4790
+ this.showStatus("/btw branch cancelled", { dim: true });
4791
+ return;
4792
+ }
4793
+ this.#btwController.dispose();
4794
+ this.#omfgController.dispose();
4795
+ this.renderInitialMessages({ clearTerminalHistory: true });
4796
+ this.updateEditorBorderColor();
4797
+ this.showStatus(
4798
+ result.sessionFile ? `Branched /btw to ${path.basename(result.sessionFile)}` : "Branched /btw",
4799
+ );
4800
+ } catch (error) {
4801
+ this.showError(`Cannot branch /btw: ${error instanceof Error ? error.message : String(error)}`);
4802
+ }
4803
+ }
4804
+
4805
+ handleOmfgCommand(complaint: string): Promise<void> {
4806
+ return this.#omfgController.start(complaint);
4807
+ }
4808
+
4809
+ hasActiveOmfg(): boolean {
4810
+ return this.#omfgController.hasActiveRequest();
4811
+ }
4812
+
4813
+ handleOmfgEscape(): boolean {
4814
+ return this.#omfgController.handleEscape();
4815
+ }
4816
+
4817
+ cycleThinkingLevel(): void {
4818
+ this.#inputController.cycleThinkingLevel();
4819
+ }
4820
+
4821
+ cycleRoleModel(direction?: "forward" | "backward"): Promise<void> {
4822
+ return this.#inputController.cycleRoleModel(direction);
4823
+ }
4824
+
4825
+ toggleToolOutputExpansion(): void {
4826
+ this.#inputController.toggleToolOutputExpansion();
4827
+ }
4828
+
4829
+ setToolsExpanded(expanded: boolean): void {
4830
+ this.#inputController.setToolsExpanded(expanded);
4831
+ }
4832
+
4833
+ toggleThinkingBlockVisibility(): void {
4834
+ this.#inputController.toggleThinkingBlockVisibility();
4835
+ }
4836
+
4837
+ toggleTodoExpansion(): void {
4838
+ this.todoExpanded = !this.todoExpanded;
4839
+ this.#renderTodoList();
4840
+ this.ui.requestRender();
4841
+ }
4842
+
4843
+ setTodos(todos: TodoItem[] | TodoPhase[]): void {
4844
+ if (todos.length > 0 && "tasks" in todos[0]) {
4845
+ this.todoPhases = todos as TodoPhase[];
4846
+ } else {
4847
+ this.todoPhases = [
4848
+ {
4849
+ name: "Todos",
4850
+ tasks: todos as TodoItem[],
4851
+ },
4852
+ ];
4853
+ }
4854
+ this.#syncTodoAutoClearTimer();
4855
+ this.#renderTodoList();
4856
+ this.ui.requestRender();
4857
+ }
4858
+
4859
+ async reloadTodos(): Promise<void> {
4860
+ await this.#loadTodoList();
4861
+ this.ui.requestRender();
4862
+ }
4863
+
4864
+ openExternalEditor(): void {
4865
+ this.#inputController.openExternalEditor();
4866
+ }
4867
+
4868
+ registerExtensionShortcuts(): void {
4869
+ this.#inputController.registerExtensionShortcuts();
4870
+ }
4871
+
4872
+ // Hook UI methods
4873
+ initHooksAndCustomTools(): Promise<void> {
4874
+ return this.#extensionUiController.initHooksAndCustomTools();
4875
+ }
4876
+
4877
+ getToolUIContext(): ExtensionUIContext | undefined {
4878
+ return this.#extensionUiController.getToolUIContext();
4879
+ }
4880
+
4881
+ emitCustomToolSessionEvent(
4882
+ reason: "start" | "switch" | "branch" | "tree" | "shutdown",
4883
+ previousSessionFile?: string,
4884
+ ): Promise<void> {
4885
+ return this.#extensionUiController.emitCustomToolSessionEvent(reason, previousSessionFile);
4886
+ }
4887
+
4888
+ setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
4889
+ this.#extensionUiController.setHookWidget(key, content, options);
4890
+ }
4891
+
4892
+ setHookStatus(key: string, text: string | undefined): void {
4893
+ this.#extensionUiController.setHookStatus(key, text);
4894
+ }
4895
+
4896
+ showHookSelector(
4897
+ title: string,
4898
+ options: ExtensionUISelectItem[],
4899
+ dialogOptions?: InteractiveSelectorDialogOptions,
4900
+ extra?: { slider?: HookSelectorSlider },
4901
+ ): Promise<string | undefined> {
4902
+ return this.#extensionUiController.showHookSelector(title, options, dialogOptions, extra);
4903
+ }
4904
+
4905
+ hideHookSelector(): void {
4906
+ this.#extensionUiController.hideHookSelector();
4907
+ }
4908
+
4909
+ showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
4910
+ return this.#extensionUiController.showHookInput(title, placeholder);
4911
+ }
4912
+
4913
+ hideHookInput(): void {
4914
+ this.#extensionUiController.hideHookInput();
4915
+ }
4916
+
4917
+ showHookEditor(
4918
+ title: string,
4919
+ prefill?: string,
4920
+ dialogOptions?: ExtensionUIDialogOptions,
4921
+ editorOptions?: { promptStyle?: boolean },
4922
+ ): Promise<string | undefined> {
4923
+ return this.#extensionUiController.showHookEditor(title, prefill, dialogOptions, editorOptions);
4924
+ }
4925
+
4926
+ hideHookEditor(): void {
4927
+ this.#extensionUiController.hideHookEditor();
4928
+ }
4929
+
4930
+ showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
4931
+ this.#extensionUiController.showHookNotify(message, type);
4932
+ }
4933
+
4934
+ showHookCustom<T>(
4935
+ factory: (
4936
+ tui: TUI,
4937
+ theme: Theme,
4938
+ keybindings: KeybindingsManager,
4939
+ done: (result: T) => void,
4940
+ ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
4941
+ options?: { overlay?: boolean },
4942
+ ): Promise<T> {
4943
+ return this.#extensionUiController.showHookCustom(factory, options);
4944
+ }
4945
+
4946
+ showExtensionError(extensionPath: string, error: string): void {
4947
+ this.#extensionUiController.showExtensionError(extensionPath, error);
4948
+ }
4949
+
4950
+ showToolError(toolName: string, error: string): void {
4951
+ this.#extensionUiController.showToolError(toolName, error);
4952
+ }
4953
+
4954
+ #subscribeToAgent(): void {
4955
+ this.#eventController.subscribeToAgent();
4956
+ }
4957
+ }