@ai-agent-forge/agent-forge 0.88.1 → 0.88.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -1
  3. package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
  4. package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
  5. package/bundled-plugins/coding-tools/agent-forge.json +1 -1
  6. package/bundled-plugins/coding-tools/plugin.json +1 -1
  7. package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
  8. package/bundled-plugins/compaction-policy/plugin.json +1 -1
  9. package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
  10. package/bundled-plugins/overflow-policy/plugin.json +1 -1
  11. package/bundled-plugins/provider-failover/agent-forge.json +1 -1
  12. package/bundled-plugins/provider-failover/plugin.json +1 -1
  13. package/bundled-plugins/retry-policy/agent-forge.json +1 -1
  14. package/bundled-plugins/retry-policy/plugin.json +1 -1
  15. package/dist/artifacts/paged-read.d.ts +6 -1
  16. package/dist/artifacts/paged-read.d.ts.map +1 -1
  17. package/dist/artifacts/paged-read.js.map +1 -1
  18. package/dist/bundle/.build-manifest.json +283 -263
  19. package/dist/bundle/chunks/{anthropic-messages-PLAKXR73.js → anthropic-messages-3FJHRAJL.js} +2 -2
  20. package/dist/bundle/chunks/anthropic.js +1 -1
  21. package/dist/bundle/chunks/{azure-openai-responses-OUWQ2AZD.js → azure-openai-responses-GCL47IDS.js} +1 -1
  22. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  23. package/dist/bundle/chunks/{chunk-Z72JWUZ4.js → chunk-32YDZI4J.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-AQLNTW2B.js → chunk-755VNWIF.js} +17 -16
  25. package/dist/bundle/chunks/{chunk-LZOEQITZ.js → chunk-B42Y2VME.js} +120 -104
  26. package/dist/bundle/chunks/{chunk-DRQDKNXA.js → chunk-BVVSHK6W.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-6R7UI4IY.js → chunk-QDLOMKF6.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-D3ARGZFR.js → chunk-V3ACW7VD.js} +3 -3
  29. package/dist/bundle/chunks/{google-generative-ai-DLII6DAT.js → google-generative-ai-6BMXWA44.js} +1 -1
  30. package/dist/bundle/chunks/{google-vertex-G7BZL7GM.js → google-vertex-6H7NLRTK.js} +1 -1
  31. package/dist/bundle/chunks/image-resize-worker.js +3 -3
  32. package/dist/bundle/chunks/{mistral-conversations-JXUK24ID.js → mistral-conversations-IXUHSFSJ.js} +1 -1
  33. package/dist/bundle/chunks/{openai-codex-responses-5GSGFIPG.js → openai-codex-responses-YHUAEVCM.js} +1 -1
  34. package/dist/bundle/chunks/openai-codex.js +1 -1
  35. package/dist/bundle/chunks/{openai-completions-EI2I6DTG.js → openai-completions-ULQCFC72.js} +1 -1
  36. package/dist/bundle/chunks/openai-responses-NLLAJDPS.js +2 -0
  37. package/dist/bundle/chunks/{openrouter-images-G4GQRUSG.js → openrouter-images-GZZQZXMS.js} +1 -1
  38. package/dist/bundle/chunks/openrouter.js +1 -1
  39. package/dist/bundle/chunks/{pi-messages-PWU2VJMR.js → pi-messages-EJTI2EJ3.js} +1 -1
  40. package/dist/bundle/chunks/virtual-modules-TQWEGNNZ.js +5 -0
  41. package/dist/bundle/cli.js +1 -1
  42. package/dist/bundle/client.js +1 -1
  43. package/dist/bundle/index.js +1 -1
  44. package/dist/bundle/rpc-entry.js +1 -1
  45. package/dist/cli/args.d.ts +0 -1
  46. package/dist/cli/args.d.ts.map +1 -1
  47. package/dist/cli/args.js +1 -4
  48. package/dist/cli/args.js.map +1 -1
  49. package/dist/cli/config-selector.d.ts.map +1 -1
  50. package/dist/cli/config-selector.js +1 -1
  51. package/dist/cli/config-selector.js.map +1 -1
  52. package/dist/cli/experimental/cli.d.ts +3 -3
  53. package/dist/cli/experimental/cli.d.ts.map +1 -1
  54. package/dist/cli/experimental/cli.js +2 -2
  55. package/dist/cli/experimental/cli.js.map +1 -1
  56. package/dist/cli/experimental/commands/agent-forge.d.ts +15 -0
  57. package/dist/cli/experimental/commands/agent-forge.d.ts.map +1 -0
  58. package/dist/cli/experimental/commands/{pi.js → agent-forge.js} +4 -4
  59. package/dist/cli/experimental/commands/agent-forge.js.map +1 -0
  60. package/dist/cli/plugin-command.d.ts.map +1 -1
  61. package/dist/cli/plugin-command.js +8 -2
  62. package/dist/cli/plugin-command.js.map +1 -1
  63. package/dist/cli/startup-ui.d.ts.map +1 -1
  64. package/dist/cli/startup-ui.js +1 -1
  65. package/dist/cli/startup-ui.js.map +1 -1
  66. package/dist/config.d.ts +4 -4
  67. package/dist/config.d.ts.map +1 -1
  68. package/dist/config.js +7 -7
  69. package/dist/config.js.map +1 -1
  70. package/dist/core/agent-forge-manifest.d.ts +7 -0
  71. package/dist/core/agent-forge-manifest.d.ts.map +1 -0
  72. package/dist/core/{pi-manifest.js → agent-forge-manifest.js} +4 -4
  73. package/dist/core/agent-forge-manifest.js.map +1 -0
  74. package/dist/core/agent-session-compaction.d.ts +1 -1
  75. package/dist/core/agent-session-compaction.d.ts.map +1 -1
  76. package/dist/core/agent-session-compaction.js +3 -3
  77. package/dist/core/agent-session-compaction.js.map +1 -1
  78. package/dist/core/agent-session-internal.d.ts +2 -0
  79. package/dist/core/agent-session-internal.d.ts.map +1 -1
  80. package/dist/core/agent-session-internal.js.map +1 -1
  81. package/dist/core/agent-session-lifecycle.d.ts.map +1 -1
  82. package/dist/core/agent-session-lifecycle.js +12 -1
  83. package/dist/core/agent-session-lifecycle.js.map +1 -1
  84. package/dist/core/agent-session-projection.d.ts.map +1 -1
  85. package/dist/core/agent-session-projection.js +3 -3
  86. package/dist/core/agent-session-projection.js.map +1 -1
  87. package/dist/core/agent-session-services.d.ts +3 -1
  88. package/dist/core/agent-session-services.d.ts.map +1 -1
  89. package/dist/core/agent-session-services.js +2 -1
  90. package/dist/core/agent-session-services.js.map +1 -1
  91. package/dist/core/agent-session-store.d.ts.map +1 -1
  92. package/dist/core/agent-session-store.js +6 -1
  93. package/dist/core/agent-session-store.js.map +1 -1
  94. package/dist/core/agent-session-tools.d.ts.map +1 -1
  95. package/dist/core/agent-session-tools.js +13 -0
  96. package/dist/core/agent-session-tools.js.map +1 -1
  97. package/dist/core/agent-session.d.ts +24 -0
  98. package/dist/core/agent-session.d.ts.map +1 -1
  99. package/dist/core/agent-session.js +58 -4
  100. package/dist/core/agent-session.js.map +1 -1
  101. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  102. package/dist/core/compaction/branch-summarization.js +1 -1
  103. package/dist/core/compaction/branch-summarization.js.map +1 -1
  104. package/dist/core/experimental.d.ts.map +1 -1
  105. package/dist/core/experimental.js +1 -3
  106. package/dist/core/experimental.js.map +1 -1
  107. package/dist/core/extensions/capability-runtime-store.d.ts +20 -1
  108. package/dist/core/extensions/capability-runtime-store.d.ts.map +1 -1
  109. package/dist/core/extensions/capability-runtime-store.js +67 -0
  110. package/dist/core/extensions/capability-runtime-store.js.map +1 -1
  111. package/dist/core/extensions/capability-runtime-types.d.ts +14 -0
  112. package/dist/core/extensions/capability-runtime-types.d.ts.map +1 -1
  113. package/dist/core/extensions/capability-runtime-types.js +7 -1
  114. package/dist/core/extensions/capability-runtime-types.js.map +1 -1
  115. package/dist/core/extensions/capability-runtime.d.ts.map +1 -1
  116. package/dist/core/extensions/capability-runtime.js +144 -42
  117. package/dist/core/extensions/capability-runtime.js.map +1 -1
  118. package/dist/core/extensions/virtual-modules.d.ts.map +1 -1
  119. package/dist/core/extensions/virtual-modules.js +6 -1
  120. package/dist/core/extensions/virtual-modules.js.map +1 -1
  121. package/dist/core/facade/coding-agent-facade.d.ts +1 -1
  122. package/dist/core/facade/coding-agent-facade.d.ts.map +1 -1
  123. package/dist/core/facade/coding-agent-facade.js +1 -1
  124. package/dist/core/facade/coding-agent-facade.js.map +1 -1
  125. package/dist/core/i18n/locales/zh-cn/cli.d.ts.map +1 -1
  126. package/dist/core/i18n/locales/zh-cn/cli.js +2 -0
  127. package/dist/core/i18n/locales/zh-cn/cli.js.map +1 -1
  128. package/dist/core/i18n/locales/zh-cn/core-contract-final.d.ts +1 -1
  129. package/dist/core/i18n/locales/zh-cn/core-contract-final.d.ts.map +1 -1
  130. package/dist/core/i18n/locales/zh-cn/core-contract-final.js +3 -3
  131. package/dist/core/i18n/locales/zh-cn/core-contract-final.js.map +1 -1
  132. package/dist/core/i18n/locales/zh-cn/core-contract-plugins.d.ts.map +1 -1
  133. package/dist/core/i18n/locales/zh-cn/core-contract-plugins.js +1 -1
  134. package/dist/core/i18n/locales/zh-cn/core-contract-plugins.js.map +1 -1
  135. package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
  136. package/dist/core/i18n/locales/zh-cn/core-errors.js +1 -1
  137. package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
  138. package/dist/core/i18n/locales/zh-cn/index.d.ts.map +1 -1
  139. package/dist/core/i18n/locales/zh-cn/index.js +2 -0
  140. package/dist/core/i18n/locales/zh-cn/index.js.map +1 -1
  141. package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
  142. package/dist/core/i18n/locales/zh-cn/interactive-mode.js +5 -0
  143. package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
  144. package/dist/core/i18n/locales/zh-cn/session-cleanup.d.ts +7 -0
  145. package/dist/core/i18n/locales/zh-cn/session-cleanup.d.ts.map +1 -0
  146. package/dist/core/i18n/locales/zh-cn/session-cleanup.js +15 -0
  147. package/dist/core/i18n/locales/zh-cn/session-cleanup.js.map +1 -0
  148. package/dist/core/i18n/locales/zh-cn/slash-commands.d.ts.map +1 -1
  149. package/dist/core/i18n/locales/zh-cn/slash-commands.js +1 -0
  150. package/dist/core/i18n/locales/zh-cn/slash-commands.js.map +1 -1
  151. package/dist/core/i18n/locales/zh-cn/tui-components-misc.d.ts.map +1 -1
  152. package/dist/core/i18n/locales/zh-cn/tui-components-misc.js +2 -0
  153. package/dist/core/i18n/locales/zh-cn/tui-components-misc.js.map +1 -1
  154. package/dist/core/model-resolver.d.ts +2 -0
  155. package/dist/core/model-resolver.d.ts.map +1 -1
  156. package/dist/core/model-resolver.js +4 -1
  157. package/dist/core/model-resolver.js.map +1 -1
  158. package/dist/core/package-resource-collection.d.ts.map +1 -1
  159. package/dist/core/package-resource-collection.js +5 -5
  160. package/dist/core/package-resource-collection.js.map +1 -1
  161. package/dist/core/provider-composer.d.ts.map +1 -1
  162. package/dist/core/provider-composer.js +3 -3
  163. package/dist/core/provider-composer.js.map +1 -1
  164. package/dist/core/resource-loader.d.ts.map +1 -1
  165. package/dist/core/resource-loader.js +1 -1
  166. package/dist/core/resource-loader.js.map +1 -1
  167. package/dist/core/sdk.d.ts +1 -1
  168. package/dist/core/sdk.d.ts.map +1 -1
  169. package/dist/core/sdk.js +30 -8
  170. package/dist/core/sdk.js.map +1 -1
  171. package/dist/core/session-cleanup.d.ts +49 -0
  172. package/dist/core/session-cleanup.d.ts.map +1 -0
  173. package/dist/core/session-cleanup.js +159 -0
  174. package/dist/core/session-cleanup.js.map +1 -0
  175. package/dist/core/session-index.d.ts +75 -0
  176. package/dist/core/session-index.d.ts.map +1 -0
  177. package/dist/core/session-index.js +455 -0
  178. package/dist/core/session-index.js.map +1 -0
  179. package/dist/core/session-manager.d.ts +9 -0
  180. package/dist/core/session-manager.d.ts.map +1 -1
  181. package/dist/core/session-manager.js +206 -51
  182. package/dist/core/session-manager.js.map +1 -1
  183. package/dist/core/settings-manager.d.ts.map +1 -1
  184. package/dist/core/settings-manager.js +5 -10
  185. package/dist/core/settings-manager.js.map +1 -1
  186. package/dist/core/slash-commands.d.ts.map +1 -1
  187. package/dist/core/slash-commands.js +5 -0
  188. package/dist/core/slash-commands.js.map +1 -1
  189. package/dist/core/system-prompt.d.ts.map +1 -1
  190. package/dist/core/system-prompt.js +12 -1
  191. package/dist/core/system-prompt.js.map +1 -1
  192. package/dist/core/timings.d.ts.map +1 -1
  193. package/dist/core/timings.js +1 -3
  194. package/dist/core/timings.js.map +1 -1
  195. package/dist/core/tools/capability-mount.d.ts +40 -0
  196. package/dist/core/tools/capability-mount.d.ts.map +1 -0
  197. package/dist/core/tools/capability-mount.js +112 -0
  198. package/dist/core/tools/capability-mount.js.map +1 -0
  199. package/dist/core/tools/render-utils.d.ts +1 -1
  200. package/dist/core/tools/render-utils.d.ts.map +1 -1
  201. package/dist/core/tools/render-utils.js.map +1 -1
  202. package/dist/core/tools/tool-definition.d.ts +1 -1
  203. package/dist/core/tools/tool-definition.d.ts.map +1 -1
  204. package/dist/core/tools/tool-definition.js.map +1 -1
  205. package/dist/core/trace-retention.d.ts +16 -1
  206. package/dist/core/trace-retention.d.ts.map +1 -1
  207. package/dist/core/trace-retention.js +43 -20
  208. package/dist/core/trace-retention.js.map +1 -1
  209. package/dist/export-html/ansi-to-html.d.ts.map +1 -0
  210. package/dist/export-html/ansi-to-html.js.map +1 -0
  211. package/dist/{core/export-html → export-html}/index.d.ts +1 -1
  212. package/dist/export-html/index.d.ts.map +1 -0
  213. package/dist/{core/export-html → export-html}/index.js +5 -5
  214. package/dist/export-html/index.js.map +1 -0
  215. package/dist/{core/export-html → export-html}/tool-renderer.d.ts +2 -2
  216. package/dist/export-html/tool-renderer.d.ts.map +1 -0
  217. package/dist/export-html/tool-renderer.js.map +1 -0
  218. package/dist/{modes/interactive → host}/model-catalog-refresh.d.ts +1 -1
  219. package/dist/host/model-catalog-refresh.d.ts.map +1 -0
  220. package/dist/{modes/interactive → host}/model-catalog-refresh.js +1 -1
  221. package/dist/host/model-catalog-refresh.js.map +1 -0
  222. package/dist/index.d.ts +5 -5
  223. package/dist/index.d.ts.map +1 -1
  224. package/dist/index.js +12 -12
  225. package/dist/index.js.map +1 -1
  226. package/dist/main.d.ts.map +1 -1
  227. package/dist/main.js +26 -14
  228. package/dist/main.js.map +1 -1
  229. package/dist/modes/interactive/components/agents-view.d.ts.map +1 -1
  230. package/dist/modes/interactive/components/agents-view.js +1 -1
  231. package/dist/modes/interactive/components/agents-view.js.map +1 -1
  232. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  233. package/dist/modes/interactive/components/armin.js +1 -1
  234. package/dist/modes/interactive/components/armin.js.map +1 -1
  235. package/dist/modes/interactive/components/assistant-message.d.ts +6 -0
  236. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  237. package/dist/modes/interactive/components/assistant-message.js +108 -27
  238. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  239. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/bash-execution.js +1 -1
  241. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  242. package/dist/modes/interactive/components/bordered-loader.d.ts +1 -1
  243. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  244. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  245. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  246. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  247. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  248. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  249. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  250. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  251. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  252. package/dist/modes/interactive/components/config-selector.js +1 -1
  253. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  254. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -1
  255. package/dist/modes/interactive/components/custom-entry.js +1 -1
  256. package/dist/modes/interactive/components/custom-entry.js.map +1 -1
  257. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  258. package/dist/modes/interactive/components/custom-message.js +1 -1
  259. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  260. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  261. package/dist/modes/interactive/components/daxnuts.js +1 -1
  262. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  263. package/dist/modes/interactive/components/dialog-editor.d.ts.map +1 -1
  264. package/dist/modes/interactive/components/dialog-editor.js +1 -1
  265. package/dist/modes/interactive/components/dialog-editor.js.map +1 -1
  266. package/dist/modes/interactive/components/dialog-input.d.ts.map +1 -1
  267. package/dist/modes/interactive/components/dialog-input.js +1 -1
  268. package/dist/modes/interactive/components/dialog-input.js.map +1 -1
  269. package/dist/modes/interactive/components/dialog-selector.d.ts.map +1 -1
  270. package/dist/modes/interactive/components/dialog-selector.js +1 -1
  271. package/dist/modes/interactive/components/dialog-selector.js.map +1 -1
  272. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  273. package/dist/modes/interactive/components/diff.js +1 -1
  274. package/dist/modes/interactive/components/diff.js.map +1 -1
  275. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  276. package/dist/modes/interactive/components/dynamic-border.js +1 -1
  277. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  278. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  279. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  280. package/dist/modes/interactive/components/first-time-setup.js +1 -1
  281. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  282. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  283. package/dist/modes/interactive/components/footer.js +1 -1
  284. package/dist/modes/interactive/components/footer.js.map +1 -1
  285. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  286. package/dist/modes/interactive/components/keybinding-hints.js +1 -1
  287. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  288. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  289. package/dist/modes/interactive/components/login-dialog.js +1 -1
  290. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  291. package/dist/modes/interactive/components/mermaid.d.ts +1 -1
  292. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -1
  293. package/dist/modes/interactive/components/mermaid.js.map +1 -1
  294. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  295. package/dist/modes/interactive/components/model-selector.js +2 -2
  296. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  297. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  298. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  299. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  300. package/dist/modes/interactive/components/plugin-manager-entry.d.ts.map +1 -1
  301. package/dist/modes/interactive/components/plugin-manager-entry.js +1 -1
  302. package/dist/modes/interactive/components/plugin-manager-entry.js.map +1 -1
  303. package/dist/modes/interactive/components/plugin-selection-entry.d.ts.map +1 -1
  304. package/dist/modes/interactive/components/plugin-selection-entry.js +1 -1
  305. package/dist/modes/interactive/components/plugin-selection-entry.js.map +1 -1
  306. package/dist/modes/interactive/components/queue-manager-selector.d.ts.map +1 -1
  307. package/dist/modes/interactive/components/queue-manager-selector.js +1 -1
  308. package/dist/modes/interactive/components/queue-manager-selector.js.map +1 -1
  309. package/dist/modes/interactive/components/render-types.d.ts +1 -1
  310. package/dist/modes/interactive/components/render-types.d.ts.map +1 -1
  311. package/dist/modes/interactive/components/render-types.js.map +1 -1
  312. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  313. package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
  314. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  315. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  316. package/dist/modes/interactive/components/session-selector-search.js +5 -1
  317. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  318. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  319. package/dist/modes/interactive/components/session-selector.js +11 -2
  320. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  321. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  322. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  323. package/dist/modes/interactive/components/settings-selector.js +1 -1
  324. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  325. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
  326. package/dist/modes/interactive/components/settings-submenu.js +1 -1
  327. package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
  328. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  329. package/dist/modes/interactive/components/show-images-selector.js +1 -1
  330. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  331. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  332. package/dist/modes/interactive/components/skill-invocation-message.js +1 -1
  333. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  334. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
  335. package/dist/modes/interactive/components/status-indicator.js +1 -1
  336. package/dist/modes/interactive/components/status-indicator.js.map +1 -1
  337. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  338. package/dist/modes/interactive/components/theme-selector.js +1 -1
  339. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  340. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  341. package/dist/modes/interactive/components/thinking-selector.js +1 -1
  342. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  343. package/dist/modes/interactive/components/tool-execution.d.ts +19 -0
  344. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  345. package/dist/modes/interactive/components/tool-execution.js +139 -4
  346. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  347. package/dist/modes/interactive/components/tool-run-group.d.ts +44 -0
  348. package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -0
  349. package/dist/modes/interactive/components/tool-run-group.js +121 -0
  350. package/dist/modes/interactive/components/tool-run-group.js.map +1 -0
  351. package/dist/modes/interactive/components/tree-list.d.ts.map +1 -1
  352. package/dist/modes/interactive/components/tree-list.js +1 -1
  353. package/dist/modes/interactive/components/tree-list.js.map +1 -1
  354. package/dist/modes/interactive/components/tree-render.d.ts.map +1 -1
  355. package/dist/modes/interactive/components/tree-render.js +1 -1
  356. package/dist/modes/interactive/components/tree-render.js.map +1 -1
  357. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  358. package/dist/modes/interactive/components/tree-selector.js +1 -1
  359. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  360. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  361. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  362. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  363. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  364. package/dist/modes/interactive/components/user-message.js +1 -1
  365. package/dist/modes/interactive/components/user-message.js.map +1 -1
  366. package/dist/modes/interactive/components/workflow-view.d.ts.map +1 -1
  367. package/dist/modes/interactive/components/workflow-view.js +1 -1
  368. package/dist/modes/interactive/components/workflow-view.js.map +1 -1
  369. package/dist/modes/interactive/interactive-auth.d.ts +1 -1
  370. package/dist/modes/interactive/interactive-auth.d.ts.map +1 -1
  371. package/dist/modes/interactive/interactive-auth.js +1 -1
  372. package/dist/modes/interactive/interactive-auth.js.map +1 -1
  373. package/dist/modes/interactive/interactive-commands.d.ts +125 -6
  374. package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
  375. package/dist/modes/interactive/interactive-commands.js +698 -4
  376. package/dist/modes/interactive/interactive-commands.js.map +1 -1
  377. package/dist/modes/interactive/interactive-events.d.ts +174 -0
  378. package/dist/modes/interactive/interactive-events.d.ts.map +1 -0
  379. package/dist/modes/interactive/interactive-events.js +704 -0
  380. package/dist/modes/interactive/interactive-events.js.map +1 -0
  381. package/dist/modes/interactive/interactive-format.d.ts.map +1 -1
  382. package/dist/modes/interactive/interactive-format.js +1 -1
  383. package/dist/modes/interactive/interactive-format.js.map +1 -1
  384. package/dist/modes/interactive/interactive-lifecycle.d.ts +94 -0
  385. package/dist/modes/interactive/interactive-lifecycle.d.ts.map +1 -0
  386. package/dist/modes/interactive/interactive-lifecycle.js +177 -0
  387. package/dist/modes/interactive/interactive-lifecycle.js.map +1 -0
  388. package/dist/modes/interactive/interactive-mode.d.ts +254 -129
  389. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  390. package/dist/modes/interactive/interactive-mode.js +275 -2101
  391. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  392. package/dist/modes/interactive/interactive-queues.d.ts +96 -0
  393. package/dist/modes/interactive/interactive-queues.d.ts.map +1 -0
  394. package/dist/modes/interactive/interactive-queues.js +183 -0
  395. package/dist/modes/interactive/interactive-queues.js.map +1 -0
  396. package/dist/modes/interactive/interactive-selectors.d.ts +95 -5
  397. package/dist/modes/interactive/interactive-selectors.d.ts.map +1 -1
  398. package/dist/modes/interactive/interactive-selectors.js +517 -6
  399. package/dist/modes/interactive/interactive-selectors.js.map +1 -1
  400. package/dist/modes/interactive/startup-services.d.ts +31 -0
  401. package/dist/modes/interactive/startup-services.d.ts.map +1 -0
  402. package/dist/modes/interactive/startup-services.js +123 -0
  403. package/dist/modes/interactive/startup-services.js.map +1 -0
  404. package/dist/modes/memory-preheat.d.ts +11 -0
  405. package/dist/modes/memory-preheat.d.ts.map +1 -0
  406. package/dist/modes/memory-preheat.js +20 -0
  407. package/dist/modes/memory-preheat.js.map +1 -0
  408. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  409. package/dist/modes/rpc/rpc-mode.js +3 -1
  410. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  411. package/dist/plugins/ecosystem/cli.d.ts.map +1 -1
  412. package/dist/plugins/ecosystem/cli.js +6 -3
  413. package/dist/plugins/ecosystem/cli.js.map +1 -1
  414. package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts +7 -1
  415. package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
  416. package/dist/plugins/ecosystem/node-plugin-bootstrap.js +50 -3
  417. package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
  418. package/dist/plugins/ecosystem/plugin-bootstrap.d.ts +28 -16
  419. package/dist/plugins/ecosystem/plugin-bootstrap.d.ts.map +1 -1
  420. package/dist/plugins/ecosystem/plugin-bootstrap.js +35 -33
  421. package/dist/plugins/ecosystem/plugin-bootstrap.js.map +1 -1
  422. package/dist/profile/plugin-profiles.d.ts +1 -1
  423. package/dist/profile/plugin-profiles.d.ts.map +1 -1
  424. package/dist/profile/plugin-profiles.js +18 -0
  425. package/dist/profile/plugin-profiles.js.map +1 -1
  426. package/dist/remote/server-assembly.d.ts.map +1 -1
  427. package/dist/remote/server-assembly.js +7 -2
  428. package/dist/remote/server-assembly.js.map +1 -1
  429. package/dist/{profiles → suites}/builtin-suites.d.ts +14 -4
  430. package/dist/suites/builtin-suites.d.ts.map +1 -0
  431. package/dist/{profiles → suites}/builtin-suites.js +20 -5
  432. package/dist/suites/builtin-suites.js.map +1 -0
  433. package/dist/suites/resolved-agent-profile.d.ts.map +1 -0
  434. package/dist/suites/resolved-agent-profile.js.map +1 -0
  435. package/dist/suites/suite-lifecycle.d.ts.map +1 -0
  436. package/dist/suites/suite-lifecycle.js.map +1 -0
  437. package/dist/suites/suite-loader.d.ts.map +1 -0
  438. package/dist/suites/suite-loader.js.map +1 -0
  439. package/dist/{modes/interactive/theme → theme}/dark.json +1 -1
  440. package/dist/{modes/interactive/theme → theme}/light.json +1 -1
  441. package/dist/{modes/interactive/theme → theme}/theme-controller.d.ts +1 -1
  442. package/dist/theme/theme-controller.d.ts.map +1 -0
  443. package/dist/{modes/interactive/theme → theme}/theme-controller.js +1 -1
  444. package/dist/theme/theme-controller.js.map +1 -0
  445. package/dist/{modes/interactive/theme → theme}/theme.d.ts +1 -1
  446. package/dist/theme/theme.d.ts.map +1 -0
  447. package/dist/{modes/interactive/theme → theme}/theme.js +5 -5
  448. package/dist/theme/theme.js.map +1 -0
  449. package/docs/compaction.md +7 -7
  450. package/docs/environment-variables.md +10 -4
  451. package/docs/keybindings.md +3 -3
  452. package/docs/models.md +17 -17
  453. package/docs/packages.md +1 -1
  454. package/docs/prompt-templates.md +1 -1
  455. package/docs/rpc.md +2 -2
  456. package/docs/sdk.md +5 -5
  457. package/docs/session-format.md +3 -3
  458. package/docs/settings.md +368 -354
  459. package/docs/shell-aliases.md +1 -1
  460. package/docs/skills.md +1 -1
  461. package/docs/terminal-setup.md +17 -17
  462. package/docs/themes.md +5 -5
  463. package/docs/tmux.md +4 -4
  464. package/docs/tui.md +1 -1
  465. package/docs/windows.md +1 -1
  466. package/examples/extensions/bash-spawn-hook/index.ts +1 -1
  467. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  468. package/examples/extensions/with-deps/package-lock.json +2 -2
  469. package/examples/extensions/with-deps/package.json +1 -6
  470. package/examples/extensions/with-deps/plugin.json +1 -1
  471. package/package.json +2 -2
  472. package/dist/bundle/chunks/openai-responses-AUJQ3NWT.js +0 -2
  473. package/dist/bundle/chunks/virtual-modules-WF6WRHP7.js +0 -5
  474. package/dist/cli/experimental/commands/pi.d.ts +0 -15
  475. package/dist/cli/experimental/commands/pi.d.ts.map +0 -1
  476. package/dist/cli/experimental/commands/pi.js.map +0 -1
  477. package/dist/core/export-html/ansi-to-html.d.ts.map +0 -1
  478. package/dist/core/export-html/ansi-to-html.js.map +0 -1
  479. package/dist/core/export-html/index.d.ts.map +0 -1
  480. package/dist/core/export-html/index.js.map +0 -1
  481. package/dist/core/export-html/tool-renderer.d.ts.map +0 -1
  482. package/dist/core/export-html/tool-renderer.js.map +0 -1
  483. package/dist/core/pi-manifest.d.ts +0 -7
  484. package/dist/core/pi-manifest.d.ts.map +0 -1
  485. package/dist/core/pi-manifest.js.map +0 -1
  486. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +0 -1
  487. package/dist/modes/interactive/model-catalog-refresh.js.map +0 -1
  488. package/dist/modes/interactive/theme/theme-controller.d.ts.map +0 -1
  489. package/dist/modes/interactive/theme/theme-controller.js.map +0 -1
  490. package/dist/modes/interactive/theme/theme.d.ts.map +0 -1
  491. package/dist/modes/interactive/theme/theme.js.map +0 -1
  492. package/dist/profiles/builtin-suites.d.ts.map +0 -1
  493. package/dist/profiles/builtin-suites.js.map +0 -1
  494. package/dist/profiles/resolved-agent-profile.d.ts.map +0 -1
  495. package/dist/profiles/resolved-agent-profile.js.map +0 -1
  496. package/dist/profiles/suite-lifecycle.d.ts.map +0 -1
  497. package/dist/profiles/suite-lifecycle.js.map +0 -1
  498. package/dist/profiles/suite-loader.d.ts.map +0 -1
  499. package/dist/profiles/suite-loader.js.map +0 -1
  500. package/dist/utils/deprecation.d.ts +0 -4
  501. package/dist/utils/deprecation.d.ts.map +0 -1
  502. package/dist/utils/deprecation.js +0 -13
  503. package/dist/utils/deprecation.js.map +0 -1
  504. /package/dist/{core/export-html → export-html}/ansi-to-html.d.ts +0 -0
  505. /package/dist/{core/export-html → export-html}/ansi-to-html.js +0 -0
  506. /package/dist/{core/export-html → export-html}/template.css +0 -0
  507. /package/dist/{core/export-html → export-html}/template.html +0 -0
  508. /package/dist/{core/export-html → export-html}/template.js +0 -0
  509. /package/dist/{core/export-html → export-html}/tool-renderer.js +0 -0
  510. /package/dist/{core/export-html → export-html}/vendor/highlight.min.js +0 -0
  511. /package/dist/{core/export-html → export-html}/vendor/marked.min.js +0 -0
  512. /package/dist/{profiles → suites}/resolved-agent-profile.d.ts +0 -0
  513. /package/dist/{profiles → suites}/resolved-agent-profile.js +0 -0
  514. /package/dist/{profiles → suites}/suite-lifecycle.d.ts +0 -0
  515. /package/dist/{profiles → suites}/suite-lifecycle.js +0 -0
  516. /package/dist/{profiles → suites}/suite-loader.d.ts +0 -0
  517. /package/dist/{profiles → suites}/suite-loader.js +0 -0
  518. /package/dist/{modes/interactive/theme → theme}/theme-schema.json +0 -0
@@ -8,89 +8,53 @@ import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { contentText } from "@agent-forge/ai";
10
10
  import * as TuiLayouts from "@agent-forge/tui";
11
- import { CombinedAutocompleteProvider, Container, getCapabilities, hyperlink, Markdown, ProcessTerminal, Spacer, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@agent-forge/tui";
12
- import chalk from "chalk";
13
- import { spawn } from "child_process";
14
- import { APP_NAME, APP_TITLE, getAgentDir, getDebugLogPath, VERSION } from "../../config.js";
15
- import { parseSkillBlock } from "../../core/agent-session.js";
16
- import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
17
- import { listAgentSessions, readAgentSession } from "../../core/agent-session-store.js";
18
- import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
19
- import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "../../core/defaults.js";
11
+ import { CombinedAutocompleteProvider, Container, getCapabilities, hyperlink, Markdown, ProcessTerminal, Spacer, setCapabilityOverrides, setKeybindings, Text, TuiAltScreen, TuiMainScreen, } from "@agent-forge/tui";
12
+ import { APP_NAME, APP_TITLE, getAgentDir, VERSION } from "../../config.js";
20
13
  import { createCodingAgentFacade, createCodingAgentRuntimeFacade, } from "../../core/facade/index.js";
21
14
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
22
- import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
15
+ import { configureHttpDispatcher } from "../../core/http-dispatcher.js";
23
16
  import { setI18nLanguage, t } from "../../core/i18n/index.js";
24
17
  import { KeybindingsManager } from "../../core/keybindings.js";
25
- import { createCompactionSummaryMessage } from "../../core/messages.js";
26
- import { DefaultPackageManager } from "../../core/package-manager.js";
27
- import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
28
- import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
18
+ import { formatMissingSessionCwdPrompt } from "../../core/session-cwd.js";
29
19
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
30
- import { getUsageCostBreakdown } from "../../core/usage-totals.js";
31
- import { listSuiteIds, resolveSuiteId, SUITE_ENV_VAR, SUITE_SESSION_ENTRY_TYPE } from "../../profiles/suite-loader.js";
20
+ import { listSuiteIds, resolveSuiteId, SUITE_ENV_VAR, SUITE_SESSION_ENTRY_TYPE } from "../../suites/suite-loader.js";
21
+ import { getEditorTheme, getMarkdownTheme, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "../../theme/theme.js";
22
+ import { InteractiveThemeController } from "../../theme/theme-controller.js";
32
23
  import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
33
24
  import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
34
25
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
35
26
  import { parseGitUrl } from "../../utils/git.js";
36
27
  import { openBrowser } from "../../utils/open-browser.js";
37
- import { killTrackedDetachedChildren } from "../../utils/shell.js";
38
28
  import { loadAllHighlightLanguages } from "../../utils/syntax-highlight.js";
39
29
  import { ensureTool } from "../../utils/tools-manager.js";
40
- import { checkForNewVersion } from "../../utils/version-check.js";
41
- import { AgentsViewComponent } from "./components/agents-view.js";
42
- import { ArminComponent } from "./components/armin.js";
30
+ import { kickMemoryModelPreheat } from "../memory-preheat.js";
43
31
  import { AssistantMessageComponent } from "./components/assistant-message.js";
44
- import { BashExecutionComponent } from "./components/bash-execution.js";
45
- import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
46
- import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
47
32
  import { CustomEditor } from "./components/custom-editor.js";
48
- import { CustomMessageComponent } from "./components/custom-message.js";
49
- import { DaxnutsComponent } from "./components/daxnuts.js";
50
33
  import { DynamicBorder } from "./components/dynamic-border.js";
51
- import { FooterComponent, formatTokens } from "./components/footer.js";
52
- import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
34
+ import { FooterComponent } from "./components/footer.js";
35
+ import { formatKeyText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
53
36
  import { createMermaidMarkdownTransformer } from "./components/mermaid.js";
54
- import { readSessionSuiteIdFromFile, SessionSelectorComponent } from "./components/session-selector.js";
55
- import { SettingsSelectorComponent } from "./components/settings-selector.js";
56
- import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
57
- import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.js";
58
- import { ToolExecutionComponent } from "./components/tool-execution.js";
59
- import { TreeSelectorComponent } from "./components/tree-selector.js";
60
- import { UserMessageComponent } from "./components/user-message.js";
61
- import { WorkflowViewComponent } from "./components/workflow-view.js";
37
+ import { readSessionSuiteIdFromFile } from "./components/session-selector.js";
38
+ import { IdleStatus, WorkingStatusIndicator, } from "./components/status-indicator.js";
62
39
  import { editInExternalEditor } from "./external-editor.js";
63
40
  import * as authFlow from "./interactive-auth.js";
64
- import { COMMAND_DISPATCH_ENTRIES, filterCapabilityCommandsForCompletions, matchCommandDispatchEntry, } from "./interactive-commands.js";
41
+ import { buildRegisteredThemes, COMMAND_DISPATCH_ENTRIES, checkDaxnutsEasterEgg, filterCapabilityCommandsForCompletions, getAppKeyDisplay, getEditorKeyDisplay, getPathCommandArgument, handleArminSaysHi, handleBashCommand, handleChangelogCommand, handleCleanSessionsCommand, handleClearCommand, handleCompactCommand, handleCopyCommand, handleDaxnuts, handleDebugCommand, handleExportCommand, handleHelpCommand, handleHotkeysCommand, handleImportCommand, handleMetricsCommand, handleNameCommand, handleReloadCommand, handleSessionCommand, handleStopAgentsCommand, matchCommandDispatchEntry, } from "./interactive-commands.js";
65
42
  import { createInteractionHostV1, hideDialogEditor, hideDialogInput, hideDialogSelector, isExpandable, showDialogConfirm, showDialogEditor, showDialogInput, showDialogNotify, showDialogSelector, } from "./interactive-dialog-ui.js";
66
- import { buildScopeGroups, findSourceInfoForPath, formatContextPath, formatDiagnostics, formatDisplayPath, formatMetricsSnapshot, formatPathWithSource, formatScopeGroups, getCompactPackageSourceLabel, getCompactPathLabel, getDisplaySourceInfo, getScopeGroup, getShortPath, isPackageSource, } from "./interactive-format.js";
67
- import { createFuzzyAutocompleteItems, formatLoginProviderCompletionDescription, getLoginProviderCompletionOptions, getLoginProviderSearchText, isAnthropicSubscriptionAuthKey, isDeadTerminalError, quoteIfNeeded, } from "./interactive-helpers.js";
43
+ import * as eventsFlow from "./interactive-events.js";
44
+ import { buildScopeGroups, findSourceInfoForPath, formatContextPath, formatDiagnostics, formatDisplayPath, formatPathWithSource, formatScopeGroups, getCompactPackageSourceLabel, getCompactPathLabel, getDisplaySourceInfo, getScopeGroup, getShortPath, isPackageSource, } from "./interactive-format.js";
45
+ import { createFuzzyAutocompleteItems, formatLoginProviderCompletionDescription, getLoginProviderCompletionOptions, getLoginProviderSearchText, isAnthropicSubscriptionAuthKey, quoteIfNeeded, } from "./interactive-helpers.js";
46
+ import * as lifecycleFlow from "./interactive-lifecycle.js";
47
+ import * as queuesFlow from "./interactive-queues.js";
68
48
  import * as selectorsFlow from "./interactive-selectors.js";
69
- import { refreshModelCatalogs } from "./model-catalog-refresh.js";
70
49
  import { getModelSearchText } from "./model-search.js";
71
- import { getAvailableThemes, getEditorTheme, getMarkdownTheme, loadThemeFromPath, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
72
- import { InteractiveThemeController } from "./theme/theme-controller.js";
50
+ import { startStartupBackgroundChecks } from "./startup-services.js";
51
+ // Re-exported for existing consumers of the interactive mode module; the
52
+ // implementation moved to ./interactive-commands.ts with the command handlers
53
+ // (拆分 slice 1).
54
+ export { buildRegisteredThemes } from "./interactive-commands.js";
73
55
  // Re-exported for existing consumers of the interactive mode module; the
74
56
  // implementation moved to ./interactive-dialog-ui.ts with the extension UI block.
75
57
  export { mapLegacyDialogResultToOutcome } from "./interactive-dialog-ui.js";
76
- /**
77
- * Rebuilds real Theme instances from the resource loader's ThemeMetaV1
78
- * projection for the theme registry (B-S2b v1). Registration points
79
- * (constructor, runtime session start, reload) need loadable instances —
80
- * setRegisteredThemes stores what it is given and getThemeByName returns the
81
- * registered instance first — while the loader surface now projects only
82
- * metadata. Re-loading is synchronous and deterministic per sourcePath;
83
- * theme names come from the same JSON the loader parsed, so meta.name ===
84
- * Theme.name and the registry keeps its own name-filter/dedupe semantics.
85
- *
86
- * Contract: a meta without a sourcePath (or one whose file no longer parses)
87
- * throws — themesOverride host inputs must always carry a loadable
88
- * sourcePath. This adds one extra parse per theme at registration time
89
- * (constructor / session start / reload, not per turn).
90
- */
91
- export function buildRegisteredThemes(themes) {
92
- return themes.map((meta) => loadThemeFromPath(meta.sourcePath));
93
- }
94
58
  class ExpandableText extends Text {
95
59
  getCollapsedText;
96
60
  getExpandedText;
@@ -103,12 +67,6 @@ class ExpandableText extends Text {
103
67
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
104
68
  }
105
69
  }
106
- function isCustomSessionEntry(item) {
107
- return "type" in item && item.type === "custom";
108
- }
109
- function isCompactionCostNotice(item) {
110
- return "type" in item && item.type === "compaction_cost";
111
- }
112
70
  // Empty-reply auto-continuation (#5, print-mode parity): sent at most once per
113
71
  // submitted turn when the turn ends with an empty assistant reply and the
114
72
  // model was neither aborted nor errored. The one-shot budget prevents loops.
@@ -230,6 +188,7 @@ export class ProviderFailoverTicker {
230
188
  }
231
189
  export class InteractiveMode {
232
190
  runtimeHost;
191
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
233
192
  renderer;
234
193
  /** Host-visible for ./interactive-dialog-ui.ts (S5 slice 2). */
235
194
  ui;
@@ -240,7 +199,9 @@ export class InteractiveMode {
240
199
  documentContainer;
241
200
  transcriptScrollView;
242
201
  fullscreenLayoutRoot;
202
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
243
203
  pendingMessagesContainer;
204
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
244
205
  statusContainer;
245
206
  /** Host-visible for ./interactive-dialog-ui.ts (S5 slice 2). */
246
207
  defaultEditor;
@@ -261,6 +222,8 @@ export class InteractiveMode {
261
222
  * Re-renders the footer once per second while the provider failover breaker is
262
223
  * open (§4.7 Phase 3). Optional-typed to match the prototype-based test stubs
263
224
  * (constructor always assigns it on real instances).
225
+ *
226
+ * Host-visible for ./interactive-events.ts (拆分 slice 5): per-event refresh.
264
227
  */
265
228
  failoverTicker;
266
229
  /** Host-visible for ./interactive-dialog-ui.ts (S5 slice 2). */
@@ -271,17 +234,22 @@ export class InteractiveMode {
271
234
  /** Host-visible for ./interactive-dialog-ui.ts (S5 slice 2). */
272
235
  keybindings;
273
236
  version;
237
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
274
238
  isInitialized = false;
275
239
  onInputCallback;
276
240
  pendingUserInputs = [];
241
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
277
242
  activeStatusIndicator = undefined;
278
243
  idleStatus = new IdleStatus();
279
244
  workingMessage = undefined;
245
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
280
246
  workingVisible = true;
281
247
  workingIndicatorOptions = undefined;
282
248
  defaultWorkingMessage = t("Working...");
283
249
  defaultHiddenThinkingLabel = t("Thinking...");
250
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
284
251
  hiddenThinkingLabel = this.defaultHiddenThinkingLabel;
252
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4): double-Ctrl+C exit window. */
285
253
  lastSigintTime = 0;
286
254
  lastEscapeTime = 0;
287
255
  changelogMarkdown = undefined;
@@ -292,37 +260,57 @@ export class InteractiveMode {
292
260
  lastStatusText = undefined;
293
261
  managedToolStatusStarted = false;
294
262
  // Streaming message tracking
263
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
295
264
  streamingComponent = undefined;
265
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
296
266
  streamingMessage = undefined;
297
267
  // Tool execution tracking: toolCallId -> component
268
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
298
269
  pendingTools = new Map();
270
+ // D-083 quiet transcript: the tool-run group for the current assistant
271
+ // turn (one group per message_start; null between turns).
272
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
273
+ activeToolRunGroup = undefined;
299
274
  // Tool output expansion state
300
275
  /** Host-visible for ./interactive-dialog-ui.ts (S5 slice 2). */
301
276
  toolOutputExpanded = false;
302
277
  // Thinking block visibility state
278
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
303
279
  hideThinkingBlock = false;
280
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
304
281
  outputPad = 1;
282
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
305
283
  mermaidMarkdownTransformer = createMermaidMarkdownTransformer({
306
284
  getMode: () => this.settingsManager.getMermaidRenderingMode(),
307
285
  theme,
308
286
  });
309
287
  // Agent subscription unsubscribe function
288
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
310
289
  unsubscribe;
290
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4). */
311
291
  signalCleanupHandlers = [];
312
292
  // Track if editor is in bash mode (text starts with !)
313
293
  isBashMode = false;
314
294
  // Track current bash execution component
295
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
315
296
  bashComponent = undefined;
316
297
  // Track pending bash components (shown in pending area, moved to chat on submit)
298
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
317
299
  pendingBashComponents = [];
318
300
  // Auto-compaction state
301
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
319
302
  autoCompactionEscapeHandler;
320
303
  // Auto-retry state
304
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
321
305
  retryEscapeHandler;
322
306
  // Messages queued while compaction is running
307
+ /** Host-visible for ./interactive-queues.ts (拆分 slice 3). */
323
308
  compactionQueuedMessages = [];
324
309
  // Shutdown state
310
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4). */
325
311
  shutdownRequested = false;
312
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4): reentrancy guard. */
313
+ isShuttingDown = false;
326
314
  // Extension UI state: host-visible for ./interactive-dialog-ui.ts (S5 slice 2).
327
315
  dialogSelector = undefined;
328
316
  dialogInput = undefined;
@@ -338,12 +326,15 @@ export class InteractiveMode {
338
326
  headerContainer;
339
327
  // Built-in header (logo + keybinding hints + changelog)
340
328
  builtInHeader = undefined;
329
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
341
330
  options;
342
331
  onRightClickPaste = () => {
343
332
  void this.handleRightClickPaste();
344
333
  };
334
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
345
335
  themeController;
346
336
  // Convenience accessors
337
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2): only the curated AgentSessionHostSurface (setTransport) is consumed there. */
347
338
  get session() {
348
339
  return this.runtimeHost.session;
349
340
  }
@@ -615,6 +606,7 @@ export class InteractiveMode {
615
606
  }
616
607
  this.ui.stop({ preserveScreen: this.renderer.mode === "fullscreen" });
617
608
  }
609
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
618
610
  switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
619
611
  const previousUi = this.renderer;
620
612
  if (mode === previousUi.mode)
@@ -832,46 +824,28 @@ export class InteractiveMode {
832
824
  this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
833
825
  }
834
826
  }
827
+ /** Host-visible for ./startup-services.ts. */
828
+ getVersion() {
829
+ return this.version;
830
+ }
831
+ /** Narrow cwd projection for startup background checks (D-048). */
832
+ getCwd() {
833
+ return this.facade.getCwd();
834
+ }
835
+ /** Live initialization read for late-arriving background checks. */
836
+ hasInitialized() {
837
+ return this.isInitialized;
838
+ }
835
839
  /**
836
840
  * Run the interactive mode. This is the main entry point.
837
841
  * Initializes the UI, shows warnings, processes initial messages, and starts the interactive loop.
838
842
  */
839
843
  async run() {
840
844
  await this.init();
841
- if (!process.env.AGENT_FORGE_OFFLINE) {
842
- const controller = new AbortController();
843
- const timeout = setTimeout(() => controller.abort(), 15_000);
844
- void refreshModelCatalogs(this.facade.getModelCatalog(), controller.signal)
845
- .then(() => this.updateAvailableProviderCount())
846
- .catch(() => { })
847
- .finally(() => clearTimeout(timeout));
848
- }
849
- // Start version check asynchronously
850
- checkForNewVersion(this.version).then((newRelease) => {
851
- if (newRelease) {
852
- this.showNewVersionNotification(newRelease);
853
- }
854
- });
855
- // Start package update check asynchronously
856
- this.checkForPackageUpdates()
857
- .then((updates) => {
858
- if (updates.length > 0) {
859
- this.showPackageUpdateNotification(updates);
860
- }
861
- })
862
- .finally(() => {
863
- // On Windows, npm can overwrite the shared console title while checking
864
- // extension package versions. Restore the product title after the startup check.
865
- if (process.platform === "win32" && this.isInitialized) {
866
- this.updateTerminalTitle();
867
- }
868
- });
869
- // Check tmux keyboard setup asynchronously
870
- this.checkTmuxKeyboardSetup().then((warning) => {
871
- if (warning) {
872
- this.showWarning(warning);
873
- }
874
- });
845
+ // Fire-and-forget startup background checks (catalog refresh, version
846
+ // check, package updates, tmux probe); scheduling lives in
847
+ // ./startup-services.ts.
848
+ startStartupBackgroundChecks(this);
875
849
  // Show startup warnings
876
850
  const { migratedProviders, startupDiagnostics, modelFallbackMessage, initialMessage, initialImages, initialMessages, } = this.options;
877
851
  for (const diagnostic of startupDiagnostics ?? []) {
@@ -958,64 +932,6 @@ export class InteractiveMode {
958
932
  this.showStatus(t("Previous reply was empty, retrying..."));
959
933
  await this.facade.prompt(EMPTY_REPLY_RETRY_PROMPT);
960
934
  }
961
- async checkForPackageUpdates() {
962
- if (process.env.AGENT_FORGE_OFFLINE) {
963
- return [];
964
- }
965
- try {
966
- const packageManager = new DefaultPackageManager({
967
- cwd: this.facade.getCwd(),
968
- agentDir: getAgentDir(),
969
- settingsManager: this.settingsManager,
970
- });
971
- const updates = await packageManager.checkForAvailableUpdates();
972
- return updates.map((update) => update.displayName);
973
- }
974
- catch {
975
- return [];
976
- }
977
- }
978
- async checkTmuxKeyboardSetup() {
979
- if (!process.env.TMUX)
980
- return undefined;
981
- const runTmuxShow = (option) => {
982
- return new Promise((resolve) => {
983
- const proc = spawn("tmux", ["show", "-gv", option], {
984
- stdio: ["ignore", "pipe", "ignore"],
985
- });
986
- let stdout = "";
987
- const timer = setTimeout(() => {
988
- proc.kill();
989
- resolve(undefined);
990
- }, 2000);
991
- proc.stdout?.on("data", (data) => {
992
- stdout += data.toString();
993
- });
994
- proc.on("error", () => {
995
- clearTimeout(timer);
996
- resolve(undefined);
997
- });
998
- proc.on("close", (code) => {
999
- clearTimeout(timer);
1000
- resolve(code === 0 ? stdout.trim() : undefined);
1001
- });
1002
- });
1003
- };
1004
- const [extendedKeys, extendedKeysFormat] = await Promise.all([
1005
- runTmuxShow("extended-keys"),
1006
- runTmuxShow("extended-keys-format"),
1007
- ]);
1008
- // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
1009
- if (extendedKeys === undefined)
1010
- return undefined;
1011
- if (extendedKeys !== "on" && extendedKeys !== "always") {
1012
- return t("tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.");
1013
- }
1014
- if (extendedKeysFormat === "xterm") {
1015
- return t("tmux extended-keys-format is xterm. Agent Forge works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.");
1016
- }
1017
- return undefined;
1018
- }
1019
935
  /**
1020
936
  * Get changelog entries to display on startup.
1021
937
  * Only shows new entries since last seen version, skips for resumed sessions.
@@ -1040,6 +956,7 @@ export class InteractiveMode {
1040
956
  }
1041
957
  return undefined;
1042
958
  }
959
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1043
960
  getMarkdownThemeWithSettings() {
1044
961
  return {
1045
962
  ...getMarkdownTheme(),
@@ -1097,6 +1014,7 @@ export class InteractiveMode {
1097
1014
  formatDiagnostics(diagnostics, sourceInfos) {
1098
1015
  return formatDiagnostics(diagnostics, sourceInfos);
1099
1016
  }
1017
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1100
1018
  showLoadedResources(options) {
1101
1019
  // Resource rendering is idempotent; chat clears no longer clear this separate container.
1102
1020
  this.loadedResourcesContainer.clear();
@@ -1226,14 +1144,17 @@ export class InteractiveMode {
1226
1144
  */
1227
1145
  async bindCurrentSessionExtensions() {
1228
1146
  await this.session.startSession();
1147
+ kickMemoryModelPreheat(this.session);
1229
1148
  setRegisteredThemes(buildRegisteredThemes(this.facade.getThemes().themes));
1230
1149
  this.setupAutocompleteProvider();
1231
1150
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1232
1151
  this.showStartupNoticesIfNeeded();
1233
1152
  }
1153
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
1234
1154
  applyFullscreenScrollbarSetting() {
1235
1155
  this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1236
1156
  }
1157
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1237
1158
  applyRuntimeSettings() {
1238
1159
  setI18nLanguage(this.settingsManager.getUiLanguage());
1239
1160
  setCapabilityOverrides(this.settingsManager.getTerminalCapabilityOverrides());
@@ -1285,6 +1206,7 @@ export class InteractiveMode {
1285
1206
  this.updateEditorBorderColor();
1286
1207
  this.updateTerminalTitle();
1287
1208
  }
1209
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1288
1210
  async handleFatalRuntimeError(prefix, error) {
1289
1211
  const message = error instanceof Error ? error.message : String(error);
1290
1212
  this.showError(`${prefix}: ${message}`);
@@ -1304,19 +1226,24 @@ export class InteractiveMode {
1304
1226
  }
1305
1227
  /**
1306
1228
  * Get a registered tool definition by name (for custom rendering).
1229
+ *
1230
+ * Host-visible for ./interactive-events.ts (拆分 slice 5).
1307
1231
  */
1308
1232
  getRegisteredToolDefinition(toolName) {
1309
- return this.facade.getToolDefinition(toolName);
1233
+ return eventsFlow.getRegisteredToolDefinition(this, toolName);
1310
1234
  }
1235
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5). */
1311
1236
  getMarkdownTransformers() {
1312
- return [this.mermaidMarkdownTransformer];
1237
+ return eventsFlow.getMarkdownTransformers(this);
1313
1238
  }
1239
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
1314
1240
  showStatusIndicator(indicator) {
1315
1241
  this.activeStatusIndicator?.dispose();
1316
1242
  this.activeStatusIndicator = indicator;
1317
1243
  this.statusContainer.clear();
1318
1244
  this.statusContainer.addChild(indicator);
1319
1245
  }
1246
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1320
1247
  clearStatusIndicator(kind) {
1321
1248
  if (kind && this.activeStatusIndicator?.kind !== kind) {
1322
1249
  return;
@@ -1329,6 +1256,7 @@ export class InteractiveMode {
1329
1256
  this.statusContainer.addChild(this.idleStatus);
1330
1257
  }
1331
1258
  }
1259
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5): turn_start working indicator. */
1332
1260
  showWorkingStatusIndicator() {
1333
1261
  this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
1334
1262
  }
@@ -1373,6 +1301,7 @@ export class InteractiveMode {
1373
1301
  this.activeStatusIndicator.setMessage(message ?? this.defaultWorkingMessage);
1374
1302
  }
1375
1303
  }
1304
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1376
1305
  resetDialogUI() {
1377
1306
  if (this.dialogSelector) {
1378
1307
  this.hideDialogSelector();
@@ -1438,6 +1367,7 @@ export class InteractiveMode {
1438
1367
  async showDialogConfirm(title, message, opts) {
1439
1368
  return showDialogConfirm(this, title, message, opts);
1440
1369
  }
1370
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
1441
1371
  async promptForMissingSessionCwd(error) {
1442
1372
  const confirmed = await this.showDialogConfirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
1443
1373
  return confirmed ? error.issue.fallbackCwd : undefined;
@@ -1719,334 +1649,24 @@ export class InteractiveMode {
1719
1649
  setEditorText(text) {
1720
1650
  this.editor.setText(text);
1721
1651
  }
1652
+ // =========================================================================
1653
+ // Agent event bridge and transcript rendering — implementation in
1654
+ // ./interactive-events.ts (拆分 slice 5) behind the EventsHost interface;
1655
+ // same-name delegates keep the rebind/subscribe flow and prototype-level
1656
+ // tests bound to these names. showManagedToolStatus, showStatus, and
1657
+ // getUserInput stay on the mode (startup/footer/main-loop faces).
1658
+ // =========================================================================
1659
+ /** Session subscription; implementation in ./interactive-events.ts (拆分 slice 5). */
1722
1660
  subscribeToAgent() {
1723
- this.unsubscribe = this.facade.subscribe(async (event) => {
1724
- await this.handleEvent(event);
1725
- });
1661
+ eventsFlow.subscribeToAgent(this);
1726
1662
  }
1663
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
1727
1664
  async handleEvent(event) {
1728
- if (!this.isInitialized) {
1729
- await this.init();
1730
- }
1731
- this.footer.invalidate();
1732
- // Failover snapshots land as entry_appended events; the ticker decides here
1733
- // whether the per-second countdown re-render is needed (§4.7 Phase 3).
1734
- this.failoverTicker?.refresh();
1735
- switch (event.type) {
1736
- case "agent_start":
1737
- this.pendingTools.clear();
1738
- // Restore main escape handler if retry handler is still active
1739
- // (retry success event fires later, but we need main handler now)
1740
- if (this.retryEscapeHandler) {
1741
- this.defaultEditor.onEscape = this.retryEscapeHandler;
1742
- this.retryEscapeHandler = undefined;
1743
- }
1744
- break;
1745
- case "turn_start":
1746
- if (this.settingsManager.getShowTerminalProgress()) {
1747
- this.ui.terminal.setProgress(true);
1748
- }
1749
- if (this.workingVisible) {
1750
- if (this.activeStatusIndicator?.kind !== "working") {
1751
- this.showWorkingStatusIndicator();
1752
- }
1753
- }
1754
- else {
1755
- this.clearStatusIndicator();
1756
- }
1757
- this.ui.requestRender();
1758
- break;
1759
- case "queue_update":
1760
- this.updatePendingMessagesDisplay();
1761
- this.ui.requestRender();
1762
- break;
1763
- case "entry_appended":
1764
- // Custom entries have no renderer source since the legacy extension
1765
- // surface was removed; nothing to append to the chat.
1766
- break;
1767
- case "session_info_changed":
1768
- this.updateTerminalTitle();
1769
- this.footer.invalidate();
1770
- this.ui.requestRender();
1771
- break;
1772
- case "thinking_level_changed":
1773
- this.footer.invalidate();
1774
- this.updateEditorBorderColor();
1775
- break;
1776
- case "message_start":
1777
- if (event.message.role === "custom") {
1778
- this.addMessageToChat(event.message);
1779
- this.ui.requestRender();
1780
- }
1781
- else if (event.message.role === "user") {
1782
- this.addMessageToChat(event.message);
1783
- this.updatePendingMessagesDisplay();
1784
- this.ui.requestRender();
1785
- }
1786
- else if (event.message.role === "assistant") {
1787
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
1788
- this.streamingMessage = event.message;
1789
- this.chatContainer.addChild(this.streamingComponent);
1790
- this.streamingComponent.updateContent(this.streamingMessage, true);
1791
- this.ui.requestRender();
1792
- }
1793
- break;
1794
- case "message_update":
1795
- if (this.streamingComponent && event.message.role === "assistant") {
1796
- this.streamingMessage = event.message;
1797
- this.streamingComponent.updateContent(this.streamingMessage, true);
1798
- for (const content of this.streamingMessage.content) {
1799
- if (content.type === "toolCall") {
1800
- if (!this.pendingTools.has(content.id)) {
1801
- const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
1802
- showImages: this.settingsManager.getShowImages(),
1803
- imageWidthCells: this.settingsManager.getImageWidthCells(),
1804
- }, this.getRegisteredToolDefinition(content.name), this.ui, this.facade.getCwd());
1805
- component.setExpanded(this.toolOutputExpanded);
1806
- this.chatContainer.addChild(component);
1807
- this.pendingTools.set(content.id, component);
1808
- }
1809
- else {
1810
- const component = this.pendingTools.get(content.id);
1811
- // v8 ignore next -- has() 已确认键存在,get() 恒返回组件
1812
- if (component) {
1813
- component.updateArgs(content.arguments);
1814
- }
1815
- }
1816
- }
1817
- }
1818
- this.ui.requestRender();
1819
- }
1820
- break;
1821
- case "message_end":
1822
- if (event.message.role === "user")
1823
- break;
1824
- if (this.streamingComponent && event.message.role === "assistant") {
1825
- this.streamingMessage = event.message;
1826
- let errorMessage;
1827
- if (this.streamingMessage.stopReason === "aborted") {
1828
- const retryAttempt = this.facade.getRetryAttempt();
1829
- errorMessage =
1830
- retryAttempt > 0
1831
- ? retryAttempt === 1
1832
- ? t("Aborted after {count} retry attempt", { count: retryAttempt })
1833
- : t("Aborted after {count} retry attempts", { count: retryAttempt })
1834
- : t("Operation aborted");
1835
- this.streamingMessage.errorMessage = errorMessage;
1836
- }
1837
- this.streamingComponent.updateContent(this.streamingMessage, false);
1838
- if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
1839
- if (!errorMessage) {
1840
- errorMessage = this.streamingMessage.errorMessage || t("Error");
1841
- }
1842
- for (const [, component] of this.pendingTools.entries()) {
1843
- component.updateResult({
1844
- content: [{ type: "text", text: errorMessage }],
1845
- isError: true,
1846
- });
1847
- }
1848
- this.pendingTools.clear();
1849
- }
1850
- else {
1851
- // Args are now complete - trigger diff computation for edit tools
1852
- for (const [, component] of this.pendingTools.entries()) {
1853
- component.setArgsComplete();
1854
- }
1855
- this.maybeShowThinkingDropNotice(this.streamingMessage);
1856
- this.maybeShowCacheMissNotice(this.streamingMessage);
1857
- }
1858
- this.streamingComponent = undefined;
1859
- this.streamingMessage = undefined;
1860
- this.footer.invalidate();
1861
- }
1862
- this.ui.requestRender();
1863
- break;
1864
- case "bash_execution_update":
1865
- // The bash execution callback handles TUI output rendering.
1866
- break;
1867
- case "tool_execution_start": {
1868
- let component = this.pendingTools.get(event.toolCallId);
1869
- if (!component) {
1870
- component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
1871
- showImages: this.settingsManager.getShowImages(),
1872
- imageWidthCells: this.settingsManager.getImageWidthCells(),
1873
- }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.facade.getCwd());
1874
- component.setExpanded(this.toolOutputExpanded);
1875
- this.chatContainer.addChild(component);
1876
- this.pendingTools.set(event.toolCallId, component);
1877
- }
1878
- component.markExecutionStarted();
1879
- this.ui.requestRender();
1880
- break;
1881
- }
1882
- case "tool_execution_update": {
1883
- const component = this.pendingTools.get(event.toolCallId);
1884
- if (component) {
1885
- component.updateResult({ ...event.partialResult, isError: false }, true);
1886
- this.ui.requestRender();
1887
- }
1888
- break;
1889
- }
1890
- case "tool_execution_end": {
1891
- const component = this.pendingTools.get(event.toolCallId);
1892
- if (component) {
1893
- component.updateResult({ ...event.result, isError: event.isError });
1894
- this.pendingTools.delete(event.toolCallId);
1895
- this.ui.requestRender();
1896
- }
1897
- break;
1898
- }
1899
- case "capability_command_result": {
1900
- const links = event.result.links?.map((link) => `${link.label}: ${link.url}`) ?? [];
1901
- const message = [event.result.message, ...links].filter(Boolean).join("\n");
1902
- if (event.result.status === "error")
1903
- this.showError(message);
1904
- else
1905
- this.showStatus(message);
1906
- this.ui.requestRender();
1907
- break;
1908
- }
1909
- case "agent_end":
1910
- if (this.settingsManager.getShowTerminalProgress()) {
1911
- this.ui.terminal.setProgress(false);
1912
- }
1913
- this.clearStatusIndicator("working");
1914
- if (this.streamingComponent) {
1915
- this.chatContainer.removeChild(this.streamingComponent);
1916
- this.streamingComponent = undefined;
1917
- this.streamingMessage = undefined;
1918
- }
1919
- this.pendingTools.clear();
1920
- this.ui.requestRender();
1921
- break;
1922
- case "agent_settled":
1923
- await this.checkShutdownRequested();
1924
- break;
1925
- case "compaction_start": {
1926
- if (this.settingsManager.getShowTerminalProgress()) {
1927
- this.ui.terminal.setProgress(true);
1928
- }
1929
- // Keep editor active; submissions are queued during compaction.
1930
- this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
1931
- this.defaultEditor.onEscape = () => {
1932
- this.facade.abortCompaction();
1933
- };
1934
- this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
1935
- this.ui.requestRender();
1936
- break;
1937
- }
1938
- case "compaction_end": {
1939
- if (this.settingsManager.getShowTerminalProgress()) {
1940
- this.ui.terminal.setProgress(false);
1941
- }
1942
- if (this.autoCompactionEscapeHandler) {
1943
- this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
1944
- this.autoCompactionEscapeHandler = undefined;
1945
- }
1946
- this.clearStatusIndicator("compaction");
1947
- if (event.aborted) {
1948
- if (event.reason === "manual") {
1949
- this.showError(t("Compaction cancelled"));
1950
- }
1951
- else {
1952
- this.showStatus(t("Auto-compaction cancelled"));
1953
- }
1954
- }
1955
- else if (event.result) {
1956
- const entries = this.facade.getContextEntries();
1957
- if (entries[0]?.type !== "compaction") {
1958
- throw new Error(t("Completed compaction is missing from the session context"));
1959
- }
1960
- this.chatContainer.clear();
1961
- // The latest compaction is prepended for model context; append it below at its chronological position.
1962
- this.renderSessionEntries(entries.slice(1));
1963
- this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
1964
- if (event.result.usage) {
1965
- this.addCompactionCostNotice({
1966
- type: "compaction_cost",
1967
- kind: "compaction",
1968
- usage: event.result.usage,
1969
- });
1970
- }
1971
- this.footer.invalidate();
1972
- }
1973
- else if (event.errorMessage) {
1974
- if (event.reason === "manual") {
1975
- this.showError(event.errorMessage);
1976
- }
1977
- else {
1978
- this.chatContainer.addChild(new Spacer(1));
1979
- this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
1980
- }
1981
- }
1982
- void this.flushCompactionQueue({ willRetry: event.willRetry });
1983
- this.ui.requestRender();
1984
- break;
1985
- }
1986
- case "compaction_skipped": {
1987
- this.showStatus(t("Auto-compaction skipped: nothing to compact"));
1988
- this.ui.requestRender();
1989
- break;
1990
- }
1991
- case "auto_retry_start": {
1992
- // Set up escape to abort retry
1993
- this.retryEscapeHandler = this.defaultEditor.onEscape;
1994
- this.defaultEditor.onEscape = () => {
1995
- this.facade.abortRetry();
1996
- };
1997
- this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
1998
- this.ui.requestRender();
1999
- break;
2000
- }
2001
- case "auto_retry_end": {
2002
- // Restore escape handler
2003
- if (this.retryEscapeHandler) {
2004
- this.defaultEditor.onEscape = this.retryEscapeHandler;
2005
- this.retryEscapeHandler = undefined;
2006
- }
2007
- this.clearStatusIndicator("retry");
2008
- // Show error only on final failure (success shows normal response)
2009
- if (!event.success) {
2010
- this.showError(t("Retry failed after {count} attempts: {error}", {
2011
- count: event.attempt,
2012
- error: event.finalError || t("Unknown error"),
2013
- }));
2014
- }
2015
- this.ui.requestRender();
2016
- break;
2017
- }
2018
- case "summarization_retry_scheduled": {
2019
- this.showError(event.errorMessage);
2020
- this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2021
- this.ui.requestRender();
2022
- break;
2023
- }
2024
- case "summarization_retry_attempt_start": {
2025
- this.clearStatusIndicator("retry");
2026
- if (event.source === "branchSummary") {
2027
- this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
2028
- }
2029
- else {
2030
- this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2031
- }
2032
- this.ui.requestRender();
2033
- break;
2034
- }
2035
- case "summarization_retry_finished": {
2036
- this.clearStatusIndicator("retry");
2037
- this.ui.requestRender();
2038
- break;
2039
- }
2040
- }
1665
+ return eventsFlow.handleEvent(this, event);
2041
1666
  }
2042
- /** Extract text content from a user message */
1667
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
2043
1668
  getUserMessageText(message) {
2044
- if (message.role !== "user")
2045
- return "";
2046
- const textBlocks = typeof message.content === "string"
2047
- ? [{ type: "text", text: message.content }]
2048
- : message.content.filter((c) => c.type === "text");
2049
- return textBlocks.map((c) => c.text).join("");
1669
+ return eventsFlow.getUserMessageText(message);
2050
1670
  }
2051
1671
  /** Show a managed-tool status update in the chat. */
2052
1672
  showManagedToolStatus(status) {
@@ -2084,276 +1704,57 @@ export class InteractiveMode {
2084
1704
  this.lastStatusText = text;
2085
1705
  this.ui.requestRender();
2086
1706
  }
1707
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
2087
1708
  addMessageToChat(message, options) {
2088
- switch (message.role) {
2089
- case "bashExecution": {
2090
- const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
2091
- if (message.output) {
2092
- component.appendOutput(message.output);
2093
- }
2094
- component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
2095
- this.chatContainer.addChild(component);
2096
- break;
2097
- }
2098
- case "custom": {
2099
- if (message.display) {
2100
- const renderer = undefined;
2101
- const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings(), this.outputPad);
2102
- component.setExpanded(this.toolOutputExpanded);
2103
- this.chatContainer.addChild(component);
2104
- }
2105
- break;
2106
- }
2107
- case "compactionSummary": {
2108
- this.chatContainer.addChild(new Spacer(1));
2109
- const component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
2110
- component.setExpanded(this.toolOutputExpanded);
2111
- this.chatContainer.addChild(component);
2112
- break;
2113
- }
2114
- case "branchSummary": {
2115
- this.chatContainer.addChild(new Spacer(1));
2116
- const component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
2117
- component.setExpanded(this.toolOutputExpanded);
2118
- this.chatContainer.addChild(component);
2119
- break;
2120
- }
2121
- case "user": {
2122
- const textContent = this.getUserMessageText(message);
2123
- if (textContent) {
2124
- if (this.chatContainer.children.length > 0) {
2125
- this.chatContainer.addChild(new Spacer(1));
2126
- }
2127
- const skillBlock = parseSkillBlock(textContent);
2128
- if (skillBlock) {
2129
- // Render skill block (collapsible)
2130
- const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings());
2131
- component.setExpanded(this.toolOutputExpanded);
2132
- this.chatContainer.addChild(component);
2133
- // Render user message separately if present
2134
- if (skillBlock.userMessage) {
2135
- this.chatContainer.addChild(new Spacer(1));
2136
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2137
- this.chatContainer.addChild(userComponent);
2138
- }
2139
- }
2140
- else {
2141
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2142
- this.chatContainer.addChild(userComponent);
2143
- }
2144
- if (options?.populateHistory) {
2145
- this.editor.addToHistory?.(textContent);
2146
- }
2147
- }
2148
- break;
2149
- }
2150
- case "assistant": {
2151
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2152
- this.chatContainer.addChild(assistantComponent);
2153
- break;
2154
- }
2155
- case "toolResult": {
2156
- // Tool results are rendered inline with tool calls, handled separately
2157
- break;
2158
- }
2159
- default: {
2160
- // v8 ignore next -- exhaustive over AgentMessage roles
2161
- const _exhaustive = message;
2162
- }
2163
- }
1709
+ eventsFlow.addMessageToChat(this, message, options);
2164
1710
  }
1711
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
2165
1712
  renderSessionItems(items, options = {}) {
2166
- this.pendingTools.clear();
2167
- const renderedPendingTools = new Map();
2168
- // Cache-miss notices are not persisted; re-derive them from the full entry
2169
- // list and re-inject them after the assistant messages that paid for them.
2170
- const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2171
- ? collectCacheMisses(this.facade.getEntries(), this.facade.getModelPriceSource())
2172
- : new Map();
2173
- if (options.updateFooter) {
2174
- this.footer.invalidate();
2175
- this.updateEditorBorderColor();
2176
- }
2177
- for (const item of items) {
2178
- if (isCustomSessionEntry(item)) {
2179
- // No entry renderer source since the legacy extension surface was
2180
- // removed; previously renderer-less entries were skipped as well.
2181
- continue;
2182
- }
2183
- if (isCompactionCostNotice(item)) {
2184
- this.addCompactionCostNotice(item);
2185
- continue;
2186
- }
2187
- const message = item;
2188
- // Assistant messages need special handling for tool calls
2189
- if (message.role === "assistant") {
2190
- this.addMessageToChat(message);
2191
- // Render tool call components
2192
- for (const content of message.content) {
2193
- if (content.type === "toolCall") {
2194
- const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2195
- showImages: this.settingsManager.getShowImages(),
2196
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2197
- }, this.getRegisteredToolDefinition(content.name), this.ui, this.facade.getCwd());
2198
- component.setExpanded(this.toolOutputExpanded);
2199
- this.chatContainer.addChild(component);
2200
- if (message.stopReason === "aborted" || message.stopReason === "error") {
2201
- let errorMessage;
2202
- if (message.stopReason === "aborted") {
2203
- const retryAttempt = this.facade.getRetryAttempt();
2204
- errorMessage =
2205
- retryAttempt > 0
2206
- ? retryAttempt === 1
2207
- ? t("Aborted after {count} retry attempt", { count: retryAttempt })
2208
- : t("Aborted after {count} retry attempts", { count: retryAttempt })
2209
- : t("Operation aborted");
2210
- }
2211
- else {
2212
- errorMessage = message.errorMessage || t("Error");
2213
- }
2214
- component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2215
- }
2216
- else {
2217
- renderedPendingTools.set(content.id, component);
2218
- }
2219
- }
2220
- }
2221
- if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2222
- const miss = cacheMisses.get(message);
2223
- if (miss)
2224
- this.addCacheMissNotice(miss);
2225
- }
2226
- }
2227
- else if (message.role === "toolResult") {
2228
- // Match tool results to pending tool components
2229
- const component = renderedPendingTools.get(message.toolCallId);
2230
- if (component) {
2231
- component.updateResult(message);
2232
- renderedPendingTools.delete(message.toolCallId);
2233
- }
2234
- }
2235
- else {
2236
- // All other messages use standard rendering
2237
- this.addMessageToChat(message, options);
2238
- }
2239
- }
2240
- for (const [toolCallId, component] of renderedPendingTools) {
2241
- this.pendingTools.set(toolCallId, component);
2242
- }
2243
- this.ui.requestRender();
1713
+ eventsFlow.renderSessionItems(this, items, options);
2244
1714
  }
2245
1715
  /**
2246
1716
  * Render session entries to chat. Used for initial load and rebuild after compaction.
2247
1717
  * @param entries Compaction-aware session entries to render
2248
1718
  * @param options.updateFooter Update footer state
2249
1719
  * @param options.populateHistory Add user messages to editor history
1720
+ *
1721
+ * Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in
1722
+ * ./interactive-events.ts.
2250
1723
  */
2251
1724
  renderSessionEntries(entries, options = {}) {
2252
- const items = entries.flatMap((entry) => {
2253
- if (entry.type === "custom") {
2254
- return [entry];
2255
- }
2256
- const messages = sessionEntryToContextMessages(entry);
2257
- if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage && messages.length > 0) {
2258
- return [...messages, { type: "compaction_cost", kind: entry.type, usage: entry.usage }];
2259
- }
2260
- return messages;
2261
- });
2262
- this.renderSessionItems(items, options);
1725
+ eventsFlow.renderSessionEntries(this, entries, options);
2263
1726
  }
2264
1727
  /**
2265
1728
  * Render billing usage for a compaction or branch summary. The notice is derived
2266
1729
  * from persisted summary usage and is not stored as a separate session entry.
1730
+ *
1731
+ * Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in
1732
+ * ./interactive-events.ts.
2267
1733
  */
2268
1734
  addCompactionCostNotice(notice) {
2269
- if (!this.settingsManager.getShowCacheMissNotices())
2270
- return;
2271
- const { usage } = notice;
2272
- const tokens = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
2273
- const cost = usage.cost.total >= 0.01 ? ` (~$${usage.cost.total.toFixed(2)})` : "";
2274
- const label = notice.kind === "compaction" ? t("Compaction") : t("Branch summary");
2275
- this.chatContainer.addChild(new Spacer(1));
2276
- this.chatContainer.addChild(new Text(theme.fg("warning", `${label}: ${t("{tokens} tokens billed{cost}", { tokens: formatTokens(tokens), cost })}`), 1, 0));
2277
- }
2278
- static countDroppedThinkingBlocks(message) {
2279
- let count = 0;
2280
- for (const diagnostic of message.diagnostics ?? []) {
2281
- if (diagnostic.type !== "anthropic_input_transformations")
2282
- continue;
2283
- const transformations = diagnostic.details?.transformations;
2284
- if (!Array.isArray(transformations))
2285
- continue;
2286
- count += transformations.filter((transformation) => typeof transformation === "object" &&
2287
- transformation !== null &&
2288
- transformation.type === "thinking_dropped").length;
2289
- }
2290
- return count;
1735
+ eventsFlow.addCompactionCostNotice(this, notice);
2291
1736
  }
1737
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
2292
1738
  maybeShowThinkingDropNotice(message) {
2293
- if (!this.settingsManager.getShowCacheMissNotices())
2294
- return;
2295
- const droppedCount = InteractiveMode.countDroppedThinkingBlocks(message);
2296
- if (droppedCount === 0)
2297
- return;
2298
- let previousDroppedCount = 0;
2299
- // message_end reaches the UI before the current message is persisted,
2300
- // so the branch's last assistant message is the previous response.
2301
- const entries = this.facade.getEntries();
2302
- for (let i = entries.length - 1; i >= 0; i--) {
2303
- const entry = entries[i];
2304
- if (entry.type === "message" && entry.message.role === "assistant") {
2305
- previousDroppedCount = InteractiveMode.countDroppedThinkingBlocks(entry.message);
2306
- break;
2307
- }
2308
- }
2309
- if (droppedCount <= previousDroppedCount)
2310
- return;
2311
- this.chatContainer.addChild(new Spacer(1));
2312
- this.chatContainer.addChild(new Text(theme.fg("warning", t("Anthropic dropped {count} thinking blocks (details in session)", { count: droppedCount })), 1, 0));
1739
+ eventsFlow.maybeShowThinkingDropNotice(this, message);
2313
1740
  }
2314
1741
  /**
2315
1742
  * Show a transcript notice when a completed assistant message paid for a
2316
1743
  * significant cache miss. Only states observable facts: the miss itself,
2317
1744
  * a model switch, or an idle gap past the cache TTL.
1745
+ *
1746
+ * Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in
1747
+ * ./interactive-events.ts.
2318
1748
  */
2319
1749
  maybeShowCacheMissNotice(message) {
2320
- if (!this.settingsManager.getShowCacheMissNotices())
2321
- return;
2322
- // Entries don't contain `message` yet: message_end fires before persistence.
2323
- const miss = detectCacheMiss(this.facade.getEntries(), message, this.facade.getModelPriceSource());
2324
- if (miss)
2325
- this.addCacheMissNotice(miss);
1750
+ eventsFlow.maybeShowCacheMissNotice(this, message);
2326
1751
  }
1752
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
2327
1753
  addCacheMissNotice(miss) {
2328
- if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
2329
- return;
2330
- const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
2331
- const reBilled = t("{tokens} tokens re-billed{cost}", { tokens: formatTokens(miss.missedTokens), cost });
2332
- let label = t("Cache miss");
2333
- if (miss.modelChanged) {
2334
- label = t("Cache miss after model switch");
2335
- }
2336
- else if (miss.idleMs >= CACHE_TTL_MS) {
2337
- label = t("Cache miss after {minutes}m idle", { minutes: Math.round(miss.idleMs / 60_000) });
2338
- }
2339
- const text = theme.fg("warning", `${label}: ${reBilled}`);
2340
- this.chatContainer.addChild(new Spacer(1));
2341
- this.chatContainer.addChild(new Text(text, 1, 0));
1754
+ eventsFlow.addCacheMissNotice(this, miss);
2342
1755
  }
2343
1756
  renderInitialMessages() {
2344
- const entries = this.facade.getContextEntries();
2345
- this.renderSessionEntries(entries, {
2346
- updateFooter: true,
2347
- populateHistory: true,
2348
- });
2349
- // Show compaction info if session was compacted
2350
- const allEntries = this.facade.getEntries();
2351
- const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
2352
- if (compactionCount > 0) {
2353
- this.showStatus(compactionCount === 1
2354
- ? t("Session compacted {count} time", { count: compactionCount })
2355
- : t("Session compacted {count} times", { count: compactionCount }));
2356
- }
1757
+ eventsFlow.renderInitialMessages(this);
2357
1758
  }
2358
1759
  async getUserInput() {
2359
1760
  const queuedInput = this.pendingUserInputs.shift();
@@ -2367,251 +1768,84 @@ export class InteractiveMode {
2367
1768
  };
2368
1769
  });
2369
1770
  }
1771
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1); implementation in ./interactive-events.ts (拆分 slice 5). */
2370
1772
  rebuildChatFromMessages() {
2371
- this.chatContainer.clear();
2372
- this.renderSessionEntries(this.facade.getContextEntries());
1773
+ eventsFlow.rebuildChatFromMessages(this);
2373
1774
  }
2374
1775
  // =========================================================================
2375
- // Key handlers
1776
+ // Exit and signal lifecycle (implementation in ./interactive-lifecycle.ts,
1777
+ // 拆分 slice 4) — same-name delegates keep the editor key wiring, init/stop,
1778
+ // and prototype-level tests bound to these names. The main loop and the TUI
1779
+ // teardown (stop) stay on the mode, as does formatResumeCommand (D-041 §3.5).
2376
1780
  // =========================================================================
1781
+ /** Ctrl+C interrupt/exit grading; implementation in ./interactive-lifecycle.ts (拆分 slice 4). */
2377
1782
  handleCtrlC() {
2378
- const now = Date.now();
2379
- if (now - this.lastSigintTime < 500) {
2380
- void this.shutdown();
2381
- }
2382
- else {
2383
- this.clearEditor();
2384
- this.lastSigintTime = now;
2385
- }
1783
+ lifecycleFlow.handleCtrlC(this);
2386
1784
  }
1785
+ /** Ctrl+D exit on empty editor; implementation in ./interactive-lifecycle.ts (拆分 slice 4). */
2387
1786
  handleCtrlD() {
2388
- // Only called when editor is empty (enforced by CustomEditor)
2389
- void this.shutdown();
1787
+ lifecycleFlow.handleCtrlD(this);
2390
1788
  }
2391
1789
  /**
2392
1790
  * Gracefully shutdown the agent.
2393
1791
  * Stops the TUI before emitting shutdown events so extension UI cleanup cannot
2394
- * repaint the final frame while the process is exiting.
1792
+ * repaint the final frame while the process is exiting; implementation in
1793
+ * ./interactive-lifecycle.ts (拆分 slice 4).
2395
1794
  */
2396
- isShuttingDown = false;
2397
1795
  async shutdown(options) {
2398
- if (this.isShuttingDown)
2399
- return;
2400
- this.isShuttingDown = true;
2401
- // Keep signal handlers registered until terminal cleanup has completed.
2402
- // `signal-exit` checks the listener list during the same SIGTERM/SIGHUP
2403
- // dispatch and re-sends the signal if only its own listeners remain.
2404
- if (options?.fromSignal) {
2405
- // Signal-triggered shutdown (SIGTERM/SIGHUP). Emit extension cleanup
2406
- // (session_shutdown) BEFORE touching the terminal. Extension teardown
2407
- // such as removing sockets does not write to the tty, so it must not be
2408
- // skipped if a later terminal-restore write fails on a dead or stalled
2409
- // terminal. If the terminal is gone, the restore writes below emit EIO,
2410
- // which the stdout/stderr error handler turns into emergencyTerminalExit;
2411
- // the render loop is already idle, so this cannot hot-spin (see #4144).
2412
- await this.runtimeFacade.dispose();
2413
- this.themeController.disableAutoSync();
2414
- await this.ui.terminal.drainInput(1000);
2415
- this.stop();
2416
- process.exit(0);
2417
- }
2418
- // Interactive quit (Ctrl+D, Ctrl+C, /quit, extension shutdown()). Stop the
2419
- // TUI before emitting shutdown events so extension UI cleanup cannot repaint
2420
- // the final frame while the process is exiting.
2421
- // Drain any in-flight Kitty key release events before stopping.
2422
- // This prevents escape sequences from leaking to the parent shell over slow SSH.
2423
- this.themeController.disableAutoSync();
2424
- await this.ui.terminal.drainInput(1000);
2425
- this.stop();
2426
- await this.runtimeFacade.dispose();
2427
- const resumeCommand = formatResumeCommand(this.runtimeFacade);
2428
- if (resumeCommand) {
2429
- process.stdout.write(`${chalk.dim(t("To resume this session:"))} ${resumeCommand}\n`);
2430
- }
2431
- process.exit(0);
1796
+ return lifecycleFlow.shutdown(this, options);
2432
1797
  }
1798
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4): dead-terminal emergency exit. */
2433
1799
  emergencyTerminalExit() {
2434
- this.isShuttingDown = true;
2435
- this.unregisterSignalHandlers();
2436
- killTrackedDetachedChildren();
2437
- // The terminal is gone. Do not run normal shutdown because TUI and
2438
- // extension cleanup can write restore sequences and re-trigger EIO.
2439
- process.exit(129);
1800
+ lifecycleFlow.emergencyTerminalExit(this);
2440
1801
  }
2441
- /**
2442
- * Last-resort handler for uncaught exceptions. The TUI puts stdin into raw
2443
- * mode and hides the cursor; without this handler, an uncaught throw from
2444
- * anywhere (e.g. an extension's async `ChildProcess.on("exit")` callback)
2445
- * tears down the process while leaving the terminal in raw mode with no
2446
- * cursor, requiring `stty sane && reset` to recover.
2447
- *
2448
- * Unlike emergencyTerminalExit, the terminal is still alive here, so we
2449
- * call ui.stop() to restore cooked mode, the cursor, and disable bracketed
2450
- * paste / Kitty / modifyOtherKeys sequences.
2451
- */
1802
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4): uncaught-exception crash handler. */
2452
1803
  uncaughtCrash(error) {
2453
- if (this.isShuttingDown) {
2454
- process.exit(1);
2455
- }
2456
- this.isShuttingDown = true;
2457
- try {
2458
- this.unregisterSignalHandlers();
2459
- }
2460
- catch { }
2461
- try {
2462
- killTrackedDetachedChildren();
2463
- }
2464
- catch { }
2465
- try {
2466
- this.ui.stop();
2467
- }
2468
- catch { }
2469
- console.error(t("{app} exiting due to uncaughtException:", { app: APP_NAME }));
2470
- console.error(error);
2471
- process.exit(1);
1804
+ lifecycleFlow.uncaughtCrash(this, error);
2472
1805
  }
2473
- /**
2474
- * Check if shutdown was requested and perform shutdown if so.
2475
- */
1806
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-lifecycle.ts (拆分 slice 4). */
2476
1807
  async checkShutdownRequested() {
2477
- if (!this.shutdownRequested)
2478
- return;
2479
- await this.shutdown();
1808
+ return lifecycleFlow.checkShutdownRequested(this);
2480
1809
  }
1810
+ /** Signal/stderr/uncaught registration; implementation in ./interactive-lifecycle.ts (拆分 slice 4). */
2481
1811
  registerSignalHandlers() {
2482
- this.unregisterSignalHandlers();
2483
- const signals = ["SIGTERM"];
2484
- if (process.platform !== "win32") {
2485
- signals.push("SIGHUP");
2486
- }
2487
- for (const signal of signals) {
2488
- const handler = () => {
2489
- // SIGHUP no longer hard-exits: graceful shutdown emits session_shutdown
2490
- // first, then attempts terminal restore. A genuinely dead terminal
2491
- // surfaces as an EIO on the restore writes, which the stdout/stderr
2492
- // error handler converts into emergencyTerminalExit (see #4144, #5080).
2493
- killTrackedDetachedChildren();
2494
- void this.shutdown({ fromSignal: true });
2495
- };
2496
- process.prependListener(signal, handler);
2497
- this.signalCleanupHandlers.push(() => process.off(signal, handler));
2498
- }
2499
- const terminalErrorHandler = (error) => {
2500
- if (isDeadTerminalError(error)) {
2501
- this.emergencyTerminalExit();
2502
- }
2503
- throw error;
2504
- };
2505
- process.stdout.on("error", terminalErrorHandler);
2506
- process.stderr.on("error", terminalErrorHandler);
2507
- this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
2508
- this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
2509
- // Restore the terminal before the process dies on any uncaught throw.
2510
- // Without this, an unhandled exception from extension code (or anywhere
2511
- // in the application) leaves the terminal in raw mode with no cursor.
2512
- const uncaughtExceptionHandler = (error) => this.uncaughtCrash(error);
2513
- process.prependListener("uncaughtException", uncaughtExceptionHandler);
2514
- this.signalCleanupHandlers.push(() => process.off("uncaughtException", uncaughtExceptionHandler));
1812
+ lifecycleFlow.registerSignalHandlers(this);
2515
1813
  }
1814
+ /** Host-visible for ./interactive-lifecycle.ts (拆分 slice 4): also called by the mode's stop(). */
2516
1815
  unregisterSignalHandlers() {
2517
- for (const cleanup of this.signalCleanupHandlers) {
2518
- cleanup();
2519
- }
2520
- this.signalCleanupHandlers = [];
1816
+ lifecycleFlow.unregisterSignalHandlers(this);
2521
1817
  }
1818
+ /** Suspend to background (Ctrl+Z); implementation in ./interactive-lifecycle.ts (拆分 slice 4). */
2522
1819
  handleCtrlZ() {
2523
- if (process.platform === "win32") {
2524
- this.showStatus(t("Suspend to background is not supported on Windows"));
2525
- return;
2526
- }
2527
- // Keep the event loop alive while suspended. Without this, stopping the TUI
2528
- // can leave Node with no ref'ed handles, causing the process to exit on fg
2529
- // before the SIGCONT handler gets a chance to restore the terminal.
2530
- const suspendKeepAlive = setInterval(() => { }, 2 ** 30);
2531
- // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2532
- // kill the backgrounded process. The handler is removed on resume.
2533
- const ignoreSigint = () => { };
2534
- process.on("SIGINT", ignoreSigint);
2535
- // Set up handler to restore TUI when resumed
2536
- process.once("SIGCONT", () => {
2537
- clearInterval(suspendKeepAlive);
2538
- process.removeListener("SIGINT", ignoreSigint);
2539
- this.ui.start();
2540
- this.ui.requestRender(true);
2541
- });
2542
- try {
2543
- // Stop the TUI (restore terminal to normal mode)
2544
- this.ui.stop();
2545
- // Send SIGTSTP to process group (pid=0 means all processes in group)
2546
- process.kill(0, "SIGTSTP");
2547
- }
2548
- catch (error) {
2549
- clearInterval(suspendKeepAlive);
2550
- process.removeListener("SIGINT", ignoreSigint);
2551
- throw error;
2552
- }
1820
+ lifecycleFlow.handleCtrlZ(this);
2553
1821
  }
2554
- async handleFollowUp(explicitText) {
2555
- const text = (explicitText ?? this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
2556
- if (!text)
2557
- return;
2558
- // Queue input during compaction (extension commands execute immediately)
2559
- if (this.facade.isCompacting()) {
2560
- this.queueCompactionMessage(text, "followUp");
2561
- return;
2562
- }
2563
- // Alt+Enter queues a follow-up message (waits until agent finishes)
2564
- // This handles extension commands (execute immediately), prompt template expansion, and queueing
2565
- if (this.facade.isStreaming()) {
2566
- // An explicit /queue <text> argument must not touch the editor draft.
2567
- if (explicitText === undefined) {
2568
- this.editor.addToHistory?.(text);
2569
- this.editor.setText("");
2570
- }
2571
- await this.facade.prompt(text, { streamingBehavior: "followUp" });
2572
- this.updatePendingMessagesDisplay();
2573
- this.ui.requestRender();
2574
- }
2575
- // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
2576
- else if (this.editor.onSubmit) {
2577
- if (explicitText === undefined)
2578
- this.editor.setText("");
2579
- this.editor.onSubmit(text);
2580
- }
1822
+ /**
1823
+ * Host-visible for ./interactive-lifecycle.ts (拆分 slice 4): the function
1824
+ * stays resident in this module (D-041 §3.5 — resume hints are host TUI
1825
+ * behavior, not a facade contract).
1826
+ */
1827
+ formatResumeCommand(session) {
1828
+ return formatResumeCommand(session);
1829
+ }
1830
+ /** Follow-up queue entry (Alt+Enter / /queue); implementation in ./interactive-queues.ts (拆分 slice 3). */
1831
+ handleFollowUp(explicitText) {
1832
+ return queuesFlow.handleFollowUp(this, explicitText);
2581
1833
  }
1834
+ /** Dequeue restore (keybinding / /dequeue); implementation in ./interactive-queues.ts (拆分 slice 3). */
2582
1835
  handleDequeue() {
2583
- const restored = this.restoreQueuedMessagesToEditor();
2584
- if (restored === 0) {
2585
- this.showStatus(t("No queued messages to restore"));
2586
- }
2587
- else if (restored === 1) {
2588
- this.showStatus(t("Restored {count} queued message to editor", { count: restored }));
2589
- }
2590
- else {
2591
- this.showStatus(t("Restored {count} queued messages to editor", { count: restored }));
2592
- }
1836
+ queuesFlow.handleDequeue(this);
2593
1837
  }
2594
- /** /queue command host face (single-entry iron law): queues the given text for after the current run. A bare /queue cannot mirror the keybinding — the editor buffer is cleared on submit before the command runs — so it prints usage instead of silently doing nothing. */
1838
+ /** /queue command host face (single-entry iron law); implementation in ./interactive-queues.ts (拆分 slice 3). */
2595
1839
  handleQueueCommand(text) {
2596
- if (text === undefined || text.trim() === "") {
2597
- this.showStatus(t("Usage: /queue <text>"));
2598
- return Promise.resolve();
2599
- }
2600
- return this.handleFollowUp(text);
1840
+ return queuesFlow.handleQueueCommand(this, text);
2601
1841
  }
2602
- /** /dequeue command host face: restore queued messages back to the editor. */
1842
+ /** /dequeue command host face; implementation in ./interactive-queues.ts (拆分 slice 3). */
2603
1843
  handleDequeueCommand() {
2604
- this.handleDequeue();
1844
+ queuesFlow.handleDequeueCommand(this);
2605
1845
  }
2606
- /** /queue-list command host face: open the queued-message manager selector. Both queues empty is a status message, not an empty overlay. */
1846
+ /** /queue-list command host face; implementation in ./interactive-queues.ts (拆分 slice 3). */
2607
1847
  handleQueueListCommand() {
2608
- const steering = this.facade.getSteeringMessages();
2609
- const followUp = this.facade.getFollowUpMessages();
2610
- if (steering.length === 0 && followUp.length === 0) {
2611
- this.showStatus(t("No queued messages to manage"));
2612
- return;
2613
- }
2614
- selectorsFlow.showQueueManagerSelector(this);
1848
+ queuesFlow.handleQueueListCommand(this);
2615
1849
  }
2616
1850
  /** /editor command host face: edit the current input in an external editor. */
2617
1851
  handleEditorCommand() {
@@ -2666,6 +1900,34 @@ export class InteractiveMode {
2666
1900
  toggleToolOutputExpansion() {
2667
1901
  this.setToolsExpanded(!this.toolOutputExpanded);
2668
1902
  }
1903
+ /**
1904
+ * D-083 quiet transcript: mount a tool component into the current
1905
+ * assistant turn's tool-run group (creating the group on first tool of
1906
+ * the turn) instead of the chat container directly. Collapsed groups
1907
+ * render one dim marker line for the whole turn.
1908
+ *
1909
+ * Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in
1910
+ * ./interactive-events.ts.
1911
+ */
1912
+ addToolToRunGroup(component) {
1913
+ eventsFlow.addToolToRunGroup(this, component);
1914
+ }
1915
+ /**
1916
+ * Narrowing-safe faces for the active group inside renderSessionItems:
1917
+ * addToolToRunGroup may (re)assign this.activeToolRunGroup behind the
1918
+ * compiler's back, so group reads go through methods instead of the
1919
+ * narrowed property reference.
1920
+ *
1921
+ * Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in
1922
+ * ./interactive-events.ts.
1923
+ */
1924
+ noteResultInActiveToolRunGroup(isError) {
1925
+ eventsFlow.noteResultInActiveToolRunGroup(this, isError);
1926
+ }
1927
+ /** Host-visible for ./interactive-events.ts (拆分 slice 5); implementation in ./interactive-events.ts. */
1928
+ markActiveToolRunGroupComplete() {
1929
+ eventsFlow.markActiveToolRunGroupComplete(this);
1930
+ }
2669
1931
  setToolsExpanded(expanded) {
2670
1932
  if (expanded === this.toolOutputExpanded)
2671
1933
  return;
@@ -2684,6 +1946,7 @@ export class InteractiveMode {
2684
1946
  this.showStatus(expanded ? t("Tool output: expanded") : t("Tool output: collapsed"));
2685
1947
  }
2686
1948
  /** Update rendered assistant messages without rebuilding live tool components. */
1949
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
2687
1950
  updateThinkingBlockVisibility() {
2688
1951
  for (const child of this.chatContainer.children) {
2689
1952
  if (child instanceof AssistantMessageComponent) {
@@ -2771,58 +2034,27 @@ export class InteractiveMode {
2771
2034
  /**
2772
2035
  * Get all queued messages (read-only).
2773
2036
  * Combines session queue and compaction queue.
2037
+ *
2038
+ * Host-visible for ./interactive-queues.ts (拆分 slice 3): the merged read
2039
+ * behind the pending-messages display.
2774
2040
  */
2775
2041
  getAllQueuedMessages() {
2776
- return {
2777
- steering: [
2778
- ...this.facade.getSteeringMessages(),
2779
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2780
- ],
2781
- followUp: [
2782
- ...this.facade.getFollowUpMessages(),
2783
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2784
- ],
2785
- };
2042
+ return queuesFlow.getAllQueuedMessages(this);
2786
2043
  }
2787
2044
  /**
2788
2045
  * Clear all queued messages and return their contents.
2789
- * Clears both session queue and compaction queue.
2046
+ * Clears both session queue and compaction queue; implementation in
2047
+ * ./interactive-queues.ts (拆分 slice 3). Stays reachable from the mode's
2048
+ * restoreQueuedMessagesToEditor.
2790
2049
  */
2791
2050
  clearAllQueues() {
2792
- const { steering, followUp } = this.facade.clearQueue();
2793
- const compactionSteering = this.compactionQueuedMessages
2794
- .filter((msg) => msg.mode === "steer")
2795
- .map((msg) => msg.text);
2796
- const compactionFollowUp = this.compactionQueuedMessages
2797
- .filter((msg) => msg.mode === "followUp")
2798
- .map((msg) => msg.text);
2799
- this.compactionQueuedMessages = [];
2800
- return {
2801
- steering: [...steering, ...compactionSteering],
2802
- followUp: [...followUp, ...compactionFollowUp],
2803
- };
2051
+ return queuesFlow.clearAllQueues(this);
2804
2052
  }
2805
- /** Host-visible for ./interactive-selectors.ts (queue manager close path). */
2053
+ /** Host-visible for ./interactive-selectors.ts (queue manager close path) and ./interactive-events.ts (拆分 slice 3); implementation in ./interactive-queues.ts. */
2806
2054
  updatePendingMessagesDisplay() {
2807
- this.pendingMessagesContainer.clear();
2808
- const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
2809
- if (steeringMessages.length > 0 || followUpMessages.length > 0) {
2810
- this.pendingMessagesContainer.addChild(new Spacer(1));
2811
- for (const message of steeringMessages) {
2812
- const text = theme.fg("dim", t("Steering: {message}", { message }));
2813
- this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2814
- }
2815
- for (const message of followUpMessages) {
2816
- const text = theme.fg("dim", t("Follow-up: {message}", { message }));
2817
- this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2818
- }
2819
- // The dequeue keybinding's shipped default was cleared (iron law):
2820
- // /queue's sibling command is the discoverable entry.
2821
- const dequeueHint = this.getAppKeyDisplay("app.message.dequeue") || "/dequeue";
2822
- const hintText = theme.fg("dim", t("↳ {key} to edit all queued messages", { key: dequeueHint }));
2823
- this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
2824
- }
2055
+ queuesFlow.updatePendingMessagesDisplay(this);
2825
2056
  }
2057
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
2826
2058
  restoreQueuedMessagesToEditor(options) {
2827
2059
  const { steering, followUp } = this.clearAllQueues();
2828
2060
  const allQueued = [...steering, ...followUp];
@@ -2846,68 +2078,13 @@ export class InteractiveMode {
2846
2078
  }
2847
2079
  return allQueued.length;
2848
2080
  }
2081
+ /** Host-visible for ./interactive-queues.ts (拆分 slice 3); implementation in ./interactive-queues.ts. */
2849
2082
  queueCompactionMessage(text, mode) {
2850
- this.compactionQueuedMessages.push({ text, mode });
2851
- this.editor.addToHistory?.(text);
2852
- this.editor.setText("");
2853
- this.updatePendingMessagesDisplay();
2854
- this.showStatus(t("Queued message for after compaction"));
2083
+ queuesFlow.queueCompactionMessage(this, text, mode);
2855
2084
  }
2085
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2) and ./interactive-events.ts; implementation in ./interactive-queues.ts (拆分 slice 3). */
2856
2086
  async flushCompactionQueue(options) {
2857
- if (this.compactionQueuedMessages.length === 0) {
2858
- return;
2859
- }
2860
- const queuedMessages = [...this.compactionQueuedMessages];
2861
- this.compactionQueuedMessages = [];
2862
- this.updatePendingMessagesDisplay();
2863
- const restoreQueue = (error) => {
2864
- this.facade.clearQueue();
2865
- this.compactionQueuedMessages = queuedMessages;
2866
- this.updatePendingMessagesDisplay();
2867
- this.showError(queuedMessages.length > 1
2868
- ? t("Failed to send queued messages: {error}", {
2869
- error: error instanceof Error ? error.message : String(error),
2870
- })
2871
- : t("Failed to send queued message: {error}", {
2872
- error: error instanceof Error ? error.message : String(error),
2873
- }));
2874
- };
2875
- try {
2876
- if (options?.willRetry) {
2877
- // When retry is pending, queue messages for the retry turn
2878
- for (const message of queuedMessages) {
2879
- if (message.mode === "followUp") {
2880
- await this.facade.followUp(message.text);
2881
- }
2882
- else {
2883
- await this.facade.steer(message.text);
2884
- }
2885
- }
2886
- this.updatePendingMessagesDisplay();
2887
- return;
2888
- }
2889
- const [firstPrompt, ...rest] = queuedMessages;
2890
- // Start a prompt when idle, or queue it into a run still finishing compaction.
2891
- const promptPromise = this.facade
2892
- .prompt(firstPrompt.text, { streamingBehavior: firstPrompt.mode })
2893
- .catch((error) => {
2894
- restoreQueue(error);
2895
- });
2896
- // Queue remaining messages
2897
- for (const message of rest) {
2898
- if (message.mode === "followUp") {
2899
- await this.facade.followUp(message.text);
2900
- }
2901
- else {
2902
- await this.facade.steer(message.text);
2903
- }
2904
- }
2905
- this.updatePendingMessagesDisplay();
2906
- void promptPromise;
2907
- }
2908
- catch (error) {
2909
- restoreQueue(error);
2910
- }
2087
+ return queuesFlow.flushCompactionQueue(this, options);
2911
2088
  }
2912
2089
  /** Move pending bash components from pending area to chat */
2913
2090
  flushPendingBashComponents() {
@@ -2919,15 +2096,14 @@ export class InteractiveMode {
2919
2096
  }
2920
2097
  // =========================================================================
2921
2098
  // Selectors
2922
- // Suite/thinking/model/scoped-models/user-message-fork/clone selectors are
2923
- // implemented in ./interactive-selectors.ts (S5 slice 5) behind the
2099
+ // All selector/view implementations — the S5 slice 5 suite/thinking/model/
2100
+ // scoped-models/user-message-fork/clone selectors and the 拆分 slice 2
2101
+ // settings/workflow/agents/tree/session selectors, resume, and the
2102
+ // startup-suite recovery — live in ./interactive-selectors.ts behind the
2924
2103
  // SelectorsHost interface; same-name delegates below keep the command
2925
2104
  // dispatch and prototype-level tests bound to these names. The selector
2926
- // infrastructure (showSelector/disposeActiveSelector), the settings/tree/
2927
- // session selectors, and resume stay on the mode: the settings and tree
2928
- // selectors assemble from broad private UI state, and the session/resume
2929
- // path is the documented session-boundary carve-out (SessionManager
2930
- // manager-level writes plus per-entry display reads).
2105
+ // infrastructure (showSelector/disposeActiveSelector) and the shared
2106
+ // detectCrossSuiteResume helper stay on the mode.
2931
2107
  // =========================================================================
2932
2108
  disposeActiveSelector() {
2933
2109
  const dispose = this.activeSelectorDispose;
@@ -2962,230 +2138,9 @@ export class InteractiveMode {
2962
2138
  this.ui.setFocus(created.focus);
2963
2139
  this.ui.requestRender();
2964
2140
  }
2141
+ /** Settings selector; implementation in ./interactive-selectors.ts (拆分 slice 2). */
2965
2142
  showSettingsSelector() {
2966
- this.showSelector((done) => {
2967
- let selector;
2968
- const defaultProvider = this.settingsManager.getDefaultProvider();
2969
- const defaultModelId = this.settingsManager.getDefaultModel();
2970
- const defaultModel = defaultProvider && defaultModelId ? `${defaultProvider}/${defaultModelId}` : t("not set");
2971
- selector = new SettingsSelectorComponent({
2972
- defaultModel,
2973
- currentModel: this.facade.getModel(),
2974
- availableDefaultModels: this.facade.getAvailableModels(),
2975
- showImages: this.settingsManager.getShowImages(),
2976
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2977
- autoResizeImages: this.settingsManager.getImageAutoResize(),
2978
- blockImages: this.settingsManager.getBlockImages(),
2979
- steeringMode: this.facade.getSteeringMode(),
2980
- followUpMode: this.facade.getFollowUpMode(),
2981
- transport: this.settingsManager.getTransport(),
2982
- httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
2983
- thinkingLevel: this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL,
2984
- availableThinkingLevels: [...THINKING_LEVEL_OPTIONS],
2985
- modelThinkingLevels: this.settingsManager.getAllModelThinkingLevels(),
2986
- currentTheme: this.themeController.getThemeSelection() || "dark",
2987
- terminalTheme: this.themeController.getTerminalTheme(),
2988
- availableThemes: getAvailableThemes(),
2989
- hideThinkingBlock: this.hideThinkingBlock,
2990
- mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
2991
- collapseChangelog: this.settingsManager.getCollapseChangelog(),
2992
- doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
2993
- uiLanguage: this.settingsManager.getUiLanguage(),
2994
- treeFilterMode: this.settingsManager.getTreeFilterMode(),
2995
- showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
2996
- showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
2997
- editorPaddingX: this.settingsManager.getEditorPaddingX(),
2998
- outputPad: this.settingsManager.getOutputPad(),
2999
- autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3000
- quietStartup: this.settingsManager.getQuietStartup(),
3001
- clearOnShrink: this.settingsManager.getClearOnShrink(),
3002
- showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3003
- tuiMode: this.ui.mode,
3004
- fullscreenExitOutput: this.settingsManager.getFullscreenExitOutput(),
3005
- fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3006
- fullscreenCopyOnSelect: this.settingsManager.getFullscreenCopyOnSelect(),
3007
- fullscreenWheelScrollLines: this.settingsManager.getFullscreenWheelScrollLines(),
3008
- warnings: this.settingsManager.getWarnings(),
3009
- }, {
3010
- onShowImagesChange: (enabled) => {
3011
- this.settingsManager.setShowImages(enabled);
3012
- for (const child of this.chatContainer.children) {
3013
- if (child instanceof ToolExecutionComponent) {
3014
- child.setShowImages(enabled);
3015
- }
3016
- }
3017
- },
3018
- onImageWidthCellsChange: (width) => {
3019
- this.settingsManager.setImageWidthCells(width);
3020
- for (const child of this.chatContainer.children) {
3021
- if (child instanceof ToolExecutionComponent) {
3022
- child.setImageWidthCells(width);
3023
- }
3024
- }
3025
- },
3026
- onAutoResizeImagesChange: (enabled) => {
3027
- this.settingsManager.setImageAutoResize(enabled);
3028
- },
3029
- onBlockImagesChange: (blocked) => {
3030
- this.settingsManager.setBlockImages(blocked);
3031
- },
3032
- onSteeringModeChange: (mode) => {
3033
- this.facade.setSteeringMode(mode);
3034
- },
3035
- onFollowUpModeChange: (mode) => {
3036
- this.facade.setFollowUpMode(mode);
3037
- },
3038
- onTransportChange: (transport) => {
3039
- this.settingsManager.setTransport(transport);
3040
- this.session.host.setTransport(transport);
3041
- },
3042
- onHttpIdleTimeoutMsChange: (timeoutMs) => {
3043
- this.settingsManager.setHttpIdleTimeoutMs(timeoutMs);
3044
- configureHttpDispatcher(timeoutMs);
3045
- this.showStatus(t("HTTP idle timeout: {value}", { value: formatHttpIdleTimeoutMs(timeoutMs) }));
3046
- },
3047
- onModelThinkingLevelChange: (provider, modelId, level) => {
3048
- this.settingsManager.setModelThinkingLevel(provider, modelId, level);
3049
- // If the override is for the current model, apply it to the session too
3050
- const current = this.facade.getModel();
3051
- if (current && current.provider === provider && current.id === modelId) {
3052
- this.facade.setThinkingLevel(level);
3053
- this.footer.invalidate();
3054
- this.updateEditorBorderColor();
3055
- }
3056
- },
3057
- onModelThinkingLevelRemove: (provider, modelId) => {
3058
- this.settingsManager.removeModelThinkingLevel(provider, modelId);
3059
- // If the override was for the current model, revert to global default
3060
- const current = this.facade.getModel();
3061
- if (current && current.provider === provider && current.id === modelId) {
3062
- const globalDefault = this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
3063
- this.facade.setThinkingLevel(globalDefault);
3064
- this.footer.invalidate();
3065
- this.updateEditorBorderColor();
3066
- }
3067
- },
3068
- onThemeChange: (themeSetting) => {
3069
- this.settingsManager.setTheme(themeSetting);
3070
- void this.themeController.setThemeSetting(themeSetting);
3071
- },
3072
- onThemePreview: (themeName) => this.themeController.preview(themeName),
3073
- onHideThinkingBlockChange: (hidden) => {
3074
- this.hideThinkingBlock = hidden;
3075
- this.settingsManager.setHideThinkingBlock(hidden);
3076
- this.updateThinkingBlockVisibility();
3077
- },
3078
- onMermaidRenderingModeChange: (mode) => {
3079
- this.settingsManager.setMermaidRenderingMode(mode);
3080
- this.chatContainer.invalidate();
3081
- this.ui.requestRender();
3082
- },
3083
- onShowCacheMissNoticesChange: (shown) => {
3084
- this.settingsManager.setShowCacheMissNotices(shown);
3085
- this.rebuildChatFromMessages();
3086
- },
3087
- onCollapseChangelogChange: (collapsed) => {
3088
- this.settingsManager.setCollapseChangelog(collapsed);
3089
- },
3090
- onQuietStartupChange: (enabled) => {
3091
- this.settingsManager.setQuietStartup(enabled);
3092
- },
3093
- onDoubleEscapeActionChange: (action) => {
3094
- this.settingsManager.setDoubleEscapeAction(action);
3095
- },
3096
- onUiLanguageChange: (language) => {
3097
- this.settingsManager.setUiLanguage(language);
3098
- setI18nLanguage(language);
3099
- this.ui.requestRender();
3100
- },
3101
- onTreeFilterModeChange: (mode) => {
3102
- this.settingsManager.setTreeFilterMode(mode);
3103
- },
3104
- onShowHardwareCursorChange: (enabled) => {
3105
- this.settingsManager.setShowHardwareCursor(enabled);
3106
- this.ui.setShowHardwareCursor(enabled);
3107
- },
3108
- onEditorPaddingXChange: (padding) => {
3109
- this.settingsManager.setEditorPaddingX(padding);
3110
- this.defaultEditor.setPaddingX(padding);
3111
- if (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {
3112
- this.editor.setPaddingX(padding);
3113
- }
3114
- },
3115
- onOutputPadChange: (padding) => {
3116
- this.settingsManager.setOutputPad(padding);
3117
- this.outputPad = padding;
3118
- if (this.streamingComponent || this.facade.isStreaming()) {
3119
- for (const child of this.chatContainer.children) {
3120
- if (child instanceof AssistantMessageComponent ||
3121
- child instanceof CustomMessageComponent ||
3122
- child instanceof UserMessageComponent) {
3123
- child.setOutputPad(padding);
3124
- }
3125
- }
3126
- if (this.streamingComponent) {
3127
- this.streamingComponent.setOutputPad(padding);
3128
- }
3129
- this.ui.requestRender();
3130
- return;
3131
- }
3132
- this.rebuildChatFromMessages();
3133
- },
3134
- onAutocompleteMaxVisibleChange: (maxVisible) => {
3135
- this.settingsManager.setAutocompleteMaxVisible(maxVisible);
3136
- this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
3137
- if (this.editor !== this.defaultEditor && this.editor.setAutocompleteMaxVisible !== undefined) {
3138
- this.editor.setAutocompleteMaxVisible(maxVisible);
3139
- }
3140
- },
3141
- onClearOnShrinkChange: (enabled) => {
3142
- this.settingsManager.setClearOnShrink(enabled);
3143
- this.ui.setClearOnShrink(enabled);
3144
- if (!enabled && !this.activeStatusIndicator) {
3145
- this.statusContainer.clear();
3146
- }
3147
- },
3148
- onShowTerminalProgressChange: (enabled) => {
3149
- this.settingsManager.setShowTerminalProgress(enabled);
3150
- },
3151
- onTuiModeChange: (mode) => {
3152
- if (!this.switchTuiMode(mode)) {
3153
- selector?.getSettingsList().updateValue("tui-mode", this.ui.mode);
3154
- this.showStatus(t("Close active overlays before changing TUI mode"));
3155
- return;
3156
- }
3157
- this.settingsManager.setTuiMode(mode);
3158
- if (!this.activeStatusIndicator)
3159
- this.statusContainer.clear();
3160
- this.showStatus(t("TUI mode: {mode}", { mode }));
3161
- },
3162
- onFullscreenExitOutputChange: (output) => {
3163
- this.settingsManager.setFullscreenExitOutput(output);
3164
- },
3165
- onFullscreenScrollbarChange: (mode) => {
3166
- this.settingsManager.setFullscreenScrollbar(mode);
3167
- this.applyFullscreenScrollbarSetting();
3168
- },
3169
- onFullscreenCopyOnSelectChange: (enabled) => {
3170
- this.settingsManager.setFullscreenCopyOnSelect(enabled);
3171
- if (this.renderer instanceof TuiAltScreen)
3172
- this.renderer.setCopyOnSelect(enabled);
3173
- },
3174
- onFullscreenWheelScrollLinesChange: (lines) => {
3175
- this.settingsManager.setFullscreenWheelScrollLines(lines);
3176
- if (this.renderer instanceof TuiAltScreen)
3177
- this.renderer.setWheelScrollLines(lines);
3178
- },
3179
- onWarningsChange: (warnings) => {
3180
- this.settingsManager.setWarnings(warnings);
3181
- },
3182
- onCancel: () => {
3183
- done();
3184
- this.ui.requestRender();
3185
- },
3186
- });
3187
- return { component: selector, focus: selector.getSettingsList() };
3188
- });
2143
+ selectorsFlow.showSettingsSelector(this);
3189
2144
  }
3190
2145
  /** /thinking command entry; implementation in ./interactive-selectors.ts (S5 slice 5). */
3191
2146
  handleThinkingCommand(searchTerm) {
@@ -3232,6 +2187,7 @@ export class InteractiveMode {
3232
2187
  * file's binding entry and cached per path — the selector refilters the
3233
2188
  * list on every keystroke.
3234
2189
  */
2190
+ /** Host-visible for ./interactive-selectors.ts (拆分 slice 2). */
3235
2191
  readSessionSuiteId(sessionPath) {
3236
2192
  this.sessionSuiteIdCache ??= new Map();
3237
2193
  if (this.sessionSuiteIdCache.has(sessionPath))
@@ -3256,36 +2212,12 @@ export class InteractiveMode {
3256
2212
  selectorsFlow.openSuiteListSelector(this, callbacks);
3257
2213
  }
3258
2214
  /**
3259
- * Interactive recovery for an unresolved startup suite id (设计 §9): pick a
3260
- * suite, persist it as the project setting, then rebuild the session with
3261
- * the selected suite via the same /new assembly path. Cancelling keeps the
3262
- * fallback assembly the runtime started with.
2215
+ * Interactive recovery for an unresolved startup suite id (设计 §9); the
2216
+ * implementation is in ./interactive-selectors.ts (拆分 slice 2). The
2217
+ * startup path on the mode calls this delegate.
3263
2218
  */
3264
2219
  async showStartupSuiteSelector(pending) {
3265
- const selected = await new Promise((resolve) => {
3266
- this.openSuiteListSelector({
3267
- availableSuites: pending.available,
3268
- note: t('Suite "{id}" was not found. The selected suite applies to the new session.', {
3269
- id: pending.requestedId,
3270
- }),
3271
- onSelect: (suiteId) => resolve(suiteId),
3272
- onCancel: () => resolve(undefined),
3273
- });
3274
- });
3275
- if (selected === undefined) {
3276
- this.showWarning(t('Suite "{id}" was not found — continuing without a suite. Use /suite to choose one.', {
3277
- id: pending.requestedId,
3278
- }));
3279
- return;
3280
- }
3281
- this.settingsManager.setProjectSuite(selected);
3282
- try {
3283
- await this.runtimeFacade.newSession();
3284
- this.showStatus(t("Suite set to {id} — new session started", { id: selected }));
3285
- }
3286
- catch (error) {
3287
- await this.handleFatalRuntimeError(t("Failed to create session"), error);
3288
- }
2220
+ return selectorsFlow.showStartupSuiteSelector(this, pending);
3289
2221
  }
3290
2222
  /** /model command entry; implementation in ./interactive-selectors.ts (S5 slice 5). */
3291
2223
  async handleModelCommand(searchTerm) {
@@ -3350,244 +2282,32 @@ export class InteractiveMode {
3350
2282
  async handleCloneCommand() {
3351
2283
  await selectorsFlow.handleCloneCommand(this);
3352
2284
  }
3353
- /**
3354
- * Workflow overlay (§9 L2/L3): read-only panorama + transition log from
3355
- * `workflow.state`/`workflow.transition` entries. Actions are forwarded to
3356
- * the editor as reviewable text; the main agent executes them via
3357
- * workflow_advance (rendering and execution stay separated).
3358
- */
2285
+ /** Workflow overlay; implementation in ./interactive-selectors.ts (拆分 slice 2). */
3359
2286
  showWorkflowView() {
3360
- const hasSnapshot = this.facade
3361
- .getEntries()
3362
- .some((entry) => entry.type === "custom" && entry.customType === "workflow.state");
3363
- if (!hasSnapshot) {
3364
- this.showStatus(t("No workflow in this session"));
3365
- return;
3366
- }
3367
- this.showSelector((done) => {
3368
- const view = new WorkflowViewComponent({
3369
- getEntries: () => this.facade.getEntries(),
3370
- maxRows: Math.max(6, Math.floor(this.ui.terminal.rows / 2)),
3371
- requestRender: () => this.ui.requestRender(),
3372
- });
3373
- view.onClose = done;
3374
- view.onForwardAction = (text) => {
3375
- done();
3376
- this.editor.setText(text);
3377
- this.ui.requestRender();
3378
- this.showStatus(t("Workflow action staged — review in the editor and submit"));
3379
- };
3380
- return { component: view, focus: view, dispose: () => view.dispose() };
3381
- });
2287
+ selectorsFlow.showWorkflowView(this);
3382
2288
  }
3383
- /**
3384
- * Subagent session browser (/agents): read-only list + detail of this
3385
- * session's subagent transcripts from the D-060 §3 on-disk store — the same
3386
- * parent-scoped summary source as the subagent_list model tool (only this
3387
- * session's children, newest first). Detail entries load lazily per
3388
- * session; a failed load renders inside the view instead of throwing.
3389
- */
2289
+ /** Subagent session browser; implementation in ./interactive-selectors.ts (拆分 slice 2). */
3390
2290
  showAgentsView() {
3391
- const agentDir = getAgentDir();
3392
- const sessions = listAgentSessions({ agentDir, parentAgentSession: this.facade.getSessionId() });
3393
- if (sessions.length === 0) {
3394
- this.showStatus(t("No subagent sessions in this session"));
3395
- return;
3396
- }
3397
- this.showSelector((done) => {
3398
- const view = new AgentsViewComponent({
3399
- sessions,
3400
- loadEntries: (sessionId) => readAgentSession({ agentDir, sessionId }),
3401
- maxRows: Math.max(6, Math.floor(this.ui.terminal.rows / 2)),
3402
- requestRender: () => this.ui.requestRender(),
3403
- });
3404
- view.onClose = done;
3405
- return { component: view, focus: view };
3406
- });
2291
+ selectorsFlow.showAgentsView(this);
3407
2292
  }
2293
+ /** Tree selector; implementation in ./interactive-selectors.ts (拆分 slice 2). */
3408
2294
  showTreeSelector(initialSelectedId) {
3409
- const tree = this.facade.getTree();
3410
- const realLeafId = this.facade.getLeafId();
3411
- const initialFilterMode = this.settingsManager.getTreeFilterMode();
3412
- if (tree.length === 0) {
3413
- this.showStatus(t("No entries in session"));
3414
- return;
3415
- }
3416
- this.showSelector((done) => {
3417
- const selector = new TreeSelectorComponent(tree, realLeafId, this.ui.terminal.rows, async (entryId) => {
3418
- // Selecting the current leaf is a no-op (already there)
3419
- if (entryId === this.facade.getLeafId()) {
3420
- done();
3421
- this.showStatus(t("Already at this point"));
3422
- return;
3423
- }
3424
- // Ask about summarization
3425
- done(); // Close selector first
3426
- // Loop until user makes a complete choice or cancels to tree
3427
- let wantsSummary = false;
3428
- let customInstructions;
3429
- // Check if we should skip the prompt (user preference to always default to no summary)
3430
- if (!this.settingsManager.getBranchSummarySkipPrompt()) {
3431
- while (true) {
3432
- const summaryChoice = await this.showDialogSelector(t("Summarize branch?"), [
3433
- t("No summary"),
3434
- t("Summarize"),
3435
- t("Summarize with custom prompt"),
3436
- ]);
3437
- if (summaryChoice === undefined) {
3438
- // User pressed escape - re-show tree selector with same selection
3439
- this.showTreeSelector(entryId);
3440
- return;
3441
- }
3442
- wantsSummary = summaryChoice !== t("No summary");
3443
- if (summaryChoice === t("Summarize with custom prompt")) {
3444
- customInstructions = await this.showDialogEditor(t("Custom summarization instructions"));
3445
- if (customInstructions === undefined) {
3446
- // User cancelled - loop back to summary selector
3447
- continue;
3448
- }
3449
- }
3450
- // User made a complete choice
3451
- break;
3452
- }
3453
- }
3454
- // The user committed to navigating: stop the active response first.
3455
- // 中断分级:导航清场只停回合,不终止后台子代理(设计 §4.2)。
3456
- if (this.facade.isStreaming()) {
3457
- this.restoreQueuedMessagesToEditor();
3458
- await this.facade.abort({ cancelAgentTasks: false });
3459
- }
3460
- // Set up escape handler and status indicator if summarizing
3461
- let showingSummaryIndicator = false;
3462
- const originalOnEscape = this.defaultEditor.onEscape;
3463
- if (wantsSummary) {
3464
- this.defaultEditor.onEscape = () => {
3465
- this.facade.abortBranchSummary();
3466
- };
3467
- this.chatContainer.addChild(new Spacer(1));
3468
- this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
3469
- showingSummaryIndicator = true;
3470
- this.ui.requestRender();
3471
- }
3472
- try {
3473
- const result = await this.facade.navigateTree(entryId, {
3474
- summarize: wantsSummary,
3475
- customInstructions,
3476
- });
3477
- if (result.aborted) {
3478
- // Summarization aborted - re-show tree selector with same selection
3479
- this.showStatus(t("Branch summarization cancelled"));
3480
- this.showTreeSelector(entryId);
3481
- return;
3482
- }
3483
- if (result.cancelled) {
3484
- this.showStatus(t("Navigation cancelled"));
3485
- return;
3486
- }
3487
- // Update UI
3488
- this.chatContainer.clear();
3489
- this.renderInitialMessages();
3490
- if (result.editorText && !this.editor.getText().trim()) {
3491
- this.editor.setText(result.editorText);
3492
- }
3493
- this.showStatus(t("Navigated to selected point"));
3494
- void this.flushCompactionQueue({ willRetry: false });
3495
- }
3496
- catch (error) {
3497
- this.showError(error instanceof Error ? error.message : String(error));
3498
- }
3499
- finally {
3500
- if (showingSummaryIndicator) {
3501
- this.clearStatusIndicator("branchSummary");
3502
- }
3503
- this.defaultEditor.onEscape = originalOnEscape;
3504
- }
3505
- }, () => {
3506
- done();
3507
- this.ui.requestRender();
3508
- }, (entryId, label) => {
3509
- this.facade.appendLabelChange(entryId, label);
3510
- this.ui.requestRender();
3511
- }, initialSelectedId, initialFilterMode);
3512
- selector.onCopy = async (text) => {
3513
- if (!text) {
3514
- this.showError(t("Selected entry has no text to copy"));
3515
- return;
3516
- }
3517
- try {
3518
- await copyToClipboard(text);
3519
- this.showStatus(t("Copied selected message to clipboard"));
3520
- }
3521
- catch (error) {
3522
- this.showError(error instanceof Error ? error.message : String(error));
3523
- }
3524
- };
3525
- return { component: selector, focus: selector };
3526
- });
2295
+ selectorsFlow.showTreeSelector(this, initialSelectedId);
3527
2296
  }
2297
+ /** Session selector; implementation in ./interactive-selectors.ts (拆分 slice 2). */
3528
2298
  showSessionSelector() {
3529
- this.showSelector((done) => {
3530
- const selector = new SessionSelectorComponent((onProgress, signal) => this.runtimeFacade.listSessions(onProgress, signal), (onProgress, signal) => this.runtimeFacade.listAllSessions(onProgress, signal), async (sessionPath) => {
3531
- done();
3532
- await this.handleResumeSession(sessionPath);
3533
- }, () => {
3534
- done();
3535
- this.ui.requestRender();
3536
- }, () => {
3537
- void this.shutdown();
3538
- }, () => this.ui.requestRender(), {
3539
- renameSession: async (sessionFilePath, nextName) => {
3540
- const next = (nextName ?? "").trim();
3541
- if (!next)
3542
- return;
3543
- const mgr = SessionManager.open(sessionFilePath);
3544
- mgr.appendSessionInfo(next);
3545
- },
3546
- showRenameHint: true,
3547
- keybindings: this.keybindings,
3548
- // 会话选择器默认只列当前方案的会话;跨方案会话仅在显式
3549
- // "show all" 后可见(设计 §10.1)。
3550
- suiteFilter: {
3551
- currentSuiteId: this.getCurrentSessionSuiteId(),
3552
- getSessionSuiteId: (session) => this.readSessionSuiteId(session.path),
3553
- },
3554
- }, this.runtimeFacade.getSessionFile());
3555
- return { component: selector, focus: selector };
3556
- });
2299
+ selectorsFlow.showSessionSelector(this);
3557
2300
  }
2301
+ /** Queue manager overlay; implementation in ./interactive-selectors.ts (S5 slice 5). Host-visible for ./interactive-queues.ts (拆分 slice 3). */
2302
+ showQueueManagerSelector() {
2303
+ selectorsFlow.showQueueManagerSelector(this);
2304
+ }
2305
+ /**
2306
+ * Session resume; implementation in ./interactive-selectors.ts (拆分 slice
2307
+ * 2). Kept as a delegate for the prototype-level test bindings.
2308
+ */
3558
2309
  async handleResumeSession(sessionPath, options) {
3559
- this.clearStatusIndicator();
3560
- try {
3561
- const result = await this.runtimeFacade.switchSession(sessionPath, {
3562
- withSession: options?.withSession,
3563
- });
3564
- if (result.cancelled) {
3565
- return result;
3566
- }
3567
- this.showStatus(t("Resumed session"));
3568
- this.detectCrossSuiteResume();
3569
- return result;
3570
- }
3571
- catch (error) {
3572
- if (error instanceof MissingSessionCwdError) {
3573
- const selectedCwd = await this.promptForMissingSessionCwd(error);
3574
- if (!selectedCwd) {
3575
- this.showStatus(t("Resume cancelled"));
3576
- return { cancelled: true };
3577
- }
3578
- const result = await this.runtimeFacade.switchSession(sessionPath, {
3579
- cwdOverride: selectedCwd,
3580
- withSession: options?.withSession,
3581
- });
3582
- if (result.cancelled) {
3583
- return result;
3584
- }
3585
- this.showStatus(t("Resumed session in current cwd"));
3586
- this.detectCrossSuiteResume();
3587
- return result;
3588
- }
3589
- return this.handleFatalRuntimeError(t("Failed to resume session"), error);
3590
- }
2310
+ return selectorsFlow.handleResumeSession(this, sessionPath, options);
3591
2311
  }
3592
2312
  /**
3593
2313
  * Cross-suite resume detection (设计 §10.1 最小可用版): compare the loaded
@@ -3597,6 +2317,9 @@ export class InteractiveMode {
3597
2317
  * behavior). On a mismatch, record the fact for later debugging: an info
3598
2318
  * status line plus a `suite_mismatch` custom entry on the session. No
3599
2319
  * context serialization or conversion is attempted.
2320
+ *
2321
+ * Host-visible for ./interactive-selectors.ts (拆分 slice 2): also called
2322
+ * from the startup/resume path on the mode, so it stays here.
3600
2323
  */
3601
2324
  detectCrossSuiteResume() {
3602
2325
  const originalSuiteId = this.getCurrentSessionSuiteId();
@@ -3730,637 +2453,88 @@ export class InteractiveMode {
3730
2453
  // Command handlers
3731
2454
  // =========================================================================
3732
2455
  async handleReloadCommand() {
3733
- if (this.facade.isStreaming()) {
3734
- this.showWarning(t("Wait for the current response to finish before reloading."));
3735
- return;
3736
- }
3737
- if (this.facade.isCompacting()) {
3738
- this.showWarning(t("Wait for compaction to finish before reloading."));
3739
- return;
3740
- }
3741
- this.resetDialogUI();
3742
- const reloadBox = new Container();
3743
- const borderColor = (s) => theme.fg("border", s);
3744
- reloadBox.addChild(new DynamicBorder(borderColor));
3745
- reloadBox.addChild(new Spacer(1));
3746
- reloadBox.addChild(new Text(theme.fg("muted", t("Reloading keybindings, skills, prompts, themes, and context files...")), 1, 0));
3747
- reloadBox.addChild(new Spacer(1));
3748
- reloadBox.addChild(new DynamicBorder(borderColor));
3749
- const previousEditor = this.editor;
3750
- this.editorContainer.clear();
3751
- this.editorContainer.addChild(reloadBox);
3752
- this.ui.setFocus(reloadBox);
3753
- this.ui.requestRender(true);
3754
- await new Promise((resolve) => process.nextTick(resolve));
3755
- const dismissReloadBox = (editor) => {
3756
- this.editorContainer.clear();
3757
- this.editorContainer.addChild(editor);
3758
- this.ui.setFocus(editor);
3759
- this.ui.requestRender();
3760
- };
3761
- let chatRestoredBeforeSessionStart = false;
3762
- let reloadBoxDismissed = false;
3763
- const restoreChatBeforeSessionStart = () => {
3764
- if (chatRestoredBeforeSessionStart) {
3765
- return;
3766
- }
3767
- this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3768
- this.outputPad = this.settingsManager.getOutputPad();
3769
- this.rebuildChatFromMessages();
3770
- chatRestoredBeforeSessionStart = true;
3771
- };
3772
- try {
3773
- await this.facade.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
3774
- restoreChatBeforeSessionStart();
3775
- this.keybindings.reload();
3776
- const activeHeader = this.builtInHeader;
3777
- if (isExpandable(activeHeader)) {
3778
- activeHeader.setExpanded(this.toolOutputExpanded);
3779
- }
3780
- setRegisteredThemes(buildRegisteredThemes(this.facade.getThemes().themes));
3781
- this.applyRuntimeSettings();
3782
- await this.themeController.applyFromSettings();
3783
- this.setupAutocompleteProvider();
3784
- this.showLoadedResources({
3785
- force: false,
3786
- showDiagnosticsWhenQuiet: true,
3787
- });
3788
- const modelsJsonError = this.facade.getModelsError();
3789
- if (modelsJsonError) {
3790
- this.showError(t("models.json error: {error}", { error: modelsJsonError }));
3791
- }
3792
- this.showStatus(t("Reloaded keybindings, skills, prompts, themes, and context files"));
3793
- dismissReloadBox(this.editor);
3794
- reloadBoxDismissed = true;
3795
- }
3796
- catch (error) {
3797
- // v8 ignore next -- reloadBoxDismissed 置位后 catch 前无可抛出点,恒走 dismiss 分支
3798
- if (!reloadBoxDismissed) {
3799
- dismissReloadBox(previousEditor);
3800
- }
3801
- this.showError(t("Reload failed: {error}", { error: error instanceof Error ? error.message : String(error) }));
3802
- }
2456
+ return handleReloadCommand(this);
3803
2457
  }
3804
2458
  async handleExportCommand(text) {
3805
- const outputPath = this.getPathCommandArgument(text, "/export");
3806
- try {
3807
- if (outputPath?.endsWith(".jsonl")) {
3808
- const filePath = this.facade.exportToJsonl(outputPath);
3809
- this.showStatus(t("Session exported to: {path}", { path: filePath }));
3810
- }
3811
- else {
3812
- const filePath = await this.facade.exportToHtml(outputPath, {
3813
- themeName: theme.name,
3814
- });
3815
- this.showStatus(t("Session exported to: {path}", { path: filePath }));
3816
- }
3817
- }
3818
- catch (error) {
3819
- this.showError(t("Failed to export session: {error}", {
3820
- error: error instanceof Error ? error.message : t("Unknown error"),
3821
- }));
3822
- }
2459
+ return handleExportCommand(this, text);
3823
2460
  }
2461
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: prototype-level test bindings call this delegate; implementation in ./interactive-commands.ts
3824
2462
  getPathCommandArgument(text, command) {
3825
- if (text === command) {
3826
- return undefined;
3827
- }
3828
- if (!text.startsWith(`${command} `)) {
3829
- return undefined;
3830
- }
3831
- const argsString = text.slice(command.length + 1).trimStart();
3832
- if (!argsString) {
3833
- return undefined;
3834
- }
3835
- const firstChar = argsString[0];
3836
- if (firstChar === '"' || firstChar === "'") {
3837
- const closingQuoteIndex = argsString.indexOf(firstChar, 1);
3838
- if (closingQuoteIndex < 0) {
3839
- return undefined;
3840
- }
3841
- return argsString.slice(1, closingQuoteIndex);
3842
- }
3843
- const firstWhitespaceIndex = argsString.search(/\s/);
3844
- if (firstWhitespaceIndex < 0) {
3845
- return argsString;
3846
- }
3847
- return argsString.slice(0, firstWhitespaceIndex);
2463
+ return getPathCommandArgument(text, command);
3848
2464
  }
3849
2465
  async handleImportCommand(text) {
3850
- const inputPath = this.getPathCommandArgument(text, "/import");
3851
- if (!inputPath) {
3852
- this.showError(t("Usage: /import <path.jsonl>"));
3853
- return;
3854
- }
3855
- const confirmed = await this.showDialogConfirm(t("Import session"), t("Replace current session with {path}?", { path: inputPath }));
3856
- if (!confirmed) {
3857
- this.showStatus(t("Import cancelled"));
3858
- return;
3859
- }
3860
- try {
3861
- this.clearStatusIndicator();
3862
- const result = await this.runtimeFacade.importFromJsonl(inputPath);
3863
- if (result.cancelled) {
3864
- this.showStatus(t("Import cancelled"));
3865
- return;
3866
- }
3867
- this.showStatus(t("Session imported from: {path}", { path: inputPath }));
3868
- }
3869
- catch (error) {
3870
- if (error instanceof MissingSessionCwdError) {
3871
- const selectedCwd = await this.promptForMissingSessionCwd(error);
3872
- if (!selectedCwd) {
3873
- this.showStatus(t("Import cancelled"));
3874
- return;
3875
- }
3876
- const result = await this.runtimeFacade.importFromJsonl(inputPath, selectedCwd);
3877
- if (result.cancelled) {
3878
- this.showStatus(t("Import cancelled"));
3879
- return;
3880
- }
3881
- this.showStatus(t("Session imported from: {path}", { path: inputPath }));
3882
- return;
3883
- }
3884
- if (error instanceof SessionImportFileNotFoundError) {
3885
- this.showError(t("Failed to import session: {error}", { error: error.message }));
3886
- return;
3887
- }
3888
- await this.handleFatalRuntimeError(t("Failed to import session"), error);
3889
- }
2466
+ return handleImportCommand(this, text);
3890
2467
  }
3891
2468
  async handleCopyCommand(options = {}) {
3892
- if (options.preferSelection &&
3893
- this.ui instanceof TuiAltScreen &&
3894
- !this.ui.getCopyOnSelect() &&
3895
- this.ui.hasActiveSelection()) {
3896
- await this.ui.copyActiveSelectionToClipboard();
3897
- return;
3898
- }
3899
- const text = this.facade.getLastAssistantText();
3900
- if (!text) {
3901
- this.showError(t("No agent messages to copy yet."));
3902
- return;
3903
- }
3904
- try {
3905
- await copyToClipboard(text);
3906
- if (options.flashConfirmation && this.ui instanceof TuiAltScreen) {
3907
- this.ui.flash(t("Copied!"));
3908
- }
3909
- else {
3910
- this.showStatus(t("Copied last agent message to clipboard"));
3911
- }
3912
- }
3913
- catch (error) {
3914
- this.showError(error instanceof Error ? error.message : String(error));
3915
- }
2469
+ return handleCopyCommand(this, options);
3916
2470
  }
3917
2471
  handleNameCommand(text) {
3918
- const name = text.replace(/^\/name\s*/, "").trim();
3919
- if (!name) {
3920
- const currentName = this.facade.getSessionName();
3921
- if (currentName) {
3922
- this.chatContainer.addChild(new Spacer(1));
3923
- this.chatContainer.addChild(new Text(theme.fg("dim", t("Session name: {name}", { name: currentName })), 1, 0));
3924
- }
3925
- else {
3926
- this.showWarning(t("Usage: /name <name>"));
3927
- }
3928
- this.ui.requestRender();
3929
- return;
3930
- }
3931
- this.facade.setSessionName(name);
3932
- const sessionName = this.facade.getSessionName();
3933
- if (sessionName !== name) {
3934
- this.showWarning(t("Session name was normalized from {from} to {to}", {
3935
- from: JSON.stringify(name),
3936
- to: JSON.stringify(sessionName),
3937
- }));
3938
- }
3939
- this.chatContainer.addChild(new Spacer(1));
3940
- this.chatContainer.addChild(new Text(theme.fg("dim", t("Session name set: {name}", { name: sessionName ?? name })), 1, 0));
3941
- this.ui.requestRender();
2472
+ handleNameCommand(this, text);
3942
2473
  }
3943
2474
  /**
3944
2475
  * 中断分级(设计 子代理运行时 §4.2,2026-10-02):/stop-agents 的唯一执行
3945
2476
  * 面——终止全部后台子代理(不触当前回合)。Esc 从不做这件事。
3946
2477
  */
3947
2478
  handleStopAgentsCommand() {
3948
- const active = this.facade.getActiveAgentTaskCount();
3949
- if (active === 0) {
3950
- this.showStatus(t("No background subagents running"));
3951
- return;
3952
- }
3953
- this.facade.abortAgentTasks();
3954
- this.showStatus(active === 1
3955
- ? t("Terminated {count} background subagent", { count: active })
3956
- : t("Terminated {count} background subagents", { count: active }));
2479
+ handleStopAgentsCommand(this);
3957
2480
  }
3958
2481
  handleSessionCommand() {
3959
- const stats = this.facade.getSessionStats();
3960
- const sessionName = this.facade.getSessionName();
3961
- const entries = this.facade.getEntries();
3962
- const cacheWaste = computeCacheWaste(entries, this.facade.getModelPriceSource());
3963
- // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
3964
- // resolves to a concrete responseModel). Usage without model attribution is
3965
- // grouped separately so the breakdown reconciles with the session total.
3966
- const usageBreakdown = getUsageCostBreakdown(entries);
3967
- let info = `${theme.bold(t("Session Info"))}\n\n`;
3968
- if (sessionName) {
3969
- info += `${theme.fg("dim", t("Name:"))} ${sessionName}\n`;
3970
- }
3971
- info += `${theme.fg("dim", t("File:"))} ${stats.sessionFile ?? t("In-memory")}\n`;
3972
- info += `${theme.fg("dim", t("ID:"))} ${stats.sessionId}\n\n`;
3973
- info += `${theme.bold(t("Messages"))}\n`;
3974
- info += `${theme.fg("dim", t("Total:"))} ${stats.totalMessages}\n`;
3975
- info += `${theme.fg("dim", t("User:"))} ${stats.userMessages}\n`;
3976
- info += `${theme.fg("dim", t("Assistant:"))} ${stats.assistantMessages}\n`;
3977
- info += `${theme.fg("dim", t("Tools:"))} ${t("{calls} calls, {results} results", {
3978
- calls: stats.toolCalls,
3979
- results: stats.toolResults,
3980
- })}\n\n`;
3981
- info += `${theme.bold(t("Tokens"))}\n`;
3982
- // "Input" is the full prompt volume. With cache activity, split it into
3983
- // cached (served from cache) vs uncached (everything else) - the only
3984
- // provider-independent split. Cache writes, where reported, are a detail
3985
- // of the uncached portion.
3986
- const { input, cacheRead, cacheWrite } = stats.tokens;
3987
- const promptTokens = input + cacheRead + cacheWrite;
3988
- info += `${theme.fg("dim", t("Input:"))} ${promptTokens.toLocaleString()}\n`;
3989
- if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
3990
- const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
3991
- info += ` ${theme.fg("dim", t("Cached:"))} ${cacheRead.toLocaleString()} ${hitRate}\n`;
3992
- const written = cacheWrite > 0
3993
- ? ` ${theme.fg("dim", t("({count} written to cache)", { count: cacheWrite.toLocaleString() }))}`
3994
- : "";
3995
- info += ` ${theme.fg("dim", t("Uncached:"))} ${(input + cacheWrite).toLocaleString()}${written}\n`;
3996
- }
3997
- info += `${theme.fg("dim", t("Output:"))} ${stats.tokens.output.toLocaleString()}\n`;
3998
- info += `${theme.fg("dim", t("Total:"))} ${stats.tokens.total.toLocaleString()}\n`;
3999
- // 双维度统计(用户裁决 2026-09-25):总计之外按供应商/模型分桶——缓存读/写、
4000
- // 命中率、费用逐桶可见。此前该拆分门控在 cost>0 且多桶才渲染,单价为 0 的
4001
- // 自定义供应商(如自建代理)永远看不到。
4002
- if (usageBreakdown.length > 0) {
4003
- info += `\n${theme.bold(t("By Provider/Model"))}\n`;
4004
- for (const entry of usageBreakdown) {
4005
- const bucket = entry.totals;
4006
- const promptBucket = bucket.input + bucket.cacheRead + bucket.cacheWrite;
4007
- let line = ` ${theme.bold(entry.key)}`;
4008
- if (promptBucket > 0) {
4009
- line += `\n ${theme.fg("dim", t("Input:"))} ${promptBucket.toLocaleString()}`;
4010
- if (bucket.cacheRead > 0 || bucket.cacheWrite > 0) {
4011
- const rate = promptBucket > 0 ? ((bucket.cacheRead / promptBucket) * 100).toFixed(1) : "0.0";
4012
- line += ` ${theme.fg("dim", t("(cache {rate}%, W {written})", { rate, written: bucket.cacheWrite.toLocaleString() }))}`;
4013
- }
4014
- }
4015
- if (bucket.output > 0)
4016
- line += `\n ${theme.fg("dim", t("Output:"))} ${bucket.output.toLocaleString()}`;
4017
- if (entry.cost > 0)
4018
- line += ` ${theme.fg("dim", `$${entry.cost.toFixed(3)}`)}`;
4019
- info += `${line}\n`;
4020
- }
4021
- }
4022
- if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4023
- info += `\n${theme.bold(t("Cost"))}\n`;
4024
- info += `${theme.fg("dim", t("Total:"))} $${stats.cost.toFixed(3)}`;
4025
- if (usageBreakdown.length > 1) {
4026
- for (const entry of usageBreakdown) {
4027
- info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", t("({tokens} tokens)", { tokens: formatTokens(entry.tokens) }))}`;
4028
- }
4029
- }
4030
- if (cacheWaste.missedTokens > 0) {
4031
- const missLabel = cacheWaste.missCount === 1
4032
- ? t("{count} miss", { count: cacheWaste.missCount })
4033
- : t("{count} misses", { count: cacheWaste.missCount });
4034
- const detail = t("{tokens} tokens, {misses}", {
4035
- tokens: cacheWaste.missedTokens.toLocaleString(),
4036
- misses: missLabel,
4037
- });
4038
- info +=
4039
- cacheWaste.missedCost >= 0.0001
4040
- ? `\n${theme.fg("dim", t("Cache Re-billed:"))} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
4041
- : `\n${theme.fg("dim", t("Cache Re-billed:"))} ${detail}`;
4042
- }
4043
- }
4044
- this.chatContainer.addChild(new Spacer(1));
4045
- this.chatContainer.addChild(new Text(info, 1, 0));
4046
- this.ui.requestRender();
2482
+ handleSessionCommand(this);
4047
2483
  }
4048
2484
  handleMetricsCommand(turnsArg) {
4049
- const provider = this.options.getMetrics;
4050
- if (!provider) {
4051
- this.showWarning(t("Metrics are not available: the observability plugin is not mounted in this session"));
4052
- return;
4053
- }
4054
- let windowTurns;
4055
- if (turnsArg !== undefined) {
4056
- const parsed = Number.parseInt(turnsArg, 10);
4057
- if (!Number.isSafeInteger(parsed) || parsed < 1) {
4058
- this.showWarning(t("Invalid turns: {turns}. Usage: /metrics [turns]", { turns: turnsArg }));
4059
- return;
4060
- }
4061
- windowTurns = parsed;
4062
- }
4063
- let snapshot;
4064
- try {
4065
- const result = provider({ windowTurns });
4066
- if (result === undefined) {
4067
- this.showWarning(t("Metrics are not available: the observability plugin is not mounted in this session"));
4068
- return;
4069
- }
4070
- snapshot = result;
4071
- }
4072
- catch (error) {
4073
- this.showError(error instanceof Error ? error.message : String(error));
4074
- return;
4075
- }
4076
- this.chatContainer.addChild(new Spacer(1));
4077
- this.chatContainer.addChild(new Text(formatMetricsSnapshot(snapshot), 1, 0));
4078
- this.ui.requestRender();
2485
+ handleMetricsCommand(this, turnsArg);
4079
2486
  }
4080
2487
  handleChangelogCommand() {
4081
- const changelogPath = getChangelogPath();
4082
- const allEntries = parseChangelog(changelogPath);
4083
- const changelogMarkdown = allEntries.length > 0
4084
- ? allEntries
4085
- .reverse()
4086
- .map((e) => normalizeChangelogLinks(e.content, e))
4087
- .join("\n\n")
4088
- : t("No changelog entries found.");
4089
- this.chatContainer.addChild(new Spacer(1));
4090
- this.chatContainer.addChild(new DynamicBorder());
4091
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", t("What's New"))), 1, 0));
4092
- this.chatContainer.addChild(new Spacer(1));
4093
- this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
4094
- this.chatContainer.addChild(new DynamicBorder());
4095
- this.ui.requestRender();
2488
+ handleChangelogCommand(this);
4096
2489
  }
4097
2490
  /**
4098
2491
  * Get capitalized display string for an app keybinding action.
2492
+ *
2493
+ * Host-visible for ./interactive-queues.ts (拆分 slice 3): the dequeue hint
2494
+ * in the pending-messages display.
4099
2495
  */
4100
2496
  getAppKeyDisplay(action) {
4101
- return keyDisplayText(action);
2497
+ return getAppKeyDisplay(action);
4102
2498
  }
4103
2499
  /**
4104
2500
  * Get capitalized display string for an editor keybinding action.
4105
2501
  */
2502
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: thin delegate kept per 拆分计划 rule 2; implementation in ./interactive-commands.ts
4106
2503
  getEditorKeyDisplay(action) {
4107
- return keyDisplayText(action);
2504
+ return getEditorKeyDisplay(action);
4108
2505
  }
4109
2506
  handleHelpCommand() {
4110
- const section = (title, rows) => {
4111
- if (rows.length === 0)
4112
- return "";
4113
- let table = `\n**${title}**\n| ${t("Command")} | ${t("Description")} |\n|---------|-------------|\n`;
4114
- for (const row of rows) {
4115
- const command = row.hint ? `\`/${row.name} ${row.hint}\`` : `\`/${row.name}\``;
4116
- table += `| ${command} | ${row.description ?? ""} |\n`;
4117
- }
4118
- return table;
4119
- };
4120
- const builtinRows = BUILTIN_SLASH_COMMANDS.map((command) => ({
4121
- name: command.name,
4122
- hint: command.argumentHint,
4123
- description: command.description,
4124
- }));
4125
- const promptRows = this.facade.getPromptTemplates().map((template) => ({
4126
- name: template.name,
4127
- hint: template.argumentHint,
4128
- description: template.description,
4129
- }));
4130
- const skillRows = this.facade.getSkills().skills.map((skill) => ({
4131
- name: `skill:${skill.name}`,
4132
- description: skill.description,
4133
- }));
4134
- const help = section(t("Built-in commands"), builtinRows) +
4135
- section(t("Prompt templates"), promptRows) +
4136
- section(t("Skills"), skillRows) +
4137
- `\n${t("Type `/` for autocomplete. Use `/hotkeys` for keyboard shortcuts.")}`;
4138
- this.chatContainer.addChild(new Spacer(1));
4139
- this.chatContainer.addChild(new DynamicBorder());
4140
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", t("Available Commands"))), 1, 0));
4141
- this.chatContainer.addChild(new Markdown(help.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
4142
- this.chatContainer.addChild(new DynamicBorder());
4143
- this.ui.requestRender();
2507
+ handleHelpCommand(this);
4144
2508
  }
4145
2509
  handleHotkeysCommand() {
4146
- // Navigation keybindings
4147
- const cursorUp = this.getEditorKeyDisplay("tui.editor.cursorUp");
4148
- const cursorDown = this.getEditorKeyDisplay("tui.editor.cursorDown");
4149
- const cursorLeft = this.getEditorKeyDisplay("tui.editor.cursorLeft");
4150
- const cursorRight = this.getEditorKeyDisplay("tui.editor.cursorRight");
4151
- const cursorWordLeft = this.getEditorKeyDisplay("tui.editor.cursorWordLeft");
4152
- const cursorWordRight = this.getEditorKeyDisplay("tui.editor.cursorWordRight");
4153
- const cursorLineStart = this.getEditorKeyDisplay("tui.editor.cursorLineStart");
4154
- const cursorLineEnd = this.getEditorKeyDisplay("tui.editor.cursorLineEnd");
4155
- const jumpForward = this.getEditorKeyDisplay("tui.editor.jumpForward");
4156
- const jumpBackward = this.getEditorKeyDisplay("tui.editor.jumpBackward");
4157
- const pageUp = this.getEditorKeyDisplay("tui.editor.pageUp");
4158
- const pageDown = this.getEditorKeyDisplay("tui.editor.pageDown");
4159
- // Editing keybindings
4160
- const submit = this.getEditorKeyDisplay("tui.input.submit");
4161
- const newLine = this.getEditorKeyDisplay("tui.input.newLine");
4162
- const deleteWordBackward = this.getEditorKeyDisplay("tui.editor.deleteWordBackward");
4163
- const deleteWordForward = this.getEditorKeyDisplay("tui.editor.deleteWordForward");
4164
- const deleteToLineStart = this.getEditorKeyDisplay("tui.editor.deleteToLineStart");
4165
- const deleteToLineEnd = this.getEditorKeyDisplay("tui.editor.deleteToLineEnd");
4166
- const yank = this.getEditorKeyDisplay("tui.editor.yank");
4167
- const yankPop = this.getEditorKeyDisplay("tui.editor.yankPop");
4168
- const undo = this.getEditorKeyDisplay("tui.editor.undo");
4169
- const tab = this.getEditorKeyDisplay("tui.input.tab");
4170
- // App keybindings
4171
- const interrupt = this.getAppKeyDisplay("app.interrupt");
4172
- const clear = this.getAppKeyDisplay("app.clear");
4173
- const exit = this.getAppKeyDisplay("app.exit");
4174
- const suspend = this.getAppKeyDisplay("app.suspend");
4175
- const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
4176
- const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
4177
- const selectModel = this.getAppKeyDisplay("app.model.select");
4178
- const expandTools = this.getAppKeyDisplay("app.tools.expand");
4179
- const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
4180
- const externalEditor = this.getAppKeyDisplay("app.editor.external");
4181
- const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
4182
- const copyMessage = this.getAppKeyDisplay("app.message.copy");
4183
- const followUp = this.getAppKeyDisplay("app.message.followUp");
4184
- const dequeue = this.getAppKeyDisplay("app.message.dequeue");
4185
- const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
4186
- // Iron law (2026-10-01): these shipped defaults were cleared in favor of
4187
- // the /model command; the row only appears when a user re-configures a key.
4188
- const selectModelRow = selectModel ? `| \`${selectModel}\` | ${t("Open model selector")} |\n` : "";
4189
- // Iron law (2026-10-02 复审): ctrl+g/ctrl+x stay bound only for the
4190
- // components without a command channel — the main editor ignores them
4191
- // (/editor and /copy are its single entries), so the rows say where
4192
- // each key actually acts.
4193
- const copyMessageRow = copyMessage ? `| \`${copyMessage}\` | ${t("Copy selection in /tree")} |\n` : "";
4194
- const externalEditorRow = externalEditor
4195
- ? `| \`${externalEditor}\` | ${t("External editor (inside dialog inputs)")} |\n`
4196
- : "";
4197
- // Iron law (2026-10-01): the message queue is the /queue and /dequeue
4198
- // commands' feature; rows only appear when a user re-configures a key.
4199
- const followUpRow = followUp ? `| \`${followUp}\` | ${t("Queue follow-up message")} |\n` : "";
4200
- const dequeueRow = dequeue ? `| \`${dequeue}\` | ${t("Restore queued messages")} |\n` : "";
4201
- const cycleThinkingRow = cycleThinkingLevel
4202
- ? `| \`${cycleThinkingLevel}\` | ${t("Cycle thinking level")} |\n`
4203
- : "";
4204
- const cycleModelsRow = cycleModelForward || cycleModelBackward
4205
- ? `| \`${cycleModelForward}\` / \`${cycleModelBackward}\` | ${t("Cycle models")} |\n`
4206
- : "";
4207
- // Suspend has no default key on native Windows; hide the row there.
4208
- const suspendRow = suspend ? `| \`${suspend}\` | ${t("Suspend to background")} |\n` : "";
4209
- const hotkeys = `
4210
- **${t("Navigation")}**
4211
- | ${t("Key")} | ${t("Action")} |
4212
- |-----|--------|
4213
- | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | ${t("Move cursor / browse history")} |
4214
- | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | ${t("Move by word")} |
4215
- | \`${cursorLineStart}\` | ${t("Start of line")} |
4216
- | \`${cursorLineEnd}\` | ${t("End of line")} |
4217
- | \`${jumpForward}\` | ${t("Jump forward to character")} |
4218
- | \`${jumpBackward}\` | ${t("Jump backward to character")} |
4219
- | \`${pageUp}\` / \`${pageDown}\` | ${t("Scroll by page")} |
4220
-
4221
- **${t("Editing")}**
4222
- | ${t("Key")} | ${t("Action")} |
4223
- |-----|--------|
4224
- | \`${submit}\` | ${t("Send message")} |
4225
- | \`${newLine}\` | ${process.platform === "win32" ? t("New line (Ctrl+Enter on Windows Terminal)") : t("New line")} |
4226
- | \`${deleteWordBackward}\` | ${t("Delete word backwards")} |
4227
- | \`${deleteWordForward}\` | ${t("Delete word forwards")} |
4228
- | \`${deleteToLineStart}\` | ${t("Delete to start of line")} |
4229
- | \`${deleteToLineEnd}\` | ${t("Delete to end of line")} |
4230
- | \`${yank}\` | ${t("Paste the most-recently-deleted text")} |
4231
- | \`${yankPop}\` | ${t("Cycle through the deleted text after pasting")} |
4232
- | \`${undo}\` | ${t("Undo")} |
4233
-
4234
- **${t("Other")}**
4235
- | ${t("Key")} | ${t("Action")} |
4236
- |-----|--------|
4237
- | \`${tab}\` | ${t("Path completion / accept autocomplete")} |
4238
- | \`${interrupt}\` | ${t("Cancel autocomplete / abort streaming")} |
4239
- | \`${clear}\` | ${t("Clear editor (first) / exit (second)")} |
4240
- | \`${exit}\` | ${t("Exit (when editor is empty)")} |
4241
- ${suspendRow}${cycleThinkingRow}${cycleModelsRow}${selectModelRow}| \`${expandTools}\` | ${t("Toggle tool output expansion")} |
4242
- | \`${toggleThinking}\` | ${t("Toggle thinking block visibility")} |
4243
- ${externalEditorRow}${copyMessageRow}${followUpRow}${dequeueRow}
4244
- | \`${pasteImage}\` | ${t("Paste image or text from clipboard")} |
4245
- | \`/\` | ${t("Slash commands")} |
4246
- | \`!\` | ${t("Run bash command")} |
4247
- | \`!!\` | ${t("Run bash command (excluded from context)")} |
4248
- `;
4249
- this.chatContainer.addChild(new Spacer(1));
4250
- this.chatContainer.addChild(new DynamicBorder());
4251
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", t("Keyboard Shortcuts"))), 1, 0));
4252
- this.chatContainer.addChild(new Spacer(1));
4253
- this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
4254
- this.chatContainer.addChild(new DynamicBorder());
4255
- this.ui.requestRender();
2510
+ handleHotkeysCommand(this);
4256
2511
  }
4257
2512
  async handleClearCommand() {
4258
- this.clearStatusIndicator();
4259
- try {
4260
- const result = await this.runtimeFacade.newSession();
4261
- if (result.cancelled) {
4262
- return;
4263
- }
4264
- this.chatContainer.addChild(new Spacer(1));
4265
- this.chatContainer.addChild(new Text(`${theme.fg("accent", t("✓ New session started"))}`, 1, 1));
4266
- this.ui.requestRender();
4267
- }
4268
- catch (error) {
4269
- await this.handleFatalRuntimeError("Failed to create session", error);
4270
- }
2513
+ return handleClearCommand(this);
4271
2514
  }
4272
2515
  handleDebugCommand() {
4273
- const width = this.ui.terminal.columns;
4274
- const height = this.ui.terminal.rows;
4275
- const allLines = this.ui.render(width);
4276
- const debugLogPath = getDebugLogPath();
4277
- const debugData = [
4278
- t("Debug output at {date}", { date: new Date().toISOString() }),
4279
- t("Terminal: {columns}x{rows}", { columns: width, rows: height }),
4280
- t("Total lines: {count}", { count: allLines.length }),
4281
- "",
4282
- t("=== All rendered lines with visible widths ==="),
4283
- ...allLines.map((line, idx) => {
4284
- const vw = visibleWidth(line);
4285
- const escaped = JSON.stringify(line);
4286
- return `[${idx}] (w=${vw}) ${escaped}`;
4287
- }),
4288
- "",
4289
- t("=== Agent messages (JSONL) ==="),
4290
- ...this.facade
4291
- .getSessionAPI()
4292
- .getMessages()
4293
- .map((msg) => JSON.stringify(msg)),
4294
- "",
4295
- ].join("\n");
4296
- fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
4297
- fs.writeFileSync(debugLogPath, debugData);
4298
- this.chatContainer.addChild(new Spacer(1));
4299
- this.chatContainer.addChild(new Text(`${theme.fg("accent", t("✓ Debug log written"))}\n${theme.fg("muted", debugLogPath)}`, 1, 1));
4300
- this.ui.requestRender();
2516
+ handleDebugCommand(this);
4301
2517
  }
4302
2518
  handleArminSaysHi() {
4303
- this.chatContainer.addChild(new Spacer(1));
4304
- this.chatContainer.addChild(new ArminComponent(this.ui));
4305
- this.ui.requestRender();
2519
+ handleArminSaysHi(this);
4306
2520
  }
2521
+ /** Host-visible for ./interactive-commands.ts (拆分 slice 1). */
4307
2522
  handleDaxnuts() {
4308
- this.chatContainer.addChild(new Spacer(1));
4309
- this.chatContainer.addChild(new DaxnutsComponent(this.ui));
4310
- this.ui.requestRender();
2523
+ handleDaxnuts(this);
4311
2524
  }
4312
2525
  /** Host-visible for ./interactive-auth.ts (S5 slice 3). */
4313
2526
  checkDaxnutsEasterEgg(model) {
4314
- if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
4315
- this.handleDaxnuts();
4316
- }
2527
+ checkDaxnutsEasterEgg(this, model);
4317
2528
  }
4318
2529
  async handleBashCommand(command, excludeFromContext = false) {
4319
- // Dispatch the user.bash lifecycle facts, then execute locally.
4320
- this.facade.emitUserBash(excludeFromContext);
4321
- // Normal execution path
4322
- const isDeferred = this.facade.isStreaming();
4323
- this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
4324
- if (isDeferred) {
4325
- // Show in pending area when agent is streaming
4326
- this.pendingMessagesContainer.addChild(this.bashComponent);
4327
- this.pendingBashComponents.push(this.bashComponent);
4328
- }
4329
- else {
4330
- // Show in chat immediately when agent is idle
4331
- this.chatContainer.addChild(this.bashComponent);
4332
- }
4333
- this.ui.requestRender();
4334
- try {
4335
- const result = await this.facade.executeBash(command, (chunk) => {
4336
- if (this.bashComponent) {
4337
- this.bashComponent.appendOutput(chunk);
4338
- this.ui.requestRender();
4339
- }
4340
- }, { excludeFromContext });
4341
- if (this.bashComponent) {
4342
- this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
4343
- }
4344
- }
4345
- catch (error) {
4346
- if (this.bashComponent) {
4347
- this.bashComponent.setComplete(undefined, false);
4348
- }
4349
- this.showError(t("Bash command failed: {error}", {
4350
- error: error instanceof Error ? error.message : t("Unknown error"),
4351
- }));
4352
- }
4353
- this.bashComponent = undefined;
4354
- this.ui.requestRender();
2530
+ await handleBashCommand(this, command, excludeFromContext);
4355
2531
  }
4356
2532
  async handleCompactCommand(customInstructions) {
4357
- this.clearStatusIndicator();
4358
- try {
4359
- await this.facade.compact(customInstructions);
4360
- }
4361
- catch {
4362
- // Ignore, will be emitted as an event
4363
- }
2533
+ return handleCompactCommand(this, customInstructions);
2534
+ }
2535
+ /** /clean-sessions:会话清理两阶段流程;输出/确认走宿主既有机制(设计 §7)。 */
2536
+ async handleCleanSessionsCommand(arg) {
2537
+ return handleCleanSessionsCommand(this, arg);
4364
2538
  }
4365
2539
  stop(fullscreenExitOutput = this.settingsManager.getFullscreenExitOutput()) {
4366
2540
  this.disposeActiveSelector();