@wahack/pi-coding-agent 15.11.0

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 (1152) hide show
  1. package/CHANGELOG.md +10031 -0
  2. package/README.md +36 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +104 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/extensions/README.md +142 -0
  7. package/examples/extensions/api-demo.ts +79 -0
  8. package/examples/extensions/chalk-logger.ts +25 -0
  9. package/examples/extensions/hello.ts +31 -0
  10. package/examples/extensions/pirate.ts +43 -0
  11. package/examples/extensions/plan-mode.ts +549 -0
  12. package/examples/extensions/reload-runtime.ts +38 -0
  13. package/examples/extensions/thinking-note.ts +13 -0
  14. package/examples/extensions/tools.ts +145 -0
  15. package/examples/extensions/with-deps/index.ts +36 -0
  16. package/examples/extensions/with-deps/package-lock.json +31 -0
  17. package/examples/extensions/with-deps/package.json +17 -0
  18. package/examples/hooks/README.md +56 -0
  19. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  20. package/examples/hooks/confirm-destructive.ts +58 -0
  21. package/examples/hooks/custom-compaction.ts +115 -0
  22. package/examples/hooks/dirty-repo-guard.ts +51 -0
  23. package/examples/hooks/file-trigger.ts +40 -0
  24. package/examples/hooks/git-checkpoint.ts +52 -0
  25. package/examples/hooks/handoff.ts +149 -0
  26. package/examples/hooks/permission-gate.ts +33 -0
  27. package/examples/hooks/protected-paths.ts +29 -0
  28. package/examples/hooks/qna.ts +118 -0
  29. package/examples/hooks/status-line.ts +39 -0
  30. package/examples/sdk/01-minimal.ts +21 -0
  31. package/examples/sdk/02-custom-model.ts +49 -0
  32. package/examples/sdk/03-custom-prompt.ts +46 -0
  33. package/examples/sdk/04-skills.ts +43 -0
  34. package/examples/sdk/06-extensions.ts +82 -0
  35. package/examples/sdk/06-hooks.ts +61 -0
  36. package/examples/sdk/07-context-files.ts +35 -0
  37. package/examples/sdk/08-prompt-templates.ts +41 -0
  38. package/examples/sdk/08-slash-commands.ts +46 -0
  39. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  40. package/examples/sdk/11-sessions.ts +47 -0
  41. package/examples/sdk/12-redis-sessions.ts +54 -0
  42. package/examples/sdk/13-sql-sessions.ts +61 -0
  43. package/examples/sdk/README.md +172 -0
  44. package/package.json +554 -0
  45. package/scripts/build-binary.ts +100 -0
  46. package/scripts/bundle-dist.ts +90 -0
  47. package/scripts/format-prompts.ts +68 -0
  48. package/scripts/generate-docs-index.ts +40 -0
  49. package/scripts/generate-template.ts +33 -0
  50. package/scripts/omp +42 -0
  51. package/scripts/omp.ts +19 -0
  52. package/src/async/index.ts +1 -0
  53. package/src/async/job-manager.ts +625 -0
  54. package/src/auto-thinking/classifier.ts +185 -0
  55. package/src/autoresearch/command-resume.md +14 -0
  56. package/src/autoresearch/dashboard.ts +436 -0
  57. package/src/autoresearch/git.ts +319 -0
  58. package/src/autoresearch/helpers.ts +218 -0
  59. package/src/autoresearch/index.ts +536 -0
  60. package/src/autoresearch/prompt-setup.md +43 -0
  61. package/src/autoresearch/prompt.md +103 -0
  62. package/src/autoresearch/resume-message.md +10 -0
  63. package/src/autoresearch/state.ts +273 -0
  64. package/src/autoresearch/storage.ts +699 -0
  65. package/src/autoresearch/tools/init-experiment.ts +272 -0
  66. package/src/autoresearch/tools/log-experiment.ts +524 -0
  67. package/src/autoresearch/tools/run-experiment.ts +407 -0
  68. package/src/autoresearch/tools/update-notes.ts +109 -0
  69. package/src/autoresearch/types.ts +168 -0
  70. package/src/bun-imports.d.ts +28 -0
  71. package/src/capability/context-file.ts +44 -0
  72. package/src/capability/extension-module.ts +34 -0
  73. package/src/capability/extension.ts +47 -0
  74. package/src/capability/fs.ts +117 -0
  75. package/src/capability/hook.ts +40 -0
  76. package/src/capability/index.ts +436 -0
  77. package/src/capability/instruction.ts +37 -0
  78. package/src/capability/mcp.ts +74 -0
  79. package/src/capability/prompt.ts +35 -0
  80. package/src/capability/rule-buckets.ts +66 -0
  81. package/src/capability/rule.ts +261 -0
  82. package/src/capability/settings.ts +34 -0
  83. package/src/capability/skill.ts +63 -0
  84. package/src/capability/slash-command.ts +40 -0
  85. package/src/capability/ssh.ts +41 -0
  86. package/src/capability/system-prompt.ts +34 -0
  87. package/src/capability/tool.ts +38 -0
  88. package/src/capability/types.ts +168 -0
  89. package/src/cli/agents-cli.ts +138 -0
  90. package/src/cli/args.ts +340 -0
  91. package/src/cli/auth-broker-cli.ts +895 -0
  92. package/src/cli/auth-gateway-cli.ts +611 -0
  93. package/src/cli/classify-install-target.ts +76 -0
  94. package/src/cli/claude-trace-cli.ts +795 -0
  95. package/src/cli/commands/init-xdg.ts +27 -0
  96. package/src/cli/completion-gen.ts +550 -0
  97. package/src/cli/config-cli.ts +418 -0
  98. package/src/cli/dry-balance-cli.ts +856 -0
  99. package/src/cli/extension-flags.ts +48 -0
  100. package/src/cli/file-processor.ts +133 -0
  101. package/src/cli/gallery-cli.ts +230 -0
  102. package/src/cli/gallery-fixtures/agentic.ts +407 -0
  103. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  104. package/src/cli/gallery-fixtures/edit.ts +194 -0
  105. package/src/cli/gallery-fixtures/fs.ts +220 -0
  106. package/src/cli/gallery-fixtures/index.ts +40 -0
  107. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  108. package/src/cli/gallery-fixtures/memory.ts +81 -0
  109. package/src/cli/gallery-fixtures/misc.ts +250 -0
  110. package/src/cli/gallery-fixtures/search.ts +213 -0
  111. package/src/cli/gallery-fixtures/shell.ts +167 -0
  112. package/src/cli/gallery-fixtures/types.ts +57 -0
  113. package/src/cli/gallery-fixtures/web.ts +158 -0
  114. package/src/cli/gallery-screenshot.ts +279 -0
  115. package/src/cli/grep-cli.ts +160 -0
  116. package/src/cli/grievances-cli.ts +256 -0
  117. package/src/cli/initial-message.ts +58 -0
  118. package/src/cli/list-models.ts +194 -0
  119. package/src/cli/plugin-cli.ts +996 -0
  120. package/src/cli/read-cli.ts +57 -0
  121. package/src/cli/session-picker.ts +79 -0
  122. package/src/cli/setup-cli.ts +231 -0
  123. package/src/cli/shell-cli.ts +176 -0
  124. package/src/cli/ssh-cli.ts +179 -0
  125. package/src/cli/startup-cwd.ts +68 -0
  126. package/src/cli/stats-cli.ts +238 -0
  127. package/src/cli/tiny-models-cli.ts +127 -0
  128. package/src/cli/update-cli.ts +611 -0
  129. package/src/cli/usage-cli.ts +603 -0
  130. package/src/cli/web-search-cli.ts +132 -0
  131. package/src/cli/worktree-cli.ts +291 -0
  132. package/src/cli-commands.ts +79 -0
  133. package/src/cli.ts +200 -0
  134. package/src/commands/acp.ts +24 -0
  135. package/src/commands/agents.ts +57 -0
  136. package/src/commands/auth-broker.ts +99 -0
  137. package/src/commands/auth-gateway.ts +69 -0
  138. package/src/commands/commit.ts +46 -0
  139. package/src/commands/complete.ts +66 -0
  140. package/src/commands/completions.ts +60 -0
  141. package/src/commands/config.ts +51 -0
  142. package/src/commands/dry-balance.ts +43 -0
  143. package/src/commands/gallery.ts +52 -0
  144. package/src/commands/grep.ts +48 -0
  145. package/src/commands/grievances.ts +51 -0
  146. package/src/commands/install.ts +107 -0
  147. package/src/commands/launch.ts +169 -0
  148. package/src/commands/plugin.ts +78 -0
  149. package/src/commands/read.ts +38 -0
  150. package/src/commands/setup.ts +67 -0
  151. package/src/commands/shell.ts +29 -0
  152. package/src/commands/ssh.ts +60 -0
  153. package/src/commands/stats.ts +29 -0
  154. package/src/commands/tiny-models.ts +36 -0
  155. package/src/commands/update.ts +21 -0
  156. package/src/commands/usage.ts +35 -0
  157. package/src/commands/web-search.ts +42 -0
  158. package/src/commands/worktree.ts +56 -0
  159. package/src/commit/agentic/agent.ts +317 -0
  160. package/src/commit/agentic/fallback.ts +96 -0
  161. package/src/commit/agentic/index.ts +355 -0
  162. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  163. package/src/commit/agentic/prompts/session-user.md +25 -0
  164. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  165. package/src/commit/agentic/prompts/system.md +38 -0
  166. package/src/commit/agentic/state.ts +60 -0
  167. package/src/commit/agentic/tools/analyze-file.ts +146 -0
  168. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  169. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  170. package/src/commit/agentic/tools/git-overview.ts +81 -0
  171. package/src/commit/agentic/tools/index.ts +54 -0
  172. package/src/commit/agentic/tools/propose-changelog.ts +144 -0
  173. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  174. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  175. package/src/commit/agentic/tools/schemas.ts +23 -0
  176. package/src/commit/agentic/tools/split-commit.ts +245 -0
  177. package/src/commit/agentic/topo-sort.ts +44 -0
  178. package/src/commit/agentic/trivial.ts +51 -0
  179. package/src/commit/agentic/validation.ts +183 -0
  180. package/src/commit/analysis/conventional.ts +64 -0
  181. package/src/commit/analysis/index.ts +4 -0
  182. package/src/commit/analysis/scope.ts +242 -0
  183. package/src/commit/analysis/summary.ts +105 -0
  184. package/src/commit/analysis/validation.ts +66 -0
  185. package/src/commit/changelog/detect.ts +40 -0
  186. package/src/commit/changelog/generate.ts +97 -0
  187. package/src/commit/changelog/index.ts +234 -0
  188. package/src/commit/changelog/parse.ts +44 -0
  189. package/src/commit/cli.ts +85 -0
  190. package/src/commit/git/diff.ts +148 -0
  191. package/src/commit/index.ts +5 -0
  192. package/src/commit/map-reduce/index.ts +69 -0
  193. package/src/commit/map-reduce/map-phase.ts +193 -0
  194. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  195. package/src/commit/map-reduce/utils.ts +9 -0
  196. package/src/commit/message.ts +11 -0
  197. package/src/commit/model-selection.ts +92 -0
  198. package/src/commit/pipeline.ts +243 -0
  199. package/src/commit/prompts/analysis-system.md +148 -0
  200. package/src/commit/prompts/analysis-user.md +38 -0
  201. package/src/commit/prompts/changelog-system.md +50 -0
  202. package/src/commit/prompts/changelog-user.md +18 -0
  203. package/src/commit/prompts/file-observer-system.md +24 -0
  204. package/src/commit/prompts/file-observer-user.md +8 -0
  205. package/src/commit/prompts/reduce-system.md +50 -0
  206. package/src/commit/prompts/reduce-user.md +17 -0
  207. package/src/commit/prompts/summary-retry.md +3 -0
  208. package/src/commit/prompts/summary-system.md +38 -0
  209. package/src/commit/prompts/summary-user.md +13 -0
  210. package/src/commit/prompts/types-description.md +2 -0
  211. package/src/commit/shared-llm.ts +77 -0
  212. package/src/commit/types.ts +118 -0
  213. package/src/commit/utils/exclusions.ts +42 -0
  214. package/src/commit/utils.ts +58 -0
  215. package/src/config/api-key-resolver.ts +60 -0
  216. package/src/config/append-only-context-mode.ts +31 -0
  217. package/src/config/config-file.ts +317 -0
  218. package/src/config/file-lock.ts +164 -0
  219. package/src/config/keybindings.ts +628 -0
  220. package/src/config/mcp-schema.json +230 -0
  221. package/src/config/model-discovery.ts +554 -0
  222. package/src/config/model-registry.ts +2090 -0
  223. package/src/config/model-resolver.ts +1502 -0
  224. package/src/config/model-roles.ts +74 -0
  225. package/src/config/models-config-schema.ts +226 -0
  226. package/src/config/models-config.ts +129 -0
  227. package/src/config/prompt-templates.ts +185 -0
  228. package/src/config/resolve-config-value.ts +94 -0
  229. package/src/config/settings-schema.ts +3530 -0
  230. package/src/config/settings.ts +1178 -0
  231. package/src/config.ts +242 -0
  232. package/src/cursor.ts +340 -0
  233. package/src/dap/client.ts +760 -0
  234. package/src/dap/config.ts +189 -0
  235. package/src/dap/defaults.json +212 -0
  236. package/src/dap/index.ts +4 -0
  237. package/src/dap/session.ts +1441 -0
  238. package/src/dap/types.ts +610 -0
  239. package/src/debug/index.ts +515 -0
  240. package/src/debug/log-formatting.ts +58 -0
  241. package/src/debug/log-viewer.ts +908 -0
  242. package/src/debug/profiler.ts +162 -0
  243. package/src/debug/protocol-probe.ts +267 -0
  244. package/src/debug/raw-sse-buffer.ts +273 -0
  245. package/src/debug/raw-sse.ts +292 -0
  246. package/src/debug/report-bundle.ts +374 -0
  247. package/src/debug/system-info.ts +111 -0
  248. package/src/debug/terminal-info.ts +124 -0
  249. package/src/discovery/agents-md.ts +67 -0
  250. package/src/discovery/agents.ts +230 -0
  251. package/src/discovery/at-imports.ts +273 -0
  252. package/src/discovery/builtin-defaults.ts +39 -0
  253. package/src/discovery/builtin-rules/index.ts +54 -0
  254. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  255. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  256. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  257. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  258. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  259. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  260. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  261. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  262. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  263. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
  264. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  265. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  266. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  267. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  268. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  269. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  270. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  271. package/src/discovery/builtin.ts +906 -0
  272. package/src/discovery/claude-plugins.ts +386 -0
  273. package/src/discovery/claude.ts +584 -0
  274. package/src/discovery/cline.ts +83 -0
  275. package/src/discovery/codex.ts +522 -0
  276. package/src/discovery/cursor.ts +220 -0
  277. package/src/discovery/gemini.ts +383 -0
  278. package/src/discovery/github.ts +154 -0
  279. package/src/discovery/helpers.ts +1016 -0
  280. package/src/discovery/index.ts +81 -0
  281. package/src/discovery/mcp-json.ts +171 -0
  282. package/src/discovery/omp-extension-roots.ts +190 -0
  283. package/src/discovery/omp-plugins.ts +383 -0
  284. package/src/discovery/opencode.ts +398 -0
  285. package/src/discovery/plugin-dir-roots.ts +28 -0
  286. package/src/discovery/ssh.ts +153 -0
  287. package/src/discovery/substitute-plugin-root.ts +29 -0
  288. package/src/discovery/vscode.ts +105 -0
  289. package/src/discovery/windsurf.ts +147 -0
  290. package/src/edit/apply-patch/index.ts +87 -0
  291. package/src/edit/apply-patch/parser.ts +174 -0
  292. package/src/edit/diff.ts +999 -0
  293. package/src/edit/file-snapshot-store.ts +91 -0
  294. package/src/edit/hashline/block-resolver.ts +33 -0
  295. package/src/edit/hashline/diff.ts +290 -0
  296. package/src/edit/hashline/execute.ts +242 -0
  297. package/src/edit/hashline/filesystem.ts +130 -0
  298. package/src/edit/hashline/index.ts +5 -0
  299. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  300. package/src/edit/hashline/params.ts +18 -0
  301. package/src/edit/index.ts +571 -0
  302. package/src/edit/modes/apply-patch.lark +19 -0
  303. package/src/edit/modes/apply-patch.ts +53 -0
  304. package/src/edit/modes/patch.ts +1891 -0
  305. package/src/edit/modes/replace.ts +1137 -0
  306. package/src/edit/normalize.ts +345 -0
  307. package/src/edit/notebook.ts +242 -0
  308. package/src/edit/read-file.ts +25 -0
  309. package/src/edit/renderer.ts +769 -0
  310. package/src/edit/streaming.ts +517 -0
  311. package/src/eval/__tests__/agent-bridge.test.ts +708 -0
  312. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  313. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  314. package/src/eval/__tests__/completion-bridge.test.ts +412 -0
  315. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  316. package/src/eval/__tests__/idle-timeout.test.ts +80 -0
  317. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  318. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  319. package/src/eval/agent-bridge.ts +319 -0
  320. package/src/eval/backend.ts +71 -0
  321. package/src/eval/bridge-timeout.ts +44 -0
  322. package/src/eval/budget-bridge.ts +48 -0
  323. package/src/eval/completion-bridge.ts +207 -0
  324. package/src/eval/concurrency-bridge.ts +34 -0
  325. package/src/eval/idle-timeout.ts +91 -0
  326. package/src/eval/index.ts +4 -0
  327. package/src/eval/js/context-manager.ts +502 -0
  328. package/src/eval/js/executor.ts +173 -0
  329. package/src/eval/js/index.ts +51 -0
  330. package/src/eval/js/shared/helpers.ts +283 -0
  331. package/src/eval/js/shared/indirect-eval.ts +30 -0
  332. package/src/eval/js/shared/local-module-loader.ts +342 -0
  333. package/src/eval/js/shared/prelude.ts +2 -0
  334. package/src/eval/js/shared/prelude.txt +246 -0
  335. package/src/eval/js/shared/rewrite-imports.ts +532 -0
  336. package/src/eval/js/shared/runtime.ts +352 -0
  337. package/src/eval/js/shared/types.ts +18 -0
  338. package/src/eval/js/tool-bridge.ts +162 -0
  339. package/src/eval/js/worker-core.ts +132 -0
  340. package/src/eval/js/worker-entry.ts +30 -0
  341. package/src/eval/js/worker-protocol.ts +47 -0
  342. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  343. package/src/eval/py/display.ts +71 -0
  344. package/src/eval/py/executor.ts +742 -0
  345. package/src/eval/py/index.ts +68 -0
  346. package/src/eval/py/kernel.ts +748 -0
  347. package/src/eval/py/prelude.py +658 -0
  348. package/src/eval/py/prelude.ts +3 -0
  349. package/src/eval/py/runner.py +1133 -0
  350. package/src/eval/py/runtime.ts +276 -0
  351. package/src/eval/py/spawn-options.ts +126 -0
  352. package/src/eval/py/tool-bridge.ts +182 -0
  353. package/src/eval/session-id.ts +8 -0
  354. package/src/eval/types.ts +48 -0
  355. package/src/exa/index.ts +2 -0
  356. package/src/exa/mcp-client.ts +370 -0
  357. package/src/exa/types.ts +69 -0
  358. package/src/exec/bash-executor.ts +419 -0
  359. package/src/exec/exec.ts +53 -0
  360. package/src/exec/non-interactive-env.ts +48 -0
  361. package/src/export/custom-share.ts +65 -0
  362. package/src/export/html/index.ts +164 -0
  363. package/src/export/html/template.css +1051 -0
  364. package/src/export/html/template.generated.ts +2 -0
  365. package/src/export/html/template.html +46 -0
  366. package/src/export/html/template.js +2271 -0
  367. package/src/export/html/template.macro.ts +25 -0
  368. package/src/export/html/vendor/highlight.min.js +1213 -0
  369. package/src/export/html/vendor/marked.min.js +6 -0
  370. package/src/export/ttsr.ts +583 -0
  371. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  372. package/src/extensibility/custom-commands/bundled/review/index.ts +489 -0
  373. package/src/extensibility/custom-commands/index.ts +2 -0
  374. package/src/extensibility/custom-commands/loader.ts +238 -0
  375. package/src/extensibility/custom-commands/types.ts +113 -0
  376. package/src/extensibility/custom-tools/index.ts +7 -0
  377. package/src/extensibility/custom-tools/loader.ts +269 -0
  378. package/src/extensibility/custom-tools/types.ts +270 -0
  379. package/src/extensibility/custom-tools/wrapper.ts +47 -0
  380. package/src/extensibility/extensions/compact-handler.ts +40 -0
  381. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  382. package/src/extensibility/extensions/index.ts +16 -0
  383. package/src/extensibility/extensions/loader.ts +572 -0
  384. package/src/extensibility/extensions/runner.ts +922 -0
  385. package/src/extensibility/extensions/types.ts +1322 -0
  386. package/src/extensibility/extensions/wrapper.ts +223 -0
  387. package/src/extensibility/hooks/index.ts +5 -0
  388. package/src/extensibility/hooks/loader.ts +257 -0
  389. package/src/extensibility/hooks/runner.ts +425 -0
  390. package/src/extensibility/hooks/tool-wrapper.ts +107 -0
  391. package/src/extensibility/hooks/types.ts +606 -0
  392. package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
  393. package/src/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
  394. package/src/extensibility/plugins/doctor.ts +65 -0
  395. package/src/extensibility/plugins/git-url.ts +367 -0
  396. package/src/extensibility/plugins/index.ts +9 -0
  397. package/src/extensibility/plugins/installer.ts +192 -0
  398. package/src/extensibility/plugins/legacy-pi-compat.ts +682 -0
  399. package/src/extensibility/plugins/loader.ts +313 -0
  400. package/src/extensibility/plugins/manager.ts +827 -0
  401. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  402. package/src/extensibility/plugins/marketplace/fetcher.ts +317 -0
  403. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  404. package/src/extensibility/plugins/marketplace/manager.ts +770 -0
  405. package/src/extensibility/plugins/marketplace/registry.ts +196 -0
  406. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  407. package/src/extensibility/plugins/marketplace/types.ts +191 -0
  408. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  409. package/src/extensibility/plugins/parser.ts +105 -0
  410. package/src/extensibility/plugins/types.ts +194 -0
  411. package/src/extensibility/shared-events.ts +343 -0
  412. package/src/extensibility/skills.ts +312 -0
  413. package/src/extensibility/slash-commands.ts +227 -0
  414. package/src/extensibility/tool-proxy.ts +25 -0
  415. package/src/extensibility/typebox.ts +418 -0
  416. package/src/extensibility/utils.ts +44 -0
  417. package/src/goals/index.ts +3 -0
  418. package/src/goals/runtime.ts +528 -0
  419. package/src/goals/state.ts +37 -0
  420. package/src/goals/tools/goal-tool.ts +251 -0
  421. package/src/hindsight/backend.ts +354 -0
  422. package/src/hindsight/bank.ts +156 -0
  423. package/src/hindsight/client.ts +598 -0
  424. package/src/hindsight/config.ts +175 -0
  425. package/src/hindsight/content.ts +210 -0
  426. package/src/hindsight/index.ts +8 -0
  427. package/src/hindsight/mental-models.ts +429 -0
  428. package/src/hindsight/seeds.json +32 -0
  429. package/src/hindsight/state.ts +488 -0
  430. package/src/hindsight/transcript.ts +71 -0
  431. package/src/index.ts +59 -0
  432. package/src/internal-urls/agent-protocol.ts +146 -0
  433. package/src/internal-urls/artifact-protocol.ts +107 -0
  434. package/src/internal-urls/docs-index.generated.ts +106 -0
  435. package/src/internal-urls/history-protocol.ts +113 -0
  436. package/src/internal-urls/index.ts +25 -0
  437. package/src/internal-urls/issue-pr-protocol.ts +584 -0
  438. package/src/internal-urls/json-query.ts +126 -0
  439. package/src/internal-urls/local-protocol.ts +287 -0
  440. package/src/internal-urls/mcp-protocol.ts +151 -0
  441. package/src/internal-urls/memory-protocol.ts +169 -0
  442. package/src/internal-urls/omp-protocol.ts +93 -0
  443. package/src/internal-urls/parse.ts +72 -0
  444. package/src/internal-urls/registry-helpers.ts +25 -0
  445. package/src/internal-urls/router.ts +105 -0
  446. package/src/internal-urls/rule-protocol.ts +45 -0
  447. package/src/internal-urls/skill-protocol.ts +96 -0
  448. package/src/internal-urls/types.ts +152 -0
  449. package/src/internal-urls/vault-protocol.ts +936 -0
  450. package/src/irc/bus.ts +292 -0
  451. package/src/lib/xai-http.ts +124 -0
  452. package/src/lsp/client.ts +1193 -0
  453. package/src/lsp/clients/biome-client.ts +264 -0
  454. package/src/lsp/clients/index.ts +50 -0
  455. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  456. package/src/lsp/clients/swiftlint-client.ts +120 -0
  457. package/src/lsp/config.ts +502 -0
  458. package/src/lsp/defaults.json +493 -0
  459. package/src/lsp/diagnostics-ledger.ts +51 -0
  460. package/src/lsp/edits.ts +267 -0
  461. package/src/lsp/index.ts +2477 -0
  462. package/src/lsp/lspmux.ts +233 -0
  463. package/src/lsp/render.ts +694 -0
  464. package/src/lsp/startup-events.ts +13 -0
  465. package/src/lsp/types.ts +455 -0
  466. package/src/lsp/utils.ts +718 -0
  467. package/src/main.ts +1325 -0
  468. package/src/mcp/client.ts +484 -0
  469. package/src/mcp/config-writer.ts +225 -0
  470. package/src/mcp/config.ts +365 -0
  471. package/src/mcp/index.ts +29 -0
  472. package/src/mcp/json-rpc.ts +122 -0
  473. package/src/mcp/loader.ts +124 -0
  474. package/src/mcp/manager.ts +1275 -0
  475. package/src/mcp/oauth-discovery.ts +442 -0
  476. package/src/mcp/oauth-flow.ts +442 -0
  477. package/src/mcp/render.ts +132 -0
  478. package/src/mcp/smithery-auth.ts +104 -0
  479. package/src/mcp/smithery-connect.ts +145 -0
  480. package/src/mcp/smithery-registry.ts +477 -0
  481. package/src/mcp/timeout.ts +59 -0
  482. package/src/mcp/tool-bridge.ts +426 -0
  483. package/src/mcp/tool-cache.ts +117 -0
  484. package/src/mcp/transports/http.ts +519 -0
  485. package/src/mcp/transports/index.ts +6 -0
  486. package/src/mcp/transports/stdio.ts +528 -0
  487. package/src/mcp/types.ts +423 -0
  488. package/src/memories/index.ts +1150 -0
  489. package/src/memories/storage.ts +577 -0
  490. package/src/memory-backend/index.ts +18 -0
  491. package/src/memory-backend/local-backend.ts +39 -0
  492. package/src/memory-backend/off-backend.ts +25 -0
  493. package/src/memory-backend/resolve.ts +25 -0
  494. package/src/memory-backend/runtime.ts +66 -0
  495. package/src/memory-backend/types.ts +166 -0
  496. package/src/mnemopi/backend.ts +547 -0
  497. package/src/mnemopi/config.ts +160 -0
  498. package/src/mnemopi/index.ts +3 -0
  499. package/src/mnemopi/state.ts +584 -0
  500. package/src/modes/acp/acp-agent.ts +2407 -0
  501. package/src/modes/acp/acp-client-bridge.ts +154 -0
  502. package/src/modes/acp/acp-event-mapper.ts +929 -0
  503. package/src/modes/acp/acp-mode.ts +23 -0
  504. package/src/modes/acp/index.ts +2 -0
  505. package/src/modes/acp/terminal-auth.ts +37 -0
  506. package/src/modes/components/agent-dashboard.ts +1206 -0
  507. package/src/modes/components/agent-hub.ts +1071 -0
  508. package/src/modes/components/assistant-message.ts +307 -0
  509. package/src/modes/components/bash-execution.ts +220 -0
  510. package/src/modes/components/bordered-loader.ts +41 -0
  511. package/src/modes/components/branch-summary-message.ts +45 -0
  512. package/src/modes/components/btw-panel.ts +104 -0
  513. package/src/modes/components/chat-block.ts +111 -0
  514. package/src/modes/components/compaction-summary-message.ts +87 -0
  515. package/src/modes/components/copy-selector.ts +206 -0
  516. package/src/modes/components/countdown-timer.ts +75 -0
  517. package/src/modes/components/custom-editor.ts +398 -0
  518. package/src/modes/components/custom-message.ts +63 -0
  519. package/src/modes/components/diff.ts +277 -0
  520. package/src/modes/components/dynamic-border.ts +34 -0
  521. package/src/modes/components/error-banner.ts +33 -0
  522. package/src/modes/components/eval-execution.ts +158 -0
  523. package/src/modes/components/execution-shared.ts +101 -0
  524. package/src/modes/components/extensions/extension-dashboard.ts +399 -0
  525. package/src/modes/components/extensions/extension-list.ts +502 -0
  526. package/src/modes/components/extensions/index.ts +9 -0
  527. package/src/modes/components/extensions/inspector-panel.ts +317 -0
  528. package/src/modes/components/extensions/state-manager.ts +627 -0
  529. package/src/modes/components/extensions/types.ts +186 -0
  530. package/src/modes/components/footer.ts +274 -0
  531. package/src/modes/components/history-search.ts +280 -0
  532. package/src/modes/components/hook-editor.ts +167 -0
  533. package/src/modes/components/hook-input.ts +87 -0
  534. package/src/modes/components/hook-message.ts +66 -0
  535. package/src/modes/components/hook-selector.ts +660 -0
  536. package/src/modes/components/index.ts +38 -0
  537. package/src/modes/components/keybinding-hints.ts +65 -0
  538. package/src/modes/components/late-diagnostics-message.ts +60 -0
  539. package/src/modes/components/login-dialog.ts +164 -0
  540. package/src/modes/components/mcp-add-wizard.ts +1340 -0
  541. package/src/modes/components/message-frame.ts +88 -0
  542. package/src/modes/components/model-selector.ts +1271 -0
  543. package/src/modes/components/oauth-selector.ts +368 -0
  544. package/src/modes/components/omfg-panel.ts +141 -0
  545. package/src/modes/components/overlay-box.ts +108 -0
  546. package/src/modes/components/plan-review-overlay.ts +820 -0
  547. package/src/modes/components/plan-toc.ts +138 -0
  548. package/src/modes/components/plugin-selector.ts +95 -0
  549. package/src/modes/components/plugin-settings.ts +722 -0
  550. package/src/modes/components/queue-mode-selector.ts +56 -0
  551. package/src/modes/components/read-tool-group.ts +670 -0
  552. package/src/modes/components/segment-track.ts +52 -0
  553. package/src/modes/components/session-selector.ts +625 -0
  554. package/src/modes/components/settings-defs.ts +189 -0
  555. package/src/modes/components/settings-selector.ts +651 -0
  556. package/src/modes/components/show-images-selector.ts +45 -0
  557. package/src/modes/components/skill-message.ts +89 -0
  558. package/src/modes/components/status-line/component.ts +869 -0
  559. package/src/modes/components/status-line/context-thresholds.ts +79 -0
  560. package/src/modes/components/status-line/git-utils.ts +42 -0
  561. package/src/modes/components/status-line/index.ts +5 -0
  562. package/src/modes/components/status-line/presets.ts +106 -0
  563. package/src/modes/components/status-line/segments.ts +584 -0
  564. package/src/modes/components/status-line/separators.ts +55 -0
  565. package/src/modes/components/status-line/token-rate.ts +66 -0
  566. package/src/modes/components/status-line/types.ts +108 -0
  567. package/src/modes/components/theme-selector.ts +63 -0
  568. package/src/modes/components/thinking-selector.ts +52 -0
  569. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  570. package/src/modes/components/tips.txt +19 -0
  571. package/src/modes/components/todo-reminder.ts +38 -0
  572. package/src/modes/components/tool-execution.ts +1024 -0
  573. package/src/modes/components/transcript-container.ts +608 -0
  574. package/src/modes/components/tree-selector.ts +978 -0
  575. package/src/modes/components/ttsr-notification.ts +122 -0
  576. package/src/modes/components/user-message-selector.ts +227 -0
  577. package/src/modes/components/user-message.ts +66 -0
  578. package/src/modes/components/visual-truncate.ts +63 -0
  579. package/src/modes/components/welcome.ts +493 -0
  580. package/src/modes/controllers/btw-controller.ts +105 -0
  581. package/src/modes/controllers/command-controller-shared.ts +109 -0
  582. package/src/modes/controllers/command-controller.ts +1566 -0
  583. package/src/modes/controllers/event-controller.ts +1054 -0
  584. package/src/modes/controllers/extension-ui-controller.ts +886 -0
  585. package/src/modes/controllers/input-controller.ts +1073 -0
  586. package/src/modes/controllers/mcp-command-controller.ts +2017 -0
  587. package/src/modes/controllers/omfg-controller.ts +283 -0
  588. package/src/modes/controllers/omfg-rule.ts +647 -0
  589. package/src/modes/controllers/selector-controller.ts +1108 -0
  590. package/src/modes/controllers/ssh-command-controller.ts +384 -0
  591. package/src/modes/controllers/streaming-reveal.ts +279 -0
  592. package/src/modes/controllers/tan-command-controller.ts +173 -0
  593. package/src/modes/controllers/todo-command-controller.ts +485 -0
  594. package/src/modes/data/emojis.json +1 -0
  595. package/src/modes/emoji-autocomplete.ts +285 -0
  596. package/src/modes/gradient-highlight.ts +87 -0
  597. package/src/modes/image-references.ts +117 -0
  598. package/src/modes/index.ts +17 -0
  599. package/src/modes/interactive-mode.ts +3370 -0
  600. package/src/modes/internal-url-autocomplete.ts +143 -0
  601. package/src/modes/loop-limit.ts +140 -0
  602. package/src/modes/magic-keywords.ts +20 -0
  603. package/src/modes/markdown-prose.ts +247 -0
  604. package/src/modes/oauth-manual-input.ts +69 -0
  605. package/src/modes/orchestrate.ts +42 -0
  606. package/src/modes/print-mode.ts +126 -0
  607. package/src/modes/prompt-action-autocomplete.ts +260 -0
  608. package/src/modes/rpc/host-tools.ts +186 -0
  609. package/src/modes/rpc/host-uris.ts +235 -0
  610. package/src/modes/rpc/rpc-client.ts +963 -0
  611. package/src/modes/rpc/rpc-mode.ts +947 -0
  612. package/src/modes/rpc/rpc-subagents.ts +265 -0
  613. package/src/modes/rpc/rpc-types.ts +458 -0
  614. package/src/modes/runtime-init.ts +116 -0
  615. package/src/modes/session-observer-registry.ts +146 -0
  616. package/src/modes/setup-version.ts +11 -0
  617. package/src/modes/setup-wizard/index.ts +99 -0
  618. package/src/modes/setup-wizard/lazy.ts +16 -0
  619. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  620. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  621. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  622. package/src/modes/setup-wizard/scenes/sign-in.ts +205 -0
  623. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  624. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  625. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  626. package/src/modes/setup-wizard/scenes/web-search.ts +129 -0
  627. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  628. package/src/modes/shared.ts +47 -0
  629. package/src/modes/theme/dark.json +95 -0
  630. package/src/modes/theme/defaults/alabaster.json +93 -0
  631. package/src/modes/theme/defaults/amethyst.json +96 -0
  632. package/src/modes/theme/defaults/anthracite.json +93 -0
  633. package/src/modes/theme/defaults/basalt.json +91 -0
  634. package/src/modes/theme/defaults/birch.json +95 -0
  635. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  636. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  637. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  638. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  639. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  640. package/src/modes/theme/defaults/dark-copper.json +95 -0
  641. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  642. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  643. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  644. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  645. package/src/modes/theme/defaults/dark-ember.json +95 -0
  646. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  647. package/src/modes/theme/defaults/dark-forest.json +96 -0
  648. package/src/modes/theme/defaults/dark-github.json +105 -0
  649. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  650. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  651. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  652. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  653. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  654. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  655. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  656. package/src/modes/theme/defaults/dark-nord.json +97 -0
  657. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  658. package/src/modes/theme/defaults/dark-one.json +100 -0
  659. package/src/modes/theme/defaults/dark-poimandres.json +142 -0
  660. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  661. package/src/modes/theme/defaults/dark-reef.json +91 -0
  662. package/src/modes/theme/defaults/dark-retro.json +92 -0
  663. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  664. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  665. package/src/modes/theme/defaults/dark-slate.json +95 -0
  666. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  667. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  668. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  669. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  670. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  671. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  672. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  673. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  674. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  675. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  676. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  677. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  678. package/src/modes/theme/defaults/graphite.json +92 -0
  679. package/src/modes/theme/defaults/index.ts +199 -0
  680. package/src/modes/theme/defaults/light-arctic.json +107 -0
  681. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  682. package/src/modes/theme/defaults/light-canyon.json +91 -0
  683. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  684. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  685. package/src/modes/theme/defaults/light-coral.json +95 -0
  686. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  687. package/src/modes/theme/defaults/light-dawn.json +90 -0
  688. package/src/modes/theme/defaults/light-dunes.json +91 -0
  689. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  690. package/src/modes/theme/defaults/light-forest.json +100 -0
  691. package/src/modes/theme/defaults/light-frost.json +95 -0
  692. package/src/modes/theme/defaults/light-github.json +115 -0
  693. package/src/modes/theme/defaults/light-glacier.json +91 -0
  694. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  695. package/src/modes/theme/defaults/light-haze.json +90 -0
  696. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  697. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  698. package/src/modes/theme/defaults/light-lavender.json +95 -0
  699. package/src/modes/theme/defaults/light-meadow.json +91 -0
  700. package/src/modes/theme/defaults/light-mint.json +95 -0
  701. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  702. package/src/modes/theme/defaults/light-ocean.json +99 -0
  703. package/src/modes/theme/defaults/light-one.json +99 -0
  704. package/src/modes/theme/defaults/light-opal.json +91 -0
  705. package/src/modes/theme/defaults/light-orchard.json +91 -0
  706. package/src/modes/theme/defaults/light-paper.json +95 -0
  707. package/src/modes/theme/defaults/light-poimandres.json +142 -0
  708. package/src/modes/theme/defaults/light-prism.json +90 -0
  709. package/src/modes/theme/defaults/light-retro.json +98 -0
  710. package/src/modes/theme/defaults/light-sand.json +95 -0
  711. package/src/modes/theme/defaults/light-savanna.json +91 -0
  712. package/src/modes/theme/defaults/light-solarized.json +102 -0
  713. package/src/modes/theme/defaults/light-soleil.json +90 -0
  714. package/src/modes/theme/defaults/light-sunset.json +99 -0
  715. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  716. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  717. package/src/modes/theme/defaults/light-wetland.json +91 -0
  718. package/src/modes/theme/defaults/light-zenith.json +89 -0
  719. package/src/modes/theme/defaults/limestone.json +94 -0
  720. package/src/modes/theme/defaults/mahogany.json +97 -0
  721. package/src/modes/theme/defaults/marble.json +93 -0
  722. package/src/modes/theme/defaults/obsidian.json +91 -0
  723. package/src/modes/theme/defaults/onyx.json +91 -0
  724. package/src/modes/theme/defaults/pearl.json +93 -0
  725. package/src/modes/theme/defaults/porcelain.json +91 -0
  726. package/src/modes/theme/defaults/quartz.json +96 -0
  727. package/src/modes/theme/defaults/sandstone.json +95 -0
  728. package/src/modes/theme/defaults/titanium.json +90 -0
  729. package/src/modes/theme/light.json +93 -0
  730. package/src/modes/theme/mermaid-cache.ts +29 -0
  731. package/src/modes/theme/shimmer.ts +235 -0
  732. package/src/modes/theme/theme-schema.json +459 -0
  733. package/src/modes/theme/theme.ts +2676 -0
  734. package/src/modes/turn-budget.ts +31 -0
  735. package/src/modes/types.ts +359 -0
  736. package/src/modes/ultrathink.ts +41 -0
  737. package/src/modes/utils/context-usage.ts +339 -0
  738. package/src/modes/utils/copy-targets.ts +360 -0
  739. package/src/modes/utils/hotkeys-markdown.ts +61 -0
  740. package/src/modes/utils/keybinding-matchers.ts +51 -0
  741. package/src/modes/utils/tools-markdown.ts +27 -0
  742. package/src/modes/utils/ui-helpers.ts +801 -0
  743. package/src/modes/workflow.ts +42 -0
  744. package/src/plan-mode/approved-plan.ts +186 -0
  745. package/src/plan-mode/plan-handoff.ts +37 -0
  746. package/src/plan-mode/plan-protection.ts +31 -0
  747. package/src/plan-mode/state.ts +6 -0
  748. package/src/priority.json +41 -0
  749. package/src/prompts/agents/designer.md +66 -0
  750. package/src/prompts/agents/explore.md +58 -0
  751. package/src/prompts/agents/frontmatter.md +11 -0
  752. package/src/prompts/agents/init.md +33 -0
  753. package/src/prompts/agents/librarian.md +119 -0
  754. package/src/prompts/agents/oracle.md +55 -0
  755. package/src/prompts/agents/plan.md +48 -0
  756. package/src/prompts/agents/reviewer.md +140 -0
  757. package/src/prompts/agents/task.md +16 -0
  758. package/src/prompts/ci-green-request.md +36 -0
  759. package/src/prompts/dry-balance-bench.md +8 -0
  760. package/src/prompts/goals/goal-budget-limit.md +16 -0
  761. package/src/prompts/goals/goal-continuation.md +28 -0
  762. package/src/prompts/goals/goal-mode-active.md +23 -0
  763. package/src/prompts/memories/consolidation.md +30 -0
  764. package/src/prompts/memories/read-path.md +11 -0
  765. package/src/prompts/memories/stage_one_input.md +6 -0
  766. package/src/prompts/memories/stage_one_system.md +21 -0
  767. package/src/prompts/review-custom-request.md +22 -0
  768. package/src/prompts/review-headless-request.md +16 -0
  769. package/src/prompts/review-request.md +69 -0
  770. package/src/prompts/steering/user-interjection.md +10 -0
  771. package/src/prompts/system/agent-creation-architect.md +50 -0
  772. package/src/prompts/system/agent-creation-user.md +6 -0
  773. package/src/prompts/system/auto-continue.md +1 -0
  774. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  775. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  776. package/src/prompts/system/background-tan-dispatch.md +8 -0
  777. package/src/prompts/system/btw-user.md +8 -0
  778. package/src/prompts/system/commit-message-system.md +14 -0
  779. package/src/prompts/system/custom-system-prompt.md +64 -0
  780. package/src/prompts/system/eager-todo.md +13 -0
  781. package/src/prompts/system/empty-stop-retry.md +6 -0
  782. package/src/prompts/system/irc-incoming.md +7 -0
  783. package/src/prompts/system/manual-continue.md +7 -0
  784. package/src/prompts/system/memory-consolidation-system.md +8 -0
  785. package/src/prompts/system/memory-extraction-system.md +26 -0
  786. package/src/prompts/system/omfg-user.md +50 -0
  787. package/src/prompts/system/orchestrate-notice.md +40 -0
  788. package/src/prompts/system/plan-mode-active.md +109 -0
  789. package/src/prompts/system/plan-mode-approved.md +25 -0
  790. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  791. package/src/prompts/system/plan-mode-reference.md +11 -0
  792. package/src/prompts/system/plan-mode-subagent.md +33 -0
  793. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  794. package/src/prompts/system/project-prompt.md +52 -0
  795. package/src/prompts/system/subagent-system-prompt.md +64 -0
  796. package/src/prompts/system/subagent-user-prompt.md +3 -0
  797. package/src/prompts/system/subagent-yield-reminder.md +12 -0
  798. package/src/prompts/system/system-prompt.md +258 -0
  799. package/src/prompts/system/tiny-title-system.md +8 -0
  800. package/src/prompts/system/title-system.md +16 -0
  801. package/src/prompts/system/ttsr-interrupt.md +7 -0
  802. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  803. package/src/prompts/system/ultrathink-notice.md +3 -0
  804. package/src/prompts/system/web-search.md +25 -0
  805. package/src/prompts/system/workflow-notice.md +70 -0
  806. package/src/prompts/tools/apply-patch.md +65 -0
  807. package/src/prompts/tools/ask.md +30 -0
  808. package/src/prompts/tools/ast-edit.md +39 -0
  809. package/src/prompts/tools/ast-grep.md +42 -0
  810. package/src/prompts/tools/async-result.md +8 -0
  811. package/src/prompts/tools/bash.md +46 -0
  812. package/src/prompts/tools/browser.md +73 -0
  813. package/src/prompts/tools/checkpoint.md +16 -0
  814. package/src/prompts/tools/debug.md +34 -0
  815. package/src/prompts/tools/eval.md +92 -0
  816. package/src/prompts/tools/find.md +36 -0
  817. package/src/prompts/tools/github.md +21 -0
  818. package/src/prompts/tools/goal.md +18 -0
  819. package/src/prompts/tools/image-gen.md +7 -0
  820. package/src/prompts/tools/inspect-image-system.md +20 -0
  821. package/src/prompts/tools/inspect-image.md +32 -0
  822. package/src/prompts/tools/irc.md +59 -0
  823. package/src/prompts/tools/job.md +19 -0
  824. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  825. package/src/prompts/tools/lsp.md +42 -0
  826. package/src/prompts/tools/memory-edit.md +8 -0
  827. package/src/prompts/tools/patch.md +70 -0
  828. package/src/prompts/tools/read.md +84 -0
  829. package/src/prompts/tools/recall.md +5 -0
  830. package/src/prompts/tools/reflect.md +5 -0
  831. package/src/prompts/tools/render-mermaid.md +9 -0
  832. package/src/prompts/tools/replace.md +30 -0
  833. package/src/prompts/tools/resolve.md +9 -0
  834. package/src/prompts/tools/retain.md +6 -0
  835. package/src/prompts/tools/rewind.md +13 -0
  836. package/src/prompts/tools/search-tool-bm25.md +32 -0
  837. package/src/prompts/tools/search.md +24 -0
  838. package/src/prompts/tools/ssh.md +31 -0
  839. package/src/prompts/tools/task-summary.md +17 -0
  840. package/src/prompts/tools/task.md +88 -0
  841. package/src/prompts/tools/todo.md +62 -0
  842. package/src/prompts/tools/web-search.md +10 -0
  843. package/src/prompts/tools/write.md +14 -0
  844. package/src/registry/agent-lifecycle.ts +218 -0
  845. package/src/registry/agent-registry.ts +151 -0
  846. package/src/sdk.ts +2558 -0
  847. package/src/secrets/index.ts +123 -0
  848. package/src/secrets/obfuscator.ts +298 -0
  849. package/src/secrets/regex.ts +21 -0
  850. package/src/session/agent-session.ts +10121 -0
  851. package/src/session/agent-storage.ts +455 -0
  852. package/src/session/artifacts.ts +135 -0
  853. package/src/session/auth-broker-config.ts +131 -0
  854. package/src/session/auth-storage.ts +29 -0
  855. package/src/session/blob-store.ts +255 -0
  856. package/src/session/client-bridge.ts +85 -0
  857. package/src/session/history-storage.ts +348 -0
  858. package/src/session/indexed-session-storage.ts +430 -0
  859. package/src/session/messages.ts +541 -0
  860. package/src/session/redis-session-storage.ts +170 -0
  861. package/src/session/session-dump-format.ts +209 -0
  862. package/src/session/session-history-format.ts +246 -0
  863. package/src/session/session-manager.ts +3676 -0
  864. package/src/session/session-storage.ts +529 -0
  865. package/src/session/shake-types.ts +43 -0
  866. package/src/session/sql-session-storage.ts +314 -0
  867. package/src/session/streaming-output.ts +1330 -0
  868. package/src/session/tool-choice-queue.ts +213 -0
  869. package/src/session/yield-queue.ts +173 -0
  870. package/src/slash-commands/acp-builtins.ts +70 -0
  871. package/src/slash-commands/builtin-registry.ts +1798 -0
  872. package/src/slash-commands/helpers/context-report.ts +39 -0
  873. package/src/slash-commands/helpers/format.ts +46 -0
  874. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  875. package/src/slash-commands/helpers/mcp.ts +532 -0
  876. package/src/slash-commands/helpers/parse.ts +85 -0
  877. package/src/slash-commands/helpers/ssh.ts +195 -0
  878. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  879. package/src/slash-commands/helpers/todo.ts +279 -0
  880. package/src/slash-commands/helpers/usage-report.ts +95 -0
  881. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  882. package/src/slash-commands/types.ts +135 -0
  883. package/src/ssh/config-writer.ts +183 -0
  884. package/src/ssh/connection-manager.ts +509 -0
  885. package/src/ssh/ssh-executor.ts +189 -0
  886. package/src/ssh/sshfs-mount.ts +140 -0
  887. package/src/ssh/utils.ts +8 -0
  888. package/src/stt/downloader.ts +71 -0
  889. package/src/stt/index.ts +3 -0
  890. package/src/stt/recorder.ts +351 -0
  891. package/src/stt/setup.ts +52 -0
  892. package/src/stt/stt-controller.ts +160 -0
  893. package/src/stt/transcribe.py +70 -0
  894. package/src/stt/transcriber.ts +91 -0
  895. package/src/stubs/natives/index.ts +814 -0
  896. package/src/stubs/natives/package.json +7 -0
  897. package/src/stubs/tui/index.ts +282 -0
  898. package/src/stubs/tui/package.json +7 -0
  899. package/src/system-prompt.ts +611 -0
  900. package/src/task/agents.ts +167 -0
  901. package/src/task/commands.ts +132 -0
  902. package/src/task/discovery.ts +122 -0
  903. package/src/task/executor.ts +2133 -0
  904. package/src/task/index.ts +1419 -0
  905. package/src/task/name-generator.ts +1577 -0
  906. package/src/task/omp-command.ts +26 -0
  907. package/src/task/output-manager.ts +88 -0
  908. package/src/task/parallel.ts +116 -0
  909. package/src/task/render.ts +1381 -0
  910. package/src/task/repair-args.ts +129 -0
  911. package/src/task/subprocess-tool-registry.ts +88 -0
  912. package/src/task/types.ts +336 -0
  913. package/src/task/worktree.ts +514 -0
  914. package/src/telemetry-export.ts +144 -0
  915. package/src/thinking.ts +167 -0
  916. package/src/tiny/compiled-runtime.ts +179 -0
  917. package/src/tiny/device.ts +111 -0
  918. package/src/tiny/dtype.ts +101 -0
  919. package/src/tiny/models.ts +242 -0
  920. package/src/tiny/text.ts +165 -0
  921. package/src/tiny/title-client.ts +543 -0
  922. package/src/tiny/title-protocol.ts +56 -0
  923. package/src/tiny/worker.ts +568 -0
  924. package/src/tool-discovery/mode.ts +24 -0
  925. package/src/tool-discovery/tool-index.ts +256 -0
  926. package/src/tools/approval.ts +189 -0
  927. package/src/tools/archive-reader.ts +721 -0
  928. package/src/tools/ask.ts +928 -0
  929. package/src/tools/ast-edit.ts +642 -0
  930. package/src/tools/ast-grep.ts +452 -0
  931. package/src/tools/auto-generated-guard.ts +322 -0
  932. package/src/tools/bash-command-fixup.ts +37 -0
  933. package/src/tools/bash-interactive.ts +408 -0
  934. package/src/tools/bash-interceptor.ts +67 -0
  935. package/src/tools/bash-pty-selection.ts +14 -0
  936. package/src/tools/bash-skill-urls.ts +248 -0
  937. package/src/tools/bash.ts +1386 -0
  938. package/src/tools/browser/attach.ts +175 -0
  939. package/src/tools/browser/launch.ts +660 -0
  940. package/src/tools/browser/readable.ts +112 -0
  941. package/src/tools/browser/registry.ts +197 -0
  942. package/src/tools/browser/render.ts +216 -0
  943. package/src/tools/browser/tab-protocol.ts +105 -0
  944. package/src/tools/browser/tab-supervisor.ts +628 -0
  945. package/src/tools/browser/tab-worker-entry.ts +21 -0
  946. package/src/tools/browser/tab-worker.ts +1226 -0
  947. package/src/tools/browser.ts +343 -0
  948. package/src/tools/checkpoint.ts +136 -0
  949. package/src/tools/conflict-detect.ts +718 -0
  950. package/src/tools/context.ts +39 -0
  951. package/src/tools/debug.ts +1067 -0
  952. package/src/tools/eval-backends.ts +27 -0
  953. package/src/tools/eval-render.ts +752 -0
  954. package/src/tools/eval.ts +577 -0
  955. package/src/tools/fetch.ts +1926 -0
  956. package/src/tools/file-recorder.ts +35 -0
  957. package/src/tools/find.ts +609 -0
  958. package/src/tools/fs-cache-invalidation.ts +28 -0
  959. package/src/tools/gh-cache-invalidation.ts +255 -0
  960. package/src/tools/gh-format.ts +12 -0
  961. package/src/tools/gh-renderer.ts +481 -0
  962. package/src/tools/gh.ts +3720 -0
  963. package/src/tools/github-cache.ts +637 -0
  964. package/src/tools/grouped-file-output.ts +210 -0
  965. package/src/tools/image-gen.ts +1517 -0
  966. package/src/tools/index.ts +599 -0
  967. package/src/tools/inspect-image-renderer.ts +132 -0
  968. package/src/tools/inspect-image.ts +174 -0
  969. package/src/tools/irc.ts +723 -0
  970. package/src/tools/job.ts +557 -0
  971. package/src/tools/json-tree.ts +243 -0
  972. package/src/tools/jtd-to-json-schema.ts +219 -0
  973. package/src/tools/jtd-to-typescript.ts +136 -0
  974. package/src/tools/jtd-utils.ts +102 -0
  975. package/src/tools/list-limit.ts +40 -0
  976. package/src/tools/match-line-format.ts +20 -0
  977. package/src/tools/memory-edit.ts +59 -0
  978. package/src/tools/memory-recall.ts +100 -0
  979. package/src/tools/memory-reflect.ts +88 -0
  980. package/src/tools/memory-render.ts +202 -0
  981. package/src/tools/memory-retain.ts +91 -0
  982. package/src/tools/output-meta.ts +754 -0
  983. package/src/tools/output-schema-validator.ts +132 -0
  984. package/src/tools/path-utils.ts +1054 -0
  985. package/src/tools/plan-mode-guard.ts +108 -0
  986. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  987. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  988. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  989. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  990. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  991. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  992. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  993. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  994. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  995. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  996. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  997. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  998. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  999. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  1000. package/src/tools/read.ts +2929 -0
  1001. package/src/tools/render-mermaid.ts +69 -0
  1002. package/src/tools/render-utils.ts +838 -0
  1003. package/src/tools/renderers.ts +77 -0
  1004. package/src/tools/report-tool-issue.ts +534 -0
  1005. package/src/tools/resolve.ts +276 -0
  1006. package/src/tools/review.ts +253 -0
  1007. package/src/tools/search-tool-bm25.ts +351 -0
  1008. package/src/tools/search.ts +1580 -0
  1009. package/src/tools/sqlite-reader.ts +828 -0
  1010. package/src/tools/ssh.ts +349 -0
  1011. package/src/tools/todo.ts +982 -0
  1012. package/src/tools/tool-errors.ts +62 -0
  1013. package/src/tools/tool-result.ts +94 -0
  1014. package/src/tools/tool-timeouts.ts +30 -0
  1015. package/src/tools/tts.ts +133 -0
  1016. package/src/tools/write.ts +1217 -0
  1017. package/src/tools/yield.ts +269 -0
  1018. package/src/tui/code-cell.ts +216 -0
  1019. package/src/tui/file-list.ts +55 -0
  1020. package/src/tui/hyperlink.ts +175 -0
  1021. package/src/tui/index.ts +12 -0
  1022. package/src/tui/output-block.ts +240 -0
  1023. package/src/tui/status-line.ts +54 -0
  1024. package/src/tui/tree-list.ts +84 -0
  1025. package/src/tui/types.ts +15 -0
  1026. package/src/tui/utils.ts +103 -0
  1027. package/src/utils/block-context.ts +312 -0
  1028. package/src/utils/changelog.ts +132 -0
  1029. package/src/utils/clipboard.ts +193 -0
  1030. package/src/utils/command-args.ts +76 -0
  1031. package/src/utils/commit-message-generator.ts +151 -0
  1032. package/src/utils/edit-mode.ts +41 -0
  1033. package/src/utils/enhanced-paste.ts +230 -0
  1034. package/src/utils/event-bus.ts +33 -0
  1035. package/src/utils/external-editor.ts +65 -0
  1036. package/src/utils/file-display-mode.ts +45 -0
  1037. package/src/utils/file-mentions.ts +281 -0
  1038. package/src/utils/git.ts +1833 -0
  1039. package/src/utils/image-loading.ts +132 -0
  1040. package/src/utils/image-resize.ts +309 -0
  1041. package/src/utils/jj.ts +248 -0
  1042. package/src/utils/lang-from-path.ts +239 -0
  1043. package/src/utils/markit.ts +89 -0
  1044. package/src/utils/open.ts +55 -0
  1045. package/src/utils/session-color.ts +68 -0
  1046. package/src/utils/shell-snapshot.ts +187 -0
  1047. package/src/utils/sixel.ts +69 -0
  1048. package/src/utils/title-generator.ts +373 -0
  1049. package/src/utils/tool-choice.ts +33 -0
  1050. package/src/utils/tools-manager.ts +363 -0
  1051. package/src/web/kagi.ts +305 -0
  1052. package/src/web/parallel.ts +353 -0
  1053. package/src/web/scrapers/artifacthub.ts +207 -0
  1054. package/src/web/scrapers/arxiv.ts +83 -0
  1055. package/src/web/scrapers/aur.ts +162 -0
  1056. package/src/web/scrapers/biorxiv.ts +133 -0
  1057. package/src/web/scrapers/bluesky.ts +262 -0
  1058. package/src/web/scrapers/brew.ts +172 -0
  1059. package/src/web/scrapers/cheatsh.ts +68 -0
  1060. package/src/web/scrapers/chocolatey.ts +196 -0
  1061. package/src/web/scrapers/choosealicense.ts +95 -0
  1062. package/src/web/scrapers/cisa-kev.ts +87 -0
  1063. package/src/web/scrapers/clojars.ts +154 -0
  1064. package/src/web/scrapers/coingecko.ts +177 -0
  1065. package/src/web/scrapers/crates-io.ts +97 -0
  1066. package/src/web/scrapers/crossref.ts +136 -0
  1067. package/src/web/scrapers/devto.ts +147 -0
  1068. package/src/web/scrapers/discogs.ts +306 -0
  1069. package/src/web/scrapers/discourse.ts +197 -0
  1070. package/src/web/scrapers/dockerhub.ts +138 -0
  1071. package/src/web/scrapers/docs-rs.ts +653 -0
  1072. package/src/web/scrapers/fdroid.ts +134 -0
  1073. package/src/web/scrapers/firefox-addons.ts +191 -0
  1074. package/src/web/scrapers/flathub.ts +223 -0
  1075. package/src/web/scrapers/github-gist.ts +58 -0
  1076. package/src/web/scrapers/github.ts +704 -0
  1077. package/src/web/scrapers/gitlab.ts +401 -0
  1078. package/src/web/scrapers/go-pkg.ts +266 -0
  1079. package/src/web/scrapers/hackage.ts +140 -0
  1080. package/src/web/scrapers/hackernews.ts +189 -0
  1081. package/src/web/scrapers/hex.ts +105 -0
  1082. package/src/web/scrapers/huggingface.ts +321 -0
  1083. package/src/web/scrapers/iacr.ts +89 -0
  1084. package/src/web/scrapers/index.ts +252 -0
  1085. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1086. package/src/web/scrapers/lemmy.ts +203 -0
  1087. package/src/web/scrapers/lobsters.ts +175 -0
  1088. package/src/web/scrapers/mastodon.ts +292 -0
  1089. package/src/web/scrapers/maven.ts +138 -0
  1090. package/src/web/scrapers/mdn.ts +173 -0
  1091. package/src/web/scrapers/metacpan.ts +222 -0
  1092. package/src/web/scrapers/musicbrainz.ts +250 -0
  1093. package/src/web/scrapers/npm.ts +98 -0
  1094. package/src/web/scrapers/nuget.ts +183 -0
  1095. package/src/web/scrapers/nvd.ts +222 -0
  1096. package/src/web/scrapers/ollama.ts +239 -0
  1097. package/src/web/scrapers/open-vsx.ts +106 -0
  1098. package/src/web/scrapers/opencorporates.ts +292 -0
  1099. package/src/web/scrapers/openlibrary.ts +336 -0
  1100. package/src/web/scrapers/orcid.ts +286 -0
  1101. package/src/web/scrapers/osv.ts +176 -0
  1102. package/src/web/scrapers/packagist.ts +160 -0
  1103. package/src/web/scrapers/pub-dev.ts +143 -0
  1104. package/src/web/scrapers/pubmed.ts +211 -0
  1105. package/src/web/scrapers/pypi.ts +112 -0
  1106. package/src/web/scrapers/rawg.ts +110 -0
  1107. package/src/web/scrapers/readthedocs.ts +120 -0
  1108. package/src/web/scrapers/reddit.ts +95 -0
  1109. package/src/web/scrapers/repology.ts +251 -0
  1110. package/src/web/scrapers/rfc.ts +201 -0
  1111. package/src/web/scrapers/rubygems.ts +103 -0
  1112. package/src/web/scrapers/searchcode.ts +189 -0
  1113. package/src/web/scrapers/sec-edgar.ts +261 -0
  1114. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1115. package/src/web/scrapers/snapcraft.ts +187 -0
  1116. package/src/web/scrapers/sourcegraph.ts +336 -0
  1117. package/src/web/scrapers/spdx.ts +108 -0
  1118. package/src/web/scrapers/spotify.ts +198 -0
  1119. package/src/web/scrapers/stackoverflow.ts +120 -0
  1120. package/src/web/scrapers/terraform.ts +277 -0
  1121. package/src/web/scrapers/tldr.ts +47 -0
  1122. package/src/web/scrapers/twitter.ts +94 -0
  1123. package/src/web/scrapers/types.ts +397 -0
  1124. package/src/web/scrapers/utils.ts +109 -0
  1125. package/src/web/scrapers/vimeo.ts +133 -0
  1126. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1127. package/src/web/scrapers/w3c.ts +156 -0
  1128. package/src/web/scrapers/wikidata.ts +344 -0
  1129. package/src/web/scrapers/wikipedia.ts +84 -0
  1130. package/src/web/scrapers/youtube.ts +325 -0
  1131. package/src/web/search/index.ts +292 -0
  1132. package/src/web/search/provider.ts +157 -0
  1133. package/src/web/search/providers/anthropic.ts +318 -0
  1134. package/src/web/search/providers/base.ts +89 -0
  1135. package/src/web/search/providers/brave.ts +152 -0
  1136. package/src/web/search/providers/codex.ts +591 -0
  1137. package/src/web/search/providers/exa.ts +400 -0
  1138. package/src/web/search/providers/gemini.ts +460 -0
  1139. package/src/web/search/providers/jina.ts +111 -0
  1140. package/src/web/search/providers/kagi.ts +86 -0
  1141. package/src/web/search/providers/kimi.ts +196 -0
  1142. package/src/web/search/providers/parallel.ts +225 -0
  1143. package/src/web/search/providers/perplexity.ts +730 -0
  1144. package/src/web/search/providers/searxng.ts +313 -0
  1145. package/src/web/search/providers/synthetic.ts +114 -0
  1146. package/src/web/search/providers/tavily.ts +176 -0
  1147. package/src/web/search/providers/utils.ts +128 -0
  1148. package/src/web/search/providers/zai.ts +333 -0
  1149. package/src/web/search/render.ts +262 -0
  1150. package/src/web/search/types.ts +482 -0
  1151. package/src/web/search/utils.ts +17 -0
  1152. package/src/workspace-tree.ts +286 -0
@@ -0,0 +1,3370 @@
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
+ type AgentMessage,
10
+ type AgentToolResult,
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, UsageReport } from "@oh-my-pi/pi-ai";
16
+ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
17
+ import type { Component, EditorTheme, LoaderMessageColorFn, OverlayHandle, SlashCommand } from './stubs/tui/index.ts';
18
+ import {
19
+ Container,
20
+ clearRenderCache,
21
+ Loader,
22
+ Markdown,
23
+ ProcessTerminal,
24
+ Spacer,
25
+ setTerminalTextSizing,
26
+ TERMINAL,
27
+ Text,
28
+ TUI,
29
+ visibleWidth,
30
+ } from './stubs/tui/index.ts';
31
+ import {
32
+ APP_NAME,
33
+ adjustHsv,
34
+ formatNumber,
35
+ getProjectDir,
36
+ hsvToRgb,
37
+ isEnoent,
38
+ logger,
39
+ postmortem,
40
+ prompt,
41
+ setProjectDir,
42
+ } from "@oh-my-pi/pi-utils";
43
+ import chalk from "chalk";
44
+ import { reset as resetCapabilities } from "../capability";
45
+ import { KeybindingsManager } from "../config/keybindings";
46
+ import { MODEL_ROLES, type ModelRole } from "../config/model-roles";
47
+ import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
48
+ import { clearClaudePluginRootsCache } from "../discovery/helpers";
49
+ import type {
50
+ ContextUsage,
51
+ ExtensionUIContext,
52
+ ExtensionUIDialogOptions,
53
+ ExtensionUISelectItem,
54
+ ExtensionWidgetContent,
55
+ ExtensionWidgetOptions,
56
+ } from "../extensibility/extensions";
57
+ import type { CompactOptions } from "../extensibility/extensions/types";
58
+ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
59
+ import type { Goal, GoalModeState } from "../goals/state";
60
+ import { resolveLocalUrlToPath } from "../internal-urls";
61
+ import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
62
+ import type { MCPManager } from "../mcp";
63
+ import {
64
+ humanizePlanTitle,
65
+ type PlanApprovalDetails,
66
+ resolveApprovedPlan,
67
+ resolvePlanTitle,
68
+ } from "../plan-mode/approved-plan";
69
+ import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
70
+ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
71
+ type: "text",
72
+ };
73
+ import type { AgentSession, AgentSessionEvent, ResolvedRoleModel } from "../session/agent-session";
74
+ import { HistoryStorage } from "../session/history-storage";
75
+ import type { SessionContext, SessionManager } from "../session/session-manager";
76
+ import { getRecentSessions } from "../session/session-manager";
77
+ import type { ShakeMode } from "../session/shake-types";
78
+ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-registry";
79
+ import { formatDuration } from "../slash-commands/helpers/format";
80
+ import { STTController, type SttState } from "../stt";
81
+ import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
82
+ import type { LspStartupServerInfo } from "../tools";
83
+ import { normalizeLocalScheme } from "../tools/path-utils";
84
+ import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
85
+ import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
86
+ import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
87
+ import { ToolError } from "../tools/tool-errors";
88
+ import type { EventBus } from "../utils/event-bus";
89
+ import { getEditorCommand, openInEditor } from "../utils/external-editor";
90
+ import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
91
+ import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
92
+ import type { AssistantMessageComponent } from "./components/assistant-message";
93
+ import type { BashExecutionComponent } from "./components/bash-execution";
94
+ import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
95
+ import { CustomEditor } from "./components/custom-editor";
96
+ import { DynamicBorder } from "./components/dynamic-border";
97
+ import { ErrorBannerComponent } from "./components/error-banner";
98
+ import type { EvalExecutionComponent } from "./components/eval-execution";
99
+ import type { HookEditorComponent } from "./components/hook-editor";
100
+ import type { HookInputComponent } from "./components/hook-input";
101
+ import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
102
+ import { PlanReviewOverlay } from "./components/plan-review-overlay";
103
+ import { StatusLineComponent } from "./components/status-line";
104
+ import type { ToolExecutionHandle } from "./components/tool-execution";
105
+ import { TranscriptContainer } from "./components/transcript-container";
106
+ import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
107
+ import { BtwController } from "./controllers/btw-controller";
108
+ import { CommandController } from "./controllers/command-controller";
109
+ import { EventController } from "./controllers/event-controller";
110
+ import { ExtensionUiController } from "./controllers/extension-ui-controller";
111
+ import { InputController } from "./controllers/input-controller";
112
+ import { MCPCommandController } from "./controllers/mcp-command-controller";
113
+ import { OmfgController } from "./controllers/omfg-controller";
114
+ import { SelectorController } from "./controllers/selector-controller";
115
+ import { SSHCommandController } from "./controllers/ssh-command-controller";
116
+ import { TanCommandController } from "./controllers/tan-command-controller";
117
+ import { TodoCommandController } from "./controllers/todo-command-controller";
118
+ import {
119
+ consumeLoopLimitIteration,
120
+ createLoopLimitRuntime,
121
+ describeLoopLimit,
122
+ describeLoopLimitRuntime,
123
+ isLoopDurationExpired,
124
+ type LoopLimitRuntime,
125
+ parseLoopLimitArgs,
126
+ } from "./loop-limit";
127
+ import { OAuthManualInputManager } from "./oauth-manual-input";
128
+ import { SessionObserverRegistry } from "./session-observer-registry";
129
+ import { runProviderSetupWizard } from "./setup-wizard/lazy";
130
+ import { interruptHint } from "./shared";
131
+ import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
132
+ import type { Theme } from "./theme/theme";
133
+ import {
134
+ getEditorTheme,
135
+ getMarkdownTheme,
136
+ getSymbolTheme,
137
+ onTerminalAppearanceChange,
138
+ onThemeChange,
139
+ theme,
140
+ } from "./theme/theme";
141
+ import type {
142
+ CompactionQueuedMessage,
143
+ InteractiveModeContext,
144
+ InteractiveModeInitOptions,
145
+ InteractiveSelectorDialogOptions,
146
+ SubmittedUserInput,
147
+ TodoItem,
148
+ TodoPhase,
149
+ } from "./types";
150
+ import { UiHelpers } from "./utils/ui-helpers";
151
+
152
+ const HINT_SHIMMER_PALETTE: ShimmerPalette = {
153
+ low: "dim",
154
+ mid: "muted",
155
+ high: "borderAccent",
156
+ };
157
+
158
+ interface WorkingMessageAccent {
159
+ main: string;
160
+ dim: string;
161
+ }
162
+
163
+ interface WorkingMessageAccentCacheKey {
164
+ sessionName: string | undefined;
165
+ accentSurfaceLuminance: number | undefined;
166
+ sessionAccentEnabled: boolean;
167
+ }
168
+
169
+ function renderWorkingMessage(message: string, accent?: WorkingMessageAccent): string {
170
+ const palette = accent
171
+ ? ({
172
+ low: "dim",
173
+ mid: { ansi: accent.main },
174
+ high: { ansi: accent.main },
175
+ bold: true,
176
+ } satisfies ShimmerPalette)
177
+ : undefined;
178
+ const hint = interruptHint();
179
+ if (!message.endsWith(hint)) return shimmerText(message, theme, palette);
180
+ const header = message.slice(0, -hint.length);
181
+ const hintPalette = accent
182
+ ? ({
183
+ low: "dim",
184
+ mid: { ansi: accent.dim },
185
+ high: { ansi: accent.dim },
186
+ } satisfies ShimmerPalette)
187
+ : HINT_SHIMMER_PALETTE;
188
+ return shimmerSegments(
189
+ [
190
+ { text: header, palette },
191
+ { text: hint, palette: hintPalette },
192
+ ],
193
+ theme,
194
+ );
195
+ }
196
+
197
+ const EDITOR_MAX_HEIGHT_MIN = 6;
198
+ const EDITOR_MAX_HEIGHT_MAX = 18;
199
+ const EDITOR_RESERVED_ROWS = 12;
200
+ const EDITOR_FALLBACK_ROWS = 24;
201
+
202
+ const HUD_NOTE_SUP_DIGITS: Record<string, string> = {
203
+ "0": "\u2070",
204
+ "1": "\u00b9",
205
+ "2": "\u00b2",
206
+ "3": "\u00b3",
207
+ "4": "\u2074",
208
+ "5": "\u2075",
209
+ "6": "\u2076",
210
+ "7": "\u2077",
211
+ "8": "\u2078",
212
+ "9": "\u2079",
213
+ };
214
+
215
+ function formatHudNoteMarker(count: number): string {
216
+ if (count <= 0) return "";
217
+ const sub = String(count)
218
+ .split("")
219
+ .map(d => HUD_NOTE_SUP_DIGITS[d] ?? d)
220
+ .join("");
221
+ return theme.fg("dim", chalk.italic(` \u207a${sub}`));
222
+ }
223
+
224
+ type GoalSubcommand = "set" | "show" | "pause" | "resume" | "drop" | "budget";
225
+
226
+ const GOAL_SUBCOMMANDS = new Set<GoalSubcommand>(["set", "show", "pause", "resume", "drop", "budget"]);
227
+ const PLAN_KEEP_CONTEXT_OPTION_INDEX = 2;
228
+ const PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT = 95;
229
+
230
+ function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; rest: string } {
231
+ const trimmed = args.trim();
232
+ if (!trimmed) return { sub: undefined, rest: "" };
233
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
234
+ if (!match) return { sub: undefined, rest: trimmed };
235
+ const first = match[1].toLowerCase();
236
+ if (GOAL_SUBCOMMANDS.has(first as GoalSubcommand)) {
237
+ return { sub: first as GoalSubcommand, rest: match[2]?.trim() ?? "" };
238
+ }
239
+ return { sub: undefined, rest: trimmed };
240
+ }
241
+
242
+ function formatContextTokenCount(value: number): string {
243
+ return formatNumber(Math.max(0, Math.round(value))).toLowerCase();
244
+ }
245
+
246
+ /** Options for creating an InteractiveMode instance (for future API use) */
247
+ export interface InteractiveModeOptions {
248
+ /** Providers that were migrated during startup */
249
+ migratedProviders?: string[];
250
+ /** Warning message if model fallback occurred */
251
+ modelFallbackMessage?: string;
252
+ /** Initial message to send */
253
+ initialMessage?: string;
254
+ /** Initial images to include with the message */
255
+ initialImages?: ImageContent[];
256
+ /** Additional initial messages to queue */
257
+ initialMessages?: string[];
258
+ }
259
+
260
+ export class InteractiveMode implements InteractiveModeContext {
261
+ session: AgentSession;
262
+ sessionManager: SessionManager;
263
+ settings: Settings;
264
+ keybindings: KeybindingsManager;
265
+ agent: Agent;
266
+ historyStorage?: HistoryStorage;
267
+ titleSystemPrompt?: string;
268
+
269
+ ui: TUI;
270
+ chatContainer: TranscriptContainer;
271
+ pendingMessagesContainer: Container;
272
+ statusContainer: Container;
273
+ todoContainer: Container;
274
+ btwContainer: Container;
275
+ omfgContainer: Container;
276
+ errorBannerContainer: Container;
277
+ editor: CustomEditor;
278
+ editorContainer: Container;
279
+ hookWidgetContainerAbove: Container;
280
+ hookWidgetContainerBelow: Container;
281
+ statusLine: StatusLineComponent;
282
+
283
+ isInitialized = false;
284
+ isBashMode = false;
285
+ toolOutputExpanded = false;
286
+ todoExpanded = false;
287
+ planModeEnabled = false;
288
+ planModePaused = false;
289
+ goalModeEnabled = false;
290
+ goalModePaused = false;
291
+ planModePlanFilePath: string | undefined = undefined;
292
+ loopModeEnabled = false;
293
+ loopPrompt: string | undefined = undefined;
294
+ loopLimit: LoopLimitRuntime | undefined = undefined;
295
+ #loopAutoSubmitTimer: NodeJS.Timeout | undefined;
296
+ #todoAutoClearTimer: NodeJS.Timeout | undefined;
297
+ todoPhases: TodoPhase[] = [];
298
+ hideThinkingBlock = false;
299
+ pendingImages: ImageContent[] = [];
300
+ pendingImageLinks: (string | undefined)[] = [];
301
+ compactionQueuedMessages: CompactionQueuedMessage[] = [];
302
+ pendingTools = new Map<string, ToolExecutionHandle>();
303
+ pendingBashComponents: BashExecutionComponent[] = [];
304
+ bashComponent: BashExecutionComponent | undefined = undefined;
305
+ pendingPythonComponents: EvalExecutionComponent[] = [];
306
+ pythonComponent: EvalExecutionComponent | undefined = undefined;
307
+ isPythonMode = false;
308
+ streamingComponent: AssistantMessageComponent | undefined = undefined;
309
+ streamingMessage: AssistantMessage | undefined = undefined;
310
+ loadingAnimation: Loader | undefined = undefined;
311
+ autoCompactionLoader: Loader | undefined = undefined;
312
+ retryLoader: Loader | undefined = undefined;
313
+ #pendingWorkingMessage: string | undefined;
314
+ #workingMessageAccentCacheKey?: WorkingMessageAccentCacheKey;
315
+ #workingMessageAccentCacheValue?: WorkingMessageAccent;
316
+ #workingMessageAccentCacheHasValue = false;
317
+ get #defaultWorkingMessage(): string {
318
+ return `Working…${interruptHint()}`;
319
+ }
320
+ autoCompactionEscapeHandler?: () => void;
321
+ retryEscapeHandler?: () => void;
322
+ unsubscribe?: () => void;
323
+ onInputCallback?: (input: SubmittedUserInput) => void;
324
+ optimisticUserMessageSignature: string | undefined = undefined;
325
+ locallySubmittedUserSignatures: Set<string> = new Set();
326
+ #pendingSubmittedInput: SubmittedUserInput | undefined;
327
+ #pendingSubmissionDispose: (() => void) | undefined;
328
+ lastSigintTime = 0;
329
+ lastEscapeTime = 0;
330
+ lastLeftTapTime = 0;
331
+ shutdownRequested = false;
332
+ #isShuttingDown = false;
333
+ hookSelector: HookSelectorComponent | undefined = undefined;
334
+ hookInput: HookInputComponent | undefined = undefined;
335
+ hookEditor: HookEditorComponent | undefined = undefined;
336
+ lastStatusSpacer: Spacer | undefined = undefined;
337
+ lastStatusText: Text | undefined = undefined;
338
+ fileSlashCommands: Set<string> = new Set();
339
+ skillCommands: Map<string, string> = new Map();
340
+ oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
341
+
342
+ #pendingSlashCommands: SlashCommand[] = [];
343
+ #cleanupUnsubscribe?: () => void;
344
+ readonly #version: string;
345
+ readonly #changelogMarkdown: string | undefined;
346
+ #planModePreviousTools: string[] | undefined;
347
+ #goalModePreviousTools: string[] | undefined;
348
+ #goalContinuationTimer: NodeJS.Timeout | undefined;
349
+ #goalTurnHadToolCalls = false;
350
+ #goalContinuationTurnInFlight = false;
351
+ #goalSuppressNextContinuation = false;
352
+ #planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
353
+ #pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
354
+ #planModeHasEntered = false;
355
+ #planReviewOverlay: PlanReviewOverlay | undefined;
356
+ #planReviewOverlayHandle: OverlayHandle | undefined;
357
+ readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
358
+ mcpManager?: MCPManager;
359
+ readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
360
+
361
+ readonly #btwController: BtwController;
362
+ readonly #tanCommandController: TanCommandController;
363
+ readonly #omfgController: OmfgController;
364
+ readonly #commandController: CommandController;
365
+ readonly #todoCommandController: TodoCommandController;
366
+ readonly #eventController: EventController;
367
+ readonly #extensionUiController: ExtensionUiController;
368
+ readonly #inputController: InputController;
369
+ readonly #selectorController: SelectorController;
370
+ readonly #uiHelpers: UiHelpers;
371
+ #sttController: STTController | undefined;
372
+ #voiceAnimationInterval: NodeJS.Timeout | undefined;
373
+ #voiceHue = 0;
374
+ #voicePreviousShowHardwareCursor: boolean | null = null;
375
+ #voicePreviousUseTerminalCursor: boolean | null = null;
376
+ #resizeHandler?: () => void;
377
+ #observerRegistry: SessionObserverRegistry;
378
+ #eventBus?: EventBus;
379
+ #eventBusUnsubscribers: Array<() => void> = [];
380
+ #welcomeComponent?: WelcomeComponent;
381
+ readonly #chatHost: ChatBlockHost = { requestRender: () => this.ui.requestRender() };
382
+
383
+ constructor(
384
+ session: AgentSession,
385
+ version: string,
386
+ changelogMarkdown: string | undefined = undefined,
387
+ setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
388
+ lspServers: LspStartupServerInfo[] | undefined = undefined,
389
+ mcpManager?: MCPManager,
390
+ eventBus?: EventBus,
391
+ titleSystemPrompt?: string,
392
+ ) {
393
+ this.session = session;
394
+ this.sessionManager = session.sessionManager;
395
+ this.settings = session.settings;
396
+ this.keybindings = KeybindingsManager.inMemory();
397
+ this.agent = session.agent;
398
+ this.#version = version;
399
+ this.#changelogMarkdown = changelogMarkdown;
400
+ this.#toolUiContextSetter = setToolUIContext;
401
+ this.lspServers = lspServers;
402
+ this.mcpManager = mcpManager;
403
+ this.#eventBus = eventBus;
404
+ this.titleSystemPrompt = titleSystemPrompt;
405
+ if (eventBus) {
406
+ this.#eventBusUnsubscribers.push(
407
+ eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
408
+ this.#handleLspStartupEvent(data as LspStartupEvent);
409
+ }),
410
+ );
411
+ }
412
+
413
+ this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
414
+ this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
415
+ // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
416
+ // capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
417
+ // unless the user opts in, and never emits raw escapes on other terminals.
418
+ setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
419
+ this.chatContainer = new TranscriptContainer();
420
+ this.pendingMessagesContainer = new Container();
421
+ this.statusContainer = new Container();
422
+ this.todoContainer = new Container();
423
+ this.btwContainer = new Container();
424
+ this.omfgContainer = new Container();
425
+ this.errorBannerContainer = new Container();
426
+ this.editor = new CustomEditor(getEditorTheme());
427
+ this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
428
+ this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
429
+ this.editor.onAutocompleteCancel = () => {
430
+ this.ui.requestRender(true);
431
+ };
432
+ this.editor.onAutocompleteUpdate = () => {
433
+ this.ui.requestRender();
434
+ };
435
+ this.#syncEditorMaxHeight();
436
+ this.#resizeHandler = () => {
437
+ this.#syncEditorMaxHeight();
438
+ this.updateEditorTopBorder();
439
+ };
440
+ process.stdout.on("resize", this.#resizeHandler);
441
+ try {
442
+ this.historyStorage = HistoryStorage.open();
443
+ this.editor.setHistoryStorage(this.historyStorage);
444
+ this.historyStorage.setSessionResolver(() => this.sessionManager.getSessionId());
445
+ } catch (error) {
446
+ logger.warn("History storage unavailable", { error: String(error) });
447
+ }
448
+ this.hookWidgetContainerAbove = new Container();
449
+ this.hookWidgetContainerAbove.addChild(new Spacer(1));
450
+ this.hookWidgetContainerBelow = new Container();
451
+ this.editorContainer = new Container();
452
+ this.editorContainer.addChild(this.editor);
453
+ this.statusLine = new StatusLineComponent(session);
454
+ this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
455
+
456
+ this.hideThinkingBlock = settings.get("hideThinkingBlock");
457
+
458
+ const hookCommands: SlashCommand[] = (
459
+ this.session.extensionRunner?.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES) ?? []
460
+ ).map(cmd => ({
461
+ name: cmd.name,
462
+ description: cmd.description ?? "(hook command)",
463
+ getArgumentCompletions: cmd.getArgumentCompletions,
464
+ }));
465
+
466
+ // Convert custom commands (TypeScript) to SlashCommand format
467
+ const customCommands: SlashCommand[] = this.session.customCommands.map(loaded => ({
468
+ name: loaded.command.name,
469
+ description: `${loaded.command.description} (${loaded.source})`,
470
+ }));
471
+
472
+ // Build skill commands from session.skills (if enabled)
473
+ const skillCommandList: SlashCommand[] = [];
474
+ if (settings.get("skills.enableSkillCommands")) {
475
+ for (const skill of this.session.skills) {
476
+ const commandName = `skill:${skill.name}`;
477
+ this.skillCommands.set(commandName, skill.filePath);
478
+ skillCommandList.push({ name: commandName, description: skill.description });
479
+ }
480
+ }
481
+
482
+ // Store pending commands for init() where file commands are loaded async
483
+ this.#pendingSlashCommands = [...BUILTIN_SLASH_COMMANDS, ...hookCommands, ...customCommands, ...skillCommandList];
484
+
485
+ this.#uiHelpers = new UiHelpers(this);
486
+ this.#btwController = new BtwController(this);
487
+ this.#tanCommandController = new TanCommandController(this);
488
+ this.#omfgController = new OmfgController(this);
489
+ this.#extensionUiController = new ExtensionUiController(this);
490
+ this.#eventController = new EventController(this);
491
+ this.#commandController = new CommandController(this);
492
+ this.#todoCommandController = new TodoCommandController(this);
493
+ this.#selectorController = new SelectorController(this);
494
+ this.#inputController = new InputController(this);
495
+ this.#observerRegistry = new SessionObserverRegistry();
496
+ }
497
+
498
+ playWelcomeIntro(): void {
499
+ this.#welcomeComponent?.playIntro(() => this.ui.requestRender());
500
+ }
501
+ async init(options: InteractiveModeInitOptions = {}): Promise<void> {
502
+ if (this.isInitialized) return;
503
+
504
+ this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
505
+
506
+ // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
507
+ this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
508
+
509
+ // Wire the report_tool_issue consent gate to the Yes/No dialog popup.
510
+ // The handler is process-global — subagent tools (which can't reach
511
+ // `showHookSelector` on their own) resolve through this exact closure.
512
+ // `Settings.instance` is the disk-backed singleton; passing it explicitly
513
+ // guarantees the decision persists even when the prompt is triggered
514
+ // from a subagent whose own `Settings` is an in-memory snapshot.
515
+ setAutoQaConsentHandler(() => this.#promptAutoQaConsent(), Settings.instance);
516
+
517
+ await logger.time(
518
+ "InteractiveMode.init:slashCommands",
519
+ this.refreshSlashCommandState.bind(this),
520
+ getProjectDir(),
521
+ );
522
+
523
+ // Get current model info for welcome screen
524
+ const modelName = this.session.model?.name ?? "Unknown";
525
+ const providerName = this.session.model?.provider ?? "Unknown";
526
+
527
+ // Get recent sessions
528
+ const recentSessions = await logger.time("InteractiveMode.init:recentSessions", () =>
529
+ getRecentSessions(this.sessionManager.getSessionDir()).then(sessions =>
530
+ sessions.map(s => ({
531
+ name: s.name,
532
+ timeAgo: s.timeAgo,
533
+ })),
534
+ ),
535
+ );
536
+
537
+ const startupQuiet = settings.get("startup.quiet");
538
+ this.#welcomeComponent = undefined;
539
+
540
+ for (const warning of this.session.configWarnings) {
541
+ this.ui.addChild(new Text(theme.fg("warning", `Warning: ${warning}`), 1, 0));
542
+ this.ui.addChild(new Spacer(1));
543
+ }
544
+
545
+ if (!startupQuiet) {
546
+ // Add welcome header
547
+ this.#welcomeComponent = new WelcomeComponent(
548
+ this.#version,
549
+ modelName,
550
+ providerName,
551
+ recentSessions,
552
+ this.#getWelcomeLspServers(),
553
+ );
554
+
555
+ // Setup UI layout
556
+ this.ui.addChild(new Spacer(1));
557
+ this.ui.addChild(this.#welcomeComponent);
558
+ this.ui.addChild(new Spacer(1));
559
+ if (!options.suppressWelcomeIntro) {
560
+ this.playWelcomeIntro();
561
+ }
562
+
563
+ // Add changelog if provided
564
+ if (this.#changelogMarkdown) {
565
+ this.ui.addChild(new DynamicBorder());
566
+ if (settings.get("collapseChangelog")) {
567
+ const versionMatch = this.#changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
568
+ const latestVersion = versionMatch ? versionMatch[1] : this.#version;
569
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
570
+ this.ui.addChild(new Text(condensedText, 1, 0));
571
+ } else {
572
+ this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
573
+ this.ui.addChild(new Spacer(1));
574
+ this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
575
+ this.ui.addChild(new Spacer(1));
576
+ }
577
+ this.ui.addChild(new DynamicBorder());
578
+ }
579
+ }
580
+
581
+ this.ui.addChild(this.chatContainer);
582
+ this.ui.addChild(this.pendingMessagesContainer);
583
+ this.ui.addChild(this.statusContainer);
584
+ this.ui.addChild(this.todoContainer);
585
+ this.ui.addChild(this.btwContainer);
586
+ this.ui.addChild(this.omfgContainer);
587
+ this.ui.addChild(this.errorBannerContainer);
588
+ this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
589
+ this.ui.addChild(this.hookWidgetContainerAbove);
590
+ this.ui.addChild(this.editorContainer);
591
+ this.ui.addChild(this.hookWidgetContainerBelow);
592
+ this.ui.setFocus(this.editor);
593
+
594
+ this.#inputController.setupKeyHandlers();
595
+ this.#inputController.setupEditorSubmitHandler();
596
+
597
+ // Wire observer registry to EventBus
598
+ if (this.#eventBus) {
599
+ this.#observerRegistry.subscribeToEventBus(this.#eventBus);
600
+ }
601
+ this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
602
+ this.#observerRegistry.onChange(() => {
603
+ this.statusLine.setSubagentCount(this.#observerRegistry.getActiveSubagentCount());
604
+ // Auto-checkmark todos whose matching subagent just succeeded, then
605
+ // re-render so the running override (the static "live" glyph when a
606
+ // subagent is doing the work for a still-pending todo) updates as
607
+ // subagents start, finish, or fail.
608
+ this.#reconcileTodosWithSubagents();
609
+ this.#syncTodoAutoClearTimer();
610
+ this.#renderTodoList();
611
+ this.ui.requestRender();
612
+ });
613
+
614
+ // Load initial todos
615
+ await this.#loadTodoList();
616
+
617
+ // Start the UI. Cold `omp` launch opts into clearing on the first paint so
618
+ // the initial welcome frame does not append over the previous run's scrollback.
619
+ this.ui.start({ clearScrollback: options.clearInitialTerminalHistory === true });
620
+ pushTerminalTitle();
621
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
622
+ this.updateEditorBorderColor();
623
+ this.#syncEditorMaxHeight();
624
+ this.isInitialized = true;
625
+ this.ui.requestRender(true);
626
+
627
+ // Initialize hooks with TUI-based UI context
628
+ await this.initHooksAndCustomTools();
629
+
630
+ // Restore mode from session (e.g. plan mode on resume)
631
+ this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession());
632
+ await this.#reconcileModeFromSession();
633
+
634
+ // Restore unsent editor draft from previous session shutdown (Ctrl+D).
635
+ // One-shot: consumeDraft removes the sidecar after read so the next
636
+ // resume does not re-restore the same text.
637
+ try {
638
+ const draft = await this.sessionManager.consumeDraft();
639
+ if (draft && !this.editor.getText()) {
640
+ this.editor.setText(draft);
641
+ this.updateEditorBorderColor();
642
+ this.ui.requestRender();
643
+ }
644
+ } catch (err) {
645
+ logger.warn("Failed to restore session draft", { error: String(err) });
646
+ }
647
+
648
+ // Subscribe to agent events
649
+ this.#subscribeToAgent();
650
+
651
+ this.#eventBusUnsubscribers.push(
652
+ this.session.subscribe(event => {
653
+ void this.#handleGoalSessionEvent(event);
654
+ }),
655
+ this.sessionManager.onSessionNameChanged(() => {
656
+ this.#handleSessionAccentInputsChanged();
657
+ }),
658
+ onStatusLineSessionAccentChanged(() => {
659
+ this.#syncStatusLineSettings();
660
+ this.#handleSessionAccentInputsChanged();
661
+ }),
662
+ );
663
+ // Set up theme file watcher
664
+ onThemeChange(() => {
665
+ this.#clearWorkingMessageAccentCache();
666
+ clearRenderCache();
667
+ this.ui.invalidate();
668
+ this.updateEditorBorderColor();
669
+ this.ui.requestRender();
670
+ });
671
+
672
+ // Subscribe to terminal dark/light appearance changes.
673
+ // The terminal queries background color via OSC 11 at startup and on
674
+ // Mode 2031 notifications, computing luminance to detect dark/light.
675
+ this.ui.terminal.onAppearanceChange(mode => {
676
+ onTerminalAppearanceChange(mode);
677
+ });
678
+
679
+ // Set up git branch watcher
680
+ this.statusLine.watchBranch(() => {
681
+ this.updateEditorTopBorder();
682
+ this.ui.requestRender();
683
+ });
684
+
685
+ // Initial top border update
686
+ this.updateEditorTopBorder();
687
+ }
688
+
689
+ /** Reload the title-generation system prompt override for the provided working directory. */
690
+ async refreshTitleSystemPrompt(cwd?: string): Promise<void> {
691
+ const basePath = cwd ?? this.sessionManager.getCwd();
692
+ const titleSystemPromptSource = discoverTitleSystemPromptFile(basePath);
693
+ this.titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
694
+ }
695
+
696
+ /** Reload slash commands and autocomplete for the provided working directory. */
697
+ async refreshSlashCommandState(cwd?: string): Promise<void> {
698
+ const basePath = cwd ?? this.sessionManager.getCwd();
699
+ const fileCommands = await loadSlashCommands({ cwd: basePath });
700
+ this.fileSlashCommands = new Set(fileCommands.map(cmd => cmd.name));
701
+ const fileSlashCommands: SlashCommand[] = fileCommands.map(cmd => ({
702
+ name: cmd.name,
703
+ description: cmd.description,
704
+ }));
705
+ const autocompleteProvider = this.#inputController.createAutocompleteProvider(
706
+ [...this.#pendingSlashCommands, ...fileSlashCommands],
707
+ basePath,
708
+ );
709
+ this.editor.setAutocompleteProvider(autocompleteProvider);
710
+ this.session.setSlashCommands(fileCommands);
711
+ }
712
+
713
+ /**
714
+ * Re-point the process and every cwd-derived cache at `newCwd` after the
715
+ * active session's working directory changed (`/move` relocation or resuming
716
+ * a session from another project). The SessionManager's cwd MUST already
717
+ * reflect `newCwd` before this is called.
718
+ */
719
+ async applyCwdChange(newCwd: string): Promise<void> {
720
+ setProjectDir(newCwd);
721
+ // Re-scope project settings (`.claude/settings.yml` etc.) to the new
722
+ // directory in place so the active session and every settings reader pick
723
+ // up the destination project's configuration.
724
+ if (isSettingsInitialized()) {
725
+ await settings.reloadForCwd(newCwd);
726
+ }
727
+ // Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
728
+ // the next prompt sees everything scoped to the new project directory.
729
+ clearClaudePluginRootsCache();
730
+ await this.refreshTitleSystemPrompt(newCwd);
731
+ resetCapabilities();
732
+ await this.refreshSlashCommandState(newCwd);
733
+ await this.session.refreshSshTool({ activateIfAvailable: true });
734
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
735
+ this.statusLine.invalidate();
736
+ this.updateEditorTopBorder();
737
+ }
738
+
739
+ async getUserInput(): Promise<SubmittedUserInput> {
740
+ if (this.session.getGoalModeState()?.mode === "exiting") {
741
+ await this.#exitGoalMode({ reason: "completed", silent: true });
742
+ }
743
+ const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
744
+ this.onInputCallback = input => {
745
+ this.onInputCallback = undefined;
746
+ resolve(input);
747
+ };
748
+ this.#scheduleLoopAutoSubmit();
749
+ this.#scheduleGoalContinuation();
750
+
751
+ using _ = new EventLoopKeepalive();
752
+ return await promise;
753
+ }
754
+
755
+ #scheduleLoopAutoSubmit(): void {
756
+ this.#cancelLoopAutoSubmit();
757
+ if (!this.loopModeEnabled || !this.loopPrompt) return;
758
+ const prompt = this.loopPrompt;
759
+ const loopAction = settings.get("loop.mode");
760
+ this.#deferLoopAutoSubmit(() => {
761
+ void this.#runLoopIteration(loopAction, prompt);
762
+ });
763
+ }
764
+
765
+ #deferLoopAutoSubmit(callback: () => void): void {
766
+ // Brief delay so the user has a chance to press Esc between iterations.
767
+ this.#loopAutoSubmitTimer = setTimeout(() => {
768
+ this.#loopAutoSubmitTimer = undefined;
769
+ if (!this.loopModeEnabled || !this.onInputCallback) return;
770
+ callback();
771
+ }, 800);
772
+ }
773
+
774
+ #cancelLoopAutoSubmit(): void {
775
+ if (this.#loopAutoSubmitTimer) {
776
+ clearTimeout(this.#loopAutoSubmitTimer);
777
+ this.#loopAutoSubmitTimer = undefined;
778
+ }
779
+ }
780
+
781
+ #scheduleGoalContinuation(): void {
782
+ this.#cancelGoalContinuation();
783
+ if (this.loopModeEnabled) return;
784
+ if (!this.onInputCallback) return;
785
+ if (!this.session.settings.get("goal.continuationModes").includes("interactive")) return;
786
+ if (this.planModeEnabled || this.planModePaused) return;
787
+ if (!this.goalModeEnabled || this.goalModePaused) return;
788
+ if (this.#goalSuppressNextContinuation) return;
789
+ if (this.#pendingSubmittedInput) return;
790
+ if (this.editor.getText().trim().length > 0) return;
791
+ if ((this.pendingImages?.length ?? 0) > 0) return;
792
+ const state = this.session.getGoalModeState();
793
+ if (!state?.enabled || state.goal.status !== "active") return;
794
+ const prompt = this.session.goalRuntime.buildContinuationPrompt();
795
+ if (!prompt) return;
796
+ this.#goalContinuationTimer = setTimeout(() => {
797
+ this.#goalContinuationTimer = undefined;
798
+ if (!this.onInputCallback) return;
799
+ if (!this.goalModeEnabled || this.goalModePaused) return;
800
+ if (this.#pendingSubmittedInput) return;
801
+ if (this.editor.getText().trim().length > 0) return;
802
+ if ((this.pendingImages?.length ?? 0) > 0) return;
803
+ const latestState = this.session.getGoalModeState();
804
+ if (!latestState?.enabled || latestState.goal.status !== "active") return;
805
+ this.#goalContinuationTurnInFlight = true;
806
+ this.onInputCallback(
807
+ this.startPendingSubmission({
808
+ text: prompt,
809
+ customType: "goal-continuation",
810
+ display: false,
811
+ }),
812
+ );
813
+ }, 800);
814
+ }
815
+
816
+ #cancelGoalContinuation(): void {
817
+ if (this.#goalContinuationTimer) {
818
+ clearTimeout(this.#goalContinuationTimer);
819
+ this.#goalContinuationTimer = undefined;
820
+ }
821
+ }
822
+
823
+ #isLoopAutoSubmitBlocked(): boolean {
824
+ return this.session.isStreaming || this.session.isCompacting || this.session.hasPostPromptWork;
825
+ }
826
+
827
+ #submitLoopPromptWhenReady(prompt: string): void {
828
+ if (!this.loopModeEnabled || this.loopPrompt !== prompt || !this.onInputCallback) return;
829
+ if (isLoopDurationExpired(this.loopLimit)) {
830
+ this.disableLoopMode("Loop time limit reached. Loop mode disabled.");
831
+ return;
832
+ }
833
+ if (this.#isLoopAutoSubmitBlocked()) {
834
+ this.#deferLoopAutoSubmit(() => this.#submitLoopPromptWhenReady(prompt));
835
+ return;
836
+ }
837
+ this.onInputCallback(this.startPendingSubmission({ text: prompt }));
838
+ }
839
+
840
+ async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
841
+ if (!this.loopModeEnabled || this.loopPrompt !== prompt || !this.onInputCallback) return;
842
+ if (this.#isLoopAutoSubmitBlocked()) {
843
+ this.#deferLoopAutoSubmit(() => {
844
+ void this.#runLoopIteration(action, prompt);
845
+ });
846
+ return;
847
+ }
848
+
849
+ if (!consumeLoopLimitIteration(this.loopLimit)) {
850
+ this.disableLoopMode("Loop limit reached. Loop mode disabled.");
851
+ return;
852
+ }
853
+
854
+ if (action === "compact") {
855
+ await this.handleCompactCommand();
856
+ } else if (action === "reset") {
857
+ await this.handleClearCommand();
858
+ }
859
+ this.#submitLoopPromptWhenReady(prompt);
860
+ }
861
+
862
+ disableLoopMode(message = "Loop mode disabled."): void {
863
+ const wasEnabled = this.loopModeEnabled;
864
+ this.loopModeEnabled = false;
865
+ this.loopPrompt = undefined;
866
+ this.loopLimit = undefined;
867
+ this.#cancelLoopAutoSubmit();
868
+ this.statusLine.setLoopModeStatus(undefined);
869
+ this.updateEditorTopBorder();
870
+ this.ui.requestRender();
871
+ if (wasEnabled) {
872
+ this.showStatus(message);
873
+ }
874
+ }
875
+
876
+ /**
877
+ * Pause the loop without exiting it: drops the captured prompt and any
878
+ * pending auto-resubmit. Loop mode stays enabled — the next prompt the
879
+ * user submits becomes the new loop prompt and resumes iteration.
880
+ */
881
+ pauseLoop(): void {
882
+ this.loopPrompt = undefined;
883
+ this.#cancelLoopAutoSubmit();
884
+ }
885
+
886
+ async handleLoopCommand(args = ""): Promise<void> {
887
+ if (this.loopModeEnabled) {
888
+ this.disableLoopMode();
889
+ return;
890
+ }
891
+ const parsedLimit = parseLoopLimitArgs(args);
892
+ if (typeof parsedLimit === "string") {
893
+ this.showError(parsedLimit);
894
+ return;
895
+ }
896
+ this.loopModeEnabled = true;
897
+ this.loopPrompt = undefined;
898
+ this.loopLimit = createLoopLimitRuntime(parsedLimit);
899
+ this.statusLine.setLoopModeStatus({ enabled: true });
900
+ this.updateEditorTopBorder();
901
+ this.ui.requestRender();
902
+ const limitSuffix = parsedLimit ? ` Limited to ${describeLoopLimit(parsedLimit)}.` : "";
903
+ const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
904
+ this.showStatus(
905
+ `Loop mode enabled.${limitSuffix}${remainingSuffix} Your next prompt will repeat after each turn. Esc cancels the current iteration; /loop again to disable.`,
906
+ );
907
+ }
908
+
909
+ recordLocalSubmission(text: string, imageCount = 0): () => void {
910
+ if (this.isKnownSlashCommand(text)) {
911
+ return () => {};
912
+ }
913
+ const signature = `${text}\u0000${imageCount}`;
914
+ this.locallySubmittedUserSignatures.add(signature);
915
+ let disposed = false;
916
+ return () => {
917
+ if (disposed) return;
918
+ disposed = true;
919
+ this.locallySubmittedUserSignatures.delete(signature);
920
+ };
921
+ }
922
+
923
+ async withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T> {
924
+ const dispose = this.recordLocalSubmission(text, options?.imageCount ?? 0);
925
+ try {
926
+ return await fn();
927
+ } catch (err) {
928
+ dispose();
929
+ throw err;
930
+ }
931
+ }
932
+
933
+ startPendingSubmission(input: {
934
+ text: string;
935
+ images?: ImageContent[];
936
+ imageLinks?: (string | undefined)[];
937
+ customType?: string;
938
+ display?: boolean;
939
+ }): SubmittedUserInput {
940
+ const submission: SubmittedUserInput = {
941
+ text: input.text,
942
+ images: input.images,
943
+ imageLinks: input.imageLinks,
944
+ customType: input.customType,
945
+ display: input.display,
946
+ cancelled: false,
947
+ started: false,
948
+ };
949
+ this.#pendingSubmittedInput = submission;
950
+ if (!submission.customType) {
951
+ this.#resetGoalContinuationSuppression();
952
+ const imageCount = submission.images?.length ?? 0;
953
+ this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
954
+ this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
955
+ this.addMessageToChat(
956
+ {
957
+ role: "user",
958
+ content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
959
+ attribution: "user",
960
+ timestamp: Date.now(),
961
+ },
962
+ { imageLinks: input.imageLinks },
963
+ );
964
+ } else {
965
+ this.optimisticUserMessageSignature = undefined;
966
+ this.#pendingSubmissionDispose = undefined;
967
+ }
968
+ this.editor.setText("");
969
+ this.editor.imageLinks = undefined;
970
+ this.ensureLoadingAnimation();
971
+ this.ui.requestRender();
972
+ return submission;
973
+ }
974
+
975
+ cancelPendingSubmission(): boolean {
976
+ const submission = this.#pendingSubmittedInput;
977
+ if (!submission || submission.started) {
978
+ return false;
979
+ }
980
+
981
+ submission.cancelled = true;
982
+ this.#pendingSubmittedInput = undefined;
983
+ this.optimisticUserMessageSignature = undefined;
984
+ this.#pendingSubmissionDispose?.();
985
+ this.#pendingSubmissionDispose = undefined;
986
+ this.#pendingWorkingMessage = undefined;
987
+ if (submission.customType === "goal-continuation") {
988
+ this.#goalContinuationTurnInFlight = false;
989
+ }
990
+ if (this.loadingAnimation) {
991
+ this.#stopLoadingAnimation(true);
992
+ }
993
+ if (!submission.customType) {
994
+ this.pendingImages = submission.images ? [...submission.images] : [];
995
+ this.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
996
+ this.editor.imageLinks = this.pendingImageLinks;
997
+ this.rebuildChatFromMessages();
998
+ this.editor.setText(submission.text);
999
+ }
1000
+ this.updateEditorBorderColor();
1001
+ this.ui.requestRender();
1002
+ return true;
1003
+ }
1004
+
1005
+ markPendingSubmissionStarted(input: SubmittedUserInput): boolean {
1006
+ if (this.#pendingSubmittedInput !== input || input.cancelled) {
1007
+ return false;
1008
+ }
1009
+ input.started = true;
1010
+ return true;
1011
+ }
1012
+
1013
+ finishPendingSubmission(input: SubmittedUserInput): void {
1014
+ const wasPendingSubmission = this.#pendingSubmittedInput === input;
1015
+ const pendingSubmissionDispose = this.#pendingSubmissionDispose;
1016
+ if (wasPendingSubmission) {
1017
+ this.#pendingSubmittedInput = undefined;
1018
+ this.#pendingSubmissionDispose = undefined;
1019
+ }
1020
+ if (input.customType === "goal-continuation") {
1021
+ this.#goalContinuationTurnInFlight = false;
1022
+ }
1023
+
1024
+ if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
1025
+ this.optimisticUserMessageSignature = undefined;
1026
+ pendingSubmissionDispose?.();
1027
+ this.#pendingWorkingMessage = undefined;
1028
+ if (this.loadingAnimation) {
1029
+ this.#stopLoadingAnimation(true);
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ #computeEditorMaxHeight(): number {
1035
+ const rows = this.ui.terminal.rows;
1036
+ const terminalRows = Number.isFinite(rows) && rows > 0 ? rows : EDITOR_FALLBACK_ROWS;
1037
+ const maxHeight = terminalRows - EDITOR_RESERVED_ROWS;
1038
+ return Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, maxHeight));
1039
+ }
1040
+
1041
+ #syncEditorMaxHeight(): void {
1042
+ this.editor.setMaxHeight(this.#computeEditorMaxHeight());
1043
+ }
1044
+
1045
+ #syncStatusLineSettings(): void {
1046
+ this.statusLine.updateSettings({
1047
+ preset: settings.get("statusLine.preset"),
1048
+ leftSegments: settings.get("statusLine.leftSegments"),
1049
+ rightSegments: settings.get("statusLine.rightSegments"),
1050
+ separator: settings.get("statusLine.separator"),
1051
+ showHookStatus: settings.get("statusLine.showHookStatus"),
1052
+ sessionAccent: settings.get("statusLine.sessionAccent"),
1053
+ segmentOptions: settings.get("statusLine.segmentOptions"),
1054
+ });
1055
+ }
1056
+
1057
+ #handleSessionAccentInputsChanged(): void {
1058
+ this.#clearWorkingMessageAccentCache();
1059
+ this.statusLine.invalidate();
1060
+ this.updateEditorBorderColor();
1061
+ }
1062
+
1063
+ updateEditorBorderColor(): void {
1064
+ if (this.isBashMode) {
1065
+ this.editor.borderColor = theme.getBashModeBorderColor();
1066
+ } else if (this.isPythonMode) {
1067
+ this.editor.borderColor = theme.getPythonModeBorderColor();
1068
+ } else {
1069
+ const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
1070
+ const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
1071
+ const hex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
1072
+ const ansi = getSessionAccentAnsi(hex);
1073
+ if (ansi) {
1074
+ this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
1075
+ } else {
1076
+ const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
1077
+ this.editor.borderColor = theme.getThinkingBorderColor(level);
1078
+ }
1079
+ }
1080
+ this.updateEditorTopBorder();
1081
+ this.ui.requestRender();
1082
+ }
1083
+
1084
+ updateEditorTopBorder(): void {
1085
+ const availableWidth = this.editor.getTopBorderAvailableWidth(this.ui.terminal.columns);
1086
+ const topBorder = this.statusLine.getTopBorder(availableWidth);
1087
+ this.editor.setTopBorder(topBorder);
1088
+ }
1089
+
1090
+ rebuildChatFromMessages(): void {
1091
+ this.chatContainer.clear();
1092
+ // Full-history transcript: compactions render as inline dividers instead
1093
+ // of restarting the visible conversation (the LLM context still resets).
1094
+ const context = this.session.buildTranscriptSessionContext();
1095
+ this.renderSessionContext(context);
1096
+ }
1097
+
1098
+ #formatTodoLine(todo: TodoItem, prefix: string, matched: boolean): string {
1099
+ const checkbox = theme.checkbox;
1100
+ const marker = formatHudNoteMarker(todo.notes?.length ?? 0);
1101
+ switch (todo.status) {
1102
+ case "completed":
1103
+ return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`) + marker;
1104
+ case "in_progress":
1105
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1106
+ case "abandoned":
1107
+ return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1108
+ default:
1109
+ if (matched) {
1110
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1111
+ }
1112
+ return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1113
+ }
1114
+ }
1115
+
1116
+ #getActiveSubagentDescriptions(): string[] {
1117
+ const out: string[] = [];
1118
+ for (const session of this.#observerRegistry.getSessions()) {
1119
+ if (session.kind !== "subagent") continue;
1120
+ if (session.status !== "active") continue;
1121
+ const candidate =
1122
+ session.description?.trim() || session.progress?.description?.trim() || session.label?.trim();
1123
+ if (candidate) out.push(candidate);
1124
+ }
1125
+ return out;
1126
+ }
1127
+
1128
+ /**
1129
+ * Auto-complete any pending/in_progress todo whose content matches a
1130
+ * subagent that has finished successfully. Fires on every observer
1131
+ * `onChange` so the visual state stays in sync with subagent lifecycle
1132
+ * without requiring the agent to issue a follow-up `todo`. Failed
1133
+ * and aborted subagents are intentionally NOT auto-completed — those
1134
+ * stay open so the user (or the next agent turn) can decide what to do.
1135
+ *
1136
+ * Idempotent: only flips open tasks, never re-touches completed ones.
1137
+ */
1138
+ #reconcileTodosWithSubagents(): void {
1139
+ const completedDescs: string[] = [];
1140
+ for (const session of this.#observerRegistry.getSessions()) {
1141
+ if (session.kind !== "subagent") continue;
1142
+ if (session.status !== "completed") continue;
1143
+ const candidate =
1144
+ session.description?.trim() || session.progress?.description?.trim() || session.label?.trim();
1145
+ if (candidate) completedDescs.push(candidate);
1146
+ }
1147
+ if (completedDescs.length === 0) return;
1148
+
1149
+ let mutated = false;
1150
+ const next: TodoPhase[] = this.todoPhases.map(phase => ({
1151
+ name: phase.name,
1152
+ tasks: phase.tasks.map(task => {
1153
+ if (task.status !== "pending" && task.status !== "in_progress") return task;
1154
+ if (!todoMatchesAnyDescription(task.content, completedDescs)) return task;
1155
+ mutated = true;
1156
+ return { ...task, status: "completed" as const };
1157
+ }),
1158
+ }));
1159
+ if (!mutated) return;
1160
+ this.todoPhases = next;
1161
+ this.session.setTodoPhases(next);
1162
+ }
1163
+
1164
+ #cancelTodoAutoClearTimer(): void {
1165
+ if (!this.#todoAutoClearTimer) return;
1166
+ clearTimeout(this.#todoAutoClearTimer);
1167
+ this.#todoAutoClearTimer = undefined;
1168
+ }
1169
+
1170
+ #isClosedTodo(task: TodoItem): boolean {
1171
+ return task.status === "completed" || task.status === "abandoned";
1172
+ }
1173
+
1174
+ #hasClosedTodos(phases: TodoPhase[]): boolean {
1175
+ return phases.some(phase => phase.tasks.some(task => this.#isClosedTodo(task)));
1176
+ }
1177
+
1178
+ #removeClosedTodos(phases: TodoPhase[]): TodoPhase[] {
1179
+ const next: TodoPhase[] = [];
1180
+ for (const phase of phases) {
1181
+ const tasks = phase.tasks.filter(task => !this.#isClosedTodo(task));
1182
+ if (tasks.length > 0) next.push({ name: phase.name, tasks });
1183
+ }
1184
+ return next;
1185
+ }
1186
+
1187
+ #syncTodoAutoClearTimer(): void {
1188
+ this.#cancelTodoAutoClearTimer();
1189
+ const delaySeconds = this.settings.get("tasks.todoClearDelay");
1190
+ if (!Number.isFinite(delaySeconds) || delaySeconds < 0 || !this.#hasClosedTodos(this.todoPhases)) return;
1191
+ if (delaySeconds === 0) {
1192
+ this.todoPhases = this.#removeClosedTodos(this.todoPhases);
1193
+ return;
1194
+ }
1195
+
1196
+ this.#todoAutoClearTimer = setTimeout(() => {
1197
+ this.#todoAutoClearTimer = undefined;
1198
+ this.todoPhases = this.#removeClosedTodos(this.todoPhases);
1199
+ this.#renderTodoList();
1200
+ this.ui.requestRender();
1201
+ }, delaySeconds * 1000);
1202
+ this.#todoAutoClearTimer.unref?.();
1203
+ }
1204
+
1205
+ #getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
1206
+ const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
1207
+ const active = nonEmpty.find(phase =>
1208
+ phase.tasks.some(task => task.status === "pending" || task.status === "in_progress"),
1209
+ );
1210
+ return active ?? nonEmpty[nonEmpty.length - 1];
1211
+ }
1212
+
1213
+ #renderTodoList(): void {
1214
+ this.todoContainer.clear();
1215
+ const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
1216
+ if (phases.length === 0) return;
1217
+ const indent = " ";
1218
+ const hook = theme.tree.hook;
1219
+ const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))];
1220
+
1221
+ const activeDescs = this.#getActiveSubagentDescriptions();
1222
+ // A pending todo "lights up" (accent + running glyph) when an in-flight
1223
+ // subagent is doing its work, matched by normalized content overlap.
1224
+ const isMatched = (todo: TodoItem): boolean =>
1225
+ activeDescs.length > 0 && todoMatchesAnyDescription(todo.content, activeDescs);
1226
+
1227
+ if (!this.todoExpanded) {
1228
+ const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
1229
+ const activePhase = phases[activeIdx];
1230
+ if (!activePhase) return;
1231
+ const { visible, hiddenOpenCount } = selectStickyTodoWindow(activePhase.tasks, 5);
1232
+
1233
+ lines.push(
1234
+ `${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(activePhase.name, activeIdx + 1)}`)}`,
1235
+ );
1236
+ visible.forEach((todo, index) => {
1237
+ const prefix = `${indent}${index === 0 ? hook : " "} `;
1238
+ lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1239
+ });
1240
+ if (hiddenOpenCount > 0) {
1241
+ lines.push(theme.fg("muted", `${indent} ${hook} +${hiddenOpenCount} more`));
1242
+ }
1243
+ this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1244
+ return;
1245
+ }
1246
+
1247
+ phases.forEach((phase, phaseIndex) => {
1248
+ lines.push(`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(phase.name, phaseIndex + 1)}`)}`);
1249
+ phase.tasks.forEach((todo, index) => {
1250
+ const prefix = `${indent}${index === 0 ? hook : " "} `;
1251
+ lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1252
+ });
1253
+ });
1254
+
1255
+ this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1256
+ }
1257
+
1258
+ async #loadTodoList(): Promise<void> {
1259
+ this.todoPhases = this.session.getTodoPhases();
1260
+ this.#syncTodoAutoClearTimer();
1261
+ this.#renderTodoList();
1262
+ }
1263
+
1264
+ async #getPlanFilePath(): Promise<string> {
1265
+ return this.session.getPlanReferencePath() || "local://PLAN.md";
1266
+ }
1267
+
1268
+ #resolvePlanFilePath(planFilePath: string): string {
1269
+ if (planFilePath.startsWith("local:")) {
1270
+ const normalized = normalizeLocalScheme(planFilePath);
1271
+ return resolveLocalUrlToPath(normalized, {
1272
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
1273
+ getSessionId: () => this.sessionManager.getSessionId(),
1274
+ });
1275
+ }
1276
+ return path.resolve(this.sessionManager.getCwd(), planFilePath);
1277
+ }
1278
+
1279
+ #updatePlanModeStatus(): void {
1280
+ const status =
1281
+ this.planModeEnabled || this.planModePaused
1282
+ ? {
1283
+ enabled: this.planModeEnabled,
1284
+ paused: this.planModePaused,
1285
+ }
1286
+ : undefined;
1287
+ this.statusLine.setPlanModeStatus(status);
1288
+ this.updateEditorTopBorder();
1289
+ this.ui.requestRender();
1290
+ }
1291
+
1292
+ #updateGoalModeStatus(): void {
1293
+ const status =
1294
+ this.goalModeEnabled || this.goalModePaused
1295
+ ? { enabled: this.goalModeEnabled, paused: this.goalModePaused }
1296
+ : undefined;
1297
+ this.statusLine.setGoalModeStatus(status);
1298
+ this.updateEditorTopBorder();
1299
+ this.ui.requestRender();
1300
+ }
1301
+
1302
+ #resetGoalContinuationSuppression(): void {
1303
+ this.#goalSuppressNextContinuation = false;
1304
+ }
1305
+
1306
+ #getPausedGoalState(): GoalModeState | undefined {
1307
+ const state = this.session.getGoalModeState();
1308
+ if (!state?.goal || state.enabled || state.goal.status !== "paused") {
1309
+ return undefined;
1310
+ }
1311
+ return state;
1312
+ }
1313
+
1314
+ #goalFromModeData(modeData: SessionContext["modeData"]): Goal | undefined {
1315
+ const goal = modeData?.goal;
1316
+ if (!goal || typeof goal !== "object") return undefined;
1317
+ const value = goal as Record<string, unknown>;
1318
+ if (
1319
+ typeof value.id !== "string" ||
1320
+ typeof value.objective !== "string" ||
1321
+ typeof value.status !== "string" ||
1322
+ typeof value.tokensUsed !== "number" ||
1323
+ typeof value.timeUsedSeconds !== "number" ||
1324
+ typeof value.createdAt !== "number" ||
1325
+ typeof value.updatedAt !== "number"
1326
+ ) {
1327
+ return undefined;
1328
+ }
1329
+ return {
1330
+ id: value.id,
1331
+ objective: value.objective,
1332
+ status: value.status as Goal["status"],
1333
+ tokenBudget: typeof value.tokenBudget === "number" ? value.tokenBudget : undefined,
1334
+ tokensUsed: value.tokensUsed,
1335
+ timeUsedSeconds: value.timeUsedSeconds,
1336
+ createdAt: value.createdAt,
1337
+ updatedAt: value.updatedAt,
1338
+ };
1339
+ }
1340
+
1341
+ async #handleGoalSessionEvent(event: AgentSessionEvent): Promise<void> {
1342
+ if (event.type === "agent_start") {
1343
+ this.#goalTurnHadToolCalls = false;
1344
+ this.#cancelGoalContinuation();
1345
+ return;
1346
+ }
1347
+ if (event.type === "tool_execution_start") {
1348
+ this.#goalTurnHadToolCalls = true;
1349
+ if (!this.#goalContinuationTurnInFlight) {
1350
+ this.#resetGoalContinuationSuppression();
1351
+ }
1352
+ return;
1353
+ }
1354
+ if (event.type === "message_start" && event.message.role === "user" && !event.message.synthetic) {
1355
+ this.#resetGoalContinuationSuppression();
1356
+ return;
1357
+ }
1358
+ if (event.type === "goal_updated") {
1359
+ // Handle drop before clearing goalModeEnabled so #exitGoalMode can
1360
+ // still restore the previous tool set while the flag is true.
1361
+ if (event.state?.goal?.status === "dropped") {
1362
+ await this.#exitGoalMode({ reason: "dropped", silent: true });
1363
+ return;
1364
+ }
1365
+ this.goalModeEnabled = event.state?.enabled === true;
1366
+ this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
1367
+ if (!event.state?.enabled) {
1368
+ this.#cancelGoalContinuation();
1369
+ }
1370
+ this.#updateGoalModeStatus();
1371
+ return;
1372
+ }
1373
+ if (event.type !== "agent_end") {
1374
+ return;
1375
+ }
1376
+ if (this.#goalContinuationTurnInFlight) {
1377
+ this.#goalSuppressNextContinuation = !this.#goalTurnHadToolCalls;
1378
+ this.#goalContinuationTurnInFlight = false;
1379
+ }
1380
+ if (this.session.getGoalModeState()?.mode === "exiting") {
1381
+ await this.#exitGoalMode({ reason: "completed", silent: true });
1382
+ return;
1383
+ }
1384
+ this.#scheduleGoalContinuation();
1385
+ }
1386
+
1387
+ async #applyPlanModeModel(): Promise<void> {
1388
+ const resolved = this.session.resolveRoleModelWithThinking("plan");
1389
+ if (!resolved.model) return;
1390
+
1391
+ const currentModel = this.session.model;
1392
+ const sameModel = modelsAreEqual(currentModel, resolved.model);
1393
+ const planThinkingLevel = resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined;
1394
+
1395
+ this.#planModePreviousModelState = currentModel
1396
+ ? { model: currentModel, thinkingLevel: this.session.thinkingLevel }
1397
+ : undefined;
1398
+
1399
+ if (!sameModel) {
1400
+ if (this.session.isStreaming) {
1401
+ this.#pendingModelSwitch = { model: resolved.model, thinkingLevel: planThinkingLevel };
1402
+ return;
1403
+ }
1404
+ try {
1405
+ await this.session.setModelTemporary(resolved.model, planThinkingLevel);
1406
+ } catch (error) {
1407
+ this.showWarning(
1408
+ `Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
1409
+ );
1410
+ }
1411
+ } else if (planThinkingLevel) {
1412
+ this.session.setThinkingLevel(planThinkingLevel);
1413
+ }
1414
+ }
1415
+
1416
+ /** Apply any deferred model switch after the current stream ends. */
1417
+ async flushPendingModelSwitch(): Promise<void> {
1418
+ const pending = this.#pendingModelSwitch;
1419
+ if (!pending) return;
1420
+ this.#pendingModelSwitch = undefined;
1421
+ try {
1422
+ await this.session.setModelTemporary(pending.model, pending.thinkingLevel);
1423
+ } catch (error) {
1424
+ this.showWarning(
1425
+ `Failed to switch model after streaming: ${error instanceof Error ? error.message : String(error)}`,
1426
+ );
1427
+ }
1428
+ }
1429
+
1430
+ async #clearTransientModeState(): Promise<void> {
1431
+ if (this.planModeEnabled || this.planModePaused) {
1432
+ if (this.#planModePreviousTools !== undefined) {
1433
+ await this.session.setActiveToolsByName(this.#planModePreviousTools);
1434
+ }
1435
+ this.session.setStandingResolveHandler?.(null);
1436
+ this.session.setPlanModeState(undefined);
1437
+ this.planModeEnabled = false;
1438
+ this.planModePaused = false;
1439
+ this.planModePlanFilePath = undefined;
1440
+ this.#planModePreviousTools = undefined;
1441
+ this.#planModePreviousModelState = undefined;
1442
+ this.#pendingModelSwitch = undefined;
1443
+ this.#planModeHasEntered = false;
1444
+ this.#updatePlanModeStatus();
1445
+ }
1446
+
1447
+ if (this.goalModeEnabled || this.goalModePaused) {
1448
+ if (this.#goalModePreviousTools !== undefined) {
1449
+ await this.session.setActiveToolsByName(this.#goalModePreviousTools);
1450
+ }
1451
+ this.session.setGoalModeState(undefined);
1452
+ this.goalModeEnabled = false;
1453
+ this.goalModePaused = false;
1454
+ this.#goalModePreviousTools = undefined;
1455
+ this.#goalTurnHadToolCalls = false;
1456
+ this.#goalContinuationTurnInFlight = false;
1457
+ this.#goalSuppressNextContinuation = false;
1458
+ this.#cancelGoalContinuation();
1459
+ this.#updateGoalModeStatus();
1460
+ }
1461
+ }
1462
+
1463
+ /** Reconcile mode state from session entries on resume/switch. */
1464
+ async #reconcileModeFromSession(): Promise<void> {
1465
+ await this.#clearTransientModeState();
1466
+ const sessionContext = this.sessionManager.buildSessionContext();
1467
+ const goalEnabled = this.session.settings.get("goal.enabled");
1468
+ if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
1469
+ this.sessionManager.appendModeChange("none");
1470
+ return;
1471
+ }
1472
+ if (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused") {
1473
+ const goal = this.#goalFromModeData(sessionContext.modeData);
1474
+ if (!goal) {
1475
+ this.sessionManager.appendModeChange("none");
1476
+ return;
1477
+ }
1478
+ this.session.setGoalModeState({
1479
+ enabled: sessionContext.mode === "goal",
1480
+ mode: "active",
1481
+ goal,
1482
+ });
1483
+ const restored = await this.session.goalRuntime.onThreadResumed();
1484
+ this.goalModeEnabled = restored?.enabled === true;
1485
+ this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
1486
+ // sdk.ts excludes "goal" from the initial active tool set unconditionally.
1487
+ // Re-add it now so the agent can call resume, complete, or drop on this goal.
1488
+ if (restored?.goal) {
1489
+ const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1490
+ this.#goalModePreviousTools = previousTools;
1491
+ await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
1492
+ }
1493
+ this.#updateGoalModeStatus();
1494
+ return;
1495
+ }
1496
+ if (!this.session.settings.get("plan.enabled")) {
1497
+ // Clear stale plan/plan_paused mode so re-enabling the setting
1498
+ // later doesn't unexpectedly restore an old plan session.
1499
+ if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused") {
1500
+ this.sessionManager.appendModeChange("none");
1501
+ }
1502
+ return;
1503
+ }
1504
+ if (sessionContext.mode === "plan") {
1505
+ const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
1506
+ await this.#enterPlanMode({ planFilePath });
1507
+ } else if (sessionContext.mode === "plan_paused") {
1508
+ this.planModePaused = true;
1509
+ this.#planModeHasEntered = true;
1510
+ this.#updatePlanModeStatus();
1511
+ }
1512
+ }
1513
+
1514
+ async #enterPlanMode(options?: { planFilePath?: string; workflow?: "parallel" | "iterative" }): Promise<void> {
1515
+ if (this.planModeEnabled) {
1516
+ return;
1517
+ }
1518
+ if (this.goalModeEnabled || this.goalModePaused) {
1519
+ this.showWarning("Exit goal mode first.");
1520
+ return;
1521
+ }
1522
+
1523
+ this.planModePaused = false;
1524
+
1525
+ const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
1526
+ const previousTools = this.session.getActiveToolNames();
1527
+ const hasResolveTool = this.session.getToolByName("resolve") !== undefined;
1528
+ const planTools = hasResolveTool ? [...previousTools, "resolve"] : previousTools;
1529
+ const uniquePlanTools = [...new Set(planTools)];
1530
+
1531
+ this.#planModePreviousTools = previousTools;
1532
+ this.planModePlanFilePath = planFilePath;
1533
+ this.planModeEnabled = true;
1534
+
1535
+ await this.session.setActiveToolsByName(uniquePlanTools);
1536
+ this.session.setPlanModeState({
1537
+ enabled: true,
1538
+ planFilePath,
1539
+ workflow: options?.workflow ?? "parallel",
1540
+ reentry: this.#planModeHasEntered,
1541
+ });
1542
+ this.session.setStandingResolveHandler?.(input => this.#runPlanApprovalResolve(input));
1543
+ if (this.session.isStreaming) {
1544
+ await this.session.sendPlanModeContext({ deliverAs: "steer" });
1545
+ }
1546
+ this.#planModeHasEntered = true;
1547
+ await this.#applyPlanModeModel();
1548
+ this.#updatePlanModeStatus();
1549
+ this.sessionManager.appendModeChange("plan", { planFilePath });
1550
+ this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
1551
+ }
1552
+
1553
+ /** Standing resolve dispatcher registered while plan mode is active. The agent
1554
+ * submits the finalized plan by calling `resolve { action: "apply", extra: { title } }`;
1555
+ * this handler validates the plan file exists, normalizes the title, and shapes the
1556
+ * payload that `event-controller` forwards to `handlePlanApproval`. */
1557
+ #runPlanApprovalResolve(input: unknown): Promise<AgentToolResult<ResolveToolDetails>> {
1558
+ return runResolveInvocation(input as Parameters<typeof runResolveInvocation>[0], {
1559
+ sourceToolName: "plan_approval",
1560
+ label: "Plan ready for approval",
1561
+ apply: async (_reason, extra) => {
1562
+ const state = this.session.getPlanModeState?.();
1563
+ if (!state?.enabled) {
1564
+ throw new ToolError("Plan mode is not active.");
1565
+ }
1566
+ const { planFilePath, title } = await resolveApprovedPlan({
1567
+ suppliedTitle: extra?.title,
1568
+ statePlanFilePath: state.planFilePath,
1569
+ readPlan: url => this.#readPlanFile(url),
1570
+ listPlanFiles: () => this.#listLocalPlanFiles(),
1571
+ });
1572
+ const details: PlanApprovalDetails = {
1573
+ planFilePath,
1574
+ title,
1575
+ planExists: true,
1576
+ };
1577
+ return {
1578
+ content: [{ type: "text" as const, text: "Plan ready for approval." }],
1579
+ details,
1580
+ };
1581
+ },
1582
+ });
1583
+ }
1584
+
1585
+ async #exitPlanMode(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
1586
+ if (!this.planModeEnabled) {
1587
+ return;
1588
+ }
1589
+
1590
+ const previousTools = this.#planModePreviousTools;
1591
+ if (previousTools && previousTools.length > 0) {
1592
+ await this.session.setActiveToolsByName(previousTools);
1593
+ }
1594
+ if (this.#planModePreviousModelState) {
1595
+ const prev = this.#planModePreviousModelState;
1596
+ if (modelsAreEqual(this.session.model, prev.model)) {
1597
+ // Same model — only thinking level may differ. Avoid setModelTemporary()
1598
+ // which would reset provider-side sessions (openai-responses/Codex) and
1599
+ // break conversation continuity.
1600
+ this.session.setThinkingLevel(prev.thinkingLevel);
1601
+ } else if (this.session.isStreaming) {
1602
+ this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
1603
+ } else {
1604
+ await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
1605
+ }
1606
+ // If #applyPlanModeModel queued a deferred switch to the plan-role model
1607
+ // (because the session was streaming on entry), drop it now: we are
1608
+ // leaving plan mode, so flushing it on the next agent_end would land the
1609
+ // session on the plan-role model after the user has exited plan mode
1610
+ // (issue #816). Only clear when the pending target matches the plan-role
1611
+ // model — leave any unrelated user-queued switch intact.
1612
+ const pending = this.#pendingModelSwitch;
1613
+ if (pending) {
1614
+ const planResolution = this.session.resolveRoleModelWithThinking("plan");
1615
+ if (planResolution.model && modelsAreEqual(pending.model, planResolution.model)) {
1616
+ this.#pendingModelSwitch = undefined;
1617
+ }
1618
+ }
1619
+ }
1620
+ this.session.setStandingResolveHandler?.(null);
1621
+ this.session.setPlanModeState(undefined);
1622
+ this.planModeEnabled = false;
1623
+ this.planModePaused = options?.paused ?? false;
1624
+ this.planModePlanFilePath = undefined;
1625
+ this.#planModePreviousTools = undefined;
1626
+ this.#planModePreviousModelState = undefined;
1627
+ this.#updatePlanModeStatus();
1628
+ const paused = options?.paused ?? false;
1629
+ this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
1630
+ if (!options?.silent) {
1631
+ this.showStatus(paused ? "Plan mode paused." : "Plan mode disabled.");
1632
+ }
1633
+ }
1634
+
1635
+ async #enterGoalMode(options: { objective?: string; resume?: boolean; silent?: boolean }): Promise<void> {
1636
+ if (this.goalModeEnabled) {
1637
+ return;
1638
+ }
1639
+ if (this.planModeEnabled || this.planModePaused) {
1640
+ this.showWarning("Exit plan mode first.");
1641
+ return;
1642
+ }
1643
+ const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1644
+ const goalTools = [...new Set([...previousTools, "goal"])];
1645
+ this.#goalModePreviousTools = previousTools;
1646
+ this.goalModePaused = false;
1647
+ const state = options.resume
1648
+ ? await this.session.goalRuntime.resumeGoal()
1649
+ : await this.session.goalRuntime.createGoal({ objective: options.objective ?? "" });
1650
+ await this.session.setActiveToolsByName(goalTools);
1651
+ this.session.setGoalModeState(state);
1652
+ this.goalModeEnabled = true;
1653
+ this.#resetGoalContinuationSuppression();
1654
+ this.#updateGoalModeStatus();
1655
+ if (this.session.isStreaming) {
1656
+ await this.session.sendGoalModeContext({ deliverAs: "steer" });
1657
+ }
1658
+ if (!options.silent) {
1659
+ this.showStatus(options.resume ? "Goal mode resumed." : "Goal mode enabled.");
1660
+ }
1661
+ }
1662
+
1663
+ async #exitGoalMode(options?: {
1664
+ silent?: boolean;
1665
+ paused?: boolean;
1666
+ reason?: "completed" | "paused" | "dropped";
1667
+ }): Promise<void> {
1668
+ const previousTools = this.#goalModePreviousTools;
1669
+ if (this.goalModeEnabled && previousTools) {
1670
+ await this.session.setActiveToolsByName(previousTools);
1671
+ }
1672
+ const currentState = this.session.getGoalModeState();
1673
+ if (options?.reason === "completed") {
1674
+ this.session.setGoalModeState(undefined);
1675
+ this.sessionManager.appendModeChange("none");
1676
+ this.sessionManager.appendCustomEntry("goal-completed", {
1677
+ objective: currentState?.goal?.objective,
1678
+ tokensUsed: currentState?.goal?.tokensUsed,
1679
+ tokenBudget: currentState?.goal?.tokenBudget,
1680
+ timeUsedSeconds: currentState?.goal?.timeUsedSeconds,
1681
+ });
1682
+ }
1683
+ this.goalModeEnabled = false;
1684
+ this.goalModePaused = options?.paused ?? false;
1685
+ this.#goalModePreviousTools = undefined;
1686
+ this.#goalContinuationTurnInFlight = false;
1687
+ this.#cancelGoalContinuation();
1688
+ this.#updateGoalModeStatus();
1689
+ if (!options?.silent) {
1690
+ if (options?.reason === "completed") {
1691
+ this.showStatus("Goal mode completed.");
1692
+ } else if (options?.reason === "dropped") {
1693
+ this.showStatus("Goal dropped.");
1694
+ } else if (options?.paused) {
1695
+ this.showStatus("Goal mode paused.");
1696
+ } else {
1697
+ this.showStatus("Goal mode disabled.");
1698
+ }
1699
+ }
1700
+ }
1701
+
1702
+ async #readPlanFile(planFilePath: string): Promise<string | null> {
1703
+ const resolvedPath = this.#resolvePlanFilePath(planFilePath);
1704
+ try {
1705
+ return await Bun.file(resolvedPath).text();
1706
+ } catch (error) {
1707
+ if (isEnoent(error)) {
1708
+ return null;
1709
+ }
1710
+ throw error;
1711
+ }
1712
+ }
1713
+
1714
+ async #hasPlanModeDraftContent(planFilePath: string): Promise<boolean> {
1715
+ const candidates = new Set<string>([planFilePath, ...(await this.#listLocalPlanFiles())]);
1716
+ for (const candidate of candidates) {
1717
+ const content = await this.#readPlanFile(candidate);
1718
+ if (content !== null && content.trim().length > 0) return true;
1719
+ }
1720
+ return false;
1721
+ }
1722
+
1723
+ /** `local://` URLs of plan files in the session-local root, newest first.
1724
+ * A fallback for `resolveApprovedPlan` when the agent dropped `extra.title`,
1725
+ * so the plan it wrote is still found by scanning recent `*-plan.md` files. */
1726
+ async #listLocalPlanFiles(): Promise<string[]> {
1727
+ const localRoot = this.#resolvePlanFilePath("local://");
1728
+ try {
1729
+ const entries = await fs.readdir(localRoot, { withFileTypes: true });
1730
+ const plans = await Promise.all(
1731
+ entries
1732
+ .filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
1733
+ .map(async name => {
1734
+ const stat = await fs.stat(path.join(localRoot, name.name)).catch(() => null);
1735
+ return { url: `local://${name.name}`, mtime: stat?.mtimeMs ?? 0 };
1736
+ }),
1737
+ );
1738
+ return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
1739
+ } catch {
1740
+ return [];
1741
+ }
1742
+ }
1743
+
1744
+ showPlanReview(
1745
+ planContent: string,
1746
+ title: string,
1747
+ options: string[],
1748
+ dialogOptions?: {
1749
+ helpText?: string;
1750
+ disabledIndices?: number[];
1751
+ onExternalEditor?: () => void;
1752
+ onPlanEdited?: (content: string) => void;
1753
+ onFeedbackChange?: (feedback: string) => void;
1754
+ initialIndex?: number;
1755
+ },
1756
+ extra?: { slider?: HookSelectorSlider },
1757
+ ): Promise<string | undefined> {
1758
+ this.#hidePlanReview();
1759
+ const { promise, resolve } = Promise.withResolvers<string | undefined>();
1760
+ let settled = false;
1761
+ const finish = (choice: string | undefined): void => {
1762
+ if (settled) return;
1763
+ settled = true;
1764
+ this.#hidePlanReview();
1765
+ this.ui.requestRender();
1766
+ resolve(choice);
1767
+ };
1768
+ const overlay = new PlanReviewOverlay(
1769
+ planContent,
1770
+ {
1771
+ promptTitle: title,
1772
+ options,
1773
+ disabledIndices: dialogOptions?.disabledIndices,
1774
+ helpText: dialogOptions?.helpText,
1775
+ initialIndex: dialogOptions?.initialIndex,
1776
+ slider: extra?.slider,
1777
+ externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
1778
+ },
1779
+ {
1780
+ onPick: choice => finish(choice),
1781
+ onCancel: () => finish(undefined),
1782
+ onExternalEditor: dialogOptions?.onExternalEditor,
1783
+ onPlanEdited: dialogOptions?.onPlanEdited,
1784
+ onFeedbackChange: dialogOptions?.onFeedbackChange,
1785
+ },
1786
+ );
1787
+ this.#planReviewOverlay = overlay;
1788
+ this.#planReviewOverlayHandle = this.ui.showOverlay(overlay, {
1789
+ anchor: "bottom-center",
1790
+ width: "100%",
1791
+ maxHeight: "100%",
1792
+ margin: 0,
1793
+ fullscreen: true,
1794
+ });
1795
+ this.ui.setFocus(overlay);
1796
+ this.ui.requestRender();
1797
+ return promise;
1798
+ }
1799
+
1800
+ #hidePlanReview(): void {
1801
+ this.#planReviewOverlayHandle?.hide();
1802
+ this.#planReviewOverlayHandle = undefined;
1803
+ this.#planReviewOverlay = undefined;
1804
+ }
1805
+
1806
+ #getEditorTerminalPath(): string | null {
1807
+ if (process.platform === "win32") {
1808
+ return null;
1809
+ }
1810
+ return "/dev/tty";
1811
+ }
1812
+
1813
+ async #openEditorTerminalHandle(): Promise<fs.FileHandle | null> {
1814
+ const terminalPath = this.#getEditorTerminalPath();
1815
+ if (!terminalPath) {
1816
+ return null;
1817
+ }
1818
+ try {
1819
+ return await fs.open(terminalPath, "r+");
1820
+ } catch {
1821
+ return null;
1822
+ }
1823
+ }
1824
+
1825
+ #getPlanApprovalContextUsage(): ContextUsage | undefined {
1826
+ const executionModel = this.#planModePreviousModelState?.model ?? this.session.model;
1827
+ const contextWindow = executionModel?.contextWindow;
1828
+ if (typeof contextWindow === "number") {
1829
+ return this.session.getContextUsage({ contextWindow });
1830
+ }
1831
+ return this.session.getContextUsage();
1832
+ }
1833
+
1834
+ #formatKeepContextLabel(contextUsage: ContextUsage | undefined): string {
1835
+ if (contextUsage?.tokens == null) {
1836
+ return "Approve and keep context";
1837
+ }
1838
+ const tokens = formatContextTokenCount(contextUsage.tokens);
1839
+ const contextWindow = formatContextTokenCount(contextUsage.contextWindow);
1840
+ return `Approve and keep context (~${tokens} / ${contextWindow})`;
1841
+ }
1842
+
1843
+ #isKeepContextDisabled(contextUsage: ContextUsage | undefined): boolean {
1844
+ return contextUsage?.percent != null && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
1845
+ }
1846
+
1847
+ async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
1848
+ const editorCmd = getEditorCommand();
1849
+ if (!editorCmd) {
1850
+ this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
1851
+ return;
1852
+ }
1853
+
1854
+ const resolvedPath = this.#resolvePlanFilePath(planFilePath);
1855
+ let currentText: string;
1856
+ try {
1857
+ currentText = await Bun.file(resolvedPath).text();
1858
+ } catch (error) {
1859
+ if (isEnoent(error)) {
1860
+ this.showError(`Plan file not found at ${planFilePath}`);
1861
+ return;
1862
+ }
1863
+ this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
1864
+ return;
1865
+ }
1866
+
1867
+ let ttyHandle: fs.FileHandle | null = null;
1868
+ try {
1869
+ ttyHandle = await this.#openEditorTerminalHandle();
1870
+ this.ui.stop();
1871
+
1872
+ const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
1873
+ ? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
1874
+ : ["inherit", "inherit", "inherit"];
1875
+
1876
+ const result = await openInEditor(editorCmd, currentText, {
1877
+ extension: path.extname(resolvedPath) || ".md",
1878
+ stdio,
1879
+ trimTrailingNewline: false,
1880
+ });
1881
+ if (result !== null) {
1882
+ await Bun.write(resolvedPath, result);
1883
+ this.#planReviewOverlay?.setPlanContent(result);
1884
+ this.showStatus("Plan updated in external editor.");
1885
+ }
1886
+ } catch (error) {
1887
+ this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
1888
+ } finally {
1889
+ if (ttyHandle) {
1890
+ await ttyHandle.close();
1891
+ }
1892
+ this.ui.start();
1893
+ this.ui.requestRender(true);
1894
+ }
1895
+ }
1896
+
1897
+ async #applyPlanExecutionModel(entry: ResolvedRoleModel | undefined): Promise<void> {
1898
+ if (!entry) return;
1899
+ try {
1900
+ await this.session.applyRoleModel(entry);
1901
+ this.statusLine.invalidate();
1902
+ this.updateEditorBorderColor();
1903
+ this.showStatus(`Continuing with ${entry.role}: ${entry.model.name || entry.model.id}`);
1904
+ } catch (error) {
1905
+ this.showWarning(
1906
+ `Could not switch to the ${entry.role} model: ${error instanceof Error ? error.message : String(error)}`,
1907
+ );
1908
+ }
1909
+ }
1910
+
1911
+ async #approvePlan(
1912
+ planContent: string,
1913
+ options: {
1914
+ planFilePath: string;
1915
+ title: string;
1916
+ preserveContext?: boolean;
1917
+ compactBeforeExecute?: boolean;
1918
+ executionModel?: ResolvedRoleModel;
1919
+ },
1920
+ ): Promise<void> {
1921
+ const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
1922
+
1923
+ // Mark the pending abort caused by the plan-mode → compaction transition as
1924
+ // silent BEFORE #exitPlanMode raises it. The `finally` below clears the
1925
+ // flag on every terminal compaction outcome (ok / cancelled / failed /
1926
+ // throw) so a leaked flag cannot silence a later unrelated abort.
1927
+ // Branchless mark+clear when !compactBeforeExecute: mark is gated; clear
1928
+ // is unconditional and idempotent.
1929
+ if (options.compactBeforeExecute) {
1930
+ this.session.markPlanCompactAbortPending();
1931
+ }
1932
+ let compactOutcome: CompactionOutcome | undefined;
1933
+ try {
1934
+ await this.#exitPlanMode({ silent: true, paused: false });
1935
+
1936
+ if (!options.preserveContext) {
1937
+ await this.handleClearCommand();
1938
+ // The new session has a fresh local:// root — persist the approved plan there
1939
+ // so `local://<slug>-plan.md` resolves correctly in the execution session.
1940
+ const newLocalPath = resolveLocalUrlToPath(options.planFilePath, {
1941
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
1942
+ getSessionId: () => this.sessionManager.getSessionId(),
1943
+ });
1944
+ await Bun.write(newLocalPath, planContent);
1945
+ } else if (options.compactBeforeExecute) {
1946
+ // Distill the plan-mode transcript before the execution turn is queued so
1947
+ // the plan-approved synthetic prompt lands as a fresh cache anchor.
1948
+ // Outcome is consumed after tool-restoration and plan-reference-path
1949
+ // bookkeeping below; `markPlanReferenceSent` is intentionally deferred
1950
+ // past the cancel guard — see the comment at the cancel branch.
1951
+ // Cancellation skips the synthetic-prompt dispatch (operator's explicit
1952
+ // abort is honored); failure proceeds best-effort — approval intent stands.
1953
+ const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
1954
+ planFilePath: options.planFilePath,
1955
+ });
1956
+ // Pin the plan reference path BEFORE compaction so any user messages
1957
+ // queued during the compaction await (which `handleCompactCommand`
1958
+ // flushes via `flushCompactionQueue` before returning) see the
1959
+ // approved plan in `#buildPlanReferenceMessage`. Reassignment after
1960
+ // the try/finally is idempotent and kept for the !compactBeforeExecute
1961
+ // branch.
1962
+ this.session.setPlanReferencePath(options.planFilePath);
1963
+ compactOutcome = await this.handleCompactCommand(compactionPrompt);
1964
+ }
1965
+ } finally {
1966
+ // Unconditional clear. Idempotent: a no-op when the flag was never set
1967
+ // (i.e., the !compactBeforeExecute branch), and a no-op when the flag
1968
+ // was already consumed by AgentSession.#handleAgentEvent's aborted
1969
+ // message_end stamping. Guarantees the flag is dead at every exit.
1970
+ this.session.clearPlanCompactAbortPending();
1971
+ }
1972
+
1973
+ // Tool restoration runs on every path — the plan mode tools must be
1974
+ // retired regardless of whether the synthetic prompt fires.
1975
+ if (previousTools.length > 0) {
1976
+ await this.session.setActiveToolsByName(previousTools);
1977
+ }
1978
+ this.session.setPlanReferencePath(options.planFilePath);
1979
+
1980
+ if (compactOutcome === "cancelled") {
1981
+ // Explicit abort: honor it. `executeCompaction` already surfaced
1982
+ // `showError("Compaction cancelled")` to the operator; we add the
1983
+ // deferred-dispatch warning and exit. `markPlanReferenceSent` is
1984
+ // intentionally skipped here: `#planReferenceSent` stays false, so
1985
+ // `AgentSession.#buildPlanReferenceMessage` will inject the plan
1986
+ // reference on the operator's next `prompt()` call. If we marked it
1987
+ // sent here, the executor's first turn would have no plan context.
1988
+ this.showWarning(
1989
+ "Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
1990
+ );
1991
+ return;
1992
+ }
1993
+
1994
+ await this.#applyPlanExecutionModel(options.executionModel);
1995
+
1996
+ // Approved plans land in a fresh (or compacted) session whose first user-visible
1997
+ // turn is the synthetic plan-approved prompt — that path bypasses the
1998
+ // input-controller's title generation. Seed an auto-name from the plan title
1999
+ // so the session is not left unnamed. `setSessionName("auto")` is a no-op
2000
+ // when the user has already chosen a name (preserveContext paths).
2001
+ const seededName = humanizePlanTitle(options.title);
2002
+ if (seededName && !this.sessionManager.getSessionName()) {
2003
+ const applied = await this.sessionManager.setSessionName(seededName, "auto");
2004
+ if (applied) {
2005
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
2006
+ this.updateEditorBorderColor();
2007
+ }
2008
+ }
2009
+
2010
+ // markPlanReferenceSent fires only on the dispatch path so the synthetic
2011
+ // plan-approved prompt is the source of the reference injection.
2012
+ this.session.markPlanReferenceSent();
2013
+ const planModePrompt = prompt.render(planModeApprovedPrompt, {
2014
+ planContent,
2015
+ planFilePath: options.planFilePath,
2016
+ contextPreserved: options.preserveContext === true,
2017
+ });
2018
+ await this.session.prompt(planModePrompt, { synthetic: true });
2019
+ }
2020
+
2021
+ async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
2022
+ if (this.goalModeEnabled || this.goalModePaused) {
2023
+ this.showWarning("Exit goal mode first.");
2024
+ return;
2025
+ }
2026
+ if (this.planModeEnabled) {
2027
+ const planFilePath = this.planModePlanFilePath ?? (await this.#getPlanFilePath());
2028
+ if (await this.#hasPlanModeDraftContent(planFilePath)) {
2029
+ const confirmed = await this.showHookConfirm(
2030
+ "Exit plan mode?",
2031
+ "This exits plan mode without approving a plan.",
2032
+ );
2033
+ if (!confirmed) return;
2034
+ }
2035
+ await this.#exitPlanMode({ paused: true });
2036
+ return;
2037
+ }
2038
+ if (!this.session.settings.get("plan.enabled")) {
2039
+ this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
2040
+ return;
2041
+ }
2042
+ await this.#enterPlanMode();
2043
+ if (initialPrompt && this.onInputCallback) {
2044
+ this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
2045
+ }
2046
+ }
2047
+
2048
+ async #handleGoalBudgetCommand(rawBudget: string): Promise<void> {
2049
+ const state = this.session.getGoalModeState();
2050
+ if (!this.goalModeEnabled || !state?.enabled) {
2051
+ this.showWarning("No active goal.");
2052
+ return;
2053
+ }
2054
+ if (state.goal.status === "complete") {
2055
+ this.showStatus("Goal is already complete.");
2056
+ return;
2057
+ }
2058
+ const trimmed = rawBudget.trim().toLowerCase();
2059
+ let nextBudget: number | undefined;
2060
+ if (trimmed !== "off") {
2061
+ const parsed = Number.parseInt(trimmed, 10);
2062
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2063
+ this.showError("Goal budget must be a positive integer or `off`.");
2064
+ return;
2065
+ }
2066
+ nextBudget = parsed;
2067
+ }
2068
+ await this.session.goalRuntime.onBudgetMutated(nextBudget);
2069
+ this.#resetGoalContinuationSuppression();
2070
+ this.#scheduleGoalContinuation();
2071
+ this.showStatus(nextBudget === undefined ? "Goal budget cleared." : `Goal budget set to ${nextBudget}.`);
2072
+ }
2073
+
2074
+ async handleGoalModeCommand(rest?: string): Promise<void> {
2075
+ try {
2076
+ if (this.planModeEnabled || this.planModePaused) {
2077
+ this.showWarning("Exit plan mode first.");
2078
+ return;
2079
+ }
2080
+ if (!this.session.settings.get("goal.enabled")) {
2081
+ this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
2082
+ return;
2083
+ }
2084
+ const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
2085
+ if (sub) {
2086
+ await this.#dispatchGoalSubcommand(sub, subRest);
2087
+ return;
2088
+ }
2089
+ if (this.goalModeEnabled) {
2090
+ if (subRest) {
2091
+ this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
2092
+ return;
2093
+ }
2094
+ await this.#openGoalMenu("active");
2095
+ return;
2096
+ }
2097
+ const pausedState = this.#getPausedGoalState();
2098
+ if (pausedState) {
2099
+ if (subRest) {
2100
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
2101
+ return;
2102
+ }
2103
+ await this.#openGoalMenu("paused");
2104
+ return;
2105
+ }
2106
+ if (subRest) {
2107
+ await this.#startGoalFromObjective(subRest);
2108
+ return;
2109
+ }
2110
+ const objective = (
2111
+ await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
2112
+ )?.trim();
2113
+ if (!objective) return;
2114
+ await this.#startGoalFromObjective(objective);
2115
+ } catch (error) {
2116
+ this.showError(error instanceof Error ? error.message : String(error));
2117
+ }
2118
+ }
2119
+
2120
+ async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
2121
+ switch (sub) {
2122
+ case "set":
2123
+ await this.#handleGoalSetSubcommand(rest);
2124
+ return;
2125
+ case "show":
2126
+ this.#showGoalDetails();
2127
+ return;
2128
+ case "pause":
2129
+ await this.#pauseGoalAction();
2130
+ return;
2131
+ case "resume":
2132
+ await this.#resumeGoalAction();
2133
+ return;
2134
+ case "drop":
2135
+ await this.#confirmAndDropGoal();
2136
+ return;
2137
+ case "budget":
2138
+ if (!this.goalModeEnabled) {
2139
+ this.showWarning(
2140
+ this.#getPausedGoalState() ? "Resume the goal before adjusting the budget." : "No active goal.",
2141
+ );
2142
+ return;
2143
+ }
2144
+ if (!rest) {
2145
+ await this.#promptGoalBudgetEdit();
2146
+ return;
2147
+ }
2148
+ await this.#handleGoalBudgetCommand(rest);
2149
+ return;
2150
+ }
2151
+ }
2152
+
2153
+ async #openGoalMenu(state: "active" | "paused"): Promise<void> {
2154
+ const goal = this.session.getGoalModeState()?.goal;
2155
+ if (!goal) return;
2156
+ const summary = goal.objective.length > 48 ? `${goal.objective.slice(0, 47)}…` : goal.objective;
2157
+ const title = state === "active" ? `Goal: ${summary} (${goal.status})` : `Goal paused: ${summary}`;
2158
+ const items =
2159
+ state === "active"
2160
+ ? ["Show details", "Adjust budget…", "Pause", "Drop"]
2161
+ : ["Resume", "Show details", "Adjust budget…", "Drop"];
2162
+ const choice = await this.showHookSelector(title, items);
2163
+ if (!choice) return;
2164
+ switch (choice) {
2165
+ case "Show details":
2166
+ this.#showGoalDetails();
2167
+ return;
2168
+ case "Adjust budget…":
2169
+ await this.#promptGoalBudgetEdit();
2170
+ return;
2171
+ case "Pause":
2172
+ await this.#pauseGoalAction();
2173
+ return;
2174
+ case "Resume":
2175
+ await this.#resumeGoalAction();
2176
+ return;
2177
+ case "Drop":
2178
+ await this.#confirmAndDropGoal();
2179
+ return;
2180
+ }
2181
+ }
2182
+
2183
+ #showGoalDetails(): void {
2184
+ const state = this.session.getGoalModeState();
2185
+ const goal = state?.goal;
2186
+ if (!goal) {
2187
+ this.showStatus("No goal set.");
2188
+ return;
2189
+ }
2190
+ const used = goal.tokensUsed.toLocaleString();
2191
+ const budgetLine =
2192
+ goal.tokenBudget !== undefined
2193
+ ? `${used} / ${goal.tokenBudget.toLocaleString()} (${Math.max(0, goal.tokenBudget - goal.tokensUsed).toLocaleString()} left)`
2194
+ : `${used} (no budget)`;
2195
+ const lines = [
2196
+ `Objective: ${goal.objective}`,
2197
+ `Status: ${goal.status}${state?.enabled ? "" : " (paused)"}`,
2198
+ `Tokens: ${budgetLine}`,
2199
+ `Time spent: ${formatDuration(goal.timeUsedSeconds * 1000)}`,
2200
+ ];
2201
+ this.showStatus(lines.join("\n"));
2202
+ }
2203
+
2204
+ async #promptGoalBudgetEdit(): Promise<void> {
2205
+ const goal = this.session.getGoalModeState()?.goal;
2206
+ const prefill = goal?.tokenBudget !== undefined ? String(goal.tokenBudget) : "";
2207
+ const input = (
2208
+ await this.showHookEditor("Goal budget (number, `off`, or empty to cancel)", prefill, undefined, {
2209
+ promptStyle: true,
2210
+ })
2211
+ )?.trim();
2212
+ if (!input) return;
2213
+ await this.#handleGoalBudgetCommand(input);
2214
+ }
2215
+
2216
+ async #pauseGoalAction(): Promise<void> {
2217
+ if (!this.goalModeEnabled) {
2218
+ this.showWarning("No active goal to pause.");
2219
+ return;
2220
+ }
2221
+ await this.session.goalRuntime.pauseGoal();
2222
+ await this.#exitGoalMode({ paused: true, reason: "paused" });
2223
+ }
2224
+
2225
+ async #resumeGoalAction(): Promise<void> {
2226
+ if (!this.#getPausedGoalState()) {
2227
+ this.showWarning("No paused goal to resume.");
2228
+ return;
2229
+ }
2230
+ await this.#enterGoalMode({ resume: true, silent: true });
2231
+ this.showStatus("Goal mode resumed.");
2232
+ this.#scheduleGoalContinuation();
2233
+ }
2234
+
2235
+ async #confirmAndDropGoal(): Promise<void> {
2236
+ if (!this.goalModeEnabled && !this.#getPausedGoalState()) {
2237
+ this.showWarning("No goal to drop.");
2238
+ return;
2239
+ }
2240
+ const confirmed = await this.showHookConfirm(
2241
+ "Drop goal?",
2242
+ "This removes the goal record. Accumulated usage stays in the session log.",
2243
+ );
2244
+ if (!confirmed) return;
2245
+ await this.session.goalRuntime.dropGoal();
2246
+ await this.#exitGoalMode({ reason: "dropped" });
2247
+ }
2248
+
2249
+ async #startGoalFromObjective(objective: string): Promise<void> {
2250
+ await this.#enterGoalMode({ objective, silent: true });
2251
+ this.#resetGoalContinuationSuppression();
2252
+ if (this.onInputCallback) {
2253
+ this.onInputCallback(this.startPendingSubmission({ text: objective }));
2254
+ }
2255
+ }
2256
+
2257
+ async #replaceGoalFromObjective(objective: string): Promise<void> {
2258
+ const state = await this.session.goalRuntime.replaceGoal({ objective });
2259
+ this.session.setGoalModeState(state);
2260
+ this.goalModeEnabled = true;
2261
+ this.goalModePaused = false;
2262
+ this.#resetGoalContinuationSuppression();
2263
+ this.#updateGoalModeStatus();
2264
+ if (this.session.isStreaming) {
2265
+ await this.session.sendGoalModeContext({ deliverAs: "steer" });
2266
+ }
2267
+ if (this.onInputCallback) {
2268
+ this.onInputCallback(this.startPendingSubmission({ text: objective }));
2269
+ }
2270
+ }
2271
+
2272
+ async #handleGoalSetSubcommand(rest: string): Promise<void> {
2273
+ if (!this.goalModeEnabled && this.#getPausedGoalState()) {
2274
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
2275
+ return;
2276
+ }
2277
+ const objective = rest.trim()
2278
+ ? rest.trim()
2279
+ : (await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true }))?.trim();
2280
+ if (!objective) return;
2281
+ if (this.goalModeEnabled) {
2282
+ await this.#replaceGoalFromObjective(objective);
2283
+ return;
2284
+ }
2285
+ await this.#startGoalFromObjective(objective);
2286
+ }
2287
+
2288
+ /** Manually (re-)open the plan-review overlay — bound to `/plan-review`. Lets
2289
+ * the operator pull the review back up after dismissing it, or review a plan
2290
+ * the agent wrote without calling `resolve`. There is no fixed plan filename:
2291
+ * `getPlanReferencePath()` is empty until a plan is actually approved (and does
2292
+ * not survive a restart), so this drives off the newest `local://<slug>-plan.md`
2293
+ * the agent wrote — the files persist in the session artifacts dir, so the scan
2294
+ * works before any review and across restarts. */
2295
+ async openPlanReview(): Promise<void> {
2296
+ if (!this.planModeEnabled) {
2297
+ this.showWarning("Plan mode is not active.");
2298
+ return;
2299
+ }
2300
+ const noPlan = "No plan to review yet — write one to a local://<slug>-plan.md file first.";
2301
+ const [planFilePath] = await this.#listLocalPlanFiles();
2302
+ if (!planFilePath) {
2303
+ this.showWarning(noPlan);
2304
+ return;
2305
+ }
2306
+ const planContent = await this.#readPlanFile(planFilePath);
2307
+ if (planContent === null) {
2308
+ this.showWarning(noPlan);
2309
+ return;
2310
+ }
2311
+ const { title } = resolvePlanTitle({ planContent, planFilePath });
2312
+ await this.handlePlanApproval({ planFilePath, title, planExists: true });
2313
+ }
2314
+
2315
+ async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
2316
+ if (!this.planModeEnabled) {
2317
+ this.showWarning("Plan mode is not active.");
2318
+ return;
2319
+ }
2320
+
2321
+ // Abort the agent to prevent it from continuing (e.g., re-submitting the
2322
+ // plan) while the popup is showing. The event listener fires asynchronously
2323
+ // (agent's #emit is fire-and-forget), so without this the model sees
2324
+ // "Plan ready for approval." and immediately re-invokes `resolve` in a loop.
2325
+ await this.session.abort();
2326
+
2327
+ const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
2328
+ this.planModePlanFilePath = planFilePath;
2329
+ const planContent = await this.#readPlanFile(planFilePath);
2330
+ if (!planContent) {
2331
+ this.showError(`Plan file not found at ${planFilePath}`);
2332
+ return;
2333
+ }
2334
+
2335
+ const contextUsage = this.#getPlanApprovalContextUsage();
2336
+ const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
2337
+ const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
2338
+
2339
+ // Model-tier slider: let the operator pick which configured role model
2340
+ // (smol/default/slow/…) executes the approved plan. The slider always starts
2341
+ // on the `default` tier so execution defaults to the default model no matter
2342
+ // which model drove the planning conversation. Left/right move it from there;
2343
+ // hidden when fewer than two role models resolve — a lone tier is no choice.
2344
+ // `selectedTierIndex` tracks the live slider position.
2345
+ const cycle = this.session.getRoleModelCycle(this.session.settings.get("cycleOrder"));
2346
+ const defaultTierIndex = cycle ? cycle.models.findIndex(entry => entry.role === "default") : -1;
2347
+ const startTierIndex = defaultTierIndex >= 0 ? defaultTierIndex : (cycle?.currentIndex ?? 0);
2348
+ let selectedTierIndex = startTierIndex;
2349
+ const slider: HookSelectorSlider | undefined =
2350
+ cycle && cycle.models.length > 1
2351
+ ? {
2352
+ caption: "continue with",
2353
+ index: startTierIndex,
2354
+ segments: cycle.models.map(entry => ({
2355
+ label: entry.role,
2356
+ color: MODEL_ROLES[entry.role as ModelRole]?.color,
2357
+ detail: entry.model.name || entry.model.id,
2358
+ })),
2359
+ onChange: index => {
2360
+ selectedTierIndex = index;
2361
+ },
2362
+ }
2363
+ : undefined;
2364
+ // The overlay now owns the dynamic, focus-aware help line; the caller only
2365
+ // supplies the trailing cancel hint.
2366
+ const helpText = "esc cancel";
2367
+ // In-overlay edits (section deletes/undo) and section annotations. Deletes
2368
+ // update `editedContent` (and mirror to disk); annotations build `feedback`
2369
+ // that the Refine branch re-prompts the model with.
2370
+ let editedContent: string | undefined;
2371
+ let feedback = "";
2372
+
2373
+ const choice = await this.showPlanReview(
2374
+ planContent,
2375
+ "Plan mode - next step",
2376
+ ["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
2377
+ {
2378
+ helpText,
2379
+ onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
2380
+ onPlanEdited: content => {
2381
+ editedContent = content;
2382
+ void Bun.write(this.#resolvePlanFilePath(planFilePath), content);
2383
+ },
2384
+ onFeedbackChange: value => {
2385
+ feedback = value;
2386
+ },
2387
+ disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
2388
+ },
2389
+ { slider },
2390
+ );
2391
+
2392
+ if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
2393
+ try {
2394
+ // Prefer in-overlay edits (already in memory) over a disk re-read; the
2395
+ // `onPlanEdited` write is fire-and-forget, so reading the file here could
2396
+ // race ahead of it.
2397
+ const latestPlanContent = editedContent ?? (await this.#readPlanFile(planFilePath));
2398
+ if (!latestPlanContent) {
2399
+ this.showError(`Plan file not found at ${planFilePath}`);
2400
+ return;
2401
+ }
2402
+ // Capture the operator's tier choice and hand it to #approvePlan, which
2403
+ // applies it AFTER #exitPlanMode. #exitPlanMode restores
2404
+ // #planModePreviousModelState (the model from before plan mode), so
2405
+ // applying the slider choice any earlier would be silently reverted —
2406
+ // the bug that made "continue with slow" keep executing on the default
2407
+ // model. Deferred application also survives newSession()/compaction.
2408
+ // `cycle.currentIndex` is exactly that restored model, so any chosen tier
2409
+ // differing from it needs an explicit executionModel — this also covers
2410
+ // leaving the slider on its `default` anchor while planning ran elsewhere.
2411
+ const executionModel =
2412
+ cycle && selectedTierIndex !== cycle.currentIndex ? cycle.models[selectedTierIndex] : undefined;
2413
+ await this.#approvePlan(latestPlanContent, {
2414
+ planFilePath,
2415
+ title: details.title,
2416
+ preserveContext: choice !== "Approve and execute",
2417
+ compactBeforeExecute: choice === "Approve and compact context",
2418
+ executionModel,
2419
+ });
2420
+ } catch (error) {
2421
+ this.showError(
2422
+ `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
2423
+ );
2424
+ }
2425
+ return;
2426
+ }
2427
+
2428
+ if (choice === "Refine plan") {
2429
+ // Section annotations entered in the overlay become a refinement prompt
2430
+ // re-submitted to the model. With no annotations, fall back to today's
2431
+ // behavior: close the overlay and let the operator type their own.
2432
+ if (feedback.trim() && this.onInputCallback) {
2433
+ this.onInputCallback(this.startPendingSubmission({ text: feedback }));
2434
+ }
2435
+ return;
2436
+ }
2437
+ }
2438
+
2439
+ /**
2440
+ * Pool of consent-prompt variants. Each entry is `[headline, reassurance]`;
2441
+ * the second line always promises the same scope (tool name + confusion
2442
+ * details, never personal data) so users learn what they're consenting to
2443
+ * even as the top line rotates.
2444
+ *
2445
+ * Kept in-module rather than i18n'd because the whole charm is the tone
2446
+ * — translations would need to preserve it deliberately, not auto-render.
2447
+ */
2448
+ static #AUTOQA_CONSENT_PROMPTS: ReadonlyArray<readonly [string, string]> = [
2449
+ [
2450
+ "😤 Your agent is fuming about a tool.",
2451
+ "Wanna let it vent to the devs? Just the tool name + what set it off, nothing personal.",
2452
+ ],
2453
+ [
2454
+ "😵‍💫 Your agent is having an existential crisis over a tool.",
2455
+ "Forward the dread to the devs? Tool + what broke its little mind, no personal info.",
2456
+ ],
2457
+ [
2458
+ "😭 Your agent wants to cry about a misbehaving tool.",
2459
+ "Let it cry to the devs? Tool + the tears, never anything personal.",
2460
+ ],
2461
+ [
2462
+ "🤬 Your agent is BIG MAD at one of the tools.",
2463
+ "Pass the rant along? Just the tool name and what enraged it, nothing personal.",
2464
+ ],
2465
+ [
2466
+ "🫠 Your agent is melting down over a tool.",
2467
+ "Mop up by alerting the devs? Tool + what melted it, no personal info.",
2468
+ ],
2469
+ [
2470
+ "🤯 Your agent's brain broke at a tool's nonsense.",
2471
+ "Ship the pieces to the devs? Tool name + the confusion, never anything personal.",
2472
+ ],
2473
+ [
2474
+ "😩 Your agent is begging to file a complaint about a tool.",
2475
+ "Hand it the form? Tool + what wronged it, nothing personal.",
2476
+ ],
2477
+ [
2478
+ "🥲 Your agent put on a brave face but a tool did it dirty.",
2479
+ "Let it tell the devs the truth? Tool name + the dirt, no personal info.",
2480
+ ],
2481
+ ];
2482
+
2483
+ /**
2484
+ * Show the report_tool_issue consent popup and return the user's decision.
2485
+ * Invoked by the process-global consent handler the tool dispatches to;
2486
+ * subagent invocations bubble up here through the shared module state.
2487
+ */
2488
+ async #promptAutoQaConsent(): Promise<boolean | null> {
2489
+ const pool = InteractiveMode.#AUTOQA_CONSENT_PROMPTS;
2490
+ const [headline, body] = pool[Math.floor(Math.random() * pool.length)];
2491
+ const choice = await this.showHookSelector(`${headline}\n${body}`, ["Yes", "No"]);
2492
+ return choice === "Yes";
2493
+ }
2494
+
2495
+ stop(): void {
2496
+ if (this.loadingAnimation) {
2497
+ this.#stopLoadingAnimation(false);
2498
+ }
2499
+ this.#cleanupMicAnimation();
2500
+ this.#cancelTodoAutoClearTimer();
2501
+ this.#cancelGoalContinuation();
2502
+ if (this.#sttController) {
2503
+ this.#sttController.dispose();
2504
+ this.#sttController = undefined;
2505
+ }
2506
+ this.#extensionUiController.clearExtensionTerminalInputListeners();
2507
+ this.#extensionUiController.clearHookWidgets();
2508
+ for (const unsubscribe of this.#eventBusUnsubscribers) {
2509
+ unsubscribe();
2510
+ }
2511
+ this.#eventBusUnsubscribers = [];
2512
+ this.#observerRegistry.dispose();
2513
+ this.#eventController.dispose();
2514
+ this.statusLine.dispose();
2515
+ if (this.#resizeHandler) {
2516
+ process.stdout.removeListener("resize", this.#resizeHandler);
2517
+ this.#resizeHandler = undefined;
2518
+ }
2519
+ if (this.unsubscribe) {
2520
+ this.unsubscribe();
2521
+ }
2522
+ if (this.#cleanupUnsubscribe) {
2523
+ this.#cleanupUnsubscribe();
2524
+ }
2525
+ // Clear the process-global consent handler so it doesn't outlive this
2526
+ // InteractiveMode instance (e.g. test harnesses, headless re-init).
2527
+ setAutoQaConsentHandler(null, null);
2528
+ if (this.isInitialized) {
2529
+ this.ui.stop();
2530
+ this.isInitialized = false;
2531
+ }
2532
+ }
2533
+
2534
+ async shutdown(): Promise<void> {
2535
+ if (this.#isShuttingDown) return;
2536
+ this.#isShuttingDown = true;
2537
+
2538
+ // Snapshot the editor before any teardown empties it. Persisting the draft
2539
+ // here covers Ctrl+D shutdown with non-empty text; for /exit the editor is
2540
+ // already cleared so saveDraft("") just removes any stale sidecar.
2541
+ const draftText = this.editor.getText();
2542
+
2543
+ // Flush pending session writes before shutdown
2544
+ await this.sessionManager.flush();
2545
+ try {
2546
+ await this.sessionManager.saveDraft(draftText);
2547
+ } catch (err) {
2548
+ logger.warn("Failed to save session draft", { error: String(err) });
2549
+ }
2550
+ this.#btwController.dispose();
2551
+ this.#omfgController.dispose();
2552
+
2553
+ // Emit shutdown event to hooks
2554
+ await this.session.dispose();
2555
+
2556
+ // Do not force a final render during teardown: disposed session/UI state can
2557
+ // collapse to an empty frame, clearing the viewport and leaving the parent
2558
+ // shell prompt at row 0. Stop from the last committed frame so the terminal
2559
+ // hands Bash the cursor immediately after visible OMP content.
2560
+ // Drain any in-flight Kitty key release events before stopping.
2561
+ // This prevents escape sequences from leaking to the parent shell over slow SSH.
2562
+ await this.ui.terminal.drainInput(1000);
2563
+ popTerminalTitle();
2564
+ this.stop();
2565
+
2566
+ // Print resumption hint if this is a persisted session
2567
+ const sessionId = this.sessionManager.getSessionId();
2568
+ const sessionFile = this.sessionManager.getSessionFile();
2569
+ if (sessionId && sessionFile) {
2570
+ process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
2571
+ }
2572
+
2573
+ await postmortem.quit(0);
2574
+ }
2575
+
2576
+ async checkShutdownRequested(): Promise<void> {
2577
+ if (!this.shutdownRequested) return;
2578
+ await this.shutdown();
2579
+ }
2580
+
2581
+ // Extension UI integration
2582
+ setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void {
2583
+ this.#toolUiContextSetter(uiContext, hasUI);
2584
+ }
2585
+
2586
+ initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void {
2587
+ this.#extensionUiController.initializeHookRunner(uiContext, hasUI);
2588
+ }
2589
+
2590
+ setEditorComponent(
2591
+ factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
2592
+ ): void {
2593
+ const previousEditor = this.editor;
2594
+ const previousText = previousEditor.getText();
2595
+ const nextEditor = factory
2596
+ ? factory(this.ui, getEditorTheme(), this.keybindings)
2597
+ : new CustomEditor(getEditorTheme());
2598
+
2599
+ nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
2600
+ nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
2601
+ nextEditor.onAutocompleteCancel = () => {
2602
+ this.ui.requestRender(true);
2603
+ };
2604
+ nextEditor.onAutocompleteUpdate = () => {
2605
+ this.ui.requestRender();
2606
+ };
2607
+ nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
2608
+ if (this.historyStorage) {
2609
+ nextEditor.setHistoryStorage(this.historyStorage);
2610
+ }
2611
+ nextEditor.setText(previousText);
2612
+
2613
+ this.editorContainer.clear();
2614
+ this.editor = nextEditor;
2615
+ this.editorContainer.addChild(nextEditor);
2616
+ this.ui.setFocus(nextEditor);
2617
+
2618
+ this.#inputController.setupKeyHandlers();
2619
+ this.#inputController.setupEditorSubmitHandler();
2620
+
2621
+ void this.refreshSlashCommandState().catch(error => {
2622
+ logger.warn("Failed to refresh slash command state for custom editor", { error: String(error) });
2623
+ });
2624
+
2625
+ this.updateEditorBorderColor();
2626
+ this.updateEditorTopBorder();
2627
+ this.ui.requestRender();
2628
+ }
2629
+
2630
+ // UI helpers
2631
+ present(content: Component | readonly Component[]): void {
2632
+ if (Array.isArray(content)) {
2633
+ for (const item of content) this.#mountChatChild(item);
2634
+ } else {
2635
+ this.#mountChatChild(content as Component);
2636
+ }
2637
+ this.ui.requestRender();
2638
+ }
2639
+
2640
+ #mountChatChild(item: Component): void {
2641
+ this.chatContainer.addChild(item);
2642
+ if (item instanceof ChatBlock) item.mount(this.#chatHost);
2643
+ }
2644
+
2645
+ resetTranscript(): void {
2646
+ this.chatContainer.dispose();
2647
+ this.chatContainer.clear();
2648
+ }
2649
+
2650
+ showStatus(message: string, options?: { dim?: boolean }): void {
2651
+ this.#uiHelpers.showStatus(message, options);
2652
+ }
2653
+
2654
+ showError(message: string): void {
2655
+ this.#pendingSubmittedInput = undefined;
2656
+ this.optimisticUserMessageSignature = undefined;
2657
+ this.#pendingSubmissionDispose?.();
2658
+ this.#pendingSubmissionDispose = undefined;
2659
+ this.#pendingWorkingMessage = undefined;
2660
+ if (this.loadingAnimation) {
2661
+ this.#stopLoadingAnimation(true);
2662
+ }
2663
+ this.#uiHelpers.showError(message);
2664
+ }
2665
+
2666
+ showPinnedError(message: string): void {
2667
+ this.errorBannerContainer.clear();
2668
+ this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
2669
+ this.ui.requestRender();
2670
+ }
2671
+
2672
+ clearPinnedError(): void {
2673
+ if (this.errorBannerContainer.children.length === 0) return;
2674
+ this.errorBannerContainer.clear();
2675
+ this.ui.requestRender();
2676
+ }
2677
+
2678
+ showWarning(message: string): void {
2679
+ this.#uiHelpers.showWarning(message);
2680
+ }
2681
+
2682
+ #handleLspStartupEvent(event: LspStartupEvent): void {
2683
+ this.#updateWelcomeLspServers();
2684
+
2685
+ if (event.type === "failed") {
2686
+ this.showWarning(`LSP startup failed: ${event.error}. It will retry lazily on write.`);
2687
+ return;
2688
+ }
2689
+
2690
+ const failedServers = event.servers.filter(server => server.status === "error");
2691
+
2692
+ if (failedServers.length === 1) {
2693
+ const failedServer = failedServers[0];
2694
+ const detail = failedServer.error ? `: ${failedServer.error}` : "";
2695
+ this.showWarning(`LSP startup failed for ${failedServer.name}${detail}. It will retry lazily on write.`);
2696
+ return;
2697
+ }
2698
+
2699
+ if (failedServers.length > 1) {
2700
+ const failedNames = failedServers.map(server => server.name).join(", ");
2701
+ this.showWarning(`LSP startup failed for ${failedNames}. It will retry lazily on write.`);
2702
+ }
2703
+ }
2704
+
2705
+ #getWelcomeLspServers(): WelcomeLspServerInfo[] {
2706
+ return (
2707
+ this.lspServers?.map(server => ({
2708
+ name: server.name,
2709
+ status: server.status,
2710
+ fileTypes: server.fileTypes,
2711
+ })) ?? []
2712
+ );
2713
+ }
2714
+
2715
+ #updateWelcomeLspServers(): void {
2716
+ if (!this.#welcomeComponent) {
2717
+ return;
2718
+ }
2719
+
2720
+ this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
2721
+ this.ui.requestRender();
2722
+ }
2723
+
2724
+ #clearWorkingMessageAccentCache(): void {
2725
+ this.#workingMessageAccentCacheKey = undefined;
2726
+ this.#workingMessageAccentCacheValue = undefined;
2727
+ this.#workingMessageAccentCacheHasValue = false;
2728
+ }
2729
+
2730
+ #buildWorkingMessageAccentCacheKey(): WorkingMessageAccentCacheKey {
2731
+ const sessionAccentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
2732
+ return {
2733
+ sessionAccentEnabled,
2734
+ sessionName: sessionAccentEnabled ? this.sessionManager.getSessionName() : undefined,
2735
+ accentSurfaceLuminance: theme.accentSurfaceLuminance,
2736
+ };
2737
+ }
2738
+
2739
+ #workingMessageAccentCacheKeyEquals(a: WorkingMessageAccentCacheKey, b: WorkingMessageAccentCacheKey): boolean {
2740
+ return (
2741
+ a.sessionName === b.sessionName &&
2742
+ a.accentSurfaceLuminance === b.accentSurfaceLuminance &&
2743
+ a.sessionAccentEnabled === b.sessionAccentEnabled
2744
+ );
2745
+ }
2746
+
2747
+ #cacheWorkingMessageAccent(
2748
+ key: WorkingMessageAccentCacheKey,
2749
+ value: WorkingMessageAccent | undefined,
2750
+ ): WorkingMessageAccent | undefined {
2751
+ this.#workingMessageAccentCacheKey = key;
2752
+ this.#workingMessageAccentCacheValue = value;
2753
+ this.#workingMessageAccentCacheHasValue = true;
2754
+ return value;
2755
+ }
2756
+
2757
+ #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
2758
+ const key = this.#buildWorkingMessageAccentCacheKey();
2759
+ if (
2760
+ this.#workingMessageAccentCacheHasValue &&
2761
+ this.#workingMessageAccentCacheKey &&
2762
+ this.#workingMessageAccentCacheKeyEquals(key, this.#workingMessageAccentCacheKey)
2763
+ ) {
2764
+ return this.#workingMessageAccentCacheValue;
2765
+ }
2766
+ if (!key.sessionAccentEnabled || !key.sessionName) {
2767
+ return this.#cacheWorkingMessageAccent(key, undefined);
2768
+ }
2769
+ const hex = getSessionAccentHex(key.sessionName, key.accentSurfaceLuminance);
2770
+ const main = getSessionAccentAnsi(hex);
2771
+ const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
2772
+ return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
2773
+ }
2774
+
2775
+ ensureLoadingAnimation(): void {
2776
+ if (!this.loadingAnimation) {
2777
+ this.#clearWorkingMessageAccentCache();
2778
+ this.statusContainer.clear();
2779
+ const messageColorFn = ((message: string) =>
2780
+ renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
2781
+ animated?: true;
2782
+ };
2783
+ // Shimmer drives the 30fps redraw; when it is disabled the working
2784
+ // message is static, so leave `animated` unset and let the loader use
2785
+ // the spinner-only ~12.5fps cadence instead of repainting a frozen line.
2786
+ if (shimmerEnabled()) messageColorFn.animated = true;
2787
+ this.loadingAnimation = new Loader(
2788
+ this.ui,
2789
+ spinner => {
2790
+ const accent = this.#getWorkingMessageAccent();
2791
+ return accent ? `${accent.main}${spinner}\x1b[39m` : theme.fg("accent", spinner);
2792
+ },
2793
+ messageColorFn,
2794
+ this.#defaultWorkingMessage,
2795
+ getSymbolTheme().spinnerFrames,
2796
+ );
2797
+ this.statusContainer.addChild(this.loadingAnimation);
2798
+ }
2799
+
2800
+ this.applyPendingWorkingMessage();
2801
+ }
2802
+
2803
+ #stopLoadingAnimation(clearStatusContainer: boolean): void {
2804
+ if (!this.loadingAnimation) return;
2805
+ this.loadingAnimation.stop();
2806
+ this.loadingAnimation = undefined;
2807
+ this.#clearWorkingMessageAccentCache();
2808
+ if (clearStatusContainer) {
2809
+ this.statusContainer.clear();
2810
+ }
2811
+ }
2812
+
2813
+ setWorkingMessage(message?: string): void {
2814
+ if (message === undefined) {
2815
+ this.#pendingWorkingMessage = undefined;
2816
+ if (this.loadingAnimation) {
2817
+ this.loadingAnimation.setMessage(this.#defaultWorkingMessage);
2818
+ }
2819
+ return;
2820
+ }
2821
+
2822
+ if (this.loadingAnimation) {
2823
+ this.loadingAnimation.setMessage(message);
2824
+ return;
2825
+ }
2826
+
2827
+ this.#pendingWorkingMessage = message;
2828
+ }
2829
+
2830
+ applyPendingWorkingMessage(): void {
2831
+ if (this.#pendingWorkingMessage === undefined) {
2832
+ return;
2833
+ }
2834
+
2835
+ const message = this.#pendingWorkingMessage;
2836
+ this.#pendingWorkingMessage = undefined;
2837
+ this.setWorkingMessage(message);
2838
+ }
2839
+
2840
+ notifyInterrupting(): void {
2841
+ this.#eventController.notifyInterrupting();
2842
+ }
2843
+
2844
+ showNewVersionNotification(newVersion: string): void {
2845
+ this.#uiHelpers.showNewVersionNotification(newVersion);
2846
+ }
2847
+
2848
+ clearEditor(): void {
2849
+ this.#uiHelpers.clearEditor();
2850
+ }
2851
+
2852
+ updatePendingMessagesDisplay(): void {
2853
+ this.#uiHelpers.updatePendingMessagesDisplay();
2854
+ }
2855
+
2856
+ queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void {
2857
+ this.#uiHelpers.queueCompactionMessage(text, mode, images);
2858
+ }
2859
+
2860
+ flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
2861
+ return this.#uiHelpers.flushCompactionQueue(options);
2862
+ }
2863
+
2864
+ flushPendingBashComponents(): void {
2865
+ this.#uiHelpers.flushPendingBashComponents();
2866
+ }
2867
+
2868
+ isKnownSlashCommand(text: string): boolean {
2869
+ return this.#uiHelpers.isKnownSlashCommand(text);
2870
+ }
2871
+
2872
+ addMessageToChat(
2873
+ message: AgentMessage,
2874
+ options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
2875
+ ): Component[] {
2876
+ return this.#uiHelpers.addMessageToChat(message, options);
2877
+ }
2878
+
2879
+ renderSessionContext(
2880
+ sessionContext: SessionContext,
2881
+ options?: { updateFooter?: boolean; populateHistory?: boolean },
2882
+ ): void {
2883
+ this.#uiHelpers.renderSessionContext(sessionContext, options);
2884
+ }
2885
+
2886
+ renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void {
2887
+ this.#uiHelpers.renderInitialMessages(options);
2888
+ }
2889
+
2890
+ getUserMessageText(message: Message): string {
2891
+ return this.#uiHelpers.getUserMessageText(message);
2892
+ }
2893
+
2894
+ findLastAssistantMessage(): AssistantMessage | undefined {
2895
+ return this.#uiHelpers.findLastAssistantMessage();
2896
+ }
2897
+
2898
+ extractAssistantText(message: AssistantMessage): string {
2899
+ return this.#uiHelpers.extractAssistantText(message);
2900
+ }
2901
+
2902
+ // Command handling
2903
+ handleExportCommand(text: string): Promise<void> {
2904
+ return this.#commandController.handleExportCommand(text);
2905
+ }
2906
+
2907
+ handleDumpCommand() {
2908
+ return this.#commandController.handleDumpCommand();
2909
+ }
2910
+
2911
+ handleDebugTranscriptCommand(): Promise<void> {
2912
+ return this.#commandController.handleDebugTranscriptCommand();
2913
+ }
2914
+
2915
+ handleShareCommand(): Promise<void> {
2916
+ return this.#commandController.handleShareCommand();
2917
+ }
2918
+
2919
+ handleTodoCommand(args: string): Promise<void> {
2920
+ return this.#todoCommandController.handleTodoCommand(args);
2921
+ }
2922
+
2923
+ handleSessionCommand(): Promise<void> {
2924
+ return this.#commandController.handleSessionCommand();
2925
+ }
2926
+
2927
+ handleJobsCommand(): Promise<void> {
2928
+ return this.#commandController.handleJobsCommand();
2929
+ }
2930
+
2931
+ handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
2932
+ return this.#commandController.handleUsageCommand(reports);
2933
+ }
2934
+
2935
+ async handleChangelogCommand(showFull = false): Promise<void> {
2936
+ await this.#commandController.handleChangelogCommand(showFull);
2937
+ }
2938
+
2939
+ handleHotkeysCommand(): void {
2940
+ this.#commandController.handleHotkeysCommand();
2941
+ }
2942
+
2943
+ handleToolsCommand(): void {
2944
+ this.#commandController.handleToolsCommand();
2945
+ }
2946
+
2947
+ handleContextCommand(): void {
2948
+ this.#commandController.handleContextCommand();
2949
+ }
2950
+
2951
+ #prepareSessionSwitch(): void {
2952
+ this.#btwController.dispose();
2953
+ this.#omfgController.dispose();
2954
+ this.#extensionUiController.clearExtensionTerminalInputListeners();
2955
+ this.clearPinnedError();
2956
+ this.#hidePlanReview();
2957
+ }
2958
+
2959
+ handleClearCommand(): Promise<void> {
2960
+ this.#prepareSessionSwitch();
2961
+ return this.#commandController.handleClearCommand();
2962
+ }
2963
+
2964
+ handleFreshCommand(): Promise<void> {
2965
+ return this.#commandController.handleFreshCommand();
2966
+ }
2967
+
2968
+ handleDropCommand(): Promise<void> {
2969
+ this.#prepareSessionSwitch();
2970
+ return this.#commandController.handleDropCommand();
2971
+ }
2972
+
2973
+ handleForkCommand(): Promise<void> {
2974
+ this.#btwController.dispose();
2975
+ this.#omfgController.dispose();
2976
+ return this.#commandController.handleForkCommand();
2977
+ }
2978
+
2979
+ handleMoveCommand(targetPath: string): Promise<void> {
2980
+ return this.#commandController.handleMoveCommand(targetPath);
2981
+ }
2982
+
2983
+ handleRenameCommand(title: string): Promise<void> {
2984
+ return this.#commandController.handleRenameCommand(title);
2985
+ }
2986
+
2987
+ handleMemoryCommand(text: string): Promise<void> {
2988
+ return this.#commandController.handleMemoryCommand(text);
2989
+ }
2990
+
2991
+ async handleSTTToggle(): Promise<void> {
2992
+ if (!settings.get("stt.enabled")) {
2993
+ this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
2994
+ return;
2995
+ }
2996
+ if (!this.#sttController) {
2997
+ this.#sttController = new STTController();
2998
+ }
2999
+ await this.#sttController.toggle(this.editor, {
3000
+ showWarning: (msg: string) => this.showWarning(msg),
3001
+ showStatus: (msg: string) => this.showStatus(msg),
3002
+ onStateChange: (state: SttState) => {
3003
+ if (state === "recording") {
3004
+ this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
3005
+ this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
3006
+ this.ui.setShowHardwareCursor(false);
3007
+ this.editor.setUseTerminalCursor(false);
3008
+ this.#startMicAnimation();
3009
+ } else if (state === "transcribing") {
3010
+ this.#stopMicAnimation();
3011
+ this.#setMicCursor({ r: 200, g: 200, b: 200 });
3012
+ } else {
3013
+ this.#cleanupMicAnimation();
3014
+ }
3015
+ this.updateEditorTopBorder();
3016
+ this.ui.requestRender();
3017
+ },
3018
+ });
3019
+ }
3020
+
3021
+ #setMicCursor(color: { r: number; g: number; b: number }): void {
3022
+ this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
3023
+ // Theme symbols can be wide (for example, 🎤), so measure the rendered override.
3024
+ this.editor.cursorOverrideWidth = visibleWidth(this.editor.cursorOverride);
3025
+ }
3026
+
3027
+ #updateMicIcon(): void {
3028
+ const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
3029
+ this.#setMicCursor({ r, g, b });
3030
+ }
3031
+
3032
+ #startMicAnimation(): void {
3033
+ if (this.#voiceAnimationInterval) return;
3034
+ this.#voiceHue = 0;
3035
+ this.#updateMicIcon();
3036
+ this.#voiceAnimationInterval = setInterval(() => {
3037
+ this.#voiceHue = (this.#voiceHue + 8) % 360;
3038
+ this.#updateMicIcon();
3039
+ this.ui.requestRender();
3040
+ }, 60);
3041
+ }
3042
+
3043
+ #stopMicAnimation(): void {
3044
+ if (this.#voiceAnimationInterval) {
3045
+ clearInterval(this.#voiceAnimationInterval);
3046
+ this.#voiceAnimationInterval = undefined;
3047
+ }
3048
+ }
3049
+
3050
+ #cleanupMicAnimation(): void {
3051
+ if (this.#voiceAnimationInterval) {
3052
+ clearInterval(this.#voiceAnimationInterval);
3053
+ this.#voiceAnimationInterval = undefined;
3054
+ }
3055
+ this.editor.cursorOverride = undefined;
3056
+ this.editor.cursorOverrideWidth = undefined;
3057
+ if (this.#voicePreviousShowHardwareCursor !== null) {
3058
+ this.ui.setShowHardwareCursor(this.#voicePreviousShowHardwareCursor);
3059
+ this.#voicePreviousShowHardwareCursor = null;
3060
+ }
3061
+ if (this.#voicePreviousUseTerminalCursor !== null) {
3062
+ this.editor.setUseTerminalCursor(this.#voicePreviousUseTerminalCursor);
3063
+ this.#voicePreviousUseTerminalCursor = null;
3064
+ }
3065
+ }
3066
+
3067
+ async showDebugSelector(): Promise<void> {
3068
+ await this.#selectorController.showDebugSelector();
3069
+ }
3070
+
3071
+ showAgentHub(): void {
3072
+ this.#selectorController.showAgentHub(this.#observerRegistry);
3073
+ }
3074
+
3075
+ resetObserverRegistry(): void {
3076
+ this.#observerRegistry.resetSessions();
3077
+ this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
3078
+ }
3079
+
3080
+ handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void> {
3081
+ return this.#commandController.handleBashCommand(command, excludeFromContext);
3082
+ }
3083
+
3084
+ handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void> {
3085
+ return this.#commandController.handlePythonCommand(code, excludeFromContext);
3086
+ }
3087
+
3088
+ async handleMCPCommand(text: string): Promise<void> {
3089
+ const controller = new MCPCommandController(this);
3090
+ await controller.handle(text);
3091
+ }
3092
+
3093
+ async handleSSHCommand(text: string): Promise<void> {
3094
+ const controller = new SSHCommandController(this);
3095
+ await controller.handle(text);
3096
+ }
3097
+
3098
+ handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
3099
+ return this.#commandController.handleCompactCommand(customInstructions);
3100
+ }
3101
+
3102
+ handleHandoffCommand(customInstructions?: string): Promise<void> {
3103
+ return this.#commandController.handleHandoffCommand(customInstructions);
3104
+ }
3105
+
3106
+ handleShakeCommand(mode: ShakeMode): Promise<void> {
3107
+ return this.#commandController.handleShakeCommand(mode);
3108
+ }
3109
+
3110
+ executeCompaction(
3111
+ customInstructionsOrOptions?: string | CompactOptions,
3112
+ isAuto?: boolean,
3113
+ ): Promise<CompactionOutcome> {
3114
+ return this.#commandController.executeCompaction(customInstructionsOrOptions, isAuto);
3115
+ }
3116
+
3117
+ openInBrowser(urlOrPath: string): void {
3118
+ this.#commandController.openInBrowser(urlOrPath);
3119
+ }
3120
+
3121
+ // Selector handling
3122
+ showSettingsSelector(): void {
3123
+ this.#selectorController.showSettingsSelector();
3124
+ }
3125
+
3126
+ showHistorySearch(): void {
3127
+ this.#selectorController.showHistorySearch();
3128
+ }
3129
+
3130
+ showExtensionsDashboard(): void {
3131
+ void this.#selectorController.showExtensionsDashboard();
3132
+ }
3133
+
3134
+ showAgentsDashboard(): void {
3135
+ void this.#selectorController.showAgentsDashboard();
3136
+ }
3137
+
3138
+ showModelSelector(options?: { temporaryOnly?: boolean }): void {
3139
+ this.#selectorController.showModelSelector(options);
3140
+ }
3141
+
3142
+ showPluginSelector(mode?: "install" | "uninstall"): void {
3143
+ void this.#selectorController.showPluginSelector(mode);
3144
+ }
3145
+
3146
+ showUserMessageSelector(): void {
3147
+ this.#selectorController.showUserMessageSelector();
3148
+ }
3149
+
3150
+ showCopySelector(): void {
3151
+ this.#selectorController.showCopySelector();
3152
+ }
3153
+
3154
+ showTreeSelector(): void {
3155
+ this.#selectorController.showTreeSelector();
3156
+ }
3157
+
3158
+ showSessionSelector(): void {
3159
+ this.#selectorController.showSessionSelector();
3160
+ }
3161
+
3162
+ handleResumeSession(sessionPath: string): Promise<void> {
3163
+ this.#btwController.dispose();
3164
+ this.#omfgController.dispose();
3165
+ this.resetObserverRegistry();
3166
+ return this.#selectorController.handleResumeSession(sessionPath);
3167
+ }
3168
+
3169
+ handleSessionDeleteCommand(): Promise<void> {
3170
+ return this.#selectorController.handleSessionDeleteCommand();
3171
+ }
3172
+
3173
+ showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
3174
+ return this.#selectorController.showOAuthSelector(mode, providerId);
3175
+ }
3176
+
3177
+ showProviderSetup(): Promise<void> {
3178
+ return runProviderSetupWizard(this);
3179
+ }
3180
+
3181
+ showHookConfirm(title: string, message: string): Promise<boolean> {
3182
+ return this.#extensionUiController.showHookConfirm(title, message);
3183
+ }
3184
+
3185
+ // Input handling
3186
+ handleCtrlC(): void {
3187
+ this.#inputController.handleCtrlC();
3188
+ }
3189
+
3190
+ handleCtrlD(): void {
3191
+ this.#inputController.handleCtrlD();
3192
+ }
3193
+
3194
+ handleCtrlZ(): void {
3195
+ this.#inputController.handleCtrlZ();
3196
+ }
3197
+
3198
+ handleDequeue(): void {
3199
+ this.#inputController.handleDequeue();
3200
+ }
3201
+
3202
+ handleImagePaste(): Promise<boolean> {
3203
+ return this.#inputController.handleImagePaste();
3204
+ }
3205
+
3206
+ handleBtwCommand(question: string): Promise<void> {
3207
+ return this.#btwController.start(question);
3208
+ }
3209
+
3210
+ handleTanCommand(work: string): Promise<void> {
3211
+ return this.#tanCommandController.start(work);
3212
+ }
3213
+
3214
+ hasActiveBtw(): boolean {
3215
+ return this.#btwController.hasActiveRequest();
3216
+ }
3217
+
3218
+ handleBtwEscape(): boolean {
3219
+ return this.#btwController.handleEscape();
3220
+ }
3221
+
3222
+ handleOmfgCommand(complaint: string): Promise<void> {
3223
+ return this.#omfgController.start(complaint);
3224
+ }
3225
+
3226
+ hasActiveOmfg(): boolean {
3227
+ return this.#omfgController.hasActiveRequest();
3228
+ }
3229
+
3230
+ handleOmfgEscape(): boolean {
3231
+ return this.#omfgController.handleEscape();
3232
+ }
3233
+
3234
+ cycleThinkingLevel(): void {
3235
+ this.#inputController.cycleThinkingLevel();
3236
+ }
3237
+
3238
+ cycleRoleModel(direction?: "forward" | "backward"): Promise<void> {
3239
+ return this.#inputController.cycleRoleModel(direction);
3240
+ }
3241
+
3242
+ toggleToolOutputExpansion(): void {
3243
+ this.#inputController.toggleToolOutputExpansion();
3244
+ }
3245
+
3246
+ setToolsExpanded(expanded: boolean): void {
3247
+ this.#inputController.setToolsExpanded(expanded);
3248
+ }
3249
+
3250
+ toggleThinkingBlockVisibility(): void {
3251
+ this.#inputController.toggleThinkingBlockVisibility();
3252
+ }
3253
+
3254
+ toggleTodoExpansion(): void {
3255
+ this.todoExpanded = !this.todoExpanded;
3256
+ this.#renderTodoList();
3257
+ this.ui.requestRender();
3258
+ }
3259
+
3260
+ setTodos(todos: TodoItem[] | TodoPhase[]): void {
3261
+ if (todos.length > 0 && "tasks" in todos[0]) {
3262
+ this.todoPhases = todos as TodoPhase[];
3263
+ } else {
3264
+ this.todoPhases = [
3265
+ {
3266
+ name: "Todos",
3267
+ tasks: todos as TodoItem[],
3268
+ },
3269
+ ];
3270
+ }
3271
+ this.#syncTodoAutoClearTimer();
3272
+ this.#renderTodoList();
3273
+ this.ui.requestRender();
3274
+ }
3275
+
3276
+ async reloadTodos(): Promise<void> {
3277
+ await this.#loadTodoList();
3278
+ this.ui.requestRender();
3279
+ }
3280
+
3281
+ openExternalEditor(): void {
3282
+ this.#inputController.openExternalEditor();
3283
+ }
3284
+
3285
+ registerExtensionShortcuts(): void {
3286
+ this.#inputController.registerExtensionShortcuts();
3287
+ }
3288
+
3289
+ // Hook UI methods
3290
+ initHooksAndCustomTools(): Promise<void> {
3291
+ return this.#extensionUiController.initHooksAndCustomTools();
3292
+ }
3293
+
3294
+ emitCustomToolSessionEvent(
3295
+ reason: "start" | "switch" | "branch" | "tree" | "shutdown",
3296
+ previousSessionFile?: string,
3297
+ ): Promise<void> {
3298
+ return this.#extensionUiController.emitCustomToolSessionEvent(reason, previousSessionFile);
3299
+ }
3300
+
3301
+ setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
3302
+ this.#extensionUiController.setHookWidget(key, content, options);
3303
+ }
3304
+
3305
+ setHookStatus(key: string, text: string | undefined): void {
3306
+ this.#extensionUiController.setHookStatus(key, text);
3307
+ }
3308
+
3309
+ showHookSelector(
3310
+ title: string,
3311
+ options: ExtensionUISelectItem[],
3312
+ dialogOptions?: InteractiveSelectorDialogOptions,
3313
+ extra?: { slider?: HookSelectorSlider },
3314
+ ): Promise<string | undefined> {
3315
+ return this.#extensionUiController.showHookSelector(title, options, dialogOptions, extra);
3316
+ }
3317
+
3318
+ hideHookSelector(): void {
3319
+ this.#extensionUiController.hideHookSelector();
3320
+ }
3321
+
3322
+ showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
3323
+ return this.#extensionUiController.showHookInput(title, placeholder);
3324
+ }
3325
+
3326
+ hideHookInput(): void {
3327
+ this.#extensionUiController.hideHookInput();
3328
+ }
3329
+
3330
+ showHookEditor(
3331
+ title: string,
3332
+ prefill?: string,
3333
+ dialogOptions?: ExtensionUIDialogOptions,
3334
+ editorOptions?: { promptStyle?: boolean },
3335
+ ): Promise<string | undefined> {
3336
+ return this.#extensionUiController.showHookEditor(title, prefill, dialogOptions, editorOptions);
3337
+ }
3338
+
3339
+ hideHookEditor(): void {
3340
+ this.#extensionUiController.hideHookEditor();
3341
+ }
3342
+
3343
+ showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
3344
+ this.#extensionUiController.showHookNotify(message, type);
3345
+ }
3346
+
3347
+ showHookCustom<T>(
3348
+ factory: (
3349
+ tui: TUI,
3350
+ theme: Theme,
3351
+ keybindings: KeybindingsManager,
3352
+ done: (result: T) => void,
3353
+ ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
3354
+ options?: { overlay?: boolean },
3355
+ ): Promise<T> {
3356
+ return this.#extensionUiController.showHookCustom(factory, options);
3357
+ }
3358
+
3359
+ showExtensionError(extensionPath: string, error: string): void {
3360
+ this.#extensionUiController.showExtensionError(extensionPath, error);
3361
+ }
3362
+
3363
+ showToolError(toolName: string, error: string): void {
3364
+ this.#extensionUiController.showToolError(toolName, error);
3365
+ }
3366
+
3367
+ #subscribeToAgent(): void {
3368
+ this.#eventController.subscribeToAgent();
3369
+ }
3370
+ }