@ai-agent-forge/agent-forge 0.88.2 → 0.88.4

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