@earendil-works/pi-coding-agent 0.83.0 → 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 (302) hide show
  1. package/CHANGELOG.md +198 -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 +19 -0
  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 +1 -1
  11. package/dist/cli/credential-print.d.ts.map +1 -1
  12. package/dist/cli/credential-print.js +6 -5
  13. package/dist/cli/credential-print.js.map +1 -1
  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-services.d.ts +1 -0
  70. package/dist/core/agent-session-services.d.ts.map +1 -1
  71. package/dist/core/agent-session-services.js +1 -0
  72. package/dist/core/agent-session-services.js.map +1 -1
  73. package/dist/core/agent-session.d.ts +1 -10
  74. package/dist/core/agent-session.d.ts.map +1 -1
  75. package/dist/core/agent-session.js +72 -70
  76. package/dist/core/agent-session.js.map +1 -1
  77. package/dist/core/auth-storage.d.ts +15 -9
  78. package/dist/core/auth-storage.d.ts.map +1 -1
  79. package/dist/core/auth-storage.js +159 -38
  80. package/dist/core/auth-storage.js.map +1 -1
  81. package/dist/core/extensions/index.d.ts +1 -1
  82. package/dist/core/extensions/index.d.ts.map +1 -1
  83. package/dist/core/extensions/index.js.map +1 -1
  84. package/dist/core/extensions/loader.d.ts.map +1 -1
  85. package/dist/core/extensions/loader.js +43 -20
  86. package/dist/core/extensions/loader.js.map +1 -1
  87. package/dist/core/extensions/runner.d.ts +2 -1
  88. package/dist/core/extensions/runner.d.ts.map +1 -1
  89. package/dist/core/extensions/runner.js +3 -0
  90. package/dist/core/extensions/runner.js.map +1 -1
  91. package/dist/core/extensions/types.d.ts +21 -3
  92. package/dist/core/extensions/types.d.ts.map +1 -1
  93. package/dist/core/extensions/types.js.map +1 -1
  94. package/dist/core/http-dispatcher.d.ts.map +1 -1
  95. package/dist/core/http-dispatcher.js +5 -0
  96. package/dist/core/http-dispatcher.js.map +1 -1
  97. package/dist/core/keybindings.d.ts +36 -4
  98. package/dist/core/keybindings.d.ts.map +1 -1
  99. package/dist/core/model-config.d.ts +29 -5
  100. package/dist/core/model-config.d.ts.map +1 -1
  101. package/dist/core/model-config.js +4 -0
  102. package/dist/core/model-config.js.map +1 -1
  103. package/dist/core/model-registry.d.ts +5 -3
  104. package/dist/core/model-registry.d.ts.map +1 -1
  105. package/dist/core/model-registry.js +13 -10
  106. package/dist/core/model-registry.js.map +1 -1
  107. package/dist/core/model-resolver.d.ts +4 -3
  108. package/dist/core/model-resolver.d.ts.map +1 -1
  109. package/dist/core/model-resolver.js +41 -10
  110. package/dist/core/model-resolver.js.map +1 -1
  111. package/dist/core/model-runtime.d.ts +28 -13
  112. package/dist/core/model-runtime.d.ts.map +1 -1
  113. package/dist/core/model-runtime.js +231 -73
  114. package/dist/core/model-runtime.js.map +1 -1
  115. package/dist/core/models-store.d.ts +12 -7
  116. package/dist/core/models-store.d.ts.map +1 -1
  117. package/dist/core/models-store.js +83 -14
  118. package/dist/core/models-store.js.map +1 -1
  119. package/dist/core/package-manager.d.ts +4 -1
  120. package/dist/core/package-manager.d.ts.map +1 -1
  121. package/dist/core/package-manager.js +63 -36
  122. package/dist/core/package-manager.js.map +1 -1
  123. package/dist/core/pi-manifest.d.ts +8 -0
  124. package/dist/core/pi-manifest.d.ts.map +1 -0
  125. package/dist/core/pi-manifest.js +25 -0
  126. package/dist/core/pi-manifest.js.map +1 -0
  127. package/dist/core/provider-composer.d.ts +4 -1
  128. package/dist/core/provider-composer.d.ts.map +1 -1
  129. package/dist/core/provider-composer.js +36 -15
  130. package/dist/core/provider-composer.js.map +1 -1
  131. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  132. package/dist/core/remote-catalog-provider.js +73 -67
  133. package/dist/core/remote-catalog-provider.js.map +1 -1
  134. package/dist/core/resource-loader.d.ts.map +1 -1
  135. package/dist/core/resource-loader.js +2 -2
  136. package/dist/core/resource-loader.js.map +1 -1
  137. package/dist/core/runtime-credentials.d.ts +5 -5
  138. package/dist/core/runtime-credentials.d.ts.map +1 -1
  139. package/dist/core/runtime-credentials.js +11 -8
  140. package/dist/core/runtime-credentials.js.map +1 -1
  141. package/dist/core/session-manager.d.ts.map +1 -1
  142. package/dist/core/session-manager.js +3 -1
  143. package/dist/core/session-manager.js.map +1 -1
  144. package/dist/core/settings-manager.d.ts +12 -0
  145. package/dist/core/settings-manager.d.ts.map +1 -1
  146. package/dist/core/settings-manager.js +40 -16
  147. package/dist/core/settings-manager.js.map +1 -1
  148. package/dist/core/tools/find.d.ts +3 -0
  149. package/dist/core/tools/find.d.ts.map +1 -1
  150. package/dist/core/tools/find.js +11 -18
  151. package/dist/core/tools/find.js.map +1 -1
  152. package/dist/core/tools/read.d.ts.map +1 -1
  153. package/dist/core/tools/read.js +1 -1
  154. package/dist/core/tools/read.js.map +1 -1
  155. package/dist/extensions/llama/index.d.ts.map +1 -1
  156. package/dist/extensions/llama/index.js +11 -2
  157. package/dist/extensions/llama/index.js.map +1 -1
  158. package/dist/extensions/llama/provider.d.ts.map +1 -1
  159. package/dist/extensions/llama/provider.js +21 -7
  160. package/dist/extensions/llama/provider.js.map +1 -1
  161. package/dist/index.d.ts +4 -4
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/main.d.ts.map +1 -1
  166. package/dist/main.js +16 -7
  167. package/dist/main.js.map +1 -1
  168. package/dist/modes/index.d.ts +1 -0
  169. package/dist/modes/index.d.ts.map +1 -1
  170. package/dist/modes/index.js.map +1 -1
  171. package/dist/modes/interactive/components/assistant-message.d.ts +5 -2
  172. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/assistant-message.js +13 -4
  174. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  175. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/custom-editor.js +7 -0
  177. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  178. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/footer.js +1 -1
  180. package/dist/modes/interactive/components/footer.js.map +1 -1
  181. package/dist/modes/interactive/components/markdown-transform.d.ts +3 -0
  182. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -0
  183. package/dist/modes/interactive/components/markdown-transform.js +19 -0
  184. package/dist/modes/interactive/components/markdown-transform.js.map +1 -0
  185. package/dist/modes/interactive/components/mermaid.d.ts +11 -0
  186. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/mermaid.js +75 -0
  188. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  189. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  190. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/model-selector.js +16 -4
  192. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  193. package/dist/modes/interactive/components/scoped-models-selector.d.ts +4 -0
  194. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/scoped-models-selector.js +26 -0
  196. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  197. package/dist/modes/interactive/components/settings-selector.d.ts +8 -2
  198. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/settings-selector.js +30 -0
  200. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  201. package/dist/modes/interactive/components/user-message.d.ts +3 -1
  202. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  203. package/dist/modes/interactive/components/user-message.js +9 -2
  204. package/dist/modes/interactive/components/user-message.js.map +1 -1
  205. package/dist/modes/interactive/interactive-mode.d.ts +33 -2
  206. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  207. package/dist/modes/interactive/interactive-mode.js +423 -138
  208. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  209. package/dist/modes/interactive/theme/dark.json +1 -0
  210. package/dist/modes/interactive/theme/light.json +1 -0
  211. package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
  212. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  213. package/dist/modes/interactive/theme/theme-controller.js +10 -1
  214. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  215. package/dist/modes/interactive/theme/theme-schema.json +5 -1
  216. package/dist/modes/interactive/theme/theme.d.ts +2 -2
  217. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  218. package/dist/modes/interactive/theme/theme.js +13 -3
  219. package/dist/modes/interactive/theme/theme.js.map +1 -1
  220. package/dist/modes/json-event.d.ts +28 -0
  221. package/dist/modes/json-event.d.ts.map +1 -0
  222. package/dist/modes/json-event.js +12 -0
  223. package/dist/modes/json-event.js.map +1 -0
  224. package/dist/modes/print-mode.d.ts.map +1 -1
  225. package/dist/modes/print-mode.js +12 -2
  226. package/dist/modes/print-mode.js.map +1 -1
  227. package/dist/modes/rpc/rpc-client.d.ts +5 -4
  228. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-client.js.map +1 -1
  230. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  231. package/dist/modes/rpc/rpc-mode.js +4 -3
  232. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  233. package/dist/package-manager-cli.d.ts.map +1 -1
  234. package/dist/package-manager-cli.js +11 -9
  235. package/dist/package-manager-cli.js.map +1 -1
  236. package/dist/rpc-entry.d.ts.map +1 -1
  237. package/dist/rpc-entry.js +1 -0
  238. package/dist/rpc-entry.js.map +1 -1
  239. package/dist/utils/abort.d.ts +5 -0
  240. package/dist/utils/abort.d.ts.map +1 -0
  241. package/dist/utils/abort.js +48 -0
  242. package/dist/utils/abort.js.map +1 -0
  243. package/dist/utils/clipboard.d.ts +1 -1
  244. package/dist/utils/clipboard.d.ts.map +1 -1
  245. package/dist/utils/clipboard.js +22 -2
  246. package/dist/utils/clipboard.js.map +1 -1
  247. package/dist/utils/management-http.d.ts +23 -0
  248. package/dist/utils/management-http.d.ts.map +1 -0
  249. package/dist/utils/management-http.js +50 -0
  250. package/dist/utils/management-http.js.map +1 -0
  251. package/dist/utils/paths.d.ts +3 -0
  252. package/dist/utils/paths.d.ts.map +1 -1
  253. package/dist/utils/paths.js +23 -1
  254. package/dist/utils/paths.js.map +1 -1
  255. package/dist/utils/tool-result-images.d.ts +19 -0
  256. package/dist/utils/tool-result-images.d.ts.map +1 -0
  257. package/dist/utils/tool-result-images.js +45 -0
  258. package/dist/utils/tool-result-images.js.map +1 -0
  259. package/dist/utils/tools-manager.d.ts.map +1 -1
  260. package/dist/utils/tools-manager.js +4 -6
  261. package/dist/utils/tools-manager.js.map +1 -1
  262. package/dist/utils/version-check.d.ts +4 -0
  263. package/dist/utils/version-check.d.ts.map +1 -1
  264. package/dist/utils/version-check.js +20 -2
  265. package/dist/utils/version-check.js.map +1 -1
  266. package/docs/compaction.md +2 -2
  267. package/docs/custom-provider.md +7 -5
  268. package/docs/extensions.md +27 -3
  269. package/docs/json.md +19 -14
  270. package/docs/keybindings.md +38 -10
  271. package/docs/models.md +24 -3
  272. package/docs/providers.md +1 -0
  273. package/docs/quickstart.md +2 -0
  274. package/docs/rpc.md +13 -11
  275. package/docs/sdk.md +21 -2
  276. package/docs/security.md +1 -1
  277. package/docs/settings.md +4 -1
  278. package/docs/themes.md +5 -3
  279. package/docs/usage.md +7 -0
  280. package/examples/extensions/custom-compaction.ts +1 -16
  281. package/examples/extensions/custom-provider-anthropic/index.ts +2 -1
  282. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  283. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  284. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -1
  285. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  286. package/examples/extensions/doom-overlay/wad-finder.ts +7 -3
  287. package/examples/extensions/gondolin/package-lock.json +2 -2
  288. package/examples/extensions/gondolin/package.json +1 -1
  289. package/examples/extensions/handoff.ts +2 -11
  290. package/examples/extensions/qna.ts +3 -7
  291. package/examples/extensions/sandbox/package-lock.json +2 -2
  292. package/examples/extensions/sandbox/package.json +1 -1
  293. package/examples/extensions/summarize.ts +7 -18
  294. package/examples/extensions/with-deps/package-lock.json +2 -2
  295. package/examples/extensions/with-deps/package.json +1 -1
  296. package/examples/rpc-extension-ui.ts +11 -2
  297. package/examples/sdk/02-custom-model.ts +1 -2
  298. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  299. package/examples/sdk/12-full-control.ts +1 -1
  300. package/examples/sdk/README.md +1 -1
  301. package/npm-shrinkwrap.json +64 -20
  302. 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,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) => {
@@ -1288,8 +1434,12 @@ export class InteractiveMode {
1288
1434
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1289
1435
  this.showStartupNoticesIfNeeded();
1290
1436
  }
1437
+ applyFullscreenScrollbarSetting() {
1438
+ this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
1439
+ }
1291
1440
  applyRuntimeSettings() {
1292
1441
  configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1442
+ this.applyFullscreenScrollbarSetting();
1293
1443
  this.footer.setSession(this.session);
1294
1444
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1295
1445
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
@@ -1353,6 +1503,9 @@ export class InteractiveMode {
1353
1503
  getRegisteredToolDefinition(toolName) {
1354
1504
  return this.session.getToolDefinition(toolName);
1355
1505
  }
1506
+ getMarkdownTransformers() {
1507
+ return [this.mermaidMarkdownTransformer, ...this.session.extensionRunner.getMarkdownTransformers()];
1508
+ }
1356
1509
  /**
1357
1510
  * Set up keyboard shortcuts registered by extensions.
1358
1511
  */
@@ -1432,7 +1585,7 @@ export class InteractiveMode {
1432
1585
  this.activeStatusIndicator?.dispose();
1433
1586
  this.activeStatusIndicator = undefined;
1434
1587
  this.statusContainer.clear();
1435
- if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1588
+ if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
1436
1589
  this.statusContainer.addChild(this.idleStatus);
1437
1590
  }
1438
1591
  }
@@ -1580,22 +1733,16 @@ export class InteractiveMode {
1580
1733
  if (this.customFooter?.dispose) {
1581
1734
  this.customFooter.dispose();
1582
1735
  }
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
- }
1736
+ this.footerContainer.clear();
1590
1737
  if (factory) {
1591
1738
  // Create and add custom footer, passing the data provider
1592
1739
  this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1593
- this.ui.addChild(this.customFooter);
1740
+ this.footerContainer.addChild(this.customFooter);
1594
1741
  }
1595
1742
  else {
1596
1743
  // Restore built-in footer
1597
1744
  this.customFooter = undefined;
1598
- this.ui.addChild(this.footer);
1745
+ this.footerContainer.addChild(this.footer);
1599
1746
  }
1600
1747
  this.ui.requestRender();
1601
1748
  }
@@ -1641,18 +1788,23 @@ export class InteractiveMode {
1641
1788
  this.ui.requestRender();
1642
1789
  }
1643
1790
  addExtensionTerminalInputListener(handler) {
1644
- const unsubscribe = this.ui.addInputListener(handler);
1645
- this.extensionTerminalInputUnsubscribers.add(unsubscribe);
1791
+ const subscription = { handler, unsubscribe: this.ui.addInputListener(handler) };
1792
+ this.extensionTerminalInputSubscriptions.add(subscription);
1646
1793
  return () => {
1647
- unsubscribe();
1648
- this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
1794
+ subscription.unsubscribe();
1795
+ this.extensionTerminalInputSubscriptions.delete(subscription);
1649
1796
  };
1650
1797
  }
1651
- clearExtensionTerminalInputListeners() {
1652
- for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
1653
- unsubscribe();
1798
+ rebindExtensionTerminalInputListeners() {
1799
+ for (const subscription of this.extensionTerminalInputSubscriptions) {
1800
+ subscription.unsubscribe();
1801
+ subscription.unsubscribe = this.ui.addInputListener(subscription.handler);
1654
1802
  }
1655
- this.extensionTerminalInputUnsubscribers.clear();
1803
+ }
1804
+ clearExtensionTerminalInputListeners() {
1805
+ for (const subscription of this.extensionTerminalInputSubscriptions)
1806
+ subscription.unsubscribe();
1807
+ this.extensionTerminalInputSubscriptions.clear();
1656
1808
  }
1657
1809
  /**
1658
1810
  * Create the ExtensionUIContext for extensions.
@@ -1747,6 +1899,7 @@ export class InteractiveMode {
1747
1899
  this.hideExtensionSelector();
1748
1900
  resolve(undefined);
1749
1901
  }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1902
+ this.disposeActiveSelector();
1750
1903
  this.editorContainer.clear();
1751
1904
  this.editorContainer.addChild(this.extensionSelector);
1752
1905
  this.ui.setFocus(this.extensionSelector);
@@ -1798,6 +1951,7 @@ export class InteractiveMode {
1798
1951
  this.hideExtensionInput();
1799
1952
  resolve(undefined);
1800
1953
  }, { tui: this.ui, timeout: opts?.timeout });
1954
+ this.disposeActiveSelector();
1801
1955
  this.editorContainer.clear();
1802
1956
  this.editorContainer.addChild(this.extensionInput);
1803
1957
  this.ui.setFocus(this.extensionInput);
@@ -1827,6 +1981,7 @@ export class InteractiveMode {
1827
1981
  this.hideExtensionEditor();
1828
1982
  resolve(undefined);
1829
1983
  }, undefined, this.settingsManager.getExternalEditorCommand());
1984
+ this.disposeActiveSelector();
1830
1985
  this.editorContainer.clear();
1831
1986
  this.editorContainer.addChild(this.extensionEditor);
1832
1987
  this.ui.setFocus(this.extensionEditor);
@@ -1851,6 +2006,7 @@ export class InteractiveMode {
1851
2006
  this.editorComponentFactory = factory;
1852
2007
  // Save text from current editor before switching
1853
2008
  const currentText = this.editor.getText();
2009
+ this.disposeActiveSelector();
1854
2010
  this.editorContainer.clear();
1855
2011
  if (factory) {
1856
2012
  // Create the custom editor with tui, theme, and keybindings
@@ -1867,6 +2023,9 @@ export class InteractiveMode {
1867
2023
  if (newEditor.setPaddingX !== undefined) {
1868
2024
  newEditor.setPaddingX(this.defaultEditor.getPaddingX());
1869
2025
  }
2026
+ if (newEditor.setAutocompleteMaxVisible !== undefined) {
2027
+ newEditor.setAutocompleteMaxVisible(this.defaultEditor.getAutocompleteMaxVisible());
2028
+ }
1870
2029
  // Set autocomplete if supported
1871
2030
  if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
1872
2031
  newEditor.setAutocompleteProvider(this.autocompleteProvider);
@@ -1971,6 +2130,7 @@ export class InteractiveMode {
1971
2130
  options?.onHandle?.(handle);
1972
2131
  }
1973
2132
  else {
2133
+ this.disposeActiveSelector();
1974
2134
  this.editorContainer.clear();
1975
2135
  this.editorContainer.addChild(component);
1976
2136
  this.ui.setFocus(component);
@@ -2057,7 +2217,7 @@ export class InteractiveMode {
2057
2217
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2058
2218
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2059
2219
  this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2060
- this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2220
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true }));
2061
2221
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2062
2222
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2063
2223
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2345,17 +2505,17 @@ export class InteractiveMode {
2345
2505
  this.ui.requestRender();
2346
2506
  }
2347
2507
  else if (event.message.role === "assistant") {
2348
- 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());
2349
2509
  this.streamingMessage = event.message;
2350
2510
  this.chatContainer.addChild(this.streamingComponent);
2351
- this.streamingComponent.updateContent(this.streamingMessage);
2511
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2352
2512
  this.ui.requestRender();
2353
2513
  }
2354
2514
  break;
2355
2515
  case "message_update":
2356
2516
  if (this.streamingComponent && event.message.role === "assistant") {
2357
2517
  this.streamingMessage = event.message;
2358
- this.streamingComponent.updateContent(this.streamingMessage);
2518
+ this.streamingComponent.updateContent(this.streamingMessage, true);
2359
2519
  for (const content of this.streamingMessage.content) {
2360
2520
  if (content.type === "toolCall") {
2361
2521
  if (!this.pendingTools.has(content.id)) {
@@ -2392,7 +2552,7 @@ export class InteractiveMode {
2392
2552
  : "Operation aborted";
2393
2553
  this.streamingMessage.errorMessage = errorMessage;
2394
2554
  }
2395
- this.streamingComponent.updateContent(this.streamingMessage);
2555
+ this.streamingComponent.updateContent(this.streamingMessage, false);
2396
2556
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2397
2557
  if (!errorMessage) {
2398
2558
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2666,12 +2826,12 @@ export class InteractiveMode {
2666
2826
  // Render user message separately if present
2667
2827
  if (skillBlock.userMessage) {
2668
2828
  this.chatContainer.addChild(new Spacer(1));
2669
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2829
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2670
2830
  this.chatContainer.addChild(userComponent);
2671
2831
  }
2672
2832
  }
2673
2833
  else {
2674
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2834
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
2675
2835
  this.chatContainer.addChild(userComponent);
2676
2836
  }
2677
2837
  if (options?.populateHistory) {
@@ -2681,7 +2841,7 @@ export class InteractiveMode {
2681
2841
  break;
2682
2842
  }
2683
2843
  case "assistant": {
2684
- 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());
2685
2845
  this.chatContainer.addChild(assistantComponent);
2686
2846
  break;
2687
2847
  }
@@ -3115,6 +3275,8 @@ export class InteractiveMode {
3115
3275
  this.setToolsExpanded(!this.toolOutputExpanded);
3116
3276
  }
3117
3277
  setToolsExpanded(expanded) {
3278
+ if (expanded === this.toolOutputExpanded)
3279
+ return;
3118
3280
  this.toolOutputExpanded = expanded;
3119
3281
  const activeHeader = this.customHeader ?? this.builtInHeader;
3120
3282
  if (isExpandable(activeHeader)) {
@@ -3170,7 +3332,7 @@ export class InteractiveMode {
3170
3332
  }
3171
3333
  showError(errorMessage) {
3172
3334
  this.chatContainer.addChild(new Spacer(1));
3173
- 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));
3174
3336
  this.ui.requestRender();
3175
3337
  }
3176
3338
  showWarning(warningMessage) {
@@ -3380,25 +3542,43 @@ export class InteractiveMode {
3380
3542
  // =========================================================================
3381
3543
  // Selectors
3382
3544
  // =========================================================================
3545
+ disposeActiveSelector() {
3546
+ const dispose = this.activeSelectorDispose;
3547
+ this.activeSelectorToken = undefined;
3548
+ this.activeSelectorDispose = undefined;
3549
+ dispose?.();
3550
+ }
3383
3551
  /**
3384
3552
  * Shows a selector component in place of the editor.
3385
3553
  * @param create Factory that receives a `done` callback and returns the component and focus target
3386
3554
  */
3387
3555
  showSelector(create) {
3556
+ const token = {};
3557
+ let dispose;
3388
3558
  const done = () => {
3559
+ dispose?.();
3560
+ if (this.activeSelectorToken !== token)
3561
+ return;
3562
+ this.activeSelectorToken = undefined;
3563
+ this.activeSelectorDispose = undefined;
3389
3564
  this.editorContainer.clear();
3390
3565
  this.editorContainer.addChild(this.editor);
3391
3566
  this.ui.setFocus(this.editor);
3392
3567
  };
3393
- 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;
3394
3573
  this.editorContainer.clear();
3395
- this.editorContainer.addChild(component);
3396
- this.ui.setFocus(focus);
3574
+ this.editorContainer.addChild(created.component);
3575
+ this.ui.setFocus(created.focus);
3397
3576
  this.ui.requestRender();
3398
3577
  }
3399
3578
  showSettingsSelector() {
3400
3579
  this.showSelector((done) => {
3401
- const selector = new SettingsSelectorComponent({
3580
+ let selector;
3581
+ selector = new SettingsSelectorComponent({
3402
3582
  autoCompact: this.session.autoCompactionEnabled,
3403
3583
  showImages: this.settingsManager.getShowImages(),
3404
3584
  imageWidthCells: this.settingsManager.getImageWidthCells(),
@@ -3415,6 +3595,7 @@ export class InteractiveMode {
3415
3595
  terminalTheme: this.themeController.getTerminalTheme(),
3416
3596
  availableThemes: getAvailableThemes(),
3417
3597
  hideThinkingBlock: this.hideThinkingBlock,
3598
+ mermaidRenderingMode: this.settingsManager.getMermaidRenderingMode(),
3418
3599
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
3419
3600
  enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
3420
3601
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
@@ -3428,6 +3609,8 @@ export class InteractiveMode {
3428
3609
  quietStartup: this.settingsManager.getQuietStartup(),
3429
3610
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3430
3611
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3612
+ tuiMode: this.ui.mode,
3613
+ fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3431
3614
  warnings: this.settingsManager.getWarnings(),
3432
3615
  }, {
3433
3616
  onAutoCompactChange: (enabled) => {
@@ -3496,6 +3679,11 @@ export class InteractiveMode {
3496
3679
  this.chatContainer.clear();
3497
3680
  this.rebuildChatFromMessages();
3498
3681
  },
3682
+ onMermaidRenderingModeChange: (mode) => {
3683
+ this.settingsManager.setMermaidRenderingMode(mode);
3684
+ this.chatContainer.invalidate();
3685
+ this.ui.requestRender();
3686
+ },
3499
3687
  onShowCacheMissNoticesChange: (shown) => {
3500
3688
  this.settingsManager.setShowCacheMissNotices(shown);
3501
3689
  this.rebuildChatFromMessages();
@@ -3565,6 +3753,21 @@ export class InteractiveMode {
3565
3753
  onShowTerminalProgressChange: (enabled) => {
3566
3754
  this.settingsManager.setShowTerminalProgress(enabled);
3567
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
+ },
3568
3771
  onWarningsChange: (warnings) => {
3569
3772
  this.settingsManager.setWarnings(warnings);
3570
3773
  },
@@ -3599,20 +3802,37 @@ export class InteractiveMode {
3599
3802
  this.showModelSelector(searchTerm);
3600
3803
  }
3601
3804
  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
- }
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);
3609
3818
  try {
3610
- await this.session.modelRuntime.refresh();
3611
- 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
+ }
3612
3826
  }
3613
- catch {
3614
- 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)}`);
3615
3831
  }
3832
+ finally {
3833
+ clearTimeout(timeout);
3834
+ }
3835
+ return findExactModelReferenceMatch(searchTerm, [...this.session.modelRuntime.getAvailableSnapshot()]);
3616
3836
  }
3617
3837
  /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3618
3838
  updateAvailableProviderCount() {
@@ -3714,73 +3934,67 @@ export class InteractiveMode {
3714
3934
  done();
3715
3935
  this.ui.requestRender();
3716
3936
  }, initialSearchInput);
3717
- return { component: selector, focus: selector };
3937
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
3718
3938
  });
3719
3939
  }
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}`));
3940
+ showModelsSelector() {
3941
+ let availableModels = [...this.session.modelRuntime.getAvailableSnapshot()];
3942
+ let availableModelIds = new Set(availableModels.map((model) => `${model.provider}/${model.id}`));
3725
3943
  const configuredPatterns = this.settingsManager.getEnabledModels();
3726
3944
  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) => {
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) => {
3754
3961
  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));
3962
+ const hasEnabledAvailableModel = enabledIds?.some((id) => availableModelIds.has(id)) ?? false;
3963
+ const allAvailableModelsEnabled = enabledIds !== null && [...availableModelIds].every((id) => enabledIds.includes(id));
3757
3964
  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,
3965
+ const newScopedModels = resolveModelScopeFromModels(enabledIds, availableModels).scopedModels;
3966
+ this.session.setScopedModels(newScopedModels.map((scoped) => ({
3967
+ model: scoped.model,
3968
+ thinkingLevel: scoped.thinkingLevel,
3762
3969
  })));
3763
3970
  }
3764
3971
  else {
3765
- // All enabled or none enabled = no filter
3766
3972
  this.session.setScopedModels([]);
3767
3973
  }
3768
- await this.updateAvailableProviderCount();
3974
+ this.updateAvailableProviderCount();
3769
3975
  this.ui.requestRender();
3770
3976
  };
3771
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);
3772
3985
  const selector = new ScopedModelsSelectorComponent({
3773
- allModels,
3986
+ allModels: availableModels,
3774
3987
  enabledModelIds: currentEnabledIds,
3988
+ refreshStatus: "Refreshing model catalogs…",
3775
3989
  }, {
3776
- onChange: async (enabledIds) => {
3777
- await updateSessionModels(enabledIds);
3990
+ onChange: (enabledIds) => {
3991
+ selectionChanged = true;
3992
+ updateSessionModels(enabledIds);
3778
3993
  },
3779
3994
  onPersist: (enabledIds) => {
3780
- // Persist to settings
3781
3995
  const allEnabled = enabledIds !== null &&
3782
- enabledIds.length === allModels.length &&
3783
- enabledIds.every((id) => allModelIds.has(id));
3996
+ enabledIds.length === availableModels.length &&
3997
+ enabledIds.every((id) => availableModelIds.has(id));
3784
3998
  const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3785
3999
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3786
4000
  this.showStatus("Model selection saved to settings");
@@ -3790,7 +4004,51 @@ export class InteractiveMode {
3790
4004
  this.ui.requestRender();
3791
4005
  },
3792
4006
  });
3793
- 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
+ };
3794
4052
  });
3795
4053
  }
3796
4054
  showUserMessageSelector() {
@@ -4052,7 +4310,7 @@ export class InteractiveMode {
4052
4310
  return options.sort((a, b) => a.name.localeCompare(b.name));
4053
4311
  }
4054
4312
  async getLogoutProviderOptions() {
4055
- return (await this.session.modelRuntime.listCredentials())
4313
+ return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4056
4314
  .map(({ providerId, type }) => ({
4057
4315
  id: providerId,
4058
4316
  name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
@@ -4070,7 +4328,6 @@ export class InteractiveMode {
4070
4328
  provider.name.toLowerCase() === normalizedProviderRef);
4071
4329
  }
4072
4330
  async handleLoginCommand(providerRef) {
4073
- await this.session.modelRuntime.getAvailable();
4074
4331
  if (!providerRef) {
4075
4332
  this.showLoginAuthTypeSelector();
4076
4333
  return;
@@ -4184,7 +4441,14 @@ export class InteractiveMode {
4184
4441
  this.showLoginAuthTypeSelector();
4185
4442
  return;
4186
4443
  }
4187
- 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
+ }
4188
4452
  if (providerOptions.length === 0) {
4189
4453
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4190
4454
  return;
@@ -4197,7 +4461,9 @@ export class InteractiveMode {
4197
4461
  return;
4198
4462
  }
4199
4463
  try {
4200
- await this.session.modelRuntime.logout(providerOption.id);
4464
+ await this.session.modelRuntime.logout(providerOption.id, {
4465
+ signal: AbortSignal.timeout(15_000),
4466
+ });
4201
4467
  await this.updateAvailableProviderCount();
4202
4468
  const message = providerOption.authType === "oauth"
4203
4469
  ? `Logged out of ${providerOption.name}`
@@ -4205,7 +4471,10 @@ export class InteractiveMode {
4205
4471
  this.showStatus(message);
4206
4472
  }
4207
4473
  catch (error) {
4208
- 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}`);
4209
4478
  }
4210
4479
  }, () => {
4211
4480
  done();
@@ -4215,12 +4484,11 @@ export class InteractiveMode {
4215
4484
  });
4216
4485
  }
4217
4486
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4218
- await this.session.modelRuntime.getAvailable();
4219
4487
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4220
4488
  let selectedModel;
4221
4489
  let selectionError;
4222
4490
  if (isUnknownModel(previousModel)) {
4223
- const availableModels = await this.session.modelRuntime.getAvailable();
4491
+ const availableModels = this.session.modelRuntime.getAvailableSnapshot();
4224
4492
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4225
4493
  if (!hasDefaultModelProvider(providerId)) {
4226
4494
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4263,6 +4531,25 @@ export class InteractiveMode {
4263
4531
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
4264
4532
  }
4265
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));
4266
4553
  }
4267
4554
  showAmbientAuthDialog(providerOption) {
4268
4555
  const restoreEditor = () => {
@@ -4308,7 +4595,10 @@ export class InteractiveMode {
4308
4595
  catch (error) {
4309
4596
  restoreEditor();
4310
4597
  const errorMsg = error instanceof Error ? error.message : String(error);
4311
- 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") {
4312
4602
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
4313
4603
  }
4314
4604
  }
@@ -4411,7 +4701,10 @@ export class InteractiveMode {
4411
4701
  catch (error) {
4412
4702
  restoreEditor();
4413
4703
  const errorMsg = error instanceof Error ? error.message : String(error);
4414
- 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") {
4415
4708
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
4416
4709
  }
4417
4710
  }
@@ -4462,7 +4755,6 @@ export class InteractiveMode {
4462
4755
  try {
4463
4756
  await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
4464
4757
  restoreChatBeforeSessionStart();
4465
- configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
4466
4758
  this.keybindings.reload();
4467
4759
  const activeHeader = this.customHeader ?? this.builtInHeader;
4468
4760
  if (isExpandable(activeHeader)) {
@@ -4470,20 +4762,7 @@ export class InteractiveMode {
4470
4762
  }
4471
4763
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
4472
4764
  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
- }
4765
+ this.applyRuntimeSettings();
4487
4766
  this.setupAutocompleteProvider();
4488
4767
  const runner = this.session.extensionRunner;
4489
4768
  this.setupExtensionShortcuts(runner);
@@ -4679,7 +4958,7 @@ export class InteractiveMode {
4679
4958
  }
4680
4959
  }
4681
4960
  }
4682
- async handleCopyCommand() {
4961
+ async handleCopyCommand(options = {}) {
4683
4962
  const text = this.session.getLastAssistantText();
4684
4963
  if (!text) {
4685
4964
  this.showError("No agent messages to copy yet.");
@@ -4687,7 +4966,12 @@ export class InteractiveMode {
4687
4966
  }
4688
4967
  try {
4689
4968
  await copyToClipboard(text);
4690
- 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
+ }
4691
4975
  }
4692
4976
  catch (error) {
4693
4977
  this.showError(error instanceof Error ? error.message : String(error));
@@ -5050,6 +5334,7 @@ export class InteractiveMode {
5050
5334
  }
5051
5335
  }
5052
5336
  stop() {
5337
+ this.disposeActiveSelector();
5053
5338
  if (this.settingsManager.getShowTerminalProgress()) {
5054
5339
  this.ui.terminal.setProgress(false);
5055
5340
  }
@@ -5062,7 +5347,7 @@ export class InteractiveMode {
5062
5347
  this.unsubscribe();
5063
5348
  }
5064
5349
  if (this.isInitialized) {
5065
- this.ui.stop();
5350
+ this.stopInteractiveTui();
5066
5351
  this.isInitialized = false;
5067
5352
  }
5068
5353
  this.unregisterSignalHandlers();