@sammorrowdrums/mcpi 0.70.9 → 0.85.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 (888) hide show
  1. package/CHANGELOG.md +1623 -1
  2. package/LICENSE +21 -0
  3. package/README.md +196 -141
  4. package/dist/bun/cli.d.ts.map +1 -1
  5. package/dist/bun/cli.js +4 -0
  6. package/dist/bun/cli.js.map +1 -1
  7. package/dist/bun/register-bedrock.d.ts.map +1 -1
  8. package/dist/bun/register-bedrock.js +1 -1
  9. package/dist/bun/register-bedrock.js.map +1 -1
  10. package/dist/bun/restore-sandbox-env.d.ts +17 -0
  11. package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
  12. package/dist/bun/restore-sandbox-env.js +36 -0
  13. package/dist/bun/restore-sandbox-env.js.map +1 -0
  14. package/dist/cli/args.d.ts +9 -2
  15. package/dist/cli/args.d.ts.map +1 -1
  16. package/dist/cli/args.js +116 -16
  17. package/dist/cli/args.js.map +1 -1
  18. package/dist/cli/auth-check.d.ts +19 -0
  19. package/dist/cli/auth-check.d.ts.map +1 -0
  20. package/dist/cli/auth-check.js +50 -0
  21. package/dist/cli/auth-check.js.map +1 -0
  22. package/dist/cli/auth-command.d.ts +24 -0
  23. package/dist/cli/auth-command.d.ts.map +1 -0
  24. package/dist/cli/auth-command.js +103 -0
  25. package/dist/cli/auth-command.js.map +1 -0
  26. package/dist/cli/config-selector.d.ts +6 -4
  27. package/dist/cli/config-selector.d.ts.map +1 -1
  28. package/dist/cli/config-selector.js +4 -4
  29. package/dist/cli/config-selector.js.map +1 -1
  30. package/dist/cli/credential-print.d.ts +13 -0
  31. package/dist/cli/credential-print.d.ts.map +1 -0
  32. package/dist/cli/credential-print.js +76 -0
  33. package/dist/cli/credential-print.js.map +1 -0
  34. package/dist/cli/experimental/auth.d.ts +16 -0
  35. package/dist/cli/experimental/auth.d.ts.map +1 -0
  36. package/dist/cli/experimental/auth.js +13 -0
  37. package/dist/cli/experimental/auth.js.map +1 -0
  38. package/dist/cli/experimental/cli.d.ts +6 -0
  39. package/dist/cli/experimental/cli.d.ts.map +1 -0
  40. package/dist/cli/experimental/cli.js +5 -0
  41. package/dist/cli/experimental/cli.js.map +1 -0
  42. package/dist/cli/experimental/command-options.d.ts +17 -0
  43. package/dist/cli/experimental/command-options.d.ts.map +1 -0
  44. package/dist/cli/experimental/command-options.js +35 -0
  45. package/dist/cli/experimental/command-options.js.map +1 -0
  46. package/dist/cli/experimental/command.d.ts +63 -0
  47. package/dist/cli/experimental/command.d.ts.map +1 -0
  48. package/dist/cli/experimental/command.js +130 -0
  49. package/dist/cli/experimental/command.js.map +1 -0
  50. package/dist/cli/experimental/commands/client.d.ts +13 -0
  51. package/dist/cli/experimental/commands/client.d.ts.map +1 -0
  52. package/dist/cli/experimental/commands/client.js +25 -0
  53. package/dist/cli/experimental/commands/client.js.map +1 -0
  54. package/dist/cli/experimental/commands/mcpi.d.ts +15 -0
  55. package/dist/cli/experimental/commands/mcpi.d.ts.map +1 -0
  56. package/dist/cli/experimental/commands/mcpi.js +28 -0
  57. package/dist/cli/experimental/commands/mcpi.js.map +1 -0
  58. package/dist/cli/experimental/commands/server.d.ts +13 -0
  59. package/dist/cli/experimental/commands/server.d.ts.map +1 -0
  60. package/dist/cli/experimental/commands/server.js +25 -0
  61. package/dist/cli/experimental/commands/server.js.map +1 -0
  62. package/dist/cli/experimental/transport-address.d.ts +10 -0
  63. package/dist/cli/experimental/transport-address.d.ts.map +1 -0
  64. package/dist/cli/experimental/transport-address.js +38 -0
  65. package/dist/cli/experimental/transport-address.js.map +1 -0
  66. package/dist/cli/file-processor.d.ts.map +1 -1
  67. package/dist/cli/file-processor.js +13 -26
  68. package/dist/cli/file-processor.js.map +1 -1
  69. package/dist/cli/initial-message.d.ts +1 -1
  70. package/dist/cli/initial-message.d.ts.map +1 -1
  71. package/dist/cli/initial-message.js.map +1 -1
  72. package/dist/cli/list-models.d.ts +2 -2
  73. package/dist/cli/list-models.d.ts.map +1 -1
  74. package/dist/cli/list-models.js +4 -4
  75. package/dist/cli/list-models.js.map +1 -1
  76. package/dist/cli/project-trust.d.ts +10 -0
  77. package/dist/cli/project-trust.d.ts.map +1 -0
  78. package/dist/cli/project-trust.js +48 -0
  79. package/dist/cli/project-trust.js.map +1 -0
  80. package/dist/cli/session-picker.d.ts +3 -2
  81. package/dist/cli/session-picker.d.ts.map +1 -1
  82. package/dist/cli/session-picker.js +5 -4
  83. package/dist/cli/session-picker.js.map +1 -1
  84. package/dist/cli/startup-ui.d.ts +20 -0
  85. package/dist/cli/startup-ui.d.ts.map +1 -0
  86. package/dist/cli/startup-ui.js +184 -0
  87. package/dist/cli/startup-ui.js.map +1 -0
  88. package/dist/cli.d.ts.map +1 -1
  89. package/dist/cli.js +6 -3
  90. package/dist/cli.js.map +1 -1
  91. package/dist/client/index.d.ts +3 -0
  92. package/dist/client/index.d.ts.map +1 -0
  93. package/dist/client/index.js +3 -0
  94. package/dist/client/index.js.map +1 -0
  95. package/dist/client/remote-session.d.ts +53 -0
  96. package/dist/client/remote-session.d.ts.map +1 -0
  97. package/dist/client/remote-session.js +340 -0
  98. package/dist/client/remote-session.js.map +1 -0
  99. package/dist/client/transcript.d.ts +12 -0
  100. package/dist/client/transcript.d.ts.map +1 -0
  101. package/dist/client/transcript.js +98 -0
  102. package/dist/client/transcript.js.map +1 -0
  103. package/dist/config.d.ts +34 -12
  104. package/dist/config.d.ts.map +1 -1
  105. package/dist/config.js +312 -61
  106. package/dist/config.js.map +1 -1
  107. package/dist/core/agent-session-runtime.d.ts +11 -9
  108. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  109. package/dist/core/agent-session-runtime.js +16 -7
  110. package/dist/core/agent-session-runtime.js.map +1 -1
  111. package/dist/core/agent-session-services.d.ts +13 -12
  112. package/dist/core/agent-session-services.d.ts.map +1 -1
  113. package/dist/core/agent-session-services.js +34 -14
  114. package/dist/core/agent-session-services.js.map +1 -1
  115. package/dist/core/agent-session.d.ts +110 -53
  116. package/dist/core/agent-session.d.ts.map +1 -1
  117. package/dist/core/agent-session.js +658 -488
  118. package/dist/core/agent-session.js.map +1 -1
  119. package/dist/core/auth-guidance.d.ts.map +1 -1
  120. package/dist/core/auth-guidance.js.map +1 -1
  121. package/dist/core/auth-storage.d.ts +35 -104
  122. package/dist/core/auth-storage.d.ts.map +1 -1
  123. package/dist/core/auth-storage.js +272 -286
  124. package/dist/core/auth-storage.js.map +1 -1
  125. package/dist/core/bash-executor.d.ts +1 -1
  126. package/dist/core/bash-executor.d.ts.map +1 -1
  127. package/dist/core/bash-executor.js +11 -8
  128. package/dist/core/bash-executor.js.map +1 -1
  129. package/dist/core/cache-stats.d.ts +49 -0
  130. package/dist/core/cache-stats.d.ts.map +1 -0
  131. package/dist/core/cache-stats.js +101 -0
  132. package/dist/core/cache-stats.js.map +1 -0
  133. package/dist/core/compaction/branch-summarization.d.ts +16 -6
  134. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  135. package/dist/core/compaction/branch-summarization.js +13 -10
  136. package/dist/core/compaction/branch-summarization.js.map +1 -1
  137. package/dist/core/compaction/compaction.d.ts +25 -9
  138. package/dist/core/compaction/compaction.d.ts.map +1 -1
  139. package/dist/core/compaction/compaction.js +163 -133
  140. package/dist/core/compaction/compaction.js.map +1 -1
  141. package/dist/core/compaction/index.d.ts +3 -3
  142. package/dist/core/compaction/index.d.ts.map +1 -1
  143. package/dist/core/compaction/index.js.map +1 -1
  144. package/dist/core/compaction/utils.d.ts +3 -3
  145. package/dist/core/compaction/utils.d.ts.map +1 -1
  146. package/dist/core/compaction/utils.js +7 -18
  147. package/dist/core/compaction/utils.js.map +1 -1
  148. package/dist/core/defaults.d.ts +1 -1
  149. package/dist/core/defaults.d.ts.map +1 -1
  150. package/dist/core/defaults.js.map +1 -1
  151. package/dist/core/env-overlay.d.ts +21 -0
  152. package/dist/core/env-overlay.d.ts.map +1 -0
  153. package/dist/core/env-overlay.js +25 -0
  154. package/dist/core/env-overlay.js.map +1 -0
  155. package/dist/core/exec.d.ts +8 -1
  156. package/dist/core/exec.d.ts.map +1 -1
  157. package/dist/core/exec.js +7 -1
  158. package/dist/core/exec.js.map +1 -1
  159. package/dist/core/experimental.d.ts +6 -0
  160. package/dist/core/experimental.d.ts.map +1 -0
  161. package/dist/core/experimental.js +8 -0
  162. package/dist/core/experimental.js.map +1 -0
  163. package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
  164. package/dist/core/export-html/ansi-to-html.js +1 -1
  165. package/dist/core/export-html/ansi-to-html.js.map +1 -1
  166. package/dist/core/export-html/index.d.ts +2 -2
  167. package/dist/core/export-html/index.d.ts.map +1 -1
  168. package/dist/core/export-html/index.js +8 -6
  169. package/dist/core/export-html/index.js.map +1 -1
  170. package/dist/core/export-html/template.css +53 -4
  171. package/dist/core/export-html/template.js +129 -35
  172. package/dist/core/export-html/tool-renderer.d.ts +2 -8
  173. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  174. package/dist/core/export-html/tool-renderer.js +15 -2
  175. package/dist/core/export-html/tool-renderer.js.map +1 -1
  176. package/dist/core/export-html/vendor/marked.min.js +75 -3
  177. package/dist/core/extensions/index.d.ts +8 -8
  178. package/dist/core/extensions/index.d.ts.map +1 -1
  179. package/dist/core/extensions/index.js.map +1 -1
  180. package/dist/core/extensions/loader.d.ts +5 -7
  181. package/dist/core/extensions/loader.d.ts.map +1 -1
  182. package/dist/core/extensions/loader.js +160 -87
  183. package/dist/core/extensions/loader.js.map +1 -1
  184. package/dist/core/extensions/runner.d.ts +32 -13
  185. package/dist/core/extensions/runner.d.ts.map +1 -1
  186. package/dist/core/extensions/runner.js +192 -4
  187. package/dist/core/extensions/runner.js.map +1 -1
  188. package/dist/core/extensions/types.d.ts +217 -69
  189. package/dist/core/extensions/types.d.ts.map +1 -1
  190. package/dist/core/extensions/types.js.map +1 -1
  191. package/dist/core/extensions/wrapper.d.ts +3 -3
  192. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  193. package/dist/core/extensions/wrapper.js +22 -3
  194. package/dist/core/extensions/wrapper.js.map +1 -1
  195. package/dist/core/footer-data-provider.d.ts +12 -0
  196. package/dist/core/footer-data-provider.d.ts.map +1 -1
  197. package/dist/core/footer-data-provider.js +30 -2
  198. package/dist/core/footer-data-provider.js.map +1 -1
  199. package/dist/core/http-dispatcher.d.ts +22 -0
  200. package/dist/core/http-dispatcher.d.ts.map +1 -0
  201. package/dist/core/http-dispatcher.js +96 -0
  202. package/dist/core/http-dispatcher.js.map +1 -0
  203. package/dist/core/index.d.ts +9 -8
  204. package/dist/core/index.d.ts.map +1 -1
  205. package/dist/core/index.js +1 -0
  206. package/dist/core/index.js.map +1 -1
  207. package/dist/core/keybindings.d.ts +79 -10
  208. package/dist/core/keybindings.d.ts.map +1 -1
  209. package/dist/core/keybindings.js +11 -10
  210. package/dist/core/keybindings.js.map +1 -1
  211. package/dist/core/messages.d.ts +2 -2
  212. package/dist/core/messages.d.ts.map +1 -1
  213. package/dist/core/messages.js.map +1 -1
  214. package/dist/core/model-config.d.ts +571 -0
  215. package/dist/core/model-config.d.ts.map +1 -0
  216. package/dist/core/model-config.js +254 -0
  217. package/dist/core/model-config.js.map +1 -0
  218. package/dist/core/model-registry.d.ts +23 -110
  219. package/dist/core/model-registry.d.ts.map +1 -1
  220. package/dist/core/model-registry.js +65 -616
  221. package/dist/core/model-registry.js.map +1 -1
  222. package/dist/core/model-resolver.d.ts +20 -7
  223. package/dist/core/model-resolver.d.ts.map +1 -1
  224. package/dist/core/model-resolver.js +139 -43
  225. package/dist/core/model-resolver.js.map +1 -1
  226. package/dist/core/model-runtime.d.ts +100 -0
  227. package/dist/core/model-runtime.d.ts.map +1 -0
  228. package/dist/core/model-runtime.js +602 -0
  229. package/dist/core/model-runtime.js.map +1 -0
  230. package/dist/core/models-store.d.ts +22 -0
  231. package/dist/core/models-store.d.ts.map +1 -0
  232. package/dist/core/models-store.js +113 -0
  233. package/dist/core/models-store.js.map +1 -0
  234. package/dist/core/output-guard.d.ts +1 -0
  235. package/dist/core/output-guard.d.ts.map +1 -1
  236. package/dist/core/output-guard.js +52 -22
  237. package/dist/core/output-guard.js.map +1 -1
  238. package/dist/core/package-manager.d.ts +21 -4
  239. package/dist/core/package-manager.d.ts.map +1 -1
  240. package/dist/core/package-manager.js +428 -191
  241. package/dist/core/package-manager.js.map +1 -1
  242. package/dist/core/pi-manifest.d.ts +8 -0
  243. package/dist/core/pi-manifest.d.ts.map +1 -0
  244. package/dist/core/pi-manifest.js +25 -0
  245. package/dist/core/pi-manifest.js.map +1 -0
  246. package/dist/core/project-trust.d.ts +15 -0
  247. package/dist/core/project-trust.d.ts.map +1 -0
  248. package/dist/core/project-trust.js +59 -0
  249. package/dist/core/project-trust.js.map +1 -0
  250. package/dist/core/prompt-templates.d.ts +4 -2
  251. package/dist/core/prompt-templates.d.ts.map +1 -1
  252. package/dist/core/prompt-templates.js +36 -50
  253. package/dist/core/prompt-templates.js.map +1 -1
  254. package/dist/core/provider-attribution.d.ts +4 -0
  255. package/dist/core/provider-attribution.d.ts.map +1 -0
  256. package/dist/core/provider-attribution.js +72 -0
  257. package/dist/core/provider-attribution.js.map +1 -0
  258. package/dist/core/provider-composer.d.ts +58 -0
  259. package/dist/core/provider-composer.d.ts.map +1 -0
  260. package/dist/core/provider-composer.js +396 -0
  261. package/dist/core/provider-composer.js.map +1 -0
  262. package/dist/core/radius.d.ts +2 -0
  263. package/dist/core/radius.d.ts.map +1 -0
  264. package/dist/core/radius.js +2 -0
  265. package/dist/core/radius.js.map +1 -0
  266. package/dist/core/remote-catalog-provider.d.ts +17 -0
  267. package/dist/core/remote-catalog-provider.d.ts.map +1 -0
  268. package/dist/core/remote-catalog-provider.js +137 -0
  269. package/dist/core/remote-catalog-provider.js.map +1 -0
  270. package/dist/core/resolve-config-value.d.ts +13 -6
  271. package/dist/core/resolve-config-value.d.ts.map +1 -1
  272. package/dist/core/resolve-config-value.js +143 -22
  273. package/dist/core/resolve-config-value.js.map +1 -1
  274. package/dist/core/resource-loader.d.ts +40 -12
  275. package/dist/core/resource-loader.d.ts.map +1 -1
  276. package/dist/core/resource-loader.js +236 -87
  277. package/dist/core/resource-loader.js.map +1 -1
  278. package/dist/core/runtime-credentials.d.ts +15 -0
  279. package/dist/core/runtime-credentials.d.ts.map +1 -0
  280. package/dist/core/runtime-credentials.js +39 -0
  281. package/dist/core/runtime-credentials.js.map +1 -0
  282. package/dist/core/sdk.d.ts +26 -25
  283. package/dist/core/sdk.d.ts.map +1 -1
  284. package/dist/core/sdk.js +56 -49
  285. package/dist/core/sdk.js.map +1 -1
  286. package/dist/core/session-manager.d.ts +42 -19
  287. package/dist/core/session-manager.d.ts.map +1 -1
  288. package/dist/core/session-manager.js +454 -223
  289. package/dist/core/session-manager.js.map +1 -1
  290. package/dist/core/settings-manager.d.ts +77 -8
  291. package/dist/core/settings-manager.d.ts.map +1 -1
  292. package/dist/core/settings-manager.js +255 -67
  293. package/dist/core/settings-manager.js.map +1 -1
  294. package/dist/core/skills.d.ts +2 -2
  295. package/dist/core/skills.d.ts.map +1 -1
  296. package/dist/core/skills.js +12 -34
  297. package/dist/core/skills.js.map +1 -1
  298. package/dist/core/slash-commands.d.ts +2 -1
  299. package/dist/core/slash-commands.d.ts.map +1 -1
  300. package/dist/core/slash-commands.js +4 -3
  301. package/dist/core/slash-commands.js.map +1 -1
  302. package/dist/core/source-info.d.ts +1 -1
  303. package/dist/core/source-info.d.ts.map +1 -1
  304. package/dist/core/source-info.js.map +1 -1
  305. package/dist/core/system-prompt.d.ts +1 -1
  306. package/dist/core/system-prompt.d.ts.map +1 -1
  307. package/dist/core/system-prompt.js +15 -25
  308. package/dist/core/system-prompt.js.map +1 -1
  309. package/dist/core/telemetry.d.ts +7 -1
  310. package/dist/core/telemetry.d.ts.map +1 -1
  311. package/dist/core/telemetry.js +7 -1
  312. package/dist/core/telemetry.js.map +1 -1
  313. package/dist/core/timings.d.ts +5 -3
  314. package/dist/core/timings.d.ts.map +1 -1
  315. package/dist/core/timings.js +26 -16
  316. package/dist/core/timings.js.map +1 -1
  317. package/dist/core/tools/bash.d.ts +11 -7
  318. package/dist/core/tools/bash.d.ts.map +1 -1
  319. package/dist/core/tools/bash.js +220 -188
  320. package/dist/core/tools/bash.js.map +1 -1
  321. package/dist/core/tools/edit-diff.d.ts +26 -5
  322. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  323. package/dist/core/tools/edit-diff.js +110 -23
  324. package/dist/core/tools/edit-diff.js.map +1 -1
  325. package/dist/core/tools/edit.d.ts +11 -5
  326. package/dist/core/tools/edit.d.ts.map +1 -1
  327. package/dist/core/tools/edit.js +67 -100
  328. package/dist/core/tools/edit.js.map +1 -1
  329. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  330. package/dist/core/tools/file-mutation-queue.js +27 -12
  331. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  332. package/dist/core/tools/find.d.ts +10 -3
  333. package/dist/core/tools/find.d.ts.map +1 -1
  334. package/dist/core/tools/find.js +41 -37
  335. package/dist/core/tools/find.js.map +1 -1
  336. package/dist/core/tools/grep.d.ts +7 -3
  337. package/dist/core/tools/grep.d.ts.map +1 -1
  338. package/dist/core/tools/grep.js +12 -8
  339. package/dist/core/tools/grep.js.map +1 -1
  340. package/dist/core/tools/index.d.ts +18 -18
  341. package/dist/core/tools/index.d.ts.map +1 -1
  342. package/dist/core/tools/index.js.map +1 -1
  343. package/dist/core/tools/ls.d.ts +7 -3
  344. package/dist/core/tools/ls.d.ts.map +1 -1
  345. package/dist/core/tools/ls.js +17 -15
  346. package/dist/core/tools/ls.js.map +1 -1
  347. package/dist/core/tools/output-accumulator.d.ts +52 -0
  348. package/dist/core/tools/output-accumulator.d.ts.map +1 -0
  349. package/dist/core/tools/output-accumulator.js +184 -0
  350. package/dist/core/tools/output-accumulator.js.map +1 -0
  351. package/dist/core/tools/path-utils.d.ts +2 -0
  352. package/dist/core/tools/path-utils.d.ts.map +1 -1
  353. package/dist/core/tools/path-utils.js +39 -21
  354. package/dist/core/tools/path-utils.js.map +1 -1
  355. package/dist/core/tools/read.d.ts +7 -3
  356. package/dist/core/tools/read.d.ts.map +1 -1
  357. package/dist/core/tools/read.js +101 -51
  358. package/dist/core/tools/read.js.map +1 -1
  359. package/dist/core/tools/render-utils.d.ts +5 -2
  360. package/dist/core/tools/render-utils.d.ts.map +1 -1
  361. package/dist/core/tools/render-utils.js +18 -2
  362. package/dist/core/tools/render-utils.js.map +1 -1
  363. package/dist/core/tools/tool-definition-wrapper.d.ts +2 -2
  364. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  365. package/dist/core/tools/tool-definition-wrapper.js +3 -3
  366. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  367. package/dist/core/tools/truncate.d.ts.map +1 -1
  368. package/dist/core/tools/truncate.js +12 -2
  369. package/dist/core/tools/truncate.js.map +1 -1
  370. package/dist/core/tools/write.d.ts +6 -2
  371. package/dist/core/tools/write.d.ts.map +1 -1
  372. package/dist/core/tools/write.js +35 -45
  373. package/dist/core/tools/write.js.map +1 -1
  374. package/dist/core/trust-manager.d.ts +36 -0
  375. package/dist/core/trust-manager.d.ts.map +1 -0
  376. package/dist/core/trust-manager.js +202 -0
  377. package/dist/core/trust-manager.js.map +1 -0
  378. package/dist/core/usage-totals.d.ts +19 -0
  379. package/dist/core/usage-totals.d.ts.map +1 -0
  380. package/dist/core/usage-totals.js +52 -0
  381. package/dist/core/usage-totals.js.map +1 -0
  382. package/dist/extensions/index.d.ts +3 -0
  383. package/dist/extensions/index.d.ts.map +1 -0
  384. package/dist/extensions/index.js +3 -0
  385. package/dist/extensions/index.js.map +1 -0
  386. package/dist/extensions/llama/client.d.ts +61 -0
  387. package/dist/extensions/llama/client.d.ts.map +1 -0
  388. package/dist/extensions/llama/client.js +302 -0
  389. package/dist/extensions/llama/client.js.map +1 -0
  390. package/dist/extensions/llama/huggingface.d.ts +23 -0
  391. package/dist/extensions/llama/huggingface.d.ts.map +1 -0
  392. package/dist/extensions/llama/huggingface.js +141 -0
  393. package/dist/extensions/llama/huggingface.js.map +1 -0
  394. package/dist/extensions/llama/index.d.ts +3 -0
  395. package/dist/extensions/llama/index.d.ts.map +1 -0
  396. package/dist/extensions/llama/index.js +217 -0
  397. package/dist/extensions/llama/index.js.map +1 -0
  398. package/dist/extensions/llama/provider.d.ts +10 -0
  399. package/dist/extensions/llama/provider.d.ts.map +1 -0
  400. package/dist/extensions/llama/provider.js +121 -0
  401. package/dist/extensions/llama/provider.js.map +1 -0
  402. package/dist/extensions/llama/ui.d.ts +42 -0
  403. package/dist/extensions/llama/ui.d.ts.map +1 -0
  404. package/dist/extensions/llama/ui.js +416 -0
  405. package/dist/extensions/llama/ui.js.map +1 -0
  406. package/dist/index.d.ts +34 -27
  407. package/dist/index.d.ts.map +1 -1
  408. package/dist/index.js +11 -5
  409. package/dist/index.js.map +1 -1
  410. package/dist/main.d.ts +2 -2
  411. package/dist/main.d.ts.map +1 -1
  412. package/dist/main.js +311 -101
  413. package/dist/main.js.map +1 -1
  414. package/dist/migrations.d.ts +5 -5
  415. package/dist/migrations.d.ts.map +1 -1
  416. package/dist/migrations.js +10 -10
  417. package/dist/migrations.js.map +1 -1
  418. package/dist/modes/index.d.ts +6 -5
  419. package/dist/modes/index.d.ts.map +1 -1
  420. package/dist/modes/index.js.map +1 -1
  421. package/dist/modes/interactive/components/armin.d.ts +1 -1
  422. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  423. package/dist/modes/interactive/components/armin.js.map +1 -1
  424. package/dist/modes/interactive/components/assistant-message.d.ts +8 -3
  425. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  426. package/dist/modes/interactive/components/assistant-message.js +55 -26
  427. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  428. package/dist/modes/interactive/components/bash-execution.d.ts +2 -2
  429. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  430. package/dist/modes/interactive/components/bash-execution.js +4 -4
  431. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  432. package/dist/modes/interactive/components/bordered-loader.d.ts +2 -2
  433. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  434. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  435. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  436. package/dist/modes/interactive/components/branch-summary-message.d.ts +2 -2
  437. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  438. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  439. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  440. package/dist/modes/interactive/components/compaction-summary-message.d.ts +2 -2
  441. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  442. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  443. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  444. package/dist/modes/interactive/components/config-selector.d.ts +37 -6
  445. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  446. package/dist/modes/interactive/components/config-selector.js +318 -34
  447. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  448. package/dist/modes/interactive/components/countdown-timer.d.ts +3 -3
  449. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
  450. package/dist/modes/interactive/components/countdown-timer.js +2 -2
  451. package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  452. package/dist/modes/interactive/components/custom-editor.d.ts +2 -2
  453. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  454. package/dist/modes/interactive/components/custom-editor.js +9 -2
  455. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  456. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  457. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  458. package/dist/modes/interactive/components/custom-entry.js +52 -0
  459. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  460. package/dist/modes/interactive/components/custom-message.d.ts +6 -4
  461. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  462. package/dist/modes/interactive/components/custom-message.js +11 -3
  463. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  464. package/dist/modes/interactive/components/daxnuts.d.ts +1 -1
  465. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  466. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  467. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  468. package/dist/modes/interactive/components/diff.js.map +1 -1
  469. package/dist/modes/interactive/components/dynamic-border.d.ts +1 -1
  470. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  471. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  472. package/dist/modes/interactive/components/extension-editor.d.ts +5 -4
  473. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  474. package/dist/modes/interactive/components/extension-editor.js +19 -32
  475. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  476. package/dist/modes/interactive/components/extension-input.d.ts +1 -1
  477. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  478. package/dist/modes/interactive/components/extension-input.js +1 -1
  479. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  480. package/dist/modes/interactive/components/extension-selector.d.ts +3 -1
  481. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  482. package/dist/modes/interactive/components/extension-selector.js +7 -2
  483. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  484. package/dist/modes/interactive/components/first-time-setup.d.ts +25 -0
  485. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -0
  486. package/dist/modes/interactive/components/first-time-setup.js +103 -0
  487. package/dist/modes/interactive/components/first-time-setup.js.map +1 -0
  488. package/dist/modes/interactive/components/footer.d.ts +9 -4
  489. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  490. package/dist/modes/interactive/components/footer.js +52 -31
  491. package/dist/modes/interactive/components/footer.js.map +1 -1
  492. package/dist/modes/interactive/components/index.d.ts +33 -31
  493. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  494. package/dist/modes/interactive/components/index.js +2 -0
  495. package/dist/modes/interactive/components/index.js.map +1 -1
  496. package/dist/modes/interactive/components/keybinding-hints.d.ts +6 -1
  497. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  498. package/dist/modes/interactive/components/keybinding-hints.js +20 -6
  499. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  500. package/dist/modes/interactive/components/login-dialog.d.ts +12 -6
  501. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  502. package/dist/modes/interactive/components/login-dialog.js +46 -17
  503. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  504. package/dist/modes/interactive/components/markdown-transform.d.ts +3 -0
  505. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -0
  506. package/dist/modes/interactive/components/markdown-transform.js +19 -0
  507. package/dist/modes/interactive/components/markdown-transform.js.map +1 -0
  508. package/dist/modes/interactive/components/mermaid.d.ts +11 -0
  509. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  510. package/dist/modes/interactive/components/mermaid.js +75 -0
  511. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  512. package/dist/modes/interactive/components/model-selector.d.ts +13 -6
  513. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  514. package/dist/modes/interactive/components/model-selector.js +92 -44
  515. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  516. package/dist/modes/interactive/components/oauth-selector.d.ts +7 -4
  517. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  518. package/dist/modes/interactive/components/oauth-selector.js +32 -28
  519. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  520. package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -1
  521. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  522. package/dist/modes/interactive/components/scoped-models-selector.js +52 -14
  523. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  524. package/dist/modes/interactive/components/session-selector-search.d.ts +1 -1
  525. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  526. package/dist/modes/interactive/components/session-selector-search.js +1 -1
  527. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  528. package/dist/modes/interactive/components/session-selector.d.ts +4 -5
  529. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  530. package/dist/modes/interactive/components/session-selector.js +18 -16
  531. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  532. package/dist/modes/interactive/components/settings-selector.d.ts +23 -2
  533. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  534. package/dist/modes/interactive/components/settings-selector.js +322 -22
  535. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  536. package/dist/modes/interactive/components/show-images-selector.d.ts +1 -1
  537. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  538. package/dist/modes/interactive/components/show-images-selector.js +1 -1
  539. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  540. package/dist/modes/interactive/components/skill-invocation-message.d.ts +2 -2
  541. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  542. package/dist/modes/interactive/components/skill-invocation-message.js +1 -1
  543. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  544. package/dist/modes/interactive/components/status-indicator.d.ts +28 -0
  545. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -0
  546. package/dist/modes/interactive/components/status-indicator.js +60 -0
  547. package/dist/modes/interactive/components/status-indicator.js.map +1 -0
  548. package/dist/modes/interactive/components/theme-selector.d.ts +1 -1
  549. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  550. package/dist/modes/interactive/components/theme-selector.js +1 -1
  551. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  552. package/dist/modes/interactive/components/thinking-selector.d.ts +2 -2
  553. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  554. package/dist/modes/interactive/components/thinking-selector.js +3 -2
  555. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  556. package/dist/modes/interactive/components/tool-execution.d.ts +2 -2
  557. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  558. package/dist/modes/interactive/components/tool-execution.js +33 -2
  559. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  560. package/dist/modes/interactive/components/tree-selector.d.ts +7 -2
  561. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  562. package/dist/modes/interactive/components/tree-selector.js +179 -26
  563. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  564. package/dist/modes/interactive/components/trust-selector.d.ts +23 -0
  565. package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -0
  566. package/dist/modes/interactive/components/trust-selector.js +91 -0
  567. package/dist/modes/interactive/components/trust-selector.js.map +1 -0
  568. package/dist/modes/interactive/components/user-message-selector.d.ts +1 -1
  569. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  570. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  571. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  572. package/dist/modes/interactive/components/user-message.d.ts +9 -3
  573. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  574. package/dist/modes/interactive/components/user-message.js +26 -6
  575. package/dist/modes/interactive/components/user-message.js.map +1 -1
  576. package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
  577. package/dist/modes/interactive/components/visual-truncate.js +1 -1
  578. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  579. package/dist/modes/interactive/external-editor.d.ts +12 -0
  580. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  581. package/dist/modes/interactive/external-editor.js +37 -0
  582. package/dist/modes/interactive/external-editor.js.map +1 -0
  583. package/dist/modes/interactive/interactive-mode.d.ts +104 -26
  584. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  585. package/dist/modes/interactive/interactive-mode.js +1565 -692
  586. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  587. package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
  588. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  589. package/dist/modes/interactive/model-catalog-refresh.js +34 -0
  590. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  591. package/dist/modes/interactive/model-search.d.ts +12 -0
  592. package/dist/modes/interactive/model-search.d.ts.map +1 -0
  593. package/dist/modes/interactive/model-search.js +15 -0
  594. package/dist/modes/interactive/model-search.js.map +1 -0
  595. package/dist/modes/interactive/theme/dark.json +10 -5
  596. package/dist/modes/interactive/theme/light.json +10 -5
  597. package/dist/modes/interactive/theme/theme-controller.d.ts +40 -0
  598. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -0
  599. package/dist/modes/interactive/theme/theme-controller.js +125 -0
  600. package/dist/modes/interactive/theme/theme-controller.js.map +1 -0
  601. package/dist/modes/interactive/theme/theme-schema.json +22 -5
  602. package/dist/modes/interactive/theme/theme.d.ts +48 -7
  603. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  604. package/dist/modes/interactive/theme/theme.js +194 -76
  605. package/dist/modes/interactive/theme/theme.js.map +1 -1
  606. package/dist/modes/json-event.d.ts +31 -0
  607. package/dist/modes/json-event.d.ts.map +1 -0
  608. package/dist/modes/json-event.js +15 -0
  609. package/dist/modes/json-event.js.map +1 -0
  610. package/dist/modes/print-mode.d.ts +3 -3
  611. package/dist/modes/print-mode.d.ts.map +1 -1
  612. package/dist/modes/print-mode.js +16 -5
  613. package/dist/modes/print-mode.js.map +1 -1
  614. package/dist/modes/rpc/rpc-client.d.ts +33 -10
  615. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  616. package/dist/modes/rpc/rpc-client.js +89 -11
  617. package/dist/modes/rpc/rpc-client.js.map +1 -1
  618. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  619. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  620. package/dist/modes/rpc/rpc-mode.js +69 -9
  621. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  622. package/dist/modes/rpc/rpc-types.d.ts +43 -5
  623. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  624. package/dist/modes/rpc/rpc-types.js.map +1 -1
  625. package/dist/package-manager-cli.d.ts +6 -2
  626. package/dist/package-manager-cli.d.ts.map +1 -1
  627. package/dist/package-manager-cli.js +527 -25
  628. package/dist/package-manager-cli.js.map +1 -1
  629. package/dist/path-migration.d.ts +13 -0
  630. package/dist/path-migration.d.ts.map +1 -0
  631. package/dist/path-migration.js +52 -0
  632. package/dist/path-migration.js.map +1 -0
  633. package/dist/rpc-entry.d.ts +3 -0
  634. package/dist/rpc-entry.d.ts.map +1 -0
  635. package/dist/rpc-entry.js +11 -0
  636. package/dist/rpc-entry.js.map +1 -0
  637. package/dist/server/create-harness.d.ts +26 -0
  638. package/dist/server/create-harness.d.ts.map +1 -0
  639. package/dist/server/create-harness.js +106 -0
  640. package/dist/server/create-harness.js.map +1 -0
  641. package/dist/utils/abort.d.ts +5 -0
  642. package/dist/utils/abort.d.ts.map +1 -0
  643. package/dist/utils/abort.js +48 -0
  644. package/dist/utils/abort.js.map +1 -0
  645. package/dist/utils/ansi.d.ts +2 -0
  646. package/dist/utils/ansi.d.ts.map +1 -0
  647. package/dist/utils/ansi.js +52 -0
  648. package/dist/utils/ansi.js.map +1 -0
  649. package/dist/utils/changelog.d.ts +2 -1
  650. package/dist/utils/changelog.d.ts.map +1 -1
  651. package/dist/utils/changelog.js +85 -0
  652. package/dist/utils/changelog.js.map +1 -1
  653. package/dist/utils/child-process.d.ts +12 -5
  654. package/dist/utils/child-process.d.ts.map +1 -1
  655. package/dist/utils/child-process.js +33 -5
  656. package/dist/utils/child-process.js.map +1 -1
  657. package/dist/utils/clipboard-image.d.ts.map +1 -1
  658. package/dist/utils/clipboard-image.js +4 -4
  659. package/dist/utils/clipboard-image.js.map +1 -1
  660. package/dist/utils/clipboard-native.d.ts +4 -1
  661. package/dist/utils/clipboard-native.d.ts.map +1 -1
  662. package/dist/utils/clipboard-native.js +14 -8
  663. package/dist/utils/clipboard-native.js.map +1 -1
  664. package/dist/utils/clipboard.d.ts +2 -0
  665. package/dist/utils/clipboard.d.ts.map +1 -1
  666. package/dist/utils/clipboard.js +129 -46
  667. package/dist/utils/clipboard.js.map +1 -1
  668. package/dist/utils/deprecation.d.ts +4 -0
  669. package/dist/utils/deprecation.d.ts.map +1 -0
  670. package/dist/utils/deprecation.js +13 -0
  671. package/dist/utils/deprecation.js.map +1 -0
  672. package/dist/utils/exif-orientation.d.ts +1 -1
  673. package/dist/utils/exif-orientation.d.ts.map +1 -1
  674. package/dist/utils/exif-orientation.js.map +1 -1
  675. package/dist/utils/git.d.ts.map +1 -1
  676. package/dist/utils/git.js +54 -22
  677. package/dist/utils/git.js.map +1 -1
  678. package/dist/utils/html.d.ts +7 -0
  679. package/dist/utils/html.d.ts.map +1 -0
  680. package/dist/utils/html.js +40 -0
  681. package/dist/utils/html.js.map +1 -0
  682. package/dist/utils/image-convert.d.ts +1 -0
  683. package/dist/utils/image-convert.d.ts.map +1 -1
  684. package/dist/utils/image-convert.js +21 -15
  685. package/dist/utils/image-convert.js.map +1 -1
  686. package/dist/utils/image-process.d.ts +18 -0
  687. package/dist/utils/image-process.d.ts.map +1 -0
  688. package/dist/utils/image-process.js +83 -0
  689. package/dist/utils/image-process.js.map +1 -0
  690. package/dist/utils/image-resize-core.d.ts +30 -0
  691. package/dist/utils/image-resize-core.d.ts.map +1 -0
  692. package/dist/utils/image-resize-core.js +124 -0
  693. package/dist/utils/image-resize-core.js.map +1 -0
  694. package/dist/utils/image-resize-worker.d.ts +2 -0
  695. package/dist/utils/image-resize-worker.d.ts.map +1 -0
  696. package/dist/utils/image-resize-worker.js +31 -0
  697. package/dist/utils/image-resize-worker.js.map +1 -0
  698. package/dist/utils/image-resize.d.ts +7 -27
  699. package/dist/utils/image-resize.d.ts.map +1 -1
  700. package/dist/utils/image-resize.js +75 -115
  701. package/dist/utils/image-resize.js.map +1 -1
  702. package/dist/utils/json.d.ts +3 -0
  703. package/dist/utils/json.d.ts.map +1 -0
  704. package/dist/utils/json.js +7 -0
  705. package/dist/utils/json.js.map +1 -0
  706. package/dist/utils/management-http.d.ts +23 -0
  707. package/dist/utils/management-http.d.ts.map +1 -0
  708. package/dist/utils/management-http.js +50 -0
  709. package/dist/utils/management-http.js.map +1 -0
  710. package/dist/utils/mime.d.ts +1 -0
  711. package/dist/utils/mime.d.ts.map +1 -1
  712. package/dist/utils/mime.js +100 -16
  713. package/dist/utils/mime.js.map +1 -1
  714. package/dist/utils/open-browser.d.ts +9 -0
  715. package/dist/utils/open-browser.d.ts.map +1 -0
  716. package/dist/utils/open-browser.js +22 -0
  717. package/dist/utils/open-browser.js.map +1 -0
  718. package/dist/utils/paths.d.ts +29 -1
  719. package/dist/utils/paths.d.ts.map +1 -1
  720. package/dist/utils/paths.js +102 -2
  721. package/dist/utils/paths.js.map +1 -1
  722. package/dist/utils/pi-user-agent.d.ts +2 -0
  723. package/dist/utils/pi-user-agent.d.ts.map +1 -0
  724. package/dist/utils/pi-user-agent.js +5 -0
  725. package/dist/utils/pi-user-agent.js.map +1 -0
  726. package/dist/utils/shell.d.ts +1 -0
  727. package/dist/utils/shell.d.ts.map +1 -1
  728. package/dist/utils/shell.js +18 -6
  729. package/dist/utils/shell.js.map +1 -1
  730. package/dist/utils/syntax-highlight.d.ts +12 -0
  731. package/dist/utils/syntax-highlight.d.ts.map +1 -0
  732. package/dist/utils/syntax-highlight.js +118 -0
  733. package/dist/utils/syntax-highlight.js.map +1 -0
  734. package/dist/utils/tool-result-images.d.ts +19 -0
  735. package/dist/utils/tool-result-images.d.ts.map +1 -0
  736. package/dist/utils/tool-result-images.js +45 -0
  737. package/dist/utils/tool-result-images.js.map +1 -0
  738. package/dist/utils/tools-manager.d.ts +10 -1
  739. package/dist/utils/tools-manager.d.ts.map +1 -1
  740. package/dist/utils/tools-manager.js +105 -36
  741. package/dist/utils/tools-manager.js.map +1 -1
  742. package/dist/utils/version-check.d.ts +19 -0
  743. package/dist/utils/version-check.d.ts.map +1 -0
  744. package/dist/utils/version-check.js +85 -0
  745. package/dist/utils/version-check.js.map +1 -0
  746. package/dist/utils/windows-self-update.d.ts +3 -0
  747. package/dist/utils/windows-self-update.d.ts.map +1 -0
  748. package/dist/utils/windows-self-update.js +77 -0
  749. package/dist/utils/windows-self-update.js.map +1 -0
  750. package/docs/compaction.md +39 -32
  751. package/docs/containerization.md +113 -0
  752. package/docs/custom-provider.md +212 -75
  753. package/docs/development.md +9 -9
  754. package/docs/docs.json +156 -0
  755. package/docs/environment-variables.md +186 -0
  756. package/docs/extensions.md +652 -140
  757. package/docs/index.md +81 -0
  758. package/docs/json.md +26 -15
  759. package/docs/keybindings.md +60 -21
  760. package/docs/llama-cpp.md +99 -0
  761. package/docs/models.md +168 -34
  762. package/docs/packages.md +46 -36
  763. package/docs/prompt-templates.md +13 -5
  764. package/docs/providers.md +147 -26
  765. package/docs/quickstart.md +167 -0
  766. package/docs/rpc.md +218 -36
  767. package/docs/sdk.md +190 -125
  768. package/docs/security.md +59 -0
  769. package/docs/{session.md → session-format.md} +48 -22
  770. package/docs/sessions.md +145 -0
  771. package/docs/settings.md +130 -25
  772. package/docs/shell-aliases.md +2 -2
  773. package/docs/skills.md +13 -14
  774. package/docs/terminal-setup.md +73 -16
  775. package/docs/termux.md +10 -10
  776. package/docs/themes.md +41 -16
  777. package/docs/tmux.md +6 -4
  778. package/docs/tui.md +85 -61
  779. package/docs/usage.md +302 -0
  780. package/docs/windows.md +2 -2
  781. package/examples/README.md +1 -1
  782. package/examples/extensions/README.md +12 -9
  783. package/examples/extensions/bash-spawn-hook.ts +2 -2
  784. package/examples/extensions/border-status-editor.ts +145 -0
  785. package/examples/extensions/built-in-tool-renderer.ts +2 -2
  786. package/examples/extensions/claude-rules.ts +1 -1
  787. package/examples/extensions/commands.ts +1 -1
  788. package/examples/extensions/custom-compaction.ts +6 -16
  789. package/examples/extensions/custom-footer.ts +1 -1
  790. package/examples/extensions/custom-header.ts +7 -7
  791. package/examples/extensions/custom-provider-anthropic/index.ts +15 -8
  792. package/examples/extensions/custom-provider-anthropic/package-lock.json +4 -4
  793. package/examples/extensions/custom-provider-anthropic/package.json +3 -3
  794. package/examples/extensions/custom-provider-gitlab-duo/index.ts +65 -9
  795. package/examples/extensions/custom-provider-gitlab-duo/package.json +2 -2
  796. package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
  797. package/examples/extensions/doom-overlay/README.md +1 -1
  798. package/examples/extensions/doom-overlay/doom-component.ts +4 -4
  799. package/examples/extensions/doom-overlay/doom-keys.ts +1 -1
  800. package/examples/extensions/doom-overlay/index.ts +5 -5
  801. package/examples/extensions/doom-overlay/wad-finder.ts +7 -3
  802. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  803. package/examples/extensions/entry-renderer.ts +41 -0
  804. package/examples/extensions/git-merge-and-resolve.ts +115 -0
  805. package/examples/extensions/github-issue-autocomplete.ts +2 -2
  806. package/examples/extensions/gondolin/index.ts +531 -0
  807. package/examples/extensions/gondolin/package-lock.json +185 -0
  808. package/examples/extensions/gondolin/package.json +19 -0
  809. package/examples/extensions/handoff.ts +50 -14
  810. package/examples/extensions/hidden-thinking-label.ts +1 -1
  811. package/examples/extensions/inline-bash.ts +2 -2
  812. package/examples/extensions/input-transform-streaming.ts +39 -0
  813. package/examples/extensions/input-transform.ts +2 -2
  814. package/examples/extensions/interactive-shell.ts +2 -2
  815. package/examples/extensions/kimi-deferred-tools.ts +61 -0
  816. package/examples/extensions/mac-system-theme.ts +2 -2
  817. package/examples/extensions/message-renderer.ts +3 -3
  818. package/examples/extensions/minimal-mode.ts +2 -2
  819. package/examples/extensions/modal-editor.ts +2 -2
  820. package/examples/extensions/model-status.ts +1 -1
  821. package/examples/extensions/notify.ts +3 -3
  822. package/examples/extensions/overlay-qa-tests.ts +242 -140
  823. package/examples/extensions/overlay-test.ts +9 -6
  824. package/examples/extensions/pirate.ts +1 -1
  825. package/examples/extensions/plan-mode/README.md +3 -2
  826. package/examples/extensions/plan-mode/index.ts +90 -40
  827. package/examples/extensions/preset.ts +16 -10
  828. package/examples/extensions/project-trust.ts +64 -0
  829. package/examples/extensions/prompt-customizer.ts +1 -1
  830. package/examples/extensions/provider-payload.ts +5 -5
  831. package/examples/extensions/qna.ts +4 -8
  832. package/examples/extensions/question.ts +41 -19
  833. package/examples/extensions/questionnaire.ts +50 -29
  834. package/examples/extensions/rainbow-editor.ts +1 -1
  835. package/examples/extensions/reload-runtime.ts +2 -2
  836. package/examples/extensions/rpc-demo.ts +1 -1
  837. package/examples/extensions/sandbox/index.ts +9 -9
  838. package/examples/extensions/sandbox/package-lock.json +7 -7
  839. package/examples/extensions/sandbox/package.json +3 -3
  840. package/examples/extensions/shutdown-command.ts +5 -5
  841. package/examples/extensions/snake.ts +2 -2
  842. package/examples/extensions/space-invaders.ts +2 -2
  843. package/examples/extensions/ssh.ts +2 -2
  844. package/examples/extensions/structured-output.ts +1 -1
  845. package/examples/extensions/subagent/README.md +18 -13
  846. package/examples/extensions/subagent/agents.ts +42 -11
  847. package/examples/extensions/subagent/index.ts +76 -30
  848. package/examples/extensions/summarize.ts +12 -19
  849. package/examples/extensions/tic-tac-toe.ts +2 -2
  850. package/examples/extensions/titlebar-spinner.ts +3 -3
  851. package/examples/extensions/todo.ts +2 -2
  852. package/examples/extensions/tool-override.ts +1 -1
  853. package/examples/extensions/tools.ts +7 -2
  854. package/examples/extensions/truncated-tool.ts +2 -2
  855. package/examples/extensions/with-deps/package-lock.json +4 -4
  856. package/examples/extensions/with-deps/package.json +4 -4
  857. package/examples/extensions/working-indicator.ts +4 -4
  858. package/examples/extensions/working-message-test.ts +1 -1
  859. package/examples/rpc-extension-ui.ts +11 -2
  860. package/examples/sdk/01-minimal.ts +15 -11
  861. package/examples/sdk/02-custom-model.ts +18 -18
  862. package/examples/sdk/03-custom-prompt.ts +25 -17
  863. package/examples/sdk/04-skills.ts +3 -3
  864. package/examples/sdk/05-tools.ts +8 -4
  865. package/examples/sdk/06-extensions.ts +13 -9
  866. package/examples/sdk/07-context-files.ts +2 -2
  867. package/examples/sdk/08-prompt-templates.ts +3 -3
  868. package/examples/sdk/09-api-keys-and-oauth.ts +19 -33
  869. package/examples/sdk/10-settings.ts +4 -4
  870. package/examples/sdk/11-sessions.ts +4 -0
  871. package/examples/sdk/12-full-control.ts +22 -21
  872. package/examples/sdk/README.md +23 -30
  873. package/npm-shrinkwrap.json +1818 -0
  874. package/package.json +117 -99
  875. package/dist/modes/interactive/assets/clankolas.png +0 -0
  876. package/dist/modes/interactive/components/auth-selector-status.d.ts +0 -20
  877. package/dist/modes/interactive/components/auth-selector-status.d.ts.map +0 -1
  878. package/dist/modes/interactive/components/auth-selector-status.js +0 -23
  879. package/dist/modes/interactive/components/auth-selector-status.js.map +0 -1
  880. package/dist/modes/interactive/components/earendil-announcement.d.ts +0 -5
  881. package/dist/modes/interactive/components/earendil-announcement.d.ts.map +0 -1
  882. package/dist/modes/interactive/components/earendil-announcement.js +0 -40
  883. package/dist/modes/interactive/components/earendil-announcement.js.map +0 -1
  884. package/docs/images/exy.png +0 -0
  885. package/docs/tree.md +0 -233
  886. package/examples/extensions/antigravity-image-gen.ts +0 -418
  887. package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -345
  888. package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
@@ -6,56 +6,70 @@ 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, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
10
- import { getProviders, } from "@sammorrowdrums/mcpi-ai";
9
+ import * as TuiLayouts from "@sammorrowdrums/mcpi-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@sammorrowdrums/mcpi-tui";
11
+ import chalk from "chalk";
11
12
  import { spawn, spawnSync } from "child_process";
12
- import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
13
+ import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, ENV_SHARE_VIEWER_URL, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, getStateDir, VERSION, } from "../../config.js";
13
14
  import { parseSkillBlock } from "../../core/agent-session.js";
14
15
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
16
+ import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
15
17
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
18
+ import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
16
19
  import { KeybindingsManager } from "../../core/keybindings.js";
17
20
  import { createCompactionSummaryMessage } from "../../core/messages.js";
18
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
21
+ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScopeFromModels, } from "../../core/model-resolver.js";
22
+ import { CredentialSynchronizationError } from "../../core/model-runtime.js";
19
23
  import { DefaultPackageManager } from "../../core/package-manager.js";
20
24
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
21
- import { SessionManager } from "../../core/session-manager.js";
25
+ import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
22
26
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
23
- import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
24
- import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
25
- import { copyToClipboard } from "../../utils/clipboard.js";
27
+ import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
28
+ import { getUsageCostBreakdown } from "../../core/usage-totals.js";
29
+ import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
30
+ import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
26
31
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
27
32
  import { parseGitUrl } from "../../utils/git.js";
33
+ import { openBrowser } from "../../utils/open-browser.js";
34
+ import { getCwdRelativePath } from "../../utils/paths.js";
28
35
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
29
36
  import { ensureTool } from "../../utils/tools-manager.js";
37
+ import { checkForNewPiVersion } from "../../utils/version-check.js";
30
38
  import { ArminComponent } from "./components/armin.js";
31
39
  import { AssistantMessageComponent } from "./components/assistant-message.js";
32
40
  import { BashExecutionComponent } from "./components/bash-execution.js";
33
41
  import { BorderedLoader } from "./components/bordered-loader.js";
34
42
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
35
43
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
36
- import { CountdownTimer } from "./components/countdown-timer.js";
37
44
  import { CustomEditor } from "./components/custom-editor.js";
45
+ import { CustomEntryComponent } from "./components/custom-entry.js";
38
46
  import { CustomMessageComponent } from "./components/custom-message.js";
39
47
  import { DaxnutsComponent } from "./components/daxnuts.js";
40
48
  import { DynamicBorder } from "./components/dynamic-border.js";
41
- import { EarendilAnnouncementComponent } from "./components/earendil-announcement.js";
42
49
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
43
50
  import { ExtensionInputComponent } from "./components/extension-input.js";
44
51
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
45
- import { FooterComponent } from "./components/footer.js";
46
- import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
52
+ import { FooterComponent, formatTokens } from "./components/footer.js";
53
+ import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
47
54
  import { LoginDialogComponent } from "./components/login-dialog.js";
55
+ import { createMermaidMarkdownTransformer } from "./components/mermaid.js";
48
56
  import { ModelSelectorComponent } from "./components/model-selector.js";
49
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
57
+ import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
50
58
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
51
59
  import { SessionSelectorComponent } from "./components/session-selector.js";
52
60
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
53
61
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
62
+ import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.js";
54
63
  import { ToolExecutionComponent } from "./components/tool-execution.js";
55
64
  import { TreeSelectorComponent } from "./components/tree-selector.js";
65
+ import { TrustSelectorComponent } from "./components/trust-selector.js";
56
66
  import { UserMessageComponent } from "./components/user-message.js";
57
67
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
58
- import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
68
+ import { editInExternalEditor } from "./external-editor.js";
69
+ import { refreshModelCatalogs } from "./model-catalog-refresh.js";
70
+ import { getModelSearchText } from "./model-search.js";
71
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
72
+ import { InteractiveThemeController } from "./theme/theme-controller.js";
59
73
  function isExpandable(obj) {
60
74
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
61
75
  }
@@ -71,75 +85,172 @@ class ExpandableText extends Text {
71
85
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
72
86
  }
73
87
  }
74
- 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.";
88
+ function isCustomSessionEntry(item) {
89
+ return "type" in item && item.type === "custom";
90
+ }
91
+ const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
92
+ function isDeadTerminalError(error) {
93
+ if (!error || typeof error !== "object" || !("code" in error)) {
94
+ return false;
95
+ }
96
+ const code = error.code;
97
+ return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
98
+ }
99
+ 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. Disable this warning in /settings.";
75
100
  function isAnthropicSubscriptionAuthKey(apiKey) {
76
101
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
77
102
  }
78
103
  function isUnknownModel(model) {
79
104
  return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
80
105
  }
106
+ function quoteIfNeeded(value) {
107
+ if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value)) {
108
+ return value;
109
+ }
110
+ return `'${value.replace(/'/g, `'\\''`)}'`;
111
+ }
112
+ export function formatResumeCommand(sessionManager) {
113
+ if (!process.stdout.isTTY)
114
+ return undefined;
115
+ if (!sessionManager.isPersisted())
116
+ return undefined;
117
+ const sessionFile = sessionManager.getSessionFile();
118
+ if (!sessionFile || !fs.existsSync(sessionFile))
119
+ return undefined;
120
+ const args = [APP_NAME];
121
+ if (!sessionManager.usesDefaultSessionDir()) {
122
+ args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
123
+ }
124
+ args.push("--session", sessionManager.getSessionId());
125
+ return args.join(" ");
126
+ }
81
127
  function hasDefaultModelProvider(providerId) {
82
128
  return providerId in defaultModelPerProvider;
83
129
  }
84
- const BEDROCK_PROVIDER_ID = "amazon-bedrock";
85
- const API_KEY_LOGIN_PROVIDERS = {
86
- anthropic: "Anthropic",
87
- [BEDROCK_PROVIDER_ID]: "Amazon Bedrock",
88
- "azure-openai-responses": "Azure OpenAI Responses",
89
- cerebras: "Cerebras",
90
- fireworks: "Fireworks",
91
- google: "Google Gemini",
92
- "google-vertex": "Google Vertex AI",
93
- groq: "Groq",
94
- huggingface: "Hugging Face",
95
- "kimi-coding": "Kimi For Coding",
96
- mistral: "Mistral",
97
- minimax: "MiniMax",
98
- "minimax-cn": "MiniMax (China)",
99
- opencode: "OpenCode Zen",
100
- "opencode-go": "OpenCode Go",
101
- openai: "OpenAI",
102
- openrouter: "OpenRouter",
103
- "vercel-ai-gateway": "Vercel AI Gateway",
104
- xai: "xAI",
105
- zai: "ZAI",
106
- };
107
- const BUILT_IN_API_KEY_LOGIN_PROVIDERS = new Set(Object.keys(API_KEY_LOGIN_PROVIDERS));
108
- const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
109
- export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
110
- if (BUILT_IN_API_KEY_LOGIN_PROVIDERS.has(providerId)) {
111
- return true;
130
+ const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
131
+ function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
132
+ const filtered = fuzzyFilter(items, prefix, getSearchText);
133
+ if (filtered.length === 0)
134
+ return null;
135
+ return filtered.map(toAutocompleteItem);
136
+ }
137
+ function getLoginProviderCompletionOptions(providerOptions) {
138
+ const byId = new Map();
139
+ for (const provider of providerOptions) {
140
+ const existing = byId.get(provider.id);
141
+ if (existing) {
142
+ if (!existing.authTypes.includes(provider.authType)) {
143
+ existing.authTypes.push(provider.authType);
144
+ existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
145
+ }
146
+ continue;
147
+ }
148
+ byId.set(provider.id, {
149
+ id: provider.id,
150
+ name: provider.name,
151
+ authTypes: [provider.authType],
152
+ });
112
153
  }
113
- if (builtInProviderIds.has(providerId)) {
114
- return false;
154
+ return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
155
+ }
156
+ function getLoginProviderSearchText(provider) {
157
+ const authTypes = provider.authTypes
158
+ .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
159
+ .join(" ");
160
+ return `${provider.id} ${provider.name} ${authTypes}`;
161
+ }
162
+ function formatLoginProviderCompletionDescription(provider) {
163
+ const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
164
+ return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
165
+ }
166
+ /** Composition root for selecting the interactive terminal renderer. */
167
+ export function createInteractiveTui(options) {
168
+ const terminal = options.terminal ?? new ProcessTerminal();
169
+ if (options.tuiMode === "fullscreen") {
170
+ const styleSearchMatch = (text) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
171
+ return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, {
172
+ searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
173
+ searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
174
+ openUrl: openBrowser,
175
+ onRightClickPaste: options.onRightClickPaste,
176
+ copySelection: async (text) => {
177
+ try {
178
+ await copyToClipboard(text);
179
+ return true;
180
+ }
181
+ catch {
182
+ return false;
183
+ }
184
+ },
185
+ });
115
186
  }
116
- return !oauthProviderIds.has(providerId);
187
+ return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
117
188
  }
118
- export function getApiKeyProviderDisplayName(providerId) {
119
- return API_KEY_LOGIN_PROVIDERS[providerId] ?? providerId;
189
+ /** Stable reference for components while InteractiveMode replaces the active renderer. */
190
+ export function createInteractiveTuiReference(getTui) {
191
+ return new Proxy({}, {
192
+ get: (_target, property) => {
193
+ const tui = getTui();
194
+ const value = Reflect.get(tui, property, tui);
195
+ if (typeof value !== "function")
196
+ return value;
197
+ let methodTui = tui;
198
+ let method = value;
199
+ return (...args) => {
200
+ const currentTui = getTui();
201
+ if (currentTui !== methodTui) {
202
+ const currentMethod = Reflect.get(currentTui, property, currentTui);
203
+ if (typeof currentMethod !== "function") {
204
+ throw new TypeError(`TUI property ${String(property)} is not callable`);
205
+ }
206
+ methodTui = currentTui;
207
+ method = currentMethod;
208
+ }
209
+ return Reflect.apply(method, methodTui, args);
210
+ };
211
+ },
212
+ set: (_target, property, value) => {
213
+ const tui = getTui();
214
+ return Reflect.set(tui, property, value, tui);
215
+ },
216
+ has: (_target, property) => Reflect.has(getTui(), property),
217
+ getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
218
+ });
120
219
  }
121
220
  export class InteractiveMode {
122
- options;
123
221
  runtimeHost;
222
+ renderer;
124
223
  ui;
224
+ mainScreenRenderState;
225
+ loadedResourcesContainer;
125
226
  chatContainer;
227
+ documentContainer;
228
+ transcriptScrollView;
229
+ fullscreenLayoutRoot;
126
230
  pendingMessagesContainer;
127
231
  statusContainer;
128
232
  defaultEditor;
129
233
  editor;
234
+ editorComponentFactory;
130
235
  autocompleteProvider;
131
236
  autocompleteProviderWrappers = [];
132
237
  fdPath;
133
238
  editorContainer;
239
+ activeSelectorToken;
240
+ activeSelectorDispose;
134
241
  footer;
242
+ footerContainer;
135
243
  footerDataProvider;
136
244
  // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
137
245
  keybindings;
138
246
  version;
139
247
  isInitialized = false;
140
248
  onInputCallback;
141
- loadingAnimation = undefined;
249
+ pendingUserInputs = [];
250
+ activeStatusIndicator = undefined;
251
+ idleStatus = new IdleStatus();
142
252
  workingMessage = undefined;
253
+ workingVisible = true;
143
254
  workingIndicatorOptions = undefined;
144
255
  defaultWorkingMessage = "Working...";
145
256
  defaultHiddenThinkingLabel = "Thinking...";
@@ -152,6 +263,7 @@ export class InteractiveMode {
152
263
  // Status line tracking (for mutating immediately-sequential status updates)
153
264
  lastStatusSpacer = undefined;
154
265
  lastStatusText = undefined;
266
+ managedToolStatusStarted = false;
155
267
  // Streaming message tracking
156
268
  streamingComponent = undefined;
157
269
  streamingMessage = undefined;
@@ -161,6 +273,11 @@ export class InteractiveMode {
161
273
  toolOutputExpanded = false;
162
274
  // Thinking block visibility state
163
275
  hideThinkingBlock = false;
276
+ outputPad = 1;
277
+ mermaidMarkdownTransformer = createMermaidMarkdownTransformer({
278
+ getMode: () => this.settingsManager.getMermaidRenderingMode(),
279
+ theme,
280
+ });
164
281
  // Skill commands: command name -> skill file path
165
282
  skillCommands = new Map();
166
283
  // Agent subscription unsubscribe function
@@ -173,11 +290,8 @@ export class InteractiveMode {
173
290
  // Track pending bash components (shown in pending area, moved to chat on submit)
174
291
  pendingBashComponents = [];
175
292
  // Auto-compaction state
176
- autoCompactionLoader = undefined;
177
293
  autoCompactionEscapeHandler;
178
294
  // Auto-retry state
179
- retryLoader = undefined;
180
- retryCountdown = undefined;
181
295
  retryEscapeHandler;
182
296
  // Messages queued while compaction is running
183
297
  compactionQueuedMessages = [];
@@ -187,7 +301,7 @@ export class InteractiveMode {
187
301
  extensionSelector = undefined;
188
302
  extensionInput = undefined;
189
303
  extensionEditor = undefined;
190
- extensionTerminalInputUnsubscribers = new Set();
304
+ extensionTerminalInputSubscriptions = new Set();
191
305
  // Extension widgets (components rendered above/below the editor)
192
306
  extensionWidgetsAbove = new Map();
193
307
  extensionWidgetsBelow = new Map();
@@ -201,6 +315,12 @@ export class InteractiveMode {
201
315
  builtInHeader = undefined;
202
316
  // Custom header from extension (undefined = use built-in header)
203
317
  customHeader = undefined;
318
+ options;
319
+ onRightClickPaste = () => {
320
+ void this.handleRightClickPaste();
321
+ };
322
+ autoTrustOnReloadCwd;
323
+ themeController;
204
324
  // Convenience accessors
205
325
  get session() {
206
326
  return this.runtimeHost.session;
@@ -215,19 +335,33 @@ export class InteractiveMode {
215
335
  return this.session.settingsManager;
216
336
  }
217
337
  constructor(runtimeHost, options = {}) {
218
- this.options = options;
219
338
  this.runtimeHost = runtimeHost;
339
+ const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
340
+ this.options = { ...options, tuiMode };
341
+ this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
220
342
  this.runtimeHost.setBeforeSessionInvalidate(() => {
221
343
  this.resetExtensionUI();
222
344
  });
223
345
  this.runtimeHost.setRebindSession(async () => {
224
- await this.rebindCurrentSession();
346
+ await this.rebindCurrentSession({ renderBeforeBind: true });
347
+ await this.themeController.applyFromSettings();
225
348
  });
226
349
  this.version = VERSION;
227
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
350
+ this.renderer = createInteractiveTui({
351
+ tuiMode,
352
+ showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
353
+ logDirectory: getStateDir(),
354
+ onRightClickPaste: this.onRightClickPaste,
355
+ });
356
+ this.ui = createInteractiveTuiReference(() => this.renderer);
228
357
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
229
358
  this.headerContainer = new Container();
359
+ this.loadedResourcesContainer = new Container();
230
360
  this.chatContainer = new Container();
361
+ this.documentContainer = new Container();
362
+ this.documentContainer.addChild(this.headerContainer);
363
+ this.documentContainer.addChild(this.loadedResourcesContainer);
364
+ this.documentContainer.addChild(this.chatContainer);
231
365
  this.pendingMessagesContainer = new Container();
232
366
  this.statusContainer = new Container();
233
367
  this.widgetContainerAbove = new Container();
@@ -246,11 +380,19 @@ export class InteractiveMode {
246
380
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
247
381
  this.footer = new FooterComponent(this.session, this.footerDataProvider);
248
382
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
383
+ this.footerContainer = new Container();
384
+ this.footerContainer.addChild(this.footer);
249
385
  // Load hide thinking block setting
250
386
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
387
+ this.outputPad = this.settingsManager.getOutputPad();
251
388
  // Register themes from resource loader and initialize
252
389
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
253
- initTheme(this.settingsManager.getTheme(), true);
390
+ this.themeController = new InteractiveThemeController(this.ui, {
391
+ getSettingsManager: () => this.settingsManager,
392
+ showError: (message) => this.showError(message),
393
+ onChanged: () => this.updateEditorBorderColor(),
394
+ initialThemeSetting: options.initialThemeSetting,
395
+ });
254
396
  }
255
397
  getAutocompleteSourceTag(sourceInfo) {
256
398
  if (!sourceInfo) {
@@ -296,33 +438,41 @@ export class InteractiveMode {
296
438
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
297
439
  name: command.name,
298
440
  description: command.description,
441
+ ...(command.argumentHint && { argumentHint: command.argumentHint }),
299
442
  }));
300
443
  const modelCommand = slashCommands.find((command) => command.name === "model");
301
444
  if (modelCommand) {
302
445
  modelCommand.getArgumentCompletions = (prefix) => {
303
- // Get available models (scoped or from registry)
304
446
  const models = this.session.scopedModels.length > 0
305
447
  ? this.session.scopedModels.map((s) => s.model)
306
- : this.session.modelRegistry.getAvailable();
448
+ : this.session.modelRuntime.getAvailableSnapshot();
307
449
  if (models.length === 0)
308
450
  return null;
309
451
  // Create items with provider/id format
310
452
  const items = models.map((m) => ({
311
453
  id: m.id,
312
454
  provider: m.provider,
455
+ name: m.name,
313
456
  label: `${m.provider}/${m.id}`,
314
457
  }));
315
- // Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
316
- const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
317
- if (filtered.length === 0)
318
- return null;
319
- return filtered.map((item) => ({
458
+ return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
320
459
  value: item.label,
321
460
  label: item.id,
322
461
  description: item.provider,
323
462
  }));
324
463
  };
325
464
  }
465
+ const loginCommand = slashCommands.find((command) => command.name === "login");
466
+ if (loginCommand) {
467
+ loginCommand.getArgumentCompletions = (prefix) => {
468
+ const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
469
+ return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
470
+ value: provider.id,
471
+ label: provider.id,
472
+ description: formatLoginProviderCompletionDescription(provider),
473
+ }));
474
+ };
475
+ }
326
476
  // Convert prompt templates to SlashCommand format for autocomplete
327
477
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
328
478
  name: cmd.name,
@@ -356,8 +506,13 @@ export class InteractiveMode {
356
506
  }
357
507
  setupAutocompleteProvider() {
358
508
  let provider = this.createBaseAutocompleteProvider();
509
+ const triggerCharacters = [];
359
510
  for (const wrapProvider of this.autocompleteProviderWrappers) {
360
511
  provider = wrapProvider(provider);
512
+ triggerCharacters.push(...(provider.triggerCharacters ?? []));
513
+ }
514
+ if (triggerCharacters.length > 0) {
515
+ provider.triggerCharacters = [...new Set(triggerCharacters)];
361
516
  }
362
517
  this.autocompleteProvider = provider;
363
518
  this.defaultEditor.setAutocompleteProvider(provider);
@@ -391,18 +546,131 @@ export class InteractiveMode {
391
546
  }
392
547
  this.chatContainer.addChild(new DynamicBorder());
393
548
  }
549
+ mountInteractiveTui(tui, components) {
550
+ for (const component of components)
551
+ tui.addChild(component);
552
+ if (TuiLayouts.isViewportTUI(tui)) {
553
+ if (!this.fullscreenLayoutRoot)
554
+ throw new Error("Fullscreen layout is not initialized");
555
+ tui.setLayoutRoot(this.fullscreenLayoutRoot);
556
+ }
557
+ }
558
+ stopInteractiveTui(fullscreenExitOutput) {
559
+ if (this.renderer.mode === "fullscreen" && fullscreenExitOutput === "transcript") {
560
+ while (this.renderer.hasOverlayEntries)
561
+ this.renderer.hideOverlay();
562
+ this.switchTuiMode("regular", false, false);
563
+ this.renderer.renderNow();
564
+ }
565
+ this.ui.stop({ preserveScreen: this.renderer.mode === "fullscreen" });
566
+ }
567
+ switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
568
+ const previousUi = this.renderer;
569
+ if (mode === previousUi.mode)
570
+ return true;
571
+ if (previousUi.hasOverlayEntries)
572
+ return false;
573
+ const components = [...previousUi.children];
574
+ const focus = previousUi.getFocusedComponent();
575
+ const terminal = previousUi.terminal;
576
+ const showHardwareCursor = previousUi.getShowHardwareCursor();
577
+ const clearOnShrink = previousUi.getClearOnShrink();
578
+ const onDebug = previousUi.onDebug;
579
+ if (previousUi instanceof TuiMainScreen) {
580
+ this.mainScreenRenderState = previousUi.captureRenderState();
581
+ }
582
+ previousUi.stop({ preserveScreen: true });
583
+ previousUi.setFocus(null);
584
+ previousUi.clear();
585
+ if (TuiLayouts.isViewportTUI(previousUi))
586
+ previousUi.setLayoutRoot(undefined);
587
+ const nextUi = createInteractiveTui({
588
+ tuiMode: mode,
589
+ showHardwareCursor,
590
+ logDirectory: getStateDir(),
591
+ terminal,
592
+ onRightClickPaste: this.onRightClickPaste,
593
+ });
594
+ nextUi.setClearOnShrink(clearOnShrink);
595
+ nextUi.onDebug = onDebug;
596
+ if (nextUi instanceof TuiMainScreen && this.mainScreenRenderState) {
597
+ nextUi.restoreRenderState(this.mainScreenRenderState);
598
+ }
599
+ this.renderer = nextUi;
600
+ this.options.tuiMode = mode;
601
+ this.mountInteractiveTui(nextUi, components);
602
+ nextUi.invalidate();
603
+ nextUi.setFocus(focus);
604
+ if (!startRenderer)
605
+ return true;
606
+ nextUi.start();
607
+ this.themeController.rebindTui();
608
+ this.rebindExtensionTerminalInputListeners();
609
+ if (restoreProgress &&
610
+ this.settingsManager.getShowTerminalProgress() &&
611
+ (this.session.isStreaming || this.session.isCompacting)) {
612
+ terminal.setProgress(true);
613
+ }
614
+ return true;
615
+ }
394
616
  async init() {
395
617
  if (this.isInitialized)
396
618
  return;
397
619
  this.registerSignalHandlers();
398
620
  // Load changelog (only show new entries, skip for resumed sessions)
399
621
  this.changelogMarkdown = this.getChangelogForDisplay();
400
- // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
401
- // Both are needed: fd for autocomplete, rg for grep tool and bash commands
402
- const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
403
- this.fdPath = fdPath;
404
- // Add header container as first child
405
- this.ui.addChild(this.headerContainer);
622
+ if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
623
+ const modelList = this.session.scopedModels
624
+ .map((sm) => {
625
+ const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
626
+ return `${sm.model.id}${thinkingStr}`;
627
+ })
628
+ .join(", ");
629
+ const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
630
+ const cycleHint = cycleKeys.length > 0
631
+ ? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`)
632
+ : "";
633
+ console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
634
+ }
635
+ // Keep one component tree and remount it when changing renderers.
636
+ this.renderWidgets(); // Initialize with default spacer
637
+ this.transcriptScrollView = new TuiLayouts.ScrollView(this.documentContainer, {
638
+ follow: "end",
639
+ primary: true,
640
+ overscroll: "chain",
641
+ scrollbar: this.settingsManager.getFullscreenScrollbar(),
642
+ scrollbarStyle: (text) => theme.bg("scrollbarThumb", text),
643
+ });
644
+ const dock = new TuiLayouts.VStack([
645
+ { component: this.pendingMessagesContainer, shrink: 1, minSize: 0 },
646
+ { component: this.statusContainer, shrink: 1, minSize: 0 },
647
+ { component: this.widgetContainerAbove, shrink: 1, minSize: 0 },
648
+ { component: this.editorContainer, shrink: 1, minSize: 3 },
649
+ { component: this.widgetContainerBelow, shrink: 1, minSize: 0 },
650
+ { component: this.footerContainer, shrink: 1, minSize: 1 },
651
+ ]);
652
+ this.fullscreenLayoutRoot = new TuiLayouts.VStack([
653
+ { component: this.transcriptScrollView, basis: 0, grow: 1, shrink: 1, minSize: 1 },
654
+ { component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
655
+ ]);
656
+ this.mountInteractiveTui(this.renderer, [
657
+ this.documentContainer,
658
+ this.pendingMessagesContainer,
659
+ this.statusContainer,
660
+ this.widgetContainerAbove,
661
+ this.editorContainer,
662
+ this.widgetContainerBelow,
663
+ this.footerContainer,
664
+ ]);
665
+ // Accept text while startup completes, but only enable interrupt, exit, and submission feedback.
666
+ this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
667
+ this.defaultEditor.onCtrlD = () => this.handleCtrlD();
668
+ this.defaultEditor.onSubmit = (text) => this.handleStartupSubmit(text);
669
+ this.ui.setFocus(this.editor);
670
+ // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
671
+ this.ui.start();
672
+ this.isInitialized = true;
673
+ await this.themeController.applyFromSettings();
406
674
  // Add header with keybindings from config (unless silenced)
407
675
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
408
676
  const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
@@ -426,7 +694,7 @@ export class InteractiveMode {
426
694
  rawKeyHint("!!", "to run bash (no context)"),
427
695
  hint("app.message.followUp", "to queue follow-up"),
428
696
  hint("app.message.dequeue", "to edit all queued messages"),
429
- hint("app.clipboard.pasteImage", "to paste image"),
697
+ hint("app.clipboard.pasteImage", "to paste image (with text fallback)"),
430
698
  rawKeyHint("drop files", "to attach"),
431
699
  ].join("\n");
432
700
  const compactInstructions = [
@@ -437,7 +705,7 @@ export class InteractiveMode {
437
705
  hint("app.tools.expand", "more"),
438
706
  ].join(theme.fg("muted", " · "));
439
707
  const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
440
- const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
708
+ const onboarding = theme.fg("dim", `${APP_NAME} can explain its own features and look up its docs. Ask it how to use or extend ${APP_NAME}.`);
441
709
  this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
442
710
  // Setup UI layout
443
711
  this.headerContainer.addChild(new Spacer(1));
@@ -449,20 +717,19 @@ export class InteractiveMode {
449
717
  this.builtInHeader = new Text("", 0, 0);
450
718
  this.headerContainer.addChild(this.builtInHeader);
451
719
  }
452
- this.ui.addChild(this.chatContainer);
453
- this.ui.addChild(this.pendingMessagesContainer);
454
- this.ui.addChild(this.statusContainer);
455
- this.renderWidgets(); // Initialize with default spacer
456
- this.ui.addChild(this.widgetContainerAbove);
457
- this.ui.addChild(this.editorContainer);
458
- this.ui.addChild(this.widgetContainerBelow);
459
- this.ui.addChild(this.footer);
460
- this.ui.setFocus(this.editor);
720
+ this.ui.requestRender();
721
+ // Ensure fd and rg are available after mounting the TUI (downloads if missing, adds to PATH via getBinDir)
722
+ // so slow downloads do not make startup appear frozen.
723
+ // Both are needed: fd for autocomplete, rg for grep tool and bash commands.
724
+ const [fdPath] = await Promise.all([
725
+ ensureTool("fd", (status) => this.showManagedToolStatus(status)),
726
+ ensureTool("rg", (status) => this.showManagedToolStatus(status)),
727
+ ]);
728
+ this.fdPath = fdPath;
729
+ // Enable the remaining input handlers only after managed-tool setup completes.
461
730
  this.setupKeyHandlers();
462
731
  this.setupEditorSubmitHandler();
463
- // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
464
- this.ui.start();
465
- this.isInitialized = true;
732
+ this.ui.requestRender();
466
733
  // Initialize extensions first so resources are shown before messages
467
734
  await this.rebindCurrentSession();
468
735
  // Render initial messages AFTER showing loaded resources
@@ -499,17 +766,33 @@ export class InteractiveMode {
499
766
  */
500
767
  async run() {
501
768
  await this.init();
769
+ if (!process.env.MCPI_OFFLINE) {
770
+ const controller = new AbortController();
771
+ const timeout = setTimeout(() => controller.abort(), 15_000);
772
+ void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
773
+ .then(() => this.updateAvailableProviderCount())
774
+ .catch(() => { })
775
+ .finally(() => clearTimeout(timeout));
776
+ }
502
777
  // Start version check asynchronously
503
- this.checkForNewVersion().then((newVersion) => {
504
- if (newVersion) {
505
- this.showNewVersionNotification(newVersion);
778
+ checkForNewPiVersion(this.version).then((newRelease) => {
779
+ if (newRelease) {
780
+ this.showNewVersionNotification(newRelease);
506
781
  }
507
782
  });
508
783
  // Start package update check asynchronously
509
- this.checkForPackageUpdates().then((updates) => {
784
+ this.checkForPackageUpdates()
785
+ .then((updates) => {
510
786
  if (updates.length > 0) {
511
787
  this.showPackageUpdateNotification(updates);
512
788
  }
789
+ })
790
+ .finally(() => {
791
+ // On Windows, npm can overwrite the shared console title while checking
792
+ // extension package versions. Restore mcpi's title after the startup check.
793
+ if (process.platform === "win32" && this.isInitialized) {
794
+ this.updateTerminalTitle();
795
+ }
513
796
  });
514
797
  // Check tmux keyboard setup asynchronously
515
798
  this.checkTmuxKeyboardSetup().then((warning) => {
@@ -522,7 +805,7 @@ export class InteractiveMode {
522
805
  if (migratedProviders && migratedProviders.length > 0) {
523
806
  this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
524
807
  }
525
- const modelsJsonError = this.session.modelRegistry.getError();
808
+ const modelsJsonError = this.session.modelRuntime.getError();
526
809
  if (modelsJsonError) {
527
810
  this.showError(`models.json error: ${modelsJsonError}`);
528
811
  }
@@ -563,31 +846,8 @@ export class InteractiveMode {
563
846
  }
564
847
  }
565
848
  }
566
- /**
567
- * Check npm registry for a newer version.
568
- */
569
- async checkForNewVersion() {
570
- if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
571
- return undefined;
572
- try {
573
- const response = await fetch("https://registry.npmjs.org/mcpi/latest", {
574
- signal: AbortSignal.timeout(10000),
575
- });
576
- if (!response.ok)
577
- return undefined;
578
- const data = (await response.json());
579
- const latestVersion = data.version;
580
- if (latestVersion && latestVersion !== this.version) {
581
- return latestVersion;
582
- }
583
- return undefined;
584
- }
585
- catch {
586
- return undefined;
587
- }
588
- }
589
849
  async checkForPackageUpdates() {
590
- if (process.env.PI_OFFLINE) {
850
+ if (process.env.MCPI_OFFLINE) {
591
851
  return [];
592
852
  }
593
853
  try {
@@ -640,7 +900,7 @@ export class InteractiveMode {
640
900
  return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
641
901
  }
642
902
  if (extendedKeysFormat === "xterm") {
643
- 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.";
903
+ return `${APP_NAME} works best with csi-u, but tmux extended-keys-format is xterm. Add \`set -g extended-keys-format csi-u\` to ~/.tmux.conf and restart tmux.`;
644
904
  }
645
905
  return undefined;
646
906
  }
@@ -657,32 +917,17 @@ export class InteractiveMode {
657
917
  const changelogPath = getChangelogPath();
658
918
  const entries = parseChangelog(changelogPath);
659
919
  if (!lastVersion) {
660
- // Fresh install - record the version, send telemetry, don't show changelog
920
+ // Fresh install - record the version, don't show changelog
661
921
  this.settingsManager.setLastChangelogVersion(VERSION);
662
- this.reportInstallTelemetry(VERSION);
663
922
  return undefined;
664
923
  }
665
924
  const newEntries = getNewEntries(entries, lastVersion);
666
925
  if (newEntries.length > 0) {
667
926
  this.settingsManager.setLastChangelogVersion(VERSION);
668
- this.reportInstallTelemetry(VERSION);
669
- return newEntries.map((e) => e.content).join("\n\n");
927
+ return newEntries.map((e) => normalizeChangelogLinks(e.content, e)).join("\n\n");
670
928
  }
671
929
  return undefined;
672
930
  }
673
- reportInstallTelemetry(version) {
674
- if (process.env.PI_OFFLINE) {
675
- return;
676
- }
677
- if (!isInstallTelemetryEnabled(this.settingsManager)) {
678
- return;
679
- }
680
- void fetch(`https://pi.dev/install?version=${encodeURIComponent(version)}`, {
681
- signal: AbortSignal.timeout(5000),
682
- })
683
- .then(() => undefined)
684
- .catch(() => undefined);
685
- }
686
931
  getMarkdownThemeWithSettings() {
687
932
  return {
688
933
  ...getMarkdownTheme(),
@@ -709,13 +954,9 @@ export class InteractiveMode {
709
954
  formatContextPath(p) {
710
955
  const cwd = path.resolve(this.sessionManager.getCwd());
711
956
  const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
712
- const relativePath = path.relative(cwd, absolutePath);
713
- const isInsideCwd = relativePath === "" ||
714
- (!relativePath.startsWith("..") &&
715
- !relativePath.startsWith(`..${path.sep}`) &&
716
- !path.isAbsolute(relativePath));
717
- if (isInsideCwd) {
718
- return relativePath || ".";
957
+ const relativePath = getCwdRelativePath(absolutePath, cwd);
958
+ if (relativePath !== undefined) {
959
+ return relativePath;
719
960
  }
720
961
  return this.formatDisplayPath(absolutePath);
721
962
  }
@@ -726,8 +967,15 @@ export class InteractiveMode {
726
967
  * Get a short path relative to the package root for display.
727
968
  */
728
969
  getShortPath(fullPath, sourceInfo) {
970
+ const normalizedFullPath = fullPath.replace(/\\/g, "/");
729
971
  const baseDir = sourceInfo?.baseDir;
730
972
  if (baseDir && this.isPackageSource(sourceInfo)) {
973
+ const normalizedBaseDir = baseDir.replace(/\\/g, "/");
974
+ const npmRootMatch = normalizedBaseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
975
+ // If fullPath is under the same node_modules root as baseDir, preserve that relative topology.
976
+ if (npmRootMatch?.[1] && normalizedFullPath.startsWith(`${npmRootMatch[1]}/`)) {
977
+ return path.posix.relative(normalizedBaseDir, normalizedFullPath);
978
+ }
731
979
  const relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));
732
980
  if (relativePath &&
733
981
  relativePath !== "." &&
@@ -738,11 +986,11 @@ export class InteractiveMode {
738
986
  }
739
987
  }
740
988
  const source = sourceInfo?.source ?? "";
741
- const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
989
+ const npmMatch = normalizedFullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
742
990
  if (npmMatch && source.startsWith("npm:")) {
743
991
  return npmMatch[2];
744
992
  }
745
- const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
993
+ const gitMatch = normalizedFullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
746
994
  if (gitMatch && source.startsWith("git:")) {
747
995
  return gitMatch[1];
748
996
  }
@@ -974,6 +1222,8 @@ export class InteractiveMode {
974
1222
  return lines.join("\n");
975
1223
  }
976
1224
  showLoadedResources(options) {
1225
+ // Resource rendering is idempotent; chat clears no longer clear this separate container.
1226
+ this.loadedResourcesContainer.clear();
977
1227
  const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
978
1228
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
979
1229
  if (!showListing && !showDiagnostics) {
@@ -989,14 +1239,17 @@ export class InteractiveMode {
989
1239
  };
990
1240
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
991
1241
  const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
992
- this.chatContainer.addChild(section);
993
- this.chatContainer.addChild(new Spacer(1));
1242
+ this.loadedResourcesContainer.addChild(section);
1243
+ this.loadedResourcesContainer.addChild(new Spacer(1));
994
1244
  };
995
1245
  const skillsResult = this.session.resourceLoader.getSkills();
996
1246
  const promptsResult = this.session.resourceLoader.getPrompts();
997
1247
  const themesResult = this.session.resourceLoader.getThemes();
998
1248
  const extensions = options?.extensions ??
999
- this.session.resourceLoader.getExtensions().extensions.map((extension) => ({
1249
+ this.session.resourceLoader
1250
+ .getExtensions()
1251
+ .extensions.filter((extension) => !extension.hidden)
1252
+ .map((extension) => ({
1000
1253
  path: extension.path,
1001
1254
  sourceInfo: extension.sourceInfo,
1002
1255
  }));
@@ -1022,9 +1275,14 @@ export class InteractiveMode {
1022
1275
  }
1023
1276
  }
1024
1277
  if (showListing) {
1025
- const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1278
+ const systemPromptSource = this.session.resourceLoader.getSystemPromptSource();
1279
+ const contextFiles = [
1280
+ ...(systemPromptSource ? [systemPromptSource] : []),
1281
+ ...this.session.resourceLoader.getAppendSystemPromptSources(),
1282
+ ...this.session.resourceLoader.getAgentsFiles().agentsFiles,
1283
+ ];
1026
1284
  if (contextFiles.length > 0) {
1027
- this.chatContainer.addChild(new Spacer(1));
1285
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1028
1286
  const contextList = contextFiles
1029
1287
  .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1030
1288
  .join("\n");
@@ -1087,14 +1345,14 @@ export class InteractiveMode {
1087
1345
  const skillDiagnostics = skillsResult.diagnostics;
1088
1346
  if (skillDiagnostics.length > 0) {
1089
1347
  const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1090
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1091
- this.chatContainer.addChild(new Spacer(1));
1348
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1349
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1092
1350
  }
1093
1351
  const promptDiagnostics = promptsResult.diagnostics;
1094
1352
  if (promptDiagnostics.length > 0) {
1095
1353
  const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1096
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1097
- this.chatContainer.addChild(new Spacer(1));
1354
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1355
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1098
1356
  }
1099
1357
  const extensionDiagnostics = [];
1100
1358
  const extensionErrors = this.session.resourceLoader.getExtensions().errors;
@@ -1110,14 +1368,14 @@ export class InteractiveMode {
1110
1368
  extensionDiagnostics.push(...shortcutDiagnostics);
1111
1369
  if (extensionDiagnostics.length > 0) {
1112
1370
  const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1113
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1114
- this.chatContainer.addChild(new Spacer(1));
1371
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1372
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1115
1373
  }
1116
1374
  const themeDiagnostics = themesResult.diagnostics;
1117
1375
  if (themeDiagnostics.length > 0) {
1118
1376
  const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1119
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1120
- this.chatContainer.addChild(new Spacer(1));
1377
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1378
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1121
1379
  }
1122
1380
  }
1123
1381
  }
@@ -1128,21 +1386,16 @@ export class InteractiveMode {
1128
1386
  const uiContext = this.createExtensionUIContext();
1129
1387
  await this.session.bindExtensions({
1130
1388
  uiContext,
1389
+ mode: "tui",
1390
+ abortHandler: () => {
1391
+ this.restoreQueuedMessagesToEditor({ abort: true });
1392
+ },
1131
1393
  commandContextActions: {
1132
- waitForIdle: () => this.session.agent.waitForIdle(),
1394
+ waitForIdle: () => this.session.waitForIdle(),
1133
1395
  newSession: async (options) => {
1134
- if (this.loadingAnimation) {
1135
- this.loadingAnimation.stop();
1136
- this.loadingAnimation = undefined;
1137
- }
1138
- this.statusContainer.clear();
1396
+ this.clearStatusIndicator();
1139
1397
  try {
1140
- const result = await this.runtimeHost.newSession(options);
1141
- if (!result.cancelled) {
1142
- this.renderCurrentSessionState();
1143
- this.ui.requestRender();
1144
- }
1145
- return result;
1398
+ return await this.runtimeHost.newSession(options);
1146
1399
  }
1147
1400
  catch (error) {
1148
1401
  return this.handleFatalRuntimeError("Failed to create session", error);
@@ -1152,7 +1405,6 @@ export class InteractiveMode {
1152
1405
  try {
1153
1406
  const result = await this.runtimeHost.fork(entryId, options);
1154
1407
  if (!result.cancelled) {
1155
- this.renderCurrentSessionState();
1156
1408
  this.editor.setText(result.selectedText ?? "");
1157
1409
  this.showStatus("Forked to new session");
1158
1410
  }
@@ -1190,7 +1442,7 @@ export class InteractiveMode {
1190
1442
  },
1191
1443
  shutdownHandler: () => {
1192
1444
  this.shutdownRequested = true;
1193
- if (!this.session.isStreaming) {
1445
+ if (this.session.isIdle) {
1194
1446
  void this.shutdown();
1195
1447
  }
1196
1448
  },
@@ -1205,13 +1457,23 @@ export class InteractiveMode {
1205
1457
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1206
1458
  this.showStartupNoticesIfNeeded();
1207
1459
  }
1460
+ applyFullscreenScrollbarSetting() {
1461
+ this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1462
+ }
1208
1463
  applyRuntimeSettings() {
1464
+ configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1465
+ this.applyFullscreenScrollbarSetting();
1209
1466
  this.footer.setSession(this.session);
1210
1467
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1211
1468
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1212
1469
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1470
+ this.outputPad = this.settingsManager.getOutputPad();
1213
1471
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1214
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
1472
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
1473
+ this.ui.setClearOnShrink(clearOnShrink);
1474
+ if (!clearOnShrink && !this.activeStatusIndicator) {
1475
+ this.statusContainer.clear();
1476
+ }
1215
1477
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
1216
1478
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
1217
1479
  this.defaultEditor.setPaddingX(editorPaddingX);
@@ -1221,12 +1483,22 @@ export class InteractiveMode {
1221
1483
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
1222
1484
  }
1223
1485
  }
1224
- async rebindCurrentSession() {
1486
+ async rebindCurrentSession(options = {}) {
1487
+ const session = this.session;
1225
1488
  this.unsubscribe?.();
1226
1489
  this.unsubscribe = undefined;
1227
1490
  this.applyRuntimeSettings();
1491
+ if (options.renderBeforeBind) {
1492
+ this.renderCurrentSessionState();
1493
+ this.subscribeToAgent();
1494
+ }
1228
1495
  await this.bindCurrentSessionExtensions();
1229
- this.subscribeToAgent();
1496
+ if (this.session !== session) {
1497
+ return;
1498
+ }
1499
+ if (!options.renderBeforeBind) {
1500
+ this.subscribeToAgent();
1501
+ }
1230
1502
  await this.updateAvailableProviderCount();
1231
1503
  this.updateEditorBorderColor();
1232
1504
  this.updateTerminalTitle();
@@ -1235,10 +1507,11 @@ export class InteractiveMode {
1235
1507
  const message = error instanceof Error ? error.message : String(error);
1236
1508
  this.showError(`${prefix}: ${message}`);
1237
1509
  stopThemeWatcher();
1238
- this.stop();
1510
+ this.stop("transcript");
1239
1511
  process.exit(1);
1240
1512
  }
1241
1513
  renderCurrentSessionState() {
1514
+ this.loadedResourcesContainer.clear();
1242
1515
  this.chatContainer.clear();
1243
1516
  this.pendingMessagesContainer.clear();
1244
1517
  this.compactionQueuedMessages = [];
@@ -1253,6 +1526,9 @@ export class InteractiveMode {
1253
1526
  getRegisteredToolDefinition(toolName) {
1254
1527
  return this.session.getToolDefinition(toolName);
1255
1528
  }
1529
+ getMarkdownTransformers() {
1530
+ return [this.mermaidMarkdownTransformer, ...this.session.extensionRunner.getMarkdownTransformers()];
1531
+ }
1256
1532
  /**
1257
1533
  * Set up keyboard shortcuts registered by extensions.
1258
1534
  */
@@ -1263,14 +1539,20 @@ export class InteractiveMode {
1263
1539
  // Create a context for shortcut handlers
1264
1540
  const createContext = () => ({
1265
1541
  ui: this.createExtensionUIContext(),
1542
+ mode: "tui",
1266
1543
  hasUI: true,
1267
1544
  cwd: this.sessionManager.getCwd(),
1268
1545
  sessionManager: this.sessionManager,
1269
- modelRegistry: this.session.modelRegistry,
1546
+ modelRegistry: extensionRunner.getModelRegistry(),
1270
1547
  model: this.session.model,
1271
- isIdle: () => !this.session.isStreaming,
1548
+ scopedModels: this.session.scopedModels,
1549
+ thinkingLevel: this.session.thinkingLevel,
1550
+ isIdle: () => this.session.isIdle,
1551
+ isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1272
1552
  signal: this.session.agent.signal,
1273
- abort: () => this.session.abort(),
1553
+ abort: () => {
1554
+ this.restoreQueuedMessagesToEditor({ abort: true });
1555
+ },
1274
1556
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
1275
1557
  shutdown: () => {
1276
1558
  this.shutdownRequested = true;
@@ -1312,9 +1594,41 @@ export class InteractiveMode {
1312
1594
  this.footerDataProvider.setExtensionStatus(key, text);
1313
1595
  this.ui.requestRender();
1314
1596
  }
1597
+ showStatusIndicator(indicator) {
1598
+ this.activeStatusIndicator?.dispose();
1599
+ this.activeStatusIndicator = indicator;
1600
+ this.statusContainer.clear();
1601
+ this.statusContainer.addChild(indicator);
1602
+ }
1603
+ clearStatusIndicator(kind) {
1604
+ if (kind && this.activeStatusIndicator?.kind !== kind) {
1605
+ return;
1606
+ }
1607
+ const hadActiveStatusIndicator = this.activeStatusIndicator !== undefined;
1608
+ this.activeStatusIndicator?.dispose();
1609
+ this.activeStatusIndicator = undefined;
1610
+ this.statusContainer.clear();
1611
+ if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
1612
+ this.statusContainer.addChild(this.idleStatus);
1613
+ }
1614
+ }
1615
+ setWorkingVisible(visible) {
1616
+ this.workingVisible = visible;
1617
+ if (!visible) {
1618
+ this.clearStatusIndicator("working");
1619
+ this.ui.requestRender();
1620
+ return;
1621
+ }
1622
+ if (this.session.isStreaming && this.activeStatusIndicator?.kind !== "working") {
1623
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
1624
+ }
1625
+ this.ui.requestRender();
1626
+ }
1315
1627
  setWorkingIndicator(options) {
1316
1628
  this.workingIndicatorOptions = options;
1317
- this.loadingAnimation?.setIndicator(options);
1629
+ if (this.activeStatusIndicator?.kind === "working") {
1630
+ this.activeStatusIndicator.setIndicator(options);
1631
+ }
1318
1632
  this.ui.requestRender();
1319
1633
  }
1320
1634
  setHiddenThinkingLabel(label) {
@@ -1400,9 +1714,10 @@ export class InteractiveMode {
1400
1714
  this.defaultEditor.onExtensionShortcut = undefined;
1401
1715
  this.updateTerminalTitle();
1402
1716
  this.workingMessage = undefined;
1717
+ this.workingVisible = true;
1403
1718
  this.setWorkingIndicator();
1404
- if (this.loadingAnimation) {
1405
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1719
+ if (this.activeStatusIndicator?.kind === "working") {
1720
+ this.activeStatusIndicator.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1406
1721
  }
1407
1722
  this.setHiddenThinkingLabel();
1408
1723
  }
@@ -1441,22 +1756,16 @@ export class InteractiveMode {
1441
1756
  if (this.customFooter?.dispose) {
1442
1757
  this.customFooter.dispose();
1443
1758
  }
1444
- // Remove current footer from UI
1445
- if (this.customFooter) {
1446
- this.ui.removeChild(this.customFooter);
1447
- }
1448
- else {
1449
- this.ui.removeChild(this.footer);
1450
- }
1759
+ this.footerContainer.clear();
1451
1760
  if (factory) {
1452
1761
  // Create and add custom footer, passing the data provider
1453
1762
  this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1454
- this.ui.addChild(this.customFooter);
1763
+ this.footerContainer.addChild(this.customFooter);
1455
1764
  }
1456
1765
  else {
1457
1766
  // Restore built-in footer
1458
1767
  this.customFooter = undefined;
1459
- this.ui.addChild(this.footer);
1768
+ this.footerContainer.addChild(this.footer);
1460
1769
  }
1461
1770
  this.ui.requestRender();
1462
1771
  }
@@ -1502,22 +1811,41 @@ export class InteractiveMode {
1502
1811
  this.ui.requestRender();
1503
1812
  }
1504
1813
  addExtensionTerminalInputListener(handler) {
1505
- const unsubscribe = this.ui.addInputListener(handler);
1506
- this.extensionTerminalInputUnsubscribers.add(unsubscribe);
1814
+ const subscription = { handler, unsubscribe: this.ui.addInputListener(handler) };
1815
+ this.extensionTerminalInputSubscriptions.add(subscription);
1507
1816
  return () => {
1508
- unsubscribe();
1509
- this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
1817
+ subscription.unsubscribe();
1818
+ this.extensionTerminalInputSubscriptions.delete(subscription);
1510
1819
  };
1511
1820
  }
1512
- clearExtensionTerminalInputListeners() {
1513
- for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
1514
- unsubscribe();
1821
+ rebindExtensionTerminalInputListeners() {
1822
+ for (const subscription of this.extensionTerminalInputSubscriptions) {
1823
+ subscription.unsubscribe();
1824
+ subscription.unsubscribe = this.ui.addInputListener(subscription.handler);
1515
1825
  }
1516
- this.extensionTerminalInputUnsubscribers.clear();
1826
+ }
1827
+ clearExtensionTerminalInputListeners() {
1828
+ for (const subscription of this.extensionTerminalInputSubscriptions)
1829
+ subscription.unsubscribe();
1830
+ this.extensionTerminalInputSubscriptions.clear();
1517
1831
  }
1518
1832
  /**
1519
1833
  * Create the ExtensionUIContext for extensions.
1520
1834
  */
1835
+ createProjectTrustContext(cwd) {
1836
+ const ui = this.createExtensionUIContext();
1837
+ return {
1838
+ cwd,
1839
+ mode: "tui",
1840
+ hasUI: true,
1841
+ ui: {
1842
+ select: ui.select,
1843
+ confirm: ui.confirm,
1844
+ input: ui.input,
1845
+ notify: ui.notify,
1846
+ },
1847
+ };
1848
+ }
1521
1849
  createExtensionUIContext() {
1522
1850
  return {
1523
1851
  select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
@@ -1528,15 +1856,11 @@ export class InteractiveMode {
1528
1856
  setStatus: (key, text) => this.setExtensionStatus(key, text),
1529
1857
  setWorkingMessage: (message) => {
1530
1858
  this.workingMessage = message;
1531
- if (this.loadingAnimation) {
1532
- if (message) {
1533
- this.loadingAnimation.setMessage(message);
1534
- }
1535
- else {
1536
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1537
- }
1859
+ if (this.activeStatusIndicator?.kind === "working") {
1860
+ this.activeStatusIndicator.setMessage(message ?? this.defaultWorkingMessage);
1538
1861
  }
1539
1862
  },
1863
+ setWorkingVisible: (visible) => this.setWorkingVisible(visible),
1540
1864
  setWorkingIndicator: (options) => this.setWorkingIndicator(options),
1541
1865
  setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
1542
1866
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
@@ -1553,6 +1877,7 @@ export class InteractiveMode {
1553
1877
  this.setupAutocompleteProvider();
1554
1878
  },
1555
1879
  setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1880
+ getEditorComponent: () => this.editorComponentFactory,
1556
1881
  get theme() {
1557
1882
  return theme;
1558
1883
  },
@@ -1560,16 +1885,13 @@ export class InteractiveMode {
1560
1885
  getTheme: (name) => getThemeByName(name),
1561
1886
  setTheme: (themeOrName) => {
1562
1887
  if (themeOrName instanceof Theme) {
1563
- setThemeInstance(themeOrName);
1564
- this.ui.requestRender();
1565
- return { success: true };
1888
+ return this.themeController.setThemeInstance(themeOrName);
1566
1889
  }
1567
- const result = setTheme(themeOrName, true);
1890
+ const result = this.themeController.setThemeName(themeOrName);
1568
1891
  if (result.success) {
1569
1892
  if (this.settingsManager.getTheme() !== themeOrName) {
1570
1893
  this.settingsManager.setTheme(themeOrName);
1571
1894
  }
1572
- this.ui.requestRender();
1573
1895
  }
1574
1896
  return result;
1575
1897
  },
@@ -1599,7 +1921,8 @@ export class InteractiveMode {
1599
1921
  opts?.signal?.removeEventListener("abort", onAbort);
1600
1922
  this.hideExtensionSelector();
1601
1923
  resolve(undefined);
1602
- }, { tui: this.ui, timeout: opts?.timeout });
1924
+ }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1925
+ this.disposeActiveSelector();
1603
1926
  this.editorContainer.clear();
1604
1927
  this.editorContainer.addChild(this.extensionSelector);
1605
1928
  this.ui.setFocus(this.extensionSelector);
@@ -1651,6 +1974,7 @@ export class InteractiveMode {
1651
1974
  this.hideExtensionInput();
1652
1975
  resolve(undefined);
1653
1976
  }, { tui: this.ui, timeout: opts?.timeout });
1977
+ this.disposeActiveSelector();
1654
1978
  this.editorContainer.clear();
1655
1979
  this.editorContainer.addChild(this.extensionInput);
1656
1980
  this.ui.setFocus(this.extensionInput);
@@ -1679,7 +2003,8 @@ export class InteractiveMode {
1679
2003
  }, () => {
1680
2004
  this.hideExtensionEditor();
1681
2005
  resolve(undefined);
1682
- });
2006
+ }, undefined, this.settingsManager.getExternalEditorCommand());
2007
+ this.disposeActiveSelector();
1683
2008
  this.editorContainer.clear();
1684
2009
  this.editorContainer.addChild(this.extensionEditor);
1685
2010
  this.ui.setFocus(this.extensionEditor);
@@ -1701,8 +2026,10 @@ export class InteractiveMode {
1701
2026
  * Pass undefined to restore the default editor.
1702
2027
  */
1703
2028
  setCustomEditorComponent(factory) {
2029
+ this.editorComponentFactory = factory;
1704
2030
  // Save text from current editor before switching
1705
2031
  const currentText = this.editor.getText();
2032
+ this.disposeActiveSelector();
1706
2033
  this.editorContainer.clear();
1707
2034
  if (factory) {
1708
2035
  // Create the custom editor with tui, theme, and keybindings
@@ -1719,6 +2046,9 @@ export class InteractiveMode {
1719
2046
  if (newEditor.setPaddingX !== undefined) {
1720
2047
  newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1721
2048
  }
2049
+ if (newEditor.setAutocompleteMaxVisible !== undefined) {
2050
+ newEditor.setAutocompleteMaxVisible(this.defaultEditor.getAutocompleteMaxVisible());
2051
+ }
1722
2052
  // Set autocomplete if supported
1723
2053
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1724
2054
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1823,6 +2153,7 @@ export class InteractiveMode {
1823
2153
  options?.onHandle?.(handle);
1824
2154
  }
1825
2155
  else {
2156
+ this.disposeActiveSelector();
1826
2157
  this.editorContainer.clear();
1827
2158
  this.editorContainer.addChild(component);
1828
2159
  this.ui.setFocus(component);
@@ -1865,7 +2196,7 @@ export class InteractiveMode {
1865
2196
  // Set up handlers on defaultEditor - they use this.editor for text access
1866
2197
  // so they work correctly regardless of which editor is active
1867
2198
  this.defaultEditor.onEscape = () => {
1868
- if (this.loadingAnimation) {
2199
+ if (this.session.isStreaming) {
1869
2200
  this.restoreQueuedMessagesToEditor({ abort: true });
1870
2201
  }
1871
2202
  else if (this.session.isBashRunning) {
@@ -1908,7 +2239,8 @@ export class InteractiveMode {
1908
2239
  this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1909
2240
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1910
2241
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1911
- this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2242
+ this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2243
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true }));
1912
2244
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
1913
2245
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
1914
2246
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -1922,31 +2254,55 @@ export class InteractiveMode {
1922
2254
  this.updateEditorBorderColor();
1923
2255
  }
1924
2256
  };
1925
- // Handle clipboard image paste (triggered on Ctrl+V)
2257
+ // Handle clipboard paste (triggered on Ctrl+V). Images are attached by path;
2258
+ // otherwise, paste plain text from the system clipboard.
1926
2259
  this.defaultEditor.onPasteImage = () => {
1927
- this.handleClipboardImagePaste();
2260
+ void this.handleClipboardPaste();
1928
2261
  };
1929
2262
  }
1930
- async handleClipboardImagePaste() {
2263
+ async handleRightClickPaste() {
2264
+ const target = this.renderer.getFocusedComponent();
2265
+ const handleInput = target?.handleInput;
2266
+ if (!target || !handleInput)
2267
+ return;
2268
+ try {
2269
+ const text = await readClipboardText();
2270
+ if (!text || this.renderer.getFocusedComponent() !== target)
2271
+ return;
2272
+ handleInput.call(target, `\x1b[200~${text}\x1b[201~`);
2273
+ this.ui.requestRender();
2274
+ }
2275
+ catch {
2276
+ // Silently ignore clipboard errors (may not have permission, etc.)
2277
+ }
2278
+ }
2279
+ async handleClipboardPaste() {
1931
2280
  try {
1932
2281
  const image = await readClipboardImage();
1933
- if (!image) {
2282
+ if (image) {
2283
+ const tmpDir = os.tmpdir();
2284
+ const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2285
+ const fileName = `mcpi-clipboard-${crypto.randomUUID()}.${ext}`;
2286
+ const filePath = path.join(tmpDir, fileName);
2287
+ fs.writeFileSync(filePath, Buffer.from(image.bytes));
2288
+ this.editor.insertTextAtCursor?.(filePath);
2289
+ this.ui.requestRender();
1934
2290
  return;
1935
2291
  }
1936
- // Write to temp file
1937
- const tmpDir = os.tmpdir();
1938
- const ext = extensionForImageMimeType(image.mimeType) ?? "png";
1939
- const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
1940
- const filePath = path.join(tmpDir, fileName);
1941
- fs.writeFileSync(filePath, Buffer.from(image.bytes));
1942
- // Insert file path directly
1943
- this.editor.insertTextAtCursor?.(filePath);
1944
- this.ui.requestRender();
2292
+ const text = await readClipboardText();
2293
+ if (text) {
2294
+ this.editor.insertTextAtCursor?.(text);
2295
+ this.ui.requestRender();
2296
+ }
1945
2297
  }
1946
2298
  catch {
1947
2299
  // Silently ignore clipboard errors (may not have permission, etc.)
1948
2300
  }
1949
2301
  }
2302
+ handleStartupSubmit(text) {
2303
+ this.editor.setText(text);
2304
+ this.showStatus("Startup is still in progress");
2305
+ }
1950
2306
  setupEditorSubmitHandler() {
1951
2307
  this.defaultEditor.onSubmit = async (text) => {
1952
2308
  text = text.trim();
@@ -2024,9 +2380,15 @@ export class InteractiveMode {
2024
2380
  this.editor.setText("");
2025
2381
  return;
2026
2382
  }
2027
- if (text === "/login") {
2028
- this.showOAuthSelector("login");
2383
+ if (text === "/trust") {
2384
+ this.showTrustSelector();
2385
+ this.editor.setText("");
2386
+ return;
2387
+ }
2388
+ if (text === "/login" || text.startsWith("/login ")) {
2389
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2029
2390
  this.editor.setText("");
2391
+ await this.handleLoginCommand(providerRef);
2030
2392
  return;
2031
2393
  }
2032
2394
  if (text === "/logout") {
@@ -2060,11 +2422,6 @@ export class InteractiveMode {
2060
2422
  this.editor.setText("");
2061
2423
  return;
2062
2424
  }
2063
- if (text === "/dementedelves") {
2064
- this.handleDementedDelves();
2065
- this.editor.setText("");
2066
- return;
2067
- }
2068
2425
  if (text === "/resume") {
2069
2426
  this.showSessionSelector();
2070
2427
  this.editor.setText("");
@@ -2120,6 +2477,9 @@ export class InteractiveMode {
2120
2477
  if (this.onInputCallback) {
2121
2478
  this.onInputCallback(text);
2122
2479
  }
2480
+ else {
2481
+ this.pendingUserInputs.push(text);
2482
+ }
2123
2483
  this.editor.addToHistory?.(text);
2124
2484
  };
2125
2485
  }
@@ -2135,6 +2495,7 @@ export class InteractiveMode {
2135
2495
  this.footer.invalidate();
2136
2496
  switch (event.type) {
2137
2497
  case "agent_start":
2498
+ this.pendingTools.clear();
2138
2499
  if (this.settingsManager.getShowTerminalProgress()) {
2139
2500
  this.ui.terminal.setProgress(true);
2140
2501
  }
@@ -2144,26 +2505,33 @@ export class InteractiveMode {
2144
2505
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2145
2506
  this.retryEscapeHandler = undefined;
2146
2507
  }
2147
- if (this.retryCountdown) {
2148
- this.retryCountdown.dispose();
2149
- this.retryCountdown = undefined;
2150
- }
2151
- if (this.retryLoader) {
2152
- this.retryLoader.stop();
2153
- this.retryLoader = undefined;
2508
+ if (this.workingVisible) {
2509
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
2154
2510
  }
2155
- if (this.loadingAnimation) {
2156
- this.loadingAnimation.stop();
2511
+ else {
2512
+ this.clearStatusIndicator();
2157
2513
  }
2158
- this.statusContainer.clear();
2159
- this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.workingMessage || this.defaultWorkingMessage, this.workingIndicatorOptions);
2160
- this.statusContainer.addChild(this.loadingAnimation);
2161
2514
  this.ui.requestRender();
2162
2515
  break;
2163
2516
  case "queue_update":
2164
2517
  this.updatePendingMessagesDisplay();
2165
2518
  this.ui.requestRender();
2166
2519
  break;
2520
+ case "entry_appended":
2521
+ if (event.entry.type === "custom") {
2522
+ this.addCustomEntryToChat(event.entry);
2523
+ this.ui.requestRender();
2524
+ }
2525
+ break;
2526
+ case "session_info_changed":
2527
+ this.updateTerminalTitle();
2528
+ this.footer.invalidate();
2529
+ this.ui.requestRender();
2530
+ break;
2531
+ case "thinking_level_changed":
2532
+ this.footer.invalidate();
2533
+ this.updateEditorBorderColor();
2534
+ break;
2167
2535
  case "message_start":
2168
2536
  if (event.message.role === "custom") {
2169
2537
  this.addMessageToChat(event.message);
@@ -2175,17 +2543,17 @@ export class InteractiveMode {
2175
2543
  this.ui.requestRender();
2176
2544
  }
2177
2545
  else if (event.message.role === "assistant") {
2178
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2546
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2179
2547
  this.streamingMessage = event.message;
2180
2548
  this.chatContainer.addChild(this.streamingComponent);
2181
- this.streamingComponent.updateContent(this.streamingMessage);
2549
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2182
2550
  this.ui.requestRender();
2183
2551
  }
2184
2552
  break;
2185
2553
  case "message_update":
2186
2554
  if (this.streamingComponent && event.message.role === "assistant") {
2187
2555
  this.streamingMessage = event.message;
2188
- this.streamingComponent.updateContent(this.streamingMessage);
2556
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2189
2557
  for (const content of this.streamingMessage.content) {
2190
2558
  if (content.type === "toolCall") {
2191
2559
  if (!this.pendingTools.has(content.id)) {
@@ -2222,7 +2590,7 @@ export class InteractiveMode {
2222
2590
  : "Operation aborted";
2223
2591
  this.streamingMessage.errorMessage = errorMessage;
2224
2592
  }
2225
- this.streamingComponent.updateContent(this.streamingMessage);
2593
+ this.streamingComponent.updateContent(this.streamingMessage, false);
2226
2594
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2227
2595
  if (!errorMessage) {
2228
2596
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2240,6 +2608,7 @@ export class InteractiveMode {
2240
2608
  for (const [, component] of this.pendingTools.entries()) {
2241
2609
  component.setArgsComplete();
2242
2610
  }
2611
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2243
2612
  }
2244
2613
  this.streamingComponent = undefined;
2245
2614
  this.streamingMessage = undefined;
@@ -2247,6 +2616,9 @@ export class InteractiveMode {
2247
2616
  }
2248
2617
  this.ui.requestRender();
2249
2618
  break;
2619
+ case "bash_execution_update":
2620
+ // The bash execution callback handles TUI output rendering.
2621
+ break;
2250
2622
  case "tool_execution_start": {
2251
2623
  let component = this.pendingTools.get(event.toolCallId);
2252
2624
  if (!component) {
@@ -2283,20 +2655,18 @@ export class InteractiveMode {
2283
2655
  if (this.settingsManager.getShowTerminalProgress()) {
2284
2656
  this.ui.terminal.setProgress(false);
2285
2657
  }
2286
- if (this.loadingAnimation) {
2287
- this.loadingAnimation.stop();
2288
- this.loadingAnimation = undefined;
2289
- this.statusContainer.clear();
2290
- }
2658
+ this.clearStatusIndicator("working");
2291
2659
  if (this.streamingComponent) {
2292
2660
  this.chatContainer.removeChild(this.streamingComponent);
2293
2661
  this.streamingComponent = undefined;
2294
2662
  this.streamingMessage = undefined;
2295
2663
  }
2296
2664
  this.pendingTools.clear();
2297
- await this.checkShutdownRequested();
2298
2665
  this.ui.requestRender();
2299
2666
  break;
2667
+ case "agent_settled":
2668
+ await this.checkShutdownRequested();
2669
+ break;
2300
2670
  case "compaction_start": {
2301
2671
  if (this.settingsManager.getShowTerminalProgress()) {
2302
2672
  this.ui.terminal.setProgress(true);
@@ -2306,13 +2676,7 @@ export class InteractiveMode {
2306
2676
  this.defaultEditor.onEscape = () => {
2307
2677
  this.session.abortCompaction();
2308
2678
  };
2309
- this.statusContainer.clear();
2310
- const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2311
- const label = event.reason === "manual"
2312
- ? `Compacting context... ${cancelHint}`
2313
- : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2314
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
2315
- this.statusContainer.addChild(this.autoCompactionLoader);
2679
+ this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2316
2680
  this.ui.requestRender();
2317
2681
  break;
2318
2682
  }
@@ -2324,11 +2688,7 @@ export class InteractiveMode {
2324
2688
  this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2325
2689
  this.autoCompactionEscapeHandler = undefined;
2326
2690
  }
2327
- if (this.autoCompactionLoader) {
2328
- this.autoCompactionLoader.stop();
2329
- this.autoCompactionLoader = undefined;
2330
- this.statusContainer.clear();
2331
- }
2691
+ this.clearStatusIndicator("compaction");
2332
2692
  if (event.aborted) {
2333
2693
  if (event.reason === "manual") {
2334
2694
  this.showError("Compaction cancelled");
@@ -2362,17 +2722,7 @@ export class InteractiveMode {
2362
2722
  this.defaultEditor.onEscape = () => {
2363
2723
  this.session.abortRetry();
2364
2724
  };
2365
- // Show retry indicator
2366
- this.statusContainer.clear();
2367
- this.retryCountdown?.dispose();
2368
- const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2369
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2370
- this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
2371
- this.retryLoader?.setMessage(retryMessage(seconds));
2372
- }, () => {
2373
- this.retryCountdown = undefined;
2374
- });
2375
- this.statusContainer.addChild(this.retryLoader);
2725
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2376
2726
  this.ui.requestRender();
2377
2727
  break;
2378
2728
  }
@@ -2382,16 +2732,7 @@ export class InteractiveMode {
2382
2732
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2383
2733
  this.retryEscapeHandler = undefined;
2384
2734
  }
2385
- if (this.retryCountdown) {
2386
- this.retryCountdown.dispose();
2387
- this.retryCountdown = undefined;
2388
- }
2389
- // Stop loader
2390
- if (this.retryLoader) {
2391
- this.retryLoader.stop();
2392
- this.retryLoader = undefined;
2393
- this.statusContainer.clear();
2394
- }
2735
+ this.clearStatusIndicator("retry");
2395
2736
  // Show error only on final failure (success shows normal response)
2396
2737
  if (!event.success) {
2397
2738
  this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
@@ -2399,6 +2740,28 @@ export class InteractiveMode {
2399
2740
  this.ui.requestRender();
2400
2741
  break;
2401
2742
  }
2743
+ case "summarization_retry_scheduled": {
2744
+ this.showError(event.errorMessage);
2745
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2746
+ this.ui.requestRender();
2747
+ break;
2748
+ }
2749
+ case "summarization_retry_attempt_start": {
2750
+ this.clearStatusIndicator("retry");
2751
+ if (event.source === "branchSummary") {
2752
+ this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
2753
+ }
2754
+ else {
2755
+ this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2756
+ }
2757
+ this.ui.requestRender();
2758
+ break;
2759
+ }
2760
+ case "summarization_retry_finished": {
2761
+ this.clearStatusIndicator("retry");
2762
+ this.ui.requestRender();
2763
+ break;
2764
+ }
2402
2765
  }
2403
2766
  }
2404
2767
  /** Extract text content from a user message */
@@ -2410,6 +2773,19 @@ export class InteractiveMode {
2410
2773
  : message.content.filter((c) => c.type === "text");
2411
2774
  return textBlocks.map((c) => c.text).join("");
2412
2775
  }
2776
+ /** Show a managed-tool status update in the chat. */
2777
+ showManagedToolStatus(status) {
2778
+ if (!this.managedToolStatusStarted) {
2779
+ this.chatContainer.addChild(new Spacer(1));
2780
+ this.managedToolStatusStarted = true;
2781
+ }
2782
+ const message = status.type === "warning" ? `Warning: ${status.message}` : status.message;
2783
+ const color = status.type === "warning" ? "warning" : "dim";
2784
+ this.chatContainer.addChild(new Text(theme.fg(color, message), 1, 0));
2785
+ this.lastStatusSpacer = undefined;
2786
+ this.lastStatusText = undefined;
2787
+ this.ui.requestRender();
2788
+ }
2413
2789
  /**
2414
2790
  * Show a status message in the chat.
2415
2791
  *
@@ -2433,6 +2809,25 @@ export class InteractiveMode {
2433
2809
  this.lastStatusText = text;
2434
2810
  this.ui.requestRender();
2435
2811
  }
2812
+ addCustomEntryToChat(entry) {
2813
+ const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
2814
+ if (!renderer) {
2815
+ return;
2816
+ }
2817
+ const component = new CustomEntryComponent(entry, renderer);
2818
+ component.setExpanded(this.toolOutputExpanded);
2819
+ if (!component.hasContent()) {
2820
+ return;
2821
+ }
2822
+ if (this.streamingComponent) {
2823
+ const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
2824
+ if (streamingIndex >= 0) {
2825
+ this.chatContainer.children.splice(streamingIndex, 0, component);
2826
+ return;
2827
+ }
2828
+ }
2829
+ this.chatContainer.addChild(component);
2830
+ }
2436
2831
  addMessageToChat(message, options) {
2437
2832
  switch (message.role) {
2438
2833
  case "bashExecution": {
@@ -2447,7 +2842,7 @@ export class InteractiveMode {
2447
2842
  case "custom": {
2448
2843
  if (message.display) {
2449
2844
  const renderer = this.session.extensionRunner.getMessageRenderer(message.customType);
2450
- const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
2845
+ const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings(), this.outputPad);
2451
2846
  component.setExpanded(this.toolOutputExpanded);
2452
2847
  this.chatContainer.addChild(component);
2453
2848
  }
@@ -2481,12 +2876,13 @@ export class InteractiveMode {
2481
2876
  this.chatContainer.addChild(component);
2482
2877
  // Render user message separately if present
2483
2878
  if (skillBlock.userMessage) {
2484
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
2879
+ this.chatContainer.addChild(new Spacer(1));
2880
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2485
2881
  this.chatContainer.addChild(userComponent);
2486
2882
  }
2487
2883
  }
2488
2884
  else {
2489
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
2885
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2490
2886
  this.chatContainer.addChild(userComponent);
2491
2887
  }
2492
2888
  if (options?.populateHistory) {
@@ -2496,7 +2892,7 @@ export class InteractiveMode {
2496
2892
  break;
2497
2893
  }
2498
2894
  case "assistant": {
2499
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2895
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2500
2896
  this.chatContainer.addChild(assistantComponent);
2501
2897
  break;
2502
2898
  }
@@ -2509,19 +2905,24 @@ export class InteractiveMode {
2509
2905
  }
2510
2906
  }
2511
2907
  }
2512
- /**
2513
- * Render session context to chat. Used for initial load and rebuild after compaction.
2514
- * @param sessionContext Session context to render
2515
- * @param options.updateFooter Update footer state
2516
- * @param options.populateHistory Add user messages to editor history
2517
- */
2518
- renderSessionContext(sessionContext, options = {}) {
2908
+ renderSessionItems(items, options = {}) {
2519
2909
  this.pendingTools.clear();
2910
+ const renderedPendingTools = new Map();
2911
+ // Cache-miss notices are not persisted; re-derive them from the full entry
2912
+ // list and re-inject them after the assistant messages that paid for them.
2913
+ const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2914
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
2915
+ : new Map();
2520
2916
  if (options.updateFooter) {
2521
2917
  this.footer.invalidate();
2522
2918
  this.updateEditorBorderColor();
2523
2919
  }
2524
- for (const message of sessionContext.messages) {
2920
+ for (const item of items) {
2921
+ if (isCustomSessionEntry(item)) {
2922
+ this.addCustomEntryToChat(item);
2923
+ continue;
2924
+ }
2925
+ const message = item;
2525
2926
  // Assistant messages need special handling for tool calls
2526
2927
  if (message.role === "assistant") {
2527
2928
  this.addMessageToChat(message);
@@ -2549,17 +2950,22 @@ export class InteractiveMode {
2549
2950
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2550
2951
  }
2551
2952
  else {
2552
- this.pendingTools.set(content.id, component);
2953
+ renderedPendingTools.set(content.id, component);
2553
2954
  }
2554
2955
  }
2555
2956
  }
2957
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2958
+ const miss = cacheMisses.get(message);
2959
+ if (miss)
2960
+ this.addCacheMissNotice(miss);
2961
+ }
2556
2962
  }
2557
2963
  else if (message.role === "toolResult") {
2558
2964
  // Match tool results to pending tool components
2559
- const component = this.pendingTools.get(message.toolCallId);
2965
+ const component = renderedPendingTools.get(message.toolCallId);
2560
2966
  if (component) {
2561
2967
  component.updateResult(message);
2562
- this.pendingTools.delete(message.toolCallId);
2968
+ renderedPendingTools.delete(message.toolCallId);
2563
2969
  }
2564
2970
  }
2565
2971
  else {
@@ -2567,16 +2973,62 @@ export class InteractiveMode {
2567
2973
  this.addMessageToChat(message, options);
2568
2974
  }
2569
2975
  }
2570
- this.pendingTools.clear();
2976
+ for (const [toolCallId, component] of renderedPendingTools) {
2977
+ this.pendingTools.set(toolCallId, component);
2978
+ }
2571
2979
  this.ui.requestRender();
2572
2980
  }
2981
+ /**
2982
+ * Render session entries to chat. Used for initial load and rebuild after compaction.
2983
+ * @param entries Compaction-aware session entries to render
2984
+ * @param options.updateFooter Update footer state
2985
+ * @param options.populateHistory Add user messages to editor history
2986
+ */
2987
+ renderSessionEntries(entries, options = {}) {
2988
+ const items = entries.flatMap((entry) => {
2989
+ if (entry.type === "custom") {
2990
+ return [entry];
2991
+ }
2992
+ return sessionEntryToContextMessages(entry);
2993
+ });
2994
+ this.renderSessionItems(items, options);
2995
+ }
2996
+ /**
2997
+ * Show a transcript notice when a completed assistant message paid for a
2998
+ * significant cache miss. Only states observable facts: the miss itself,
2999
+ * a model switch, or an idle gap past the cache TTL.
3000
+ */
3001
+ maybeShowCacheMissNotice(message) {
3002
+ if (!this.settingsManager.getShowCacheMissNotices())
3003
+ return;
3004
+ // Entries don't contain `message` yet: message_end fires before persistence.
3005
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
3006
+ if (miss)
3007
+ this.addCacheMissNotice(miss);
3008
+ }
3009
+ addCacheMissNotice(miss) {
3010
+ if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
3011
+ return;
3012
+ const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
3013
+ const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
3014
+ let label = "Cache miss";
3015
+ if (miss.modelChanged) {
3016
+ label = "Cache miss after model switch";
3017
+ }
3018
+ else if (miss.idleMs >= CACHE_TTL_MS) {
3019
+ label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
3020
+ }
3021
+ const text = theme.fg("warning", `${label}: ${reBilled}`);
3022
+ this.chatContainer.addChild(new Spacer(1));
3023
+ this.chatContainer.addChild(new Text(text, 1, 0));
3024
+ }
2573
3025
  renderInitialMessages() {
2574
- // Get aligned messages and entries from session context
2575
- const context = this.sessionManager.buildSessionContext();
2576
- this.renderSessionContext(context, {
3026
+ const entries = this.sessionManager.buildContextEntries();
3027
+ this.renderSessionEntries(entries, {
2577
3028
  updateFooter: true,
2578
3029
  populateHistory: true,
2579
3030
  });
3031
+ this.renderProjectTrustWarningIfNeeded();
2580
3032
  // Show compaction info if session was compacted
2581
3033
  const allEntries = this.sessionManager.getEntries();
2582
3034
  const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
@@ -2585,7 +3037,20 @@ export class InteractiveMode {
2585
3037
  this.showStatus(`Session compacted ${times}`);
2586
3038
  }
2587
3039
  }
3040
+ renderProjectTrustWarningIfNeeded() {
3041
+ if (this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(this.sessionManager.getCwd())) {
3042
+ return;
3043
+ }
3044
+ if (this.chatContainer.children.length > 0) {
3045
+ this.chatContainer.addChild(new Spacer(1));
3046
+ }
3047
+ this.chatContainer.addChild(new Text(theme.fg("warning", `This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart mcpi.`), 1, 0));
3048
+ }
2588
3049
  async getUserInput() {
3050
+ const queuedInput = this.pendingUserInputs.shift();
3051
+ if (queuedInput !== undefined) {
3052
+ return queuedInput;
3053
+ }
2589
3054
  return new Promise((resolve) => {
2590
3055
  this.onInputCallback = (text) => {
2591
3056
  this.onInputCallback = undefined;
@@ -2595,8 +3060,7 @@ export class InteractiveMode {
2595
3060
  }
2596
3061
  rebuildChatFromMessages() {
2597
3062
  this.chatContainer.clear();
2598
- const context = this.sessionManager.buildSessionContext();
2599
- this.renderSessionContext(context);
3063
+ this.renderSessionEntries(this.sessionManager.buildContextEntries());
2600
3064
  }
2601
3065
  // =========================================================================
2602
3066
  // Key handlers
@@ -2621,20 +3085,84 @@ export class InteractiveMode {
2621
3085
  * repaint the final frame while the process is exiting.
2622
3086
  */
2623
3087
  isShuttingDown = false;
2624
- async shutdown() {
3088
+ async shutdown(options) {
2625
3089
  if (this.isShuttingDown)
2626
3090
  return;
2627
3091
  this.isShuttingDown = true;
2628
- this.unregisterSignalHandlers();
3092
+ // Keep signal handlers registered until terminal cleanup has completed.
3093
+ // `signal-exit` checks the listener list during the same SIGTERM/SIGHUP
3094
+ // dispatch and re-sends the signal if only its own listeners remain.
3095
+ if (options?.fromSignal) {
3096
+ // Signal-triggered shutdown (SIGTERM/SIGHUP). Emit extension cleanup
3097
+ // (session_shutdown) BEFORE touching the terminal. Extension teardown
3098
+ // such as removing sockets does not write to the tty, so it must not be
3099
+ // skipped if a later terminal-restore write fails on a dead or stalled
3100
+ // terminal. If the terminal is gone, the restore writes below emit EIO,
3101
+ // which the stdout/stderr error handler turns into emergencyTerminalExit;
3102
+ // the render loop is already idle, so this cannot hot-spin (see #4144).
3103
+ await this.runtimeHost.dispose();
3104
+ this.themeController.disableAutoSync();
3105
+ await this.ui.terminal.drainInput(1000);
3106
+ this.stop();
3107
+ process.exit(0);
3108
+ }
3109
+ // Interactive quit (Ctrl+D, Ctrl+C, /quit, extension shutdown()). Stop the
3110
+ // TUI before emitting shutdown events so extension UI cleanup cannot repaint
3111
+ // the final frame while the process is exiting.
2629
3112
  // Drain any in-flight Kitty key release events before stopping.
2630
3113
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
3114
+ this.themeController.disableAutoSync();
2631
3115
  await this.ui.terminal.drainInput(1000);
2632
3116
  this.stop();
2633
3117
  await this.runtimeHost.dispose();
3118
+ const resumeCommand = formatResumeCommand(this.sessionManager);
3119
+ if (resumeCommand) {
3120
+ process.stdout.write(`${chalk.dim("To resume this session:")} ${resumeCommand}\n`);
3121
+ }
2634
3122
  process.exit(0);
2635
3123
  }
3124
+ emergencyTerminalExit() {
3125
+ this.isShuttingDown = true;
3126
+ this.unregisterSignalHandlers();
3127
+ killTrackedDetachedChildren();
3128
+ // The terminal is gone. Do not run normal shutdown because TUI and
3129
+ // extension cleanup can write restore sequences and re-trigger EIO.
3130
+ process.exit(129);
3131
+ }
2636
3132
  /**
2637
- * Check if shutdown was requested and perform shutdown if so.
3133
+ * Last-resort handler for uncaught exceptions. The TUI puts stdin into raw
3134
+ * mode and hides the cursor; without this handler, an uncaught throw from
3135
+ * anywhere (e.g. an extension's async `ChildProcess.on("exit")` callback)
3136
+ * tears down the process while leaving the terminal in raw mode with no
3137
+ * cursor, requiring `stty sane && reset` to recover.
3138
+ *
3139
+ * Unlike emergencyTerminalExit, the terminal is still alive here, so we
3140
+ * call ui.stop() to restore cooked mode, the cursor, and disable bracketed
3141
+ * paste / Kitty / modifyOtherKeys sequences.
3142
+ */
3143
+ uncaughtCrash(error) {
3144
+ if (this.isShuttingDown) {
3145
+ process.exit(1);
3146
+ }
3147
+ this.isShuttingDown = true;
3148
+ try {
3149
+ this.unregisterSignalHandlers();
3150
+ }
3151
+ catch { }
3152
+ try {
3153
+ killTrackedDetachedChildren();
3154
+ }
3155
+ catch { }
3156
+ try {
3157
+ this.ui.stop();
3158
+ }
3159
+ catch { }
3160
+ console.error(`${APP_NAME} exiting due to uncaughtException:`);
3161
+ console.error(error);
3162
+ process.exit(1);
3163
+ }
3164
+ /**
3165
+ * Check if shutdown was requested and perform shutdown if so.
2638
3166
  */
2639
3167
  async checkShutdownRequested() {
2640
3168
  if (!this.shutdownRequested)
@@ -2649,12 +3177,32 @@ export class InteractiveMode {
2649
3177
  }
2650
3178
  for (const signal of signals) {
2651
3179
  const handler = () => {
3180
+ // SIGHUP no longer hard-exits: graceful shutdown emits session_shutdown
3181
+ // first, then attempts terminal restore. A genuinely dead terminal
3182
+ // surfaces as an EIO on the restore writes, which the stdout/stderr
3183
+ // error handler converts into emergencyTerminalExit (see #4144, #5080).
2652
3184
  killTrackedDetachedChildren();
2653
- void this.shutdown();
3185
+ void this.shutdown({ fromSignal: true });
2654
3186
  };
2655
- process.on(signal, handler);
3187
+ process.prependListener(signal, handler);
2656
3188
  this.signalCleanupHandlers.push(() => process.off(signal, handler));
2657
3189
  }
3190
+ const terminalErrorHandler = (error) => {
3191
+ if (isDeadTerminalError(error)) {
3192
+ this.emergencyTerminalExit();
3193
+ }
3194
+ throw error;
3195
+ };
3196
+ process.stdout.on("error", terminalErrorHandler);
3197
+ process.stderr.on("error", terminalErrorHandler);
3198
+ this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
3199
+ this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
3200
+ // Restore the terminal before the process dies on any uncaught throw.
3201
+ // Without this, an unhandled exception from extension code (or anywhere
3202
+ // in mcpi) leaves the terminal in raw mode with no cursor.
3203
+ const uncaughtExceptionHandler = (error) => this.uncaughtCrash(error);
3204
+ process.prependListener("uncaughtException", uncaughtExceptionHandler);
3205
+ this.signalCleanupHandlers.push(() => process.off("uncaughtException", uncaughtExceptionHandler));
2658
3206
  }
2659
3207
  unregisterSignalHandlers() {
2660
3208
  for (const cleanup of this.signalCleanupHandlers) {
@@ -2721,6 +3269,7 @@ export class InteractiveMode {
2721
3269
  }
2722
3270
  // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
2723
3271
  else if (this.editor.onSubmit) {
3272
+ this.editor.setText("");
2724
3273
  this.editor.onSubmit(text);
2725
3274
  }
2726
3275
  }
@@ -2777,17 +3326,21 @@ export class InteractiveMode {
2777
3326
  this.setToolsExpanded(!this.toolOutputExpanded);
2778
3327
  }
2779
3328
  setToolsExpanded(expanded) {
3329
+ if (expanded === this.toolOutputExpanded)
3330
+ return;
2780
3331
  this.toolOutputExpanded = expanded;
2781
3332
  const activeHeader = this.customHeader ?? this.builtInHeader;
2782
3333
  if (isExpandable(activeHeader)) {
2783
3334
  activeHeader.setExpanded(expanded);
2784
3335
  }
2785
- for (const child of this.chatContainer.children) {
2786
- if (isExpandable(child)) {
2787
- child.setExpanded(expanded);
3336
+ for (const container of [this.loadedResourcesContainer, this.chatContainer]) {
3337
+ for (const child of container.children) {
3338
+ if (isExpandable(child)) {
3339
+ child.setExpanded(expanded);
3340
+ }
2788
3341
  }
2789
3342
  }
2790
- this.ui.requestRender();
3343
+ this.showStatus(`Tool output: ${expanded ? "expanded" : "collapsed"}`);
2791
3344
  }
2792
3345
  toggleThinkingBlockVisibility() {
2793
3346
  this.hideThinkingBlock = !this.hideThinkingBlock;
@@ -2803,45 +3356,21 @@ export class InteractiveMode {
2803
3356
  }
2804
3357
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2805
3358
  }
2806
- openExternalEditor() {
2807
- // Determine editor (respect $VISUAL, then $EDITOR)
2808
- const editorCmd = process.env.VISUAL || process.env.EDITOR;
2809
- if (!editorCmd) {
2810
- this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
2811
- return;
2812
- }
2813
- const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
2814
- const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
3359
+ async handleOpenExternalEditor() {
3360
+ const editorCmd = this.settingsManager.getExternalEditorCommand();
3361
+ const content = this.editor.getExpandedText?.() ?? this.editor.getText();
3362
+ this.ui.stop();
2815
3363
  try {
2816
- // Write current content to temp file
2817
- fs.writeFileSync(tmpFile, currentText, "utf-8");
2818
- // Stop TUI to release terminal
2819
- this.ui.stop();
2820
- // Split by space to support editor arguments (e.g., "code --wait")
2821
- const [editor, ...editorArgs] = editorCmd.split(" ");
2822
- // Spawn editor synchronously with inherited stdio for interactive editing
2823
- const result = spawnSync(editor, [...editorArgs, tmpFile], {
2824
- stdio: "inherit",
2825
- shell: process.platform === "win32",
3364
+ const result = await editInExternalEditor({
3365
+ command: editorCmd,
3366
+ content,
2826
3367
  });
2827
- // On successful exit (status 0), replace editor content
2828
- if (result.status === 0) {
2829
- const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
2830
- this.editor.setText(newContent);
3368
+ if (result.status === "complete") {
3369
+ this.editor.setText(result.content);
2831
3370
  }
2832
- // On non-zero exit, keep original text (no action needed)
2833
3371
  }
2834
3372
  finally {
2835
- // Clean up temp file
2836
- try {
2837
- fs.unlinkSync(tmpFile);
2838
- }
2839
- catch {
2840
- // Ignore cleanup errors
2841
- }
2842
- // Restart TUI
2843
3373
  this.ui.start();
2844
- // Force full re-render since external editor uses alternate screen
2845
3374
  this.ui.requestRender(true);
2846
3375
  }
2847
3376
  }
@@ -2854,7 +3383,7 @@ export class InteractiveMode {
2854
3383
  }
2855
3384
  showError(errorMessage) {
2856
3385
  this.chatContainer.addChild(new Spacer(1));
2857
- this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3386
+ this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), this.outputPad, 0));
2858
3387
  this.ui.requestRender();
2859
3388
  }
2860
3389
  showWarning(warningMessage) {
@@ -2862,19 +3391,23 @@ export class InteractiveMode {
2862
3391
  this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
2863
3392
  this.ui.requestRender();
2864
3393
  }
2865
- showNewVersionNotification(newVersion) {
2866
- const action = theme.fg("accent", getUpdateInstruction("@sammorrowdrums/mcpi"));
2867
- const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2868
- const changelogUrl = theme.fg("accent", "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
2869
- const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
3394
+ showNewVersionNotification(release) {
3395
+ const action = theme.fg("accent", `${APP_NAME} update`);
3396
+ const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
3397
+ const changelogUrl = "https://github.com/SamMorrowDrums/mcpi/releases";
3398
+ const changelogLink = getCapabilities().hyperlinks
3399
+ ? hyperlink(theme.fg("accent", changelogUrl), changelogUrl)
3400
+ : theme.fg("accent", changelogUrl);
3401
+ const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
2870
3402
  this.chatContainer.addChild(new Spacer(1));
2871
3403
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2872
- this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
3404
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0));
3405
+ this.chatContainer.addChild(new Text(changelogLine, 1, 0));
2873
3406
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2874
3407
  this.ui.requestRender();
2875
3408
  }
2876
3409
  showPackageUpdateNotification(packages) {
2877
- const action = theme.fg("accent", `${APP_NAME} update`);
3410
+ const action = theme.fg("accent", `${APP_NAME} update --extensions`);
2878
3411
  const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
2879
3412
  const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
2880
3413
  this.chatContainer.addChild(new Spacer(1));
@@ -3016,8 +3549,10 @@ export class InteractiveMode {
3016
3549
  for (const message of preCommands) {
3017
3550
  await this.session.prompt(message.text);
3018
3551
  }
3019
- // Send first prompt (starts streaming)
3020
- const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
3552
+ // Start a prompt when idle, or queue it into a run still finishing compaction.
3553
+ const promptPromise = this.session
3554
+ .prompt(firstPrompt.text, { streamingBehavior: firstPrompt.mode })
3555
+ .catch((error) => {
3021
3556
  restoreQueue(error);
3022
3557
  });
3023
3558
  // Queue remaining messages
@@ -3050,25 +3585,43 @@ export class InteractiveMode {
3050
3585
  // =========================================================================
3051
3586
  // Selectors
3052
3587
  // =========================================================================
3588
+ disposeActiveSelector() {
3589
+ const dispose = this.activeSelectorDispose;
3590
+ this.activeSelectorToken = undefined;
3591
+ this.activeSelectorDispose = undefined;
3592
+ dispose?.();
3593
+ }
3053
3594
  /**
3054
3595
  * Shows a selector component in place of the editor.
3055
3596
  * @param create Factory that receives a `done` callback and returns the component and focus target
3056
3597
  */
3057
3598
  showSelector(create) {
3599
+ const token = {};
3600
+ let dispose;
3058
3601
  const done = () => {
3602
+ dispose?.();
3603
+ if (this.activeSelectorToken !== token)
3604
+ return;
3605
+ this.activeSelectorToken = undefined;
3606
+ this.activeSelectorDispose = undefined;
3059
3607
  this.editorContainer.clear();
3060
3608
  this.editorContainer.addChild(this.editor);
3061
3609
  this.ui.setFocus(this.editor);
3062
3610
  };
3063
- const { component, focus } = create(done);
3611
+ const created = create(done);
3612
+ dispose = created.dispose;
3613
+ this.disposeActiveSelector();
3614
+ this.activeSelectorToken = token;
3615
+ this.activeSelectorDispose = dispose;
3064
3616
  this.editorContainer.clear();
3065
- this.editorContainer.addChild(component);
3066
- this.ui.setFocus(focus);
3617
+ this.editorContainer.addChild(created.component);
3618
+ this.ui.setFocus(created.focus);
3067
3619
  this.ui.requestRender();
3068
3620
  }
3069
3621
  showSettingsSelector() {
3070
3622
  this.showSelector((done) => {
3071
- const selector = new SettingsSelectorComponent({
3623
+ let selector;
3624
+ selector = new SettingsSelectorComponent({
3072
3625
  autoCompact: this.session.autoCompactionEnabled,
3073
3626
  showImages: this.settingsManager.getShowImages(),
3074
3627
  imageWidthCells: this.settingsManager.getImageWidthCells(),
@@ -3078,21 +3631,31 @@ export class InteractiveMode {
3078
3631
  steeringMode: this.session.steeringMode,
3079
3632
  followUpMode: this.session.followUpMode,
3080
3633
  transport: this.settingsManager.getTransport(),
3634
+ httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
3081
3635
  thinkingLevel: this.session.thinkingLevel,
3082
3636
  availableThinkingLevels: this.session.getAvailableThinkingLevels(),
3083
- currentTheme: this.settingsManager.getTheme() || "dark",
3637
+ currentTheme: this.themeController.getThemeSelection() || "dark",
3638
+ terminalTheme: this.themeController.getTerminalTheme(),
3084
3639
  availableThemes: getAvailableThemes(),
3085
3640
  hideThinkingBlock: this.hideThinkingBlock,
3641
+ mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
3086
3642
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3087
3643
  enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
3088
3644
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3089
3645
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3090
3646
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3647
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3648
+ defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3091
3649
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3650
+ outputPad: this.settingsManager.getOutputPad(),
3092
3651
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3093
3652
  quietStartup: this.settingsManager.getQuietStartup(),
3094
3653
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3095
3654
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3655
+ tuiMode: this.ui.mode,
3656
+ fullscreenExitOutput: this.settingsManager.getFullscreenExitOutput(),
3657
+ fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3658
+ warnings: this.settingsManager.getWarnings(),
3096
3659
  }, {
3097
3660
  onAutoCompactChange: (enabled) => {
3098
3661
  this.session.setAutoCompactionEnabled(enabled);
@@ -3134,26 +3697,21 @@ export class InteractiveMode {
3134
3697
  this.settingsManager.setTransport(transport);
3135
3698
  this.session.agent.transport = transport;
3136
3699
  },
3700
+ onHttpIdleTimeoutMsChange: (timeoutMs) => {
3701
+ this.settingsManager.setHttpIdleTimeoutMs(timeoutMs);
3702
+ configureHttpDispatcher(timeoutMs);
3703
+ this.showStatus(`HTTP idle timeout: ${formatHttpIdleTimeoutMs(timeoutMs)}`);
3704
+ },
3137
3705
  onThinkingLevelChange: (level) => {
3138
3706
  this.session.setThinkingLevel(level);
3139
3707
  this.footer.invalidate();
3140
3708
  this.updateEditorBorderColor();
3141
3709
  },
3142
- onThemeChange: (themeName) => {
3143
- const result = setTheme(themeName, true);
3144
- this.settingsManager.setTheme(themeName);
3145
- this.ui.invalidate();
3146
- if (!result.success) {
3147
- this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
3148
- }
3149
- },
3150
- onThemePreview: (themeName) => {
3151
- const result = setTheme(themeName, true);
3152
- if (result.success) {
3153
- this.ui.invalidate();
3154
- this.ui.requestRender();
3155
- }
3710
+ onThemeChange: (themeSetting) => {
3711
+ this.settingsManager.setTheme(themeSetting);
3712
+ void this.themeController.setThemeSetting(themeSetting);
3156
3713
  },
3714
+ onThemePreview: (themeName) => this.themeController.preview(themeName),
3157
3715
  onHideThinkingBlockChange: (hidden) => {
3158
3716
  this.hideThinkingBlock = hidden;
3159
3717
  this.settingsManager.setHideThinkingBlock(hidden);
@@ -3165,6 +3723,15 @@ export class InteractiveMode {
3165
3723
  this.chatContainer.clear();
3166
3724
  this.rebuildChatFromMessages();
3167
3725
  },
3726
+ onMermaidRenderingModeChange: (mode) => {
3727
+ this.settingsManager.setMermaidRenderingMode(mode);
3728
+ this.chatContainer.invalidate();
3729
+ this.ui.requestRender();
3730
+ },
3731
+ onShowCacheMissNoticesChange: (shown) => {
3732
+ this.settingsManager.setShowCacheMissNotices(shown);
3733
+ this.rebuildChatFromMessages();
3734
+ },
3168
3735
  onCollapseChangelogChange: (collapsed) => {
3169
3736
  this.settingsManager.setCollapseChangelog(collapsed);
3170
3737
  },
@@ -3174,6 +3741,9 @@ export class InteractiveMode {
3174
3741
  onQuietStartupChange: (enabled) => {
3175
3742
  this.settingsManager.setQuietStartup(enabled);
3176
3743
  },
3744
+ onDefaultProjectTrustChange: (defaultProjectTrust) => {
3745
+ this.settingsManager.setDefaultProjectTrust(defaultProjectTrust);
3746
+ },
3177
3747
  onDoubleEscapeActionChange: (action) => {
3178
3748
  this.settingsManager.setDoubleEscapeAction(action);
3179
3749
  },
@@ -3191,6 +3761,25 @@ export class InteractiveMode {
3191
3761
  this.editor.setPaddingX(padding);
3192
3762
  }
3193
3763
  },
3764
+ onOutputPadChange: (padding) => {
3765
+ this.settingsManager.setOutputPad(padding);
3766
+ this.outputPad = padding;
3767
+ if (this.streamingComponent || this.session.isStreaming) {
3768
+ for (const child of this.chatContainer.children) {
3769
+ if (child instanceof AssistantMessageComponent ||
3770
+ child instanceof CustomMessageComponent ||
3771
+ child instanceof UserMessageComponent) {
3772
+ child.setOutputPad(padding);
3773
+ }
3774
+ }
3775
+ if (this.streamingComponent) {
3776
+ this.streamingComponent.setOutputPad(padding);
3777
+ }
3778
+ this.ui.requestRender();
3779
+ return;
3780
+ }
3781
+ this.rebuildChatFromMessages();
3782
+ },
3194
3783
  onAutocompleteMaxVisibleChange: (maxVisible) => {
3195
3784
  this.settingsManager.setAutocompleteMaxVisible(maxVisible);
3196
3785
  this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
@@ -3201,10 +3790,34 @@ export class InteractiveMode {
3201
3790
  onClearOnShrinkChange: (enabled) => {
3202
3791
  this.settingsManager.setClearOnShrink(enabled);
3203
3792
  this.ui.setClearOnShrink(enabled);
3793
+ if (!enabled && !this.activeStatusIndicator) {
3794
+ this.statusContainer.clear();
3795
+ }
3204
3796
  },
3205
3797
  onShowTerminalProgressChange: (enabled) => {
3206
3798
  this.settingsManager.setShowTerminalProgress(enabled);
3207
3799
  },
3800
+ onTuiModeChange: (mode) => {
3801
+ if (!this.switchTuiMode(mode)) {
3802
+ selector?.getSettingsList().updateValue("tui-mode", this.ui.mode);
3803
+ this.showStatus("Close active overlays before changing TUI mode");
3804
+ return;
3805
+ }
3806
+ this.settingsManager.setTuiMode(mode);
3807
+ if (!this.activeStatusIndicator)
3808
+ this.statusContainer.clear();
3809
+ this.showStatus(`TUI mode: ${mode}`);
3810
+ },
3811
+ onFullscreenExitOutputChange: (output) => {
3812
+ this.settingsManager.setFullscreenExitOutput(output);
3813
+ },
3814
+ onFullscreenScrollbarChange: (mode) => {
3815
+ this.settingsManager.setFullscreenScrollbar(mode);
3816
+ this.applyFullscreenScrollbarSetting();
3817
+ },
3818
+ onWarningsChange: (warnings) => {
3819
+ this.settingsManager.setWarnings(warnings);
3820
+ },
3208
3821
  onCancel: () => {
3209
3822
  done();
3210
3823
  this.ui.requestRender();
@@ -3236,42 +3849,63 @@ export class InteractiveMode {
3236
3849
  this.showModelSelector(searchTerm);
3237
3850
  }
3238
3851
  async findExactModelMatch(searchTerm) {
3239
- const models = await this.getModelCandidates();
3240
- return findExactModelReferenceMatch(searchTerm, models);
3241
- }
3242
- async getModelCandidates() {
3243
- if (this.session.scopedModels.length > 0) {
3244
- return this.session.scopedModels.map((scoped) => scoped.model);
3245
- }
3246
- this.session.modelRegistry.refresh();
3852
+ const cachedModels = this.session.scopedModels.length > 0
3853
+ ? this.session.scopedModels.map((scoped) => scoped.model)
3854
+ : [...this.session.modelRuntime.getAvailableSnapshot()];
3855
+ const cachedMatch = findExactModelReferenceMatch(searchTerm, cachedModels);
3856
+ if (cachedMatch || this.session.scopedModels.length > 0)
3857
+ return cachedMatch;
3858
+ this.showStatus("Refreshing model catalogs…");
3859
+ const controller = new AbortController();
3860
+ let timedOut = false;
3861
+ const timeout = setTimeout(() => {
3862
+ timedOut = true;
3863
+ controller.abort();
3864
+ }, 15_000);
3247
3865
  try {
3248
- return await this.session.modelRegistry.getAvailable();
3866
+ const result = await refreshModelCatalogs(this.session.modelRuntime, controller.signal);
3867
+ if (result.aborted && timedOut) {
3868
+ this.showWarning("Model refresh timed out; searching cached models.");
3869
+ }
3870
+ else if (result.errors.size > 0) {
3871
+ this.showWarning(`Could not refresh ${[...result.errors.keys()].join(", ")}; searching cached models.`);
3872
+ }
3249
3873
  }
3250
- catch {
3251
- return [];
3874
+ catch (error) {
3875
+ this.showWarning(timedOut
3876
+ ? "Model refresh timed out; searching cached models."
3877
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`);
3252
3878
  }
3879
+ finally {
3880
+ clearTimeout(timeout);
3881
+ }
3882
+ return findExactModelReferenceMatch(searchTerm, [...this.session.modelRuntime.getAvailableSnapshot()]);
3253
3883
  }
3254
- /** Update the footer's available provider count from current model candidates */
3255
- async updateAvailableProviderCount() {
3256
- const models = await this.getModelCandidates();
3257
- const uniqueProviders = new Set(models.map((m) => m.provider));
3884
+ /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3885
+ updateAvailableProviderCount() {
3886
+ const models = this.session.scopedModels.length > 0
3887
+ ? this.session.scopedModels.map((scoped) => scoped.model)
3888
+ : this.session.modelRuntime.getAvailableSnapshot();
3889
+ const uniqueProviders = new Set(models.map((model) => model.provider));
3258
3890
  this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
3259
3891
  }
3260
3892
  async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
3261
- if (this.anthropicSubscriptionWarningShown) {
3893
+ if (this.settingsManager.getWarnings().anthropicExtraUsage === false) {
3262
3894
  return;
3263
3895
  }
3264
- if (!model || model.provider !== "anthropic") {
3896
+ if (this.anthropicSubscriptionWarningShown) {
3265
3897
  return;
3266
3898
  }
3267
- const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
3268
- if (storedCredential?.type === "oauth") {
3269
- this.anthropicSubscriptionWarningShown = true;
3270
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3899
+ if (!model || model.provider !== "anthropic") {
3271
3900
  return;
3272
3901
  }
3273
3902
  try {
3274
- const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
3903
+ if ((await this.session.modelRuntime.checkAuth("anthropic"))?.type === "oauth") {
3904
+ this.anthropicSubscriptionWarningShown = true;
3905
+ this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3906
+ return;
3907
+ }
3908
+ const apiKey = (await this.session.modelRuntime.getAuth(model.provider))?.auth.apiKey;
3275
3909
  if (!isAnthropicSubscriptionAuthKey(apiKey)) {
3276
3910
  return;
3277
3911
  }
@@ -3282,9 +3916,54 @@ export class InteractiveMode {
3282
3916
  // Ignore auth lookup failures for warning-only checks.
3283
3917
  }
3284
3918
  }
3919
+ maybeSaveImplicitProjectTrustAfterReload() {
3920
+ const cwd = this.sessionManager.getCwd();
3921
+ if (this.autoTrustOnReloadCwd !== cwd) {
3922
+ return false;
3923
+ }
3924
+ if (!this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(cwd)) {
3925
+ return false;
3926
+ }
3927
+ const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir);
3928
+ try {
3929
+ if (trustStore.get(cwd) !== null) {
3930
+ this.autoTrustOnReloadCwd = undefined;
3931
+ return false;
3932
+ }
3933
+ trustStore.set(cwd, true);
3934
+ this.autoTrustOnReloadCwd = undefined;
3935
+ return true;
3936
+ }
3937
+ catch (error) {
3938
+ this.showWarning(`Could not save project trust after reload: ${error instanceof Error ? error.message : String(error)}`);
3939
+ return false;
3940
+ }
3941
+ }
3942
+ showTrustSelector() {
3943
+ const cwd = this.sessionManager.getCwd();
3944
+ const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir);
3945
+ const savedDecision = trustStore.getEntry(cwd);
3946
+ this.showSelector((done) => {
3947
+ const selector = new TrustSelectorComponent({
3948
+ cwd,
3949
+ savedDecision,
3950
+ projectTrusted: this.settingsManager.isProjectTrusted(),
3951
+ onSelect: (selection) => {
3952
+ trustStore.setMany(selection.updates);
3953
+ done();
3954
+ this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart ${APP_NAME} for this to take effect.`);
3955
+ },
3956
+ onCancel: () => {
3957
+ done();
3958
+ this.ui.requestRender();
3959
+ },
3960
+ });
3961
+ return { component: selector, focus: selector };
3962
+ });
3963
+ }
3285
3964
  showModelSelector(initialSearchInput) {
3286
3965
  this.showSelector((done) => {
3287
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3966
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRuntime, this.session.scopedModels, async (model) => {
3288
3967
  try {
3289
3968
  await this.session.setModel(model);
3290
3969
  this.footer.invalidate();
@@ -3302,64 +3981,68 @@ export class InteractiveMode {
3302
3981
  done();
3303
3982
  this.ui.requestRender();
3304
3983
  }, initialSearchInput);
3305
- return { component: selector, focus: selector };
3984
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
3306
3985
  });
3307
3986
  }
3308
- async showModelsSelector() {
3309
- // Get all available models
3310
- this.session.modelRegistry.refresh();
3311
- const allModels = this.session.modelRegistry.getAvailable();
3312
- if (allModels.length === 0) {
3313
- this.showStatus("No models available");
3314
- return;
3315
- }
3316
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3987
+ showModelsSelector() {
3988
+ let availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
3989
+ let availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
3990
+ const configuredPatterns = this.settingsManager.getEnabledModels();
3317
3991
  const sessionScopedModels = this.session.scopedModels;
3318
- const hasSessionScope = sessionScopedModels.length > 0;
3319
- // Build enabled model IDs from session state or settings
3320
- let currentEnabledIds = null;
3321
- if (hasSessionScope) {
3322
- // Use current session's scoped models
3323
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3324
- }
3325
- else {
3326
- // Fall back to settings
3327
- const patterns = this.settingsManager.getEnabledModels();
3328
- if (patterns !== undefined && patterns.length > 0) {
3329
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3330
- currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3331
- }
3332
- }
3333
- // Helper to update session's scoped models (session-only, no persist)
3334
- const updateSessionModels = async (enabledIds) => {
3992
+ const configuredEnabledIds = (models) => {
3993
+ if (!configuredPatterns?.length)
3994
+ return null;
3995
+ const resolved = resolveModelScopeFromModels(configuredPatterns, models);
3996
+ const ids = resolved.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3997
+ for (const diagnostic of resolved.diagnostics) {
3998
+ if (diagnostic.code === "no-match" && !ids.includes(diagnostic.pattern))
3999
+ ids.push(diagnostic.pattern);
4000
+ }
4001
+ return ids;
4002
+ };
4003
+ let currentEnabledIds = sessionScopedModels.length > 0
4004
+ ? sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`)
4005
+ : configuredEnabledIds(availableModels);
4006
+ let selectionChanged = false;
4007
+ const updateSessionModels = (enabledIds) => {
3335
4008
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3336
- if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3337
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3338
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3339
- model: sm.model,
3340
- thinkingLevel: sm.thinkingLevel,
4009
+ const hasEnabledAvailableModel = enabledIds?.some((id) => availableModelIds.has(id)) ?? false;
4010
+ const allAvailableModelsEnabled = enabledIds !== null && [...availableModelIds].every((id) => enabledIds.includes(id));
4011
+ if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
4012
+ const newScopedModels = resolveModelScopeFromModels(enabledIds, availableModels).scopedModels;
4013
+ this.session.setScopedModels(newScopedModels.map((scoped) => ({
4014
+ model: scoped.model,
4015
+ thinkingLevel: scoped.thinkingLevel,
3341
4016
  })));
3342
4017
  }
3343
4018
  else {
3344
- // All enabled or none enabled = no filter
3345
4019
  this.session.setScopedModels([]);
3346
4020
  }
3347
- await this.updateAvailableProviderCount();
4021
+ this.updateAvailableProviderCount();
3348
4022
  this.ui.requestRender();
3349
4023
  };
3350
4024
  this.showSelector((done) => {
4025
+ let disposed = false;
4026
+ let timedOut = false;
4027
+ const controller = new AbortController();
4028
+ const timeout = setTimeout(() => {
4029
+ timedOut = true;
4030
+ controller.abort();
4031
+ }, 15_000);
3351
4032
  const selector = new ScopedModelsSelectorComponent({
3352
- allModels,
4033
+ allModels: availableModels,
3353
4034
  enabledModelIds: currentEnabledIds,
4035
+ refreshStatus: "Refreshing model catalogs…",
3354
4036
  }, {
3355
- onChange: async (enabledIds) => {
3356
- await updateSessionModels(enabledIds);
4037
+ onChange: (enabledIds) => {
4038
+ selectionChanged = true;
4039
+ updateSessionModels(enabledIds);
3357
4040
  },
3358
4041
  onPersist: (enabledIds) => {
3359
- // Persist to settings
3360
- const newPatterns = enabledIds === null || enabledIds.length === allModels.length
3361
- ? undefined // All enabled = clear filter
3362
- : enabledIds;
4042
+ const allEnabled = enabledIds !== null &&
4043
+ enabledIds.length === availableModels.length &&
4044
+ enabledIds.every((id) => availableModelIds.has(id));
4045
+ const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3363
4046
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3364
4047
  this.showStatus("Model selection saved to settings");
3365
4048
  },
@@ -3368,7 +4051,50 @@ export class InteractiveMode {
3368
4051
  this.ui.requestRender();
3369
4052
  },
3370
4053
  });
3371
- return { component: selector, focus: selector };
4054
+ void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
4055
+ .then((result) => {
4056
+ if (disposed)
4057
+ return;
4058
+ availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
4059
+ availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
4060
+ if (!selectionChanged && sessionScopedModels.length === 0) {
4061
+ currentEnabledIds = configuredEnabledIds(availableModels);
4062
+ selector.updateModels(availableModels, currentEnabledIds);
4063
+ }
4064
+ else {
4065
+ selector.updateModels(availableModels);
4066
+ }
4067
+ if (currentEnabledIds !== null)
4068
+ updateSessionModels(currentEnabledIds);
4069
+ if (result.aborted && timedOut) {
4070
+ selector.setRefreshStatus("Model refresh timed out; showing cached models.", "warning");
4071
+ }
4072
+ else if (result.errors.size > 0) {
4073
+ selector.setRefreshStatus(`Could not refresh ${[...result.errors.keys()].join(", ")}; showing cached models.`, "warning");
4074
+ }
4075
+ else {
4076
+ selector.setRefreshStatus("Model catalogs refreshed.", "success");
4077
+ }
4078
+ this.ui.requestRender();
4079
+ })
4080
+ .catch((error) => {
4081
+ if (disposed)
4082
+ return;
4083
+ selector.setRefreshStatus(timedOut
4084
+ ? "Model refresh timed out; showing cached models."
4085
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
4086
+ this.ui.requestRender();
4087
+ })
4088
+ .finally(() => clearTimeout(timeout));
4089
+ return {
4090
+ component: selector,
4091
+ focus: selector,
4092
+ dispose: () => {
4093
+ disposed = true;
4094
+ clearTimeout(timeout);
4095
+ controller.abort();
4096
+ },
4097
+ };
3372
4098
  });
3373
4099
  }
3374
4100
  showUserMessageSelector() {
@@ -3380,20 +4106,17 @@ export class InteractiveMode {
3380
4106
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3381
4107
  this.showSelector((done) => {
3382
4108
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
4109
+ done();
3383
4110
  try {
3384
4111
  const result = await this.runtimeHost.fork(entryId);
3385
4112
  if (result.cancelled) {
3386
- done();
3387
4113
  this.ui.requestRender();
3388
4114
  return;
3389
4115
  }
3390
- this.renderCurrentSessionState();
3391
4116
  this.editor.setText(result.selectedText ?? "");
3392
- done();
3393
4117
  this.showStatus("Forked to new session");
3394
4118
  }
3395
4119
  catch (error) {
3396
- done();
3397
4120
  this.showError(error instanceof Error ? error.message : String(error));
3398
4121
  }
3399
4122
  }, () => {
@@ -3415,7 +4138,6 @@ export class InteractiveMode {
3415
4138
  this.ui.requestRender();
3416
4139
  return;
3417
4140
  }
3418
- this.renderCurrentSessionState();
3419
4141
  this.editor.setText("");
3420
4142
  this.showStatus("Cloned to new session");
3421
4143
  }
@@ -3434,7 +4156,7 @@ export class InteractiveMode {
3434
4156
  this.showSelector((done) => {
3435
4157
  const selector = new TreeSelectorComponent(tree, realLeafId, this.ui.terminal.rows, async (entryId) => {
3436
4158
  // Selecting the current leaf is a no-op (already there)
3437
- if (entryId === realLeafId) {
4159
+ if (entryId === this.sessionManager.getLeafId()) {
3438
4160
  done();
3439
4161
  this.showStatus("Already at this point");
3440
4162
  return;
@@ -3469,16 +4191,21 @@ export class InteractiveMode {
3469
4191
  break;
3470
4192
  }
3471
4193
  }
3472
- // Set up escape handler and loader if summarizing
3473
- let summaryLoader;
4194
+ // The user committed to navigating: stop the active response first.
4195
+ if (this.session.isStreaming) {
4196
+ this.restoreQueuedMessagesToEditor();
4197
+ await this.session.abort();
4198
+ }
4199
+ // Set up escape handler and status indicator if summarizing
4200
+ let showingSummaryIndicator = false;
3474
4201
  const originalOnEscape = this.defaultEditor.onEscape;
3475
4202
  if (wantsSummary) {
3476
4203
  this.defaultEditor.onEscape = () => {
3477
4204
  this.session.abortBranchSummary();
3478
4205
  };
3479
4206
  this.chatContainer.addChild(new Spacer(1));
3480
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3481
- this.statusContainer.addChild(summaryLoader);
4207
+ this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
4208
+ showingSummaryIndicator = true;
3482
4209
  this.ui.requestRender();
3483
4210
  }
3484
4211
  try {
@@ -3509,9 +4236,8 @@ export class InteractiveMode {
3509
4236
  this.showError(error instanceof Error ? error.message : String(error));
3510
4237
  }
3511
4238
  finally {
3512
- if (summaryLoader) {
3513
- summaryLoader.stop();
3514
- this.statusContainer.clear();
4239
+ if (showingSummaryIndicator) {
4240
+ this.clearStatusIndicator("branchSummary");
3515
4241
  }
3516
4242
  this.defaultEditor.onEscape = originalOnEscape;
3517
4243
  }
@@ -3522,12 +4248,27 @@ export class InteractiveMode {
3522
4248
  this.sessionManager.appendLabelChange(entryId, label);
3523
4249
  this.ui.requestRender();
3524
4250
  }, initialSelectedId, initialFilterMode);
4251
+ selector.onCopy = async (text) => {
4252
+ if (!text) {
4253
+ this.showError("Selected entry has no text to copy");
4254
+ return;
4255
+ }
4256
+ try {
4257
+ await copyToClipboard(text);
4258
+ this.showStatus("Copied selected message to clipboard");
4259
+ }
4260
+ catch (error) {
4261
+ this.showError(error instanceof Error ? error.message : String(error));
4262
+ }
4263
+ };
3525
4264
  return { component: selector, focus: selector };
3526
4265
  });
3527
4266
  }
3528
4267
  showSessionSelector() {
3529
4268
  this.showSelector((done) => {
3530
- const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), SessionManager.listAll, async (sessionPath) => {
4269
+ const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), (onProgress) => this.sessionManager.usesDefaultSessionDir()
4270
+ ? SessionManager.listAll(onProgress)
4271
+ : SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress), async (sessionPath) => {
3531
4272
  done();
3532
4273
  await this.handleResumeSession(sessionPath);
3533
4274
  }, () => {
@@ -3550,19 +4291,15 @@ export class InteractiveMode {
3550
4291
  });
3551
4292
  }
3552
4293
  async handleResumeSession(sessionPath, options) {
3553
- if (this.loadingAnimation) {
3554
- this.loadingAnimation.stop();
3555
- this.loadingAnimation = undefined;
3556
- }
3557
- this.statusContainer.clear();
4294
+ this.clearStatusIndicator();
3558
4295
  try {
3559
4296
  const result = await this.runtimeHost.switchSession(sessionPath, {
3560
4297
  withSession: options?.withSession,
4298
+ projectTrustContextFactory: (cwd) => this.createProjectTrustContext(cwd),
3561
4299
  });
3562
4300
  if (result.cancelled) {
3563
4301
  return result;
3564
4302
  }
3565
- this.renderCurrentSessionState();
3566
4303
  this.showStatus("Resumed session");
3567
4304
  return result;
3568
4305
  }
@@ -3576,11 +4313,11 @@ export class InteractiveMode {
3576
4313
  const result = await this.runtimeHost.switchSession(sessionPath, {
3577
4314
  cwdOverride: selectedCwd,
3578
4315
  withSession: options?.withSession,
4316
+ projectTrustContextFactory: (cwd) => this.createProjectTrustContext(cwd),
3579
4317
  });
3580
4318
  if (result.cancelled) {
3581
4319
  return result;
3582
4320
  }
3583
- this.renderCurrentSessionState();
3584
4321
  this.showStatus("Resumed session in current cwd");
3585
4322
  return result;
3586
4323
  }
@@ -3588,54 +4325,124 @@ export class InteractiveMode {
3588
4325
  }
3589
4326
  }
3590
4327
  getLoginProviderOptions(authType) {
3591
- const authStorage = this.session.modelRegistry.authStorage;
3592
- const oauthProviders = authStorage.getOAuthProviders();
3593
- const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
3594
- const options = oauthProviders.map((provider) => ({
3595
- id: provider.id,
3596
- name: provider.name,
3597
- authType: "oauth",
3598
- }));
3599
- const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
3600
- for (const providerId of modelProviders) {
3601
- if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
3602
- continue;
4328
+ const options = [];
4329
+ for (const provider of this.session.modelRuntime.getProviders()) {
4330
+ const authStatus = this.session.modelRuntime.getProviderAuthStatus(provider.id);
4331
+ const status = authStatus.configured
4332
+ ? {
4333
+ type: this.session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
4334
+ source: authStatus.label ?? authStatus.source,
4335
+ }
4336
+ : undefined;
4337
+ if ((!authType || authType === "oauth") && provider.auth.oauth) {
4338
+ options.push({
4339
+ id: provider.id,
4340
+ name: provider.name,
4341
+ authType: "oauth",
4342
+ method: provider.auth.oauth,
4343
+ status,
4344
+ });
3603
4345
  }
3604
- options.push({
3605
- id: providerId,
3606
- name: getApiKeyProviderDisplayName(providerId),
3607
- authType: "api_key",
3608
- });
4346
+ if ((!authType || authType === "api_key") && provider.auth.apiKey) {
4347
+ options.push({
4348
+ id: provider.id,
4349
+ name: provider.name,
4350
+ authType: "api_key",
4351
+ method: provider.auth.apiKey,
4352
+ status,
4353
+ });
4354
+ }
4355
+ }
4356
+ return options.sort((a, b) => a.name.localeCompare(b.name));
4357
+ }
4358
+ async getLogoutProviderOptions() {
4359
+ return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4360
+ .map(({ providerId, type }) => ({
4361
+ id: providerId,
4362
+ name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
4363
+ authType: type,
4364
+ status: { type, source: "stored credential" },
4365
+ }))
4366
+ .sort((a, b) => a.name.localeCompare(b.name));
4367
+ }
4368
+ findLoginProviderOptions(providerRef) {
4369
+ const normalizedProviderRef = providerRef.trim().toLowerCase();
4370
+ if (!normalizedProviderRef) {
4371
+ return [];
3609
4372
  }
3610
- const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
3611
- return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
4373
+ return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
4374
+ provider.name.toLowerCase() === normalizedProviderRef);
3612
4375
  }
3613
- getLogoutProviderOptions() {
3614
- const authStorage = this.session.modelRegistry.authStorage;
3615
- const oauthNameById = new Map(authStorage.getOAuthProviders().map((provider) => [provider.id, provider.name]));
3616
- const options = [];
3617
- for (const providerId of authStorage.list()) {
3618
- const credential = authStorage.get(providerId);
3619
- if (!credential) {
3620
- continue;
4376
+ async handleLoginCommand(providerRef) {
4377
+ if (!providerRef) {
4378
+ this.showLoginAuthTypeSelector();
4379
+ return;
4380
+ }
4381
+ const providerOptions = this.findLoginProviderOptions(providerRef);
4382
+ if (providerOptions.length === 1) {
4383
+ await this.startProviderLogin(providerOptions[0]);
4384
+ return;
4385
+ }
4386
+ if (providerOptions.length > 1) {
4387
+ const providerIds = new Set(providerOptions.map((provider) => provider.id));
4388
+ if (providerIds.size === 1) {
4389
+ this.showLoginAuthTypeSelector(providerOptions);
4390
+ return;
3621
4391
  }
3622
- options.push({
3623
- id: providerId,
3624
- name: credential.type === "oauth"
3625
- ? (oauthNameById.get(providerId) ?? providerId)
3626
- : getApiKeyProviderDisplayName(providerId),
3627
- authType: credential.type,
3628
- });
3629
4392
  }
3630
- return options.sort((a, b) => a.name.localeCompare(b.name));
4393
+ this.showLoginProviderSelector(undefined, providerRef);
4394
+ }
4395
+ async startProviderLogin(providerOption) {
4396
+ if (providerOption.authType === "oauth") {
4397
+ await this.showLoginDialog(providerOption.id, providerOption.name);
4398
+ }
4399
+ else if (providerOption.method?.login) {
4400
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4401
+ }
4402
+ else {
4403
+ this.showAmbientAuthDialog(providerOption);
4404
+ }
3631
4405
  }
3632
- showLoginAuthTypeSelector() {
3633
- const subscriptionLabel = "Use a subscription";
3634
- const apiKeyLabel = "Use an API key";
4406
+ showLoginAuthTypeSelector(providerOptions) {
4407
+ const oauthProvider = providerOptions?.find((provider) => provider.authType === "oauth");
4408
+ const oauthLoginLabel = oauthProvider?.method && "loginLabel" in oauthProvider.method ? oauthProvider.method.loginLabel : undefined;
4409
+ const subscriptionLabel = oauthLoginLabel ?? "Sign in with an account";
4410
+ const apiKeyLabel = "Sign in with an API key";
4411
+ const availableAuthTypes = providerOptions
4412
+ ? new Set(providerOptions.map((provider) => provider.authType))
4413
+ : new Set(["oauth", "api_key"]);
4414
+ const options = [];
4415
+ if (availableAuthTypes.has("oauth")) {
4416
+ options.push(subscriptionLabel);
4417
+ }
4418
+ if (availableAuthTypes.has("api_key")) {
4419
+ options.push(apiKeyLabel);
4420
+ }
4421
+ if (options.length === 0) {
4422
+ this.showStatus("No login methods available.");
4423
+ return;
4424
+ }
4425
+ if (providerOptions && options.length === 1) {
4426
+ const providerOption = providerOptions[0];
4427
+ if (providerOption) {
4428
+ void this.startProviderLogin(providerOption);
4429
+ }
4430
+ return;
4431
+ }
4432
+ const title = providerOptions?.[0]
4433
+ ? `Select authentication method for ${providerOptions[0].name}:`
4434
+ : "Select authentication method:";
3635
4435
  this.showSelector((done) => {
3636
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4436
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
3637
4437
  done();
3638
4438
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4439
+ if (providerOptions) {
4440
+ const providerOption = providerOptions.find((provider) => provider.authType === authType);
4441
+ if (providerOption) {
4442
+ void this.startProviderLogin(providerOption);
4443
+ }
4444
+ return;
4445
+ }
3639
4446
  this.showLoginProviderSelector(authType);
3640
4447
  }, () => {
3641
4448
  done();
@@ -3644,32 +4451,34 @@ export class InteractiveMode {
3644
4451
  return { component: selector, focus: selector };
3645
4452
  });
3646
4453
  }
3647
- showLoginProviderSelector(authType) {
4454
+ showLoginProviderSelector(authType, initialSearchInput) {
3648
4455
  const providerOptions = this.getLoginProviderOptions(authType);
3649
4456
  if (providerOptions.length === 0) {
3650
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
4457
+ const message = authType === "oauth"
4458
+ ? "No subscription providers available."
4459
+ : authType === "api_key"
4460
+ ? "No API key providers available."
4461
+ : "No login providers available.";
4462
+ this.showStatus(message);
3651
4463
  return;
3652
4464
  }
3653
4465
  this.showSelector((done) => {
3654
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4466
+ const selector = new OAuthSelectorComponent("login", providerOptions, async (providerId, selectedAuthType) => {
3655
4467
  done();
3656
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4468
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
3657
4469
  if (!providerOption) {
3658
4470
  return;
3659
4471
  }
3660
- if (providerOption.authType === "oauth") {
3661
- await this.showLoginDialog(providerOption.id, providerOption.name);
3662
- }
3663
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3664
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4472
+ await this.startProviderLogin(providerOption);
4473
+ }, () => {
4474
+ done();
4475
+ if (authType) {
4476
+ this.showLoginAuthTypeSelector();
3665
4477
  }
3666
4478
  else {
3667
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4479
+ this.ui.requestRender();
3668
4480
  }
3669
- }, () => {
3670
- done();
3671
- this.showLoginAuthTypeSelector();
3672
- });
4481
+ }, initialSearchInput);
3673
4482
  return { component: selector, focus: selector };
3674
4483
  });
3675
4484
  }
@@ -3678,29 +4487,40 @@ export class InteractiveMode {
3678
4487
  this.showLoginAuthTypeSelector();
3679
4488
  return;
3680
4489
  }
3681
- const providerOptions = this.getLogoutProviderOptions();
4490
+ let providerOptions;
4491
+ try {
4492
+ providerOptions = await this.getLogoutProviderOptions();
4493
+ }
4494
+ catch (error) {
4495
+ this.showError(`Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`);
4496
+ return;
4497
+ }
3682
4498
  if (providerOptions.length === 0) {
3683
- this.showStatus("No providers logged in. Use /login first.");
4499
+ this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
3684
4500
  return;
3685
4501
  }
3686
4502
  this.showSelector((done) => {
3687
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4503
+ const selector = new OAuthSelectorComponent(mode, providerOptions, async (providerId) => {
3688
4504
  done();
3689
4505
  const providerOption = providerOptions.find((provider) => provider.id === providerId);
3690
4506
  if (!providerOption) {
3691
4507
  return;
3692
4508
  }
3693
4509
  try {
3694
- this.session.modelRegistry.authStorage.logout(providerOption.id);
3695
- this.session.modelRegistry.refresh();
4510
+ await this.session.modelRuntime.logout(providerOption.id, {
4511
+ signal: AbortSignal.timeout(15_000),
4512
+ });
3696
4513
  await this.updateAvailableProviderCount();
3697
4514
  const message = providerOption.authType === "oauth"
3698
4515
  ? `Logged out of ${providerOption.name}`
3699
- : `Removed API key for ${providerOption.name}`;
4516
+ : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
3700
4517
  this.showStatus(message);
3701
4518
  }
3702
4519
  catch (error) {
3703
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
4520
+ const message = error instanceof Error ? error.message : String(error);
4521
+ this.showError(error instanceof CredentialSynchronizationError
4522
+ ? `Credentials removed for ${providerOption.name}, but local model state could not be synchronized: ${message}`
4523
+ : `Logout failed: ${message}`);
3704
4524
  }
3705
4525
  }, () => {
3706
4526
  done();
@@ -3710,12 +4530,11 @@ export class InteractiveMode {
3710
4530
  });
3711
4531
  }
3712
4532
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
3713
- this.session.modelRegistry.refresh();
3714
4533
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
3715
4534
  let selectedModel;
3716
4535
  let selectionError;
3717
4536
  if (isUnknownModel(previousModel)) {
3718
- const availableModels = this.session.modelRegistry.getAvailable();
4537
+ const availableModels = this.session.modelRuntime.getAvailableSnapshot();
3719
4538
  const providerModels = availableModels.filter((model) => model.provider === providerId);
3720
4539
  if (!hasDefaultModelProvider(providerId)) {
3721
4540
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -3758,21 +4577,35 @@ export class InteractiveMode {
3758
4577
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
3759
4578
  }
3760
4579
  }
4580
+ const controller = new AbortController();
4581
+ const timeout = setTimeout(() => controller.abort(), 15_000);
4582
+ void this.session.modelRuntime
4583
+ .refresh({ providers: [providerId], signal: controller.signal })
4584
+ .then((result) => {
4585
+ if (result.aborted) {
4586
+ this.showWarning(`${actionLabel}, but its model catalog refresh timed out; using cached models.`);
4587
+ }
4588
+ else if (result.errors.size > 0) {
4589
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed; using cached models.`);
4590
+ }
4591
+ this.updateAvailableProviderCount();
4592
+ this.footer.invalidate();
4593
+ this.ui.requestRender();
4594
+ })
4595
+ .catch((error) => {
4596
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
4597
+ })
4598
+ .finally(() => clearTimeout(timeout));
3761
4599
  }
3762
- showBedrockSetupDialog(providerId, providerName) {
4600
+ showAmbientAuthDialog(providerOption) {
3763
4601
  const restoreEditor = () => {
3764
4602
  this.editorContainer.clear();
3765
4603
  this.editorContainer.addChild(this.editor);
3766
4604
  this.ui.setFocus(this.editor);
3767
4605
  this.ui.requestRender();
3768
4606
  };
3769
- const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
3770
- dialog.showInfo([
3771
- theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
3772
- theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
3773
- theme.fg("muted", "See:"),
3774
- theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
3775
- ]);
4607
+ const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
4608
+ dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside ${APP_NAME}.`, [], true);
3776
4609
  this.editorContainer.clear();
3777
4610
  this.editorContainer.addChild(dialog);
3778
4611
  this.ui.setFocus(dialog);
@@ -3783,6 +4616,13 @@ export class InteractiveMode {
3783
4616
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3784
4617
  // Completion handled below
3785
4618
  }, providerName);
4619
+ if (providerId === "amazon-bedrock") {
4620
+ dialog.showDetails([
4621
+ theme.fg("text", "You can also use an AWS profile, IAM keys, or role-based credentials."),
4622
+ theme.fg("muted", "See:"),
4623
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4624
+ ]);
4625
+ }
3786
4626
  this.editorContainer.clear();
3787
4627
  this.editorContainer.addChild(dialog);
3788
4628
  this.ui.setFocus(dialog);
@@ -3794,46 +4634,105 @@ export class InteractiveMode {
3794
4634
  this.ui.requestRender();
3795
4635
  };
3796
4636
  try {
3797
- const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
3798
- if (!apiKey) {
3799
- throw new Error("API key cannot be empty.");
3800
- }
3801
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
4637
+ await this.loginProvider(dialog, providerId, "api_key");
3802
4638
  restoreEditor();
3803
4639
  await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
3804
4640
  }
3805
4641
  catch (error) {
3806
4642
  restoreEditor();
3807
4643
  const errorMsg = error instanceof Error ? error.message : String(error);
3808
- if (errorMsg !== "Login cancelled") {
4644
+ if (error instanceof CredentialSynchronizationError) {
4645
+ this.showError(`Saved API key for ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4646
+ }
4647
+ else if (errorMsg !== "Login cancelled") {
3809
4648
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
3810
4649
  }
3811
4650
  }
3812
4651
  }
4652
+ showAuthSelect(dialog, prompt) {
4653
+ return new Promise((resolve, reject) => {
4654
+ const restoreDialog = () => {
4655
+ this.editorContainer.clear();
4656
+ this.editorContainer.addChild(dialog);
4657
+ this.ui.setFocus(dialog);
4658
+ this.ui.requestRender();
4659
+ };
4660
+ const labels = prompt.options.map((option) => option.label);
4661
+ const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
4662
+ restoreDialog();
4663
+ const id = prompt.options.find((option) => option.label === optionLabel)?.id;
4664
+ if (id)
4665
+ resolve(id);
4666
+ else
4667
+ reject(new Error("Login cancelled"));
4668
+ }, () => {
4669
+ restoreDialog();
4670
+ reject(new Error("Login cancelled"));
4671
+ });
4672
+ this.editorContainer.clear();
4673
+ this.editorContainer.addChild(selector);
4674
+ this.ui.setFocus(selector);
4675
+ this.ui.requestRender();
4676
+ });
4677
+ }
4678
+ async showAuthPrompt(dialog, prompt) {
4679
+ let response;
4680
+ if (prompt.type === "select") {
4681
+ response = this.showAuthSelect(dialog, prompt);
4682
+ }
4683
+ else if (prompt.type === "manual_code") {
4684
+ response = dialog.showManualInput(prompt.message);
4685
+ }
4686
+ else {
4687
+ response = dialog.showPrompt(prompt.message, prompt.placeholder);
4688
+ }
4689
+ if (!prompt.signal)
4690
+ return response;
4691
+ if (prompt.signal.aborted)
4692
+ throw new Error("Login cancelled");
4693
+ const signal = prompt.signal;
4694
+ let onAbort;
4695
+ const aborted = new Promise((_resolve, reject) => {
4696
+ onAbort = () => reject(new Error("Login cancelled"));
4697
+ signal.addEventListener("abort", onAbort, { once: true });
4698
+ });
4699
+ try {
4700
+ return await Promise.race([response, aborted]);
4701
+ }
4702
+ finally {
4703
+ if (onAbort)
4704
+ signal.removeEventListener("abort", onAbort);
4705
+ }
4706
+ }
4707
+ notifyAuthDialog(dialog, event) {
4708
+ if (event.type === "auth_url") {
4709
+ dialog.showAuth(event.url, event.instructions);
4710
+ }
4711
+ else if (event.type === "device_code") {
4712
+ dialog.showDeviceCode(event);
4713
+ dialog.showWaiting("Waiting for authentication...");
4714
+ }
4715
+ else if (event.type === "info") {
4716
+ dialog.showInfo(event.message, event.links);
4717
+ }
4718
+ else {
4719
+ dialog.showProgress(event.message);
4720
+ }
4721
+ }
4722
+ async loginProvider(dialog, providerId, method) {
4723
+ await this.session.modelRuntime.login(providerId, method, {
4724
+ signal: dialog.signal,
4725
+ prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
4726
+ notify: (event) => this.notifyAuthDialog(dialog, event),
4727
+ });
4728
+ }
3813
4729
  async showLoginDialog(providerId, providerName) {
3814
- const providerInfo = this.session.modelRegistry.authStorage
3815
- .getOAuthProviders()
3816
- .find((provider) => provider.id === providerId);
3817
4730
  const previousModel = this.session.model;
3818
- // Providers that use callback servers (can paste redirect URL)
3819
- const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
3820
- // Create login dialog component
3821
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3822
- // Completion handled below
3823
- }, providerName);
3824
- // Show dialog in editor container
4731
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => { }, providerName);
3825
4732
  this.editorContainer.clear();
3826
4733
  this.editorContainer.addChild(dialog);
3827
4734
  this.ui.setFocus(dialog);
3828
4735
  this.ui.requestRender();
3829
- // Promise for manual code input (racing with callback server)
3830
- let manualCodeResolve;
3831
- let manualCodeReject;
3832
- const manualCodePromise = new Promise((resolve, reject) => {
3833
- manualCodeResolve = resolve;
3834
- manualCodeReject = reject;
3835
- });
3836
- // Restore editor helper
3837
4736
  const restoreEditor = () => {
3838
4737
  this.editorContainer.clear();
3839
4738
  this.editorContainer.addChild(this.editor);
@@ -3841,49 +4740,17 @@ export class InteractiveMode {
3841
4740
  this.ui.requestRender();
3842
4741
  };
3843
4742
  try {
3844
- await this.session.modelRegistry.authStorage.login(providerId, {
3845
- onAuth: (info) => {
3846
- dialog.showAuth(info.url, info.instructions);
3847
- if (usesCallbackServer) {
3848
- // Show input for manual paste, racing with callback
3849
- dialog
3850
- .showManualInput("Paste redirect URL below, or complete login in browser:")
3851
- .then((value) => {
3852
- if (value && manualCodeResolve) {
3853
- manualCodeResolve(value);
3854
- manualCodeResolve = undefined;
3855
- }
3856
- })
3857
- .catch(() => {
3858
- if (manualCodeReject) {
3859
- manualCodeReject(new Error("Login cancelled"));
3860
- manualCodeReject = undefined;
3861
- }
3862
- });
3863
- }
3864
- else if (providerId === "github-copilot") {
3865
- // GitHub Copilot polls after onAuth
3866
- dialog.showWaiting("Waiting for browser authentication...");
3867
- }
3868
- // For Anthropic: onPrompt is called immediately after
3869
- },
3870
- onPrompt: async (prompt) => {
3871
- return dialog.showPrompt(prompt.message, prompt.placeholder);
3872
- },
3873
- onProgress: (message) => {
3874
- dialog.showProgress(message);
3875
- },
3876
- onManualCodeInput: () => manualCodePromise,
3877
- signal: dialog.signal,
3878
- });
3879
- // Success
4743
+ await this.loginProvider(dialog, providerId, "oauth");
3880
4744
  restoreEditor();
3881
4745
  await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
3882
4746
  }
3883
4747
  catch (error) {
3884
4748
  restoreEditor();
3885
4749
  const errorMsg = error instanceof Error ? error.message : String(error);
3886
- if (errorMsg !== "Login cancelled") {
4750
+ if (error instanceof CredentialSynchronizationError) {
4751
+ this.showError(`Logged in to ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4752
+ }
4753
+ else if (errorMsg !== "Login cancelled") {
3887
4754
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
3888
4755
  }
3889
4756
  }
@@ -3905,7 +4772,7 @@ export class InteractiveMode {
3905
4772
  const borderColor = (s) => theme.fg("border", s);
3906
4773
  reloadBox.addChild(new DynamicBorder(borderColor));
3907
4774
  reloadBox.addChild(new Spacer(1));
3908
- reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4775
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
3909
4776
  reloadBox.addChild(new Spacer(1));
3910
4777
  reloadBox.addChild(new DynamicBorder(borderColor));
3911
4778
  const previousEditor = this.editor;
@@ -3920,47 +4787,50 @@ export class InteractiveMode {
3920
4787
  this.ui.setFocus(editor);
3921
4788
  this.ui.requestRender();
3922
4789
  };
4790
+ let chatRestoredBeforeSessionStart = false;
4791
+ let reloadBoxDismissed = false;
4792
+ const restoreChatBeforeSessionStart = () => {
4793
+ if (chatRestoredBeforeSessionStart) {
4794
+ return;
4795
+ }
4796
+ this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
4797
+ this.outputPad = this.settingsManager.getOutputPad();
4798
+ this.rebuildChatFromMessages();
4799
+ chatRestoredBeforeSessionStart = true;
4800
+ };
3923
4801
  try {
3924
- await this.session.reload();
4802
+ await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
4803
+ restoreChatBeforeSessionStart();
3925
4804
  this.keybindings.reload();
3926
4805
  const activeHeader = this.customHeader ?? this.builtInHeader;
3927
4806
  if (isExpandable(activeHeader)) {
3928
4807
  activeHeader.setExpanded(this.toolOutputExpanded);
3929
4808
  }
3930
4809
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3931
- this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3932
- const themeName = this.settingsManager.getTheme();
3933
- const themeResult = themeName ? setTheme(themeName, true) : { success: true };
3934
- if (!themeResult.success) {
3935
- this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
3936
- }
3937
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
3938
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
3939
- this.defaultEditor.setPaddingX(editorPaddingX);
3940
- this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
3941
- if (this.editor !== this.defaultEditor) {
3942
- this.editor.setPaddingX?.(editorPaddingX);
3943
- this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
3944
- }
3945
- this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
3946
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
4810
+ await this.themeController.applyFromSettings();
4811
+ this.applyRuntimeSettings();
3947
4812
  this.setupAutocompleteProvider();
3948
4813
  const runner = this.session.extensionRunner;
3949
4814
  this.setupExtensionShortcuts(runner);
3950
- this.rebuildChatFromMessages();
3951
- dismissReloadBox(this.editor);
3952
4815
  this.showLoadedResources({
3953
4816
  force: false,
3954
4817
  showDiagnosticsWhenQuiet: true,
3955
4818
  });
3956
- const modelsJsonError = this.session.modelRegistry.getError();
4819
+ const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
4820
+ const modelsJsonError = this.session.modelRuntime.getError();
3957
4821
  if (modelsJsonError) {
3958
4822
  this.showError(`models.json error: ${modelsJsonError}`);
3959
4823
  }
3960
- this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
4824
+ this.showStatus(savedImplicitProjectTrust
4825
+ ? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
4826
+ : "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
4827
+ dismissReloadBox(this.editor);
4828
+ reloadBoxDismissed = true;
3961
4829
  }
3962
4830
  catch (error) {
3963
- dismissReloadBox(previousEditor);
4831
+ if (!reloadBoxDismissed) {
4832
+ dismissReloadBox(previousEditor);
4833
+ }
3964
4834
  this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
3965
4835
  }
3966
4836
  }
@@ -3972,7 +4842,9 @@ export class InteractiveMode {
3972
4842
  this.showStatus(`Session exported to: ${filePath}`);
3973
4843
  }
3974
4844
  else {
3975
- const filePath = await this.session.exportToHtml(outputPath);
4845
+ const filePath = await this.session.exportToHtml(outputPath, {
4846
+ themeName: theme.name,
4847
+ });
3976
4848
  this.showStatus(`Session exported to: ${filePath}`);
3977
4849
  }
3978
4850
  }
@@ -4017,17 +4889,12 @@ export class InteractiveMode {
4017
4889
  return;
4018
4890
  }
4019
4891
  try {
4020
- if (this.loadingAnimation) {
4021
- this.loadingAnimation.stop();
4022
- this.loadingAnimation = undefined;
4023
- }
4024
- this.statusContainer.clear();
4892
+ this.clearStatusIndicator();
4025
4893
  const result = await this.runtimeHost.importFromJsonl(inputPath);
4026
4894
  if (result.cancelled) {
4027
4895
  this.showStatus("Import cancelled");
4028
4896
  return;
4029
4897
  }
4030
- this.renderCurrentSessionState();
4031
4898
  this.showStatus(`Session imported from: ${inputPath}`);
4032
4899
  }
4033
4900
  catch (error) {
@@ -4042,7 +4909,6 @@ export class InteractiveMode {
4042
4909
  this.showStatus("Import cancelled");
4043
4910
  return;
4044
4911
  }
4045
- this.renderCurrentSessionState();
4046
4912
  this.showStatus(`Session imported from: ${inputPath}`);
4047
4913
  return;
4048
4914
  }
@@ -4069,7 +4935,7 @@ export class InteractiveMode {
4069
4935
  // Export to a temp file
4070
4936
  const tmpFile = path.join(os.tmpdir(), "session.html");
4071
4937
  try {
4072
- await this.session.exportToHtml(tmpFile);
4938
+ await this.session.exportToHtml(tmpFile, { themeName: theme.name });
4073
4939
  }
4074
4940
  catch (error) {
4075
4941
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -4131,7 +4997,13 @@ export class InteractiveMode {
4131
4997
  }
4132
4998
  // Create the preview URL
4133
4999
  const previewUrl = getShareViewerUrl(gistId);
4134
- this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
5000
+ if (previewUrl) {
5001
+ this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
5002
+ }
5003
+ else {
5004
+ this.showStatus(`Gist: ${gistUrl}`);
5005
+ this.showError(`No share viewer is configured, so no share URL was produced. Set ${ENV_SHARE_VIEWER_URL} to a share viewer base URL to enable share links.`);
5006
+ }
4135
5007
  }
4136
5008
  catch (error) {
4137
5009
  if (!loader.signal.aborted) {
@@ -4140,7 +5012,7 @@ export class InteractiveMode {
4140
5012
  }
4141
5013
  }
4142
5014
  }
4143
- async handleCopyCommand() {
5015
+ async handleCopyCommand(options = {}) {
4144
5016
  const text = this.session.getLastAssistantText();
4145
5017
  if (!text) {
4146
5018
  this.showError("No agent messages to copy yet.");
@@ -4148,7 +5020,12 @@ export class InteractiveMode {
4148
5020
  }
4149
5021
  try {
4150
5022
  await copyToClipboard(text);
4151
- this.showStatus("Copied last agent message to clipboard");
5023
+ if (options.flashConfirmation && this.ui instanceof TuiAltScreen) {
5024
+ this.ui.flash("Copied!");
5025
+ }
5026
+ else {
5027
+ this.showStatus("Copied last agent message to clipboard");
5028
+ }
4152
5029
  }
4153
5030
  catch (error) {
4154
5031
  this.showError(error instanceof Error ? error.message : String(error));
@@ -4168,15 +5045,24 @@ export class InteractiveMode {
4168
5045
  this.ui.requestRender();
4169
5046
  return;
4170
5047
  }
4171
- this.sessionManager.appendSessionInfo(name);
4172
- this.updateTerminalTitle();
5048
+ this.session.setSessionName(name);
5049
+ const sessionName = this.sessionManager.getSessionName();
5050
+ if (sessionName !== name) {
5051
+ this.showWarning(`Session name was normalized from ${JSON.stringify(name)} to ${JSON.stringify(sessionName)}`);
5052
+ }
4173
5053
  this.chatContainer.addChild(new Spacer(1));
4174
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
5054
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${sessionName ?? name}`), 1, 0));
4175
5055
  this.ui.requestRender();
4176
5056
  }
4177
5057
  handleSessionCommand() {
4178
5058
  const stats = this.session.getSessionStats();
4179
5059
  const sessionName = this.sessionManager.getSessionName();
5060
+ const entries = this.sessionManager.getEntries();
5061
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
5062
+ // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
5063
+ // resolves to a concrete responseModel). Usage without model attribution is
5064
+ // grouped separately so the breakdown reconciles with the session total.
5065
+ const usageBreakdown = getUsageCostBreakdown(entries);
4180
5066
  let info = `${theme.bold("Session Info")}\n\n`;
4181
5067
  if (sessionName) {
4182
5068
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4184,24 +5070,42 @@ export class InteractiveMode {
4184
5070
  info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4185
5071
  info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4186
5072
  info += `${theme.bold("Messages")}\n`;
5073
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4187
5074
  info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4188
5075
  info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
4189
- info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
4190
- info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
4191
- info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
5076
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4192
5077
  info += `${theme.bold("Tokens")}\n`;
4193
- info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
4194
- info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4195
- if (stats.tokens.cacheRead > 0) {
4196
- info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
4197
- }
4198
- if (stats.tokens.cacheWrite > 0) {
4199
- info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
5078
+ // "Input" is the full prompt volume. With cache activity, split it into
5079
+ // cached (served from cache) vs uncached (everything else) - the only
5080
+ // provider-independent split. Cache writes, where reported, are a detail
5081
+ // of the uncached portion.
5082
+ const { input, cacheRead, cacheWrite } = stats.tokens;
5083
+ const promptTokens = input + cacheRead + cacheWrite;
5084
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
5085
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
5086
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
5087
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
5088
+ const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
5089
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
4200
5090
  }
5091
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4201
5092
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
4202
- if (stats.cost > 0) {
5093
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4203
5094
  info += `\n${theme.bold("Cost")}\n`;
4204
- info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
5095
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
5096
+ if (usageBreakdown.length > 1) {
5097
+ for (const entry of usageBreakdown) {
5098
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
5099
+ }
5100
+ }
5101
+ if (cacheWaste.missedTokens > 0) {
5102
+ const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
5103
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
5104
+ info +=
5105
+ cacheWaste.missedCost >= 0.0001
5106
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
5107
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
5108
+ }
4205
5109
  }
4206
5110
  this.chatContainer.addChild(new Spacer(1));
4207
5111
  this.chatContainer.addChild(new Text(info, 1, 0));
@@ -4213,7 +5117,7 @@ export class InteractiveMode {
4213
5117
  const changelogMarkdown = allEntries.length > 0
4214
5118
  ? allEntries
4215
5119
  .reverse()
4216
- .map((e) => e.content)
5120
+ .map((e) => normalizeChangelogLinks(e.content, e))
4217
5121
  .join("\n\n")
4218
5122
  : "No changelog entries found.";
4219
5123
  this.chatContainer.addChild(new Spacer(1));
@@ -4224,29 +5128,17 @@ export class InteractiveMode {
4224
5128
  this.chatContainer.addChild(new DynamicBorder());
4225
5129
  this.ui.requestRender();
4226
5130
  }
4227
- /**
4228
- * Capitalize keybinding for display (e.g., "ctrl+c" -> "Ctrl+C").
4229
- */
4230
- capitalizeKey(key) {
4231
- return key
4232
- .split("/")
4233
- .map((k) => k
4234
- .split("+")
4235
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
4236
- .join("+"))
4237
- .join("/");
4238
- }
4239
5131
  /**
4240
5132
  * Get capitalized display string for an app keybinding action.
4241
5133
  */
4242
5134
  getAppKeyDisplay(action) {
4243
- return this.capitalizeKey(keyText(action));
5135
+ return keyDisplayText(action);
4244
5136
  }
4245
5137
  /**
4246
5138
  * Get capitalized display string for an editor keybinding action.
4247
5139
  */
4248
5140
  getEditorKeyDisplay(action) {
4249
- return this.capitalizeKey(keyText(action));
5141
+ return keyDisplayText(action);
4250
5142
  }
4251
5143
  handleHotkeysCommand() {
4252
5144
  // Navigation keybindings
@@ -4285,6 +5177,7 @@ export class InteractiveMode {
4285
5177
  const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
4286
5178
  const externalEditor = this.getAppKeyDisplay("app.editor.external");
4287
5179
  const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
5180
+ const copyMessage = this.getAppKeyDisplay("app.message.copy");
4288
5181
  const followUp = this.getAppKeyDisplay("app.message.followUp");
4289
5182
  const dequeue = this.getAppKeyDisplay("app.message.dequeue");
4290
5183
  const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
@@ -4292,7 +5185,7 @@ export class InteractiveMode {
4292
5185
  **Navigation**
4293
5186
  | Key | Action |
4294
5187
  |-----|--------|
4295
- | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
5188
+ | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history |
4296
5189
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
4297
5190
  | \`${cursorLineStart}\` | Start of line |
4298
5191
  | \`${cursorLineEnd}\` | End of line |
@@ -4327,9 +5220,10 @@ export class InteractiveMode {
4327
5220
  | \`${expandTools}\` | Toggle tool output expansion |
4328
5221
  | \`${toggleThinking}\` | Toggle thinking block visibility |
4329
5222
  | \`${externalEditor}\` | Edit message in external editor |
5223
+ | \`${copyMessage}\` | Copy last assistant message |
4330
5224
  | \`${followUp}\` | Queue follow-up message |
4331
5225
  | \`${dequeue}\` | Restore queued messages |
4332
- | \`${pasteImage}\` | Paste image from clipboard |
5226
+ | \`${pasteImage}\` | Paste image or text from clipboard |
4333
5227
  | \`/\` | Slash commands |
4334
5228
  | \`!\` | Run bash command |
4335
5229
  | \`!!\` | Run bash command (excluded from context) |
@@ -4345,7 +5239,7 @@ export class InteractiveMode {
4345
5239
  `;
4346
5240
  for (const [key, shortcut] of shortcuts) {
4347
5241
  const description = shortcut.description ?? shortcut.extensionPath;
4348
- const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
5242
+ const keyDisplay = formatKeyText(key, { capitalize: true });
4349
5243
  hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
4350
5244
  }
4351
5245
  }
@@ -4358,17 +5252,12 @@ export class InteractiveMode {
4358
5252
  this.ui.requestRender();
4359
5253
  }
4360
5254
  async handleClearCommand() {
4361
- if (this.loadingAnimation) {
4362
- this.loadingAnimation.stop();
4363
- this.loadingAnimation = undefined;
4364
- }
4365
- this.statusContainer.clear();
5255
+ this.clearStatusIndicator();
4366
5256
  try {
4367
5257
  const result = await this.runtimeHost.newSession();
4368
5258
  if (result.cancelled) {
4369
5259
  return;
4370
5260
  }
4371
- this.renderCurrentSessionState();
4372
5261
  this.chatContainer.addChild(new Spacer(1));
4373
5262
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
4374
5263
  this.ui.requestRender();
@@ -4409,11 +5298,6 @@ export class InteractiveMode {
4409
5298
  this.chatContainer.addChild(new ArminComponent(this.ui));
4410
5299
  this.ui.requestRender();
4411
5300
  }
4412
- handleDementedDelves() {
4413
- this.chatContainer.addChild(new Spacer(1));
4414
- this.chatContainer.addChild(new EarendilAnnouncementComponent());
4415
- this.ui.requestRender();
4416
- }
4417
5301
  handleDaxnuts() {
4418
5302
  this.chatContainer.addChild(new Spacer(1));
4419
5303
  this.chatContainer.addChild(new DaxnutsComponent(this.ui));
@@ -4490,17 +5374,7 @@ export class InteractiveMode {
4490
5374
  this.ui.requestRender();
4491
5375
  }
4492
5376
  async handleCompactCommand(customInstructions) {
4493
- const entries = this.sessionManager.getEntries();
4494
- const messageCount = entries.filter((e) => e.type === "message").length;
4495
- if (messageCount < 2) {
4496
- this.showWarning("Nothing to compact (no messages yet)");
4497
- return;
4498
- }
4499
- if (this.loadingAnimation) {
4500
- this.loadingAnimation.stop();
4501
- this.loadingAnimation = undefined;
4502
- }
4503
- this.statusContainer.clear();
5377
+ this.clearStatusIndicator();
4504
5378
  try {
4505
5379
  await this.session.compact(customInstructions);
4506
5380
  }
@@ -4508,15 +5382,13 @@ export class InteractiveMode {
4508
5382
  // Ignore, will be emitted as an event
4509
5383
  }
4510
5384
  }
4511
- stop() {
4512
- this.unregisterSignalHandlers();
5385
+ stop(fullscreenExitOutput = this.settingsManager.getFullscreenExitOutput()) {
5386
+ this.disposeActiveSelector();
4513
5387
  if (this.settingsManager.getShowTerminalProgress()) {
4514
5388
  this.ui.terminal.setProgress(false);
4515
5389
  }
4516
- if (this.loadingAnimation) {
4517
- this.loadingAnimation.stop();
4518
- this.loadingAnimation = undefined;
4519
- }
5390
+ this.clearStatusIndicator();
5391
+ this.themeController.disableAutoSync();
4520
5392
  this.clearExtensionTerminalInputListeners();
4521
5393
  this.footer.dispose();
4522
5394
  this.footerDataProvider.dispose();
@@ -4524,9 +5396,10 @@ export class InteractiveMode {
4524
5396
  this.unsubscribe();
4525
5397
  }
4526
5398
  if (this.isInitialized) {
4527
- this.ui.stop();
5399
+ this.stopInteractiveTui(fullscreenExitOutput);
4528
5400
  this.isInitialized = false;
4529
5401
  }
5402
+ this.unregisterSignalHandlers();
4530
5403
  }
4531
5404
  }
4532
5405
  //# sourceMappingURL=interactive-mode.js.map