@averyyy/pi-coding-agent 0.82.1-piclient.9 → 0.84.1-piclient.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/CHANGELOG.md +272 -0
  2. package/README.md +6 -1
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +27 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/auth-check.d.ts +19 -0
  8. package/dist/cli/auth-check.d.ts.map +1 -0
  9. package/dist/cli/auth-check.js +50 -0
  10. package/dist/cli/auth-check.js.map +1 -0
  11. package/dist/cli/auth-command.d.ts +24 -0
  12. package/dist/cli/auth-command.d.ts.map +1 -0
  13. package/dist/cli/auth-command.js +102 -0
  14. package/dist/cli/auth-command.js.map +1 -0
  15. package/dist/cli/config-selector.d.ts.map +1 -1
  16. package/dist/cli/config-selector.js +2 -2
  17. package/dist/cli/config-selector.js.map +1 -1
  18. package/dist/cli/credential-print.d.ts +13 -0
  19. package/dist/cli/credential-print.d.ts.map +1 -0
  20. package/dist/cli/credential-print.js +76 -0
  21. package/dist/cli/credential-print.js.map +1 -0
  22. package/dist/cli/experimental/auth.d.ts +16 -0
  23. package/dist/cli/experimental/auth.d.ts.map +1 -0
  24. package/dist/cli/experimental/auth.js +13 -0
  25. package/dist/cli/experimental/auth.js.map +1 -0
  26. package/dist/cli/experimental/cli.d.ts +6 -0
  27. package/dist/cli/experimental/cli.d.ts.map +1 -0
  28. package/dist/cli/experimental/cli.js +5 -0
  29. package/dist/cli/experimental/cli.js.map +1 -0
  30. package/dist/cli/experimental/command-options.d.ts +17 -0
  31. package/dist/cli/experimental/command-options.d.ts.map +1 -0
  32. package/dist/cli/experimental/command-options.js +35 -0
  33. package/dist/cli/experimental/command-options.js.map +1 -0
  34. package/dist/cli/experimental/command.d.ts +63 -0
  35. package/dist/cli/experimental/command.d.ts.map +1 -0
  36. package/dist/cli/experimental/command.js +130 -0
  37. package/dist/cli/experimental/command.js.map +1 -0
  38. package/dist/cli/experimental/commands/client.d.ts +13 -0
  39. package/dist/cli/experimental/commands/client.d.ts.map +1 -0
  40. package/dist/cli/experimental/commands/client.js +25 -0
  41. package/dist/cli/experimental/commands/client.js.map +1 -0
  42. package/dist/cli/experimental/commands/pi.d.ts +15 -0
  43. package/dist/cli/experimental/commands/pi.d.ts.map +1 -0
  44. package/dist/cli/experimental/commands/pi.js +28 -0
  45. package/dist/cli/experimental/commands/pi.js.map +1 -0
  46. package/dist/cli/experimental/commands/server.d.ts +13 -0
  47. package/dist/cli/experimental/commands/server.d.ts.map +1 -0
  48. package/dist/cli/experimental/commands/server.js +25 -0
  49. package/dist/cli/experimental/commands/server.js.map +1 -0
  50. package/dist/cli/experimental/transport-address.d.ts +10 -0
  51. package/dist/cli/experimental/transport-address.d.ts.map +1 -0
  52. package/dist/cli/experimental/transport-address.js +38 -0
  53. package/dist/cli/experimental/transport-address.js.map +1 -0
  54. package/dist/cli/list-models.d.ts +1 -1
  55. package/dist/cli/list-models.d.ts.map +1 -1
  56. package/dist/cli/list-models.js +2 -2
  57. package/dist/cli/list-models.js.map +1 -1
  58. package/dist/cli/startup-ui.d.ts +1 -1
  59. package/dist/cli/startup-ui.d.ts.map +1 -1
  60. package/dist/cli/startup-ui.js +2 -2
  61. package/dist/cli/startup-ui.js.map +1 -1
  62. package/dist/cli.d.ts.map +1 -1
  63. package/dist/cli.js +1 -0
  64. package/dist/cli.js.map +1 -1
  65. package/dist/client/index.d.ts +3 -0
  66. package/dist/client/index.d.ts.map +1 -0
  67. package/dist/client/index.js +3 -0
  68. package/dist/client/index.js.map +1 -0
  69. package/dist/client/remote-session.d.ts +53 -0
  70. package/dist/client/remote-session.d.ts.map +1 -0
  71. package/dist/client/remote-session.js +340 -0
  72. package/dist/client/remote-session.js.map +1 -0
  73. package/dist/client/transcript.d.ts +12 -0
  74. package/dist/client/transcript.d.ts.map +1 -0
  75. package/dist/client/transcript.js +98 -0
  76. package/dist/client/transcript.js.map +1 -0
  77. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  78. package/dist/core/agent-session-runtime.js +3 -0
  79. package/dist/core/agent-session-runtime.js.map +1 -1
  80. package/dist/core/agent-session-services.d.ts +1 -0
  81. package/dist/core/agent-session-services.d.ts.map +1 -1
  82. package/dist/core/agent-session-services.js +1 -0
  83. package/dist/core/agent-session-services.js.map +1 -1
  84. package/dist/core/agent-session.d.ts +3 -12
  85. package/dist/core/agent-session.d.ts.map +1 -1
  86. package/dist/core/agent-session.js +74 -65
  87. package/dist/core/agent-session.js.map +1 -1
  88. package/dist/core/auth-storage.d.ts +25 -9
  89. package/dist/core/auth-storage.d.ts.map +1 -1
  90. package/dist/core/auth-storage.js +237 -39
  91. package/dist/core/auth-storage.js.map +1 -1
  92. package/dist/core/extensions/index.d.ts +1 -1
  93. package/dist/core/extensions/index.d.ts.map +1 -1
  94. package/dist/core/extensions/index.js.map +1 -1
  95. package/dist/core/extensions/loader.d.ts.map +1 -1
  96. package/dist/core/extensions/loader.js +43 -20
  97. package/dist/core/extensions/loader.js.map +1 -1
  98. package/dist/core/extensions/runner.d.ts +3 -1
  99. package/dist/core/extensions/runner.d.ts.map +1 -1
  100. package/dist/core/extensions/runner.js +10 -0
  101. package/dist/core/extensions/runner.js.map +1 -1
  102. package/dist/core/extensions/types.d.ts +33 -3
  103. package/dist/core/extensions/types.d.ts.map +1 -1
  104. package/dist/core/extensions/types.js.map +1 -1
  105. package/dist/core/footer-data-provider.d.ts +10 -0
  106. package/dist/core/footer-data-provider.d.ts.map +1 -1
  107. package/dist/core/footer-data-provider.js +1 -1
  108. package/dist/core/footer-data-provider.js.map +1 -1
  109. package/dist/core/http-dispatcher.d.ts.map +1 -1
  110. package/dist/core/http-dispatcher.js +5 -0
  111. package/dist/core/http-dispatcher.js.map +1 -1
  112. package/dist/core/keybindings.d.ts +44 -4
  113. package/dist/core/keybindings.d.ts.map +1 -1
  114. package/dist/core/model-config.d.ts +29 -5
  115. package/dist/core/model-config.d.ts.map +1 -1
  116. package/dist/core/model-config.js +4 -0
  117. package/dist/core/model-config.js.map +1 -1
  118. package/dist/core/model-registry.d.ts +5 -3
  119. package/dist/core/model-registry.d.ts.map +1 -1
  120. package/dist/core/model-registry.js +13 -10
  121. package/dist/core/model-registry.js.map +1 -1
  122. package/dist/core/model-resolver.d.ts +4 -3
  123. package/dist/core/model-resolver.d.ts.map +1 -1
  124. package/dist/core/model-resolver.js +42 -10
  125. package/dist/core/model-resolver.js.map +1 -1
  126. package/dist/core/model-runtime.d.ts +32 -13
  127. package/dist/core/model-runtime.d.ts.map +1 -1
  128. package/dist/core/model-runtime.js +233 -73
  129. package/dist/core/model-runtime.js.map +1 -1
  130. package/dist/core/models-store.d.ts +12 -7
  131. package/dist/core/models-store.d.ts.map +1 -1
  132. package/dist/core/models-store.js +83 -14
  133. package/dist/core/models-store.js.map +1 -1
  134. package/dist/core/package-manager.d.ts +4 -1
  135. package/dist/core/package-manager.d.ts.map +1 -1
  136. package/dist/core/package-manager.js +76 -42
  137. package/dist/core/package-manager.js.map +1 -1
  138. package/dist/core/pi-manifest.d.ts +8 -0
  139. package/dist/core/pi-manifest.d.ts.map +1 -0
  140. package/dist/core/pi-manifest.js +25 -0
  141. package/dist/core/pi-manifest.js.map +1 -0
  142. package/dist/core/pi-server-client.d.ts +2 -2
  143. package/dist/core/pi-server-client.d.ts.map +1 -1
  144. package/dist/core/pi-server-client.js +2 -0
  145. package/dist/core/pi-server-client.js.map +1 -1
  146. package/dist/core/provider-composer.d.ts +4 -1
  147. package/dist/core/provider-composer.d.ts.map +1 -1
  148. package/dist/core/provider-composer.js +36 -15
  149. package/dist/core/provider-composer.js.map +1 -1
  150. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  151. package/dist/core/remote-catalog-provider.js +73 -67
  152. package/dist/core/remote-catalog-provider.js.map +1 -1
  153. package/dist/core/resource-loader.d.ts +15 -0
  154. package/dist/core/resource-loader.d.ts.map +1 -1
  155. package/dist/core/resource-loader.js +64 -10
  156. package/dist/core/resource-loader.js.map +1 -1
  157. package/dist/core/runtime-credentials.d.ts +5 -5
  158. package/dist/core/runtime-credentials.d.ts.map +1 -1
  159. package/dist/core/runtime-credentials.js +11 -8
  160. package/dist/core/runtime-credentials.js.map +1 -1
  161. package/dist/core/session-manager.d.ts.map +1 -1
  162. package/dist/core/session-manager.js +3 -1
  163. package/dist/core/session-manager.js.map +1 -1
  164. package/dist/core/settings-manager.d.ts +12 -0
  165. package/dist/core/settings-manager.d.ts.map +1 -1
  166. package/dist/core/settings-manager.js +40 -16
  167. package/dist/core/settings-manager.js.map +1 -1
  168. package/dist/core/tools/bash.d.ts +4 -0
  169. package/dist/core/tools/bash.d.ts.map +1 -1
  170. package/dist/core/tools/bash.js +6 -4
  171. package/dist/core/tools/bash.js.map +1 -1
  172. package/dist/core/tools/edit.d.ts +4 -0
  173. package/dist/core/tools/edit.d.ts.map +1 -1
  174. package/dist/core/tools/edit.js +11 -7
  175. package/dist/core/tools/edit.js.map +1 -1
  176. package/dist/core/tools/find.d.ts +7 -0
  177. package/dist/core/tools/find.d.ts.map +1 -1
  178. package/dist/core/tools/find.js +16 -19
  179. package/dist/core/tools/find.js.map +1 -1
  180. package/dist/core/tools/grep.d.ts +4 -0
  181. package/dist/core/tools/grep.d.ts.map +1 -1
  182. package/dist/core/tools/grep.js +5 -1
  183. package/dist/core/tools/grep.js.map +1 -1
  184. package/dist/core/tools/ls.d.ts +4 -0
  185. package/dist/core/tools/ls.d.ts.map +1 -1
  186. package/dist/core/tools/ls.js +5 -1
  187. package/dist/core/tools/ls.js.map +1 -1
  188. package/dist/core/tools/read.d.ts +4 -0
  189. package/dist/core/tools/read.d.ts.map +1 -1
  190. package/dist/core/tools/read.js +7 -3
  191. package/dist/core/tools/read.js.map +1 -1
  192. package/dist/core/tools/render-utils.d.ts.map +1 -1
  193. package/dist/core/tools/render-utils.js +6 -3
  194. package/dist/core/tools/render-utils.js.map +1 -1
  195. package/dist/core/tools/write.d.ts +4 -0
  196. package/dist/core/tools/write.d.ts.map +1 -1
  197. package/dist/core/tools/write.js +6 -2
  198. package/dist/core/tools/write.js.map +1 -1
  199. package/dist/extensions/llama/index.d.ts.map +1 -1
  200. package/dist/extensions/llama/index.js +11 -2
  201. package/dist/extensions/llama/index.js.map +1 -1
  202. package/dist/extensions/llama/provider.d.ts.map +1 -1
  203. package/dist/extensions/llama/provider.js +22 -8
  204. package/dist/extensions/llama/provider.js.map +1 -1
  205. package/dist/index.d.ts +4 -4
  206. package/dist/index.d.ts.map +1 -1
  207. package/dist/index.js +1 -1
  208. package/dist/index.js.map +1 -1
  209. package/dist/main.d.ts.map +1 -1
  210. package/dist/main.js +94 -6
  211. package/dist/main.js.map +1 -1
  212. package/dist/modes/index.d.ts +1 -0
  213. package/dist/modes/index.d.ts.map +1 -1
  214. package/dist/modes/index.js.map +1 -1
  215. package/dist/modes/interactive/components/assistant-message.d.ts +5 -2
  216. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/assistant-message.js +13 -4
  218. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  219. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/custom-editor.js +7 -0
  221. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  222. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  223. package/dist/modes/interactive/components/footer.js +2 -2
  224. package/dist/modes/interactive/components/footer.js.map +1 -1
  225. package/dist/modes/interactive/components/markdown-transform.d.ts +3 -0
  226. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -0
  227. package/dist/modes/interactive/components/markdown-transform.js +19 -0
  228. package/dist/modes/interactive/components/markdown-transform.js.map +1 -0
  229. package/dist/modes/interactive/components/mermaid.d.ts +11 -0
  230. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  231. package/dist/modes/interactive/components/mermaid.js +75 -0
  232. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  233. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  234. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  235. package/dist/modes/interactive/components/model-selector.js +20 -5
  236. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  237. package/dist/modes/interactive/components/scoped-models-selector.d.ts +4 -0
  238. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  239. package/dist/modes/interactive/components/scoped-models-selector.js +26 -0
  240. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  241. package/dist/modes/interactive/components/settings-selector.d.ts +8 -2
  242. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  243. package/dist/modes/interactive/components/settings-selector.js +30 -0
  244. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  245. package/dist/modes/interactive/components/user-message.d.ts +3 -1
  246. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  247. package/dist/modes/interactive/components/user-message.js +9 -2
  248. package/dist/modes/interactive/components/user-message.js.map +1 -1
  249. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  250. package/dist/modes/interactive/external-editor.js +7 -5
  251. package/dist/modes/interactive/external-editor.js.map +1 -1
  252. package/dist/modes/interactive/interactive-mode.d.ts +36 -2
  253. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  254. package/dist/modes/interactive/interactive-mode.js +481 -150
  255. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  256. package/dist/modes/interactive/theme/dark.json +1 -0
  257. package/dist/modes/interactive/theme/light.json +1 -0
  258. package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
  259. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  260. package/dist/modes/interactive/theme/theme-controller.js +10 -1
  261. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  262. package/dist/modes/interactive/theme/theme-schema.json +5 -1
  263. package/dist/modes/interactive/theme/theme.d.ts +2 -2
  264. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  265. package/dist/modes/interactive/theme/theme.js +24 -6
  266. package/dist/modes/interactive/theme/theme.js.map +1 -1
  267. package/dist/modes/json-event.d.ts +28 -0
  268. package/dist/modes/json-event.d.ts.map +1 -0
  269. package/dist/modes/json-event.js +12 -0
  270. package/dist/modes/json-event.js.map +1 -0
  271. package/dist/modes/print-mode.d.ts.map +1 -1
  272. package/dist/modes/print-mode.js +12 -2
  273. package/dist/modes/print-mode.js.map +1 -1
  274. package/dist/modes/rpc/rpc-client.d.ts +5 -4
  275. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  276. package/dist/modes/rpc/rpc-client.js.map +1 -1
  277. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  278. package/dist/modes/rpc/rpc-mode.js +17 -3
  279. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  280. package/dist/package-manager-cli.d.ts.map +1 -1
  281. package/dist/package-manager-cli.js +11 -9
  282. package/dist/package-manager-cli.js.map +1 -1
  283. package/dist/rpc-entry.d.ts.map +1 -1
  284. package/dist/rpc-entry.js +1 -0
  285. package/dist/rpc-entry.js.map +1 -1
  286. package/dist/server/create-harness.d.ts +26 -0
  287. package/dist/server/create-harness.d.ts.map +1 -0
  288. package/dist/server/create-harness.js +104 -0
  289. package/dist/server/create-harness.js.map +1 -0
  290. package/dist/utils/abort.d.ts +5 -0
  291. package/dist/utils/abort.d.ts.map +1 -0
  292. package/dist/utils/abort.js +48 -0
  293. package/dist/utils/abort.js.map +1 -0
  294. package/dist/utils/clipboard.d.ts +1 -1
  295. package/dist/utils/clipboard.d.ts.map +1 -1
  296. package/dist/utils/clipboard.js +22 -2
  297. package/dist/utils/clipboard.js.map +1 -1
  298. package/dist/utils/management-http.d.ts +23 -0
  299. package/dist/utils/management-http.d.ts.map +1 -0
  300. package/dist/utils/management-http.js +50 -0
  301. package/dist/utils/management-http.js.map +1 -0
  302. package/dist/utils/paths.d.ts +3 -0
  303. package/dist/utils/paths.d.ts.map +1 -1
  304. package/dist/utils/paths.js +23 -1
  305. package/dist/utils/paths.js.map +1 -1
  306. package/dist/utils/tool-result-images.d.ts +19 -0
  307. package/dist/utils/tool-result-images.d.ts.map +1 -0
  308. package/dist/utils/tool-result-images.js +45 -0
  309. package/dist/utils/tool-result-images.js.map +1 -0
  310. package/dist/utils/tools-manager.d.ts.map +1 -1
  311. package/dist/utils/tools-manager.js +4 -6
  312. package/dist/utils/tools-manager.js.map +1 -1
  313. package/dist/utils/version-check.d.ts +4 -0
  314. package/dist/utils/version-check.d.ts.map +1 -1
  315. package/dist/utils/version-check.js +20 -2
  316. package/dist/utils/version-check.js.map +1 -1
  317. package/docs/compaction.md +2 -2
  318. package/docs/custom-provider.md +16 -8
  319. package/docs/extensions.md +33 -6
  320. package/docs/json.md +19 -14
  321. package/docs/keybindings.md +46 -14
  322. package/docs/models.md +24 -3
  323. package/docs/providers.md +10 -1
  324. package/docs/quickstart.md +2 -0
  325. package/docs/rpc.md +13 -11
  326. package/docs/sdk.md +21 -2
  327. package/docs/security.md +1 -1
  328. package/docs/session-format.md +2 -0
  329. package/docs/settings.md +4 -1
  330. package/docs/themes.md +5 -3
  331. package/docs/usage.md +7 -0
  332. package/examples/extensions/custom-compaction.ts +1 -16
  333. package/examples/extensions/custom-provider-anthropic/index.ts +10 -3
  334. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  335. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  336. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -1
  337. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  338. package/examples/extensions/doom-overlay/wad-finder.ts +7 -3
  339. package/examples/extensions/gondolin/package-lock.json +2 -2
  340. package/examples/extensions/gondolin/package.json +1 -1
  341. package/examples/extensions/handoff.ts +2 -11
  342. package/examples/extensions/qna.ts +3 -7
  343. package/examples/extensions/sandbox/package-lock.json +2 -2
  344. package/examples/extensions/sandbox/package.json +1 -1
  345. package/examples/extensions/summarize.ts +7 -18
  346. package/examples/extensions/with-deps/package-lock.json +2 -2
  347. package/examples/extensions/with-deps/package.json +1 -1
  348. package/examples/rpc-extension-ui.ts +11 -2
  349. package/examples/sdk/02-custom-model.ts +1 -2
  350. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  351. package/examples/sdk/12-full-control.ts +3 -1
  352. package/examples/sdk/README.md +1 -1
  353. package/package.json +14 -7
@@ -6,7 +6,8 @@ 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, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
9
+ import * as TuiLayouts from "@earendil-works/pi-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
10
11
  import chalk from "chalk";
11
12
  import { spawn, spawnSync } from "child_process";
12
13
  import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
@@ -16,7 +17,8 @@ import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, }
16
17
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
18
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
18
19
  import { KeybindingsManager } from "../../core/keybindings.js";
19
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope, resolveModelScopeWithDiagnostics, } from "../../core/model-resolver.js";
20
+ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScopeFromModels, } from "../../core/model-resolver.js";
21
+ import { CredentialSynchronizationError } from "../../core/model-runtime.js";
20
22
  import { DefaultPackageManager } from "../../core/package-manager.js";
21
23
  import { getPiClientUpdateMarkerPath, PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE, readPiClientUpdateMarker, writePiClientRuntimeReloadState, } from "../../core/pi-client-runtime-update.js";
22
24
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
@@ -29,6 +31,7 @@ import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelo
29
31
  import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
30
32
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
31
33
  import { parseGitUrl } from "../../utils/git.js";
34
+ import { openBrowser } from "../../utils/open-browser.js";
32
35
  import { getCwdRelativePath } from "../../utils/paths.js";
33
36
  import { getPiUserAgent } from "../../utils/pi-user-agent.js";
34
37
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
@@ -52,6 +55,7 @@ import { ExtensionSelectorComponent } from "./components/extension-selector.js";
52
55
  import { FooterComponent, formatTokens } from "./components/footer.js";
53
56
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
54
57
  import { LoginDialogComponent } from "./components/login-dialog.js";
58
+ import { createMermaidMarkdownTransformer } from "./components/mermaid.js";
55
59
  import { ModelSelectorComponent } from "./components/model-selector.js";
56
60
  import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
57
61
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
@@ -161,11 +165,58 @@ function formatLoginProviderCompletionDescription(provider) {
161
165
  const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
162
166
  return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
163
167
  }
168
+ /** Composition root for selecting the interactive terminal renderer. */
169
+ export function createInteractiveTui(options) {
170
+ const terminal = options.terminal ?? new ProcessTerminal();
171
+ if (options.tuiMode === "fullscreen") {
172
+ return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, {
173
+ openUrl: openBrowser,
174
+ onRightClickPaste: options.onRightClickPaste,
175
+ });
176
+ }
177
+ return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
178
+ }
179
+ /** Stable reference for components while InteractiveMode replaces the active renderer. */
180
+ export function createInteractiveTuiReference(getTui) {
181
+ return new Proxy({}, {
182
+ get: (_target, property) => {
183
+ const tui = getTui();
184
+ const value = Reflect.get(tui, property, tui);
185
+ if (typeof value !== "function")
186
+ return value;
187
+ let methodTui = tui;
188
+ let method = value;
189
+ return (...args) => {
190
+ const currentTui = getTui();
191
+ if (currentTui !== methodTui) {
192
+ const currentMethod = Reflect.get(currentTui, property, currentTui);
193
+ if (typeof currentMethod !== "function") {
194
+ throw new TypeError(`TUI property ${String(property)} is not callable`);
195
+ }
196
+ methodTui = currentTui;
197
+ method = currentMethod;
198
+ }
199
+ return Reflect.apply(method, methodTui, args);
200
+ };
201
+ },
202
+ set: (_target, property, value) => {
203
+ const tui = getTui();
204
+ return Reflect.set(tui, property, value, tui);
205
+ },
206
+ has: (_target, property) => Reflect.has(getTui(), property),
207
+ getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
208
+ });
209
+ }
164
210
  export class InteractiveMode {
165
211
  runtimeHost;
212
+ renderer;
166
213
  ui;
214
+ mainScreenRenderState;
167
215
  loadedResourcesContainer;
168
216
  chatContainer;
217
+ documentContainer;
218
+ transcriptScrollView;
219
+ fullscreenLayoutRoot;
169
220
  pendingMessagesContainer;
170
221
  statusContainer;
171
222
  defaultEditor;
@@ -175,7 +226,10 @@ export class InteractiveMode {
175
226
  autocompleteProviderWrappers = [];
176
227
  fdPath;
177
228
  editorContainer;
229
+ activeSelectorToken;
230
+ activeSelectorDispose;
178
231
  footer;
232
+ footerContainer;
179
233
  footerDataProvider;
180
234
  // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
181
235
  keybindings;
@@ -209,6 +263,10 @@ export class InteractiveMode {
209
263
  // Thinking block visibility state
210
264
  hideThinkingBlock = false;
211
265
  outputPad = 1;
266
+ mermaidMarkdownTransformer = createMermaidMarkdownTransformer({
267
+ getMode: () => this.settingsManager.getMermaidRenderingMode(),
268
+ theme,
269
+ });
212
270
  // Skill commands: command name -> skill file path
213
271
  skillCommands = new Map();
214
272
  // Agent subscription unsubscribe function
@@ -232,7 +290,7 @@ export class InteractiveMode {
232
290
  extensionSelector = undefined;
233
291
  extensionInput = undefined;
234
292
  extensionEditor = undefined;
235
- extensionTerminalInputUnsubscribers = new Set();
293
+ extensionTerminalInputSubscriptions = new Set();
236
294
  // Extension widgets (components rendered above/below the editor)
237
295
  extensionWidgetsAbove = new Map();
238
296
  extensionWidgetsBelow = new Map();
@@ -247,6 +305,9 @@ export class InteractiveMode {
247
305
  // Custom header from extension (undefined = use built-in header)
248
306
  customHeader = undefined;
249
307
  options;
308
+ onRightClickPaste = () => {
309
+ void this.handleRightClickPaste();
310
+ };
250
311
  autoTrustOnReloadCwd;
251
312
  themeController;
252
313
  // Convenience accessors
@@ -264,7 +325,8 @@ export class InteractiveMode {
264
325
  }
265
326
  constructor(runtimeHost, options = {}) {
266
327
  this.runtimeHost = runtimeHost;
267
- this.options = options;
328
+ const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
329
+ this.options = { ...options, tuiMode };
268
330
  this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
269
331
  this.runtimeHost.setBeforeSessionInvalidate(() => {
270
332
  this.resetExtensionUI();
@@ -273,11 +335,21 @@ export class InteractiveMode {
273
335
  await this.rebindCurrentSession({ renderBeforeBind: true });
274
336
  });
275
337
  this.version = VERSION;
276
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor(), getAgentDir());
338
+ this.renderer = createInteractiveTui({
339
+ tuiMode,
340
+ showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
341
+ logDirectory: getAgentDir(),
342
+ onRightClickPaste: this.onRightClickPaste,
343
+ });
344
+ this.ui = createInteractiveTuiReference(() => this.renderer);
277
345
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
278
346
  this.headerContainer = new Container();
279
347
  this.loadedResourcesContainer = new Container();
280
348
  this.chatContainer = new Container();
349
+ this.documentContainer = new Container();
350
+ this.documentContainer.addChild(this.headerContainer);
351
+ this.documentContainer.addChild(this.loadedResourcesContainer);
352
+ this.documentContainer.addChild(this.chatContainer);
281
353
  this.pendingMessagesContainer = new Container();
282
354
  this.statusContainer = new Container();
283
355
  this.widgetContainerAbove = new Container();
@@ -296,6 +368,8 @@ export class InteractiveMode {
296
368
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
297
369
  this.footer = new FooterComponent(this.session, this.footerDataProvider);
298
370
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
371
+ this.footerContainer = new Container();
372
+ this.footerContainer.addChild(this.footer);
299
373
  // Load hide thinking block setting
300
374
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
301
375
  this.outputPad = this.settingsManager.getOutputPad();
@@ -351,11 +425,10 @@ export class InteractiveMode {
351
425
  }));
352
426
  const modelCommand = slashCommands.find((command) => command.name === "model");
353
427
  if (modelCommand) {
354
- modelCommand.getArgumentCompletions = async (prefix) => {
355
- // Get available models (scoped or from registry)
428
+ modelCommand.getArgumentCompletions = (prefix) => {
356
429
  const models = this.session.scopedModels.length > 0
357
430
  ? this.session.scopedModels.map((s) => s.model)
358
- : await this.session.modelRuntime.getAvailable();
431
+ : this.session.modelRuntime.getAvailableSnapshot();
359
432
  if (models.length === 0)
360
433
  return null;
361
434
  // Create items with provider/id format
@@ -456,6 +529,73 @@ export class InteractiveMode {
456
529
  }
457
530
  this.chatContainer.addChild(new DynamicBorder());
458
531
  }
532
+ mountInteractiveTui(tui, components) {
533
+ for (const component of components)
534
+ tui.addChild(component);
535
+ if (TuiLayouts.isViewportTUI(tui)) {
536
+ if (!this.fullscreenLayoutRoot)
537
+ throw new Error("Fullscreen layout is not initialized");
538
+ tui.setLayoutRoot(this.fullscreenLayoutRoot);
539
+ }
540
+ }
541
+ stopInteractiveTui() {
542
+ if (this.renderer.mode === "fullscreen") {
543
+ while (this.renderer.hasOverlayEntries)
544
+ this.renderer.hideOverlay();
545
+ this.switchTuiMode("regular", false, false);
546
+ this.renderer.renderNow();
547
+ }
548
+ this.ui.stop();
549
+ }
550
+ switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
551
+ const previousUi = this.renderer;
552
+ if (mode === previousUi.mode)
553
+ return true;
554
+ if (previousUi.hasOverlayEntries)
555
+ return false;
556
+ const components = [...previousUi.children];
557
+ const focus = previousUi.getFocusedComponent();
558
+ const terminal = previousUi.terminal;
559
+ const showHardwareCursor = previousUi.getShowHardwareCursor();
560
+ const clearOnShrink = previousUi.getClearOnShrink();
561
+ const onDebug = previousUi.onDebug;
562
+ if (previousUi instanceof TuiMainScreen) {
563
+ this.mainScreenRenderState = previousUi.captureRenderState();
564
+ }
565
+ previousUi.stop({ preserveScreen: true });
566
+ previousUi.setFocus(null);
567
+ previousUi.clear();
568
+ if (TuiLayouts.isViewportTUI(previousUi))
569
+ previousUi.setLayoutRoot(undefined);
570
+ const nextUi = createInteractiveTui({
571
+ tuiMode: mode,
572
+ showHardwareCursor,
573
+ logDirectory: getAgentDir(),
574
+ terminal,
575
+ onRightClickPaste: this.onRightClickPaste,
576
+ });
577
+ nextUi.setClearOnShrink(clearOnShrink);
578
+ nextUi.onDebug = onDebug;
579
+ if (nextUi instanceof TuiMainScreen && this.mainScreenRenderState) {
580
+ nextUi.restoreRenderState(this.mainScreenRenderState);
581
+ }
582
+ this.renderer = nextUi;
583
+ this.options.tuiMode = mode;
584
+ this.mountInteractiveTui(nextUi, components);
585
+ nextUi.invalidate();
586
+ nextUi.setFocus(focus);
587
+ if (!startRenderer)
588
+ return true;
589
+ nextUi.start();
590
+ this.themeController.rebindTui();
591
+ this.rebindExtensionTerminalInputListeners();
592
+ if (restoreProgress &&
593
+ this.settingsManager.getShowTerminalProgress() &&
594
+ (this.session.isStreaming || this.session.isCompacting)) {
595
+ terminal.setProgress(true);
596
+ }
597
+ return true;
598
+ }
459
599
  async init() {
460
600
  if (this.isInitialized)
461
601
  return;
@@ -479,18 +619,36 @@ export class InteractiveMode {
479
619
  : "";
480
620
  console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
481
621
  }
482
- // Add header container as first child. Populate it after applying theme settings.
483
- // Keep loaded resources before chat so restored session messages never precede them.
484
- this.ui.addChild(this.headerContainer);
485
- this.ui.addChild(this.loadedResourcesContainer);
486
- this.ui.addChild(this.chatContainer);
487
- this.ui.addChild(this.pendingMessagesContainer);
488
- this.ui.addChild(this.statusContainer);
622
+ // Keep one component tree and remount it when changing renderers.
489
623
  this.renderWidgets(); // Initialize with default spacer
490
- this.ui.addChild(this.widgetContainerAbove);
491
- this.ui.addChild(this.editorContainer);
492
- this.ui.addChild(this.widgetContainerBelow);
493
- this.ui.addChild(this.footer);
624
+ this.transcriptScrollView = new TuiLayouts.ScrollView(this.documentContainer, {
625
+ follow: "end",
626
+ primary: true,
627
+ overscroll: "chain",
628
+ scrollbar: this.settingsManager.getFullscreenScrollbar(),
629
+ scrollbarStyle: (text) => theme.bg("scrollbarThumb", text),
630
+ });
631
+ const dock = new TuiLayouts.VStack([
632
+ { component: this.pendingMessagesContainer, shrink: 1, minSize: 0 },
633
+ { component: this.statusContainer, shrink: 1, minSize: 0 },
634
+ { component: this.widgetContainerAbove, shrink: 1, minSize: 0 },
635
+ { component: this.editorContainer, shrink: 1, minSize: 3 },
636
+ { component: this.widgetContainerBelow, shrink: 1, minSize: 0 },
637
+ { component: this.footerContainer, shrink: 1, minSize: 1 },
638
+ ]);
639
+ this.fullscreenLayoutRoot = new TuiLayouts.VStack([
640
+ { component: this.transcriptScrollView, basis: 0, grow: 1, shrink: 1, minSize: 1 },
641
+ { component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
642
+ ]);
643
+ this.mountInteractiveTui(this.renderer, [
644
+ this.documentContainer,
645
+ this.pendingMessagesContainer,
646
+ this.statusContainer,
647
+ this.widgetContainerAbove,
648
+ this.editorContainer,
649
+ this.widgetContainerBelow,
650
+ this.footerContainer,
651
+ ]);
494
652
  this.ui.setFocus(this.editor);
495
653
  this.setupKeyHandlers();
496
654
  this.setupEditorSubmitHandler();
@@ -582,10 +740,13 @@ export class InteractiveMode {
582
740
  async run() {
583
741
  await this.init();
584
742
  if (!process.env.PI_OFFLINE) {
743
+ const controller = new AbortController();
744
+ const timeout = setTimeout(() => controller.abort(), 15_000);
585
745
  void this.session.modelRuntime
586
- .refresh()
746
+ .refresh({ signal: controller.signal })
587
747
  .then(() => this.updateAvailableProviderCount())
588
- .catch(() => { });
748
+ .catch(() => { })
749
+ .finally(() => clearTimeout(timeout));
589
750
  }
590
751
  // Start version check asynchronously
591
752
  checkForNewPiVersion(this.version).then((newRelease) => {
@@ -769,13 +930,14 @@ export class InteractiveMode {
769
930
  // Extension System
770
931
  // =========================================================================
771
932
  formatDisplayPath(p) {
772
- const home = os.homedir();
773
- let result = p;
774
- // Replace home directory with ~
775
- if (result.startsWith(home)) {
776
- result = `~${result.slice(home.length)}`;
933
+ const normalizedPath = p.replaceAll("\\", "/");
934
+ const normalizedHome = os.homedir().replaceAll("\\", "/").replace(/\/+$/, "");
935
+ if (normalizedPath === normalizedHome)
936
+ return "~";
937
+ if (normalizedPath.startsWith(`${normalizedHome}/`)) {
938
+ return `~/${normalizedPath.slice(normalizedHome.length + 1)}`;
777
939
  }
778
- return result;
940
+ return p;
779
941
  }
780
942
  formatExtensionDisplayPath(path) {
781
943
  let result = this.formatDisplayPath(path);
@@ -1106,7 +1268,12 @@ export class InteractiveMode {
1106
1268
  }
1107
1269
  }
1108
1270
  if (showListing) {
1109
- const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1271
+ const systemPromptSource = this.session.resourceLoader.getSystemPromptSource();
1272
+ const contextFiles = [
1273
+ ...(systemPromptSource ? [systemPromptSource] : []),
1274
+ ...this.session.resourceLoader.getAppendSystemPromptSources(),
1275
+ ...this.session.resourceLoader.getAgentsFiles().agentsFiles,
1276
+ ];
1110
1277
  if (contextFiles.length > 0) {
1111
1278
  this.loadedResourcesContainer.addChild(new Spacer(1));
1112
1279
  const contextList = contextFiles
@@ -1283,8 +1450,12 @@ export class InteractiveMode {
1283
1450
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1284
1451
  this.showStartupNoticesIfNeeded();
1285
1452
  }
1453
+ applyFullscreenScrollbarSetting() {
1454
+ this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1455
+ }
1286
1456
  applyRuntimeSettings() {
1287
1457
  configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1458
+ this.applyFullscreenScrollbarSetting();
1288
1459
  this.footer.setSession(this.session);
1289
1460
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1290
1461
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
@@ -1306,16 +1477,19 @@ export class InteractiveMode {
1306
1477
  }
1307
1478
  }
1308
1479
  async rebindCurrentSession(options = {}) {
1480
+ const session = this.session;
1309
1481
  this.unsubscribe?.();
1310
1482
  this.unsubscribe = undefined;
1311
1483
  this.applyRuntimeSettings();
1312
1484
  if (options.renderBeforeBind) {
1313
1485
  this.renderCurrentSessionState();
1314
1486
  this.subscribeToAgent();
1315
- await this.bindCurrentSessionExtensions();
1316
1487
  }
1317
- else {
1318
- await this.bindCurrentSessionExtensions();
1488
+ await this.bindCurrentSessionExtensions();
1489
+ if (this.session !== session) {
1490
+ return;
1491
+ }
1492
+ if (!options.renderBeforeBind) {
1319
1493
  this.subscribeToAgent();
1320
1494
  }
1321
1495
  await this.updateAvailableProviderCount();
@@ -1345,6 +1519,9 @@ export class InteractiveMode {
1345
1519
  getRegisteredToolDefinition(toolName) {
1346
1520
  return this.session.getToolDefinition(toolName);
1347
1521
  }
1522
+ getMarkdownTransformers() {
1523
+ return [this.mermaidMarkdownTransformer, ...this.session.extensionRunner.getMarkdownTransformers()];
1524
+ }
1348
1525
  /**
1349
1526
  * Set up keyboard shortcuts registered by extensions.
1350
1527
  */
@@ -1361,6 +1538,7 @@ export class InteractiveMode {
1361
1538
  sessionManager: this.sessionManager,
1362
1539
  modelRegistry: extensionRunner.getModelRegistry(),
1363
1540
  model: this.session.model,
1541
+ scopedModels: this.session.scopedModels,
1364
1542
  thinkingLevel: this.session.thinkingLevel,
1365
1543
  isIdle: () => this.session.isIdle,
1366
1544
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
@@ -1423,7 +1601,7 @@ export class InteractiveMode {
1423
1601
  this.activeStatusIndicator?.dispose();
1424
1602
  this.activeStatusIndicator = undefined;
1425
1603
  this.statusContainer.clear();
1426
- if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1604
+ if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
1427
1605
  this.statusContainer.addChild(this.idleStatus);
1428
1606
  }
1429
1607
  }
@@ -1580,22 +1758,16 @@ export class InteractiveMode {
1580
1758
  if (this.customFooter?.dispose) {
1581
1759
  this.customFooter.dispose();
1582
1760
  }
1583
- // Remove current footer from UI
1584
- if (this.customFooter) {
1585
- this.ui.removeChild(this.customFooter);
1586
- }
1587
- else {
1588
- this.ui.removeChild(this.footer);
1589
- }
1761
+ this.footerContainer.clear();
1590
1762
  if (factory) {
1591
1763
  // Create and add custom footer, passing the data provider
1592
1764
  this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1593
- this.ui.addChild(this.customFooter);
1765
+ this.footerContainer.addChild(this.customFooter);
1594
1766
  }
1595
1767
  else {
1596
1768
  // Restore built-in footer
1597
1769
  this.customFooter = undefined;
1598
- this.ui.addChild(this.footer);
1770
+ this.footerContainer.addChild(this.footer);
1599
1771
  }
1600
1772
  this.ui.requestRender();
1601
1773
  }
@@ -1641,18 +1813,23 @@ export class InteractiveMode {
1641
1813
  this.ui.requestRender();
1642
1814
  }
1643
1815
  addExtensionTerminalInputListener(handler) {
1644
- const unsubscribe = this.ui.addInputListener(handler);
1645
- this.extensionTerminalInputUnsubscribers.add(unsubscribe);
1816
+ const subscription = { handler, unsubscribe: this.ui.addInputListener(handler) };
1817
+ this.extensionTerminalInputSubscriptions.add(subscription);
1646
1818
  return () => {
1647
- unsubscribe();
1648
- this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
1819
+ subscription.unsubscribe();
1820
+ this.extensionTerminalInputSubscriptions.delete(subscription);
1649
1821
  };
1650
1822
  }
1651
- clearExtensionTerminalInputListeners() {
1652
- for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
1653
- unsubscribe();
1823
+ rebindExtensionTerminalInputListeners() {
1824
+ for (const subscription of this.extensionTerminalInputSubscriptions) {
1825
+ subscription.unsubscribe();
1826
+ subscription.unsubscribe = this.ui.addInputListener(subscription.handler);
1654
1827
  }
1655
- this.extensionTerminalInputUnsubscribers.clear();
1828
+ }
1829
+ clearExtensionTerminalInputListeners() {
1830
+ for (const subscription of this.extensionTerminalInputSubscriptions)
1831
+ subscription.unsubscribe();
1832
+ this.extensionTerminalInputSubscriptions.clear();
1656
1833
  }
1657
1834
  /**
1658
1835
  * Create the ExtensionUIContext for extensions.
@@ -1747,6 +1924,7 @@ export class InteractiveMode {
1747
1924
  this.hideExtensionSelector();
1748
1925
  resolve(undefined);
1749
1926
  }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1927
+ this.disposeActiveSelector();
1750
1928
  this.editorContainer.clear();
1751
1929
  this.editorContainer.addChild(this.extensionSelector);
1752
1930
  this.ui.setFocus(this.extensionSelector);
@@ -1798,6 +1976,7 @@ export class InteractiveMode {
1798
1976
  this.hideExtensionInput();
1799
1977
  resolve(undefined);
1800
1978
  }, { tui: this.ui, timeout: opts?.timeout });
1979
+ this.disposeActiveSelector();
1801
1980
  this.editorContainer.clear();
1802
1981
  this.editorContainer.addChild(this.extensionInput);
1803
1982
  this.ui.setFocus(this.extensionInput);
@@ -1827,6 +2006,7 @@ export class InteractiveMode {
1827
2006
  this.hideExtensionEditor();
1828
2007
  resolve(undefined);
1829
2008
  }, undefined, this.settingsManager.getExternalEditorCommand());
2009
+ this.disposeActiveSelector();
1830
2010
  this.editorContainer.clear();
1831
2011
  this.editorContainer.addChild(this.extensionEditor);
1832
2012
  this.ui.setFocus(this.extensionEditor);
@@ -1851,6 +2031,7 @@ export class InteractiveMode {
1851
2031
  this.editorComponentFactory = factory;
1852
2032
  // Save text from current editor before switching
1853
2033
  const currentText = this.editor.getText();
2034
+ this.disposeActiveSelector();
1854
2035
  this.editorContainer.clear();
1855
2036
  if (factory) {
1856
2037
  // Create the custom editor with tui, theme, and keybindings
@@ -1867,6 +2048,9 @@ export class InteractiveMode {
1867
2048
  if (newEditor.setPaddingX !== undefined) {
1868
2049
  newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1869
2050
  }
2051
+ if (newEditor.setAutocompleteMaxVisible !== undefined) {
2052
+ newEditor.setAutocompleteMaxVisible(this.defaultEditor.getAutocompleteMaxVisible());
2053
+ }
1870
2054
  // Set autocomplete if supported
1871
2055
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1872
2056
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1971,6 +2155,7 @@ export class InteractiveMode {
1971
2155
  options?.onHandle?.(handle);
1972
2156
  }
1973
2157
  else {
2158
+ this.disposeActiveSelector();
1974
2159
  this.editorContainer.clear();
1975
2160
  this.editorContainer.addChild(component);
1976
2161
  this.ui.setFocus(component);
@@ -2057,7 +2242,7 @@ export class InteractiveMode {
2057
2242
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2058
2243
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2059
2244
  this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2060
- this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2245
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true }));
2061
2246
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2062
2247
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2063
2248
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2077,6 +2262,22 @@ export class InteractiveMode {
2077
2262
  void this.handleClipboardPaste();
2078
2263
  };
2079
2264
  }
2265
+ async handleRightClickPaste() {
2266
+ const target = this.renderer.getFocusedComponent();
2267
+ const handleInput = target?.handleInput;
2268
+ if (!target || !handleInput)
2269
+ return;
2270
+ try {
2271
+ const text = await readClipboardText();
2272
+ if (!text || this.renderer.getFocusedComponent() !== target)
2273
+ return;
2274
+ handleInput.call(target, `\x1b[200~${text}\x1b[201~`);
2275
+ this.ui.requestRender();
2276
+ }
2277
+ catch {
2278
+ // Silently ignore clipboard errors (may not have permission, etc.)
2279
+ }
2280
+ }
2080
2281
  async handleClipboardPaste() {
2081
2282
  try {
2082
2283
  const image = await readClipboardImage();
@@ -2351,17 +2552,17 @@ export class InteractiveMode {
2351
2552
  this.ui.requestRender();
2352
2553
  }
2353
2554
  else if (event.message.role === "assistant") {
2354
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2555
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2355
2556
  this.streamingMessage = event.message;
2356
2557
  this.chatContainer.addChild(this.streamingComponent);
2357
- this.streamingComponent.updateContent(this.streamingMessage);
2558
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2358
2559
  this.ui.requestRender();
2359
2560
  }
2360
2561
  break;
2361
2562
  case "message_update":
2362
2563
  if (this.streamingComponent && event.message.role === "assistant") {
2363
2564
  this.streamingMessage = event.message;
2364
- this.streamingComponent.updateContent(this.streamingMessage);
2565
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2365
2566
  for (const content of this.streamingMessage.content) {
2366
2567
  if (content.type === "toolCall") {
2367
2568
  if (!this.pendingTools.has(content.id)) {
@@ -2398,7 +2599,7 @@ export class InteractiveMode {
2398
2599
  : "Operation aborted";
2399
2600
  this.streamingMessage.errorMessage = errorMessage;
2400
2601
  }
2401
- this.streamingComponent.updateContent(this.streamingMessage);
2602
+ this.streamingComponent.updateContent(this.streamingMessage, false);
2402
2603
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2403
2604
  if (!errorMessage) {
2404
2605
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2673,12 +2874,12 @@ export class InteractiveMode {
2673
2874
  // Render user message separately if present
2674
2875
  if (skillBlock.userMessage) {
2675
2876
  this.chatContainer.addChild(new Spacer(1));
2676
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2877
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2677
2878
  this.chatContainer.addChild(userComponent);
2678
2879
  }
2679
2880
  }
2680
2881
  else {
2681
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2882
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2682
2883
  this.chatContainer.addChild(userComponent);
2683
2884
  }
2684
2885
  if (options?.populateHistory) {
@@ -2688,7 +2889,7 @@ export class InteractiveMode {
2688
2889
  break;
2689
2890
  }
2690
2891
  case "assistant": {
2691
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2892
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2692
2893
  this.chatContainer.addChild(assistantComponent);
2693
2894
  break;
2694
2895
  }
@@ -3122,6 +3323,8 @@ export class InteractiveMode {
3122
3323
  this.setToolsExpanded(!this.toolOutputExpanded);
3123
3324
  }
3124
3325
  setToolsExpanded(expanded) {
3326
+ if (expanded === this.toolOutputExpanded)
3327
+ return;
3125
3328
  this.toolOutputExpanded = expanded;
3126
3329
  const activeHeader = this.customHeader ?? this.builtInHeader;
3127
3330
  if (isExpandable(activeHeader)) {
@@ -3134,7 +3337,7 @@ export class InteractiveMode {
3134
3337
  }
3135
3338
  }
3136
3339
  }
3137
- this.ui.requestRender();
3340
+ this.showStatus(`Tool output: ${expanded ? "expanded" : "collapsed"}`);
3138
3341
  }
3139
3342
  toggleThinkingBlockVisibility() {
3140
3343
  this.hideThinkingBlock = !this.hideThinkingBlock;
@@ -3177,7 +3380,7 @@ export class InteractiveMode {
3177
3380
  }
3178
3381
  showError(errorMessage) {
3179
3382
  this.chatContainer.addChild(new Spacer(1));
3180
- this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3383
+ this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), this.outputPad, 0));
3181
3384
  this.ui.requestRender();
3182
3385
  }
3183
3386
  showWarning(warningMessage) {
@@ -3387,25 +3590,43 @@ export class InteractiveMode {
3387
3590
  // =========================================================================
3388
3591
  // Selectors
3389
3592
  // =========================================================================
3593
+ disposeActiveSelector() {
3594
+ const dispose = this.activeSelectorDispose;
3595
+ this.activeSelectorToken = undefined;
3596
+ this.activeSelectorDispose = undefined;
3597
+ dispose?.();
3598
+ }
3390
3599
  /**
3391
3600
  * Shows a selector component in place of the editor.
3392
3601
  * @param create Factory that receives a `done` callback and returns the component and focus target
3393
3602
  */
3394
3603
  showSelector(create) {
3604
+ const token = {};
3605
+ let dispose;
3395
3606
  const done = () => {
3607
+ dispose?.();
3608
+ if (this.activeSelectorToken !== token)
3609
+ return;
3610
+ this.activeSelectorToken = undefined;
3611
+ this.activeSelectorDispose = undefined;
3396
3612
  this.editorContainer.clear();
3397
3613
  this.editorContainer.addChild(this.editor);
3398
3614
  this.ui.setFocus(this.editor);
3399
3615
  };
3400
- const { component, focus } = create(done);
3616
+ const created = create(done);
3617
+ dispose = created.dispose;
3618
+ this.disposeActiveSelector();
3619
+ this.activeSelectorToken = token;
3620
+ this.activeSelectorDispose = dispose;
3401
3621
  this.editorContainer.clear();
3402
- this.editorContainer.addChild(component);
3403
- this.ui.setFocus(focus);
3622
+ this.editorContainer.addChild(created.component);
3623
+ this.ui.setFocus(created.focus);
3404
3624
  this.ui.requestRender();
3405
3625
  }
3406
3626
  showSettingsSelector() {
3407
3627
  this.showSelector((done) => {
3408
- const selector = new SettingsSelectorComponent({
3628
+ let selector;
3629
+ selector = new SettingsSelectorComponent({
3409
3630
  autoCompact: this.session.autoCompactionEnabled,
3410
3631
  showImages: this.settingsManager.getShowImages(),
3411
3632
  imageWidthCells: this.settingsManager.getImageWidthCells(),
@@ -3422,6 +3643,7 @@ export class InteractiveMode {
3422
3643
  terminalTheme: this.themeController.getTerminalTheme(),
3423
3644
  availableThemes: getAvailableThemes(),
3424
3645
  hideThinkingBlock: this.hideThinkingBlock,
3646
+ mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
3425
3647
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3426
3648
  enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
3427
3649
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
@@ -3435,6 +3657,8 @@ export class InteractiveMode {
3435
3657
  quietStartup: this.settingsManager.getQuietStartup(),
3436
3658
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3437
3659
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3660
+ tuiMode: this.ui.mode,
3661
+ fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3438
3662
  warnings: this.settingsManager.getWarnings(),
3439
3663
  }, {
3440
3664
  onAutoCompactChange: (enabled) => {
@@ -3503,6 +3727,11 @@ export class InteractiveMode {
3503
3727
  this.chatContainer.clear();
3504
3728
  this.rebuildChatFromMessages();
3505
3729
  },
3730
+ onMermaidRenderingModeChange: (mode) => {
3731
+ this.settingsManager.setMermaidRenderingMode(mode);
3732
+ this.chatContainer.invalidate();
3733
+ this.ui.requestRender();
3734
+ },
3506
3735
  onShowCacheMissNoticesChange: (shown) => {
3507
3736
  this.settingsManager.setShowCacheMissNotices(shown);
3508
3737
  this.rebuildChatFromMessages();
@@ -3572,6 +3801,21 @@ export class InteractiveMode {
3572
3801
  onShowTerminalProgressChange: (enabled) => {
3573
3802
  this.settingsManager.setShowTerminalProgress(enabled);
3574
3803
  },
3804
+ onTuiModeChange: (mode) => {
3805
+ if (!this.switchTuiMode(mode)) {
3806
+ selector?.getSettingsList().updateValue("tui-mode", this.ui.mode);
3807
+ this.showStatus("Close active overlays before changing TUI mode");
3808
+ return;
3809
+ }
3810
+ this.settingsManager.setTuiMode(mode);
3811
+ if (!this.activeStatusIndicator)
3812
+ this.statusContainer.clear();
3813
+ this.showStatus(`TUI mode: ${mode}`);
3814
+ },
3815
+ onFullscreenScrollbarChange: (mode) => {
3816
+ this.settingsManager.setFullscreenScrollbar(mode);
3817
+ this.applyFullscreenScrollbarSetting();
3818
+ },
3575
3819
  onWarningsChange: (warnings) => {
3576
3820
  this.settingsManager.setWarnings(warnings);
3577
3821
  },
@@ -3606,20 +3850,37 @@ export class InteractiveMode {
3606
3850
  this.showModelSelector(searchTerm);
3607
3851
  }
3608
3852
  async findExactModelMatch(searchTerm) {
3609
- const models = await this.getModelCandidates();
3610
- return findExactModelReferenceMatch(searchTerm, models);
3611
- }
3612
- async getModelCandidates() {
3613
- if (this.session.scopedModels.length > 0) {
3614
- return this.session.scopedModels.map((scoped) => scoped.model);
3615
- }
3853
+ const cachedModels = this.session.scopedModels.length > 0
3854
+ ? this.session.scopedModels.map((scoped) => scoped.model)
3855
+ : [...this.session.modelRuntime.getAvailableSnapshot()];
3856
+ const cachedMatch = findExactModelReferenceMatch(searchTerm, cachedModels);
3857
+ if (cachedMatch || this.session.scopedModels.length > 0)
3858
+ return cachedMatch;
3859
+ this.showStatus("Refreshing model catalogs…");
3860
+ const controller = new AbortController();
3861
+ let timedOut = false;
3862
+ const timeout = setTimeout(() => {
3863
+ timedOut = true;
3864
+ controller.abort();
3865
+ }, 15_000);
3616
3866
  try {
3617
- await this.session.modelRuntime.refresh();
3618
- return [...(await this.session.modelRuntime.getAvailable())];
3867
+ const result = await this.session.modelRuntime.refresh({ signal: controller.signal });
3868
+ if (result.aborted && timedOut) {
3869
+ this.showWarning("Model refresh timed out; searching cached models.");
3870
+ }
3871
+ else if (result.errors.size > 0) {
3872
+ this.showWarning(`Could not refresh ${[...result.errors.keys()].join(", ")}; searching cached models.`);
3873
+ }
3619
3874
  }
3620
- catch {
3621
- return [];
3875
+ catch (error) {
3876
+ this.showWarning(timedOut
3877
+ ? "Model refresh timed out; searching cached models."
3878
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`);
3879
+ }
3880
+ finally {
3881
+ clearTimeout(timeout);
3622
3882
  }
3883
+ return findExactModelReferenceMatch(searchTerm, [...this.session.modelRuntime.getAvailableSnapshot()]);
3623
3884
  }
3624
3885
  /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3625
3886
  updateAvailableProviderCount() {
@@ -3721,73 +3982,67 @@ export class InteractiveMode {
3721
3982
  done();
3722
3983
  this.ui.requestRender();
3723
3984
  }, initialSearchInput);
3724
- return { component: selector, focus: selector };
3985
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
3725
3986
  });
3726
3987
  }
3727
- async showModelsSelector() {
3728
- // Get all available models
3729
- await this.session.modelRuntime.refresh();
3730
- const allModels = [...(await this.session.modelRuntime.getAvailable())];
3731
- const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
3988
+ showModelsSelector() {
3989
+ let availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
3990
+ let availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
3732
3991
  const configuredPatterns = this.settingsManager.getEnabledModels();
3733
3992
  const sessionScopedModels = this.session.scopedModels;
3734
- if (allModels.length === 0 && !configuredPatterns?.length && sessionScopedModels.length === 0) {
3735
- this.showStatus("No models available");
3736
- return;
3737
- }
3738
- const configuredScope = configuredPatterns?.length
3739
- ? await resolveModelScopeWithDiagnostics(configuredPatterns, this.session.modelRuntime)
3740
- : undefined;
3741
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3742
- const hasSessionScope = sessionScopedModels.length > 0;
3743
- // Build enabled model IDs from session state or settings
3744
- let currentEnabledIds = null;
3745
- if (hasSessionScope) {
3746
- // Use current session's scoped models
3747
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3748
- }
3749
- else if (configuredScope) {
3750
- currentEnabledIds = configuredScope.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3751
- }
3752
- for (const diagnostic of configuredScope?.diagnostics ?? []) {
3753
- if (diagnostic.code !== "no-match")
3754
- continue;
3755
- currentEnabledIds ??= [];
3756
- if (!currentEnabledIds.includes(diagnostic.pattern))
3757
- currentEnabledIds.push(diagnostic.pattern);
3758
- }
3759
- // Helper to update session's scoped models (session-only, no persist)
3760
- const updateSessionModels = async (enabledIds) => {
3993
+ const configuredEnabledIds = (models) => {
3994
+ if (!configuredPatterns?.length)
3995
+ return null;
3996
+ const resolved = resolveModelScopeFromModels(configuredPatterns, models);
3997
+ const ids = resolved.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3998
+ for (const diagnostic of resolved.diagnostics) {
3999
+ if (diagnostic.code === "no-match" && !ids.includes(diagnostic.pattern))
4000
+ ids.push(diagnostic.pattern);
4001
+ }
4002
+ return ids;
4003
+ };
4004
+ let currentEnabledIds = sessionScopedModels.length > 0
4005
+ ? sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`)
4006
+ : configuredEnabledIds(availableModels);
4007
+ let selectionChanged = false;
4008
+ const updateSessionModels = (enabledIds) => {
3761
4009
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3762
- const hasEnabledAvailableModel = enabledIds?.some((id) => allModelIds.has(id)) ?? false;
3763
- const allAvailableModelsEnabled = enabledIds !== null && [...allModelIds].every((id) => enabledIds.includes(id));
4010
+ const hasEnabledAvailableModel = enabledIds?.some((id) => availableModelIds.has(id)) ?? false;
4011
+ const allAvailableModelsEnabled = enabledIds !== null && [...availableModelIds].every((id) => enabledIds.includes(id));
3764
4012
  if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
3765
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3766
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3767
- model: sm.model,
3768
- thinkingLevel: sm.thinkingLevel,
4013
+ const newScopedModels = resolveModelScopeFromModels(enabledIds, availableModels).scopedModels;
4014
+ this.session.setScopedModels(newScopedModels.map((scoped) => ({
4015
+ model: scoped.model,
4016
+ thinkingLevel: scoped.thinkingLevel,
3769
4017
  })));
3770
4018
  }
3771
4019
  else {
3772
- // All enabled or none enabled = no filter
3773
4020
  this.session.setScopedModels([]);
3774
4021
  }
3775
- await this.updateAvailableProviderCount();
4022
+ this.updateAvailableProviderCount();
3776
4023
  this.ui.requestRender();
3777
4024
  };
3778
4025
  this.showSelector((done) => {
4026
+ let disposed = false;
4027
+ let timedOut = false;
4028
+ const controller = new AbortController();
4029
+ const timeout = setTimeout(() => {
4030
+ timedOut = true;
4031
+ controller.abort();
4032
+ }, 15_000);
3779
4033
  const selector = new ScopedModelsSelectorComponent({
3780
- allModels,
4034
+ allModels: availableModels,
3781
4035
  enabledModelIds: currentEnabledIds,
4036
+ refreshStatus: "Refreshing model catalogs…",
3782
4037
  }, {
3783
- onChange: async (enabledIds) => {
3784
- await updateSessionModels(enabledIds);
4038
+ onChange: (enabledIds) => {
4039
+ selectionChanged = true;
4040
+ updateSessionModels(enabledIds);
3785
4041
  },
3786
4042
  onPersist: (enabledIds) => {
3787
- // Persist to settings
3788
4043
  const allEnabled = enabledIds !== null &&
3789
- enabledIds.length === allModels.length &&
3790
- enabledIds.every((id) => allModelIds.has(id));
4044
+ enabledIds.length === availableModels.length &&
4045
+ enabledIds.every((id) => availableModelIds.has(id));
3791
4046
  const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3792
4047
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3793
4048
  this.showStatus("Model selection saved to settings");
@@ -3797,7 +4052,51 @@ export class InteractiveMode {
3797
4052
  this.ui.requestRender();
3798
4053
  },
3799
4054
  });
3800
- return { component: selector, focus: selector };
4055
+ void this.session.modelRuntime
4056
+ .refresh({ signal: controller.signal })
4057
+ .then((result) => {
4058
+ if (disposed)
4059
+ return;
4060
+ availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
4061
+ availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
4062
+ if (!selectionChanged && sessionScopedModels.length === 0) {
4063
+ currentEnabledIds = configuredEnabledIds(availableModels);
4064
+ selector.updateModels(availableModels, currentEnabledIds);
4065
+ }
4066
+ else {
4067
+ selector.updateModels(availableModels);
4068
+ }
4069
+ if (currentEnabledIds !== null)
4070
+ updateSessionModels(currentEnabledIds);
4071
+ if (result.aborted && timedOut) {
4072
+ selector.setRefreshStatus("Model refresh timed out; showing cached models.", "warning");
4073
+ }
4074
+ else if (result.errors.size > 0) {
4075
+ selector.setRefreshStatus(`Could not refresh ${[...result.errors.keys()].join(", ")}; showing cached models.`, "warning");
4076
+ }
4077
+ else {
4078
+ selector.setRefreshStatus("Model catalogs refreshed.", "success");
4079
+ }
4080
+ this.ui.requestRender();
4081
+ })
4082
+ .catch((error) => {
4083
+ if (disposed)
4084
+ return;
4085
+ selector.setRefreshStatus(timedOut
4086
+ ? "Model refresh timed out; showing cached models."
4087
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
4088
+ this.ui.requestRender();
4089
+ })
4090
+ .finally(() => clearTimeout(timeout));
4091
+ return {
4092
+ component: selector,
4093
+ focus: selector,
4094
+ dispose: () => {
4095
+ disposed = true;
4096
+ clearTimeout(timeout);
4097
+ controller.abort();
4098
+ },
4099
+ };
3801
4100
  });
3802
4101
  }
3803
4102
  showUserMessageSelector() {
@@ -3859,7 +4158,7 @@ export class InteractiveMode {
3859
4158
  this.showSelector((done) => {
3860
4159
  const selector = new TreeSelectorComponent(tree, realLeafId, this.ui.terminal.rows, async (entryId) => {
3861
4160
  // Selecting the current leaf is a no-op (already there)
3862
- if (entryId === realLeafId) {
4161
+ if (entryId === this.sessionManager.getLeafId()) {
3863
4162
  done();
3864
4163
  this.showStatus("Already at this point");
3865
4164
  return;
@@ -3894,6 +4193,11 @@ export class InteractiveMode {
3894
4193
  break;
3895
4194
  }
3896
4195
  }
4196
+ // The user committed to navigating: stop the active response first.
4197
+ if (this.session.isStreaming) {
4198
+ this.restoreQueuedMessagesToEditor();
4199
+ await this.session.abort();
4200
+ }
3897
4201
  // Set up escape handler and status indicator if summarizing
3898
4202
  let showingSummaryIndicator = false;
3899
4203
  const originalOnEscape = this.defaultEditor.onEscape;
@@ -4054,7 +4358,7 @@ export class InteractiveMode {
4054
4358
  return options.sort((a, b) => a.name.localeCompare(b.name));
4055
4359
  }
4056
4360
  async getLogoutProviderOptions() {
4057
- return (await this.session.modelRuntime.listCredentials())
4361
+ return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4058
4362
  .map(({ providerId, type }) => ({
4059
4363
  id: providerId,
4060
4364
  name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
@@ -4072,7 +4376,6 @@ export class InteractiveMode {
4072
4376
  provider.name.toLowerCase() === normalizedProviderRef);
4073
4377
  }
4074
4378
  async handleLoginCommand(providerRef) {
4075
- await this.session.modelRuntime.getAvailable();
4076
4379
  if (!providerRef) {
4077
4380
  this.showLoginAuthTypeSelector();
4078
4381
  return;
@@ -4186,7 +4489,14 @@ export class InteractiveMode {
4186
4489
  this.showLoginAuthTypeSelector();
4187
4490
  return;
4188
4491
  }
4189
- const providerOptions = await this.getLogoutProviderOptions();
4492
+ let providerOptions;
4493
+ try {
4494
+ providerOptions = await this.getLogoutProviderOptions();
4495
+ }
4496
+ catch (error) {
4497
+ this.showError(`Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`);
4498
+ return;
4499
+ }
4190
4500
  if (providerOptions.length === 0) {
4191
4501
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4192
4502
  return;
@@ -4199,7 +4509,9 @@ export class InteractiveMode {
4199
4509
  return;
4200
4510
  }
4201
4511
  try {
4202
- await this.session.modelRuntime.logout(providerOption.id);
4512
+ await this.session.modelRuntime.logout(providerOption.id, {
4513
+ signal: AbortSignal.timeout(15_000),
4514
+ });
4203
4515
  await this.updateAvailableProviderCount();
4204
4516
  const message = providerOption.authType === "oauth"
4205
4517
  ? `Logged out of ${providerOption.name}`
@@ -4207,7 +4519,10 @@ export class InteractiveMode {
4207
4519
  this.showStatus(message);
4208
4520
  }
4209
4521
  catch (error) {
4210
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
4522
+ const message = error instanceof Error ? error.message : String(error);
4523
+ this.showError(error instanceof CredentialSynchronizationError
4524
+ ? `Credentials removed for ${providerOption.name}, but local model state could not be synchronized: ${message}`
4525
+ : `Logout failed: ${message}`);
4211
4526
  }
4212
4527
  }, () => {
4213
4528
  done();
@@ -4217,12 +4532,11 @@ export class InteractiveMode {
4217
4532
  });
4218
4533
  }
4219
4534
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4220
- await this.session.modelRuntime.getAvailable();
4221
4535
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4222
4536
  let selectedModel;
4223
4537
  let selectionError;
4224
4538
  if (isUnknownModel(previousModel)) {
4225
- const availableModels = await this.session.modelRuntime.getAvailable();
4539
+ const availableModels = this.session.modelRuntime.getAvailableSnapshot();
4226
4540
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4227
4541
  if (!hasDefaultModelProvider(providerId)) {
4228
4542
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4265,6 +4579,25 @@ export class InteractiveMode {
4265
4579
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
4266
4580
  }
4267
4581
  }
4582
+ const controller = new AbortController();
4583
+ const timeout = setTimeout(() => controller.abort(), 15_000);
4584
+ void this.session.modelRuntime
4585
+ .refresh({ providers: [providerId], signal: controller.signal })
4586
+ .then((result) => {
4587
+ if (result.aborted) {
4588
+ this.showWarning(`${actionLabel}, but its model catalog refresh timed out; using cached models.`);
4589
+ }
4590
+ else if (result.errors.size > 0) {
4591
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed; using cached models.`);
4592
+ }
4593
+ this.updateAvailableProviderCount();
4594
+ this.footer.invalidate();
4595
+ this.ui.requestRender();
4596
+ })
4597
+ .catch((error) => {
4598
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
4599
+ })
4600
+ .finally(() => clearTimeout(timeout));
4268
4601
  }
4269
4602
  showAmbientAuthDialog(providerOption) {
4270
4603
  const restoreEditor = () => {
@@ -4310,7 +4643,10 @@ export class InteractiveMode {
4310
4643
  catch (error) {
4311
4644
  restoreEditor();
4312
4645
  const errorMsg = error instanceof Error ? error.message : String(error);
4313
- if (errorMsg !== "Login cancelled") {
4646
+ if (error instanceof CredentialSynchronizationError) {
4647
+ this.showError(`Saved API key for ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4648
+ }
4649
+ else if (errorMsg !== "Login cancelled") {
4314
4650
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
4315
4651
  }
4316
4652
  }
@@ -4413,7 +4749,10 @@ export class InteractiveMode {
4413
4749
  catch (error) {
4414
4750
  restoreEditor();
4415
4751
  const errorMsg = error instanceof Error ? error.message : String(error);
4416
- if (errorMsg !== "Login cancelled") {
4752
+ if (error instanceof CredentialSynchronizationError) {
4753
+ this.showError(`Logged in to ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4754
+ }
4755
+ else if (errorMsg !== "Login cancelled") {
4417
4756
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
4418
4757
  }
4419
4758
  }
@@ -4483,7 +4822,6 @@ export class InteractiveMode {
4483
4822
  try {
4484
4823
  await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
4485
4824
  restoreChatBeforeSessionStart();
4486
- configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
4487
4825
  this.keybindings.reload();
4488
4826
  const activeHeader = this.customHeader ?? this.builtInHeader;
4489
4827
  if (isExpandable(activeHeader)) {
@@ -4491,20 +4829,7 @@ export class InteractiveMode {
4491
4829
  }
4492
4830
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
4493
4831
  await this.themeController.applyFromSettings();
4494
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
4495
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
4496
- this.defaultEditor.setPaddingX(editorPaddingX);
4497
- this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
4498
- if (this.editor !== this.defaultEditor) {
4499
- this.editor.setPaddingX?.(editorPaddingX);
4500
- this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4501
- }
4502
- this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4503
- const clearOnShrink = this.settingsManager.getClearOnShrink();
4504
- this.ui.setClearOnShrink(clearOnShrink);
4505
- if (!clearOnShrink && !this.activeStatusIndicator) {
4506
- this.statusContainer.clear();
4507
- }
4832
+ this.applyRuntimeSettings();
4508
4833
  this.setupAutocompleteProvider();
4509
4834
  const runner = this.session.extensionRunner;
4510
4835
  this.setupExtensionShortcuts(runner);
@@ -4700,7 +5025,7 @@ export class InteractiveMode {
4700
5025
  }
4701
5026
  }
4702
5027
  }
4703
- async handleCopyCommand() {
5028
+ async handleCopyCommand(options = {}) {
4704
5029
  const text = this.session.getLastAssistantText();
4705
5030
  if (!text) {
4706
5031
  this.showError("No agent messages to copy yet.");
@@ -4708,7 +5033,12 @@ export class InteractiveMode {
4708
5033
  }
4709
5034
  try {
4710
5035
  await copyToClipboard(text);
4711
- this.showStatus("Copied last agent message to clipboard");
5036
+ if (options.flashConfirmation && this.ui instanceof TuiAltScreen) {
5037
+ this.ui.flash("Copied!");
5038
+ }
5039
+ else {
5040
+ this.showStatus("Copied last agent message to clipboard");
5041
+ }
4712
5042
  }
4713
5043
  catch (error) {
4714
5044
  this.showError(error instanceof Error ? error.message : String(error));
@@ -5071,6 +5401,7 @@ export class InteractiveMode {
5071
5401
  }
5072
5402
  }
5073
5403
  stop() {
5404
+ this.disposeActiveSelector();
5074
5405
  if (this.settingsManager.getShowTerminalProgress()) {
5075
5406
  this.ui.terminal.setProgress(false);
5076
5407
  }
@@ -5083,7 +5414,7 @@ export class InteractiveMode {
5083
5414
  this.unsubscribe();
5084
5415
  }
5085
5416
  if (this.isInitialized) {
5086
- this.ui.stop();
5417
+ this.stopInteractiveTui();
5087
5418
  this.isInitialized = false;
5088
5419
  }
5089
5420
  this.unregisterSignalHandlers();