@phi-code-admin/phi-code 0.74.2 → 0.75.0

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