@averyyy/pi-coding-agent 0.83.0-piclient.1 → 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 (344) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +5 -0
  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 +22 -3
  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 +5 -15
  19. package/dist/cli/credential-print.d.ts.map +1 -1
  20. package/dist/cli/credential-print.js +42 -82
  21. package/dist/cli/credential-print.js.map +1 -1
  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-services.d.ts +1 -0
  78. package/dist/core/agent-session-services.d.ts.map +1 -1
  79. package/dist/core/agent-session-services.js +1 -0
  80. package/dist/core/agent-session-services.js.map +1 -1
  81. package/dist/core/agent-session.d.ts +2 -11
  82. package/dist/core/agent-session.d.ts.map +1 -1
  83. package/dist/core/agent-session.js +61 -59
  84. package/dist/core/agent-session.js.map +1 -1
  85. package/dist/core/auth-storage.d.ts +25 -9
  86. package/dist/core/auth-storage.d.ts.map +1 -1
  87. package/dist/core/auth-storage.js +237 -39
  88. package/dist/core/auth-storage.js.map +1 -1
  89. package/dist/core/extensions/index.d.ts +1 -1
  90. package/dist/core/extensions/index.d.ts.map +1 -1
  91. package/dist/core/extensions/index.js.map +1 -1
  92. package/dist/core/extensions/loader.d.ts.map +1 -1
  93. package/dist/core/extensions/loader.js +43 -20
  94. package/dist/core/extensions/loader.js.map +1 -1
  95. package/dist/core/extensions/runner.d.ts +2 -1
  96. package/dist/core/extensions/runner.d.ts.map +1 -1
  97. package/dist/core/extensions/runner.js +3 -0
  98. package/dist/core/extensions/runner.js.map +1 -1
  99. package/dist/core/extensions/types.d.ts +26 -3
  100. package/dist/core/extensions/types.d.ts.map +1 -1
  101. package/dist/core/extensions/types.js.map +1 -1
  102. package/dist/core/http-dispatcher.d.ts.map +1 -1
  103. package/dist/core/http-dispatcher.js +5 -0
  104. package/dist/core/http-dispatcher.js.map +1 -1
  105. package/dist/core/keybindings.d.ts +44 -4
  106. package/dist/core/keybindings.d.ts.map +1 -1
  107. package/dist/core/model-config.d.ts +29 -5
  108. package/dist/core/model-config.d.ts.map +1 -1
  109. package/dist/core/model-config.js +4 -0
  110. package/dist/core/model-config.js.map +1 -1
  111. package/dist/core/model-registry.d.ts +5 -3
  112. package/dist/core/model-registry.d.ts.map +1 -1
  113. package/dist/core/model-registry.js +13 -10
  114. package/dist/core/model-registry.js.map +1 -1
  115. package/dist/core/model-resolver.d.ts +4 -3
  116. package/dist/core/model-resolver.d.ts.map +1 -1
  117. package/dist/core/model-resolver.js +42 -10
  118. package/dist/core/model-resolver.js.map +1 -1
  119. package/dist/core/model-runtime.d.ts +30 -13
  120. package/dist/core/model-runtime.d.ts.map +1 -1
  121. package/dist/core/model-runtime.js +233 -73
  122. package/dist/core/model-runtime.js.map +1 -1
  123. package/dist/core/models-store.d.ts +12 -7
  124. package/dist/core/models-store.d.ts.map +1 -1
  125. package/dist/core/models-store.js +83 -14
  126. package/dist/core/models-store.js.map +1 -1
  127. package/dist/core/package-manager.d.ts +4 -1
  128. package/dist/core/package-manager.d.ts.map +1 -1
  129. package/dist/core/package-manager.js +63 -36
  130. package/dist/core/package-manager.js.map +1 -1
  131. package/dist/core/pi-manifest.d.ts +8 -0
  132. package/dist/core/pi-manifest.d.ts.map +1 -0
  133. package/dist/core/pi-manifest.js +25 -0
  134. package/dist/core/pi-manifest.js.map +1 -0
  135. package/dist/core/pi-server-client.d.ts +2 -2
  136. package/dist/core/pi-server-client.d.ts.map +1 -1
  137. package/dist/core/pi-server-client.js +2 -0
  138. package/dist/core/pi-server-client.js.map +1 -1
  139. package/dist/core/provider-composer.d.ts +4 -1
  140. package/dist/core/provider-composer.d.ts.map +1 -1
  141. package/dist/core/provider-composer.js +36 -15
  142. package/dist/core/provider-composer.js.map +1 -1
  143. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  144. package/dist/core/remote-catalog-provider.js +73 -67
  145. package/dist/core/remote-catalog-provider.js.map +1 -1
  146. package/dist/core/resource-loader.d.ts.map +1 -1
  147. package/dist/core/resource-loader.js +2 -2
  148. package/dist/core/resource-loader.js.map +1 -1
  149. package/dist/core/runtime-credentials.d.ts +5 -5
  150. package/dist/core/runtime-credentials.d.ts.map +1 -1
  151. package/dist/core/runtime-credentials.js +11 -8
  152. package/dist/core/runtime-credentials.js.map +1 -1
  153. package/dist/core/session-manager.d.ts.map +1 -1
  154. package/dist/core/session-manager.js +3 -1
  155. package/dist/core/session-manager.js.map +1 -1
  156. package/dist/core/settings-manager.d.ts +12 -0
  157. package/dist/core/settings-manager.d.ts.map +1 -1
  158. package/dist/core/settings-manager.js +40 -16
  159. package/dist/core/settings-manager.js.map +1 -1
  160. package/dist/core/tools/bash.d.ts +4 -0
  161. package/dist/core/tools/bash.d.ts.map +1 -1
  162. package/dist/core/tools/bash.js +6 -4
  163. package/dist/core/tools/bash.js.map +1 -1
  164. package/dist/core/tools/edit.d.ts +4 -0
  165. package/dist/core/tools/edit.d.ts.map +1 -1
  166. package/dist/core/tools/edit.js +11 -7
  167. package/dist/core/tools/edit.js.map +1 -1
  168. package/dist/core/tools/find.d.ts +7 -0
  169. package/dist/core/tools/find.d.ts.map +1 -1
  170. package/dist/core/tools/find.js +16 -19
  171. package/dist/core/tools/find.js.map +1 -1
  172. package/dist/core/tools/grep.d.ts +4 -0
  173. package/dist/core/tools/grep.d.ts.map +1 -1
  174. package/dist/core/tools/grep.js +5 -1
  175. package/dist/core/tools/grep.js.map +1 -1
  176. package/dist/core/tools/ls.d.ts +4 -0
  177. package/dist/core/tools/ls.d.ts.map +1 -1
  178. package/dist/core/tools/ls.js +5 -1
  179. package/dist/core/tools/ls.js.map +1 -1
  180. package/dist/core/tools/read.d.ts +4 -0
  181. package/dist/core/tools/read.d.ts.map +1 -1
  182. package/dist/core/tools/read.js +7 -3
  183. package/dist/core/tools/read.js.map +1 -1
  184. package/dist/core/tools/render-utils.d.ts.map +1 -1
  185. package/dist/core/tools/render-utils.js +6 -3
  186. package/dist/core/tools/render-utils.js.map +1 -1
  187. package/dist/core/tools/write.d.ts +4 -0
  188. package/dist/core/tools/write.d.ts.map +1 -1
  189. package/dist/core/tools/write.js +6 -2
  190. package/dist/core/tools/write.js.map +1 -1
  191. package/dist/extensions/llama/index.d.ts.map +1 -1
  192. package/dist/extensions/llama/index.js +11 -2
  193. package/dist/extensions/llama/index.js.map +1 -1
  194. package/dist/extensions/llama/provider.d.ts.map +1 -1
  195. package/dist/extensions/llama/provider.js +21 -7
  196. package/dist/extensions/llama/provider.js.map +1 -1
  197. package/dist/index.d.ts +4 -4
  198. package/dist/index.d.ts.map +1 -1
  199. package/dist/index.js +1 -1
  200. package/dist/index.js.map +1 -1
  201. package/dist/main.d.ts.map +1 -1
  202. package/dist/main.js +70 -25
  203. package/dist/main.js.map +1 -1
  204. package/dist/modes/index.d.ts +1 -0
  205. package/dist/modes/index.d.ts.map +1 -1
  206. package/dist/modes/index.js.map +1 -1
  207. package/dist/modes/interactive/components/assistant-message.d.ts +5 -2
  208. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  209. package/dist/modes/interactive/components/assistant-message.js +13 -4
  210. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  211. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  212. package/dist/modes/interactive/components/custom-editor.js +7 -0
  213. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  214. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  215. package/dist/modes/interactive/components/footer.js +2 -2
  216. package/dist/modes/interactive/components/footer.js.map +1 -1
  217. package/dist/modes/interactive/components/markdown-transform.d.ts +3 -0
  218. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -0
  219. package/dist/modes/interactive/components/markdown-transform.js +19 -0
  220. package/dist/modes/interactive/components/markdown-transform.js.map +1 -0
  221. package/dist/modes/interactive/components/mermaid.d.ts +11 -0
  222. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  223. package/dist/modes/interactive/components/mermaid.js +75 -0
  224. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  225. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  226. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  227. package/dist/modes/interactive/components/model-selector.js +16 -4
  228. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  229. package/dist/modes/interactive/components/scoped-models-selector.d.ts +4 -0
  230. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/scoped-models-selector.js +26 -0
  232. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  233. package/dist/modes/interactive/components/settings-selector.d.ts +8 -2
  234. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  235. package/dist/modes/interactive/components/settings-selector.js +30 -0
  236. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  237. package/dist/modes/interactive/components/user-message.d.ts +3 -1
  238. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  239. package/dist/modes/interactive/components/user-message.js +9 -2
  240. package/dist/modes/interactive/components/user-message.js.map +1 -1
  241. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  242. package/dist/modes/interactive/external-editor.js +7 -5
  243. package/dist/modes/interactive/external-editor.js.map +1 -1
  244. package/dist/modes/interactive/interactive-mode.d.ts +36 -2
  245. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  246. package/dist/modes/interactive/interactive-mode.js +461 -144
  247. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  248. package/dist/modes/interactive/theme/dark.json +1 -0
  249. package/dist/modes/interactive/theme/light.json +1 -0
  250. package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
  251. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  252. package/dist/modes/interactive/theme/theme-controller.js +10 -1
  253. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  254. package/dist/modes/interactive/theme/theme-schema.json +5 -1
  255. package/dist/modes/interactive/theme/theme.d.ts +2 -2
  256. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  257. package/dist/modes/interactive/theme/theme.js +24 -6
  258. package/dist/modes/interactive/theme/theme.js.map +1 -1
  259. package/dist/modes/json-event.d.ts +28 -0
  260. package/dist/modes/json-event.d.ts.map +1 -0
  261. package/dist/modes/json-event.js +12 -0
  262. package/dist/modes/json-event.js.map +1 -0
  263. package/dist/modes/print-mode.d.ts.map +1 -1
  264. package/dist/modes/print-mode.js +12 -2
  265. package/dist/modes/print-mode.js.map +1 -1
  266. package/dist/modes/rpc/rpc-client.d.ts +5 -4
  267. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  268. package/dist/modes/rpc/rpc-client.js.map +1 -1
  269. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  270. package/dist/modes/rpc/rpc-mode.js +4 -3
  271. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  272. package/dist/package-manager-cli.d.ts.map +1 -1
  273. package/dist/package-manager-cli.js +11 -9
  274. package/dist/package-manager-cli.js.map +1 -1
  275. package/dist/rpc-entry.d.ts.map +1 -1
  276. package/dist/rpc-entry.js +1 -0
  277. package/dist/rpc-entry.js.map +1 -1
  278. package/dist/server/create-harness.d.ts +26 -0
  279. package/dist/server/create-harness.d.ts.map +1 -0
  280. package/dist/server/create-harness.js +104 -0
  281. package/dist/server/create-harness.js.map +1 -0
  282. package/dist/utils/abort.d.ts +5 -0
  283. package/dist/utils/abort.d.ts.map +1 -0
  284. package/dist/utils/abort.js +48 -0
  285. package/dist/utils/abort.js.map +1 -0
  286. package/dist/utils/clipboard.d.ts +1 -1
  287. package/dist/utils/clipboard.d.ts.map +1 -1
  288. package/dist/utils/clipboard.js +22 -2
  289. package/dist/utils/clipboard.js.map +1 -1
  290. package/dist/utils/management-http.d.ts +23 -0
  291. package/dist/utils/management-http.d.ts.map +1 -0
  292. package/dist/utils/management-http.js +50 -0
  293. package/dist/utils/management-http.js.map +1 -0
  294. package/dist/utils/paths.d.ts +3 -0
  295. package/dist/utils/paths.d.ts.map +1 -1
  296. package/dist/utils/paths.js +23 -1
  297. package/dist/utils/paths.js.map +1 -1
  298. package/dist/utils/tool-result-images.d.ts +19 -0
  299. package/dist/utils/tool-result-images.d.ts.map +1 -0
  300. package/dist/utils/tool-result-images.js +45 -0
  301. package/dist/utils/tool-result-images.js.map +1 -0
  302. package/dist/utils/tools-manager.d.ts.map +1 -1
  303. package/dist/utils/tools-manager.js +4 -6
  304. package/dist/utils/tools-manager.js.map +1 -1
  305. package/dist/utils/version-check.d.ts +4 -0
  306. package/dist/utils/version-check.d.ts.map +1 -1
  307. package/dist/utils/version-check.js +20 -2
  308. package/dist/utils/version-check.js.map +1 -1
  309. package/docs/compaction.md +2 -2
  310. package/docs/custom-provider.md +7 -5
  311. package/docs/extensions.md +30 -5
  312. package/docs/json.md +19 -14
  313. package/docs/keybindings.md +46 -14
  314. package/docs/models.md +24 -3
  315. package/docs/providers.md +9 -1
  316. package/docs/quickstart.md +2 -0
  317. package/docs/rpc.md +13 -11
  318. package/docs/sdk.md +21 -2
  319. package/docs/security.md +1 -1
  320. package/docs/settings.md +4 -1
  321. package/docs/themes.md +5 -3
  322. package/docs/usage.md +7 -0
  323. package/examples/extensions/custom-compaction.ts +1 -16
  324. package/examples/extensions/custom-provider-anthropic/index.ts +2 -1
  325. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  326. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  327. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -1
  328. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  329. package/examples/extensions/doom-overlay/wad-finder.ts +7 -3
  330. package/examples/extensions/gondolin/package-lock.json +2 -2
  331. package/examples/extensions/gondolin/package.json +1 -1
  332. package/examples/extensions/handoff.ts +2 -11
  333. package/examples/extensions/qna.ts +3 -7
  334. package/examples/extensions/sandbox/package-lock.json +2 -2
  335. package/examples/extensions/sandbox/package.json +1 -1
  336. package/examples/extensions/summarize.ts +7 -18
  337. package/examples/extensions/with-deps/package-lock.json +2 -2
  338. package/examples/extensions/with-deps/package.json +1 -1
  339. package/examples/rpc-extension-ui.ts +11 -2
  340. package/examples/sdk/02-custom-model.ts +1 -2
  341. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  342. package/examples/sdk/12-full-control.ts +1 -1
  343. package/examples/sdk/README.md +1 -1
  344. package/package.json +13 -6
@@ -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);
@@ -1288,8 +1450,12 @@ export class InteractiveMode {
1288
1450
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1289
1451
  this.showStartupNoticesIfNeeded();
1290
1452
  }
1453
+ applyFullscreenScrollbarSetting() {
1454
+ this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1455
+ }
1291
1456
  applyRuntimeSettings() {
1292
1457
  configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1458
+ this.applyFullscreenScrollbarSetting();
1293
1459
  this.footer.setSession(this.session);
1294
1460
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1295
1461
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
@@ -1353,6 +1519,9 @@ export class InteractiveMode {
1353
1519
  getRegisteredToolDefinition(toolName) {
1354
1520
  return this.session.getToolDefinition(toolName);
1355
1521
  }
1522
+ getMarkdownTransformers() {
1523
+ return [this.mermaidMarkdownTransformer, ...this.session.extensionRunner.getMarkdownTransformers()];
1524
+ }
1356
1525
  /**
1357
1526
  * Set up keyboard shortcuts registered by extensions.
1358
1527
  */
@@ -1432,7 +1601,7 @@ export class InteractiveMode {
1432
1601
  this.activeStatusIndicator?.dispose();
1433
1602
  this.activeStatusIndicator = undefined;
1434
1603
  this.statusContainer.clear();
1435
- if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1604
+ if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
1436
1605
  this.statusContainer.addChild(this.idleStatus);
1437
1606
  }
1438
1607
  }
@@ -1589,22 +1758,16 @@ export class InteractiveMode {
1589
1758
  if (this.customFooter?.dispose) {
1590
1759
  this.customFooter.dispose();
1591
1760
  }
1592
- // Remove current footer from UI
1593
- if (this.customFooter) {
1594
- this.ui.removeChild(this.customFooter);
1595
- }
1596
- else {
1597
- this.ui.removeChild(this.footer);
1598
- }
1761
+ this.footerContainer.clear();
1599
1762
  if (factory) {
1600
1763
  // Create and add custom footer, passing the data provider
1601
1764
  this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1602
- this.ui.addChild(this.customFooter);
1765
+ this.footerContainer.addChild(this.customFooter);
1603
1766
  }
1604
1767
  else {
1605
1768
  // Restore built-in footer
1606
1769
  this.customFooter = undefined;
1607
- this.ui.addChild(this.footer);
1770
+ this.footerContainer.addChild(this.footer);
1608
1771
  }
1609
1772
  this.ui.requestRender();
1610
1773
  }
@@ -1650,18 +1813,23 @@ export class InteractiveMode {
1650
1813
  this.ui.requestRender();
1651
1814
  }
1652
1815
  addExtensionTerminalInputListener(handler) {
1653
- const unsubscribe = this.ui.addInputListener(handler);
1654
- this.extensionTerminalInputUnsubscribers.add(unsubscribe);
1816
+ const subscription = { handler, unsubscribe: this.ui.addInputListener(handler) };
1817
+ this.extensionTerminalInputSubscriptions.add(subscription);
1655
1818
  return () => {
1656
- unsubscribe();
1657
- this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
1819
+ subscription.unsubscribe();
1820
+ this.extensionTerminalInputSubscriptions.delete(subscription);
1658
1821
  };
1659
1822
  }
1660
- clearExtensionTerminalInputListeners() {
1661
- for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
1662
- unsubscribe();
1823
+ rebindExtensionTerminalInputListeners() {
1824
+ for (const subscription of this.extensionTerminalInputSubscriptions) {
1825
+ subscription.unsubscribe();
1826
+ subscription.unsubscribe = this.ui.addInputListener(subscription.handler);
1663
1827
  }
1664
- this.extensionTerminalInputUnsubscribers.clear();
1828
+ }
1829
+ clearExtensionTerminalInputListeners() {
1830
+ for (const subscription of this.extensionTerminalInputSubscriptions)
1831
+ subscription.unsubscribe();
1832
+ this.extensionTerminalInputSubscriptions.clear();
1665
1833
  }
1666
1834
  /**
1667
1835
  * Create the ExtensionUIContext for extensions.
@@ -1756,6 +1924,7 @@ export class InteractiveMode {
1756
1924
  this.hideExtensionSelector();
1757
1925
  resolve(undefined);
1758
1926
  }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1927
+ this.disposeActiveSelector();
1759
1928
  this.editorContainer.clear();
1760
1929
  this.editorContainer.addChild(this.extensionSelector);
1761
1930
  this.ui.setFocus(this.extensionSelector);
@@ -1807,6 +1976,7 @@ export class InteractiveMode {
1807
1976
  this.hideExtensionInput();
1808
1977
  resolve(undefined);
1809
1978
  }, { tui: this.ui, timeout: opts?.timeout });
1979
+ this.disposeActiveSelector();
1810
1980
  this.editorContainer.clear();
1811
1981
  this.editorContainer.addChild(this.extensionInput);
1812
1982
  this.ui.setFocus(this.extensionInput);
@@ -1836,6 +2006,7 @@ export class InteractiveMode {
1836
2006
  this.hideExtensionEditor();
1837
2007
  resolve(undefined);
1838
2008
  }, undefined, this.settingsManager.getExternalEditorCommand());
2009
+ this.disposeActiveSelector();
1839
2010
  this.editorContainer.clear();
1840
2011
  this.editorContainer.addChild(this.extensionEditor);
1841
2012
  this.ui.setFocus(this.extensionEditor);
@@ -1860,6 +2031,7 @@ export class InteractiveMode {
1860
2031
  this.editorComponentFactory = factory;
1861
2032
  // Save text from current editor before switching
1862
2033
  const currentText = this.editor.getText();
2034
+ this.disposeActiveSelector();
1863
2035
  this.editorContainer.clear();
1864
2036
  if (factory) {
1865
2037
  // Create the custom editor with tui, theme, and keybindings
@@ -1876,6 +2048,9 @@ export class InteractiveMode {
1876
2048
  if (newEditor.setPaddingX !== undefined) {
1877
2049
  newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1878
2050
  }
2051
+ if (newEditor.setAutocompleteMaxVisible !== undefined) {
2052
+ newEditor.setAutocompleteMaxVisible(this.defaultEditor.getAutocompleteMaxVisible());
2053
+ }
1879
2054
  // Set autocomplete if supported
1880
2055
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1881
2056
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1980,6 +2155,7 @@ export class InteractiveMode {
1980
2155
  options?.onHandle?.(handle);
1981
2156
  }
1982
2157
  else {
2158
+ this.disposeActiveSelector();
1983
2159
  this.editorContainer.clear();
1984
2160
  this.editorContainer.addChild(component);
1985
2161
  this.ui.setFocus(component);
@@ -2066,7 +2242,7 @@ export class InteractiveMode {
2066
2242
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2067
2243
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2068
2244
  this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2069
- this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2245
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true }));
2070
2246
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2071
2247
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2072
2248
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2086,6 +2262,22 @@ export class InteractiveMode {
2086
2262
  void this.handleClipboardPaste();
2087
2263
  };
2088
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
+ }
2089
2281
  async handleClipboardPaste() {
2090
2282
  try {
2091
2283
  const image = await readClipboardImage();
@@ -2360,17 +2552,17 @@ export class InteractiveMode {
2360
2552
  this.ui.requestRender();
2361
2553
  }
2362
2554
  else if (event.message.role === "assistant") {
2363
- 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());
2364
2556
  this.streamingMessage = event.message;
2365
2557
  this.chatContainer.addChild(this.streamingComponent);
2366
- this.streamingComponent.updateContent(this.streamingMessage);
2558
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2367
2559
  this.ui.requestRender();
2368
2560
  }
2369
2561
  break;
2370
2562
  case "message_update":
2371
2563
  if (this.streamingComponent && event.message.role === "assistant") {
2372
2564
  this.streamingMessage = event.message;
2373
- this.streamingComponent.updateContent(this.streamingMessage);
2565
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2374
2566
  for (const content of this.streamingMessage.content) {
2375
2567
  if (content.type === "toolCall") {
2376
2568
  if (!this.pendingTools.has(content.id)) {
@@ -2407,7 +2599,7 @@ export class InteractiveMode {
2407
2599
  : "Operation aborted";
2408
2600
  this.streamingMessage.errorMessage = errorMessage;
2409
2601
  }
2410
- this.streamingComponent.updateContent(this.streamingMessage);
2602
+ this.streamingComponent.updateContent(this.streamingMessage, false);
2411
2603
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2412
2604
  if (!errorMessage) {
2413
2605
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2682,12 +2874,12 @@ export class InteractiveMode {
2682
2874
  // Render user message separately if present
2683
2875
  if (skillBlock.userMessage) {
2684
2876
  this.chatContainer.addChild(new Spacer(1));
2685
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2877
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2686
2878
  this.chatContainer.addChild(userComponent);
2687
2879
  }
2688
2880
  }
2689
2881
  else {
2690
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2882
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2691
2883
  this.chatContainer.addChild(userComponent);
2692
2884
  }
2693
2885
  if (options?.populateHistory) {
@@ -2697,7 +2889,7 @@ export class InteractiveMode {
2697
2889
  break;
2698
2890
  }
2699
2891
  case "assistant": {
2700
- 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());
2701
2893
  this.chatContainer.addChild(assistantComponent);
2702
2894
  break;
2703
2895
  }
@@ -3131,6 +3323,8 @@ export class InteractiveMode {
3131
3323
  this.setToolsExpanded(!this.toolOutputExpanded);
3132
3324
  }
3133
3325
  setToolsExpanded(expanded) {
3326
+ if (expanded === this.toolOutputExpanded)
3327
+ return;
3134
3328
  this.toolOutputExpanded = expanded;
3135
3329
  const activeHeader = this.customHeader ?? this.builtInHeader;
3136
3330
  if (isExpandable(activeHeader)) {
@@ -3186,7 +3380,7 @@ export class InteractiveMode {
3186
3380
  }
3187
3381
  showError(errorMessage) {
3188
3382
  this.chatContainer.addChild(new Spacer(1));
3189
- 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));
3190
3384
  this.ui.requestRender();
3191
3385
  }
3192
3386
  showWarning(warningMessage) {
@@ -3396,25 +3590,43 @@ export class InteractiveMode {
3396
3590
  // =========================================================================
3397
3591
  // Selectors
3398
3592
  // =========================================================================
3593
+ disposeActiveSelector() {
3594
+ const dispose = this.activeSelectorDispose;
3595
+ this.activeSelectorToken = undefined;
3596
+ this.activeSelectorDispose = undefined;
3597
+ dispose?.();
3598
+ }
3399
3599
  /**
3400
3600
  * Shows a selector component in place of the editor.
3401
3601
  * @param create Factory that receives a `done` callback and returns the component and focus target
3402
3602
  */
3403
3603
  showSelector(create) {
3604
+ const token = {};
3605
+ let dispose;
3404
3606
  const done = () => {
3607
+ dispose?.();
3608
+ if (this.activeSelectorToken !== token)
3609
+ return;
3610
+ this.activeSelectorToken = undefined;
3611
+ this.activeSelectorDispose = undefined;
3405
3612
  this.editorContainer.clear();
3406
3613
  this.editorContainer.addChild(this.editor);
3407
3614
  this.ui.setFocus(this.editor);
3408
3615
  };
3409
- 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;
3410
3621
  this.editorContainer.clear();
3411
- this.editorContainer.addChild(component);
3412
- this.ui.setFocus(focus);
3622
+ this.editorContainer.addChild(created.component);
3623
+ this.ui.setFocus(created.focus);
3413
3624
  this.ui.requestRender();
3414
3625
  }
3415
3626
  showSettingsSelector() {
3416
3627
  this.showSelector((done) => {
3417
- const selector = new SettingsSelectorComponent({
3628
+ let selector;
3629
+ selector = new SettingsSelectorComponent({
3418
3630
  autoCompact: this.session.autoCompactionEnabled,
3419
3631
  showImages: this.settingsManager.getShowImages(),
3420
3632
  imageWidthCells: this.settingsManager.getImageWidthCells(),
@@ -3431,6 +3643,7 @@ export class InteractiveMode {
3431
3643
  terminalTheme: this.themeController.getTerminalTheme(),
3432
3644
  availableThemes: getAvailableThemes(),
3433
3645
  hideThinkingBlock: this.hideThinkingBlock,
3646
+ mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
3434
3647
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3435
3648
  enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
3436
3649
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
@@ -3444,6 +3657,8 @@ export class InteractiveMode {
3444
3657
  quietStartup: this.settingsManager.getQuietStartup(),
3445
3658
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3446
3659
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3660
+ tuiMode: this.ui.mode,
3661
+ fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3447
3662
  warnings: this.settingsManager.getWarnings(),
3448
3663
  }, {
3449
3664
  onAutoCompactChange: (enabled) => {
@@ -3512,6 +3727,11 @@ export class InteractiveMode {
3512
3727
  this.chatContainer.clear();
3513
3728
  this.rebuildChatFromMessages();
3514
3729
  },
3730
+ onMermaidRenderingModeChange: (mode) => {
3731
+ this.settingsManager.setMermaidRenderingMode(mode);
3732
+ this.chatContainer.invalidate();
3733
+ this.ui.requestRender();
3734
+ },
3515
3735
  onShowCacheMissNoticesChange: (shown) => {
3516
3736
  this.settingsManager.setShowCacheMissNotices(shown);
3517
3737
  this.rebuildChatFromMessages();
@@ -3581,6 +3801,21 @@ export class InteractiveMode {
3581
3801
  onShowTerminalProgressChange: (enabled) => {
3582
3802
  this.settingsManager.setShowTerminalProgress(enabled);
3583
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
+ },
3584
3819
  onWarningsChange: (warnings) => {
3585
3820
  this.settingsManager.setWarnings(warnings);
3586
3821
  },
@@ -3615,20 +3850,37 @@ export class InteractiveMode {
3615
3850
  this.showModelSelector(searchTerm);
3616
3851
  }
3617
3852
  async findExactModelMatch(searchTerm) {
3618
- const models = await this.getModelCandidates();
3619
- return findExactModelReferenceMatch(searchTerm, models);
3620
- }
3621
- async getModelCandidates() {
3622
- if (this.session.scopedModels.length > 0) {
3623
- return this.session.scopedModels.map((scoped) => scoped.model);
3624
- }
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);
3625
3866
  try {
3626
- await this.session.modelRuntime.refresh();
3627
- 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
+ }
3628
3874
  }
3629
- catch {
3630
- 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)}`);
3631
3879
  }
3880
+ finally {
3881
+ clearTimeout(timeout);
3882
+ }
3883
+ return findExactModelReferenceMatch(searchTerm, [...this.session.modelRuntime.getAvailableSnapshot()]);
3632
3884
  }
3633
3885
  /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3634
3886
  updateAvailableProviderCount() {
@@ -3730,73 +3982,67 @@ export class InteractiveMode {
3730
3982
  done();
3731
3983
  this.ui.requestRender();
3732
3984
  }, initialSearchInput);
3733
- return { component: selector, focus: selector };
3985
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
3734
3986
  });
3735
3987
  }
3736
- async showModelsSelector() {
3737
- // Get all available models
3738
- await this.session.modelRuntime.refresh();
3739
- const allModels = [...(await this.session.modelRuntime.getAvailable())];
3740
- 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}`));
3741
3991
  const configuredPatterns = this.settingsManager.getEnabledModels();
3742
3992
  const sessionScopedModels = this.session.scopedModels;
3743
- if (allModels.length === 0 && !configuredPatterns?.length && sessionScopedModels.length === 0) {
3744
- this.showStatus("No models available");
3745
- return;
3746
- }
3747
- const configuredScope = configuredPatterns?.length
3748
- ? await resolveModelScopeWithDiagnostics(configuredPatterns, this.session.modelRuntime)
3749
- : undefined;
3750
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3751
- const hasSessionScope = sessionScopedModels.length > 0;
3752
- // Build enabled model IDs from session state or settings
3753
- let currentEnabledIds = null;
3754
- if (hasSessionScope) {
3755
- // Use current session's scoped models
3756
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3757
- }
3758
- else if (configuredScope) {
3759
- currentEnabledIds = configuredScope.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3760
- }
3761
- for (const diagnostic of configuredScope?.diagnostics ?? []) {
3762
- if (diagnostic.code !== "no-match")
3763
- continue;
3764
- currentEnabledIds ??= [];
3765
- if (!currentEnabledIds.includes(diagnostic.pattern))
3766
- currentEnabledIds.push(diagnostic.pattern);
3767
- }
3768
- // Helper to update session's scoped models (session-only, no persist)
3769
- 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) => {
3770
4009
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3771
- const hasEnabledAvailableModel = enabledIds?.some((id) => allModelIds.has(id)) ?? false;
3772
- 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));
3773
4012
  if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
3774
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3775
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3776
- model: sm.model,
3777
- 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,
3778
4017
  })));
3779
4018
  }
3780
4019
  else {
3781
- // All enabled or none enabled = no filter
3782
4020
  this.session.setScopedModels([]);
3783
4021
  }
3784
- await this.updateAvailableProviderCount();
4022
+ this.updateAvailableProviderCount();
3785
4023
  this.ui.requestRender();
3786
4024
  };
3787
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);
3788
4033
  const selector = new ScopedModelsSelectorComponent({
3789
- allModels,
4034
+ allModels: availableModels,
3790
4035
  enabledModelIds: currentEnabledIds,
4036
+ refreshStatus: "Refreshing model catalogs…",
3791
4037
  }, {
3792
- onChange: async (enabledIds) => {
3793
- await updateSessionModels(enabledIds);
4038
+ onChange: (enabledIds) => {
4039
+ selectionChanged = true;
4040
+ updateSessionModels(enabledIds);
3794
4041
  },
3795
4042
  onPersist: (enabledIds) => {
3796
- // Persist to settings
3797
4043
  const allEnabled = enabledIds !== null &&
3798
- enabledIds.length === allModels.length &&
3799
- enabledIds.every((id) => allModelIds.has(id));
4044
+ enabledIds.length === availableModels.length &&
4045
+ enabledIds.every((id) => availableModelIds.has(id));
3800
4046
  const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3801
4047
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3802
4048
  this.showStatus("Model selection saved to settings");
@@ -3806,7 +4052,51 @@ export class InteractiveMode {
3806
4052
  this.ui.requestRender();
3807
4053
  },
3808
4054
  });
3809
- 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
+ };
3810
4100
  });
3811
4101
  }
3812
4102
  showUserMessageSelector() {
@@ -4068,7 +4358,7 @@ export class InteractiveMode {
4068
4358
  return options.sort((a, b) => a.name.localeCompare(b.name));
4069
4359
  }
4070
4360
  async getLogoutProviderOptions() {
4071
- return (await this.session.modelRuntime.listCredentials())
4361
+ return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4072
4362
  .map(({ providerId, type }) => ({
4073
4363
  id: providerId,
4074
4364
  name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
@@ -4086,7 +4376,6 @@ export class InteractiveMode {
4086
4376
  provider.name.toLowerCase() === normalizedProviderRef);
4087
4377
  }
4088
4378
  async handleLoginCommand(providerRef) {
4089
- await this.session.modelRuntime.getAvailable();
4090
4379
  if (!providerRef) {
4091
4380
  this.showLoginAuthTypeSelector();
4092
4381
  return;
@@ -4200,7 +4489,14 @@ export class InteractiveMode {
4200
4489
  this.showLoginAuthTypeSelector();
4201
4490
  return;
4202
4491
  }
4203
- 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
+ }
4204
4500
  if (providerOptions.length === 0) {
4205
4501
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4206
4502
  return;
@@ -4213,7 +4509,9 @@ export class InteractiveMode {
4213
4509
  return;
4214
4510
  }
4215
4511
  try {
4216
- await this.session.modelRuntime.logout(providerOption.id);
4512
+ await this.session.modelRuntime.logout(providerOption.id, {
4513
+ signal: AbortSignal.timeout(15_000),
4514
+ });
4217
4515
  await this.updateAvailableProviderCount();
4218
4516
  const message = providerOption.authType === "oauth"
4219
4517
  ? `Logged out of ${providerOption.name}`
@@ -4221,7 +4519,10 @@ export class InteractiveMode {
4221
4519
  this.showStatus(message);
4222
4520
  }
4223
4521
  catch (error) {
4224
- 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}`);
4225
4526
  }
4226
4527
  }, () => {
4227
4528
  done();
@@ -4231,12 +4532,11 @@ export class InteractiveMode {
4231
4532
  });
4232
4533
  }
4233
4534
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4234
- await this.session.modelRuntime.getAvailable();
4235
4535
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4236
4536
  let selectedModel;
4237
4537
  let selectionError;
4238
4538
  if (isUnknownModel(previousModel)) {
4239
- const availableModels = await this.session.modelRuntime.getAvailable();
4539
+ const availableModels = this.session.modelRuntime.getAvailableSnapshot();
4240
4540
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4241
4541
  if (!hasDefaultModelProvider(providerId)) {
4242
4542
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4279,6 +4579,25 @@ export class InteractiveMode {
4279
4579
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
4280
4580
  }
4281
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));
4282
4601
  }
4283
4602
  showAmbientAuthDialog(providerOption) {
4284
4603
  const restoreEditor = () => {
@@ -4324,7 +4643,10 @@ export class InteractiveMode {
4324
4643
  catch (error) {
4325
4644
  restoreEditor();
4326
4645
  const errorMsg = error instanceof Error ? error.message : String(error);
4327
- 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") {
4328
4650
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
4329
4651
  }
4330
4652
  }
@@ -4427,7 +4749,10 @@ export class InteractiveMode {
4427
4749
  catch (error) {
4428
4750
  restoreEditor();
4429
4751
  const errorMsg = error instanceof Error ? error.message : String(error);
4430
- 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") {
4431
4756
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
4432
4757
  }
4433
4758
  }
@@ -4497,7 +4822,6 @@ export class InteractiveMode {
4497
4822
  try {
4498
4823
  await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
4499
4824
  restoreChatBeforeSessionStart();
4500
- configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
4501
4825
  this.keybindings.reload();
4502
4826
  const activeHeader = this.customHeader ?? this.builtInHeader;
4503
4827
  if (isExpandable(activeHeader)) {
@@ -4505,20 +4829,7 @@ export class InteractiveMode {
4505
4829
  }
4506
4830
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
4507
4831
  await this.themeController.applyFromSettings();
4508
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
4509
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
4510
- this.defaultEditor.setPaddingX(editorPaddingX);
4511
- this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
4512
- if (this.editor !== this.defaultEditor) {
4513
- this.editor.setPaddingX?.(editorPaddingX);
4514
- this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4515
- }
4516
- this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4517
- const clearOnShrink = this.settingsManager.getClearOnShrink();
4518
- this.ui.setClearOnShrink(clearOnShrink);
4519
- if (!clearOnShrink && !this.activeStatusIndicator) {
4520
- this.statusContainer.clear();
4521
- }
4832
+ this.applyRuntimeSettings();
4522
4833
  this.setupAutocompleteProvider();
4523
4834
  const runner = this.session.extensionRunner;
4524
4835
  this.setupExtensionShortcuts(runner);
@@ -4714,7 +5025,7 @@ export class InteractiveMode {
4714
5025
  }
4715
5026
  }
4716
5027
  }
4717
- async handleCopyCommand() {
5028
+ async handleCopyCommand(options = {}) {
4718
5029
  const text = this.session.getLastAssistantText();
4719
5030
  if (!text) {
4720
5031
  this.showError("No agent messages to copy yet.");
@@ -4722,7 +5033,12 @@ export class InteractiveMode {
4722
5033
  }
4723
5034
  try {
4724
5035
  await copyToClipboard(text);
4725
- 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
+ }
4726
5042
  }
4727
5043
  catch (error) {
4728
5044
  this.showError(error instanceof Error ? error.message : String(error));
@@ -5085,6 +5401,7 @@ export class InteractiveMode {
5085
5401
  }
5086
5402
  }
5087
5403
  stop() {
5404
+ this.disposeActiveSelector();
5088
5405
  if (this.settingsManager.getShowTerminalProgress()) {
5089
5406
  this.ui.terminal.setProgress(false);
5090
5407
  }
@@ -5097,7 +5414,7 @@ export class InteractiveMode {
5097
5414
  this.unsubscribe();
5098
5415
  }
5099
5416
  if (this.isInitialized) {
5100
- this.ui.stop();
5417
+ this.stopInteractiveTui();
5101
5418
  this.isInitialized = false;
5102
5419
  }
5103
5420
  this.unregisterSignalHandlers();