@earendil-works/pi-coding-agent 0.83.0 → 0.84.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 (335) 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 +1 -10
  82. package/dist/core/agent-session.d.ts.map +1 -1
  83. package/dist/core/agent-session.js +72 -70
  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/provider-composer.d.ts +4 -1
  136. package/dist/core/provider-composer.d.ts.map +1 -1
  137. package/dist/core/provider-composer.js +36 -15
  138. package/dist/core/provider-composer.js.map +1 -1
  139. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  140. package/dist/core/remote-catalog-provider.js +73 -67
  141. package/dist/core/remote-catalog-provider.js.map +1 -1
  142. package/dist/core/resource-loader.d.ts.map +1 -1
  143. package/dist/core/resource-loader.js +2 -2
  144. package/dist/core/resource-loader.js.map +1 -1
  145. package/dist/core/runtime-credentials.d.ts +5 -5
  146. package/dist/core/runtime-credentials.d.ts.map +1 -1
  147. package/dist/core/runtime-credentials.js +11 -8
  148. package/dist/core/runtime-credentials.js.map +1 -1
  149. package/dist/core/session-manager.d.ts.map +1 -1
  150. package/dist/core/session-manager.js +3 -1
  151. package/dist/core/session-manager.js.map +1 -1
  152. package/dist/core/settings-manager.d.ts +12 -0
  153. package/dist/core/settings-manager.d.ts.map +1 -1
  154. package/dist/core/settings-manager.js +40 -16
  155. package/dist/core/settings-manager.js.map +1 -1
  156. package/dist/core/tools/bash.d.ts +4 -0
  157. package/dist/core/tools/bash.d.ts.map +1 -1
  158. package/dist/core/tools/bash.js +6 -4
  159. package/dist/core/tools/bash.js.map +1 -1
  160. package/dist/core/tools/edit.d.ts +4 -0
  161. package/dist/core/tools/edit.d.ts.map +1 -1
  162. package/dist/core/tools/edit.js +11 -7
  163. package/dist/core/tools/edit.js.map +1 -1
  164. package/dist/core/tools/find.d.ts +7 -0
  165. package/dist/core/tools/find.d.ts.map +1 -1
  166. package/dist/core/tools/find.js +16 -19
  167. package/dist/core/tools/find.js.map +1 -1
  168. package/dist/core/tools/grep.d.ts +4 -0
  169. package/dist/core/tools/grep.d.ts.map +1 -1
  170. package/dist/core/tools/grep.js +5 -1
  171. package/dist/core/tools/grep.js.map +1 -1
  172. package/dist/core/tools/ls.d.ts +4 -0
  173. package/dist/core/tools/ls.d.ts.map +1 -1
  174. package/dist/core/tools/ls.js +5 -1
  175. package/dist/core/tools/ls.js.map +1 -1
  176. package/dist/core/tools/read.d.ts +4 -0
  177. package/dist/core/tools/read.d.ts.map +1 -1
  178. package/dist/core/tools/read.js +7 -3
  179. package/dist/core/tools/read.js.map +1 -1
  180. package/dist/core/tools/write.d.ts +4 -0
  181. package/dist/core/tools/write.d.ts.map +1 -1
  182. package/dist/core/tools/write.js +6 -2
  183. package/dist/core/tools/write.js.map +1 -1
  184. package/dist/extensions/llama/index.d.ts.map +1 -1
  185. package/dist/extensions/llama/index.js +11 -2
  186. package/dist/extensions/llama/index.js.map +1 -1
  187. package/dist/extensions/llama/provider.d.ts.map +1 -1
  188. package/dist/extensions/llama/provider.js +21 -7
  189. package/dist/extensions/llama/provider.js.map +1 -1
  190. package/dist/index.d.ts +4 -4
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.js +1 -1
  193. package/dist/index.js.map +1 -1
  194. package/dist/main.d.ts.map +1 -1
  195. package/dist/main.js +70 -25
  196. package/dist/main.js.map +1 -1
  197. package/dist/modes/index.d.ts +1 -0
  198. package/dist/modes/index.d.ts.map +1 -1
  199. package/dist/modes/index.js.map +1 -1
  200. package/dist/modes/interactive/components/assistant-message.d.ts +5 -2
  201. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/assistant-message.js +13 -4
  203. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  204. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  205. package/dist/modes/interactive/components/custom-editor.js +7 -0
  206. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  207. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  208. package/dist/modes/interactive/components/footer.js +1 -1
  209. package/dist/modes/interactive/components/footer.js.map +1 -1
  210. package/dist/modes/interactive/components/markdown-transform.d.ts +3 -0
  211. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -0
  212. package/dist/modes/interactive/components/markdown-transform.js +19 -0
  213. package/dist/modes/interactive/components/markdown-transform.js.map +1 -0
  214. package/dist/modes/interactive/components/mermaid.d.ts +11 -0
  215. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  216. package/dist/modes/interactive/components/mermaid.js +75 -0
  217. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  218. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  219. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/model-selector.js +16 -4
  221. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  222. package/dist/modes/interactive/components/scoped-models-selector.d.ts +4 -0
  223. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  224. package/dist/modes/interactive/components/scoped-models-selector.js +26 -0
  225. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  226. package/dist/modes/interactive/components/settings-selector.d.ts +8 -2
  227. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  228. package/dist/modes/interactive/components/settings-selector.js +30 -0
  229. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  230. package/dist/modes/interactive/components/user-message.d.ts +3 -1
  231. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  232. package/dist/modes/interactive/components/user-message.js +9 -2
  233. package/dist/modes/interactive/components/user-message.js.map +1 -1
  234. package/dist/modes/interactive/interactive-mode.d.ts +36 -2
  235. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  236. package/dist/modes/interactive/interactive-mode.js +454 -138
  237. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  238. package/dist/modes/interactive/theme/dark.json +1 -0
  239. package/dist/modes/interactive/theme/light.json +1 -0
  240. package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
  241. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  242. package/dist/modes/interactive/theme/theme-controller.js +10 -1
  243. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  244. package/dist/modes/interactive/theme/theme-schema.json +5 -1
  245. package/dist/modes/interactive/theme/theme.d.ts +2 -2
  246. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  247. package/dist/modes/interactive/theme/theme.js +24 -6
  248. package/dist/modes/interactive/theme/theme.js.map +1 -1
  249. package/dist/modes/json-event.d.ts +28 -0
  250. package/dist/modes/json-event.d.ts.map +1 -0
  251. package/dist/modes/json-event.js +12 -0
  252. package/dist/modes/json-event.js.map +1 -0
  253. package/dist/modes/print-mode.d.ts.map +1 -1
  254. package/dist/modes/print-mode.js +12 -2
  255. package/dist/modes/print-mode.js.map +1 -1
  256. package/dist/modes/rpc/rpc-client.d.ts +5 -4
  257. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  258. package/dist/modes/rpc/rpc-client.js.map +1 -1
  259. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  260. package/dist/modes/rpc/rpc-mode.js +4 -3
  261. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  262. package/dist/package-manager-cli.d.ts.map +1 -1
  263. package/dist/package-manager-cli.js +11 -9
  264. package/dist/package-manager-cli.js.map +1 -1
  265. package/dist/rpc-entry.d.ts.map +1 -1
  266. package/dist/rpc-entry.js +1 -0
  267. package/dist/rpc-entry.js.map +1 -1
  268. package/dist/server/create-harness.d.ts +26 -0
  269. package/dist/server/create-harness.d.ts.map +1 -0
  270. package/dist/server/create-harness.js +104 -0
  271. package/dist/server/create-harness.js.map +1 -0
  272. package/dist/utils/abort.d.ts +5 -0
  273. package/dist/utils/abort.d.ts.map +1 -0
  274. package/dist/utils/abort.js +48 -0
  275. package/dist/utils/abort.js.map +1 -0
  276. package/dist/utils/clipboard.d.ts +1 -1
  277. package/dist/utils/clipboard.d.ts.map +1 -1
  278. package/dist/utils/clipboard.js +22 -2
  279. package/dist/utils/clipboard.js.map +1 -1
  280. package/dist/utils/management-http.d.ts +23 -0
  281. package/dist/utils/management-http.d.ts.map +1 -0
  282. package/dist/utils/management-http.js +50 -0
  283. package/dist/utils/management-http.js.map +1 -0
  284. package/dist/utils/paths.d.ts +3 -0
  285. package/dist/utils/paths.d.ts.map +1 -1
  286. package/dist/utils/paths.js +23 -1
  287. package/dist/utils/paths.js.map +1 -1
  288. package/dist/utils/tool-result-images.d.ts +19 -0
  289. package/dist/utils/tool-result-images.d.ts.map +1 -0
  290. package/dist/utils/tool-result-images.js +45 -0
  291. package/dist/utils/tool-result-images.js.map +1 -0
  292. package/dist/utils/tools-manager.d.ts.map +1 -1
  293. package/dist/utils/tools-manager.js +4 -6
  294. package/dist/utils/tools-manager.js.map +1 -1
  295. package/dist/utils/version-check.d.ts +4 -0
  296. package/dist/utils/version-check.d.ts.map +1 -1
  297. package/dist/utils/version-check.js +20 -2
  298. package/dist/utils/version-check.js.map +1 -1
  299. package/docs/compaction.md +2 -2
  300. package/docs/custom-provider.md +7 -5
  301. package/docs/extensions.md +30 -5
  302. package/docs/json.md +19 -14
  303. package/docs/keybindings.md +46 -14
  304. package/docs/models.md +24 -3
  305. package/docs/providers.md +9 -1
  306. package/docs/quickstart.md +2 -0
  307. package/docs/rpc.md +13 -11
  308. package/docs/sdk.md +21 -2
  309. package/docs/security.md +1 -1
  310. package/docs/settings.md +4 -1
  311. package/docs/themes.md +5 -3
  312. package/docs/usage.md +7 -0
  313. package/examples/extensions/custom-compaction.ts +1 -16
  314. package/examples/extensions/custom-provider-anthropic/index.ts +2 -1
  315. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  316. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  317. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -1
  318. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  319. package/examples/extensions/doom-overlay/wad-finder.ts +7 -3
  320. package/examples/extensions/gondolin/package-lock.json +2 -2
  321. package/examples/extensions/gondolin/package.json +1 -1
  322. package/examples/extensions/handoff.ts +2 -11
  323. package/examples/extensions/qna.ts +3 -7
  324. package/examples/extensions/sandbox/package-lock.json +2 -2
  325. package/examples/extensions/sandbox/package.json +1 -1
  326. package/examples/extensions/summarize.ts +7 -18
  327. package/examples/extensions/with-deps/package-lock.json +2 -2
  328. package/examples/extensions/with-deps/package.json +1 -1
  329. package/examples/rpc-extension-ui.ts +11 -2
  330. package/examples/sdk/02-custom-model.ts +1 -2
  331. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  332. package/examples/sdk/12-full-control.ts +1 -1
  333. package/examples/sdk/README.md +1 -1
  334. package/npm-shrinkwrap.json +64 -20
  335. 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";
@@ -17,7 +18,8 @@ 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
20
  import { createCompactionSummaryMessage } from "../../core/messages.js";
20
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope, resolveModelScopeWithDiagnostics, } from "../../core/model-resolver.js";
21
+ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScopeFromModels, } from "../../core/model-resolver.js";
22
+ import { CredentialSynchronizationError } from "../../core/model-runtime.js";
21
23
  import { DefaultPackageManager } from "../../core/package-manager.js";
22
24
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
23
25
  import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.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) => {
@@ -1288,8 +1449,12 @@ export class InteractiveMode {
1288
1449
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1289
1450
  this.showStartupNoticesIfNeeded();
1290
1451
  }
1452
+ applyFullscreenScrollbarSetting() {
1453
+ this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1454
+ }
1291
1455
  applyRuntimeSettings() {
1292
1456
  configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1457
+ this.applyFullscreenScrollbarSetting();
1293
1458
  this.footer.setSession(this.session);
1294
1459
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1295
1460
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
@@ -1353,6 +1518,9 @@ export class InteractiveMode {
1353
1518
  getRegisteredToolDefinition(toolName) {
1354
1519
  return this.session.getToolDefinition(toolName);
1355
1520
  }
1521
+ getMarkdownTransformers() {
1522
+ return [this.mermaidMarkdownTransformer, ...this.session.extensionRunner.getMarkdownTransformers()];
1523
+ }
1356
1524
  /**
1357
1525
  * Set up keyboard shortcuts registered by extensions.
1358
1526
  */
@@ -1432,7 +1600,7 @@ export class InteractiveMode {
1432
1600
  this.activeStatusIndicator?.dispose();
1433
1601
  this.activeStatusIndicator = undefined;
1434
1602
  this.statusContainer.clear();
1435
- if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1603
+ if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
1436
1604
  this.statusContainer.addChild(this.idleStatus);
1437
1605
  }
1438
1606
  }
@@ -1580,22 +1748,16 @@ export class InteractiveMode {
1580
1748
  if (this.customFooter?.dispose) {
1581
1749
  this.customFooter.dispose();
1582
1750
  }
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
- }
1751
+ this.footerContainer.clear();
1590
1752
  if (factory) {
1591
1753
  // Create and add custom footer, passing the data provider
1592
1754
  this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1593
- this.ui.addChild(this.customFooter);
1755
+ this.footerContainer.addChild(this.customFooter);
1594
1756
  }
1595
1757
  else {
1596
1758
  // Restore built-in footer
1597
1759
  this.customFooter = undefined;
1598
- this.ui.addChild(this.footer);
1760
+ this.footerContainer.addChild(this.footer);
1599
1761
  }
1600
1762
  this.ui.requestRender();
1601
1763
  }
@@ -1641,18 +1803,23 @@ export class InteractiveMode {
1641
1803
  this.ui.requestRender();
1642
1804
  }
1643
1805
  addExtensionTerminalInputListener(handler) {
1644
- const unsubscribe = this.ui.addInputListener(handler);
1645
- this.extensionTerminalInputUnsubscribers.add(unsubscribe);
1806
+ const subscription = { handler, unsubscribe: this.ui.addInputListener(handler) };
1807
+ this.extensionTerminalInputSubscriptions.add(subscription);
1646
1808
  return () => {
1647
- unsubscribe();
1648
- this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
1809
+ subscription.unsubscribe();
1810
+ this.extensionTerminalInputSubscriptions.delete(subscription);
1649
1811
  };
1650
1812
  }
1651
- clearExtensionTerminalInputListeners() {
1652
- for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
1653
- unsubscribe();
1813
+ rebindExtensionTerminalInputListeners() {
1814
+ for (const subscription of this.extensionTerminalInputSubscriptions) {
1815
+ subscription.unsubscribe();
1816
+ subscription.unsubscribe = this.ui.addInputListener(subscription.handler);
1654
1817
  }
1655
- this.extensionTerminalInputUnsubscribers.clear();
1818
+ }
1819
+ clearExtensionTerminalInputListeners() {
1820
+ for (const subscription of this.extensionTerminalInputSubscriptions)
1821
+ subscription.unsubscribe();
1822
+ this.extensionTerminalInputSubscriptions.clear();
1656
1823
  }
1657
1824
  /**
1658
1825
  * Create the ExtensionUIContext for extensions.
@@ -1747,6 +1914,7 @@ export class InteractiveMode {
1747
1914
  this.hideExtensionSelector();
1748
1915
  resolve(undefined);
1749
1916
  }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1917
+ this.disposeActiveSelector();
1750
1918
  this.editorContainer.clear();
1751
1919
  this.editorContainer.addChild(this.extensionSelector);
1752
1920
  this.ui.setFocus(this.extensionSelector);
@@ -1798,6 +1966,7 @@ export class InteractiveMode {
1798
1966
  this.hideExtensionInput();
1799
1967
  resolve(undefined);
1800
1968
  }, { tui: this.ui, timeout: opts?.timeout });
1969
+ this.disposeActiveSelector();
1801
1970
  this.editorContainer.clear();
1802
1971
  this.editorContainer.addChild(this.extensionInput);
1803
1972
  this.ui.setFocus(this.extensionInput);
@@ -1827,6 +1996,7 @@ export class InteractiveMode {
1827
1996
  this.hideExtensionEditor();
1828
1997
  resolve(undefined);
1829
1998
  }, undefined, this.settingsManager.getExternalEditorCommand());
1999
+ this.disposeActiveSelector();
1830
2000
  this.editorContainer.clear();
1831
2001
  this.editorContainer.addChild(this.extensionEditor);
1832
2002
  this.ui.setFocus(this.extensionEditor);
@@ -1851,6 +2021,7 @@ export class InteractiveMode {
1851
2021
  this.editorComponentFactory = factory;
1852
2022
  // Save text from current editor before switching
1853
2023
  const currentText = this.editor.getText();
2024
+ this.disposeActiveSelector();
1854
2025
  this.editorContainer.clear();
1855
2026
  if (factory) {
1856
2027
  // Create the custom editor with tui, theme, and keybindings
@@ -1867,6 +2038,9 @@ export class InteractiveMode {
1867
2038
  if (newEditor.setPaddingX !== undefined) {
1868
2039
  newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1869
2040
  }
2041
+ if (newEditor.setAutocompleteMaxVisible !== undefined) {
2042
+ newEditor.setAutocompleteMaxVisible(this.defaultEditor.getAutocompleteMaxVisible());
2043
+ }
1870
2044
  // Set autocomplete if supported
1871
2045
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1872
2046
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1971,6 +2145,7 @@ export class InteractiveMode {
1971
2145
  options?.onHandle?.(handle);
1972
2146
  }
1973
2147
  else {
2148
+ this.disposeActiveSelector();
1974
2149
  this.editorContainer.clear();
1975
2150
  this.editorContainer.addChild(component);
1976
2151
  this.ui.setFocus(component);
@@ -2057,7 +2232,7 @@ export class InteractiveMode {
2057
2232
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2058
2233
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2059
2234
  this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2060
- this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2235
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true }));
2061
2236
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2062
2237
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2063
2238
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2077,6 +2252,22 @@ export class InteractiveMode {
2077
2252
  void this.handleClipboardPaste();
2078
2253
  };
2079
2254
  }
2255
+ async handleRightClickPaste() {
2256
+ const target = this.renderer.getFocusedComponent();
2257
+ const handleInput = target?.handleInput;
2258
+ if (!target || !handleInput)
2259
+ return;
2260
+ try {
2261
+ const text = await readClipboardText();
2262
+ if (!text || this.renderer.getFocusedComponent() !== target)
2263
+ return;
2264
+ handleInput.call(target, `\x1b[200~${text}\x1b[201~`);
2265
+ this.ui.requestRender();
2266
+ }
2267
+ catch {
2268
+ // Silently ignore clipboard errors (may not have permission, etc.)
2269
+ }
2270
+ }
2080
2271
  async handleClipboardPaste() {
2081
2272
  try {
2082
2273
  const image = await readClipboardImage();
@@ -2345,17 +2536,17 @@ export class InteractiveMode {
2345
2536
  this.ui.requestRender();
2346
2537
  }
2347
2538
  else if (event.message.role === "assistant") {
2348
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2539
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2349
2540
  this.streamingMessage = event.message;
2350
2541
  this.chatContainer.addChild(this.streamingComponent);
2351
- this.streamingComponent.updateContent(this.streamingMessage);
2542
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2352
2543
  this.ui.requestRender();
2353
2544
  }
2354
2545
  break;
2355
2546
  case "message_update":
2356
2547
  if (this.streamingComponent && event.message.role === "assistant") {
2357
2548
  this.streamingMessage = event.message;
2358
- this.streamingComponent.updateContent(this.streamingMessage);
2549
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2359
2550
  for (const content of this.streamingMessage.content) {
2360
2551
  if (content.type === "toolCall") {
2361
2552
  if (!this.pendingTools.has(content.id)) {
@@ -2392,7 +2583,7 @@ export class InteractiveMode {
2392
2583
  : "Operation aborted";
2393
2584
  this.streamingMessage.errorMessage = errorMessage;
2394
2585
  }
2395
- this.streamingComponent.updateContent(this.streamingMessage);
2586
+ this.streamingComponent.updateContent(this.streamingMessage, false);
2396
2587
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2397
2588
  if (!errorMessage) {
2398
2589
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2666,12 +2857,12 @@ export class InteractiveMode {
2666
2857
  // Render user message separately if present
2667
2858
  if (skillBlock.userMessage) {
2668
2859
  this.chatContainer.addChild(new Spacer(1));
2669
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2860
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2670
2861
  this.chatContainer.addChild(userComponent);
2671
2862
  }
2672
2863
  }
2673
2864
  else {
2674
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2865
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2675
2866
  this.chatContainer.addChild(userComponent);
2676
2867
  }
2677
2868
  if (options?.populateHistory) {
@@ -2681,7 +2872,7 @@ export class InteractiveMode {
2681
2872
  break;
2682
2873
  }
2683
2874
  case "assistant": {
2684
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2875
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2685
2876
  this.chatContainer.addChild(assistantComponent);
2686
2877
  break;
2687
2878
  }
@@ -3115,6 +3306,8 @@ export class InteractiveMode {
3115
3306
  this.setToolsExpanded(!this.toolOutputExpanded);
3116
3307
  }
3117
3308
  setToolsExpanded(expanded) {
3309
+ if (expanded === this.toolOutputExpanded)
3310
+ return;
3118
3311
  this.toolOutputExpanded = expanded;
3119
3312
  const activeHeader = this.customHeader ?? this.builtInHeader;
3120
3313
  if (isExpandable(activeHeader)) {
@@ -3170,7 +3363,7 @@ export class InteractiveMode {
3170
3363
  }
3171
3364
  showError(errorMessage) {
3172
3365
  this.chatContainer.addChild(new Spacer(1));
3173
- this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3366
+ this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), this.outputPad, 0));
3174
3367
  this.ui.requestRender();
3175
3368
  }
3176
3369
  showWarning(warningMessage) {
@@ -3380,25 +3573,43 @@ export class InteractiveMode {
3380
3573
  // =========================================================================
3381
3574
  // Selectors
3382
3575
  // =========================================================================
3576
+ disposeActiveSelector() {
3577
+ const dispose = this.activeSelectorDispose;
3578
+ this.activeSelectorToken = undefined;
3579
+ this.activeSelectorDispose = undefined;
3580
+ dispose?.();
3581
+ }
3383
3582
  /**
3384
3583
  * Shows a selector component in place of the editor.
3385
3584
  * @param create Factory that receives a `done` callback and returns the component and focus target
3386
3585
  */
3387
3586
  showSelector(create) {
3587
+ const token = {};
3588
+ let dispose;
3388
3589
  const done = () => {
3590
+ dispose?.();
3591
+ if (this.activeSelectorToken !== token)
3592
+ return;
3593
+ this.activeSelectorToken = undefined;
3594
+ this.activeSelectorDispose = undefined;
3389
3595
  this.editorContainer.clear();
3390
3596
  this.editorContainer.addChild(this.editor);
3391
3597
  this.ui.setFocus(this.editor);
3392
3598
  };
3393
- const { component, focus } = create(done);
3599
+ const created = create(done);
3600
+ dispose = created.dispose;
3601
+ this.disposeActiveSelector();
3602
+ this.activeSelectorToken = token;
3603
+ this.activeSelectorDispose = dispose;
3394
3604
  this.editorContainer.clear();
3395
- this.editorContainer.addChild(component);
3396
- this.ui.setFocus(focus);
3605
+ this.editorContainer.addChild(created.component);
3606
+ this.ui.setFocus(created.focus);
3397
3607
  this.ui.requestRender();
3398
3608
  }
3399
3609
  showSettingsSelector() {
3400
3610
  this.showSelector((done) => {
3401
- const selector = new SettingsSelectorComponent({
3611
+ let selector;
3612
+ selector = new SettingsSelectorComponent({
3402
3613
  autoCompact: this.session.autoCompactionEnabled,
3403
3614
  showImages: this.settingsManager.getShowImages(),
3404
3615
  imageWidthCells: this.settingsManager.getImageWidthCells(),
@@ -3415,6 +3626,7 @@ export class InteractiveMode {
3415
3626
  terminalTheme: this.themeController.getTerminalTheme(),
3416
3627
  availableThemes: getAvailableThemes(),
3417
3628
  hideThinkingBlock: this.hideThinkingBlock,
3629
+ mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
3418
3630
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3419
3631
  enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
3420
3632
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
@@ -3428,6 +3640,8 @@ export class InteractiveMode {
3428
3640
  quietStartup: this.settingsManager.getQuietStartup(),
3429
3641
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3430
3642
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3643
+ tuiMode: this.ui.mode,
3644
+ fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3431
3645
  warnings: this.settingsManager.getWarnings(),
3432
3646
  }, {
3433
3647
  onAutoCompactChange: (enabled) => {
@@ -3496,6 +3710,11 @@ export class InteractiveMode {
3496
3710
  this.chatContainer.clear();
3497
3711
  this.rebuildChatFromMessages();
3498
3712
  },
3713
+ onMermaidRenderingModeChange: (mode) => {
3714
+ this.settingsManager.setMermaidRenderingMode(mode);
3715
+ this.chatContainer.invalidate();
3716
+ this.ui.requestRender();
3717
+ },
3499
3718
  onShowCacheMissNoticesChange: (shown) => {
3500
3719
  this.settingsManager.setShowCacheMissNotices(shown);
3501
3720
  this.rebuildChatFromMessages();
@@ -3565,6 +3784,21 @@ export class InteractiveMode {
3565
3784
  onShowTerminalProgressChange: (enabled) => {
3566
3785
  this.settingsManager.setShowTerminalProgress(enabled);
3567
3786
  },
3787
+ onTuiModeChange: (mode) => {
3788
+ if (!this.switchTuiMode(mode)) {
3789
+ selector?.getSettingsList().updateValue("tui-mode", this.ui.mode);
3790
+ this.showStatus("Close active overlays before changing TUI mode");
3791
+ return;
3792
+ }
3793
+ this.settingsManager.setTuiMode(mode);
3794
+ if (!this.activeStatusIndicator)
3795
+ this.statusContainer.clear();
3796
+ this.showStatus(`TUI mode: ${mode}`);
3797
+ },
3798
+ onFullscreenScrollbarChange: (mode) => {
3799
+ this.settingsManager.setFullscreenScrollbar(mode);
3800
+ this.applyFullscreenScrollbarSetting();
3801
+ },
3568
3802
  onWarningsChange: (warnings) => {
3569
3803
  this.settingsManager.setWarnings(warnings);
3570
3804
  },
@@ -3599,20 +3833,37 @@ export class InteractiveMode {
3599
3833
  this.showModelSelector(searchTerm);
3600
3834
  }
3601
3835
  async findExactModelMatch(searchTerm) {
3602
- const models = await this.getModelCandidates();
3603
- return findExactModelReferenceMatch(searchTerm, models);
3604
- }
3605
- async getModelCandidates() {
3606
- if (this.session.scopedModels.length > 0) {
3607
- return this.session.scopedModels.map((scoped) => scoped.model);
3608
- }
3836
+ const cachedModels = this.session.scopedModels.length > 0
3837
+ ? this.session.scopedModels.map((scoped) => scoped.model)
3838
+ : [...this.session.modelRuntime.getAvailableSnapshot()];
3839
+ const cachedMatch = findExactModelReferenceMatch(searchTerm, cachedModels);
3840
+ if (cachedMatch || this.session.scopedModels.length > 0)
3841
+ return cachedMatch;
3842
+ this.showStatus("Refreshing model catalogs…");
3843
+ const controller = new AbortController();
3844
+ let timedOut = false;
3845
+ const timeout = setTimeout(() => {
3846
+ timedOut = true;
3847
+ controller.abort();
3848
+ }, 15_000);
3609
3849
  try {
3610
- await this.session.modelRuntime.refresh();
3611
- return [...(await this.session.modelRuntime.getAvailable())];
3850
+ const result = await this.session.modelRuntime.refresh({ signal: controller.signal });
3851
+ if (result.aborted && timedOut) {
3852
+ this.showWarning("Model refresh timed out; searching cached models.");
3853
+ }
3854
+ else if (result.errors.size > 0) {
3855
+ this.showWarning(`Could not refresh ${[...result.errors.keys()].join(", ")}; searching cached models.`);
3856
+ }
3612
3857
  }
3613
- catch {
3614
- return [];
3858
+ catch (error) {
3859
+ this.showWarning(timedOut
3860
+ ? "Model refresh timed out; searching cached models."
3861
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`);
3615
3862
  }
3863
+ finally {
3864
+ clearTimeout(timeout);
3865
+ }
3866
+ return findExactModelReferenceMatch(searchTerm, [...this.session.modelRuntime.getAvailableSnapshot()]);
3616
3867
  }
3617
3868
  /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3618
3869
  updateAvailableProviderCount() {
@@ -3714,73 +3965,67 @@ export class InteractiveMode {
3714
3965
  done();
3715
3966
  this.ui.requestRender();
3716
3967
  }, initialSearchInput);
3717
- return { component: selector, focus: selector };
3968
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
3718
3969
  });
3719
3970
  }
3720
- async showModelsSelector() {
3721
- // Get all available models
3722
- await this.session.modelRuntime.refresh();
3723
- const allModels = [...(await this.session.modelRuntime.getAvailable())];
3724
- const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
3971
+ showModelsSelector() {
3972
+ let availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
3973
+ let availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
3725
3974
  const configuredPatterns = this.settingsManager.getEnabledModels();
3726
3975
  const sessionScopedModels = this.session.scopedModels;
3727
- if (allModels.length === 0 && !configuredPatterns?.length && sessionScopedModels.length === 0) {
3728
- this.showStatus("No models available");
3729
- return;
3730
- }
3731
- const configuredScope = configuredPatterns?.length
3732
- ? await resolveModelScopeWithDiagnostics(configuredPatterns, this.session.modelRuntime)
3733
- : undefined;
3734
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3735
- const hasSessionScope = sessionScopedModels.length > 0;
3736
- // Build enabled model IDs from session state or settings
3737
- let currentEnabledIds = null;
3738
- if (hasSessionScope) {
3739
- // Use current session's scoped models
3740
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3741
- }
3742
- else if (configuredScope) {
3743
- currentEnabledIds = configuredScope.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3744
- }
3745
- for (const diagnostic of configuredScope?.diagnostics ?? []) {
3746
- if (diagnostic.code !== "no-match")
3747
- continue;
3748
- currentEnabledIds ??= [];
3749
- if (!currentEnabledIds.includes(diagnostic.pattern))
3750
- currentEnabledIds.push(diagnostic.pattern);
3751
- }
3752
- // Helper to update session's scoped models (session-only, no persist)
3753
- const updateSessionModels = async (enabledIds) => {
3976
+ const configuredEnabledIds = (models) => {
3977
+ if (!configuredPatterns?.length)
3978
+ return null;
3979
+ const resolved = resolveModelScopeFromModels(configuredPatterns, models);
3980
+ const ids = resolved.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3981
+ for (const diagnostic of resolved.diagnostics) {
3982
+ if (diagnostic.code === "no-match" && !ids.includes(diagnostic.pattern))
3983
+ ids.push(diagnostic.pattern);
3984
+ }
3985
+ return ids;
3986
+ };
3987
+ let currentEnabledIds = sessionScopedModels.length > 0
3988
+ ? sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`)
3989
+ : configuredEnabledIds(availableModels);
3990
+ let selectionChanged = false;
3991
+ const updateSessionModels = (enabledIds) => {
3754
3992
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3755
- const hasEnabledAvailableModel = enabledIds?.some((id) => allModelIds.has(id)) ?? false;
3756
- const allAvailableModelsEnabled = enabledIds !== null && [...allModelIds].every((id) => enabledIds.includes(id));
3993
+ const hasEnabledAvailableModel = enabledIds?.some((id) => availableModelIds.has(id)) ?? false;
3994
+ const allAvailableModelsEnabled = enabledIds !== null && [...availableModelIds].every((id) => enabledIds.includes(id));
3757
3995
  if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
3758
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3759
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3760
- model: sm.model,
3761
- thinkingLevel: sm.thinkingLevel,
3996
+ const newScopedModels = resolveModelScopeFromModels(enabledIds, availableModels).scopedModels;
3997
+ this.session.setScopedModels(newScopedModels.map((scoped) => ({
3998
+ model: scoped.model,
3999
+ thinkingLevel: scoped.thinkingLevel,
3762
4000
  })));
3763
4001
  }
3764
4002
  else {
3765
- // All enabled or none enabled = no filter
3766
4003
  this.session.setScopedModels([]);
3767
4004
  }
3768
- await this.updateAvailableProviderCount();
4005
+ this.updateAvailableProviderCount();
3769
4006
  this.ui.requestRender();
3770
4007
  };
3771
4008
  this.showSelector((done) => {
4009
+ let disposed = false;
4010
+ let timedOut = false;
4011
+ const controller = new AbortController();
4012
+ const timeout = setTimeout(() => {
4013
+ timedOut = true;
4014
+ controller.abort();
4015
+ }, 15_000);
3772
4016
  const selector = new ScopedModelsSelectorComponent({
3773
- allModels,
4017
+ allModels: availableModels,
3774
4018
  enabledModelIds: currentEnabledIds,
4019
+ refreshStatus: "Refreshing model catalogs…",
3775
4020
  }, {
3776
- onChange: async (enabledIds) => {
3777
- await updateSessionModels(enabledIds);
4021
+ onChange: (enabledIds) => {
4022
+ selectionChanged = true;
4023
+ updateSessionModels(enabledIds);
3778
4024
  },
3779
4025
  onPersist: (enabledIds) => {
3780
- // Persist to settings
3781
4026
  const allEnabled = enabledIds !== null &&
3782
- enabledIds.length === allModels.length &&
3783
- enabledIds.every((id) => allModelIds.has(id));
4027
+ enabledIds.length === availableModels.length &&
4028
+ enabledIds.every((id) => availableModelIds.has(id));
3784
4029
  const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3785
4030
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3786
4031
  this.showStatus("Model selection saved to settings");
@@ -3790,7 +4035,51 @@ export class InteractiveMode {
3790
4035
  this.ui.requestRender();
3791
4036
  },
3792
4037
  });
3793
- return { component: selector, focus: selector };
4038
+ void this.session.modelRuntime
4039
+ .refresh({ signal: controller.signal })
4040
+ .then((result) => {
4041
+ if (disposed)
4042
+ return;
4043
+ availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
4044
+ availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
4045
+ if (!selectionChanged && sessionScopedModels.length === 0) {
4046
+ currentEnabledIds = configuredEnabledIds(availableModels);
4047
+ selector.updateModels(availableModels, currentEnabledIds);
4048
+ }
4049
+ else {
4050
+ selector.updateModels(availableModels);
4051
+ }
4052
+ if (currentEnabledIds !== null)
4053
+ updateSessionModels(currentEnabledIds);
4054
+ if (result.aborted && timedOut) {
4055
+ selector.setRefreshStatus("Model refresh timed out; showing cached models.", "warning");
4056
+ }
4057
+ else if (result.errors.size > 0) {
4058
+ selector.setRefreshStatus(`Could not refresh ${[...result.errors.keys()].join(", ")}; showing cached models.`, "warning");
4059
+ }
4060
+ else {
4061
+ selector.setRefreshStatus("Model catalogs refreshed.", "success");
4062
+ }
4063
+ this.ui.requestRender();
4064
+ })
4065
+ .catch((error) => {
4066
+ if (disposed)
4067
+ return;
4068
+ selector.setRefreshStatus(timedOut
4069
+ ? "Model refresh timed out; showing cached models."
4070
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
4071
+ this.ui.requestRender();
4072
+ })
4073
+ .finally(() => clearTimeout(timeout));
4074
+ return {
4075
+ component: selector,
4076
+ focus: selector,
4077
+ dispose: () => {
4078
+ disposed = true;
4079
+ clearTimeout(timeout);
4080
+ controller.abort();
4081
+ },
4082
+ };
3794
4083
  });
3795
4084
  }
3796
4085
  showUserMessageSelector() {
@@ -4052,7 +4341,7 @@ export class InteractiveMode {
4052
4341
  return options.sort((a, b) => a.name.localeCompare(b.name));
4053
4342
  }
4054
4343
  async getLogoutProviderOptions() {
4055
- return (await this.session.modelRuntime.listCredentials())
4344
+ return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4056
4345
  .map(({ providerId, type }) => ({
4057
4346
  id: providerId,
4058
4347
  name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
@@ -4070,7 +4359,6 @@ export class InteractiveMode {
4070
4359
  provider.name.toLowerCase() === normalizedProviderRef);
4071
4360
  }
4072
4361
  async handleLoginCommand(providerRef) {
4073
- await this.session.modelRuntime.getAvailable();
4074
4362
  if (!providerRef) {
4075
4363
  this.showLoginAuthTypeSelector();
4076
4364
  return;
@@ -4184,7 +4472,14 @@ export class InteractiveMode {
4184
4472
  this.showLoginAuthTypeSelector();
4185
4473
  return;
4186
4474
  }
4187
- const providerOptions = await this.getLogoutProviderOptions();
4475
+ let providerOptions;
4476
+ try {
4477
+ providerOptions = await this.getLogoutProviderOptions();
4478
+ }
4479
+ catch (error) {
4480
+ this.showError(`Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`);
4481
+ return;
4482
+ }
4188
4483
  if (providerOptions.length === 0) {
4189
4484
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4190
4485
  return;
@@ -4197,7 +4492,9 @@ export class InteractiveMode {
4197
4492
  return;
4198
4493
  }
4199
4494
  try {
4200
- await this.session.modelRuntime.logout(providerOption.id);
4495
+ await this.session.modelRuntime.logout(providerOption.id, {
4496
+ signal: AbortSignal.timeout(15_000),
4497
+ });
4201
4498
  await this.updateAvailableProviderCount();
4202
4499
  const message = providerOption.authType === "oauth"
4203
4500
  ? `Logged out of ${providerOption.name}`
@@ -4205,7 +4502,10 @@ export class InteractiveMode {
4205
4502
  this.showStatus(message);
4206
4503
  }
4207
4504
  catch (error) {
4208
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
4505
+ const message = error instanceof Error ? error.message : String(error);
4506
+ this.showError(error instanceof CredentialSynchronizationError
4507
+ ? `Credentials removed for ${providerOption.name}, but local model state could not be synchronized: ${message}`
4508
+ : `Logout failed: ${message}`);
4209
4509
  }
4210
4510
  }, () => {
4211
4511
  done();
@@ -4215,12 +4515,11 @@ export class InteractiveMode {
4215
4515
  });
4216
4516
  }
4217
4517
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4218
- await this.session.modelRuntime.getAvailable();
4219
4518
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4220
4519
  let selectedModel;
4221
4520
  let selectionError;
4222
4521
  if (isUnknownModel(previousModel)) {
4223
- const availableModels = await this.session.modelRuntime.getAvailable();
4522
+ const availableModels = this.session.modelRuntime.getAvailableSnapshot();
4224
4523
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4225
4524
  if (!hasDefaultModelProvider(providerId)) {
4226
4525
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4263,6 +4562,25 @@ export class InteractiveMode {
4263
4562
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
4264
4563
  }
4265
4564
  }
4565
+ const controller = new AbortController();
4566
+ const timeout = setTimeout(() => controller.abort(), 15_000);
4567
+ void this.session.modelRuntime
4568
+ .refresh({ providers: [providerId], signal: controller.signal })
4569
+ .then((result) => {
4570
+ if (result.aborted) {
4571
+ this.showWarning(`${actionLabel}, but its model catalog refresh timed out; using cached models.`);
4572
+ }
4573
+ else if (result.errors.size > 0) {
4574
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed; using cached models.`);
4575
+ }
4576
+ this.updateAvailableProviderCount();
4577
+ this.footer.invalidate();
4578
+ this.ui.requestRender();
4579
+ })
4580
+ .catch((error) => {
4581
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
4582
+ })
4583
+ .finally(() => clearTimeout(timeout));
4266
4584
  }
4267
4585
  showAmbientAuthDialog(providerOption) {
4268
4586
  const restoreEditor = () => {
@@ -4308,7 +4626,10 @@ export class InteractiveMode {
4308
4626
  catch (error) {
4309
4627
  restoreEditor();
4310
4628
  const errorMsg = error instanceof Error ? error.message : String(error);
4311
- if (errorMsg !== "Login cancelled") {
4629
+ if (error instanceof CredentialSynchronizationError) {
4630
+ this.showError(`Saved API key for ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4631
+ }
4632
+ else if (errorMsg !== "Login cancelled") {
4312
4633
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
4313
4634
  }
4314
4635
  }
@@ -4411,7 +4732,10 @@ export class InteractiveMode {
4411
4732
  catch (error) {
4412
4733
  restoreEditor();
4413
4734
  const errorMsg = error instanceof Error ? error.message : String(error);
4414
- if (errorMsg !== "Login cancelled") {
4735
+ if (error instanceof CredentialSynchronizationError) {
4736
+ this.showError(`Logged in to ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4737
+ }
4738
+ else if (errorMsg !== "Login cancelled") {
4415
4739
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
4416
4740
  }
4417
4741
  }
@@ -4462,7 +4786,6 @@ export class InteractiveMode {
4462
4786
  try {
4463
4787
  await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
4464
4788
  restoreChatBeforeSessionStart();
4465
- configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
4466
4789
  this.keybindings.reload();
4467
4790
  const activeHeader = this.customHeader ?? this.builtInHeader;
4468
4791
  if (isExpandable(activeHeader)) {
@@ -4470,20 +4793,7 @@ export class InteractiveMode {
4470
4793
  }
4471
4794
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
4472
4795
  await this.themeController.applyFromSettings();
4473
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
4474
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
4475
- this.defaultEditor.setPaddingX(editorPaddingX);
4476
- this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
4477
- if (this.editor !== this.defaultEditor) {
4478
- this.editor.setPaddingX?.(editorPaddingX);
4479
- this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4480
- }
4481
- this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4482
- const clearOnShrink = this.settingsManager.getClearOnShrink();
4483
- this.ui.setClearOnShrink(clearOnShrink);
4484
- if (!clearOnShrink && !this.activeStatusIndicator) {
4485
- this.statusContainer.clear();
4486
- }
4796
+ this.applyRuntimeSettings();
4487
4797
  this.setupAutocompleteProvider();
4488
4798
  const runner = this.session.extensionRunner;
4489
4799
  this.setupExtensionShortcuts(runner);
@@ -4679,7 +4989,7 @@ export class InteractiveMode {
4679
4989
  }
4680
4990
  }
4681
4991
  }
4682
- async handleCopyCommand() {
4992
+ async handleCopyCommand(options = {}) {
4683
4993
  const text = this.session.getLastAssistantText();
4684
4994
  if (!text) {
4685
4995
  this.showError("No agent messages to copy yet.");
@@ -4687,7 +4997,12 @@ export class InteractiveMode {
4687
4997
  }
4688
4998
  try {
4689
4999
  await copyToClipboard(text);
4690
- this.showStatus("Copied last agent message to clipboard");
5000
+ if (options.flashConfirmation && this.ui instanceof TuiAltScreen) {
5001
+ this.ui.flash("Copied!");
5002
+ }
5003
+ else {
5004
+ this.showStatus("Copied last agent message to clipboard");
5005
+ }
4691
5006
  }
4692
5007
  catch (error) {
4693
5008
  this.showError(error instanceof Error ? error.message : String(error));
@@ -5050,6 +5365,7 @@ export class InteractiveMode {
5050
5365
  }
5051
5366
  }
5052
5367
  stop() {
5368
+ this.disposeActiveSelector();
5053
5369
  if (this.settingsManager.getShowTerminalProgress()) {
5054
5370
  this.ui.terminal.setProgress(false);
5055
5371
  }
@@ -5062,7 +5378,7 @@ export class InteractiveMode {
5062
5378
  this.unsubscribe();
5063
5379
  }
5064
5380
  if (this.isInitialized) {
5065
- this.ui.stop();
5381
+ this.stopInteractiveTui();
5066
5382
  this.isInitialized = false;
5067
5383
  }
5068
5384
  this.unregisterSignalHandlers();