@phi-code-admin/phi-code 0.74.3 → 0.75.1

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 (655) hide show
  1. package/CHANGELOG.md +1186 -4
  2. package/README.md +478 -379
  3. package/dist/bun/cli.d.ts +3 -0
  4. package/dist/bun/cli.d.ts.map +1 -0
  5. package/dist/bun/cli.js +9 -0
  6. package/dist/bun/cli.js.map +1 -0
  7. package/dist/bun/register-bedrock.d.ts +2 -0
  8. package/dist/bun/register-bedrock.d.ts.map +1 -0
  9. package/dist/bun/register-bedrock.js +4 -0
  10. package/dist/bun/register-bedrock.js.map +1 -0
  11. package/dist/bun/restore-sandbox-env.d.ts +13 -0
  12. package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
  13. package/dist/bun/restore-sandbox-env.js +32 -0
  14. package/dist/bun/restore-sandbox-env.js.map +1 -0
  15. package/dist/cli/args.d.ts +12 -7
  16. package/dist/cli/args.d.ts.map +1 -1
  17. package/dist/cli/args.js +87 -45
  18. package/dist/cli/args.js.map +1 -1
  19. package/dist/cli/config-selector.d.ts.map +1 -1
  20. package/dist/cli/config-selector.js.map +1 -1
  21. package/dist/cli/file-processor.d.ts.map +1 -1
  22. package/dist/cli/file-processor.js +4 -0
  23. package/dist/cli/file-processor.js.map +1 -1
  24. package/dist/cli/initial-message.d.ts +18 -0
  25. package/dist/cli/initial-message.d.ts.map +1 -0
  26. package/dist/cli/initial-message.js +22 -0
  27. package/dist/cli/initial-message.js.map +1 -0
  28. package/dist/cli/list-models.d.ts.map +1 -1
  29. package/dist/cli/list-models.js +7 -1
  30. package/dist/cli/list-models.js.map +1 -1
  31. package/dist/cli/session-picker.d.ts.map +1 -1
  32. package/dist/cli/session-picker.js +2 -1
  33. package/dist/cli/session-picker.js.map +1 -1
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +9 -5
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config.d.ts +24 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +226 -30
  40. package/dist/config.js.map +1 -1
  41. package/dist/core/agent-session-runtime.d.ts +117 -0
  42. package/dist/core/agent-session-runtime.d.ts.map +1 -0
  43. package/dist/core/agent-session-runtime.js +300 -0
  44. package/dist/core/agent-session-runtime.js.map +1 -0
  45. package/dist/core/agent-session-services.d.ts +86 -0
  46. package/dist/core/agent-session-services.d.ts.map +1 -0
  47. package/dist/core/agent-session-services.js +117 -0
  48. package/dist/core/agent-session-services.js.map +1 -0
  49. package/dist/core/agent-session.d.ts +63 -82
  50. package/dist/core/agent-session.d.ts.map +1 -1
  51. package/dist/core/agent-session.js +674 -628
  52. package/dist/core/agent-session.js.map +1 -1
  53. package/dist/core/api-key-store.d.ts +87 -0
  54. package/dist/core/api-key-store.d.ts.map +1 -0
  55. package/dist/core/api-key-store.js +168 -0
  56. package/dist/core/api-key-store.js.map +1 -0
  57. package/dist/core/auth-guidance.d.ts +5 -0
  58. package/dist/core/auth-guidance.d.ts.map +1 -0
  59. package/dist/core/auth-guidance.js +21 -0
  60. package/dist/core/auth-guidance.js.map +1 -0
  61. package/dist/core/auth-storage.d.ts +12 -5
  62. package/dist/core/auth-storage.d.ts.map +1 -1
  63. package/dist/core/auth-storage.js +34 -8
  64. package/dist/core/auth-storage.js.map +1 -1
  65. package/dist/core/bash-executor.d.ts +0 -15
  66. package/dist/core/bash-executor.d.ts.map +1 -1
  67. package/dist/core/bash-executor.js +28 -129
  68. package/dist/core/bash-executor.js.map +1 -1
  69. package/dist/core/compaction/branch-summarization.d.ts +2 -0
  70. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  71. package/dist/core/compaction/branch-summarization.js +3 -2
  72. package/dist/core/compaction/branch-summarization.js.map +1 -1
  73. package/dist/core/compaction/compaction.d.ts +4 -4
  74. package/dist/core/compaction/compaction.d.ts.map +1 -1
  75. package/dist/core/compaction/compaction.js +32 -27
  76. package/dist/core/compaction/compaction.js.map +1 -1
  77. package/dist/core/compaction/index.d.ts.map +1 -1
  78. package/dist/core/compaction/utils.d.ts.map +1 -1
  79. package/dist/core/compaction/utils.js.map +1 -1
  80. package/dist/core/config-watcher.d.ts +47 -0
  81. package/dist/core/config-watcher.d.ts.map +1 -0
  82. package/dist/core/config-watcher.js +135 -0
  83. package/dist/core/config-watcher.js.map +1 -0
  84. package/dist/core/default-models.json +80 -0
  85. package/dist/core/defaults.d.ts.map +1 -1
  86. package/dist/core/diagnostics.d.ts.map +1 -1
  87. package/dist/core/event-bus.d.ts.map +1 -1
  88. package/dist/core/event-bus.js.map +1 -1
  89. package/dist/core/exec.d.ts.map +1 -1
  90. package/dist/core/exec.js +7 -3
  91. package/dist/core/exec.js.map +1 -1
  92. package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
  93. package/dist/core/export-html/ansi-to-html.js +1 -1
  94. package/dist/core/export-html/ansi-to-html.js.map +1 -1
  95. package/dist/core/export-html/index.d.ts +7 -4
  96. package/dist/core/export-html/index.d.ts.map +1 -1
  97. package/dist/core/export-html/index.js +15 -13
  98. package/dist/core/export-html/index.js.map +1 -1
  99. package/dist/core/export-html/template.css +112 -17
  100. package/dist/core/export-html/template.html +1 -0
  101. package/dist/core/export-html/template.js +312 -64
  102. package/dist/core/export-html/tool-renderer.d.ts +9 -10
  103. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  104. package/dist/core/export-html/tool-renderer.js +61 -16
  105. package/dist/core/export-html/tool-renderer.js.map +1 -1
  106. package/dist/core/extensions/index.d.ts +5 -4
  107. package/dist/core/extensions/index.d.ts.map +1 -1
  108. package/dist/core/extensions/index.js +2 -2
  109. package/dist/core/extensions/index.js.map +1 -1
  110. package/dist/core/extensions/loader.d.ts +0 -1
  111. package/dist/core/extensions/loader.d.ts.map +1 -1
  112. package/dist/core/extensions/loader.js +98 -18
  113. package/dist/core/extensions/loader.js.map +1 -1
  114. package/dist/core/extensions/runner.d.ts +27 -14
  115. package/dist/core/extensions/runner.d.ts.map +1 -1
  116. package/dist/core/extensions/runner.js +299 -115
  117. package/dist/core/extensions/runner.js.map +1 -1
  118. package/dist/core/extensions/types.d.ts +200 -44
  119. package/dist/core/extensions/types.d.ts.map +1 -1
  120. package/dist/core/extensions/types.js +10 -0
  121. package/dist/core/extensions/types.js.map +1 -1
  122. package/dist/core/extensions/wrapper.d.ts +4 -11
  123. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  124. package/dist/core/extensions/wrapper.js +7 -87
  125. package/dist/core/extensions/wrapper.js.map +1 -1
  126. package/dist/core/footer-data-provider.d.ts +22 -2
  127. package/dist/core/footer-data-provider.d.ts.map +1 -1
  128. package/dist/core/footer-data-provider.js +225 -49
  129. package/dist/core/footer-data-provider.js.map +1 -1
  130. package/dist/core/index.d.ts +5 -2
  131. package/dist/core/index.d.ts.map +1 -1
  132. package/dist/core/index.js +5 -2
  133. package/dist/core/index.js.map +1 -1
  134. package/dist/core/keybindings.d.ts +348 -50
  135. package/dist/core/keybindings.d.ts.map +1 -1
  136. package/dist/core/keybindings.js +276 -132
  137. package/dist/core/keybindings.js.map +1 -1
  138. package/dist/core/messages.d.ts.map +1 -1
  139. package/dist/core/messages.js.map +1 -1
  140. package/dist/core/model-registry.d.ts +41 -5
  141. package/dist/core/model-registry.d.ts.map +1 -1
  142. package/dist/core/model-registry.js +316 -136
  143. package/dist/core/model-registry.js.map +1 -1
  144. package/dist/core/model-resolver.d.ts +6 -0
  145. package/dist/core/model-resolver.d.ts.map +1 -1
  146. package/dist/core/model-resolver.js +70 -37
  147. package/dist/core/model-resolver.js.map +1 -1
  148. package/dist/core/output-guard.d.ts +6 -0
  149. package/dist/core/output-guard.d.ts.map +1 -0
  150. package/dist/core/output-guard.js +59 -0
  151. package/dist/core/output-guard.js.map +1 -0
  152. package/dist/core/package-manager.d.ts +49 -7
  153. package/dist/core/package-manager.d.ts.map +1 -1
  154. package/dist/core/package-manager.js +655 -122
  155. package/dist/core/package-manager.js.map +1 -1
  156. package/dist/core/prompt-templates.d.ts +12 -10
  157. package/dist/core/prompt-templates.d.ts.map +1 -1
  158. package/dist/core/prompt-templates.js +37 -38
  159. package/dist/core/prompt-templates.js.map +1 -1
  160. package/dist/core/provider-display-names.d.ts +2 -0
  161. package/dist/core/provider-display-names.d.ts.map +1 -0
  162. package/dist/core/provider-display-names.js +33 -0
  163. package/dist/core/provider-display-names.js.map +1 -0
  164. package/dist/core/resolve-config-value.d.ts +6 -0
  165. package/dist/core/resolve-config-value.d.ts.map +1 -1
  166. package/dist/core/resolve-config-value.js +75 -8
  167. package/dist/core/resolve-config-value.js.map +1 -1
  168. package/dist/core/resource-loader.d.ts +18 -8
  169. package/dist/core/resource-loader.d.ts.map +1 -1
  170. package/dist/core/resource-loader.js +217 -123
  171. package/dist/core/resource-loader.js.map +1 -1
  172. package/dist/core/sdk.d.ts +25 -8
  173. package/dist/core/sdk.d.ts.map +1 -1
  174. package/dist/core/sdk.js +84 -37
  175. package/dist/core/sdk.js.map +1 -1
  176. package/dist/core/session-cwd.d.ts +19 -0
  177. package/dist/core/session-cwd.d.ts.map +1 -0
  178. package/dist/core/session-cwd.js +38 -0
  179. package/dist/core/session-cwd.js.map +1 -0
  180. package/dist/core/session-manager.d.ts +11 -1
  181. package/dist/core/session-manager.d.ts.map +1 -1
  182. package/dist/core/session-manager.js +42 -27
  183. package/dist/core/session-manager.js.map +1 -1
  184. package/dist/core/settings-manager.d.ts +34 -5
  185. package/dist/core/settings-manager.d.ts.map +1 -1
  186. package/dist/core/settings-manager.js +113 -13
  187. package/dist/core/settings-manager.js.map +1 -1
  188. package/dist/core/skills.d.ts +13 -11
  189. package/dist/core/skills.d.ts.map +1 -1
  190. package/dist/core/skills.js +59 -19
  191. package/dist/core/skills.js.map +1 -1
  192. package/dist/core/slash-commands.d.ts +2 -3
  193. package/dist/core/slash-commands.d.ts.map +1 -1
  194. package/dist/core/slash-commands.js +9 -6
  195. package/dist/core/slash-commands.js.map +1 -1
  196. package/dist/core/source-info.d.ts +18 -0
  197. package/dist/core/source-info.d.ts.map +1 -0
  198. package/dist/core/source-info.js +19 -0
  199. package/dist/core/source-info.js.map +1 -0
  200. package/dist/core/system-prompt.d.ts +3 -3
  201. package/dist/core/system-prompt.d.ts.map +1 -1
  202. package/dist/core/system-prompt.js +16 -55
  203. package/dist/core/system-prompt.js.map +1 -1
  204. package/dist/core/telemetry.d.ts +3 -0
  205. package/dist/core/telemetry.d.ts.map +1 -0
  206. package/dist/core/telemetry.js +9 -0
  207. package/dist/core/telemetry.js.map +1 -0
  208. package/dist/core/timings.d.ts +1 -0
  209. package/dist/core/timings.d.ts.map +1 -1
  210. package/dist/core/timings.js +6 -0
  211. package/dist/core/timings.js.map +1 -1
  212. package/dist/core/tools/bash.d.ts +27 -14
  213. package/dist/core/tools/bash.d.ts.map +1 -1
  214. package/dist/core/tools/bash.js +301 -208
  215. package/dist/core/tools/bash.js.map +1 -1
  216. package/dist/core/tools/edit-diff.d.ts +23 -1
  217. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  218. package/dist/core/tools/edit-diff.js +154 -59
  219. package/dist/core/tools/edit-diff.js.map +1 -1
  220. package/dist/core/tools/edit.d.ts +22 -12
  221. package/dist/core/tools/edit.d.ts.map +1 -1
  222. package/dist/core/tools/edit.js +243 -65
  223. package/dist/core/tools/edit.js.map +1 -1
  224. package/dist/core/tools/file-mutation-queue.d.ts +6 -0
  225. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
  226. package/dist/core/tools/file-mutation-queue.js +37 -0
  227. package/dist/core/tools/file-mutation-queue.js.map +1 -0
  228. package/dist/core/tools/find.d.ts +10 -14
  229. package/dist/core/tools/find.d.ts.map +1 -1
  230. package/dist/core/tools/find.js +202 -110
  231. package/dist/core/tools/find.js.map +1 -1
  232. package/dist/core/tools/grep.d.ts +14 -22
  233. package/dist/core/tools/grep.d.ts.map +1 -1
  234. package/dist/core/tools/grep.js +100 -35
  235. package/dist/core/tools/grep.js.map +1 -1
  236. package/dist/core/tools/index.d.ts +27 -60
  237. package/dist/core/tools/index.d.ts.map +1 -1
  238. package/dist/core/tools/index.js +96 -45
  239. package/dist/core/tools/index.js.map +1 -1
  240. package/dist/core/tools/ls.d.ts +8 -11
  241. package/dist/core/tools/ls.d.ts.map +1 -1
  242. package/dist/core/tools/ls.js +66 -15
  243. package/dist/core/tools/ls.js.map +1 -1
  244. package/dist/core/tools/output-accumulator.d.ts +50 -0
  245. package/dist/core/tools/output-accumulator.d.ts.map +1 -0
  246. package/dist/core/tools/output-accumulator.js +178 -0
  247. package/dist/core/tools/output-accumulator.js.map +1 -0
  248. package/dist/core/tools/path-utils.d.ts.map +1 -1
  249. package/dist/core/tools/path-utils.js +1 -1
  250. package/dist/core/tools/path-utils.js.map +1 -1
  251. package/dist/core/tools/read.d.ts +9 -13
  252. package/dist/core/tools/read.d.ts.map +1 -1
  253. package/dist/core/tools/read.js +175 -52
  254. package/dist/core/tools/read.js.map +1 -1
  255. package/dist/core/tools/render-utils.d.ts +21 -0
  256. package/dist/core/tools/render-utils.d.ts.map +1 -0
  257. package/dist/core/tools/render-utils.js +49 -0
  258. package/dist/core/tools/render-utils.js.map +1 -0
  259. package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
  260. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
  261. package/dist/core/tools/tool-definition-wrapper.js +34 -0
  262. package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
  263. package/dist/core/tools/truncate.d.ts.map +1 -1
  264. package/dist/core/tools/truncate.js.map +1 -1
  265. package/dist/core/tools/write.d.ts +8 -11
  266. package/dist/core/tools/write.d.ts.map +1 -1
  267. package/dist/core/tools/write.js +167 -32
  268. package/dist/core/tools/write.js.map +1 -1
  269. package/dist/index.d.ts +12 -9
  270. package/dist/index.d.ts.map +1 -1
  271. package/dist/index.js +12 -10
  272. package/dist/index.js.map +1 -1
  273. package/dist/main.d.ts +5 -1
  274. package/dist/main.d.ts.map +1 -1
  275. package/dist/main.js +326 -404
  276. package/dist/main.js.map +1 -1
  277. package/dist/migrations.d.ts +2 -2
  278. package/dist/migrations.d.ts.map +1 -1
  279. package/dist/migrations.js +24 -4
  280. package/dist/migrations.js.map +1 -1
  281. package/dist/modes/index.d.ts.map +1 -1
  282. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  283. package/dist/modes/interactive/components/armin.js +10 -6
  284. package/dist/modes/interactive/components/armin.js.map +1 -1
  285. package/dist/modes/interactive/components/assistant-message.d.ts +5 -1
  286. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  287. package/dist/modes/interactive/components/assistant-message.js +32 -3
  288. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  289. package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
  290. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  291. package/dist/modes/interactive/components/bash-execution.js +31 -12
  292. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  293. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  294. package/dist/modes/interactive/components/bordered-loader.js +7 -1
  295. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  296. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  297. package/dist/modes/interactive/components/branch-summary-message.js +5 -3
  298. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  299. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  300. package/dist/modes/interactive/components/compaction-summary-message.js +5 -3
  301. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  302. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  303. package/dist/modes/interactive/components/config-selector.js +49 -16
  304. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  305. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
  306. package/dist/modes/interactive/components/countdown-timer.js +5 -0
  307. package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  308. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  309. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  310. package/dist/modes/interactive/components/custom-editor.js +14 -7
  311. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  312. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  313. package/dist/modes/interactive/components/custom-message.js +6 -1
  314. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  315. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  316. package/dist/modes/interactive/components/daxnuts.js +8 -6
  317. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  318. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  319. package/dist/modes/interactive/components/diff.js.map +1 -1
  320. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  321. package/dist/modes/interactive/components/dynamic-border.js +1 -0
  322. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  323. package/dist/modes/interactive/components/earendil-announcement.d.ts +5 -0
  324. package/dist/modes/interactive/components/earendil-announcement.d.ts.map +1 -0
  325. package/dist/modes/interactive/components/earendil-announcement.js +40 -0
  326. package/dist/modes/interactive/components/earendil-announcement.js.map +1 -0
  327. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  328. package/dist/modes/interactive/components/extension-editor.js +16 -10
  329. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  330. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  331. package/dist/modes/interactive/components/extension-input.js +13 -7
  332. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  333. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  334. package/dist/modes/interactive/components/extension-selector.js +18 -11
  335. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  336. package/dist/modes/interactive/components/footer.d.ts +1 -0
  337. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  338. package/dist/modes/interactive/components/footer.js +7 -2
  339. package/dist/modes/interactive/components/footer.js.map +1 -1
  340. package/dist/modes/interactive/components/index.d.ts +1 -1
  341. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  342. package/dist/modes/interactive/components/index.js +1 -1
  343. package/dist/modes/interactive/components/index.js.map +1 -1
  344. package/dist/modes/interactive/components/keybinding-hints.d.ts +8 -36
  345. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  346. package/dist/modes/interactive/components/keybinding-hints.js +23 -48
  347. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  348. package/dist/modes/interactive/components/login-dialog.d.ts +5 -1
  349. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  350. package/dist/modes/interactive/components/login-dialog.js +35 -14
  351. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  352. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  353. package/dist/modes/interactive/components/model-selector.js +41 -22
  354. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  355. package/dist/modes/interactive/components/oauth-selector.d.ts +18 -6
  356. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  357. package/dist/modes/interactive/components/oauth-selector.js +104 -31
  358. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  359. package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -12
  360. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  361. package/dist/modes/interactive/components/scoped-models-selector.js +61 -42
  362. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  363. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  364. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  365. package/dist/modes/interactive/components/session-selector.d.ts +2 -1
  366. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  367. package/dist/modes/interactive/components/session-selector.js +109 -73
  368. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  369. package/dist/modes/interactive/components/settings-selector.d.ts +9 -0
  370. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  371. package/dist/modes/interactive/components/settings-selector.js +84 -4
  372. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  373. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  374. package/dist/modes/interactive/components/show-images-selector.js +6 -1
  375. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  376. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  377. package/dist/modes/interactive/components/skill-invocation-message.js +5 -3
  378. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  379. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  380. package/dist/modes/interactive/components/theme-selector.js +7 -1
  381. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  382. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  383. package/dist/modes/interactive/components/thinking-selector.js +6 -1
  384. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  385. package/dist/modes/interactive/components/tool-execution.d.ts +20 -34
  386. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  387. package/dist/modes/interactive/components/tool-execution.js +158 -636
  388. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  389. package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
  390. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  391. package/dist/modes/interactive/components/tree-selector.js +224 -52
  392. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  393. package/dist/modes/interactive/components/user-message-selector.d.ts +2 -2
  394. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  395. package/dist/modes/interactive/components/user-message-selector.js +20 -16
  396. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  397. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  398. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  399. package/dist/modes/interactive/components/user-message.js +8 -6
  400. package/dist/modes/interactive/components/user-message.js.map +1 -1
  401. package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
  402. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  403. package/dist/modes/interactive/interactive-mode.d.ts +67 -39
  404. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  405. package/dist/modes/interactive/interactive-mode.js +1587 -710
  406. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  407. package/dist/modes/interactive/theme/dark.json +1 -1
  408. package/dist/modes/interactive/theme/light.json +1 -1
  409. package/dist/modes/interactive/theme/theme.d.ts +3 -0
  410. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  411. package/dist/modes/interactive/theme/theme.js +101 -72
  412. package/dist/modes/interactive/theme/theme.js.map +1 -1
  413. package/dist/modes/print-mode.d.ts +2 -2
  414. package/dist/modes/print-mode.d.ts.map +1 -1
  415. package/dist/modes/print-mode.js +107 -77
  416. package/dist/modes/print-mode.js.map +1 -1
  417. package/dist/modes/rpc/jsonl.d.ts +17 -0
  418. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  419. package/dist/modes/rpc/jsonl.js +49 -0
  420. package/dist/modes/rpc/jsonl.js.map +1 -0
  421. package/dist/modes/rpc/rpc-client.d.ts +8 -1
  422. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  423. package/dist/modes/rpc/rpc-client.js +22 -16
  424. package/dist/modes/rpc/rpc-client.js.map +1 -1
  425. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  426. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  427. package/dist/modes/rpc/rpc-mode.js +184 -94
  428. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  429. package/dist/modes/rpc/rpc-types.d.ts +14 -4
  430. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  431. package/dist/modes/rpc/rpc-types.js.map +1 -1
  432. package/dist/package-manager-cli.d.ts +4 -0
  433. package/dist/package-manager-cli.d.ts.map +1 -0
  434. package/dist/package-manager-cli.js +460 -0
  435. package/dist/package-manager-cli.js.map +1 -0
  436. package/dist/utils/changelog.d.ts.map +1 -1
  437. package/dist/utils/changelog.js.map +1 -1
  438. package/dist/utils/child-process.d.ts +12 -0
  439. package/dist/utils/child-process.d.ts.map +1 -0
  440. package/dist/utils/child-process.js +86 -0
  441. package/dist/utils/child-process.js.map +1 -0
  442. package/dist/utils/clipboard-image.d.ts.map +1 -1
  443. package/dist/utils/clipboard-image.js +94 -11
  444. package/dist/utils/clipboard-image.js.map +1 -1
  445. package/dist/utils/clipboard-native.d.ts +1 -0
  446. package/dist/utils/clipboard-native.d.ts.map +1 -1
  447. package/dist/utils/clipboard-native.js.map +1 -1
  448. package/dist/utils/clipboard.d.ts +1 -1
  449. package/dist/utils/clipboard.d.ts.map +1 -1
  450. package/dist/utils/clipboard.js +96 -46
  451. package/dist/utils/clipboard.js.map +1 -1
  452. package/dist/utils/exif-orientation.d.ts +5 -0
  453. package/dist/utils/exif-orientation.d.ts.map +1 -0
  454. package/dist/utils/exif-orientation.js +158 -0
  455. package/dist/utils/exif-orientation.js.map +1 -0
  456. package/dist/utils/frontmatter.d.ts.map +1 -1
  457. package/dist/utils/frontmatter.js.map +1 -1
  458. package/dist/utils/fs-watch.d.ts +5 -0
  459. package/dist/utils/fs-watch.d.ts.map +1 -0
  460. package/dist/utils/fs-watch.js +25 -0
  461. package/dist/utils/fs-watch.js.map +1 -0
  462. package/dist/utils/git.d.ts.map +1 -1
  463. package/dist/utils/git.js.map +1 -1
  464. package/dist/utils/image-convert.d.ts.map +1 -1
  465. package/dist/utils/image-convert.js +5 -1
  466. package/dist/utils/image-convert.js.map +1 -1
  467. package/dist/utils/image-resize.d.ts +5 -5
  468. package/dist/utils/image-resize.d.ts.map +1 -1
  469. package/dist/utils/image-resize.js +51 -95
  470. package/dist/utils/image-resize.js.map +1 -1
  471. package/dist/utils/mime.d.ts.map +1 -1
  472. package/dist/utils/mime.js.map +1 -1
  473. package/dist/utils/paths.d.ts +16 -0
  474. package/dist/utils/paths.d.ts.map +1 -0
  475. package/dist/utils/paths.js +50 -0
  476. package/dist/utils/paths.js.map +1 -0
  477. package/dist/utils/photon.d.ts.map +1 -1
  478. package/dist/utils/photon.js.map +1 -1
  479. package/dist/utils/pi-user-agent.d.ts +2 -0
  480. package/dist/utils/pi-user-agent.d.ts.map +1 -0
  481. package/dist/utils/pi-user-agent.js +5 -0
  482. package/dist/utils/pi-user-agent.js.map +1 -0
  483. package/dist/utils/shell.d.ts +10 -6
  484. package/dist/utils/shell.d.ts.map +1 -1
  485. package/dist/utils/shell.js +29 -25
  486. package/dist/utils/shell.js.map +1 -1
  487. package/dist/utils/sleep.d.ts.map +1 -1
  488. package/dist/utils/sleep.js.map +1 -1
  489. package/dist/utils/tools-manager.d.ts.map +1 -1
  490. package/dist/utils/tools-manager.js +11 -6
  491. package/dist/utils/tools-manager.js.map +1 -1
  492. package/dist/utils/version-check.d.ts +14 -0
  493. package/dist/utils/version-check.d.ts.map +1 -0
  494. package/dist/utils/version-check.js +77 -0
  495. package/dist/utils/version-check.js.map +1 -0
  496. package/docs/compaction.md +394 -0
  497. package/docs/custom-provider.md +646 -0
  498. package/docs/development.md +71 -0
  499. package/docs/docs.json +148 -0
  500. package/docs/extensions.md +2596 -0
  501. package/docs/images/doom-extension.png +0 -0
  502. package/docs/images/exy.png +0 -0
  503. package/docs/images/interactive-mode.png +0 -0
  504. package/docs/images/tree-view.png +0 -0
  505. package/docs/index.md +70 -0
  506. package/docs/json.md +82 -0
  507. package/docs/keybindings.md +197 -0
  508. package/docs/models.md +474 -0
  509. package/docs/packages.md +223 -0
  510. package/docs/prompt-templates.md +88 -0
  511. package/docs/providers.md +243 -0
  512. package/docs/quickstart.md +142 -0
  513. package/docs/rpc.md +1407 -0
  514. package/docs/sdk.md +1149 -0
  515. package/docs/session-format.md +412 -0
  516. package/docs/sessions.md +137 -0
  517. package/docs/settings.md +279 -0
  518. package/docs/shell-aliases.md +13 -0
  519. package/docs/skills.md +232 -0
  520. package/docs/terminal-setup.md +106 -0
  521. package/docs/termux.md +127 -0
  522. package/docs/themes.md +295 -0
  523. package/docs/tmux.md +61 -0
  524. package/docs/tui.md +918 -0
  525. package/docs/usage.md +277 -0
  526. package/docs/windows.md +17 -0
  527. package/examples/README.md +25 -0
  528. package/examples/extensions/README.md +208 -0
  529. package/examples/extensions/auto-commit-on-exit.ts +49 -0
  530. package/examples/extensions/bash-spawn-hook.ts +30 -0
  531. package/examples/extensions/bookmark.ts +50 -0
  532. package/examples/extensions/border-status-editor.ts +150 -0
  533. package/examples/extensions/built-in-tool-renderer.ts +249 -0
  534. package/examples/extensions/claude-rules.ts +86 -0
  535. package/examples/extensions/commands.ts +72 -0
  536. package/examples/extensions/confirm-destructive.ts +59 -0
  537. package/examples/extensions/custom-compaction.ts +127 -0
  538. package/examples/extensions/custom-footer.ts +64 -0
  539. package/examples/extensions/custom-header.ts +73 -0
  540. package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  541. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  542. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  543. package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
  544. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  545. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
  546. package/examples/extensions/dirty-repo-guard.ts +56 -0
  547. package/examples/extensions/doom-overlay/README.md +46 -0
  548. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  549. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  550. package/examples/extensions/doom-overlay/doom/build.sh +152 -0
  551. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  552. package/examples/extensions/doom-overlay/doom-component.ts +132 -0
  553. package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  554. package/examples/extensions/doom-overlay/doom-keys.ts +104 -0
  555. package/examples/extensions/doom-overlay/index.ts +74 -0
  556. package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  557. package/examples/extensions/dynamic-resources/SKILL.md +8 -0
  558. package/examples/extensions/dynamic-resources/dynamic.json +79 -0
  559. package/examples/extensions/dynamic-resources/dynamic.md +5 -0
  560. package/examples/extensions/dynamic-resources/index.ts +15 -0
  561. package/examples/extensions/dynamic-tools.ts +74 -0
  562. package/examples/extensions/event-bus.ts +43 -0
  563. package/examples/extensions/file-trigger.ts +41 -0
  564. package/examples/extensions/git-checkpoint.ts +53 -0
  565. package/examples/extensions/github-issue-autocomplete.ts +185 -0
  566. package/examples/extensions/handoff.ts +191 -0
  567. package/examples/extensions/hello.ts +26 -0
  568. package/examples/extensions/hidden-thinking-label.ts +53 -0
  569. package/examples/extensions/inline-bash.ts +94 -0
  570. package/examples/extensions/input-transform.ts +43 -0
  571. package/examples/extensions/interactive-shell.ts +196 -0
  572. package/examples/extensions/mac-system-theme.ts +47 -0
  573. package/examples/extensions/message-renderer.ts +59 -0
  574. package/examples/extensions/minimal-mode.ts +426 -0
  575. package/examples/extensions/modal-editor.ts +85 -0
  576. package/examples/extensions/model-status.ts +31 -0
  577. package/examples/extensions/notify.ts +55 -0
  578. package/examples/extensions/overlay-qa-tests.ts +1348 -0
  579. package/examples/extensions/overlay-test.ts +150 -0
  580. package/examples/extensions/permission-gate.ts +34 -0
  581. package/examples/extensions/pirate.ts +47 -0
  582. package/examples/extensions/plan-mode/README.md +65 -0
  583. package/examples/extensions/plan-mode/index.ts +340 -0
  584. package/examples/extensions/plan-mode/utils.ts +168 -0
  585. package/examples/extensions/preset.ts +430 -0
  586. package/examples/extensions/prompt-customizer.ts +97 -0
  587. package/examples/extensions/protected-paths.ts +30 -0
  588. package/examples/extensions/provider-payload.ts +18 -0
  589. package/examples/extensions/qna.ts +122 -0
  590. package/examples/extensions/question.ts +264 -0
  591. package/examples/extensions/questionnaire.ts +427 -0
  592. package/examples/extensions/rainbow-editor.ts +88 -0
  593. package/examples/extensions/reload-runtime.ts +37 -0
  594. package/examples/extensions/rpc-demo.ts +118 -0
  595. package/examples/extensions/sandbox/index.ts +321 -0
  596. package/examples/extensions/sandbox/package-lock.json +92 -0
  597. package/examples/extensions/sandbox/package.json +19 -0
  598. package/examples/extensions/send-user-message.ts +97 -0
  599. package/examples/extensions/session-name.ts +27 -0
  600. package/examples/extensions/shutdown-command.ts +63 -0
  601. package/examples/extensions/snake.ts +343 -0
  602. package/examples/extensions/space-invaders.ts +560 -0
  603. package/examples/extensions/ssh.ts +220 -0
  604. package/examples/extensions/status-line.ts +32 -0
  605. package/examples/extensions/structured-output.ts +65 -0
  606. package/examples/extensions/subagent/README.md +172 -0
  607. package/examples/extensions/subagent/agents/planner.md +37 -0
  608. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  609. package/examples/extensions/subagent/agents/scout.md +50 -0
  610. package/examples/extensions/subagent/agents/worker.md +24 -0
  611. package/examples/extensions/subagent/agents.ts +126 -0
  612. package/examples/extensions/subagent/index.ts +987 -0
  613. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  614. package/examples/extensions/subagent/prompts/implement.md +10 -0
  615. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  616. package/examples/extensions/summarize.ts +206 -0
  617. package/examples/extensions/system-prompt-header.ts +17 -0
  618. package/examples/extensions/tic-tac-toe.ts +1008 -0
  619. package/examples/extensions/timed-confirm.ts +70 -0
  620. package/examples/extensions/titlebar-spinner.ts +58 -0
  621. package/examples/extensions/todo.ts +297 -0
  622. package/examples/extensions/tool-override.ts +144 -0
  623. package/examples/extensions/tools.ts +141 -0
  624. package/examples/extensions/trigger-compact.ts +50 -0
  625. package/examples/extensions/truncated-tool.ts +195 -0
  626. package/examples/extensions/widget-placement.ts +9 -0
  627. package/examples/extensions/with-deps/index.ts +32 -0
  628. package/examples/extensions/with-deps/package-lock.json +31 -0
  629. package/examples/extensions/with-deps/package.json +22 -0
  630. package/examples/extensions/working-indicator.ts +123 -0
  631. package/examples/extensions/working-message-test.ts +25 -0
  632. package/examples/rpc-extension-ui.ts +632 -0
  633. package/examples/sdk/01-minimal.ts +22 -0
  634. package/examples/sdk/02-custom-model.ts +49 -0
  635. package/examples/sdk/03-custom-prompt.ts +62 -0
  636. package/examples/sdk/04-skills.ts +55 -0
  637. package/examples/sdk/05-tools.ts +44 -0
  638. package/examples/sdk/06-extensions.ts +90 -0
  639. package/examples/sdk/07-context-files.ts +42 -0
  640. package/examples/sdk/08-prompt-templates.ts +51 -0
  641. package/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  642. package/examples/sdk/10-settings.ts +53 -0
  643. package/examples/sdk/11-sessions.ts +48 -0
  644. package/examples/sdk/12-full-control.ts +73 -0
  645. package/examples/sdk/13-session-runtime.ts +67 -0
  646. package/examples/sdk/README.md +147 -0
  647. package/extensions/phi/init.ts +15 -1
  648. package/extensions/phi/keys.ts +186 -0
  649. package/extensions/phi/providers/alibaba.ts +126 -0
  650. package/extensions/phi/providers/opencode-go.ts +204 -0
  651. package/extensions/phi/setup.ts +692 -0
  652. package/extensions/phi/smart-router.ts +8 -0
  653. package/package.json +17 -12
  654. package/scripts/copy-assets.sh +0 -0
  655. package/scripts/migrate-sessions.sh +0 -0
@@ -6,35 +6,48 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "phi-code-tui";
10
9
  import { spawn, spawnSync } from "child_process";
11
- import { APP_NAME, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
10
+ import { getProviders, } from "phi-code-ai";
11
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "phi-code-tui";
12
+ import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
12
13
  import { parseSkillBlock } from "../../core/agent-session.js";
14
+ import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
13
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
14
16
  import { KeybindingsManager } from "../../core/keybindings.js";
15
17
  import { createCompactionSummaryMessage } from "../../core/messages.js";
16
- import { resolveModelScope } from "../../core/model-resolver.js";
18
+ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
19
+ import { DefaultPackageManager } from "../../core/package-manager.js";
20
+ import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
21
+ import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
17
22
  import { SessionManager } from "../../core/session-manager.js";
18
23
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
24
+ import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
19
25
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
20
26
  import { copyToClipboard } from "../../utils/clipboard.js";
21
27
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
28
+ import { parseGitUrl } from "../../utils/git.js";
29
+ import { getCwdRelativePath } from "../../utils/paths.js";
30
+ import { getPiUserAgent } from "../../utils/pi-user-agent.js";
31
+ import { killTrackedDetachedChildren } from "../../utils/shell.js";
22
32
  import { ensureTool } from "../../utils/tools-manager.js";
33
+ import { checkForNewPiVersion } from "../../utils/version-check.js";
23
34
  import { ArminComponent } from "./components/armin.js";
24
35
  import { AssistantMessageComponent } from "./components/assistant-message.js";
25
36
  import { BashExecutionComponent } from "./components/bash-execution.js";
26
37
  import { BorderedLoader } from "./components/bordered-loader.js";
27
38
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
28
39
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
40
+ import { CountdownTimer } from "./components/countdown-timer.js";
29
41
  import { CustomEditor } from "./components/custom-editor.js";
30
42
  import { CustomMessageComponent } from "./components/custom-message.js";
31
43
  import { DaxnutsComponent } from "./components/daxnuts.js";
32
44
  import { DynamicBorder } from "./components/dynamic-border.js";
45
+ import { EarendilAnnouncementComponent } from "./components/earendil-announcement.js";
33
46
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
34
47
  import { ExtensionInputComponent } from "./components/extension-input.js";
35
48
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
36
49
  import { FooterComponent } from "./components/footer.js";
37
- import { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./components/keybinding-hints.js";
50
+ import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
38
51
  import { LoginDialogComponent } from "./components/login-dialog.js";
39
52
  import { ModelSelectorComponent } from "./components/model-selector.js";
40
53
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
@@ -46,12 +59,140 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
46
59
  import { TreeSelectorComponent } from "./components/tree-selector.js";
47
60
  import { UserMessageComponent } from "./components/user-message.js";
48
61
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
49
- import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, Theme, theme, } from "./theme/theme.js";
62
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
50
63
  function isExpandable(obj) {
51
64
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
52
65
  }
66
+ class ExpandableText extends Text {
67
+ getCollapsedText;
68
+ getExpandedText;
69
+ constructor(getCollapsedText, getExpandedText, expanded = false, paddingX = 0, paddingY = 0) {
70
+ super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
71
+ this.getCollapsedText = getCollapsedText;
72
+ this.getExpandedText = getExpandedText;
73
+ }
74
+ setExpanded(expanded) {
75
+ this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
76
+ }
77
+ }
78
+ const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
79
+ function isDeadTerminalError(error) {
80
+ if (!error || typeof error !== "object" || !("code" in error)) {
81
+ return false;
82
+ }
83
+ const code = error.code;
84
+ return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
85
+ }
86
+ const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
87
+ function isAnthropicSubscriptionAuthKey(apiKey) {
88
+ return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
89
+ }
90
+ function isUnknownModel(model) {
91
+ return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
92
+ }
93
+ function hasDefaultModelProvider(providerId) {
94
+ return providerId in defaultModelPerProvider;
95
+ }
96
+ const BEDROCK_PROVIDER_ID = "amazon-bedrock";
97
+ const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
98
+ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
99
+ if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
100
+ return true;
101
+ }
102
+ if (builtInProviderIds.has(providerId)) {
103
+ return false;
104
+ }
105
+ return !oauthProviderIds.has(providerId);
106
+ }
53
107
  export class InteractiveMode {
108
+ options;
109
+ runtimeHost;
110
+ ui;
111
+ chatContainer;
112
+ pendingMessagesContainer;
113
+ statusContainer;
114
+ defaultEditor;
115
+ editor;
116
+ editorComponentFactory;
117
+ autocompleteProvider;
118
+ autocompleteProviderWrappers = [];
119
+ fdPath;
120
+ editorContainer;
121
+ footer;
122
+ footerDataProvider;
123
+ // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
124
+ keybindings;
125
+ version;
126
+ isInitialized = false;
127
+ onInputCallback;
128
+ loadingAnimation = undefined;
129
+ workingMessage = undefined;
130
+ workingVisible = true;
131
+ workingIndicatorOptions = undefined;
132
+ defaultWorkingMessage = "Working...";
133
+ defaultHiddenThinkingLabel = "Thinking...";
134
+ hiddenThinkingLabel = this.defaultHiddenThinkingLabel;
135
+ lastSigintTime = 0;
136
+ lastEscapeTime = 0;
137
+ changelogMarkdown = undefined;
138
+ startupNoticesShown = false;
139
+ anthropicSubscriptionWarningShown = false;
140
+ // Status line tracking (for mutating immediately-sequential status updates)
141
+ lastStatusSpacer = undefined;
142
+ lastStatusText = undefined;
143
+ // Streaming message tracking
144
+ streamingComponent = undefined;
145
+ streamingMessage = undefined;
146
+ // Tool execution tracking: toolCallId -> component
147
+ pendingTools = new Map();
148
+ // Tool output expansion state
149
+ toolOutputExpanded = false;
150
+ // Thinking block visibility state
151
+ hideThinkingBlock = false;
152
+ // Skill commands: command name -> skill file path
153
+ skillCommands = new Map();
154
+ // Agent subscription unsubscribe function
155
+ unsubscribe;
156
+ signalCleanupHandlers = [];
157
+ // Track if editor is in bash mode (text starts with !)
158
+ isBashMode = false;
159
+ // Track current bash execution component
160
+ bashComponent = undefined;
161
+ // Track pending bash components (shown in pending area, moved to chat on submit)
162
+ pendingBashComponents = [];
163
+ // Auto-compaction state
164
+ autoCompactionLoader = undefined;
165
+ autoCompactionEscapeHandler;
166
+ // Auto-retry state
167
+ retryLoader = undefined;
168
+ retryCountdown = undefined;
169
+ retryEscapeHandler;
170
+ // Messages queued while compaction is running
171
+ compactionQueuedMessages = [];
172
+ // Shutdown state
173
+ shutdownRequested = false;
174
+ // Extension UI state
175
+ extensionSelector = undefined;
176
+ extensionInput = undefined;
177
+ extensionEditor = undefined;
178
+ extensionTerminalInputUnsubscribers = new Set();
179
+ // Extension widgets (components rendered above/below the editor)
180
+ extensionWidgetsAbove = new Map();
181
+ extensionWidgetsBelow = new Map();
182
+ widgetContainerAbove;
183
+ widgetContainerBelow;
184
+ // Custom footer from extension (undefined = use built-in footer)
185
+ customFooter = undefined;
186
+ // Header container that holds the built-in or custom header
187
+ headerContainer;
188
+ // Built-in header (logo + keybinding hints + changelog)
189
+ builtInHeader = undefined;
190
+ // Custom header from extension (undefined = use built-in header)
191
+ customHeader = undefined;
54
192
  // Convenience accessors
193
+ get session() {
194
+ return this.runtimeHost.session;
195
+ }
55
196
  get agent() {
56
197
  return this.session.agent;
57
198
  }
@@ -61,63 +202,15 @@ export class InteractiveMode {
61
202
  get settingsManager() {
62
203
  return this.session.settingsManager;
63
204
  }
64
- constructor(session, options = {}) {
205
+ constructor(runtimeHost, options = {}) {
65
206
  this.options = options;
66
- this.isInitialized = false;
67
- this.loadingAnimation = undefined;
68
- this.pendingWorkingMessage = undefined;
69
- this.defaultWorkingMessage = "Working...";
70
- this.lastSigintTime = 0;
71
- this.lastEscapeTime = 0;
72
- this.changelogMarkdown = undefined;
73
- // Status line tracking (for mutating immediately-sequential status updates)
74
- this.lastStatusSpacer = undefined;
75
- this.lastStatusText = undefined;
76
- // Streaming message tracking
77
- this.streamingComponent = undefined;
78
- this.streamingMessage = undefined;
79
- // Tool execution tracking: toolCallId -> component
80
- this.pendingTools = new Map();
81
- // Tool output expansion state
82
- this.toolOutputExpanded = false;
83
- // Thinking block visibility state
84
- this.hideThinkingBlock = false;
85
- // Skill commands: command name -> skill file path
86
- this.skillCommands = new Map();
87
- // Track if editor is in bash mode (text starts with !)
88
- this.isBashMode = false;
89
- // Track current bash execution component
90
- this.bashComponent = undefined;
91
- // Track pending bash components (shown in pending area, moved to chat on submit)
92
- this.pendingBashComponents = [];
93
- // Auto-compaction state
94
- this.autoCompactionLoader = undefined;
95
- // Auto-retry state
96
- this.retryLoader = undefined;
97
- // Messages queued while compaction is running
98
- this.compactionQueuedMessages = [];
99
- // Shutdown state
100
- this.shutdownRequested = false;
101
- // Extension UI state
102
- this.extensionSelector = undefined;
103
- this.extensionInput = undefined;
104
- this.extensionEditor = undefined;
105
- this.extensionTerminalInputUnsubscribers = new Set();
106
- // Extension widgets (components rendered above/below the editor)
107
- this.extensionWidgetsAbove = new Map();
108
- this.extensionWidgetsBelow = new Map();
109
- // Custom footer from extension (undefined = use built-in footer)
110
- this.customFooter = undefined;
111
- // Built-in header (logo + keybinding hints + changelog)
112
- this.builtInHeader = undefined;
113
- // Custom header from extension (undefined = use built-in header)
114
- this.customHeader = undefined;
115
- /**
116
- * Gracefully shutdown the agent.
117
- * Emits shutdown event to extensions, then exits.
118
- */
119
- this.isShuttingDown = false;
120
- this.session = session;
207
+ this.runtimeHost = runtimeHost;
208
+ this.runtimeHost.setBeforeSessionInvalidate(() => {
209
+ this.resetExtensionUI();
210
+ });
211
+ this.runtimeHost.setRebindSession(async () => {
212
+ await this.rebindCurrentSession();
213
+ });
121
214
  this.version = VERSION;
122
215
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
123
216
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
@@ -128,6 +221,7 @@ export class InteractiveMode {
128
221
  this.widgetContainerAbove = new Container();
129
222
  this.widgetContainerBelow = new Container();
130
223
  this.keybindings = KeybindingsManager.create();
224
+ setKeybindings(this.keybindings);
131
225
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
132
226
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
133
227
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
@@ -137,16 +231,55 @@ export class InteractiveMode {
137
231
  this.editor = this.defaultEditor;
138
232
  this.editorContainer = new Container();
139
233
  this.editorContainer.addChild(this.editor);
140
- this.footerDataProvider = new FooterDataProvider();
141
- this.footer = new FooterComponent(session, this.footerDataProvider);
142
- this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
234
+ this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
235
+ this.footer = new FooterComponent(this.session, this.footerDataProvider);
236
+ this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
143
237
  // Load hide thinking block setting
144
238
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
145
239
  // Register themes from resource loader and initialize
146
240
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
147
241
  initTheme(this.settingsManager.getTheme(), true);
148
242
  }
149
- setupAutocomplete(fdPath) {
243
+ getAutocompleteSourceTag(sourceInfo) {
244
+ if (!sourceInfo) {
245
+ return undefined;
246
+ }
247
+ const scopePrefix = sourceInfo.scope === "user" ? "u" : sourceInfo.scope === "project" ? "p" : "t";
248
+ const source = sourceInfo.source.trim();
249
+ if (source === "auto" || source === "local" || source === "cli") {
250
+ return scopePrefix;
251
+ }
252
+ if (source.startsWith("npm:")) {
253
+ return `${scopePrefix}:${source}`;
254
+ }
255
+ const gitSource = parseGitUrl(source);
256
+ if (gitSource) {
257
+ const ref = gitSource.ref ? `@${gitSource.ref}` : "";
258
+ return `${scopePrefix}:git:${gitSource.host}/${gitSource.path}${ref}`;
259
+ }
260
+ return scopePrefix;
261
+ }
262
+ prefixAutocompleteDescription(description, sourceInfo) {
263
+ const sourceTag = this.getAutocompleteSourceTag(sourceInfo);
264
+ if (!sourceTag) {
265
+ return description;
266
+ }
267
+ return description ? `[${sourceTag}] ${description}` : `[${sourceTag}]`;
268
+ }
269
+ getBuiltInCommandConflictDiagnostics(extensionRunner) {
270
+ const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
271
+ return extensionRunner
272
+ .getRegisteredCommands()
273
+ .filter((command) => builtinNames.has(command.name))
274
+ .map((command) => ({
275
+ type: "warning",
276
+ message: command.invocationName === command.name
277
+ ? `Extension command '/${command.name}' conflicts with built-in interactive command. Skipping in autocomplete.`
278
+ : `Extension command '/${command.name}' conflicts with built-in interactive command. Available as '/${command.invocationName}'.`,
279
+ path: command.sourceInfo.path,
280
+ }));
281
+ }
282
+ createBaseAutocompleteProvider() {
150
283
  // Define commands for autocomplete
151
284
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
152
285
  name: command.name,
@@ -181,13 +314,17 @@ export class InteractiveMode {
181
314
  // Convert prompt templates to SlashCommand format for autocomplete
182
315
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
183
316
  name: cmd.name,
184
- description: cmd.description,
317
+ description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
318
+ ...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
185
319
  }));
186
320
  // Convert extension commands to SlashCommand format
187
321
  const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
188
- const extensionCommands = (this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []).map((cmd) => ({
189
- name: cmd.name,
190
- description: cmd.description ?? "(extension command)",
322
+ const extensionCommands = this.session.extensionRunner
323
+ .getRegisteredCommands()
324
+ .filter((cmd) => !builtinCommandNames.has(cmd.name))
325
+ .map((cmd) => ({
326
+ name: cmd.invocationName,
327
+ description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
191
328
  getArgumentCompletions: cmd.getArgumentCompletions,
192
329
  }));
193
330
  // Build skill commands from session.skills (if enabled)
@@ -197,19 +334,55 @@ export class InteractiveMode {
197
334
  for (const skill of this.session.resourceLoader.getSkills().skills) {
198
335
  const commandName = `skill:${skill.name}`;
199
336
  this.skillCommands.set(commandName, skill.filePath);
200
- skillCommandList.push({ name: commandName, description: skill.description });
337
+ skillCommandList.push({
338
+ name: commandName,
339
+ description: this.prefixAutocompleteDescription(skill.description, skill.sourceInfo),
340
+ });
201
341
  }
202
342
  }
203
- // Setup autocomplete
204
- this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), fdPath);
205
- this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
343
+ return new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], this.sessionManager.getCwd(), this.fdPath);
344
+ }
345
+ setupAutocompleteProvider() {
346
+ let provider = this.createBaseAutocompleteProvider();
347
+ for (const wrapProvider of this.autocompleteProviderWrappers) {
348
+ provider = wrapProvider(provider);
349
+ }
350
+ this.autocompleteProvider = provider;
351
+ this.defaultEditor.setAutocompleteProvider(provider);
206
352
  if (this.editor !== this.defaultEditor) {
207
- this.editor.setAutocompleteProvider?.(this.autocompleteProvider);
353
+ this.editor.setAutocompleteProvider?.(provider);
354
+ }
355
+ }
356
+ showStartupNoticesIfNeeded() {
357
+ if (this.startupNoticesShown) {
358
+ return;
359
+ }
360
+ this.startupNoticesShown = true;
361
+ if (!this.changelogMarkdown) {
362
+ return;
363
+ }
364
+ if (this.chatContainer.children.length > 0) {
365
+ this.chatContainer.addChild(new Spacer(1));
366
+ }
367
+ this.chatContainer.addChild(new DynamicBorder());
368
+ if (this.settingsManager.getCollapseChangelog()) {
369
+ const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
370
+ const latestVersion = versionMatch ? versionMatch[1] : this.version;
371
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
372
+ this.chatContainer.addChild(new Text(condensedText, 1, 0));
208
373
  }
374
+ else {
375
+ this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
376
+ this.chatContainer.addChild(new Spacer(1));
377
+ this.chatContainer.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
378
+ this.chatContainer.addChild(new Spacer(1));
379
+ }
380
+ this.chatContainer.addChild(new DynamicBorder());
209
381
  }
210
382
  async init() {
211
383
  if (this.isInitialized)
212
384
  return;
385
+ this.registerSignalHandlers();
213
386
  // Load changelog (only show new entries, skip for resumed sessions)
214
387
  this.changelogMarkdown = this.getChangelogForDisplay();
215
388
  // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
@@ -218,102 +391,99 @@ export class InteractiveMode {
218
391
  this.fdPath = fdPath;
219
392
  // Add header container as first child
220
393
  this.ui.addChild(this.headerContainer);
221
- // Always show the gradient logo
222
- {
223
- // Gradient colors: yellow orange pink → magenta
224
- const gradientColors = [
225
- [255, 230, 0], // yellow
226
- [255, 180, 0], // orange-yellow
227
- [255, 140, 0], // orange
228
- [255, 100, 50], // red-orange
229
- [255, 70, 100], // pink
230
- [220, 50, 150], // hot pink
231
- [200, 50, 180], // magenta-pink
232
- [180, 50, 220], // magenta
233
- ];
234
- const applyGradient = (line) => {
235
- const chars = [...line];
236
- const visibleChars = chars.filter(c => c !== ' ');
237
- const totalVisible = visibleChars.length;
238
- let visibleIdx = 0;
239
- return chars.map(c => {
240
- if (c === ' ')
241
- return c;
242
- const t = totalVisible > 1 ? visibleIdx / (totalVisible - 1) : 0;
243
- visibleIdx++;
244
- const segLen = gradientColors.length - 1;
245
- const seg = Math.min(Math.floor(t * segLen), segLen - 1);
246
- const local = (t * segLen) - seg;
247
- const [r1, g1, b1] = gradientColors[seg];
248
- const [r2, g2, b2] = gradientColors[seg + 1];
249
- const r = Math.round(r1 + (r2 - r1) * local);
250
- const g = Math.round(g1 + (g2 - g1) * local);
251
- const b = Math.round(b1 + (b2 - b1) * local);
252
- return `\x1b[38;2;${r};${g};${b}m${c}\x1b[0m`;
253
- }).join('');
254
- };
255
- const asciiLines = [
256
- " ██████╗ ██╗ ██╗██╗",
257
- " ██╔══██╗██║ ██║██║",
258
- " ██████╔╝███████║██║",
259
- " ██╔═══╝ ██╔══██║██║",
260
- " ██║ ██║ ██║██║",
261
- " ╚═╝ ╚═╝ ╚═╝╚═╝",
262
- ];
263
- const asciiLogo = asciiLines.map(line => applyGradient(line)).join("\n");
264
- const phiLabel = applyGradient( " + APP_NAME.toUpperCase());
265
- const logo = asciiLogo + "\n " + phiLabel + theme.fg("dim", ` v${this.version}`) + theme.fg("dim", " — The Ultimate Coding Agent");
266
- // Show keyboard shortcuts only when not quiet
267
- let headerContent = logo;
268
- if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
269
- const kb = this.keybindings;
270
- const hint = (action, desc) => appKeyHint(kb, action, desc);
271
- const instructions = [
272
- hint("interrupt", "to interrupt"),
273
- hint("clear", "to clear"),
274
- rawKeyHint(`${appKey(kb, "clear")} twice`, "to exit"),
275
- hint("exit", "to exit (empty)"),
276
- hint("suspend", "to suspend"),
277
- keyHint("deleteToLineEnd", "to delete to end"),
278
- hint("cycleThinkingLevel", "to cycle thinking level"),
279
- rawKeyHint(`${appKey(kb, "cycleModelForward")}/${appKey(kb, "cycleModelBackward")}`, "to cycle models"),
280
- hint("selectModel", "to select model"),
281
- hint("expandTools", "to expand tools"),
282
- hint("toggleThinking", "to expand thinking"),
283
- hint("externalEditor", "for external editor"),
284
- rawKeyHint("/", "for commands"),
285
- rawKeyHint("!", "to run bash"),
286
- rawKeyHint("!!", "to run bash (no context)"),
287
- hint("followUp", "to queue follow-up"),
288
- hint("dequeue", "to edit all queued messages"),
289
- hint("pasteImage", "to paste image"),
290
- rawKeyHint("drop files", "to attach"),
291
- ].join("\n");
292
- headerContent = `${logo}\n${instructions}`;
293
- }
294
- this.builtInHeader = new Text(headerContent, 1, 0);
295
- // Setup UI layout
296
- this.headerContainer.addChild(new Spacer(1));
297
- this.headerContainer.addChild(this.builtInHeader);
298
- this.headerContainer.addChild(new Spacer(1));
299
- // Add changelog if provided
300
- if (this.changelogMarkdown) {
301
- this.headerContainer.addChild(new DynamicBorder());
302
- if (this.settingsManager.getCollapseChangelog()) {
303
- const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
304
- const latestVersion = versionMatch ? versionMatch[1] : this.version;
305
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
306
- this.headerContainer.addChild(new Text(condensedText, 1, 0));
307
- }
308
- else {
309
- this.headerContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
310
- this.headerContainer.addChild(new Spacer(1));
311
- this.headerContainer.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
312
- this.headerContainer.addChild(new Spacer(1));
313
- }
314
- this.headerContainer.addChild(new DynamicBorder());
315
- }
394
+ // Phi Code gradient ASCII logo (yellow -> orange -> pink -> magenta).
395
+ // ALWAYS displayed at startup, independent of quietStartup setting.
396
+ // quietStartup only gates the keyboard shortcuts panel below.
397
+ const gradientColors = [
398
+ [255, 220, 50],
399
+ [255, 165, 50],
400
+ [255, 100, 100],
401
+ [230, 80, 150],
402
+ [200, 50, 180],
403
+ [180, 50, 220],
404
+ ];
405
+ const applyGradient = (line) => {
406
+ const chars = [...line];
407
+ const visibleChars = chars.filter((c) => c !== " ");
408
+ const totalVisible = visibleChars.length;
409
+ let visibleIdx = 0;
410
+ return chars
411
+ .map((c) => {
412
+ if (c === " ")
413
+ return c;
414
+ const t = totalVisible > 1 ? visibleIdx / (totalVisible - 1) : 0;
415
+ visibleIdx++;
416
+ const segLen = gradientColors.length - 1;
417
+ const seg = Math.min(Math.floor(t * segLen), segLen - 1);
418
+ const local = t * segLen - seg;
419
+ const [r1, g1, b1] = gradientColors[seg];
420
+ const [r2, g2, b2] = gradientColors[seg + 1];
421
+ const r = Math.round(r1 + (r2 - r1) * local);
422
+ const g = Math.round(g1 + (g2 - g1) * local);
423
+ const b = Math.round(b1 + (b2 - b1) * local);
424
+ return `\x1b[38;2;${r};${g};${b}m${c}\x1b[0m`;
425
+ })
426
+ .join("");
427
+ };
428
+ const asciiLines = [
429
+ " ██████╗ ██╗ ██╗██╗",
430
+ " ██╔══██╗██║ ██║██║",
431
+ " ██████╔╝███████║██║",
432
+ " ██╔═══╝ ██╔══██║██║",
433
+ " ██║ ██║ ██║██║",
434
+ " ╚═╝ ╚═╝ ╚═╝╚═╝",
435
+ ];
436
+ const asciiLogo = asciiLines.map((line) => applyGradient(line)).join("\n");
437
+ const phiLabel = applyGradient( ${APP_NAME.toUpperCase()}`);
438
+ const logo = asciiLogo +
439
+ "\n " +
440
+ phiLabel +
441
+ theme.fg("dim", ` v${this.version}`) +
442
+ theme.fg("dim", " The Ultimate Coding Agent");
443
+ // Show keyboard hints panel only when not silenced (quietStartup)
444
+ if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
445
+ // Build startup instructions using keybinding hint helpers
446
+ const hint = (keybinding, description) => keyHint(keybinding, description);
447
+ const expandedInstructions = [
448
+ hint("app.interrupt", "to interrupt"),
449
+ hint("app.clear", "to clear"),
450
+ rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
451
+ hint("app.exit", "to exit (empty)"),
452
+ hint("app.suspend", "to suspend"),
453
+ keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
454
+ hint("app.thinking.cycle", "to cycle thinking level"),
455
+ rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
456
+ hint("app.model.select", "to select model"),
457
+ hint("app.tools.expand", "to expand tools"),
458
+ hint("app.thinking.toggle", "to expand thinking"),
459
+ hint("app.editor.external", "for external editor"),
460
+ rawKeyHint("/", "for commands"),
461
+ rawKeyHint("!", "to run bash"),
462
+ rawKeyHint("!!", "to run bash (no context)"),
463
+ hint("app.message.followUp", "to queue follow-up"),
464
+ hint("app.message.dequeue", "to edit all queued messages"),
465
+ hint("app.clipboard.pasteImage", "to paste image"),
466
+ rawKeyHint("drop files", "to attach"),
467
+ ].join("\n");
468
+ const compactInstructions = [
469
+ hint("app.interrupt", "interrupt"),
470
+ rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
471
+ rawKeyHint("/", "commands"),
472
+ rawKeyHint("!", "bash"),
473
+ hint("app.tools.expand", "more"),
474
+ ].join(theme.fg("muted", " · "));
475
+ const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
476
+ const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
477
+ this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
478
+ }
479
+ else {
480
+ // quietStartup: show only the logo, no keyboard hints panel
481
+ this.builtInHeader = new Text(logo, 1, 0);
316
482
  }
483
+ // Setup UI layout (always show the header with logo)
484
+ this.headerContainer.addChild(new Spacer(1));
485
+ this.headerContainer.addChild(this.builtInHeader);
486
+ this.headerContainer.addChild(new Spacer(1));
317
487
  this.ui.addChild(this.chatContainer);
318
488
  this.ui.addChild(this.pendingMessagesContainer);
319
489
  this.ui.addChild(this.statusContainer);
@@ -325,17 +495,13 @@ export class InteractiveMode {
325
495
  this.ui.setFocus(this.editor);
326
496
  this.setupKeyHandlers();
327
497
  this.setupEditorSubmitHandler();
498
+ // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
499
+ this.ui.start();
500
+ this.isInitialized = true;
328
501
  // Initialize extensions first so resources are shown before messages
329
- await this.initExtensions();
502
+ await this.rebindCurrentSession();
330
503
  // Render initial messages AFTER showing loaded resources
331
504
  this.renderInitialMessages();
332
- // Start the UI
333
- this.ui.start();
334
- this.isInitialized = true;
335
- // Set terminal title
336
- this.updateTerminalTitle();
337
- // Subscribe to agent events
338
- this.subscribeToAgent();
339
505
  // Set up theme file watcher
340
506
  onThemeChange(() => {
341
507
  this.ui.invalidate();
@@ -353,13 +519,13 @@ export class InteractiveMode {
353
519
  * Update terminal title with session name and cwd.
354
520
  */
355
521
  updateTerminalTitle() {
356
- const cwdBasename = path.basename(process.cwd());
522
+ const cwdBasename = path.basename(this.sessionManager.getCwd());
357
523
  const sessionName = this.sessionManager.getSessionName();
358
524
  if (sessionName) {
359
- this.ui.terminal.setTitle( - ${sessionName} - ${cwdBasename}`);
525
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
360
526
  }
361
527
  else {
362
- this.ui.terminal.setTitle( - ${cwdBasename}`);
528
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
363
529
  }
364
530
  }
365
531
  /**
@@ -369,11 +535,23 @@ export class InteractiveMode {
369
535
  async run() {
370
536
  await this.init();
371
537
  // Start version check asynchronously
372
- this.checkForNewVersion().then((newVersion) => {
538
+ checkForNewPiVersion(this.version).then((newVersion) => {
373
539
  if (newVersion) {
374
540
  this.showNewVersionNotification(newVersion);
375
541
  }
376
542
  });
543
+ // Start package update check asynchronously
544
+ this.checkForPackageUpdates().then((updates) => {
545
+ if (updates.length > 0) {
546
+ this.showPackageUpdateNotification(updates);
547
+ }
548
+ });
549
+ // Check tmux keyboard setup asynchronously
550
+ this.checkTmuxKeyboardSetup().then((warning) => {
551
+ if (warning) {
552
+ this.showWarning(warning);
553
+ }
554
+ });
377
555
  // Show startup warnings
378
556
  const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
379
557
  if (migratedProviders && migratedProviders.length > 0) {
@@ -386,6 +564,7 @@ export class InteractiveMode {
386
564
  if (modelFallbackMessage) {
387
565
  this.showWarning(modelFallbackMessage);
388
566
  }
567
+ void this.maybeWarnAboutAnthropicSubscriptionAuth();
389
568
  // Process initial messages
390
569
  if (initialMessage) {
391
570
  try {
@@ -419,28 +598,63 @@ export class InteractiveMode {
419
598
  }
420
599
  }
421
600
  }
422
- /**
423
- * Check npm registry for a newer version.
424
- */
425
- async checkForNewVersion() {
426
- if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
427
- return undefined;
601
+ async checkForPackageUpdates() {
602
+ if (process.env.PI_OFFLINE) {
603
+ return [];
604
+ }
428
605
  try {
429
- const response = await fetch("https://registry.npmjs.org/phi-code/latest", {
430
- signal: AbortSignal.timeout(10000),
606
+ const packageManager = new DefaultPackageManager({
607
+ cwd: this.sessionManager.getCwd(),
608
+ agentDir: getAgentDir(),
609
+ settingsManager: this.settingsManager,
431
610
  });
432
- if (!response.ok)
433
- return undefined;
434
- const data = (await response.json());
435
- const latestVersion = data.version;
436
- if (latestVersion && latestVersion !== this.version) {
437
- return latestVersion;
438
- }
439
- return undefined;
611
+ const updates = await packageManager.checkForAvailableUpdates();
612
+ return updates.map((update) => update.displayName);
440
613
  }
441
614
  catch {
615
+ return [];
616
+ }
617
+ }
618
+ async checkTmuxKeyboardSetup() {
619
+ if (!process.env.TMUX)
620
+ return undefined;
621
+ const runTmuxShow = (option) => {
622
+ return new Promise((resolve) => {
623
+ const proc = spawn("tmux", ["show", "-gv", option], {
624
+ stdio: ["ignore", "pipe", "ignore"],
625
+ });
626
+ let stdout = "";
627
+ const timer = setTimeout(() => {
628
+ proc.kill();
629
+ resolve(undefined);
630
+ }, 2000);
631
+ proc.stdout?.on("data", (data) => {
632
+ stdout += data.toString();
633
+ });
634
+ proc.on("error", () => {
635
+ clearTimeout(timer);
636
+ resolve(undefined);
637
+ });
638
+ proc.on("close", (code) => {
639
+ clearTimeout(timer);
640
+ resolve(code === 0 ? stdout.trim() : undefined);
641
+ });
642
+ });
643
+ };
644
+ const [extendedKeys, extendedKeysFormat] = await Promise.all([
645
+ runTmuxShow("extended-keys"),
646
+ runTmuxShow("extended-keys-format"),
647
+ ]);
648
+ // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
649
+ if (extendedKeys === undefined)
442
650
  return undefined;
651
+ if (extendedKeys !== "on" && extendedKeys !== "always") {
652
+ return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
653
+ }
654
+ if (extendedKeysFormat === "xterm") {
655
+ return "tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
443
656
  }
657
+ return undefined;
444
658
  }
445
659
  /**
446
660
  * Get changelog entries to display on startup.
@@ -455,19 +669,35 @@ export class InteractiveMode {
455
669
  const changelogPath = getChangelogPath();
456
670
  const entries = parseChangelog(changelogPath);
457
671
  if (!lastVersion) {
458
- // Fresh install - just record the version, don't show changelog
672
+ // Fresh install - record the version, send telemetry, don't show changelog
459
673
  this.settingsManager.setLastChangelogVersion(VERSION);
674
+ this.reportInstallTelemetry(VERSION);
460
675
  return undefined;
461
676
  }
462
- else {
463
- const newEntries = getNewEntries(entries, lastVersion);
464
- if (newEntries.length > 0) {
465
- this.settingsManager.setLastChangelogVersion(VERSION);
466
- return newEntries.map((e) => e.content).join("\n\n");
467
- }
677
+ const newEntries = getNewEntries(entries, lastVersion);
678
+ if (newEntries.length > 0) {
679
+ this.settingsManager.setLastChangelogVersion(VERSION);
680
+ this.reportInstallTelemetry(VERSION);
681
+ return newEntries.map((e) => e.content).join("\n\n");
468
682
  }
469
683
  return undefined;
470
684
  }
685
+ reportInstallTelemetry(version) {
686
+ if (process.env.PI_OFFLINE) {
687
+ return;
688
+ }
689
+ if (!isInstallTelemetryEnabled(this.settingsManager)) {
690
+ return;
691
+ }
692
+ void fetch(`https://pi.dev/api/report-install?version=${encodeURIComponent(version)}`, {
693
+ headers: {
694
+ "User-Agent": getPiUserAgent(version),
695
+ },
696
+ signal: AbortSignal.timeout(5000),
697
+ })
698
+ .then(() => undefined)
699
+ .catch(() => undefined);
700
+ }
471
701
  getMarkdownThemeWithSettings() {
472
702
  return {
473
703
  ...getMarkdownTheme(),
@@ -486,24 +716,139 @@ export class InteractiveMode {
486
716
  }
487
717
  return result;
488
718
  }
719
+ formatExtensionDisplayPath(path) {
720
+ let result = this.formatDisplayPath(path);
721
+ result = result.replace(/\/index\.ts$/, "").replace(/\/index\.js$/, "");
722
+ return result;
723
+ }
724
+ formatContextPath(p) {
725
+ const cwd = path.resolve(this.sessionManager.getCwd());
726
+ const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
727
+ const relativePath = getCwdRelativePath(absolutePath, cwd);
728
+ if (relativePath !== undefined) {
729
+ return relativePath;
730
+ }
731
+ return this.formatDisplayPath(absolutePath);
732
+ }
733
+ getStartupExpansionState() {
734
+ return this.options.verbose || this.toolOutputExpanded;
735
+ }
489
736
  /**
490
737
  * Get a short path relative to the package root for display.
491
738
  */
492
- getShortPath(fullPath, source) {
493
- // For npm packages, show path relative to node_modules/pkg/
739
+ getShortPath(fullPath, sourceInfo) {
740
+ const baseDir = sourceInfo?.baseDir;
741
+ if (baseDir && this.isPackageSource(sourceInfo)) {
742
+ const relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));
743
+ if (relativePath &&
744
+ relativePath !== "." &&
745
+ !relativePath.startsWith("..") &&
746
+ !relativePath.startsWith(`..${path.sep}`) &&
747
+ !path.isAbsolute(relativePath)) {
748
+ return relativePath.replace(/\\/g, "/");
749
+ }
750
+ }
751
+ const source = sourceInfo?.source ?? "";
494
752
  const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
495
753
  if (npmMatch && source.startsWith("npm:")) {
496
754
  return npmMatch[2];
497
755
  }
498
- // For git packages, show path relative to repo root
499
756
  const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
500
757
  if (gitMatch && source.startsWith("git:")) {
501
758
  return gitMatch[1];
502
759
  }
503
- // For local/auto, just use formatDisplayPath
504
760
  return this.formatDisplayPath(fullPath);
505
761
  }
506
- getDisplaySourceInfo(source, scope) {
762
+ getCompactPathLabel(resourcePath, sourceInfo) {
763
+ const shortPath = this.getShortPath(resourcePath, sourceInfo);
764
+ const normalizedPath = shortPath.replace(/\\/g, "/");
765
+ const segments = normalizedPath.split("/").filter((segment) => segment.length > 0 && segment !== "~");
766
+ if (segments.length > 0) {
767
+ return segments[segments.length - 1];
768
+ }
769
+ return shortPath;
770
+ }
771
+ getCompactPackageSourceLabel(sourceInfo) {
772
+ const source = sourceInfo?.source ?? "";
773
+ if (source.startsWith("npm:")) {
774
+ return source.slice("npm:".length) || source;
775
+ }
776
+ const gitSource = parseGitUrl(source);
777
+ if (gitSource) {
778
+ return gitSource.path || source;
779
+ }
780
+ return source;
781
+ }
782
+ getCompactExtensionLabel(resourcePath, sourceInfo) {
783
+ if (!this.isPackageSource(sourceInfo)) {
784
+ return this.getCompactPathLabel(resourcePath, sourceInfo);
785
+ }
786
+ const sourceLabel = this.getCompactPackageSourceLabel(sourceInfo);
787
+ if (!sourceLabel) {
788
+ return this.getCompactPathLabel(resourcePath, sourceInfo);
789
+ }
790
+ const shortPath = this.getShortPath(resourcePath, sourceInfo).replace(/\\/g, "/");
791
+ const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
792
+ const parsedPath = path.posix.parse(packagePath);
793
+ if (parsedPath.name === "index") {
794
+ return !parsedPath.dir || parsedPath.dir === "." ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;
795
+ }
796
+ return `${sourceLabel}:${packagePath}`;
797
+ }
798
+ getCompactDisplayPathSegments(resourcePath) {
799
+ return this.formatDisplayPath(resourcePath)
800
+ .replace(/\\/g, "/")
801
+ .split("/")
802
+ .filter((segment) => segment.length > 0 && segment !== "~");
803
+ }
804
+ getCompactNonPackageExtensionLabel(resourcePath, index, allPaths) {
805
+ const segments = allPaths[index]?.segments;
806
+ if (!segments || segments.length === 0) {
807
+ return this.getCompactPathLabel(resourcePath);
808
+ }
809
+ for (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {
810
+ const candidate = segments.slice(-segmentCount).join("/");
811
+ const isUnique = allPaths.every((item, itemIndex) => {
812
+ if (itemIndex === index) {
813
+ return true;
814
+ }
815
+ return item.segments.slice(-segmentCount).join("/") !== candidate;
816
+ });
817
+ if (isUnique) {
818
+ return candidate;
819
+ }
820
+ }
821
+ return segments.join("/");
822
+ }
823
+ getCompactExtensionLabels(extensions) {
824
+ const nonPackageExtensions = extensions
825
+ .map((extension) => {
826
+ const segments = this.getCompactDisplayPathSegments(extension.path);
827
+ const lastSegment = segments[segments.length - 1];
828
+ if (segments.length > 1 && (lastSegment === "index.ts" || lastSegment === "index.js")) {
829
+ segments.pop();
830
+ }
831
+ return {
832
+ path: extension.path,
833
+ sourceInfo: extension.sourceInfo,
834
+ segments,
835
+ };
836
+ })
837
+ .filter((extension) => !this.isPackageSource(extension.sourceInfo));
838
+ return extensions.map((extension) => {
839
+ if (this.isPackageSource(extension.sourceInfo)) {
840
+ return this.getCompactExtensionLabel(extension.path, extension.sourceInfo);
841
+ }
842
+ const nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);
843
+ if (nonPackageIndex === -1) {
844
+ return this.getCompactPathLabel(extension.path, extension.sourceInfo);
845
+ }
846
+ return this.getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);
847
+ });
848
+ }
849
+ getDisplaySourceInfo(sourceInfo) {
850
+ const source = sourceInfo?.source ?? "local";
851
+ const scope = sourceInfo?.scope ?? "project";
507
852
  if (source === "local") {
508
853
  if (scope === "user") {
509
854
  return { label: "user", color: "muted" };
@@ -522,7 +867,9 @@ export class InteractiveMode {
522
867
  const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
523
868
  return { label: source, scopeLabel, color: "accent" };
524
869
  }
525
- getScopeGroup(source, scope) {
870
+ getScopeGroup(sourceInfo) {
871
+ const source = sourceInfo?.source ?? "local";
872
+ const scope = sourceInfo?.scope ?? "project";
526
873
  if (source === "cli" || scope === "temporary")
527
874
  return "path";
528
875
  if (scope === "user")
@@ -531,28 +878,27 @@ export class InteractiveMode {
531
878
  return "project";
532
879
  return "path";
533
880
  }
534
- isPackageSource(source) {
881
+ isPackageSource(sourceInfo) {
882
+ const source = sourceInfo?.source ?? "";
535
883
  return source.startsWith("npm:") || source.startsWith("git:");
536
884
  }
537
- buildScopeGroups(paths, metadata) {
885
+ buildScopeGroups(items) {
538
886
  const groups = {
539
887
  user: { scope: "user", paths: [], packages: new Map() },
540
888
  project: { scope: "project", paths: [], packages: new Map() },
541
889
  path: { scope: "path", paths: [], packages: new Map() },
542
890
  };
543
- for (const p of paths) {
544
- const meta = this.findMetadata(p, metadata);
545
- const source = meta?.source ?? "local";
546
- const scope = meta?.scope ?? "project";
547
- const groupKey = this.getScopeGroup(source, scope);
891
+ for (const item of items) {
892
+ const groupKey = this.getScopeGroup(item.sourceInfo);
548
893
  const group = groups[groupKey];
549
- if (this.isPackageSource(source)) {
894
+ const source = item.sourceInfo?.source ?? "local";
895
+ if (this.isPackageSource(item.sourceInfo)) {
550
896
  const list = group.packages.get(source) ?? [];
551
- list.push(p);
897
+ list.push(item);
552
898
  group.packages.set(source, list);
553
899
  }
554
900
  else {
555
- group.paths.push(p);
901
+ group.paths.push(item);
556
902
  }
557
903
  }
558
904
  return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
@@ -561,57 +907,44 @@ export class InteractiveMode {
561
907
  const lines = [];
562
908
  for (const group of groups) {
563
909
  lines.push(` ${theme.fg("accent", group.scope)}`);
564
- const sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b));
565
- for (const p of sortedPaths) {
566
- lines.push(theme.fg("dim", ` ${options.formatPath(p)}`));
910
+ const sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));
911
+ for (const item of sortedPaths) {
912
+ lines.push(theme.fg("dim", ` ${options.formatPath(item)}`));
567
913
  }
568
914
  const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
569
- for (const [source, paths] of sortedPackages) {
915
+ for (const [source, items] of sortedPackages) {
570
916
  lines.push(` ${theme.fg("mdLink", source)}`);
571
- const sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b));
572
- for (const p of sortedPackagePaths) {
573
- lines.push(theme.fg("dim", ` ${options.formatPackagePath(p, source)}`));
917
+ const sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));
918
+ for (const item of sortedPackagePaths) {
919
+ lines.push(theme.fg("dim", ` ${options.formatPackagePath(item, source)}`));
574
920
  }
575
921
  }
576
922
  }
577
923
  return lines.join("\n");
578
924
  }
579
- /**
580
- * Find metadata for a path, checking parent directories if exact match fails.
581
- * Package manager stores metadata for directories, but we display file paths.
582
- */
583
- findMetadata(p, metadata) {
584
- // Try exact match first
585
- const exact = metadata.get(p);
925
+ findSourceInfoForPath(p, sourceInfos) {
926
+ const exact = sourceInfos.get(p);
586
927
  if (exact)
587
928
  return exact;
588
- // Try parent directories (package manager stores directory paths)
589
929
  let current = p;
590
930
  while (current.includes("/")) {
591
931
  current = current.substring(0, current.lastIndexOf("/"));
592
- const parent = metadata.get(current);
932
+ const parent = sourceInfos.get(current);
593
933
  if (parent)
594
934
  return parent;
595
935
  }
596
936
  return undefined;
597
937
  }
598
- /**
599
- * Format a path with its source/scope info from metadata.
600
- */
601
- formatPathWithSource(p, metadata) {
602
- const meta = this.findMetadata(p, metadata);
603
- if (meta) {
604
- const shortPath = this.getShortPath(p, meta.source);
605
- const { label, scopeLabel } = this.getDisplaySourceInfo(meta.source, meta.scope);
938
+ formatPathWithSource(p, sourceInfo) {
939
+ if (sourceInfo) {
940
+ const shortPath = this.getShortPath(p, sourceInfo);
941
+ const { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);
606
942
  const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
607
943
  return `${labelText} ${shortPath}`;
608
944
  }
609
945
  return this.formatDisplayPath(p);
610
946
  }
611
- /**
612
- * Format resource diagnostics with nice collision display using metadata.
613
- */
614
- formatDiagnostics(diagnostics, metadata) {
947
+ formatDiagnostics(diagnostics, sourceInfos) {
615
948
  const lines = [];
616
949
  // Group collision diagnostics by name
617
950
  const collisions = new Map();
@@ -632,21 +965,17 @@ export class InteractiveMode {
632
965
  if (!first)
633
966
  continue;
634
967
  lines.push(theme.fg("warning", ` "${name}" collision:`));
635
- // Show winner
636
- lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, metadata)}`));
637
- // Show all losers
968
+ lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`));
638
969
  for (const d of collisionList) {
639
970
  if (d.collision) {
640
- lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, metadata)} (skipped)`));
971
+ lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`));
641
972
  }
642
973
  }
643
974
  }
644
- // Format other diagnostics (skill name collisions, parse errors, etc.)
645
975
  for (const d of otherDiagnostics) {
646
976
  if (d.path) {
647
- // Use metadata-aware formatting for paths
648
- const sourceInfo = this.formatPathWithSource(d.path, metadata);
649
- lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${sourceInfo}`));
977
+ const formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));
978
+ lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${formattedPath}`));
650
979
  lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
651
980
  }
652
981
  else {
@@ -661,11 +990,48 @@ export class InteractiveMode {
661
990
  if (!showListing && !showDiagnostics) {
662
991
  return;
663
992
  }
664
- const metadata = this.session.resourceLoader.getPathMetadata();
665
993
  const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
994
+ const formatCompactList = (items, options) => {
995
+ const labels = items.map((item) => item.trim()).filter((item) => item.length > 0);
996
+ if (options?.sort !== false) {
997
+ labels.sort((a, b) => a.localeCompare(b));
998
+ }
999
+ return theme.fg("dim", ` ${labels.join(", ")}`);
1000
+ };
1001
+ const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1002
+ const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1003
+ this.chatContainer.addChild(section);
1004
+ this.chatContainer.addChild(new Spacer(1));
1005
+ };
666
1006
  const skillsResult = this.session.resourceLoader.getSkills();
667
1007
  const promptsResult = this.session.resourceLoader.getPrompts();
668
1008
  const themesResult = this.session.resourceLoader.getThemes();
1009
+ const extensions = options?.extensions ??
1010
+ this.session.resourceLoader.getExtensions().extensions.map((extension) => ({
1011
+ path: extension.path,
1012
+ sourceInfo: extension.sourceInfo,
1013
+ }));
1014
+ const sourceInfos = new Map();
1015
+ for (const extension of extensions) {
1016
+ if (extension.sourceInfo) {
1017
+ sourceInfos.set(extension.path, extension.sourceInfo);
1018
+ }
1019
+ }
1020
+ for (const skill of skillsResult.skills) {
1021
+ if (skill.sourceInfo) {
1022
+ sourceInfos.set(skill.filePath, skill.sourceInfo);
1023
+ }
1024
+ }
1025
+ for (const prompt of promptsResult.prompts) {
1026
+ if (prompt.sourceInfo) {
1027
+ sourceInfos.set(prompt.filePath, prompt.sourceInfo);
1028
+ }
1029
+ }
1030
+ for (const loadedTheme of themesResult.themes) {
1031
+ if (loadedTheme.sourcePath && loadedTheme.sourceInfo) {
1032
+ sourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);
1033
+ }
1034
+ }
669
1035
  if (showListing) {
670
1036
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
671
1037
  if (contextFiles.length > 0) {
@@ -673,72 +1039,71 @@ export class InteractiveMode {
673
1039
  const contextList = contextFiles
674
1040
  .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
675
1041
  .join("\n");
676
- this.chatContainer.addChild(new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0));
677
- this.chatContainer.addChild(new Spacer(1));
1042
+ const contextCompactList = formatCompactList(contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), { sort: false });
1043
+ addLoadedSection("Context", contextCompactList, contextList);
678
1044
  }
679
1045
  const skills = skillsResult.skills;
680
1046
  if (skills.length > 0) {
681
- const skillPaths = skills.map((s) => s.filePath);
682
- const groups = this.buildScopeGroups(skillPaths, metadata);
1047
+ const groups = this.buildScopeGroups(skills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })));
683
1048
  const skillList = this.formatScopeGroups(groups, {
684
- formatPath: (p) => this.formatDisplayPath(p),
685
- formatPackagePath: (p, source) => this.getShortPath(p, source),
1049
+ formatPath: (item) => this.formatDisplayPath(item.path),
1050
+ formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
686
1051
  });
687
- this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0));
688
- this.chatContainer.addChild(new Spacer(1));
1052
+ const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
1053
+ addLoadedSection("Skills", skillCompactList, skillList);
689
1054
  }
690
1055
  const templates = this.session.promptTemplates;
691
1056
  if (templates.length > 0) {
692
- const templatePaths = templates.map((t) => t.filePath);
693
- const groups = this.buildScopeGroups(templatePaths, metadata);
1057
+ const groups = this.buildScopeGroups(templates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })));
694
1058
  const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
695
1059
  const templateList = this.formatScopeGroups(groups, {
696
- formatPath: (p) => {
697
- const template = templateByPath.get(p);
698
- return template ? `/${template.name}` : this.formatDisplayPath(p);
1060
+ formatPath: (item) => {
1061
+ const template = templateByPath.get(item.path);
1062
+ return template ? `/${template.name}` : this.formatDisplayPath(item.path);
699
1063
  },
700
- formatPackagePath: (p) => {
701
- const template = templateByPath.get(p);
702
- return template ? `/${template.name}` : this.formatDisplayPath(p);
1064
+ formatPackagePath: (item) => {
1065
+ const template = templateByPath.get(item.path);
1066
+ return template ? `/${template.name}` : this.formatDisplayPath(item.path);
703
1067
  },
704
1068
  });
705
- this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0));
706
- this.chatContainer.addChild(new Spacer(1));
1069
+ const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
1070
+ addLoadedSection("Prompts", promptCompactList, templateList);
707
1071
  }
708
- const extensionPaths = options?.extensionPaths ?? [];
709
- if (extensionPaths.length > 0) {
710
- const groups = this.buildScopeGroups(extensionPaths, metadata);
1072
+ if (extensions.length > 0) {
1073
+ const groups = this.buildScopeGroups(extensions);
711
1074
  const extList = this.formatScopeGroups(groups, {
712
- formatPath: (p) => this.formatDisplayPath(p),
713
- formatPackagePath: (p, source) => this.getShortPath(p, source),
1075
+ formatPath: (item) => this.formatExtensionDisplayPath(item.path),
1076
+ formatPackagePath: (item) => this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
714
1077
  });
715
- this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0));
716
- this.chatContainer.addChild(new Spacer(1));
1078
+ const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
1079
+ addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
717
1080
  }
718
1081
  // Show loaded themes (excluding built-in)
719
1082
  const loadedThemes = themesResult.themes;
720
1083
  const customThemes = loadedThemes.filter((t) => t.sourcePath);
721
1084
  if (customThemes.length > 0) {
722
- const themePaths = customThemes.map((t) => t.sourcePath);
723
- const groups = this.buildScopeGroups(themePaths, metadata);
1085
+ const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
1086
+ path: loadedTheme.sourcePath,
1087
+ sourceInfo: loadedTheme.sourceInfo,
1088
+ })));
724
1089
  const themeList = this.formatScopeGroups(groups, {
725
- formatPath: (p) => this.formatDisplayPath(p),
726
- formatPackagePath: (p, source) => this.getShortPath(p, source),
1090
+ formatPath: (item) => this.formatDisplayPath(item.path),
1091
+ formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
727
1092
  });
728
- this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0));
729
- this.chatContainer.addChild(new Spacer(1));
1093
+ const themeCompactList = formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
1094
+ addLoadedSection("Themes", themeCompactList, themeList);
730
1095
  }
731
1096
  }
732
1097
  if (showDiagnostics) {
733
1098
  const skillDiagnostics = skillsResult.diagnostics;
734
1099
  if (skillDiagnostics.length > 0) {
735
- const warningLines = this.formatDiagnostics(skillDiagnostics, metadata);
1100
+ const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
736
1101
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
737
1102
  this.chatContainer.addChild(new Spacer(1));
738
1103
  }
739
1104
  const promptDiagnostics = promptsResult.diagnostics;
740
1105
  if (promptDiagnostics.length > 0) {
741
- const warningLines = this.formatDiagnostics(promptDiagnostics, metadata);
1106
+ const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
742
1107
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
743
1108
  this.chatContainer.addChild(new Spacer(1));
744
1109
  }
@@ -749,18 +1114,19 @@ export class InteractiveMode {
749
1114
  extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
750
1115
  }
751
1116
  }
752
- const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? [];
1117
+ const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics();
753
1118
  extensionDiagnostics.push(...commandDiagnostics);
754
- const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
1119
+ extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
1120
+ const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
755
1121
  extensionDiagnostics.push(...shortcutDiagnostics);
756
1122
  if (extensionDiagnostics.length > 0) {
757
- const warningLines = this.formatDiagnostics(extensionDiagnostics, metadata);
1123
+ const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
758
1124
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
759
1125
  this.chatContainer.addChild(new Spacer(1));
760
1126
  }
761
1127
  const themeDiagnostics = themesResult.diagnostics;
762
1128
  if (themeDiagnostics.length > 0) {
763
- const warningLines = this.formatDiagnostics(themeDiagnostics, metadata);
1129
+ const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
764
1130
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
765
1131
  this.chatContainer.addChild(new Spacer(1));
766
1132
  }
@@ -769,7 +1135,7 @@ export class InteractiveMode {
769
1135
  /**
770
1136
  * Initialize the extension system with TUI-based UI context.
771
1137
  */
772
- async initExtensions() {
1138
+ async bindCurrentSessionExtensions() {
773
1139
  const uiContext = this.createExtensionUIContext();
774
1140
  await this.session.bindExtensions({
775
1141
  uiContext,
@@ -781,33 +1147,31 @@ export class InteractiveMode {
781
1147
  this.loadingAnimation = undefined;
782
1148
  }
783
1149
  this.statusContainer.clear();
784
- // Delegate to AgentSession (handles setup + agent state sync)
785
- const success = await this.session.newSession(options);
786
- if (!success) {
787
- return { cancelled: true };
1150
+ try {
1151
+ const result = await this.runtimeHost.newSession(options);
1152
+ if (!result.cancelled) {
1153
+ this.renderCurrentSessionState();
1154
+ this.ui.requestRender();
1155
+ }
1156
+ return result;
1157
+ }
1158
+ catch (error) {
1159
+ return this.handleFatalRuntimeError("Failed to create session", error);
788
1160
  }
789
- // Clear UI state
790
- this.chatContainer.clear();
791
- this.pendingMessagesContainer.clear();
792
- this.compactionQueuedMessages = [];
793
- this.streamingComponent = undefined;
794
- this.streamingMessage = undefined;
795
- this.pendingTools.clear();
796
- // Render any messages added via setup, or show empty session
797
- this.renderInitialMessages();
798
- this.ui.requestRender();
799
- return { cancelled: false };
800
1161
  },
801
- fork: async (entryId) => {
802
- const result = await this.session.fork(entryId);
803
- if (result.cancelled) {
804
- return { cancelled: true };
1162
+ fork: async (entryId, options) => {
1163
+ try {
1164
+ const result = await this.runtimeHost.fork(entryId, options);
1165
+ if (!result.cancelled) {
1166
+ this.renderCurrentSessionState();
1167
+ this.editor.setText(result.selectedText ?? "");
1168
+ this.showStatus("Forked to new session");
1169
+ }
1170
+ return { cancelled: result.cancelled };
1171
+ }
1172
+ catch (error) {
1173
+ return this.handleFatalRuntimeError("Failed to fork session", error);
805
1174
  }
806
- this.chatContainer.clear();
807
- this.renderInitialMessages();
808
- this.editor.setText(result.selectedText);
809
- this.showStatus("Forked to new session");
810
- return { cancelled: false };
811
1175
  },
812
1176
  navigateTree: async (targetId, options) => {
813
1177
  const result = await this.session.navigateTree(targetId, {
@@ -825,11 +1189,11 @@ export class InteractiveMode {
825
1189
  this.editor.setText(result.editorText);
826
1190
  }
827
1191
  this.showStatus("Navigated to selected point");
1192
+ void this.flushCompactionQueue({ willRetry: false });
828
1193
  return { cancelled: false };
829
1194
  },
830
- switchSession: async (sessionPath) => {
831
- await this.handleResumeSession(sessionPath);
832
- return { cancelled: false };
1195
+ switchSession: async (sessionPath, options) => {
1196
+ return this.handleResumeSession(sessionPath, options);
833
1197
  },
834
1198
  reload: async () => {
835
1199
  await this.handleReloadCommand();
@@ -846,22 +1210,59 @@ export class InteractiveMode {
846
1210
  },
847
1211
  });
848
1212
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
849
- this.setupAutocomplete(this.fdPath);
1213
+ this.setupAutocompleteProvider();
850
1214
  const extensionRunner = this.session.extensionRunner;
851
- if (!extensionRunner) {
852
- this.showLoadedResources({ extensionPaths: [], force: false });
853
- return;
854
- }
855
1215
  this.setupExtensionShortcuts(extensionRunner);
856
- this.showLoadedResources({ extensionPaths: extensionRunner.getExtensionPaths(), force: false });
1216
+ this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1217
+ this.showStartupNoticesIfNeeded();
1218
+ }
1219
+ applyRuntimeSettings() {
1220
+ this.footer.setSession(this.session);
1221
+ this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1222
+ this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1223
+ this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1224
+ this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1225
+ this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
1226
+ const editorPaddingX = this.settingsManager.getEditorPaddingX();
1227
+ const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
1228
+ this.defaultEditor.setPaddingX(editorPaddingX);
1229
+ this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
1230
+ if (this.editor !== this.defaultEditor) {
1231
+ this.editor.setPaddingX?.(editorPaddingX);
1232
+ this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
1233
+ }
1234
+ }
1235
+ async rebindCurrentSession() {
1236
+ this.unsubscribe?.();
1237
+ this.unsubscribe = undefined;
1238
+ this.applyRuntimeSettings();
1239
+ await this.bindCurrentSessionExtensions();
1240
+ this.subscribeToAgent();
1241
+ await this.updateAvailableProviderCount();
1242
+ this.updateEditorBorderColor();
1243
+ this.updateTerminalTitle();
1244
+ }
1245
+ async handleFatalRuntimeError(prefix, error) {
1246
+ const message = error instanceof Error ? error.message : String(error);
1247
+ this.showError(`${prefix}: ${message}`);
1248
+ stopThemeWatcher();
1249
+ this.stop();
1250
+ process.exit(1);
1251
+ }
1252
+ renderCurrentSessionState() {
1253
+ this.chatContainer.clear();
1254
+ this.pendingMessagesContainer.clear();
1255
+ this.compactionQueuedMessages = [];
1256
+ this.streamingComponent = undefined;
1257
+ this.streamingMessage = undefined;
1258
+ this.pendingTools.clear();
1259
+ this.renderInitialMessages();
857
1260
  }
858
1261
  /**
859
1262
  * Get a registered tool definition by name (for custom rendering).
860
1263
  */
861
1264
  getRegisteredToolDefinition(toolName) {
862
- const tools = this.session.extensionRunner?.getAllRegisteredTools() ?? [];
863
- const registeredTool = tools.find((t) => t.definition.name === toolName);
864
- return registeredTool?.definition;
1265
+ return this.session.getToolDefinition(toolName);
865
1266
  }
866
1267
  /**
867
1268
  * Set up keyboard shortcuts registered by extensions.
@@ -874,11 +1275,12 @@ export class InteractiveMode {
874
1275
  const createContext = () => ({
875
1276
  ui: this.createExtensionUIContext(),
876
1277
  hasUI: true,
877
- cwd: process.cwd(),
1278
+ cwd: this.sessionManager.getCwd(),
878
1279
  sessionManager: this.sessionManager,
879
1280
  modelRegistry: this.session.modelRegistry,
880
1281
  model: this.session.model,
881
1282
  isIdle: () => !this.session.isStreaming,
1283
+ signal: this.session.agent.signal,
882
1284
  abort: () => this.session.abort(),
883
1285
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
884
1286
  shutdown: () => {
@@ -888,10 +1290,8 @@ export class InteractiveMode {
888
1290
  compact: (options) => {
889
1291
  void (async () => {
890
1292
  try {
891
- const result = await this.executeCompaction(options?.customInstructions, false);
892
- if (result) {
893
- options?.onComplete?.(result);
894
- }
1293
+ const result = await this.session.compact(options?.customInstructions);
1294
+ options?.onComplete?.(result);
895
1295
  }
896
1296
  catch (error) {
897
1297
  const err = error instanceof Error ? error : new Error(String(error));
@@ -923,6 +1323,50 @@ export class InteractiveMode {
923
1323
  this.footerDataProvider.setExtensionStatus(key, text);
924
1324
  this.ui.requestRender();
925
1325
  }
1326
+ getWorkingLoaderMessage() {
1327
+ return this.workingMessage ?? this.defaultWorkingMessage;
1328
+ }
1329
+ createWorkingLoader() {
1330
+ return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.workingIndicatorOptions);
1331
+ }
1332
+ stopWorkingLoader() {
1333
+ if (this.loadingAnimation) {
1334
+ this.loadingAnimation.stop();
1335
+ this.loadingAnimation = undefined;
1336
+ }
1337
+ this.statusContainer.clear();
1338
+ }
1339
+ setWorkingVisible(visible) {
1340
+ this.workingVisible = visible;
1341
+ if (!visible) {
1342
+ this.stopWorkingLoader();
1343
+ this.ui.requestRender();
1344
+ return;
1345
+ }
1346
+ if (this.session.isStreaming && !this.loadingAnimation) {
1347
+ this.statusContainer.clear();
1348
+ this.loadingAnimation = this.createWorkingLoader();
1349
+ this.statusContainer.addChild(this.loadingAnimation);
1350
+ }
1351
+ this.ui.requestRender();
1352
+ }
1353
+ setWorkingIndicator(options) {
1354
+ this.workingIndicatorOptions = options;
1355
+ this.loadingAnimation?.setIndicator(options);
1356
+ this.ui.requestRender();
1357
+ }
1358
+ setHiddenThinkingLabel(label) {
1359
+ this.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;
1360
+ for (const child of this.chatContainer.children) {
1361
+ if (child instanceof AssistantMessageComponent) {
1362
+ child.setHiddenThinkingLabel(this.hiddenThinkingLabel);
1363
+ }
1364
+ }
1365
+ if (this.streamingComponent) {
1366
+ this.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel);
1367
+ }
1368
+ this.ui.requestRender();
1369
+ }
926
1370
  /**
927
1371
  * Set an extension widget (string array or custom component).
928
1372
  */
@@ -988,15 +1432,21 @@ export class InteractiveMode {
988
1432
  this.clearExtensionWidgets();
989
1433
  this.footerDataProvider.clearExtensionStatuses();
990
1434
  this.footer.invalidate();
1435
+ this.autocompleteProviderWrappers = [];
991
1436
  this.setCustomEditorComponent(undefined);
1437
+ this.setupAutocompleteProvider();
992
1438
  this.defaultEditor.onExtensionShortcut = undefined;
993
1439
  this.updateTerminalTitle();
1440
+ this.workingMessage = undefined;
1441
+ this.workingVisible = true;
1442
+ this.setWorkingIndicator();
994
1443
  if (this.loadingAnimation) {
995
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1444
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
996
1445
  }
1446
+ this.setHiddenThinkingLabel();
997
1447
  }
998
1448
  // Maximum total widget lines to prevent viewport overflow
999
- static { this.MAX_WIDGET_LINES = 10; }
1449
+ static MAX_WIDGET_LINES = 10;
1000
1450
  /**
1001
1451
  * Render all extension widgets to the widget container.
1002
1452
  */
@@ -1067,6 +1517,9 @@ export class InteractiveMode {
1067
1517
  if (factory) {
1068
1518
  // Create and add custom header
1069
1519
  this.customHeader = factory(this.ui, theme);
1520
+ if (isExpandable(this.customHeader)) {
1521
+ this.customHeader.setExpanded(this.toolOutputExpanded);
1522
+ }
1070
1523
  if (index !== -1) {
1071
1524
  this.headerContainer.children[index] = this.customHeader;
1072
1525
  }
@@ -1078,6 +1531,9 @@ export class InteractiveMode {
1078
1531
  else {
1079
1532
  // Restore built-in header
1080
1533
  this.customHeader = undefined;
1534
+ if (isExpandable(this.builtInHeader)) {
1535
+ this.builtInHeader.setExpanded(this.toolOutputExpanded);
1536
+ }
1081
1537
  if (index !== -1) {
1082
1538
  this.headerContainer.children[index] = this.builtInHeader;
1083
1539
  }
@@ -1110,19 +1566,14 @@ export class InteractiveMode {
1110
1566
  onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1111
1567
  setStatus: (key, text) => this.setExtensionStatus(key, text),
1112
1568
  setWorkingMessage: (message) => {
1569
+ this.workingMessage = message;
1113
1570
  if (this.loadingAnimation) {
1114
- if (message) {
1115
- this.loadingAnimation.setMessage(message);
1116
- }
1117
- else {
1118
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1119
- }
1120
- }
1121
- else {
1122
- // Queue message for when loadingAnimation is created (handles agent_start race)
1123
- this.pendingWorkingMessage = message;
1571
+ this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
1124
1572
  }
1125
1573
  },
1574
+ setWorkingVisible: (visible) => this.setWorkingVisible(visible),
1575
+ setWorkingIndicator: (options) => this.setWorkingIndicator(options),
1576
+ setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
1126
1577
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
1127
1578
  setFooter: (factory) => this.setExtensionFooter(factory),
1128
1579
  setHeader: (factory) => this.setExtensionHeader(factory),
@@ -1130,9 +1581,14 @@ export class InteractiveMode {
1130
1581
  custom: (factory, options) => this.showExtensionCustom(factory, options),
1131
1582
  pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1132
1583
  setEditorText: (text) => this.editor.setText(text),
1133
- getEditorText: () => this.editor.getText(),
1584
+ getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
1134
1585
  editor: (title, prefill) => this.showExtensionEditor(title, prefill),
1586
+ addAutocompleteProvider: (factory) => {
1587
+ this.autocompleteProviderWrappers.push(factory);
1588
+ this.setupAutocompleteProvider();
1589
+ },
1135
1590
  setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1591
+ getEditorComponent: () => this.editorComponentFactory,
1136
1592
  get theme() {
1137
1593
  return theme;
1138
1594
  },
@@ -1204,6 +1660,10 @@ export class InteractiveMode {
1204
1660
  const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts);
1205
1661
  return result === "Yes";
1206
1662
  }
1663
+ async promptForMissingSessionCwd(error) {
1664
+ const confirmed = await this.showExtensionConfirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
1665
+ return confirmed ? error.issue.fallbackCwd : undefined;
1666
+ }
1207
1667
  /**
1208
1668
  * Show a text input for extensions.
1209
1669
  */
@@ -1277,6 +1737,7 @@ export class InteractiveMode {
1277
1737
  * Pass undefined to restore the default editor.
1278
1738
  */
1279
1739
  setCustomEditorComponent(factory) {
1740
+ this.editorComponentFactory = factory;
1280
1741
  // Save text from current editor before switching
1281
1742
  const currentText = this.editor.getText();
1282
1743
  this.editorContainer.clear();
@@ -1441,7 +1902,7 @@ export class InteractiveMode {
1441
1902
  // Set up handlers on defaultEditor - they use this.editor for text access
1442
1903
  // so they work correctly regardless of which editor is active
1443
1904
  this.defaultEditor.onEscape = () => {
1444
- if (this.loadingAnimation) {
1905
+ if (this.session.isStreaming) {
1445
1906
  this.restoreQueuedMessagesToEditor({ abort: true });
1446
1907
  }
1447
1908
  else if (this.session.isBashRunning) {
@@ -1473,24 +1934,24 @@ export class InteractiveMode {
1473
1934
  }
1474
1935
  };
1475
1936
  // Register app action handlers
1476
- this.defaultEditor.onAction("clear", () => this.handleCtrlC());
1937
+ this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
1477
1938
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1478
- this.defaultEditor.onAction("suspend", () => this.handleCtrlZ());
1479
- this.defaultEditor.onAction("cycleThinkingLevel", () => this.cycleThinkingLevel());
1480
- this.defaultEditor.onAction("cycleModelForward", () => this.cycleModel("forward"));
1481
- this.defaultEditor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
1939
+ this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
1940
+ this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
1941
+ this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
1942
+ this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1482
1943
  // Global debug handler on TUI (works regardless of focus)
1483
1944
  this.ui.onDebug = () => this.handleDebugCommand();
1484
- this.defaultEditor.onAction("selectModel", () => this.showModelSelector());
1485
- this.defaultEditor.onAction("expandTools", () => this.toggleToolOutputExpansion());
1486
- this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
1487
- this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor());
1488
- this.defaultEditor.onAction("followUp", () => this.handleFollowUp());
1489
- this.defaultEditor.onAction("dequeue", () => this.handleDequeue());
1490
- this.defaultEditor.onAction("newSession", () => this.handleClearCommand());
1491
- this.defaultEditor.onAction("tree", () => this.showTreeSelector());
1492
- this.defaultEditor.onAction("fork", () => this.showUserMessageSelector());
1493
- this.defaultEditor.onAction("resume", () => this.showSessionSelector());
1945
+ this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1946
+ this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1947
+ this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1948
+ this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
1949
+ this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
1950
+ this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
1951
+ this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
1952
+ this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
1953
+ this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
1954
+ this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
1494
1955
  this.defaultEditor.onChange = (text) => {
1495
1956
  const wasBashMode = this.isBashMode;
1496
1957
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1545,18 +2006,23 @@ export class InteractiveMode {
1545
2006
  await this.handleModelCommand(searchTerm);
1546
2007
  return;
1547
2008
  }
1548
- if (text.startsWith("/export")) {
2009
+ if (text === "/export" || text.startsWith("/export ")) {
1549
2010
  await this.handleExportCommand(text);
1550
2011
  this.editor.setText("");
1551
2012
  return;
1552
2013
  }
2014
+ if (text === "/import" || text.startsWith("/import ")) {
2015
+ await this.handleImportCommand(text);
2016
+ this.editor.setText("");
2017
+ return;
2018
+ }
1553
2019
  if (text === "/share") {
1554
2020
  await this.handleShareCommand();
1555
2021
  this.editor.setText("");
1556
2022
  return;
1557
2023
  }
1558
2024
  if (text === "/copy") {
1559
- this.handleCopyCommand();
2025
+ await this.handleCopyCommand();
1560
2026
  this.editor.setText("");
1561
2027
  return;
1562
2028
  }
@@ -1585,6 +2051,11 @@ export class InteractiveMode {
1585
2051
  this.editor.setText("");
1586
2052
  return;
1587
2053
  }
2054
+ if (text === "/clone") {
2055
+ this.editor.setText("");
2056
+ await this.handleCloneCommand();
2057
+ return;
2058
+ }
1588
2059
  if (text === "/tree") {
1589
2060
  this.showTreeSelector();
1590
2061
  this.editor.setText("");
@@ -1626,6 +2097,11 @@ export class InteractiveMode {
1626
2097
  this.editor.setText("");
1627
2098
  return;
1628
2099
  }
2100
+ if (text === "/dementedelves") {
2101
+ this.handleDementedDelves();
2102
+ this.editor.setText("");
2103
+ return;
2104
+ }
1629
2105
  if (text === "/resume") {
1630
2106
  this.showSessionSelector();
1631
2107
  this.editor.setText("");
@@ -1696,31 +2172,44 @@ export class InteractiveMode {
1696
2172
  this.footer.invalidate();
1697
2173
  switch (event.type) {
1698
2174
  case "agent_start":
2175
+ this.pendingTools.clear();
2176
+ if (this.settingsManager.getShowTerminalProgress()) {
2177
+ this.ui.terminal.setProgress(true);
2178
+ }
1699
2179
  // Restore main escape handler if retry handler is still active
1700
2180
  // (retry success event fires later, but we need main handler now)
1701
2181
  if (this.retryEscapeHandler) {
1702
2182
  this.defaultEditor.onEscape = this.retryEscapeHandler;
1703
2183
  this.retryEscapeHandler = undefined;
1704
2184
  }
2185
+ if (this.retryCountdown) {
2186
+ this.retryCountdown.dispose();
2187
+ this.retryCountdown = undefined;
2188
+ }
1705
2189
  if (this.retryLoader) {
1706
2190
  this.retryLoader.stop();
1707
2191
  this.retryLoader = undefined;
1708
2192
  }
1709
- if (this.loadingAnimation) {
1710
- this.loadingAnimation.stop();
1711
- }
1712
- this.statusContainer.clear();
1713
- this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
1714
- this.statusContainer.addChild(this.loadingAnimation);
1715
- // Apply any pending working message queued before loader existed
1716
- if (this.pendingWorkingMessage !== undefined) {
1717
- if (this.pendingWorkingMessage) {
1718
- this.loadingAnimation.setMessage(this.pendingWorkingMessage);
1719
- }
1720
- this.pendingWorkingMessage = undefined;
2193
+ this.stopWorkingLoader();
2194
+ if (this.workingVisible) {
2195
+ this.loadingAnimation = this.createWorkingLoader();
2196
+ this.statusContainer.addChild(this.loadingAnimation);
1721
2197
  }
1722
2198
  this.ui.requestRender();
1723
2199
  break;
2200
+ case "queue_update":
2201
+ this.updatePendingMessagesDisplay();
2202
+ this.ui.requestRender();
2203
+ break;
2204
+ case "session_info_changed":
2205
+ this.updateTerminalTitle();
2206
+ this.footer.invalidate();
2207
+ this.ui.requestRender();
2208
+ break;
2209
+ case "thinking_level_changed":
2210
+ this.footer.invalidate();
2211
+ this.updateEditorBorderColor();
2212
+ break;
1724
2213
  case "message_start":
1725
2214
  if (event.message.role === "custom") {
1726
2215
  this.addMessageToChat(event.message);
@@ -1732,7 +2221,7 @@ export class InteractiveMode {
1732
2221
  this.ui.requestRender();
1733
2222
  }
1734
2223
  else if (event.message.role === "assistant") {
1735
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
2224
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
1736
2225
  this.streamingMessage = event.message;
1737
2226
  this.chatContainer.addChild(this.streamingComponent);
1738
2227
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -1746,9 +2235,10 @@ export class InteractiveMode {
1746
2235
  for (const content of this.streamingMessage.content) {
1747
2236
  if (content.type === "toolCall") {
1748
2237
  if (!this.pendingTools.has(content.id)) {
1749
- const component = new ToolExecutionComponent(content.name, content.arguments, {
2238
+ const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
1750
2239
  showImages: this.settingsManager.getShowImages(),
1751
- }, this.getRegisteredToolDefinition(content.name), this.ui);
2240
+ imageWidthCells: this.settingsManager.getImageWidthCells(),
2241
+ }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
1752
2242
  component.setExpanded(this.toolOutputExpanded);
1753
2243
  this.chatContainer.addChild(component);
1754
2244
  this.pendingTools.set(content.id, component);
@@ -1804,15 +2294,18 @@ export class InteractiveMode {
1804
2294
  this.ui.requestRender();
1805
2295
  break;
1806
2296
  case "tool_execution_start": {
1807
- if (!this.pendingTools.has(event.toolCallId)) {
1808
- const component = new ToolExecutionComponent(event.toolName, event.args, {
2297
+ let component = this.pendingTools.get(event.toolCallId);
2298
+ if (!component) {
2299
+ component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
1809
2300
  showImages: this.settingsManager.getShowImages(),
1810
- }, this.getRegisteredToolDefinition(event.toolName), this.ui);
2301
+ imageWidthCells: this.settingsManager.getImageWidthCells(),
2302
+ }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
1811
2303
  component.setExpanded(this.toolOutputExpanded);
1812
2304
  this.chatContainer.addChild(component);
1813
2305
  this.pendingTools.set(event.toolCallId, component);
1814
- this.ui.requestRender();
1815
2306
  }
2307
+ component.markExecutionStarted();
2308
+ this.ui.requestRender();
1816
2309
  break;
1817
2310
  }
1818
2311
  case "tool_execution_update": {
@@ -1833,6 +2326,9 @@ export class InteractiveMode {
1833
2326
  break;
1834
2327
  }
1835
2328
  case "agent_end":
2329
+ if (this.settingsManager.getShowTerminalProgress()) {
2330
+ this.ui.terminal.setProgress(false);
2331
+ }
1836
2332
  if (this.loadingAnimation) {
1837
2333
  this.loadingAnimation.stop();
1838
2334
  this.loadingAnimation = undefined;
@@ -1847,54 +2343,60 @@ export class InteractiveMode {
1847
2343
  await this.checkShutdownRequested();
1848
2344
  this.ui.requestRender();
1849
2345
  break;
1850
- case "auto_compaction_start": {
2346
+ case "compaction_start": {
2347
+ if (this.settingsManager.getShowTerminalProgress()) {
2348
+ this.ui.terminal.setProgress(true);
2349
+ }
1851
2350
  // Keep editor active; submissions are queued during compaction.
1852
- // Set up escape to abort auto-compaction
1853
2351
  this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
1854
2352
  this.defaultEditor.onEscape = () => {
1855
2353
  this.session.abortCompaction();
1856
2354
  };
1857
- // Show compacting indicator with reason
1858
2355
  this.statusContainer.clear();
1859
- const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
1860
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${appKey(this.keybindings, "interrupt")} to cancel)`);
2356
+ const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2357
+ const label = event.reason === "manual"
2358
+ ? `Compacting context... ${cancelHint}`
2359
+ : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2360
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
1861
2361
  this.statusContainer.addChild(this.autoCompactionLoader);
1862
2362
  this.ui.requestRender();
1863
2363
  break;
1864
2364
  }
1865
- case "auto_compaction_end": {
1866
- // Restore escape handler
2365
+ case "compaction_end": {
2366
+ if (this.settingsManager.getShowTerminalProgress()) {
2367
+ this.ui.terminal.setProgress(false);
2368
+ }
1867
2369
  if (this.autoCompactionEscapeHandler) {
1868
2370
  this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
1869
2371
  this.autoCompactionEscapeHandler = undefined;
1870
2372
  }
1871
- // Stop loader
1872
2373
  if (this.autoCompactionLoader) {
1873
2374
  this.autoCompactionLoader.stop();
1874
2375
  this.autoCompactionLoader = undefined;
1875
2376
  this.statusContainer.clear();
1876
2377
  }
1877
- // Handle result
1878
2378
  if (event.aborted) {
1879
- this.showStatus("Auto-compaction cancelled");
2379
+ if (event.reason === "manual") {
2380
+ this.showError("Compaction cancelled");
2381
+ }
2382
+ else {
2383
+ this.showStatus("Auto-compaction cancelled");
2384
+ }
1880
2385
  }
1881
2386
  else if (event.result) {
1882
- // Rebuild chat to show compacted state
1883
2387
  this.chatContainer.clear();
1884
2388
  this.rebuildChatFromMessages();
1885
- // Add compaction component at bottom so user sees it without scrolling
1886
- this.addMessageToChat({
1887
- role: "compactionSummary",
1888
- tokensBefore: event.result.tokensBefore,
1889
- summary: event.result.summary,
1890
- timestamp: Date.now(),
1891
- });
2389
+ this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
1892
2390
  this.footer.invalidate();
1893
2391
  }
1894
2392
  else if (event.errorMessage) {
1895
- // Compaction failed (e.g., quota exceeded, API error)
1896
- this.chatContainer.addChild(new Spacer(1));
1897
- this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
2393
+ if (event.reason === "manual") {
2394
+ this.showError(event.errorMessage);
2395
+ }
2396
+ else {
2397
+ this.chatContainer.addChild(new Spacer(1));
2398
+ this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
2399
+ }
1898
2400
  }
1899
2401
  void this.flushCompactionQueue({ willRetry: event.willRetry });
1900
2402
  this.ui.requestRender();
@@ -1908,8 +2410,14 @@ export class InteractiveMode {
1908
2410
  };
1909
2411
  // Show retry indicator
1910
2412
  this.statusContainer.clear();
1911
- const delaySeconds = Math.round(event.delayMs / 1000);
1912
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, "interrupt")} to cancel)`);
2413
+ this.retryCountdown?.dispose();
2414
+ const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2415
+ this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2416
+ this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
2417
+ this.retryLoader?.setMessage(retryMessage(seconds));
2418
+ }, () => {
2419
+ this.retryCountdown = undefined;
2420
+ });
1913
2421
  this.statusContainer.addChild(this.retryLoader);
1914
2422
  this.ui.requestRender();
1915
2423
  break;
@@ -1920,6 +2428,10 @@ export class InteractiveMode {
1920
2428
  this.defaultEditor.onEscape = this.retryEscapeHandler;
1921
2429
  this.retryEscapeHandler = undefined;
1922
2430
  }
2431
+ if (this.retryCountdown) {
2432
+ this.retryCountdown.dispose();
2433
+ this.retryCountdown = undefined;
2434
+ }
1923
2435
  // Stop loader
1924
2436
  if (this.retryLoader) {
1925
2437
  this.retryLoader.stop();
@@ -1980,7 +2492,7 @@ export class InteractiveMode {
1980
2492
  }
1981
2493
  case "custom": {
1982
2494
  if (message.display) {
1983
- const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
2495
+ const renderer = this.session.extensionRunner.getMessageRenderer(message.customType);
1984
2496
  const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
1985
2497
  component.setExpanded(this.toolOutputExpanded);
1986
2498
  this.chatContainer.addChild(component);
@@ -2004,10 +2516,12 @@ export class InteractiveMode {
2004
2516
  case "user": {
2005
2517
  const textContent = this.getUserMessageText(message);
2006
2518
  if (textContent) {
2519
+ if (this.chatContainer.children.length > 0) {
2520
+ this.chatContainer.addChild(new Spacer(1));
2521
+ }
2007
2522
  const skillBlock = parseSkillBlock(textContent);
2008
2523
  if (skillBlock) {
2009
2524
  // Render skill block (collapsible)
2010
- this.chatContainer.addChild(new Spacer(1));
2011
2525
  const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings());
2012
2526
  component.setExpanded(this.toolOutputExpanded);
2013
2527
  this.chatContainer.addChild(component);
@@ -2028,7 +2542,7 @@ export class InteractiveMode {
2028
2542
  break;
2029
2543
  }
2030
2544
  case "assistant": {
2031
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
2545
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2032
2546
  this.chatContainer.addChild(assistantComponent);
2033
2547
  break;
2034
2548
  }
@@ -2049,6 +2563,7 @@ export class InteractiveMode {
2049
2563
  */
2050
2564
  renderSessionContext(sessionContext, options = {}) {
2051
2565
  this.pendingTools.clear();
2566
+ const renderedPendingTools = new Map();
2052
2567
  if (options.updateFooter) {
2053
2568
  this.footer.invalidate();
2054
2569
  this.updateEditorBorderColor();
@@ -2060,7 +2575,10 @@ export class InteractiveMode {
2060
2575
  // Render tool call components
2061
2576
  for (const content of message.content) {
2062
2577
  if (content.type === "toolCall") {
2063
- const component = new ToolExecutionComponent(content.name, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
2578
+ const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2579
+ showImages: this.settingsManager.getShowImages(),
2580
+ imageWidthCells: this.settingsManager.getImageWidthCells(),
2581
+ }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2064
2582
  component.setExpanded(this.toolOutputExpanded);
2065
2583
  this.chatContainer.addChild(component);
2066
2584
  if (message.stopReason === "aborted" || message.stopReason === "error") {
@@ -2078,17 +2596,17 @@ export class InteractiveMode {
2078
2596
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2079
2597
  }
2080
2598
  else {
2081
- this.pendingTools.set(content.id, component);
2599
+ renderedPendingTools.set(content.id, component);
2082
2600
  }
2083
2601
  }
2084
2602
  }
2085
2603
  }
2086
2604
  else if (message.role === "toolResult") {
2087
2605
  // Match tool results to pending tool components
2088
- const component = this.pendingTools.get(message.toolCallId);
2606
+ const component = renderedPendingTools.get(message.toolCallId);
2089
2607
  if (component) {
2090
2608
  component.updateResult(message);
2091
- this.pendingTools.delete(message.toolCallId);
2609
+ renderedPendingTools.delete(message.toolCallId);
2092
2610
  }
2093
2611
  }
2094
2612
  else {
@@ -2096,7 +2614,9 @@ export class InteractiveMode {
2096
2614
  this.addMessageToChat(message, options);
2097
2615
  }
2098
2616
  }
2099
- this.pendingTools.clear();
2617
+ for (const [toolCallId, component] of renderedPendingTools) {
2618
+ this.pendingTools.set(toolCallId, component);
2619
+ }
2100
2620
  this.ui.requestRender();
2101
2621
  }
2102
2622
  renderInitialMessages() {
@@ -2144,26 +2664,32 @@ export class InteractiveMode {
2144
2664
  // Only called when editor is empty (enforced by CustomEditor)
2145
2665
  void this.shutdown();
2146
2666
  }
2667
+ /**
2668
+ * Gracefully shutdown the agent.
2669
+ * Stops the TUI before emitting shutdown events so extension UI cleanup cannot
2670
+ * repaint the final frame while the process is exiting.
2671
+ */
2672
+ isShuttingDown = false;
2147
2673
  async shutdown() {
2148
2674
  if (this.isShuttingDown)
2149
2675
  return;
2150
2676
  this.isShuttingDown = true;
2151
- // Emit shutdown event to extensions
2152
- const extensionRunner = this.session.extensionRunner;
2153
- if (extensionRunner?.hasHandlers("session_shutdown")) {
2154
- await extensionRunner.emit({
2155
- type: "session_shutdown",
2156
- });
2157
- }
2158
- // Wait for any pending renders to complete
2159
- // requestRender() uses process.nextTick(), so we wait one tick
2160
- await new Promise((resolve) => process.nextTick(resolve));
2677
+ this.unregisterSignalHandlers();
2161
2678
  // Drain any in-flight Kitty key release events before stopping.
2162
2679
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
2163
2680
  await this.ui.terminal.drainInput(1000);
2164
2681
  this.stop();
2682
+ await this.runtimeHost.dispose();
2165
2683
  process.exit(0);
2166
2684
  }
2685
+ emergencyTerminalExit() {
2686
+ this.isShuttingDown = true;
2687
+ this.unregisterSignalHandlers();
2688
+ killTrackedDetachedChildren();
2689
+ // The terminal is gone. Do not run normal shutdown because TUI and
2690
+ // extension cleanup can write restore sequences and re-trigger EIO.
2691
+ process.exit(129);
2692
+ }
2167
2693
  /**
2168
2694
  * Check if shutdown was requested and perform shutdown if so.
2169
2695
  */
@@ -2172,21 +2698,71 @@ export class InteractiveMode {
2172
2698
  return;
2173
2699
  await this.shutdown();
2174
2700
  }
2701
+ registerSignalHandlers() {
2702
+ this.unregisterSignalHandlers();
2703
+ const signals = ["SIGTERM"];
2704
+ if (process.platform !== "win32") {
2705
+ signals.push("SIGHUP");
2706
+ }
2707
+ for (const signal of signals) {
2708
+ const handler = () => {
2709
+ if (signal === "SIGHUP") {
2710
+ this.emergencyTerminalExit();
2711
+ }
2712
+ killTrackedDetachedChildren();
2713
+ void this.shutdown();
2714
+ };
2715
+ process.prependListener(signal, handler);
2716
+ this.signalCleanupHandlers.push(() => process.off(signal, handler));
2717
+ }
2718
+ const terminalErrorHandler = (error) => {
2719
+ if (isDeadTerminalError(error)) {
2720
+ this.emergencyTerminalExit();
2721
+ }
2722
+ throw error;
2723
+ };
2724
+ process.stdout.on("error", terminalErrorHandler);
2725
+ process.stderr.on("error", terminalErrorHandler);
2726
+ this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
2727
+ this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
2728
+ }
2729
+ unregisterSignalHandlers() {
2730
+ for (const cleanup of this.signalCleanupHandlers) {
2731
+ cleanup();
2732
+ }
2733
+ this.signalCleanupHandlers = [];
2734
+ }
2175
2735
  handleCtrlZ() {
2736
+ if (process.platform === "win32") {
2737
+ this.showStatus("Suspend to background is not supported on Windows");
2738
+ return;
2739
+ }
2740
+ // Keep the event loop alive while suspended. Without this, stopping the TUI
2741
+ // can leave Node with no ref'ed handles, causing the process to exit on fg
2742
+ // before the SIGCONT handler gets a chance to restore the terminal.
2743
+ const suspendKeepAlive = setInterval(() => { }, 2 ** 30);
2176
2744
  // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2177
2745
  // kill the backgrounded process. The handler is removed on resume.
2178
2746
  const ignoreSigint = () => { };
2179
2747
  process.on("SIGINT", ignoreSigint);
2180
2748
  // Set up handler to restore TUI when resumed
2181
2749
  process.once("SIGCONT", () => {
2750
+ clearInterval(suspendKeepAlive);
2182
2751
  process.removeListener("SIGINT", ignoreSigint);
2183
2752
  this.ui.start();
2184
2753
  this.ui.requestRender(true);
2185
2754
  });
2186
- // Stop the TUI (restore terminal to normal mode)
2187
- this.ui.stop();
2188
- // Send SIGTSTP to process group (pid=0 means all processes in group)
2189
- process.kill(0, "SIGTSTP");
2755
+ try {
2756
+ // Stop the TUI (restore terminal to normal mode)
2757
+ this.ui.stop();
2758
+ // Send SIGTSTP to process group (pid=0 means all processes in group)
2759
+ process.kill(0, "SIGTSTP");
2760
+ }
2761
+ catch (error) {
2762
+ clearInterval(suspendKeepAlive);
2763
+ process.removeListener("SIGINT", ignoreSigint);
2764
+ throw error;
2765
+ }
2190
2766
  }
2191
2767
  async handleFollowUp() {
2192
2768
  const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
@@ -2215,6 +2791,7 @@ export class InteractiveMode {
2215
2791
  }
2216
2792
  // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
2217
2793
  else if (this.editor.onSubmit) {
2794
+ this.editor.setText("");
2218
2795
  this.editor.onSubmit(text);
2219
2796
  }
2220
2797
  }
@@ -2260,6 +2837,7 @@ export class InteractiveMode {
2260
2837
  this.updateEditorBorderColor();
2261
2838
  const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
2262
2839
  this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
2840
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);
2263
2841
  }
2264
2842
  }
2265
2843
  catch (error) {
@@ -2271,6 +2849,10 @@ export class InteractiveMode {
2271
2849
  }
2272
2850
  setToolsExpanded(expanded) {
2273
2851
  this.toolOutputExpanded = expanded;
2852
+ const activeHeader = this.customHeader ?? this.builtInHeader;
2853
+ if (isExpandable(activeHeader)) {
2854
+ activeHeader.setExpanded(expanded);
2855
+ }
2274
2856
  for (const child of this.chatContainer.children) {
2275
2857
  if (isExpandable(child)) {
2276
2858
  child.setExpanded(expanded);
@@ -2311,6 +2893,7 @@ export class InteractiveMode {
2311
2893
  // Spawn editor synchronously with inherited stdio for interactive editing
2312
2894
  const result = spawnSync(editor, [...editorArgs, tmpFile], {
2313
2895
  stdio: "inherit",
2896
+ shell: process.platform === "win32",
2314
2897
  });
2315
2898
  // On successful exit (status 0), replace editor content
2316
2899
  if (result.status === 0) {
@@ -2351,16 +2934,29 @@ export class InteractiveMode {
2351
2934
  this.ui.requestRender();
2352
2935
  }
2353
2936
  showNewVersionNotification(newVersion) {
2354
- const action = theme.fg("accent", getUpdateInstruction("phi-code"));
2355
- const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2356
- const changelogUrl = theme.fg("accent", "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
2357
- const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
2937
+ const action = theme.fg("accent", `${APP_NAME} update`);
2938
+ const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
2939
+ const changelogUrl = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md";
2940
+ const changelogLink = getCapabilities().hyperlinks
2941
+ ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
2942
+ : theme.fg("accent", changelogUrl);
2943
+ const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
2358
2944
  this.chatContainer.addChild(new Spacer(1));
2359
2945
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2360
2946
  this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
2361
2947
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2362
2948
  this.ui.requestRender();
2363
2949
  }
2950
+ showPackageUpdateNotification(packages) {
2951
+ const action = theme.fg("accent", `${APP_NAME} update`);
2952
+ const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
2953
+ const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
2954
+ this.chatContainer.addChild(new Spacer(1));
2955
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2956
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
2957
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2958
+ this.ui.requestRender();
2959
+ }
2364
2960
  /**
2365
2961
  * Get all queued messages (read-only).
2366
2962
  * Combines session queue and compaction queue.
@@ -2408,7 +3004,7 @@ export class InteractiveMode {
2408
3004
  const text = theme.fg("dim", `Follow-up: ${message}`);
2409
3005
  this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2410
3006
  }
2411
- const dequeueHint = this.getAppKeyDisplay("dequeue");
3007
+ const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
2412
3008
  const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
2413
3009
  this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
2414
3010
  }
@@ -2444,8 +3040,6 @@ export class InteractiveMode {
2444
3040
  if (!text.startsWith("/"))
2445
3041
  return false;
2446
3042
  const extensionRunner = this.session.extensionRunner;
2447
- if (!extensionRunner)
2448
- return false;
2449
3043
  const spaceIndex = text.indexOf(" ");
2450
3044
  const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
2451
3045
  return !!extensionRunner.getCommand(commandName);
@@ -2551,6 +3145,7 @@ export class InteractiveMode {
2551
3145
  const selector = new SettingsSelectorComponent({
2552
3146
  autoCompact: this.session.autoCompactionEnabled,
2553
3147
  showImages: this.settingsManager.getShowImages(),
3148
+ imageWidthCells: this.settingsManager.getImageWidthCells(),
2554
3149
  autoResizeImages: this.settingsManager.getImageAutoResize(),
2555
3150
  blockImages: this.settingsManager.getBlockImages(),
2556
3151
  enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
@@ -2563,6 +3158,7 @@ export class InteractiveMode {
2563
3158
  availableThemes: getAvailableThemes(),
2564
3159
  hideThinkingBlock: this.hideThinkingBlock,
2565
3160
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3161
+ enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
2566
3162
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
2567
3163
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
2568
3164
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
@@ -2570,6 +3166,8 @@ export class InteractiveMode {
2570
3166
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
2571
3167
  quietStartup: this.settingsManager.getQuietStartup(),
2572
3168
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3169
+ showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3170
+ warnings: this.settingsManager.getWarnings(),
2573
3171
  }, {
2574
3172
  onAutoCompactChange: (enabled) => {
2575
3173
  this.session.setAutoCompactionEnabled(enabled);
@@ -2583,6 +3181,14 @@ export class InteractiveMode {
2583
3181
  }
2584
3182
  }
2585
3183
  },
3184
+ onImageWidthCellsChange: (width) => {
3185
+ this.settingsManager.setImageWidthCells(width);
3186
+ for (const child of this.chatContainer.children) {
3187
+ if (child instanceof ToolExecutionComponent) {
3188
+ child.setImageWidthCells(width);
3189
+ }
3190
+ }
3191
+ },
2586
3192
  onAutoResizeImagesChange: (enabled) => {
2587
3193
  this.settingsManager.setImageAutoResize(enabled);
2588
3194
  },
@@ -2591,7 +3197,7 @@ export class InteractiveMode {
2591
3197
  },
2592
3198
  onEnableSkillCommandsChange: (enabled) => {
2593
3199
  this.settingsManager.setEnableSkillCommands(enabled);
2594
- this.setupAutocomplete(this.fdPath);
3200
+ this.setupAutocompleteProvider();
2595
3201
  },
2596
3202
  onSteeringModeChange: (mode) => {
2597
3203
  this.session.setSteeringMode(mode);
@@ -2601,7 +3207,7 @@ export class InteractiveMode {
2601
3207
  },
2602
3208
  onTransportChange: (transport) => {
2603
3209
  this.settingsManager.setTransport(transport);
2604
- this.session.agent.setTransport(transport);
3210
+ this.session.agent.transport = transport;
2605
3211
  },
2606
3212
  onThinkingLevelChange: (level) => {
2607
3213
  this.session.setThinkingLevel(level);
@@ -2637,6 +3243,9 @@ export class InteractiveMode {
2637
3243
  onCollapseChangelogChange: (collapsed) => {
2638
3244
  this.settingsManager.setCollapseChangelog(collapsed);
2639
3245
  },
3246
+ onEnableInstallTelemetryChange: (enabled) => {
3247
+ this.settingsManager.setEnableInstallTelemetry(enabled);
3248
+ },
2640
3249
  onQuietStartupChange: (enabled) => {
2641
3250
  this.settingsManager.setQuietStartup(enabled);
2642
3251
  },
@@ -2668,6 +3277,12 @@ export class InteractiveMode {
2668
3277
  this.settingsManager.setClearOnShrink(enabled);
2669
3278
  this.ui.setClearOnShrink(enabled);
2670
3279
  },
3280
+ onShowTerminalProgressChange: (enabled) => {
3281
+ this.settingsManager.setShowTerminalProgress(enabled);
3282
+ },
3283
+ onWarningsChange: (warnings) => {
3284
+ this.settingsManager.setWarnings(warnings);
3285
+ },
2671
3286
  onCancel: () => {
2672
3287
  done();
2673
3288
  this.ui.requestRender();
@@ -2688,6 +3303,7 @@ export class InteractiveMode {
2688
3303
  this.footer.invalidate();
2689
3304
  this.updateEditorBorderColor();
2690
3305
  this.showStatus(`Model: ${model.id}`);
3306
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
2691
3307
  this.checkDaxnutsEasterEgg(model);
2692
3308
  }
2693
3309
  catch (error) {
@@ -2698,28 +3314,8 @@ export class InteractiveMode {
2698
3314
  this.showModelSelector(searchTerm);
2699
3315
  }
2700
3316
  async findExactModelMatch(searchTerm) {
2701
- const term = searchTerm.trim();
2702
- if (!term)
2703
- return undefined;
2704
- let targetProvider;
2705
- let targetModelId = "";
2706
- if (term.includes("/")) {
2707
- const parts = term.split("/", 2);
2708
- targetProvider = parts[0]?.trim().toLowerCase();
2709
- targetModelId = parts[1]?.trim().toLowerCase() ?? "";
2710
- }
2711
- else {
2712
- targetModelId = term.toLowerCase();
2713
- }
2714
- if (!targetModelId)
2715
- return undefined;
2716
3317
  const models = await this.getModelCandidates();
2717
- const exactMatches = models.filter((item) => {
2718
- const idMatch = item.id.toLowerCase() === targetModelId;
2719
- const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;
2720
- return idMatch && providerMatch;
2721
- });
2722
- return exactMatches.length === 1 ? exactMatches[0] : undefined;
3318
+ return findExactModelReferenceMatch(searchTerm, models);
2723
3319
  }
2724
3320
  async getModelCandidates() {
2725
3321
  if (this.session.scopedModels.length > 0) {
@@ -2739,6 +3335,34 @@ export class InteractiveMode {
2739
3335
  const uniqueProviders = new Set(models.map((m) => m.provider));
2740
3336
  this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
2741
3337
  }
3338
+ async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
3339
+ if (this.settingsManager.getWarnings().anthropicExtraUsage === false) {
3340
+ return;
3341
+ }
3342
+ if (this.anthropicSubscriptionWarningShown) {
3343
+ return;
3344
+ }
3345
+ if (!model || model.provider !== "anthropic") {
3346
+ return;
3347
+ }
3348
+ const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
3349
+ if (storedCredential?.type === "oauth") {
3350
+ this.anthropicSubscriptionWarningShown = true;
3351
+ this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3352
+ return;
3353
+ }
3354
+ try {
3355
+ const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
3356
+ if (!isAnthropicSubscriptionAuthKey(apiKey)) {
3357
+ return;
3358
+ }
3359
+ this.anthropicSubscriptionWarningShown = true;
3360
+ this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3361
+ }
3362
+ catch {
3363
+ // Ignore auth lookup failures for warning-only checks.
3364
+ }
3365
+ }
2742
3366
  showModelSelector(initialSearchInput) {
2743
3367
  this.showSelector((done) => {
2744
3368
  const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
@@ -2748,6 +3372,7 @@ export class InteractiveMode {
2748
3372
  this.updateEditorBorderColor();
2749
3373
  done();
2750
3374
  this.showStatus(`Model: ${model.id}`);
3375
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
2751
3376
  this.checkDaxnutsEasterEgg(model);
2752
3377
  }
2753
3378
  catch (error) {
@@ -2773,33 +3398,24 @@ export class InteractiveMode {
2773
3398
  const sessionScopedModels = this.session.scopedModels;
2774
3399
  const hasSessionScope = sessionScopedModels.length > 0;
2775
3400
  // Build enabled model IDs from session state or settings
2776
- const enabledModelIds = new Set();
2777
- let hasFilter = false;
3401
+ let currentEnabledIds = null;
2778
3402
  if (hasSessionScope) {
2779
3403
  // Use current session's scoped models
2780
- for (const sm of sessionScopedModels) {
2781
- enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
2782
- }
2783
- hasFilter = true;
3404
+ currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
2784
3405
  }
2785
3406
  else {
2786
3407
  // Fall back to settings
2787
3408
  const patterns = this.settingsManager.getEnabledModels();
2788
3409
  if (patterns !== undefined && patterns.length > 0) {
2789
- hasFilter = true;
2790
3410
  const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
2791
- for (const sm of scopedModels) {
2792
- enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
2793
- }
3411
+ currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
2794
3412
  }
2795
3413
  }
2796
- // Track current enabled state (session-only until persisted)
2797
- const currentEnabledIds = new Set(enabledModelIds);
2798
- let currentHasFilter = hasFilter;
2799
3414
  // Helper to update session's scoped models (session-only, no persist)
2800
3415
  const updateSessionModels = async (enabledIds) => {
2801
- if (enabledIds.size > 0 && enabledIds.size < allModels.length) {
2802
- const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
3416
+ currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3417
+ if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3418
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
2803
3419
  this.session.setScopedModels(newScopedModels.map((sm) => ({
2804
3420
  model: sm.model,
2805
3421
  thinkingLevel: sm.thinkingLevel,
@@ -2816,49 +3432,16 @@ export class InteractiveMode {
2816
3432
  const selector = new ScopedModelsSelectorComponent({
2817
3433
  allModels,
2818
3434
  enabledModelIds: currentEnabledIds,
2819
- hasEnabledModelsFilter: currentHasFilter,
2820
3435
  }, {
2821
- onModelToggle: async (modelId, enabled) => {
2822
- if (enabled) {
2823
- currentEnabledIds.add(modelId);
2824
- }
2825
- else {
2826
- currentEnabledIds.delete(modelId);
2827
- }
2828
- currentHasFilter = true;
2829
- await updateSessionModels(currentEnabledIds);
2830
- },
2831
- onEnableAll: async (allModelIds) => {
2832
- currentEnabledIds.clear();
2833
- for (const id of allModelIds) {
2834
- currentEnabledIds.add(id);
2835
- }
2836
- currentHasFilter = false;
2837
- await updateSessionModels(currentEnabledIds);
2838
- },
2839
- onClearAll: async () => {
2840
- currentEnabledIds.clear();
2841
- currentHasFilter = true;
2842
- await updateSessionModels(currentEnabledIds);
2843
- },
2844
- onToggleProvider: async (_provider, modelIds, enabled) => {
2845
- for (const id of modelIds) {
2846
- if (enabled) {
2847
- currentEnabledIds.add(id);
2848
- }
2849
- else {
2850
- currentEnabledIds.delete(id);
2851
- }
2852
- }
2853
- currentHasFilter = true;
2854
- await updateSessionModels(currentEnabledIds);
3436
+ onChange: async (enabledIds) => {
3437
+ await updateSessionModels(enabledIds);
2855
3438
  },
2856
3439
  onPersist: (enabledIds) => {
2857
3440
  // Persist to settings
2858
- const newPatterns = enabledIds.length === allModels.length
3441
+ const newPatterns = enabledIds === null || enabledIds.length === allModels.length
2859
3442
  ? undefined // All enabled = clear filter
2860
3443
  : enabledIds;
2861
- this.settingsManager.setEnabledModels(newPatterns);
3444
+ this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
2862
3445
  this.showStatus("Model selection saved to settings");
2863
3446
  },
2864
3447
  onCancel: () => {
@@ -2875,27 +3458,52 @@ export class InteractiveMode {
2875
3458
  this.showStatus("No messages to fork from");
2876
3459
  return;
2877
3460
  }
3461
+ const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
2878
3462
  this.showSelector((done) => {
2879
3463
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
2880
- const result = await this.session.fork(entryId);
2881
- if (result.cancelled) {
2882
- // Extension cancelled the fork
3464
+ try {
3465
+ const result = await this.runtimeHost.fork(entryId);
3466
+ if (result.cancelled) {
3467
+ done();
3468
+ this.ui.requestRender();
3469
+ return;
3470
+ }
3471
+ this.renderCurrentSessionState();
3472
+ this.editor.setText(result.selectedText ?? "");
2883
3473
  done();
2884
- this.ui.requestRender();
2885
- return;
3474
+ this.showStatus("Forked to new session");
3475
+ }
3476
+ catch (error) {
3477
+ done();
3478
+ this.showError(error instanceof Error ? error.message : String(error));
2886
3479
  }
2887
- this.chatContainer.clear();
2888
- this.renderInitialMessages();
2889
- this.editor.setText(result.selectedText);
2890
- done();
2891
- this.showStatus("Branched to new session");
2892
3480
  }, () => {
2893
3481
  done();
2894
3482
  this.ui.requestRender();
2895
- });
3483
+ }, initialSelectedId);
2896
3484
  return { component: selector, focus: selector.getMessageList() };
2897
3485
  });
2898
3486
  }
3487
+ async handleCloneCommand() {
3488
+ const leafId = this.sessionManager.getLeafId();
3489
+ if (!leafId) {
3490
+ this.showStatus("Nothing to clone yet");
3491
+ return;
3492
+ }
3493
+ try {
3494
+ const result = await this.runtimeHost.fork(leafId, { position: "at" });
3495
+ if (result.cancelled) {
3496
+ this.ui.requestRender();
3497
+ return;
3498
+ }
3499
+ this.renderCurrentSessionState();
3500
+ this.editor.setText("");
3501
+ this.showStatus("Cloned to new session");
3502
+ }
3503
+ catch (error) {
3504
+ this.showError(error instanceof Error ? error.message : String(error));
3505
+ }
3506
+ }
2899
3507
  showTreeSelector(initialSelectedId) {
2900
3508
  const tree = this.sessionManager.getTree();
2901
3509
  const realLeafId = this.sessionManager.getLeafId();
@@ -2950,7 +3558,7 @@ export class InteractiveMode {
2950
3558
  this.session.abortBranchSummary();
2951
3559
  };
2952
3560
  this.chatContainer.addChild(new Spacer(1));
2953
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`);
3561
+ summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
2954
3562
  this.statusContainer.addChild(summaryLoader);
2955
3563
  this.ui.requestRender();
2956
3564
  }
@@ -2976,6 +3584,7 @@ export class InteractiveMode {
2976
3584
  this.editor.setText(result.editorText);
2977
3585
  }
2978
3586
  this.showStatus("Navigated to selected point");
3587
+ void this.flushCompactionQueue({ willRetry: false });
2979
3588
  }
2980
3589
  catch (error) {
2981
3590
  this.showError(error instanceof Error ? error.message : String(error));
@@ -3021,73 +3630,297 @@ export class InteractiveMode {
3021
3630
  return { component: selector, focus: selector };
3022
3631
  });
3023
3632
  }
3024
- async handleResumeSession(sessionPath) {
3025
- // Stop loading animation
3633
+ async handleResumeSession(sessionPath, options) {
3026
3634
  if (this.loadingAnimation) {
3027
3635
  this.loadingAnimation.stop();
3028
3636
  this.loadingAnimation = undefined;
3029
3637
  }
3030
3638
  this.statusContainer.clear();
3031
- // Clear UI state
3032
- this.pendingMessagesContainer.clear();
3033
- this.compactionQueuedMessages = [];
3034
- this.streamingComponent = undefined;
3035
- this.streamingMessage = undefined;
3036
- this.pendingTools.clear();
3037
- // Switch session via AgentSession (emits extension session events)
3038
- await this.session.switchSession(sessionPath);
3039
- // Clear and re-render the chat
3040
- this.chatContainer.clear();
3041
- this.renderInitialMessages();
3042
- this.showStatus("Resumed session");
3639
+ try {
3640
+ const result = await this.runtimeHost.switchSession(sessionPath, {
3641
+ withSession: options?.withSession,
3642
+ });
3643
+ if (result.cancelled) {
3644
+ return result;
3645
+ }
3646
+ this.renderCurrentSessionState();
3647
+ this.showStatus("Resumed session");
3648
+ return result;
3649
+ }
3650
+ catch (error) {
3651
+ if (error instanceof MissingSessionCwdError) {
3652
+ const selectedCwd = await this.promptForMissingSessionCwd(error);
3653
+ if (!selectedCwd) {
3654
+ this.showStatus("Resume cancelled");
3655
+ return { cancelled: true };
3656
+ }
3657
+ const result = await this.runtimeHost.switchSession(sessionPath, {
3658
+ cwdOverride: selectedCwd,
3659
+ withSession: options?.withSession,
3660
+ });
3661
+ if (result.cancelled) {
3662
+ return result;
3663
+ }
3664
+ this.renderCurrentSessionState();
3665
+ this.showStatus("Resumed session in current cwd");
3666
+ return result;
3667
+ }
3668
+ return this.handleFatalRuntimeError("Failed to resume session", error);
3669
+ }
3043
3670
  }
3044
- async showOAuthSelector(mode) {
3045
- if (mode === "logout") {
3046
- const providers = this.session.modelRegistry.authStorage.list();
3047
- const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth");
3048
- if (loggedInProviders.length === 0) {
3049
- this.showStatus("No OAuth providers logged in. Use /login first.");
3050
- return;
3671
+ getLoginProviderOptions(authType) {
3672
+ const authStorage = this.session.modelRegistry.authStorage;
3673
+ const oauthProviders = authStorage.getOAuthProviders();
3674
+ const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
3675
+ const options = oauthProviders.map((provider) => ({
3676
+ id: provider.id,
3677
+ name: provider.name,
3678
+ authType: "oauth",
3679
+ }));
3680
+ const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
3681
+ for (const providerId of modelProviders) {
3682
+ if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
3683
+ continue;
3684
+ }
3685
+ options.push({
3686
+ id: providerId,
3687
+ name: this.session.modelRegistry.getProviderDisplayName(providerId),
3688
+ authType: "api_key",
3689
+ });
3690
+ }
3691
+ const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
3692
+ return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
3693
+ }
3694
+ getLogoutProviderOptions() {
3695
+ const authStorage = this.session.modelRegistry.authStorage;
3696
+ const options = [];
3697
+ for (const providerId of authStorage.list()) {
3698
+ const credential = authStorage.get(providerId);
3699
+ if (!credential) {
3700
+ continue;
3051
3701
  }
3702
+ options.push({
3703
+ id: providerId,
3704
+ name: this.session.modelRegistry.getProviderDisplayName(providerId),
3705
+ authType: credential.type,
3706
+ });
3707
+ }
3708
+ return options.sort((a, b) => a.name.localeCompare(b.name));
3709
+ }
3710
+ showLoginAuthTypeSelector() {
3711
+ const subscriptionLabel = "Use a subscription";
3712
+ const apiKeyLabel = "Use an API key";
3713
+ this.showSelector((done) => {
3714
+ const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
3715
+ done();
3716
+ const authType = option === subscriptionLabel ? "oauth" : "api_key";
3717
+ this.showLoginProviderSelector(authType);
3718
+ }, () => {
3719
+ done();
3720
+ this.ui.requestRender();
3721
+ });
3722
+ return { component: selector, focus: selector };
3723
+ });
3724
+ }
3725
+ showLoginProviderSelector(authType) {
3726
+ const providerOptions = this.getLoginProviderOptions(authType);
3727
+ if (providerOptions.length === 0) {
3728
+ this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
3729
+ return;
3730
+ }
3731
+ this.showSelector((done) => {
3732
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
3733
+ done();
3734
+ const providerOption = providerOptions.find((provider) => provider.id === providerId);
3735
+ if (!providerOption) {
3736
+ return;
3737
+ }
3738
+ if (providerOption.authType === "oauth") {
3739
+ await this.showLoginDialog(providerOption.id, providerOption.name);
3740
+ }
3741
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3742
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
3743
+ }
3744
+ else {
3745
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
3746
+ }
3747
+ }, () => {
3748
+ done();
3749
+ this.showLoginAuthTypeSelector();
3750
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
3751
+ return { component: selector, focus: selector };
3752
+ });
3753
+ }
3754
+ async showOAuthSelector(mode) {
3755
+ if (mode === "login") {
3756
+ this.showLoginAuthTypeSelector();
3757
+ return;
3758
+ }
3759
+ const providerOptions = this.getLogoutProviderOptions();
3760
+ if (providerOptions.length === 0) {
3761
+ this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
3762
+ return;
3052
3763
  }
3053
3764
  this.showSelector((done) => {
3054
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
3765
+ const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
3055
3766
  done();
3056
- if (mode === "login") {
3057
- await this.showLoginDialog(providerId);
3767
+ const providerOption = providerOptions.find((provider) => provider.id === providerId);
3768
+ if (!providerOption) {
3769
+ return;
3770
+ }
3771
+ try {
3772
+ this.session.modelRegistry.authStorage.logout(providerOption.id);
3773
+ this.session.modelRegistry.refresh();
3774
+ await this.updateAvailableProviderCount();
3775
+ const message = providerOption.authType === "oauth"
3776
+ ? `Logged out of ${providerOption.name}`
3777
+ : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
3778
+ this.showStatus(message);
3779
+ }
3780
+ catch (error) {
3781
+ this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3782
+ }
3783
+ }, () => {
3784
+ done();
3785
+ this.ui.requestRender();
3786
+ });
3787
+ return { component: selector, focus: selector };
3788
+ });
3789
+ }
3790
+ async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
3791
+ this.session.modelRegistry.refresh();
3792
+ const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
3793
+ let selectedModel;
3794
+ let selectionError;
3795
+ if (isUnknownModel(previousModel)) {
3796
+ const availableModels = this.session.modelRegistry.getAvailable();
3797
+ const providerModels = availableModels.filter((model) => model.provider === providerId);
3798
+ if (!hasDefaultModelProvider(providerId)) {
3799
+ selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
3800
+ }
3801
+ else if (providerModels.length === 0) {
3802
+ selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
3803
+ }
3804
+ else {
3805
+ const defaultModelId = defaultModelPerProvider[providerId];
3806
+ selectedModel = providerModels.find((model) => model.id === defaultModelId);
3807
+ if (!selectedModel) {
3808
+ selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
3058
3809
  }
3059
3810
  else {
3060
- // Logout flow
3061
- const providerInfo = this.session.modelRegistry.authStorage
3062
- .getOAuthProviders()
3063
- .find((p) => p.id === providerId);
3064
- const providerName = providerInfo?.name || providerId;
3065
3811
  try {
3066
- this.session.modelRegistry.authStorage.logout(providerId);
3067
- this.session.modelRegistry.refresh();
3068
- await this.updateAvailableProviderCount();
3069
- this.showStatus(`Logged out of ${providerName}`);
3812
+ await this.session.setModel(selectedModel);
3070
3813
  }
3071
3814
  catch (error) {
3072
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3815
+ selectedModel = undefined;
3816
+ const errorMessage = error instanceof Error ? error.message : String(error);
3817
+ selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
3073
3818
  }
3074
3819
  }
3075
- }, () => {
3076
- done();
3820
+ }
3821
+ }
3822
+ await this.updateAvailableProviderCount();
3823
+ this.footer.invalidate();
3824
+ this.updateEditorBorderColor();
3825
+ if (selectedModel) {
3826
+ this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
3827
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
3828
+ this.checkDaxnutsEasterEgg(selectedModel);
3829
+ }
3830
+ else {
3831
+ this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
3832
+ if (selectionError) {
3833
+ this.showError(selectionError);
3834
+ }
3835
+ else {
3836
+ void this.maybeWarnAboutAnthropicSubscriptionAuth();
3837
+ }
3838
+ }
3839
+ }
3840
+ showBedrockSetupDialog(providerId, providerName) {
3841
+ const restoreEditor = () => {
3842
+ this.editorContainer.clear();
3843
+ this.editorContainer.addChild(this.editor);
3844
+ this.ui.setFocus(this.editor);
3845
+ this.ui.requestRender();
3846
+ };
3847
+ const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
3848
+ dialog.showInfo([
3849
+ theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
3850
+ theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
3851
+ theme.fg("muted", "See:"),
3852
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
3853
+ ]);
3854
+ this.editorContainer.clear();
3855
+ this.editorContainer.addChild(dialog);
3856
+ this.ui.setFocus(dialog);
3857
+ this.ui.requestRender();
3858
+ }
3859
+ async showApiKeyLoginDialog(providerId, providerName) {
3860
+ const previousModel = this.session.model;
3861
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3862
+ // Completion handled below
3863
+ }, providerName);
3864
+ this.editorContainer.clear();
3865
+ this.editorContainer.addChild(dialog);
3866
+ this.ui.setFocus(dialog);
3867
+ this.ui.requestRender();
3868
+ const restoreEditor = () => {
3869
+ this.editorContainer.clear();
3870
+ this.editorContainer.addChild(this.editor);
3871
+ this.ui.setFocus(this.editor);
3872
+ this.ui.requestRender();
3873
+ };
3874
+ try {
3875
+ const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
3876
+ if (!apiKey) {
3877
+ throw new Error("API key cannot be empty.");
3878
+ }
3879
+ this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
3880
+ restoreEditor();
3881
+ await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
3882
+ }
3883
+ catch (error) {
3884
+ restoreEditor();
3885
+ const errorMsg = error instanceof Error ? error.message : String(error);
3886
+ if (errorMsg !== "Login cancelled") {
3887
+ this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
3888
+ }
3889
+ }
3890
+ }
3891
+ showOAuthLoginSelect(dialog, prompt) {
3892
+ return new Promise((resolve) => {
3893
+ const restoreDialog = () => {
3894
+ this.editorContainer.clear();
3895
+ this.editorContainer.addChild(dialog);
3896
+ this.ui.setFocus(dialog);
3077
3897
  this.ui.requestRender();
3898
+ };
3899
+ const labels = prompt.options.map((option) => option.label);
3900
+ const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
3901
+ restoreDialog();
3902
+ resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
3903
+ }, () => {
3904
+ restoreDialog();
3905
+ resolve(undefined);
3078
3906
  });
3079
- return { component: selector, focus: selector };
3907
+ this.editorContainer.clear();
3908
+ this.editorContainer.addChild(selector);
3909
+ this.ui.setFocus(selector);
3910
+ this.ui.requestRender();
3080
3911
  });
3081
3912
  }
3082
- async showLoginDialog(providerId) {
3083
- const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
3084
- const providerName = providerInfo?.name || providerId;
3913
+ async showLoginDialog(providerId, providerName) {
3914
+ const providerInfo = this.session.modelRegistry.authStorage
3915
+ .getOAuthProviders()
3916
+ .find((provider) => provider.id === providerId);
3917
+ const previousModel = this.session.model;
3085
3918
  // Providers that use callback servers (can paste redirect URL)
3086
3919
  const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
3087
3920
  // Create login dialog component
3088
3921
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3089
3922
  // Completion handled below
3090
- });
3923
+ }, providerName);
3091
3924
  // Show dialog in editor container
3092
3925
  this.editorContainer.clear();
3093
3926
  this.editorContainer.addChild(dialog);
@@ -3140,14 +3973,13 @@ export class InteractiveMode {
3140
3973
  onProgress: (message) => {
3141
3974
  dialog.showProgress(message);
3142
3975
  },
3976
+ onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
3143
3977
  onManualCodeInput: () => manualCodePromise,
3144
3978
  signal: dialog.signal,
3145
3979
  });
3146
3980
  // Success
3147
3981
  restoreEditor();
3148
- this.session.modelRegistry.refresh();
3149
- await this.updateAvailableProviderCount();
3150
- this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
3982
+ await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
3151
3983
  }
3152
3984
  catch (error) {
3153
3985
  restoreEditor();
@@ -3170,16 +4002,20 @@ export class InteractiveMode {
3170
4002
  return;
3171
4003
  }
3172
4004
  this.resetExtensionUI();
3173
- const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3174
- cancellable: false,
3175
- });
4005
+ const reloadBox = new Container();
4006
+ const borderColor = (s) => theme.fg("border", s);
4007
+ reloadBox.addChild(new DynamicBorder(borderColor));
4008
+ reloadBox.addChild(new Spacer(1));
4009
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4010
+ reloadBox.addChild(new Spacer(1));
4011
+ reloadBox.addChild(new DynamicBorder(borderColor));
3176
4012
  const previousEditor = this.editor;
3177
4013
  this.editorContainer.clear();
3178
- this.editorContainer.addChild(loader);
3179
- this.ui.setFocus(loader);
3180
- this.ui.requestRender();
3181
- const dismissLoader = (editor) => {
3182
- loader.dispose();
4014
+ this.editorContainer.addChild(reloadBox);
4015
+ this.ui.setFocus(reloadBox);
4016
+ this.ui.requestRender(true);
4017
+ await new Promise((resolve) => process.nextTick(resolve));
4018
+ const dismissReloadBox = (editor) => {
3183
4019
  this.editorContainer.clear();
3184
4020
  this.editorContainer.addChild(editor);
3185
4021
  this.ui.setFocus(editor);
@@ -3187,6 +4023,11 @@ export class InteractiveMode {
3187
4023
  };
3188
4024
  try {
3189
4025
  await this.session.reload();
4026
+ this.keybindings.reload();
4027
+ const activeHeader = this.customHeader ?? this.builtInHeader;
4028
+ if (isExpandable(activeHeader)) {
4029
+ activeHeader.setExpanded(this.toolOutputExpanded);
4030
+ }
3190
4031
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3191
4032
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3192
4033
  const themeName = this.settingsManager.getTheme();
@@ -3204,15 +4045,12 @@ export class InteractiveMode {
3204
4045
  }
3205
4046
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
3206
4047
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
3207
- this.setupAutocomplete(this.fdPath);
4048
+ this.setupAutocompleteProvider();
3208
4049
  const runner = this.session.extensionRunner;
3209
- if (runner) {
3210
- this.setupExtensionShortcuts(runner);
3211
- }
4050
+ this.setupExtensionShortcuts(runner);
3212
4051
  this.rebuildChatFromMessages();
3213
- dismissLoader(this.editor);
4052
+ dismissReloadBox(this.editor);
3214
4053
  this.showLoadedResources({
3215
- extensionPaths: runner?.getExtensionPaths() ?? [],
3216
4054
  force: false,
3217
4055
  showDiagnosticsWhenQuiet: true,
3218
4056
  });
@@ -3220,24 +4058,102 @@ export class InteractiveMode {
3220
4058
  if (modelsJsonError) {
3221
4059
  this.showError(`models.json error: ${modelsJsonError}`);
3222
4060
  }
3223
- this.showStatus("Reloaded extensions, skills, prompts, themes");
4061
+ this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
3224
4062
  }
3225
4063
  catch (error) {
3226
- dismissLoader(previousEditor);
4064
+ dismissReloadBox(previousEditor);
3227
4065
  this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
3228
4066
  }
3229
4067
  }
3230
4068
  async handleExportCommand(text) {
3231
- const parts = text.split(/\s+/);
3232
- const outputPath = parts.length > 1 ? parts[1] : undefined;
4069
+ const outputPath = this.getPathCommandArgument(text, "/export");
3233
4070
  try {
3234
- const filePath = await this.session.exportToHtml(outputPath);
3235
- this.showStatus(`Session exported to: ${filePath}`);
4071
+ if (outputPath?.endsWith(".jsonl")) {
4072
+ const filePath = this.session.exportToJsonl(outputPath);
4073
+ this.showStatus(`Session exported to: ${filePath}`);
4074
+ }
4075
+ else {
4076
+ const filePath = await this.session.exportToHtml(outputPath);
4077
+ this.showStatus(`Session exported to: ${filePath}`);
4078
+ }
3236
4079
  }
3237
4080
  catch (error) {
3238
4081
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
3239
4082
  }
3240
4083
  }
4084
+ getPathCommandArgument(text, command) {
4085
+ if (text === command) {
4086
+ return undefined;
4087
+ }
4088
+ if (!text.startsWith(`${command} `)) {
4089
+ return undefined;
4090
+ }
4091
+ const argsString = text.slice(command.length + 1).trimStart();
4092
+ if (!argsString) {
4093
+ return undefined;
4094
+ }
4095
+ const firstChar = argsString[0];
4096
+ if (firstChar === '"' || firstChar === "'") {
4097
+ const closingQuoteIndex = argsString.indexOf(firstChar, 1);
4098
+ if (closingQuoteIndex < 0) {
4099
+ return undefined;
4100
+ }
4101
+ return argsString.slice(1, closingQuoteIndex);
4102
+ }
4103
+ const firstWhitespaceIndex = argsString.search(/\s/);
4104
+ if (firstWhitespaceIndex < 0) {
4105
+ return argsString;
4106
+ }
4107
+ return argsString.slice(0, firstWhitespaceIndex);
4108
+ }
4109
+ async handleImportCommand(text) {
4110
+ const inputPath = this.getPathCommandArgument(text, "/import");
4111
+ if (!inputPath) {
4112
+ this.showError("Usage: /import <path.jsonl>");
4113
+ return;
4114
+ }
4115
+ const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`);
4116
+ if (!confirmed) {
4117
+ this.showStatus("Import cancelled");
4118
+ return;
4119
+ }
4120
+ try {
4121
+ if (this.loadingAnimation) {
4122
+ this.loadingAnimation.stop();
4123
+ this.loadingAnimation = undefined;
4124
+ }
4125
+ this.statusContainer.clear();
4126
+ const result = await this.runtimeHost.importFromJsonl(inputPath);
4127
+ if (result.cancelled) {
4128
+ this.showStatus("Import cancelled");
4129
+ return;
4130
+ }
4131
+ this.renderCurrentSessionState();
4132
+ this.showStatus(`Session imported from: ${inputPath}`);
4133
+ }
4134
+ catch (error) {
4135
+ if (error instanceof MissingSessionCwdError) {
4136
+ const selectedCwd = await this.promptForMissingSessionCwd(error);
4137
+ if (!selectedCwd) {
4138
+ this.showStatus("Import cancelled");
4139
+ return;
4140
+ }
4141
+ const result = await this.runtimeHost.importFromJsonl(inputPath, selectedCwd);
4142
+ if (result.cancelled) {
4143
+ this.showStatus("Import cancelled");
4144
+ return;
4145
+ }
4146
+ this.renderCurrentSessionState();
4147
+ this.showStatus(`Session imported from: ${inputPath}`);
4148
+ return;
4149
+ }
4150
+ if (error instanceof SessionImportFileNotFoundError) {
4151
+ this.showError(`Failed to import session: ${error.message}`);
4152
+ return;
4153
+ }
4154
+ await this.handleFatalRuntimeError("Failed to import session", error);
4155
+ }
4156
+ }
3241
4157
  async handleShareCommand() {
3242
4158
  // Check if gh is available and logged in
3243
4159
  try {
@@ -3325,14 +4241,14 @@ export class InteractiveMode {
3325
4241
  }
3326
4242
  }
3327
4243
  }
3328
- handleCopyCommand() {
4244
+ async handleCopyCommand() {
3329
4245
  const text = this.session.getLastAssistantText();
3330
4246
  if (!text) {
3331
4247
  this.showError("No agent messages to copy yet.");
3332
4248
  return;
3333
4249
  }
3334
4250
  try {
3335
- copyToClipboard(text);
4251
+ await copyToClipboard(text);
3336
4252
  this.showStatus("Copied last agent message to clipboard");
3337
4253
  }
3338
4254
  catch (error) {
@@ -3353,8 +4269,7 @@ export class InteractiveMode {
3353
4269
  this.ui.requestRender();
3354
4270
  return;
3355
4271
  }
3356
- this.sessionManager.appendSessionInfo(name);
3357
- this.updateTerminalTitle();
4272
+ this.session.setSessionName(name);
3358
4273
  this.chatContainer.addChild(new Spacer(1));
3359
4274
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
3360
4275
  this.ui.requestRender();
@@ -3409,69 +4324,63 @@ export class InteractiveMode {
3409
4324
  this.chatContainer.addChild(new DynamicBorder());
3410
4325
  this.ui.requestRender();
3411
4326
  }
3412
- /**
3413
- * Capitalize keybinding for display (e.g., "ctrl+c" -> "Ctrl+C").
3414
- */
3415
- capitalizeKey(key) {
3416
- return key
3417
- .split("/")
3418
- .map((k) => k
3419
- .split("+")
3420
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
3421
- .join("+"))
3422
- .join("/");
3423
- }
3424
4327
  /**
3425
4328
  * Get capitalized display string for an app keybinding action.
3426
4329
  */
3427
4330
  getAppKeyDisplay(action) {
3428
- return this.capitalizeKey(appKey(this.keybindings, action));
4331
+ return keyDisplayText(action);
3429
4332
  }
3430
4333
  /**
3431
4334
  * Get capitalized display string for an editor keybinding action.
3432
4335
  */
3433
4336
  getEditorKeyDisplay(action) {
3434
- return this.capitalizeKey(editorKey(action));
4337
+ return keyDisplayText(action);
3435
4338
  }
3436
4339
  handleHotkeysCommand() {
3437
4340
  // Navigation keybindings
3438
- const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
3439
- const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
3440
- const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
3441
- const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3442
- const jumpForward = this.getEditorKeyDisplay("jumpForward");
3443
- const jumpBackward = this.getEditorKeyDisplay("jumpBackward");
3444
- const pageUp = this.getEditorKeyDisplay("pageUp");
3445
- const pageDown = this.getEditorKeyDisplay("pageDown");
4341
+ const cursorUp = this.getEditorKeyDisplay("tui.editor.cursorUp");
4342
+ const cursorDown = this.getEditorKeyDisplay("tui.editor.cursorDown");
4343
+ const cursorLeft = this.getEditorKeyDisplay("tui.editor.cursorLeft");
4344
+ const cursorRight = this.getEditorKeyDisplay("tui.editor.cursorRight");
4345
+ const cursorWordLeft = this.getEditorKeyDisplay("tui.editor.cursorWordLeft");
4346
+ const cursorWordRight = this.getEditorKeyDisplay("tui.editor.cursorWordRight");
4347
+ const cursorLineStart = this.getEditorKeyDisplay("tui.editor.cursorLineStart");
4348
+ const cursorLineEnd = this.getEditorKeyDisplay("tui.editor.cursorLineEnd");
4349
+ const jumpForward = this.getEditorKeyDisplay("tui.editor.jumpForward");
4350
+ const jumpBackward = this.getEditorKeyDisplay("tui.editor.jumpBackward");
4351
+ const pageUp = this.getEditorKeyDisplay("tui.editor.pageUp");
4352
+ const pageDown = this.getEditorKeyDisplay("tui.editor.pageDown");
3446
4353
  // Editing keybindings
3447
- const submit = this.getEditorKeyDisplay("submit");
3448
- const newLine = this.getEditorKeyDisplay("newLine");
3449
- const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
3450
- const deleteWordForward = this.getEditorKeyDisplay("deleteWordForward");
3451
- const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
3452
- const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
3453
- const yank = this.getEditorKeyDisplay("yank");
3454
- const yankPop = this.getEditorKeyDisplay("yankPop");
3455
- const undo = this.getEditorKeyDisplay("undo");
3456
- const tab = this.getEditorKeyDisplay("tab");
4354
+ const submit = this.getEditorKeyDisplay("tui.input.submit");
4355
+ const newLine = this.getEditorKeyDisplay("tui.input.newLine");
4356
+ const deleteWordBackward = this.getEditorKeyDisplay("tui.editor.deleteWordBackward");
4357
+ const deleteWordForward = this.getEditorKeyDisplay("tui.editor.deleteWordForward");
4358
+ const deleteToLineStart = this.getEditorKeyDisplay("tui.editor.deleteToLineStart");
4359
+ const deleteToLineEnd = this.getEditorKeyDisplay("tui.editor.deleteToLineEnd");
4360
+ const yank = this.getEditorKeyDisplay("tui.editor.yank");
4361
+ const yankPop = this.getEditorKeyDisplay("tui.editor.yankPop");
4362
+ const undo = this.getEditorKeyDisplay("tui.editor.undo");
4363
+ const tab = this.getEditorKeyDisplay("tui.input.tab");
3457
4364
  // App keybindings
3458
- const interrupt = this.getAppKeyDisplay("interrupt");
3459
- const clear = this.getAppKeyDisplay("clear");
3460
- const exit = this.getAppKeyDisplay("exit");
3461
- const suspend = this.getAppKeyDisplay("suspend");
3462
- const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
3463
- const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3464
- const selectModel = this.getAppKeyDisplay("selectModel");
3465
- const expandTools = this.getAppKeyDisplay("expandTools");
3466
- const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3467
- const externalEditor = this.getAppKeyDisplay("externalEditor");
3468
- const followUp = this.getAppKeyDisplay("followUp");
3469
- const dequeue = this.getAppKeyDisplay("dequeue");
4365
+ const interrupt = this.getAppKeyDisplay("app.interrupt");
4366
+ const clear = this.getAppKeyDisplay("app.clear");
4367
+ const exit = this.getAppKeyDisplay("app.exit");
4368
+ const suspend = this.getAppKeyDisplay("app.suspend");
4369
+ const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
4370
+ const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
4371
+ const selectModel = this.getAppKeyDisplay("app.model.select");
4372
+ const expandTools = this.getAppKeyDisplay("app.tools.expand");
4373
+ const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
4374
+ const externalEditor = this.getAppKeyDisplay("app.editor.external");
4375
+ const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
4376
+ const followUp = this.getAppKeyDisplay("app.message.followUp");
4377
+ const dequeue = this.getAppKeyDisplay("app.message.dequeue");
4378
+ const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
3470
4379
  let hotkeys = `
3471
4380
  **Navigation**
3472
4381
  | Key | Action |
3473
4382
  |-----|--------|
3474
- | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
4383
+ | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
3475
4384
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3476
4385
  | \`${cursorLineStart}\` | Start of line |
3477
4386
  | \`${cursorLineEnd}\` | End of line |
@@ -3501,33 +4410,31 @@ export class InteractiveMode {
3501
4410
  | \`${exit}\` | Exit (when editor is empty) |
3502
4411
  | \`${suspend}\` | Suspend to background |
3503
4412
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3504
- | \`${cycleModelForward}\` | Cycle models |
4413
+ | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
3505
4414
  | \`${selectModel}\` | Open model selector |
3506
4415
  | \`${expandTools}\` | Toggle tool output expansion |
3507
4416
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3508
4417
  | \`${externalEditor}\` | Edit message in external editor |
3509
4418
  | \`${followUp}\` | Queue follow-up message |
3510
4419
  | \`${dequeue}\` | Restore queued messages |
3511
- | \`Ctrl+V\` | Paste image from clipboard |
4420
+ | \`${pasteImage}\` | Paste image from clipboard |
3512
4421
  | \`/\` | Slash commands |
3513
4422
  | \`!\` | Run bash command |
3514
4423
  | \`!!\` | Run bash command (excluded from context) |
3515
4424
  `;
3516
4425
  // Add extension-registered shortcuts
3517
4426
  const extensionRunner = this.session.extensionRunner;
3518
- if (extensionRunner) {
3519
- const shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());
3520
- if (shortcuts.size > 0) {
3521
- hotkeys += `
4427
+ const shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());
4428
+ if (shortcuts.size > 0) {
4429
+ hotkeys += `
3522
4430
  **Extensions**
3523
4431
  | Key | Action |
3524
4432
  |-----|--------|
3525
4433
  `;
3526
- for (const [key, shortcut] of shortcuts) {
3527
- const description = shortcut.description ?? shortcut.extensionPath;
3528
- const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
3529
- hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
3530
- }
4434
+ for (const [key, shortcut] of shortcuts) {
4435
+ const description = shortcut.description ?? shortcut.extensionPath;
4436
+ const keyDisplay = formatKeyText(key, { capitalize: true });
4437
+ hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
3531
4438
  }
3532
4439
  }
3533
4440
  this.chatContainer.addChild(new Spacer(1));
@@ -3539,25 +4446,24 @@ export class InteractiveMode {
3539
4446
  this.ui.requestRender();
3540
4447
  }
3541
4448
  async handleClearCommand() {
3542
- // Stop loading animation
3543
4449
  if (this.loadingAnimation) {
3544
4450
  this.loadingAnimation.stop();
3545
4451
  this.loadingAnimation = undefined;
3546
4452
  }
3547
4453
  this.statusContainer.clear();
3548
- // New session via session (emits extension session events)
3549
- await this.session.newSession();
3550
- // Clear UI state
3551
- this.headerContainer.clear();
3552
- this.chatContainer.clear();
3553
- this.pendingMessagesContainer.clear();
3554
- this.compactionQueuedMessages = [];
3555
- this.streamingComponent = undefined;
3556
- this.streamingMessage = undefined;
3557
- this.pendingTools.clear();
3558
- this.chatContainer.addChild(new Spacer(1));
3559
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
3560
- this.ui.requestRender();
4454
+ try {
4455
+ const result = await this.runtimeHost.newSession();
4456
+ if (result.cancelled) {
4457
+ return;
4458
+ }
4459
+ this.renderCurrentSessionState();
4460
+ this.chatContainer.addChild(new Spacer(1));
4461
+ this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
4462
+ this.ui.requestRender();
4463
+ }
4464
+ catch (error) {
4465
+ await this.handleFatalRuntimeError("Failed to create session", error);
4466
+ }
3561
4467
  }
3562
4468
  handleDebugCommand() {
3563
4469
  const width = this.ui.terminal.columns;
@@ -3591,6 +4497,11 @@ export class InteractiveMode {
3591
4497
  this.chatContainer.addChild(new ArminComponent(this.ui));
3592
4498
  this.ui.requestRender();
3593
4499
  }
4500
+ handleDementedDelves() {
4501
+ this.chatContainer.addChild(new Spacer(1));
4502
+ this.chatContainer.addChild(new EarendilAnnouncementComponent());
4503
+ this.ui.requestRender();
4504
+ }
3594
4505
  handleDaxnuts() {
3595
4506
  this.chatContainer.addChild(new Spacer(1));
3596
4507
  this.chatContainer.addChild(new DaxnutsComponent(this.ui));
@@ -3604,14 +4515,12 @@ export class InteractiveMode {
3604
4515
  async handleBashCommand(command, excludeFromContext = false) {
3605
4516
  const extensionRunner = this.session.extensionRunner;
3606
4517
  // Emit user_bash event to let extensions intercept
3607
- const eventResult = extensionRunner
3608
- ? await extensionRunner.emitUserBash({
3609
- type: "user_bash",
3610
- command,
3611
- excludeFromContext,
3612
- cwd: process.cwd(),
3613
- })
3614
- : undefined;
4518
+ const eventResult = await extensionRunner.emitUserBash({
4519
+ type: "user_bash",
4520
+ command,
4521
+ excludeFromContext,
4522
+ cwd: this.sessionManager.getCwd(),
4523
+ });
3615
4524
  // If extension returned a full result, use it directly
3616
4525
  if (eventResult?.result) {
3617
4526
  const result = eventResult.result;
@@ -3675,55 +4584,23 @@ export class InteractiveMode {
3675
4584
  this.showWarning("Nothing to compact (no messages yet)");
3676
4585
  return;
3677
4586
  }
3678
- await this.executeCompaction(customInstructions, false);
3679
- }
3680
- async executeCompaction(customInstructions, isAuto = false) {
3681
- // Stop loading animation
3682
4587
  if (this.loadingAnimation) {
3683
4588
  this.loadingAnimation.stop();
3684
4589
  this.loadingAnimation = undefined;
3685
4590
  }
3686
4591
  this.statusContainer.clear();
3687
- // Set up escape handler during compaction
3688
- const originalOnEscape = this.defaultEditor.onEscape;
3689
- this.defaultEditor.onEscape = () => {
3690
- this.session.abortCompaction();
3691
- };
3692
- // Show compacting status
3693
- this.chatContainer.addChild(new Spacer(1));
3694
- const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`;
3695
- const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
3696
- const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
3697
- this.statusContainer.addChild(compactingLoader);
3698
- this.ui.requestRender();
3699
- let result;
3700
4592
  try {
3701
- result = await this.session.compact(customInstructions);
3702
- // Rebuild UI
3703
- this.rebuildChatFromMessages();
3704
- // Add compaction component at bottom so user sees it without scrolling
3705
- const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
3706
- this.addMessageToChat(msg);
3707
- this.footer.invalidate();
4593
+ await this.session.compact(customInstructions);
3708
4594
  }
3709
- catch (error) {
3710
- const message = error instanceof Error ? error.message : String(error);
3711
- if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
3712
- this.showError("Compaction cancelled");
3713
- }
3714
- else {
3715
- this.showError(`Compaction failed: ${message}`);
3716
- }
3717
- }
3718
- finally {
3719
- compactingLoader.stop();
3720
- this.statusContainer.clear();
3721
- this.defaultEditor.onEscape = originalOnEscape;
4595
+ catch {
4596
+ // Ignore, will be emitted as an event
3722
4597
  }
3723
- void this.flushCompactionQueue({ willRetry: false });
3724
- return result;
3725
4598
  }
3726
4599
  stop() {
4600
+ this.unregisterSignalHandlers();
4601
+ if (this.settingsManager.getShowTerminalProgress()) {
4602
+ this.ui.terminal.setProgress(false);
4603
+ }
3727
4604
  if (this.loadingAnimation) {
3728
4605
  this.loadingAnimation.stop();
3729
4606
  this.loadingAnimation = undefined;