@earendil-works/pi-coding-agent 0.82.1 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +6 -1
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +27 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/config-selector.d.ts.map +1 -1
  8. package/dist/cli/config-selector.js +2 -2
  9. package/dist/cli/config-selector.js.map +1 -1
  10. package/dist/cli/credential-print.d.ts +23 -0
  11. package/dist/cli/credential-print.d.ts.map +1 -0
  12. package/dist/cli/credential-print.js +117 -0
  13. package/dist/cli/credential-print.js.map +1 -0
  14. package/dist/cli/experimental/auth.d.ts +16 -0
  15. package/dist/cli/experimental/auth.d.ts.map +1 -0
  16. package/dist/cli/experimental/auth.js +13 -0
  17. package/dist/cli/experimental/auth.js.map +1 -0
  18. package/dist/cli/experimental/cli.d.ts +6 -0
  19. package/dist/cli/experimental/cli.d.ts.map +1 -0
  20. package/dist/cli/experimental/cli.js +5 -0
  21. package/dist/cli/experimental/cli.js.map +1 -0
  22. package/dist/cli/experimental/command-options.d.ts +17 -0
  23. package/dist/cli/experimental/command-options.d.ts.map +1 -0
  24. package/dist/cli/experimental/command-options.js +35 -0
  25. package/dist/cli/experimental/command-options.js.map +1 -0
  26. package/dist/cli/experimental/command.d.ts +63 -0
  27. package/dist/cli/experimental/command.d.ts.map +1 -0
  28. package/dist/cli/experimental/command.js +130 -0
  29. package/dist/cli/experimental/command.js.map +1 -0
  30. package/dist/cli/experimental/commands/client.d.ts +13 -0
  31. package/dist/cli/experimental/commands/client.d.ts.map +1 -0
  32. package/dist/cli/experimental/commands/client.js +25 -0
  33. package/dist/cli/experimental/commands/client.js.map +1 -0
  34. package/dist/cli/experimental/commands/pi.d.ts +15 -0
  35. package/dist/cli/experimental/commands/pi.d.ts.map +1 -0
  36. package/dist/cli/experimental/commands/pi.js +28 -0
  37. package/dist/cli/experimental/commands/pi.js.map +1 -0
  38. package/dist/cli/experimental/commands/server.d.ts +13 -0
  39. package/dist/cli/experimental/commands/server.d.ts.map +1 -0
  40. package/dist/cli/experimental/commands/server.js +25 -0
  41. package/dist/cli/experimental/commands/server.js.map +1 -0
  42. package/dist/cli/experimental/transport-address.d.ts +10 -0
  43. package/dist/cli/experimental/transport-address.d.ts.map +1 -0
  44. package/dist/cli/experimental/transport-address.js +38 -0
  45. package/dist/cli/experimental/transport-address.js.map +1 -0
  46. package/dist/cli/list-models.d.ts +1 -1
  47. package/dist/cli/list-models.d.ts.map +1 -1
  48. package/dist/cli/list-models.js +2 -2
  49. package/dist/cli/list-models.js.map +1 -1
  50. package/dist/cli/startup-ui.d.ts +1 -1
  51. package/dist/cli/startup-ui.d.ts.map +1 -1
  52. package/dist/cli/startup-ui.js +2 -2
  53. package/dist/cli/startup-ui.js.map +1 -1
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +1 -0
  56. package/dist/cli.js.map +1 -1
  57. package/dist/client/index.d.ts +3 -0
  58. package/dist/client/index.d.ts.map +1 -0
  59. package/dist/client/index.js +3 -0
  60. package/dist/client/index.js.map +1 -0
  61. package/dist/client/remote-session.d.ts +53 -0
  62. package/dist/client/remote-session.d.ts.map +1 -0
  63. package/dist/client/remote-session.js +340 -0
  64. package/dist/client/remote-session.js.map +1 -0
  65. package/dist/client/transcript.d.ts +12 -0
  66. package/dist/client/transcript.d.ts.map +1 -0
  67. package/dist/client/transcript.js +98 -0
  68. package/dist/client/transcript.js.map +1 -0
  69. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  70. package/dist/core/agent-session-runtime.js +3 -0
  71. package/dist/core/agent-session-runtime.js.map +1 -1
  72. package/dist/core/agent-session-services.d.ts +1 -0
  73. package/dist/core/agent-session-services.d.ts.map +1 -1
  74. package/dist/core/agent-session-services.js +1 -0
  75. package/dist/core/agent-session-services.js.map +1 -1
  76. package/dist/core/agent-session.d.ts +2 -11
  77. package/dist/core/agent-session.d.ts.map +1 -1
  78. package/dist/core/agent-session.js +85 -76
  79. package/dist/core/agent-session.js.map +1 -1
  80. package/dist/core/auth-storage.d.ts +15 -9
  81. package/dist/core/auth-storage.d.ts.map +1 -1
  82. package/dist/core/auth-storage.js +159 -38
  83. package/dist/core/auth-storage.js.map +1 -1
  84. package/dist/core/extensions/index.d.ts +1 -1
  85. package/dist/core/extensions/index.d.ts.map +1 -1
  86. package/dist/core/extensions/index.js.map +1 -1
  87. package/dist/core/extensions/loader.d.ts.map +1 -1
  88. package/dist/core/extensions/loader.js +43 -20
  89. package/dist/core/extensions/loader.js.map +1 -1
  90. package/dist/core/extensions/runner.d.ts +3 -1
  91. package/dist/core/extensions/runner.d.ts.map +1 -1
  92. package/dist/core/extensions/runner.js +10 -0
  93. package/dist/core/extensions/runner.js.map +1 -1
  94. package/dist/core/extensions/types.d.ts +28 -3
  95. package/dist/core/extensions/types.d.ts.map +1 -1
  96. package/dist/core/extensions/types.js.map +1 -1
  97. package/dist/core/footer-data-provider.d.ts +10 -0
  98. package/dist/core/footer-data-provider.d.ts.map +1 -1
  99. package/dist/core/footer-data-provider.js +1 -1
  100. package/dist/core/footer-data-provider.js.map +1 -1
  101. package/dist/core/http-dispatcher.d.ts.map +1 -1
  102. package/dist/core/http-dispatcher.js +5 -0
  103. package/dist/core/http-dispatcher.js.map +1 -1
  104. package/dist/core/keybindings.d.ts +36 -4
  105. package/dist/core/keybindings.d.ts.map +1 -1
  106. package/dist/core/model-config.d.ts +29 -5
  107. package/dist/core/model-config.d.ts.map +1 -1
  108. package/dist/core/model-config.js +4 -0
  109. package/dist/core/model-config.js.map +1 -1
  110. package/dist/core/model-registry.d.ts +5 -3
  111. package/dist/core/model-registry.d.ts.map +1 -1
  112. package/dist/core/model-registry.js +13 -10
  113. package/dist/core/model-registry.js.map +1 -1
  114. package/dist/core/model-resolver.d.ts +4 -3
  115. package/dist/core/model-resolver.d.ts.map +1 -1
  116. package/dist/core/model-resolver.js +41 -10
  117. package/dist/core/model-resolver.js.map +1 -1
  118. package/dist/core/model-runtime.d.ts +30 -13
  119. package/dist/core/model-runtime.d.ts.map +1 -1
  120. package/dist/core/model-runtime.js +231 -73
  121. package/dist/core/model-runtime.js.map +1 -1
  122. package/dist/core/models-store.d.ts +12 -7
  123. package/dist/core/models-store.d.ts.map +1 -1
  124. package/dist/core/models-store.js +83 -14
  125. package/dist/core/models-store.js.map +1 -1
  126. package/dist/core/package-manager.d.ts +4 -1
  127. package/dist/core/package-manager.d.ts.map +1 -1
  128. package/dist/core/package-manager.js +76 -42
  129. package/dist/core/package-manager.js.map +1 -1
  130. package/dist/core/pi-manifest.d.ts +8 -0
  131. package/dist/core/pi-manifest.d.ts.map +1 -0
  132. package/dist/core/pi-manifest.js +25 -0
  133. package/dist/core/pi-manifest.js.map +1 -0
  134. package/dist/core/provider-composer.d.ts +4 -1
  135. package/dist/core/provider-composer.d.ts.map +1 -1
  136. package/dist/core/provider-composer.js +36 -15
  137. package/dist/core/provider-composer.js.map +1 -1
  138. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  139. package/dist/core/remote-catalog-provider.js +73 -67
  140. package/dist/core/remote-catalog-provider.js.map +1 -1
  141. package/dist/core/resource-loader.d.ts +15 -0
  142. package/dist/core/resource-loader.d.ts.map +1 -1
  143. package/dist/core/resource-loader.js +64 -10
  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/find.d.ts +3 -0
  157. package/dist/core/tools/find.d.ts.map +1 -1
  158. package/dist/core/tools/find.js +11 -18
  159. package/dist/core/tools/find.js.map +1 -1
  160. package/dist/core/tools/read.d.ts.map +1 -1
  161. package/dist/core/tools/read.js +1 -1
  162. package/dist/core/tools/read.js.map +1 -1
  163. package/dist/extensions/llama/index.d.ts.map +1 -1
  164. package/dist/extensions/llama/index.js +11 -2
  165. package/dist/extensions/llama/index.js.map +1 -1
  166. package/dist/extensions/llama/provider.d.ts.map +1 -1
  167. package/dist/extensions/llama/provider.js +22 -8
  168. package/dist/extensions/llama/provider.js.map +1 -1
  169. package/dist/index.d.ts +4 -4
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +1 -1
  172. package/dist/index.js.map +1 -1
  173. package/dist/main.d.ts.map +1 -1
  174. package/dist/main.js +57 -5
  175. package/dist/main.js.map +1 -1
  176. package/dist/modes/index.d.ts +1 -0
  177. package/dist/modes/index.d.ts.map +1 -1
  178. package/dist/modes/index.js.map +1 -1
  179. package/dist/modes/interactive/components/assistant-message.d.ts +5 -2
  180. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/assistant-message.js +13 -4
  182. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  183. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  184. package/dist/modes/interactive/components/custom-editor.js +7 -0
  185. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  186. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  187. package/dist/modes/interactive/components/footer.js +1 -1
  188. package/dist/modes/interactive/components/footer.js.map +1 -1
  189. package/dist/modes/interactive/components/markdown-transform.d.ts +3 -0
  190. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -0
  191. package/dist/modes/interactive/components/markdown-transform.js +19 -0
  192. package/dist/modes/interactive/components/markdown-transform.js.map +1 -0
  193. package/dist/modes/interactive/components/mermaid.d.ts +11 -0
  194. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  195. package/dist/modes/interactive/components/mermaid.js +75 -0
  196. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  197. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  198. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/model-selector.js +20 -5
  200. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  201. package/dist/modes/interactive/components/scoped-models-selector.d.ts +4 -0
  202. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  203. package/dist/modes/interactive/components/scoped-models-selector.js +26 -0
  204. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  205. package/dist/modes/interactive/components/settings-selector.d.ts +8 -2
  206. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/settings-selector.js +30 -0
  208. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  209. package/dist/modes/interactive/components/user-message.d.ts +3 -1
  210. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/user-message.js +9 -2
  212. package/dist/modes/interactive/components/user-message.js.map +1 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +33 -2
  214. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  215. package/dist/modes/interactive/interactive-mode.js +443 -144
  216. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  217. package/dist/modes/interactive/theme/dark.json +1 -0
  218. package/dist/modes/interactive/theme/light.json +1 -0
  219. package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
  220. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  221. package/dist/modes/interactive/theme/theme-controller.js +10 -1
  222. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  223. package/dist/modes/interactive/theme/theme-schema.json +5 -1
  224. package/dist/modes/interactive/theme/theme.d.ts +2 -2
  225. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  226. package/dist/modes/interactive/theme/theme.js +13 -3
  227. package/dist/modes/interactive/theme/theme.js.map +1 -1
  228. package/dist/modes/json-event.d.ts +28 -0
  229. package/dist/modes/json-event.d.ts.map +1 -0
  230. package/dist/modes/json-event.js +12 -0
  231. package/dist/modes/json-event.js.map +1 -0
  232. package/dist/modes/print-mode.d.ts.map +1 -1
  233. package/dist/modes/print-mode.js +12 -2
  234. package/dist/modes/print-mode.js.map +1 -1
  235. package/dist/modes/rpc/rpc-client.d.ts +5 -4
  236. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  237. package/dist/modes/rpc/rpc-client.js.map +1 -1
  238. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  239. package/dist/modes/rpc/rpc-mode.js +17 -3
  240. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  241. package/dist/package-manager-cli.d.ts.map +1 -1
  242. package/dist/package-manager-cli.js +11 -9
  243. package/dist/package-manager-cli.js.map +1 -1
  244. package/dist/rpc-entry.d.ts.map +1 -1
  245. package/dist/rpc-entry.js +1 -0
  246. package/dist/rpc-entry.js.map +1 -1
  247. package/dist/utils/abort.d.ts +5 -0
  248. package/dist/utils/abort.d.ts.map +1 -0
  249. package/dist/utils/abort.js +48 -0
  250. package/dist/utils/abort.js.map +1 -0
  251. package/dist/utils/clipboard.d.ts +1 -1
  252. package/dist/utils/clipboard.d.ts.map +1 -1
  253. package/dist/utils/clipboard.js +22 -2
  254. package/dist/utils/clipboard.js.map +1 -1
  255. package/dist/utils/management-http.d.ts +23 -0
  256. package/dist/utils/management-http.d.ts.map +1 -0
  257. package/dist/utils/management-http.js +50 -0
  258. package/dist/utils/management-http.js.map +1 -0
  259. package/dist/utils/paths.d.ts +3 -0
  260. package/dist/utils/paths.d.ts.map +1 -1
  261. package/dist/utils/paths.js +23 -1
  262. package/dist/utils/paths.js.map +1 -1
  263. package/dist/utils/tool-result-images.d.ts +19 -0
  264. package/dist/utils/tool-result-images.d.ts.map +1 -0
  265. package/dist/utils/tool-result-images.js +45 -0
  266. package/dist/utils/tool-result-images.js.map +1 -0
  267. package/dist/utils/tools-manager.d.ts.map +1 -1
  268. package/dist/utils/tools-manager.js +4 -6
  269. package/dist/utils/tools-manager.js.map +1 -1
  270. package/dist/utils/version-check.d.ts +4 -0
  271. package/dist/utils/version-check.d.ts.map +1 -1
  272. package/dist/utils/version-check.js +20 -2
  273. package/dist/utils/version-check.js.map +1 -1
  274. package/docs/compaction.md +2 -2
  275. package/docs/custom-provider.md +16 -8
  276. package/docs/extensions.md +30 -4
  277. package/docs/json.md +19 -14
  278. package/docs/keybindings.md +38 -10
  279. package/docs/models.md +24 -3
  280. package/docs/providers.md +2 -0
  281. package/docs/quickstart.md +2 -0
  282. package/docs/rpc.md +13 -11
  283. package/docs/sdk.md +21 -2
  284. package/docs/security.md +1 -1
  285. package/docs/session-format.md +2 -0
  286. package/docs/settings.md +4 -1
  287. package/docs/themes.md +5 -3
  288. package/docs/usage.md +7 -0
  289. package/examples/extensions/custom-compaction.ts +1 -16
  290. package/examples/extensions/custom-provider-anthropic/index.ts +10 -3
  291. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  292. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  293. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -1
  294. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  295. package/examples/extensions/doom-overlay/wad-finder.ts +7 -3
  296. package/examples/extensions/gondolin/package-lock.json +2 -2
  297. package/examples/extensions/gondolin/package.json +1 -1
  298. package/examples/extensions/handoff.ts +2 -11
  299. package/examples/extensions/qna.ts +3 -7
  300. package/examples/extensions/sandbox/package-lock.json +2 -2
  301. package/examples/extensions/sandbox/package.json +1 -1
  302. package/examples/extensions/summarize.ts +7 -18
  303. package/examples/extensions/with-deps/package-lock.json +2 -2
  304. package/examples/extensions/with-deps/package.json +1 -1
  305. package/examples/rpc-extension-ui.ts +11 -2
  306. package/examples/sdk/02-custom-model.ts +1 -2
  307. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  308. package/examples/sdk/12-full-control.ts +3 -1
  309. package/examples/sdk/README.md +1 -1
  310. package/npm-shrinkwrap.json +70 -26
  311. package/package.json +14 -7
@@ -6,7 +6,8 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
9
+ import * as TuiLayouts from "@earendil-works/pi-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
10
11
  import chalk from "chalk";
11
12
  import { spawn, spawnSync } from "child_process";
12
13
  import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
@@ -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,48 @@ 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, { openUrl: openBrowser });
173
+ }
174
+ return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
175
+ }
176
+ /** Stable reference for components while InteractiveMode replaces the active renderer. */
177
+ export function createInteractiveTuiReference(getTui) {
178
+ return new Proxy({}, {
179
+ get: (_target, property) => {
180
+ const tui = getTui();
181
+ const value = Reflect.get(tui, property, tui);
182
+ if (typeof value !== "function")
183
+ return value;
184
+ return (...args) => {
185
+ const tui = getTui();
186
+ const method = Reflect.get(tui, property, tui);
187
+ if (typeof method !== "function")
188
+ throw new TypeError(`TUI property ${String(property)} is not callable`);
189
+ return Reflect.apply(method, tui, args);
190
+ };
191
+ },
192
+ set: (_target, property, value) => {
193
+ const tui = getTui();
194
+ return Reflect.set(tui, property, value, tui);
195
+ },
196
+ has: (_target, property) => Reflect.has(getTui(), property),
197
+ getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
198
+ });
199
+ }
164
200
  export class InteractiveMode {
165
201
  runtimeHost;
202
+ renderer;
166
203
  ui;
204
+ mainScreenRenderState;
167
205
  loadedResourcesContainer;
168
206
  chatContainer;
207
+ documentContainer;
208
+ transcriptScrollView;
209
+ fullscreenLayoutRoot;
169
210
  pendingMessagesContainer;
170
211
  statusContainer;
171
212
  defaultEditor;
@@ -175,7 +216,10 @@ export class InteractiveMode {
175
216
  autocompleteProviderWrappers = [];
176
217
  fdPath;
177
218
  editorContainer;
219
+ activeSelectorToken;
220
+ activeSelectorDispose;
178
221
  footer;
222
+ footerContainer;
179
223
  footerDataProvider;
180
224
  // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
181
225
  keybindings;
@@ -209,6 +253,10 @@ export class InteractiveMode {
209
253
  // Thinking block visibility state
210
254
  hideThinkingBlock = false;
211
255
  outputPad = 1;
256
+ mermaidMarkdownTransformer = createMermaidMarkdownTransformer({
257
+ getMode: () => this.settingsManager.getMermaidRenderingMode(),
258
+ theme,
259
+ });
212
260
  // Skill commands: command name -> skill file path
213
261
  skillCommands = new Map();
214
262
  // Agent subscription unsubscribe function
@@ -232,7 +280,7 @@ export class InteractiveMode {
232
280
  extensionSelector = undefined;
233
281
  extensionInput = undefined;
234
282
  extensionEditor = undefined;
235
- extensionTerminalInputUnsubscribers = new Set();
283
+ extensionTerminalInputSubscriptions = new Set();
236
284
  // Extension widgets (components rendered above/below the editor)
237
285
  extensionWidgetsAbove = new Map();
238
286
  extensionWidgetsBelow = new Map();
@@ -264,7 +312,8 @@ export class InteractiveMode {
264
312
  }
265
313
  constructor(runtimeHost, options = {}) {
266
314
  this.runtimeHost = runtimeHost;
267
- this.options = options;
315
+ const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
316
+ this.options = { ...options, tuiMode };
268
317
  this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
269
318
  this.runtimeHost.setBeforeSessionInvalidate(() => {
270
319
  this.resetExtensionUI();
@@ -273,11 +322,20 @@ export class InteractiveMode {
273
322
  await this.rebindCurrentSession({ renderBeforeBind: true });
274
323
  });
275
324
  this.version = VERSION;
276
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor(), getAgentDir());
325
+ this.renderer = createInteractiveTui({
326
+ tuiMode,
327
+ showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
328
+ logDirectory: getAgentDir(),
329
+ });
330
+ this.ui = createInteractiveTuiReference(() => this.renderer);
277
331
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
278
332
  this.headerContainer = new Container();
279
333
  this.loadedResourcesContainer = new Container();
280
334
  this.chatContainer = new Container();
335
+ this.documentContainer = new Container();
336
+ this.documentContainer.addChild(this.headerContainer);
337
+ this.documentContainer.addChild(this.loadedResourcesContainer);
338
+ this.documentContainer.addChild(this.chatContainer);
281
339
  this.pendingMessagesContainer = new Container();
282
340
  this.statusContainer = new Container();
283
341
  this.widgetContainerAbove = new Container();
@@ -296,6 +354,8 @@ export class InteractiveMode {
296
354
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
297
355
  this.footer = new FooterComponent(this.session, this.footerDataProvider);
298
356
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
357
+ this.footerContainer = new Container();
358
+ this.footerContainer.addChild(this.footer);
299
359
  // Load hide thinking block setting
300
360
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
301
361
  this.outputPad = this.settingsManager.getOutputPad();
@@ -351,11 +411,10 @@ export class InteractiveMode {
351
411
  }));
352
412
  const modelCommand = slashCommands.find((command) => command.name === "model");
353
413
  if (modelCommand) {
354
- modelCommand.getArgumentCompletions = async (prefix) => {
355
- // Get available models (scoped or from registry)
414
+ modelCommand.getArgumentCompletions = (prefix) => {
356
415
  const models = this.session.scopedModels.length > 0
357
416
  ? this.session.scopedModels.map((s) => s.model)
358
- : await this.session.modelRuntime.getAvailable();
417
+ : this.session.modelRuntime.getAvailableSnapshot();
359
418
  if (models.length === 0)
360
419
  return null;
361
420
  // Create items with provider/id format
@@ -456,6 +515,72 @@ export class InteractiveMode {
456
515
  }
457
516
  this.chatContainer.addChild(new DynamicBorder());
458
517
  }
518
+ mountInteractiveTui(tui, components) {
519
+ for (const component of components)
520
+ tui.addChild(component);
521
+ if (TuiLayouts.isViewportTUI(tui)) {
522
+ if (!this.fullscreenLayoutRoot)
523
+ throw new Error("Fullscreen layout is not initialized");
524
+ tui.setLayoutRoot(this.fullscreenLayoutRoot);
525
+ }
526
+ }
527
+ stopInteractiveTui() {
528
+ if (this.renderer.mode === "fullscreen") {
529
+ while (this.renderer.hasOverlayEntries)
530
+ this.renderer.hideOverlay();
531
+ this.switchTuiMode("regular", false, false);
532
+ this.renderer.renderNow();
533
+ }
534
+ this.ui.stop();
535
+ }
536
+ switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
537
+ const previousUi = this.renderer;
538
+ if (mode === previousUi.mode)
539
+ return true;
540
+ if (previousUi.hasOverlayEntries)
541
+ return false;
542
+ const components = [...previousUi.children];
543
+ const focus = previousUi.getFocusedComponent();
544
+ const terminal = previousUi.terminal;
545
+ const showHardwareCursor = previousUi.getShowHardwareCursor();
546
+ const clearOnShrink = previousUi.getClearOnShrink();
547
+ const onDebug = previousUi.onDebug;
548
+ if (previousUi instanceof TuiMainScreen) {
549
+ this.mainScreenRenderState = previousUi.captureRenderState();
550
+ }
551
+ previousUi.stop({ preserveScreen: true });
552
+ previousUi.setFocus(null);
553
+ previousUi.clear();
554
+ if (TuiLayouts.isViewportTUI(previousUi))
555
+ previousUi.setLayoutRoot(undefined);
556
+ const nextUi = createInteractiveTui({
557
+ tuiMode: mode,
558
+ showHardwareCursor,
559
+ logDirectory: getAgentDir(),
560
+ terminal,
561
+ });
562
+ nextUi.setClearOnShrink(clearOnShrink);
563
+ nextUi.onDebug = onDebug;
564
+ if (nextUi instanceof TuiMainScreen && this.mainScreenRenderState) {
565
+ nextUi.restoreRenderState(this.mainScreenRenderState);
566
+ }
567
+ this.renderer = nextUi;
568
+ this.options.tuiMode = mode;
569
+ this.mountInteractiveTui(nextUi, components);
570
+ nextUi.invalidate();
571
+ nextUi.setFocus(focus);
572
+ if (!startRenderer)
573
+ return true;
574
+ nextUi.start();
575
+ this.themeController.rebindTui();
576
+ this.rebindExtensionTerminalInputListeners();
577
+ if (restoreProgress &&
578
+ this.settingsManager.getShowTerminalProgress() &&
579
+ (this.session.isStreaming || this.session.isCompacting)) {
580
+ terminal.setProgress(true);
581
+ }
582
+ return true;
583
+ }
459
584
  async init() {
460
585
  if (this.isInitialized)
461
586
  return;
@@ -479,18 +604,36 @@ export class InteractiveMode {
479
604
  : "";
480
605
  console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
481
606
  }
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);
607
+ // Keep one component tree and remount it when changing renderers.
489
608
  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);
609
+ this.transcriptScrollView = new TuiLayouts.ScrollView(this.documentContainer, {
610
+ follow: "end",
611
+ primary: true,
612
+ overscroll: "chain",
613
+ scrollbar: this.settingsManager.getFullscreenScrollbar(),
614
+ scrollbarStyle: (text) => theme.bg("scrollbarThumb", text),
615
+ });
616
+ const dock = new TuiLayouts.VStack([
617
+ { component: this.pendingMessagesContainer, shrink: 1, minSize: 0 },
618
+ { component: this.statusContainer, shrink: 1, minSize: 0 },
619
+ { component: this.widgetContainerAbove, shrink: 1, minSize: 0 },
620
+ { component: this.editorContainer, shrink: 1, minSize: 3 },
621
+ { component: this.widgetContainerBelow, shrink: 1, minSize: 0 },
622
+ { component: this.footerContainer, shrink: 1, minSize: 1 },
623
+ ]);
624
+ this.fullscreenLayoutRoot = new TuiLayouts.VStack([
625
+ { component: this.transcriptScrollView, basis: 0, grow: 1, shrink: 1, minSize: 1 },
626
+ { component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
627
+ ]);
628
+ this.mountInteractiveTui(this.renderer, [
629
+ this.documentContainer,
630
+ this.pendingMessagesContainer,
631
+ this.statusContainer,
632
+ this.widgetContainerAbove,
633
+ this.editorContainer,
634
+ this.widgetContainerBelow,
635
+ this.footerContainer,
636
+ ]);
494
637
  this.ui.setFocus(this.editor);
495
638
  this.setupKeyHandlers();
496
639
  this.setupEditorSubmitHandler();
@@ -582,10 +725,13 @@ export class InteractiveMode {
582
725
  async run() {
583
726
  await this.init();
584
727
  if (!process.env.PI_OFFLINE) {
728
+ const controller = new AbortController();
729
+ const timeout = setTimeout(() => controller.abort(), 15_000);
585
730
  void this.session.modelRuntime
586
- .refresh()
731
+ .refresh({ signal: controller.signal })
587
732
  .then(() => this.updateAvailableProviderCount())
588
- .catch(() => { });
733
+ .catch(() => { })
734
+ .finally(() => clearTimeout(timeout));
589
735
  }
590
736
  // Start version check asynchronously
591
737
  checkForNewPiVersion(this.version).then((newRelease) => {
@@ -1106,7 +1252,12 @@ export class InteractiveMode {
1106
1252
  }
1107
1253
  }
1108
1254
  if (showListing) {
1109
- const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1255
+ const systemPromptSource = this.session.resourceLoader.getSystemPromptSource();
1256
+ const contextFiles = [
1257
+ ...(systemPromptSource ? [systemPromptSource] : []),
1258
+ ...this.session.resourceLoader.getAppendSystemPromptSources(),
1259
+ ...this.session.resourceLoader.getAgentsFiles().agentsFiles,
1260
+ ];
1110
1261
  if (contextFiles.length > 0) {
1111
1262
  this.loadedResourcesContainer.addChild(new Spacer(1));
1112
1263
  const contextList = contextFiles
@@ -1283,8 +1434,12 @@ export class InteractiveMode {
1283
1434
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1284
1435
  this.showStartupNoticesIfNeeded();
1285
1436
  }
1437
+ applyFullscreenScrollbarSetting() {
1438
+ this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1439
+ }
1286
1440
  applyRuntimeSettings() {
1287
1441
  configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1442
+ this.applyFullscreenScrollbarSetting();
1288
1443
  this.footer.setSession(this.session);
1289
1444
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1290
1445
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
@@ -1306,16 +1461,19 @@ export class InteractiveMode {
1306
1461
  }
1307
1462
  }
1308
1463
  async rebindCurrentSession(options = {}) {
1464
+ const session = this.session;
1309
1465
  this.unsubscribe?.();
1310
1466
  this.unsubscribe = undefined;
1311
1467
  this.applyRuntimeSettings();
1312
1468
  if (options.renderBeforeBind) {
1313
1469
  this.renderCurrentSessionState();
1314
1470
  this.subscribeToAgent();
1315
- await this.bindCurrentSessionExtensions();
1316
1471
  }
1317
- else {
1318
- await this.bindCurrentSessionExtensions();
1472
+ await this.bindCurrentSessionExtensions();
1473
+ if (this.session !== session) {
1474
+ return;
1475
+ }
1476
+ if (!options.renderBeforeBind) {
1319
1477
  this.subscribeToAgent();
1320
1478
  }
1321
1479
  await this.updateAvailableProviderCount();
@@ -1345,6 +1503,9 @@ export class InteractiveMode {
1345
1503
  getRegisteredToolDefinition(toolName) {
1346
1504
  return this.session.getToolDefinition(toolName);
1347
1505
  }
1506
+ getMarkdownTransformers() {
1507
+ return [this.mermaidMarkdownTransformer, ...this.session.extensionRunner.getMarkdownTransformers()];
1508
+ }
1348
1509
  /**
1349
1510
  * Set up keyboard shortcuts registered by extensions.
1350
1511
  */
@@ -1361,6 +1522,7 @@ export class InteractiveMode {
1361
1522
  sessionManager: this.sessionManager,
1362
1523
  modelRegistry: extensionRunner.getModelRegistry(),
1363
1524
  model: this.session.model,
1525
+ scopedModels: this.session.scopedModels,
1364
1526
  thinkingLevel: this.session.thinkingLevel,
1365
1527
  isIdle: () => this.session.isIdle,
1366
1528
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
@@ -1423,7 +1585,7 @@ export class InteractiveMode {
1423
1585
  this.activeStatusIndicator?.dispose();
1424
1586
  this.activeStatusIndicator = undefined;
1425
1587
  this.statusContainer.clear();
1426
- if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1588
+ if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
1427
1589
  this.statusContainer.addChild(this.idleStatus);
1428
1590
  }
1429
1591
  }
@@ -1571,22 +1733,16 @@ export class InteractiveMode {
1571
1733
  if (this.customFooter?.dispose) {
1572
1734
  this.customFooter.dispose();
1573
1735
  }
1574
- // Remove current footer from UI
1575
- if (this.customFooter) {
1576
- this.ui.removeChild(this.customFooter);
1577
- }
1578
- else {
1579
- this.ui.removeChild(this.footer);
1580
- }
1736
+ this.footerContainer.clear();
1581
1737
  if (factory) {
1582
1738
  // Create and add custom footer, passing the data provider
1583
1739
  this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1584
- this.ui.addChild(this.customFooter);
1740
+ this.footerContainer.addChild(this.customFooter);
1585
1741
  }
1586
1742
  else {
1587
1743
  // Restore built-in footer
1588
1744
  this.customFooter = undefined;
1589
- this.ui.addChild(this.footer);
1745
+ this.footerContainer.addChild(this.footer);
1590
1746
  }
1591
1747
  this.ui.requestRender();
1592
1748
  }
@@ -1632,18 +1788,23 @@ export class InteractiveMode {
1632
1788
  this.ui.requestRender();
1633
1789
  }
1634
1790
  addExtensionTerminalInputListener(handler) {
1635
- const unsubscribe = this.ui.addInputListener(handler);
1636
- this.extensionTerminalInputUnsubscribers.add(unsubscribe);
1791
+ const subscription = { handler, unsubscribe: this.ui.addInputListener(handler) };
1792
+ this.extensionTerminalInputSubscriptions.add(subscription);
1637
1793
  return () => {
1638
- unsubscribe();
1639
- this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
1794
+ subscription.unsubscribe();
1795
+ this.extensionTerminalInputSubscriptions.delete(subscription);
1640
1796
  };
1641
1797
  }
1642
- clearExtensionTerminalInputListeners() {
1643
- for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
1644
- unsubscribe();
1798
+ rebindExtensionTerminalInputListeners() {
1799
+ for (const subscription of this.extensionTerminalInputSubscriptions) {
1800
+ subscription.unsubscribe();
1801
+ subscription.unsubscribe = this.ui.addInputListener(subscription.handler);
1645
1802
  }
1646
- this.extensionTerminalInputUnsubscribers.clear();
1803
+ }
1804
+ clearExtensionTerminalInputListeners() {
1805
+ for (const subscription of this.extensionTerminalInputSubscriptions)
1806
+ subscription.unsubscribe();
1807
+ this.extensionTerminalInputSubscriptions.clear();
1647
1808
  }
1648
1809
  /**
1649
1810
  * Create the ExtensionUIContext for extensions.
@@ -1738,6 +1899,7 @@ export class InteractiveMode {
1738
1899
  this.hideExtensionSelector();
1739
1900
  resolve(undefined);
1740
1901
  }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1902
+ this.disposeActiveSelector();
1741
1903
  this.editorContainer.clear();
1742
1904
  this.editorContainer.addChild(this.extensionSelector);
1743
1905
  this.ui.setFocus(this.extensionSelector);
@@ -1789,6 +1951,7 @@ export class InteractiveMode {
1789
1951
  this.hideExtensionInput();
1790
1952
  resolve(undefined);
1791
1953
  }, { tui: this.ui, timeout: opts?.timeout });
1954
+ this.disposeActiveSelector();
1792
1955
  this.editorContainer.clear();
1793
1956
  this.editorContainer.addChild(this.extensionInput);
1794
1957
  this.ui.setFocus(this.extensionInput);
@@ -1818,6 +1981,7 @@ export class InteractiveMode {
1818
1981
  this.hideExtensionEditor();
1819
1982
  resolve(undefined);
1820
1983
  }, undefined, this.settingsManager.getExternalEditorCommand());
1984
+ this.disposeActiveSelector();
1821
1985
  this.editorContainer.clear();
1822
1986
  this.editorContainer.addChild(this.extensionEditor);
1823
1987
  this.ui.setFocus(this.extensionEditor);
@@ -1842,6 +2006,7 @@ export class InteractiveMode {
1842
2006
  this.editorComponentFactory = factory;
1843
2007
  // Save text from current editor before switching
1844
2008
  const currentText = this.editor.getText();
2009
+ this.disposeActiveSelector();
1845
2010
  this.editorContainer.clear();
1846
2011
  if (factory) {
1847
2012
  // Create the custom editor with tui, theme, and keybindings
@@ -1858,6 +2023,9 @@ export class InteractiveMode {
1858
2023
  if (newEditor.setPaddingX !== undefined) {
1859
2024
  newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1860
2025
  }
2026
+ if (newEditor.setAutocompleteMaxVisible !== undefined) {
2027
+ newEditor.setAutocompleteMaxVisible(this.defaultEditor.getAutocompleteMaxVisible());
2028
+ }
1861
2029
  // Set autocomplete if supported
1862
2030
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1863
2031
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1962,6 +2130,7 @@ export class InteractiveMode {
1962
2130
  options?.onHandle?.(handle);
1963
2131
  }
1964
2132
  else {
2133
+ this.disposeActiveSelector();
1965
2134
  this.editorContainer.clear();
1966
2135
  this.editorContainer.addChild(component);
1967
2136
  this.ui.setFocus(component);
@@ -2048,7 +2217,7 @@ export class InteractiveMode {
2048
2217
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2049
2218
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2050
2219
  this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2051
- this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2220
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true }));
2052
2221
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2053
2222
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2054
2223
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2336,17 +2505,17 @@ export class InteractiveMode {
2336
2505
  this.ui.requestRender();
2337
2506
  }
2338
2507
  else if (event.message.role === "assistant") {
2339
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2508
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2340
2509
  this.streamingMessage = event.message;
2341
2510
  this.chatContainer.addChild(this.streamingComponent);
2342
- this.streamingComponent.updateContent(this.streamingMessage);
2511
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2343
2512
  this.ui.requestRender();
2344
2513
  }
2345
2514
  break;
2346
2515
  case "message_update":
2347
2516
  if (this.streamingComponent && event.message.role === "assistant") {
2348
2517
  this.streamingMessage = event.message;
2349
- this.streamingComponent.updateContent(this.streamingMessage);
2518
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2350
2519
  for (const content of this.streamingMessage.content) {
2351
2520
  if (content.type === "toolCall") {
2352
2521
  if (!this.pendingTools.has(content.id)) {
@@ -2383,7 +2552,7 @@ export class InteractiveMode {
2383
2552
  : "Operation aborted";
2384
2553
  this.streamingMessage.errorMessage = errorMessage;
2385
2554
  }
2386
- this.streamingComponent.updateContent(this.streamingMessage);
2555
+ this.streamingComponent.updateContent(this.streamingMessage, false);
2387
2556
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2388
2557
  if (!errorMessage) {
2389
2558
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2657,12 +2826,12 @@ export class InteractiveMode {
2657
2826
  // Render user message separately if present
2658
2827
  if (skillBlock.userMessage) {
2659
2828
  this.chatContainer.addChild(new Spacer(1));
2660
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2829
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2661
2830
  this.chatContainer.addChild(userComponent);
2662
2831
  }
2663
2832
  }
2664
2833
  else {
2665
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2834
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2666
2835
  this.chatContainer.addChild(userComponent);
2667
2836
  }
2668
2837
  if (options?.populateHistory) {
@@ -2672,7 +2841,7 @@ export class InteractiveMode {
2672
2841
  break;
2673
2842
  }
2674
2843
  case "assistant": {
2675
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2844
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad, this.getMarkdownTransformers());
2676
2845
  this.chatContainer.addChild(assistantComponent);
2677
2846
  break;
2678
2847
  }
@@ -3106,6 +3275,8 @@ export class InteractiveMode {
3106
3275
  this.setToolsExpanded(!this.toolOutputExpanded);
3107
3276
  }
3108
3277
  setToolsExpanded(expanded) {
3278
+ if (expanded === this.toolOutputExpanded)
3279
+ return;
3109
3280
  this.toolOutputExpanded = expanded;
3110
3281
  const activeHeader = this.customHeader ?? this.builtInHeader;
3111
3282
  if (isExpandable(activeHeader)) {
@@ -3118,7 +3289,7 @@ export class InteractiveMode {
3118
3289
  }
3119
3290
  }
3120
3291
  }
3121
- this.ui.requestRender();
3292
+ this.showStatus(`Tool output: ${expanded ? "expanded" : "collapsed"}`);
3122
3293
  }
3123
3294
  toggleThinkingBlockVisibility() {
3124
3295
  this.hideThinkingBlock = !this.hideThinkingBlock;
@@ -3161,7 +3332,7 @@ export class InteractiveMode {
3161
3332
  }
3162
3333
  showError(errorMessage) {
3163
3334
  this.chatContainer.addChild(new Spacer(1));
3164
- this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3335
+ this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), this.outputPad, 0));
3165
3336
  this.ui.requestRender();
3166
3337
  }
3167
3338
  showWarning(warningMessage) {
@@ -3371,25 +3542,43 @@ export class InteractiveMode {
3371
3542
  // =========================================================================
3372
3543
  // Selectors
3373
3544
  // =========================================================================
3545
+ disposeActiveSelector() {
3546
+ const dispose = this.activeSelectorDispose;
3547
+ this.activeSelectorToken = undefined;
3548
+ this.activeSelectorDispose = undefined;
3549
+ dispose?.();
3550
+ }
3374
3551
  /**
3375
3552
  * Shows a selector component in place of the editor.
3376
3553
  * @param create Factory that receives a `done` callback and returns the component and focus target
3377
3554
  */
3378
3555
  showSelector(create) {
3556
+ const token = {};
3557
+ let dispose;
3379
3558
  const done = () => {
3559
+ dispose?.();
3560
+ if (this.activeSelectorToken !== token)
3561
+ return;
3562
+ this.activeSelectorToken = undefined;
3563
+ this.activeSelectorDispose = undefined;
3380
3564
  this.editorContainer.clear();
3381
3565
  this.editorContainer.addChild(this.editor);
3382
3566
  this.ui.setFocus(this.editor);
3383
3567
  };
3384
- const { component, focus } = create(done);
3568
+ const created = create(done);
3569
+ dispose = created.dispose;
3570
+ this.disposeActiveSelector();
3571
+ this.activeSelectorToken = token;
3572
+ this.activeSelectorDispose = dispose;
3385
3573
  this.editorContainer.clear();
3386
- this.editorContainer.addChild(component);
3387
- this.ui.setFocus(focus);
3574
+ this.editorContainer.addChild(created.component);
3575
+ this.ui.setFocus(created.focus);
3388
3576
  this.ui.requestRender();
3389
3577
  }
3390
3578
  showSettingsSelector() {
3391
3579
  this.showSelector((done) => {
3392
- const selector = new SettingsSelectorComponent({
3580
+ let selector;
3581
+ selector = new SettingsSelectorComponent({
3393
3582
  autoCompact: this.session.autoCompactionEnabled,
3394
3583
  showImages: this.settingsManager.getShowImages(),
3395
3584
  imageWidthCells: this.settingsManager.getImageWidthCells(),
@@ -3406,6 +3595,7 @@ export class InteractiveMode {
3406
3595
  terminalTheme: this.themeController.getTerminalTheme(),
3407
3596
  availableThemes: getAvailableThemes(),
3408
3597
  hideThinkingBlock: this.hideThinkingBlock,
3598
+ mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
3409
3599
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3410
3600
  enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
3411
3601
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
@@ -3419,6 +3609,8 @@ export class InteractiveMode {
3419
3609
  quietStartup: this.settingsManager.getQuietStartup(),
3420
3610
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3421
3611
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3612
+ tuiMode: this.ui.mode,
3613
+ fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3422
3614
  warnings: this.settingsManager.getWarnings(),
3423
3615
  }, {
3424
3616
  onAutoCompactChange: (enabled) => {
@@ -3487,6 +3679,11 @@ export class InteractiveMode {
3487
3679
  this.chatContainer.clear();
3488
3680
  this.rebuildChatFromMessages();
3489
3681
  },
3682
+ onMermaidRenderingModeChange: (mode) => {
3683
+ this.settingsManager.setMermaidRenderingMode(mode);
3684
+ this.chatContainer.invalidate();
3685
+ this.ui.requestRender();
3686
+ },
3490
3687
  onShowCacheMissNoticesChange: (shown) => {
3491
3688
  this.settingsManager.setShowCacheMissNotices(shown);
3492
3689
  this.rebuildChatFromMessages();
@@ -3556,6 +3753,21 @@ export class InteractiveMode {
3556
3753
  onShowTerminalProgressChange: (enabled) => {
3557
3754
  this.settingsManager.setShowTerminalProgress(enabled);
3558
3755
  },
3756
+ onTuiModeChange: (mode) => {
3757
+ if (!this.switchTuiMode(mode)) {
3758
+ selector?.getSettingsList().updateValue("tui-mode", this.ui.mode);
3759
+ this.showStatus("Close active overlays before changing TUI mode");
3760
+ return;
3761
+ }
3762
+ this.settingsManager.setTuiMode(mode);
3763
+ if (!this.activeStatusIndicator)
3764
+ this.statusContainer.clear();
3765
+ this.showStatus(`TUI mode: ${mode}`);
3766
+ },
3767
+ onFullscreenScrollbarChange: (mode) => {
3768
+ this.settingsManager.setFullscreenScrollbar(mode);
3769
+ this.applyFullscreenScrollbarSetting();
3770
+ },
3559
3771
  onWarningsChange: (warnings) => {
3560
3772
  this.settingsManager.setWarnings(warnings);
3561
3773
  },
@@ -3590,20 +3802,37 @@ export class InteractiveMode {
3590
3802
  this.showModelSelector(searchTerm);
3591
3803
  }
3592
3804
  async findExactModelMatch(searchTerm) {
3593
- const models = await this.getModelCandidates();
3594
- return findExactModelReferenceMatch(searchTerm, models);
3595
- }
3596
- async getModelCandidates() {
3597
- if (this.session.scopedModels.length > 0) {
3598
- return this.session.scopedModels.map((scoped) => scoped.model);
3599
- }
3805
+ const cachedModels = this.session.scopedModels.length > 0
3806
+ ? this.session.scopedModels.map((scoped) => scoped.model)
3807
+ : [...this.session.modelRuntime.getAvailableSnapshot()];
3808
+ const cachedMatch = findExactModelReferenceMatch(searchTerm, cachedModels);
3809
+ if (cachedMatch || this.session.scopedModels.length > 0)
3810
+ return cachedMatch;
3811
+ this.showStatus("Refreshing model catalogs…");
3812
+ const controller = new AbortController();
3813
+ let timedOut = false;
3814
+ const timeout = setTimeout(() => {
3815
+ timedOut = true;
3816
+ controller.abort();
3817
+ }, 15_000);
3600
3818
  try {
3601
- await this.session.modelRuntime.refresh();
3602
- return [...(await this.session.modelRuntime.getAvailable())];
3819
+ const result = await this.session.modelRuntime.refresh({ signal: controller.signal });
3820
+ if (result.aborted && timedOut) {
3821
+ this.showWarning("Model refresh timed out; searching cached models.");
3822
+ }
3823
+ else if (result.errors.size > 0) {
3824
+ this.showWarning(`Could not refresh ${[...result.errors.keys()].join(", ")}; searching cached models.`);
3825
+ }
3603
3826
  }
3604
- catch {
3605
- return [];
3827
+ catch (error) {
3828
+ this.showWarning(timedOut
3829
+ ? "Model refresh timed out; searching cached models."
3830
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`);
3831
+ }
3832
+ finally {
3833
+ clearTimeout(timeout);
3606
3834
  }
3835
+ return findExactModelReferenceMatch(searchTerm, [...this.session.modelRuntime.getAvailableSnapshot()]);
3607
3836
  }
3608
3837
  /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3609
3838
  updateAvailableProviderCount() {
@@ -3705,73 +3934,67 @@ export class InteractiveMode {
3705
3934
  done();
3706
3935
  this.ui.requestRender();
3707
3936
  }, initialSearchInput);
3708
- return { component: selector, focus: selector };
3937
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
3709
3938
  });
3710
3939
  }
3711
- async showModelsSelector() {
3712
- // Get all available models
3713
- await this.session.modelRuntime.refresh();
3714
- const allModels = [...(await this.session.modelRuntime.getAvailable())];
3715
- const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
3940
+ showModelsSelector() {
3941
+ let availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
3942
+ let availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
3716
3943
  const configuredPatterns = this.settingsManager.getEnabledModels();
3717
3944
  const sessionScopedModels = this.session.scopedModels;
3718
- if (allModels.length === 0 && !configuredPatterns?.length && sessionScopedModels.length === 0) {
3719
- this.showStatus("No models available");
3720
- return;
3721
- }
3722
- const configuredScope = configuredPatterns?.length
3723
- ? await resolveModelScopeWithDiagnostics(configuredPatterns, this.session.modelRuntime)
3724
- : undefined;
3725
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3726
- const hasSessionScope = sessionScopedModels.length > 0;
3727
- // Build enabled model IDs from session state or settings
3728
- let currentEnabledIds = null;
3729
- if (hasSessionScope) {
3730
- // Use current session's scoped models
3731
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3732
- }
3733
- else if (configuredScope) {
3734
- currentEnabledIds = configuredScope.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3735
- }
3736
- for (const diagnostic of configuredScope?.diagnostics ?? []) {
3737
- if (diagnostic.code !== "no-match")
3738
- continue;
3739
- currentEnabledIds ??= [];
3740
- if (!currentEnabledIds.includes(diagnostic.pattern))
3741
- currentEnabledIds.push(diagnostic.pattern);
3742
- }
3743
- // Helper to update session's scoped models (session-only, no persist)
3744
- const updateSessionModels = async (enabledIds) => {
3945
+ const configuredEnabledIds = (models) => {
3946
+ if (!configuredPatterns?.length)
3947
+ return null;
3948
+ const resolved = resolveModelScopeFromModels(configuredPatterns, models);
3949
+ const ids = resolved.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3950
+ for (const diagnostic of resolved.diagnostics) {
3951
+ if (diagnostic.code === "no-match" && !ids.includes(diagnostic.pattern))
3952
+ ids.push(diagnostic.pattern);
3953
+ }
3954
+ return ids;
3955
+ };
3956
+ let currentEnabledIds = sessionScopedModels.length > 0
3957
+ ? sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`)
3958
+ : configuredEnabledIds(availableModels);
3959
+ let selectionChanged = false;
3960
+ const updateSessionModels = (enabledIds) => {
3745
3961
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3746
- const hasEnabledAvailableModel = enabledIds?.some((id) => allModelIds.has(id)) ?? false;
3747
- const allAvailableModelsEnabled = enabledIds !== null && [...allModelIds].every((id) => enabledIds.includes(id));
3962
+ const hasEnabledAvailableModel = enabledIds?.some((id) => availableModelIds.has(id)) ?? false;
3963
+ const allAvailableModelsEnabled = enabledIds !== null && [...availableModelIds].every((id) => enabledIds.includes(id));
3748
3964
  if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
3749
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3750
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3751
- model: sm.model,
3752
- thinkingLevel: sm.thinkingLevel,
3965
+ const newScopedModels = resolveModelScopeFromModels(enabledIds, availableModels).scopedModels;
3966
+ this.session.setScopedModels(newScopedModels.map((scoped) => ({
3967
+ model: scoped.model,
3968
+ thinkingLevel: scoped.thinkingLevel,
3753
3969
  })));
3754
3970
  }
3755
3971
  else {
3756
- // All enabled or none enabled = no filter
3757
3972
  this.session.setScopedModels([]);
3758
3973
  }
3759
- await this.updateAvailableProviderCount();
3974
+ this.updateAvailableProviderCount();
3760
3975
  this.ui.requestRender();
3761
3976
  };
3762
3977
  this.showSelector((done) => {
3978
+ let disposed = false;
3979
+ let timedOut = false;
3980
+ const controller = new AbortController();
3981
+ const timeout = setTimeout(() => {
3982
+ timedOut = true;
3983
+ controller.abort();
3984
+ }, 15_000);
3763
3985
  const selector = new ScopedModelsSelectorComponent({
3764
- allModels,
3986
+ allModels: availableModels,
3765
3987
  enabledModelIds: currentEnabledIds,
3988
+ refreshStatus: "Refreshing model catalogs…",
3766
3989
  }, {
3767
- onChange: async (enabledIds) => {
3768
- await updateSessionModels(enabledIds);
3990
+ onChange: (enabledIds) => {
3991
+ selectionChanged = true;
3992
+ updateSessionModels(enabledIds);
3769
3993
  },
3770
3994
  onPersist: (enabledIds) => {
3771
- // Persist to settings
3772
3995
  const allEnabled = enabledIds !== null &&
3773
- enabledIds.length === allModels.length &&
3774
- enabledIds.every((id) => allModelIds.has(id));
3996
+ enabledIds.length === availableModels.length &&
3997
+ enabledIds.every((id) => availableModelIds.has(id));
3775
3998
  const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3776
3999
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3777
4000
  this.showStatus("Model selection saved to settings");
@@ -3781,7 +4004,51 @@ export class InteractiveMode {
3781
4004
  this.ui.requestRender();
3782
4005
  },
3783
4006
  });
3784
- return { component: selector, focus: selector };
4007
+ void this.session.modelRuntime
4008
+ .refresh({ signal: controller.signal })
4009
+ .then((result) => {
4010
+ if (disposed)
4011
+ return;
4012
+ availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
4013
+ availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
4014
+ if (!selectionChanged && sessionScopedModels.length === 0) {
4015
+ currentEnabledIds = configuredEnabledIds(availableModels);
4016
+ selector.updateModels(availableModels, currentEnabledIds);
4017
+ }
4018
+ else {
4019
+ selector.updateModels(availableModels);
4020
+ }
4021
+ if (currentEnabledIds !== null)
4022
+ updateSessionModels(currentEnabledIds);
4023
+ if (result.aborted && timedOut) {
4024
+ selector.setRefreshStatus("Model refresh timed out; showing cached models.", "warning");
4025
+ }
4026
+ else if (result.errors.size > 0) {
4027
+ selector.setRefreshStatus(`Could not refresh ${[...result.errors.keys()].join(", ")}; showing cached models.`, "warning");
4028
+ }
4029
+ else {
4030
+ selector.setRefreshStatus("Model catalogs refreshed.", "success");
4031
+ }
4032
+ this.ui.requestRender();
4033
+ })
4034
+ .catch((error) => {
4035
+ if (disposed)
4036
+ return;
4037
+ selector.setRefreshStatus(timedOut
4038
+ ? "Model refresh timed out; showing cached models."
4039
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
4040
+ this.ui.requestRender();
4041
+ })
4042
+ .finally(() => clearTimeout(timeout));
4043
+ return {
4044
+ component: selector,
4045
+ focus: selector,
4046
+ dispose: () => {
4047
+ disposed = true;
4048
+ clearTimeout(timeout);
4049
+ controller.abort();
4050
+ },
4051
+ };
3785
4052
  });
3786
4053
  }
3787
4054
  showUserMessageSelector() {
@@ -3843,7 +4110,7 @@ export class InteractiveMode {
3843
4110
  this.showSelector((done) => {
3844
4111
  const selector = new TreeSelectorComponent(tree, realLeafId, this.ui.terminal.rows, async (entryId) => {
3845
4112
  // Selecting the current leaf is a no-op (already there)
3846
- if (entryId === realLeafId) {
4113
+ if (entryId === this.sessionManager.getLeafId()) {
3847
4114
  done();
3848
4115
  this.showStatus("Already at this point");
3849
4116
  return;
@@ -3878,6 +4145,11 @@ export class InteractiveMode {
3878
4145
  break;
3879
4146
  }
3880
4147
  }
4148
+ // The user committed to navigating: stop the active response first.
4149
+ if (this.session.isStreaming) {
4150
+ this.restoreQueuedMessagesToEditor();
4151
+ await this.session.abort();
4152
+ }
3881
4153
  // Set up escape handler and status indicator if summarizing
3882
4154
  let showingSummaryIndicator = false;
3883
4155
  const originalOnEscape = this.defaultEditor.onEscape;
@@ -4038,7 +4310,7 @@ export class InteractiveMode {
4038
4310
  return options.sort((a, b) => a.name.localeCompare(b.name));
4039
4311
  }
4040
4312
  async getLogoutProviderOptions() {
4041
- return (await this.session.modelRuntime.listCredentials())
4313
+ return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4042
4314
  .map(({ providerId, type }) => ({
4043
4315
  id: providerId,
4044
4316
  name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
@@ -4056,7 +4328,6 @@ export class InteractiveMode {
4056
4328
  provider.name.toLowerCase() === normalizedProviderRef);
4057
4329
  }
4058
4330
  async handleLoginCommand(providerRef) {
4059
- await this.session.modelRuntime.getAvailable();
4060
4331
  if (!providerRef) {
4061
4332
  this.showLoginAuthTypeSelector();
4062
4333
  return;
@@ -4170,7 +4441,14 @@ export class InteractiveMode {
4170
4441
  this.showLoginAuthTypeSelector();
4171
4442
  return;
4172
4443
  }
4173
- const providerOptions = await this.getLogoutProviderOptions();
4444
+ let providerOptions;
4445
+ try {
4446
+ providerOptions = await this.getLogoutProviderOptions();
4447
+ }
4448
+ catch (error) {
4449
+ this.showError(`Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`);
4450
+ return;
4451
+ }
4174
4452
  if (providerOptions.length === 0) {
4175
4453
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4176
4454
  return;
@@ -4183,7 +4461,9 @@ export class InteractiveMode {
4183
4461
  return;
4184
4462
  }
4185
4463
  try {
4186
- await this.session.modelRuntime.logout(providerOption.id);
4464
+ await this.session.modelRuntime.logout(providerOption.id, {
4465
+ signal: AbortSignal.timeout(15_000),
4466
+ });
4187
4467
  await this.updateAvailableProviderCount();
4188
4468
  const message = providerOption.authType === "oauth"
4189
4469
  ? `Logged out of ${providerOption.name}`
@@ -4191,7 +4471,10 @@ export class InteractiveMode {
4191
4471
  this.showStatus(message);
4192
4472
  }
4193
4473
  catch (error) {
4194
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
4474
+ const message = error instanceof Error ? error.message : String(error);
4475
+ this.showError(error instanceof CredentialSynchronizationError
4476
+ ? `Credentials removed for ${providerOption.name}, but local model state could not be synchronized: ${message}`
4477
+ : `Logout failed: ${message}`);
4195
4478
  }
4196
4479
  }, () => {
4197
4480
  done();
@@ -4201,12 +4484,11 @@ export class InteractiveMode {
4201
4484
  });
4202
4485
  }
4203
4486
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4204
- await this.session.modelRuntime.getAvailable();
4205
4487
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4206
4488
  let selectedModel;
4207
4489
  let selectionError;
4208
4490
  if (isUnknownModel(previousModel)) {
4209
- const availableModels = await this.session.modelRuntime.getAvailable();
4491
+ const availableModels = this.session.modelRuntime.getAvailableSnapshot();
4210
4492
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4211
4493
  if (!hasDefaultModelProvider(providerId)) {
4212
4494
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4249,6 +4531,25 @@ export class InteractiveMode {
4249
4531
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
4250
4532
  }
4251
4533
  }
4534
+ const controller = new AbortController();
4535
+ const timeout = setTimeout(() => controller.abort(), 15_000);
4536
+ void this.session.modelRuntime
4537
+ .refresh({ providers: [providerId], signal: controller.signal })
4538
+ .then((result) => {
4539
+ if (result.aborted) {
4540
+ this.showWarning(`${actionLabel}, but its model catalog refresh timed out; using cached models.`);
4541
+ }
4542
+ else if (result.errors.size > 0) {
4543
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed; using cached models.`);
4544
+ }
4545
+ this.updateAvailableProviderCount();
4546
+ this.footer.invalidate();
4547
+ this.ui.requestRender();
4548
+ })
4549
+ .catch((error) => {
4550
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
4551
+ })
4552
+ .finally(() => clearTimeout(timeout));
4252
4553
  }
4253
4554
  showAmbientAuthDialog(providerOption) {
4254
4555
  const restoreEditor = () => {
@@ -4294,7 +4595,10 @@ export class InteractiveMode {
4294
4595
  catch (error) {
4295
4596
  restoreEditor();
4296
4597
  const errorMsg = error instanceof Error ? error.message : String(error);
4297
- if (errorMsg !== "Login cancelled") {
4598
+ if (error instanceof CredentialSynchronizationError) {
4599
+ this.showError(`Saved API key for ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4600
+ }
4601
+ else if (errorMsg !== "Login cancelled") {
4298
4602
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
4299
4603
  }
4300
4604
  }
@@ -4397,7 +4701,10 @@ export class InteractiveMode {
4397
4701
  catch (error) {
4398
4702
  restoreEditor();
4399
4703
  const errorMsg = error instanceof Error ? error.message : String(error);
4400
- if (errorMsg !== "Login cancelled") {
4704
+ if (error instanceof CredentialSynchronizationError) {
4705
+ this.showError(`Logged in to ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
4706
+ }
4707
+ else if (errorMsg !== "Login cancelled") {
4401
4708
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
4402
4709
  }
4403
4710
  }
@@ -4448,7 +4755,6 @@ export class InteractiveMode {
4448
4755
  try {
4449
4756
  await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
4450
4757
  restoreChatBeforeSessionStart();
4451
- configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
4452
4758
  this.keybindings.reload();
4453
4759
  const activeHeader = this.customHeader ?? this.builtInHeader;
4454
4760
  if (isExpandable(activeHeader)) {
@@ -4456,20 +4762,7 @@ export class InteractiveMode {
4456
4762
  }
4457
4763
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
4458
4764
  await this.themeController.applyFromSettings();
4459
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
4460
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
4461
- this.defaultEditor.setPaddingX(editorPaddingX);
4462
- this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
4463
- if (this.editor !== this.defaultEditor) {
4464
- this.editor.setPaddingX?.(editorPaddingX);
4465
- this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4466
- }
4467
- this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4468
- const clearOnShrink = this.settingsManager.getClearOnShrink();
4469
- this.ui.setClearOnShrink(clearOnShrink);
4470
- if (!clearOnShrink && !this.activeStatusIndicator) {
4471
- this.statusContainer.clear();
4472
- }
4765
+ this.applyRuntimeSettings();
4473
4766
  this.setupAutocompleteProvider();
4474
4767
  const runner = this.session.extensionRunner;
4475
4768
  this.setupExtensionShortcuts(runner);
@@ -4665,7 +4958,7 @@ export class InteractiveMode {
4665
4958
  }
4666
4959
  }
4667
4960
  }
4668
- async handleCopyCommand() {
4961
+ async handleCopyCommand(options = {}) {
4669
4962
  const text = this.session.getLastAssistantText();
4670
4963
  if (!text) {
4671
4964
  this.showError("No agent messages to copy yet.");
@@ -4673,7 +4966,12 @@ export class InteractiveMode {
4673
4966
  }
4674
4967
  try {
4675
4968
  await copyToClipboard(text);
4676
- this.showStatus("Copied last agent message to clipboard");
4969
+ if (options.flashConfirmation && this.ui instanceof TuiAltScreen) {
4970
+ this.ui.flash("Copied!");
4971
+ }
4972
+ else {
4973
+ this.showStatus("Copied last agent message to clipboard");
4974
+ }
4677
4975
  }
4678
4976
  catch (error) {
4679
4977
  this.showError(error instanceof Error ? error.message : String(error));
@@ -5036,6 +5334,7 @@ export class InteractiveMode {
5036
5334
  }
5037
5335
  }
5038
5336
  stop() {
5337
+ this.disposeActiveSelector();
5039
5338
  if (this.settingsManager.getShowTerminalProgress()) {
5040
5339
  this.ui.terminal.setProgress(false);
5041
5340
  }
@@ -5048,7 +5347,7 @@ export class InteractiveMode {
5048
5347
  this.unsubscribe();
5049
5348
  }
5050
5349
  if (this.isInitialized) {
5051
- this.ui.stop();
5350
+ this.stopInteractiveTui();
5052
5351
  this.isInitialized = false;
5053
5352
  }
5054
5353
  this.unregisterSignalHandlers();