@selesai/code 0.5.14 → 0.5.15

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 (235) hide show
  1. package/dist/bun/cli.d.ts.map +1 -1
  2. package/dist/bun/cli.js +2 -0
  3. package/dist/bun/cli.js.map +1 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +3 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/list-models.d.ts +2 -2
  8. package/dist/cli/list-models.d.ts.map +1 -1
  9. package/dist/cli/list-models.js +3 -3
  10. package/dist/cli/list-models.js.map +1 -1
  11. package/dist/config.d.ts +1 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +2 -2
  14. package/dist/config.js.map +1 -1
  15. package/dist/core/agent-session-auto-handoff.test.d.ts.map +1 -1
  16. package/dist/core/agent-session-auto-handoff.test.js +2 -2
  17. package/dist/core/agent-session-auto-handoff.test.js.map +1 -1
  18. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  19. package/dist/core/agent-session-runtime.js +3 -0
  20. package/dist/core/agent-session-runtime.js.map +1 -1
  21. package/dist/core/agent-session-services.d.ts +3 -6
  22. package/dist/core/agent-session-services.d.ts.map +1 -1
  23. package/dist/core/agent-session-services.js +23 -9
  24. package/dist/core/agent-session-services.js.map +1 -1
  25. package/dist/core/agent-session.d.ts +28 -10
  26. package/dist/core/agent-session.d.ts.map +1 -1
  27. package/dist/core/agent-session.js +136 -90
  28. package/dist/core/agent-session.js.map +1 -1
  29. package/dist/core/auth-storage.d.ts +20 -98
  30. package/dist/core/auth-storage.d.ts.map +1 -1
  31. package/dist/core/auth-storage.js +59 -251
  32. package/dist/core/auth-storage.js.map +1 -1
  33. package/dist/core/built-in-extensions.d.ts +3 -0
  34. package/dist/core/built-in-extensions.d.ts.map +1 -0
  35. package/dist/core/built-in-extensions.js +3 -0
  36. package/dist/core/built-in-extensions.js.map +1 -0
  37. package/dist/core/cache-stats.d.ts +2 -2
  38. package/dist/core/cache-stats.d.ts.map +1 -1
  39. package/dist/core/cache-stats.js +1 -1
  40. package/dist/core/cache-stats.js.map +1 -1
  41. package/dist/core/compaction/branch-summarization.d.ts +8 -2
  42. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  43. package/dist/core/compaction/branch-summarization.js +8 -11
  44. package/dist/core/compaction/branch-summarization.js.map +1 -1
  45. package/dist/core/compaction/compaction.d.ts +19 -3
  46. package/dist/core/compaction/compaction.d.ts.map +1 -1
  47. package/dist/core/compaction/compaction.js +63 -26
  48. package/dist/core/compaction/compaction.js.map +1 -1
  49. package/dist/core/compaction/utils.d.ts +1 -1
  50. package/dist/core/compaction/utils.d.ts.map +1 -1
  51. package/dist/core/compaction/utils.js +6 -17
  52. package/dist/core/compaction/utils.js.map +1 -1
  53. package/dist/core/extensions/loader.d.ts.map +1 -1
  54. package/dist/core/extensions/loader.js +21 -4
  55. package/dist/core/extensions/loader.js.map +1 -1
  56. package/dist/core/extensions/runner.d.ts +4 -1
  57. package/dist/core/extensions/runner.d.ts.map +1 -1
  58. package/dist/core/extensions/runner.js +39 -0
  59. package/dist/core/extensions/runner.js.map +1 -1
  60. package/dist/core/extensions/types.d.ts +24 -3
  61. package/dist/core/extensions/types.d.ts.map +1 -1
  62. package/dist/core/extensions/types.js.map +1 -1
  63. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  64. package/dist/core/extensions/wrapper.js +22 -3
  65. package/dist/core/extensions/wrapper.js.map +1 -1
  66. package/dist/core/keybindings.d.ts +8 -3
  67. package/dist/core/keybindings.d.ts.map +1 -1
  68. package/dist/core/keybindings.js +7 -3
  69. package/dist/core/keybindings.js.map +1 -1
  70. package/dist/core/llama/client.d.ts +61 -0
  71. package/dist/core/llama/client.d.ts.map +1 -0
  72. package/dist/core/llama/client.js +302 -0
  73. package/dist/core/llama/client.js.map +1 -0
  74. package/dist/core/llama/huggingface.d.ts +23 -0
  75. package/dist/core/llama/huggingface.d.ts.map +1 -0
  76. package/dist/core/llama/huggingface.js +141 -0
  77. package/dist/core/llama/huggingface.js.map +1 -0
  78. package/dist/core/llama/index.d.ts +3 -0
  79. package/dist/core/llama/index.d.ts.map +1 -0
  80. package/dist/core/llama/index.js +208 -0
  81. package/dist/core/llama/index.js.map +1 -0
  82. package/dist/core/llama/provider.d.ts +10 -0
  83. package/dist/core/llama/provider.d.ts.map +1 -0
  84. package/dist/core/llama/provider.js +102 -0
  85. package/dist/core/llama/provider.js.map +1 -0
  86. package/dist/core/llama/ui.d.ts +42 -0
  87. package/dist/core/llama/ui.d.ts.map +1 -0
  88. package/dist/core/llama/ui.js +416 -0
  89. package/dist/core/llama/ui.js.map +1 -0
  90. package/dist/core/model-config.d.ts +513 -0
  91. package/dist/core/model-config.d.ts.map +1 -0
  92. package/dist/core/model-config.js +272 -0
  93. package/dist/core/model-config.js.map +1 -0
  94. package/dist/core/model-registry.d.ts +20 -126
  95. package/dist/core/model-registry.d.ts.map +1 -1
  96. package/dist/core/model-registry.js +62 -780
  97. package/dist/core/model-registry.js.map +1 -1
  98. package/dist/core/model-resolver.d.ts +6 -6
  99. package/dist/core/model-resolver.d.ts.map +1 -1
  100. package/dist/core/model-resolver.js +21 -18
  101. package/dist/core/model-resolver.js.map +1 -1
  102. package/dist/core/model-runtime.d.ts +83 -0
  103. package/dist/core/model-runtime.d.ts.map +1 -0
  104. package/dist/core/model-runtime.js +451 -0
  105. package/dist/core/model-runtime.js.map +1 -0
  106. package/dist/core/models-store.d.ts +17 -0
  107. package/dist/core/models-store.d.ts.map +1 -0
  108. package/dist/core/models-store.js +45 -0
  109. package/dist/core/models-store.js.map +1 -0
  110. package/dist/core/package-manager.d.ts.map +1 -1
  111. package/dist/core/package-manager.js +7 -2
  112. package/dist/core/package-manager.js.map +1 -1
  113. package/dist/core/prompt-templates.d.ts +1 -0
  114. package/dist/core/prompt-templates.d.ts.map +1 -1
  115. package/dist/core/prompt-templates.js +4 -4
  116. package/dist/core/prompt-templates.js.map +1 -1
  117. package/dist/core/provider-composer.d.ts +55 -0
  118. package/dist/core/provider-composer.d.ts.map +1 -0
  119. package/dist/core/provider-composer.js +375 -0
  120. package/dist/core/provider-composer.js.map +1 -0
  121. package/dist/core/radius.d.ts +2 -0
  122. package/dist/core/radius.d.ts.map +1 -0
  123. package/dist/core/radius.js +2 -0
  124. package/dist/core/radius.js.map +1 -0
  125. package/dist/core/remote-catalog-provider.d.ts +5 -0
  126. package/dist/core/remote-catalog-provider.d.ts.map +1 -0
  127. package/dist/core/remote-catalog-provider.js +102 -0
  128. package/dist/core/remote-catalog-provider.js.map +1 -0
  129. package/dist/core/resource-loader.d.ts.map +1 -1
  130. package/dist/core/resource-loader.js +1 -0
  131. package/dist/core/resource-loader.js.map +1 -1
  132. package/dist/core/runtime-credentials.d.ts +17 -0
  133. package/dist/core/runtime-credentials.d.ts.map +1 -0
  134. package/dist/core/runtime-credentials.js +42 -0
  135. package/dist/core/runtime-credentials.js.map +1 -0
  136. package/dist/core/sdk.d.ts +4 -7
  137. package/dist/core/sdk.d.ts.map +1 -1
  138. package/dist/core/sdk.js +18 -25
  139. package/dist/core/sdk.js.map +1 -1
  140. package/dist/core/session-manager.d.ts +12 -7
  141. package/dist/core/session-manager.d.ts.map +1 -1
  142. package/dist/core/session-manager.js +103 -27
  143. package/dist/core/session-manager.js.map +1 -1
  144. package/dist/core/system-prompt.d.ts.map +1 -1
  145. package/dist/core/system-prompt.js +1 -11
  146. package/dist/core/system-prompt.js.map +1 -1
  147. package/dist/core/tools/read.d.ts.map +1 -1
  148. package/dist/core/tools/read.js +1 -1
  149. package/dist/core/tools/read.js.map +1 -1
  150. package/dist/core/usage-totals.d.ts +19 -0
  151. package/dist/core/usage-totals.d.ts.map +1 -0
  152. package/dist/core/usage-totals.js +52 -0
  153. package/dist/core/usage-totals.js.map +1 -0
  154. package/dist/extensions/index.d.ts +3 -0
  155. package/dist/extensions/index.d.ts.map +1 -0
  156. package/dist/extensions/index.js +9 -0
  157. package/dist/extensions/index.js.map +1 -0
  158. package/dist/extensions/llama/client.d.ts +61 -0
  159. package/dist/extensions/llama/client.d.ts.map +1 -0
  160. package/dist/extensions/llama/client.js +309 -0
  161. package/dist/extensions/llama/client.js.map +1 -0
  162. package/dist/extensions/llama/huggingface.d.ts +23 -0
  163. package/dist/extensions/llama/huggingface.d.ts.map +1 -0
  164. package/dist/extensions/llama/huggingface.js +146 -0
  165. package/dist/extensions/llama/huggingface.js.map +1 -0
  166. package/dist/extensions/llama/index.d.ts +3 -0
  167. package/dist/extensions/llama/index.d.ts.map +1 -0
  168. package/dist/extensions/llama/index.js +211 -0
  169. package/dist/extensions/llama/index.js.map +1 -0
  170. package/dist/extensions/llama/provider.d.ts +10 -0
  171. package/dist/extensions/llama/provider.d.ts.map +1 -0
  172. package/dist/extensions/llama/provider.js +106 -0
  173. package/dist/extensions/llama/provider.js.map +1 -0
  174. package/dist/extensions/llama/ui.d.ts +42 -0
  175. package/dist/extensions/llama/ui.d.ts.map +1 -0
  176. package/dist/extensions/llama/ui.js +420 -0
  177. package/dist/extensions/llama/ui.js.map +1 -0
  178. package/dist/extensions/tokenin-onboarding.ts +3 -3
  179. package/dist/index.d.ts +4 -3
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +3 -3
  182. package/dist/index.js.map +1 -1
  183. package/dist/main.d.ts.map +1 -1
  184. package/dist/main.js +21 -19
  185. package/dist/main.js.map +1 -1
  186. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  187. package/dist/modes/interactive/components/assistant-message.js +22 -10
  188. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  189. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  190. package/dist/modes/interactive/components/custom-editor.js +1 -1
  191. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  192. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/footer.js +24 -23
  194. package/dist/modes/interactive/components/footer.js.map +1 -1
  195. package/dist/modes/interactive/components/login-dialog.d.ts +5 -5
  196. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/login-dialog.js +17 -8
  198. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  199. package/dist/modes/interactive/components/model-selector.d.ts +11 -4
  200. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  201. package/dist/modes/interactive/components/model-selector.js +72 -41
  202. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  203. package/dist/modes/interactive/components/oauth-selector.d.ts +4 -4
  204. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  205. package/dist/modes/interactive/components/oauth-selector.js +14 -27
  206. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  207. package/dist/modes/interactive/components/tree-selector.d.ts +5 -0
  208. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  209. package/dist/modes/interactive/components/tree-selector.js +49 -12
  210. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  211. package/dist/modes/interactive/interactive-mode.d.ts +8 -5
  212. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  213. package/dist/modes/interactive/interactive-mode.js +253 -225
  214. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  215. package/dist/modes/rpc/rpc-client.d.ts +4 -0
  216. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  217. package/dist/modes/rpc/rpc-client.js +7 -0
  218. package/dist/modes/rpc/rpc-client.js.map +1 -1
  219. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  220. package/dist/modes/rpc/rpc-mode.js +6 -2
  221. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  222. package/dist/modes/rpc/rpc-types.d.ts +11 -0
  223. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  224. package/dist/modes/rpc/rpc-types.js.map +1 -1
  225. package/dist/package-manager-cli.d.ts.map +1 -1
  226. package/dist/package-manager-cli.js +67 -5
  227. package/dist/package-manager-cli.js.map +1 -1
  228. package/dist/utils/clipboard-native.d.ts +1 -0
  229. package/dist/utils/clipboard-native.d.ts.map +1 -1
  230. package/dist/utils/clipboard-native.js.map +1 -1
  231. package/dist/utils/clipboard.d.ts +2 -0
  232. package/dist/utils/clipboard.d.ts.map +1 -1
  233. package/dist/utils/clipboard.js +13 -0
  234. package/dist/utils/clipboard.js.map +1 -1
  235. package/package.json +4 -4
@@ -5,7 +5,6 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
- import { getProviders, } from "@earendil-works/pi-ai/compat";
9
8
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
10
9
  import chalk from "chalk";
11
10
  import { spawn, spawnSync } from "child_process";
@@ -19,15 +18,15 @@ import { KeybindingsManager } from "../../core/keybindings.js";
19
18
  import { createCompactionSummaryMessage } from "../../core/messages.js";
20
19
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
21
20
  import { DefaultPackageManager } from "../../core/package-manager.js";
22
- import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
23
21
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
24
22
  import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
25
23
  import { buildGitCommandPrompt, GIT_COMMAND_MODES } from "../../core/git-command.js";
26
24
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
27
25
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
28
26
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
27
+ import { getUsageCostBreakdown } from "../../core/usage-totals.js";
29
28
  import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
30
- import { copyToClipboard } from "../../utils/clipboard.js";
29
+ import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
31
30
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
32
31
  import { parseGitUrl } from "../../utils/git.js";
33
32
  import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
@@ -95,7 +94,7 @@ function isDeadTerminalError(error) {
95
94
  const code = error.code;
96
95
  return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
97
96
  }
98
- const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
97
+ const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage. Disable this warning in /settings.";
99
98
  function isAnthropicSubscriptionAuthKey(apiKey) {
100
99
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
101
100
  }
@@ -126,17 +125,6 @@ export function formatResumeCommand(sessionManager) {
126
125
  function hasDefaultModelProvider(providerId) {
127
126
  return providerId in defaultModelPerProvider;
128
127
  }
129
- const BEDROCK_PROVIDER_ID = "amazon-bedrock";
130
- const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
131
- export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
132
- if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
133
- return true;
134
- }
135
- if (builtInProviderIds.has(providerId)) {
136
- return false;
137
- }
138
- return !oauthProviderIds.has(providerId);
139
- }
140
128
  const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
141
129
  function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
142
130
  const filtered = fuzzyFilter(items, prefix, getSearchText);
@@ -364,11 +352,11 @@ export class InteractiveMode {
364
352
  }));
365
353
  const modelCommand = slashCommands.find((command) => command.name === "model");
366
354
  if (modelCommand) {
367
- modelCommand.getArgumentCompletions = (prefix) => {
355
+ modelCommand.getArgumentCompletions = async (prefix) => {
368
356
  // Get available models (scoped or from registry)
369
357
  const models = this.session.scopedModels.length > 0
370
358
  ? this.session.scopedModels.map((s) => s.model)
371
- : this.session.modelRegistry.getAvailable();
359
+ : await this.session.modelRuntime.getAvailable();
372
360
  if (models.length === 0)
373
361
  return null;
374
362
  // Create items with provider/id format
@@ -544,7 +532,7 @@ export class InteractiveMode {
544
532
  rawKeyHint("!!", "to run bash (no context)"),
545
533
  hint("app.message.followUp", "to queue follow-up"),
546
534
  hint("app.message.dequeue", "to edit all queued messages"),
547
- hint("app.clipboard.pasteImage", "to paste image"),
535
+ hint("app.clipboard.pasteImage", "to paste image (with text fallback)"),
548
536
  rawKeyHint("drop files", "to attach"),
549
537
  ].join("\n");
550
538
  const compactInstructions = [
@@ -604,6 +592,12 @@ export class InteractiveMode {
604
592
  */
605
593
  async run() {
606
594
  await this.init();
595
+ if (!process.env.PI_OFFLINE) {
596
+ void this.session.modelRuntime
597
+ .refresh()
598
+ .then(() => this.updateAvailableProviderCount())
599
+ .catch(() => { });
600
+ }
607
601
  // Start version check asynchronously
608
602
  checkForNewPiVersion(this.version, PACKAGE_NAME).then((newRelease) => {
609
603
  if (newRelease) {
@@ -611,10 +605,18 @@ export class InteractiveMode {
611
605
  }
612
606
  });
613
607
  // Start package update check asynchronously
614
- this.checkForPackageUpdates().then((updates) => {
608
+ this.checkForPackageUpdates()
609
+ .then((updates) => {
615
610
  if (updates.length > 0) {
616
611
  this.showPackageUpdateNotification(updates);
617
612
  }
613
+ })
614
+ .finally(() => {
615
+ // On Windows, npm can overwrite the shared console title while checking
616
+ // extension package versions. Restore Pi's title after the startup check.
617
+ if (process.platform === "win32" && this.isInitialized) {
618
+ this.updateTerminalTitle();
619
+ }
618
620
  });
619
621
  // Check tmux keyboard setup asynchronously
620
622
  this.checkTmuxKeyboardSetup().then((warning) => {
@@ -627,7 +629,7 @@ export class InteractiveMode {
627
629
  if (migratedProviders && migratedProviders.length > 0) {
628
630
  this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
629
631
  }
630
- const modelsJsonError = this.session.modelRegistry.getError();
632
+ const modelsJsonError = this.session.modelRuntime.getError();
631
633
  if (modelsJsonError) {
632
634
  this.showError(`models.json error: ${modelsJsonError}`);
633
635
  }
@@ -1079,7 +1081,10 @@ export class InteractiveMode {
1079
1081
  const promptsResult = this.session.resourceLoader.getPrompts();
1080
1082
  const themesResult = this.session.resourceLoader.getThemes();
1081
1083
  const extensions = options?.extensions ??
1082
- this.session.resourceLoader.getExtensions().extensions.map((extension) => ({
1084
+ this.session.resourceLoader
1085
+ .getExtensions()
1086
+ .extensions.filter((extension) => !extension.hidden)
1087
+ .map((extension) => ({
1083
1088
  path: extension.path,
1084
1089
  sourceInfo: extension.sourceInfo,
1085
1090
  }));
@@ -1358,7 +1363,7 @@ export class InteractiveMode {
1358
1363
  hasUI: true,
1359
1364
  cwd: this.sessionManager.getCwd(),
1360
1365
  sessionManager: this.sessionManager,
1361
- modelRegistry: this.session.modelRegistry,
1366
+ modelRegistry: extensionRunner.getModelRegistry(),
1362
1367
  model: this.session.model,
1363
1368
  isIdle: () => this.session.isIdle,
1364
1369
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
@@ -2046,6 +2051,7 @@ export class InteractiveMode {
2046
2051
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2047
2052
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2048
2053
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2054
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2049
2055
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2050
2056
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2051
2057
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2059,59 +2065,51 @@ export class InteractiveMode {
2059
2065
  this.updateEditorBorderColor();
2060
2066
  }
2061
2067
  };
2062
- // Handle clipboard image paste (triggered on Ctrl+V)
2068
+ // Handle clipboard paste (triggered on Ctrl+V). Images are attached by path;
2069
+ // otherwise, paste plain text from the system clipboard.
2063
2070
  this.defaultEditor.onPasteImage = () => {
2064
- this.handleClipboardImagePaste();
2071
+ void this.handleClipboardPaste();
2065
2072
  };
2066
2073
  }
2067
- async handleClipboardImagePaste() {
2074
+ async handleClipboardPaste() {
2068
2075
  try {
2069
2076
  const image = await readClipboardImage();
2070
- if (!image) {
2071
- this.showWarning("No image found in clipboard");
2072
- return;
2073
- }
2074
- const tmpDir = os.tmpdir();
2075
- const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2076
- const fileName = `pi-clipboard-${Date.now()}-${this.pendingInputImages.length + 1}.${ext}`;
2077
- const filePath = path.join(tmpDir, fileName);
2078
- fs.writeFileSync(filePath, Buffer.from(image.bytes));
2079
- let attachment;
2080
- let dimensionNote;
2081
- const autoResizeImages = this.settingsManager.getImageAutoResize();
2082
- if (autoResizeImages) {
2083
- const resized = await resizeImage(image.bytes, image.mimeType).catch(() => null);
2084
- if (!resized) {
2085
- this.showWarning("Clipboard image attached without resize");
2086
- attachment = {
2087
- type: "image",
2088
- mimeType: image.mimeType,
2089
- data: Buffer.from(image.bytes).toString("base64"),
2090
- };
2077
+ if (image) {
2078
+ const tmpDir = os.tmpdir();
2079
+ const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2080
+ const fileName = `selesai-clipboard-${Date.now()}-${this.pendingInputImages.length + 1}.${ext}`;
2081
+ const filePath = path.join(tmpDir, fileName);
2082
+ fs.writeFileSync(filePath, Buffer.from(image.bytes));
2083
+ let attachment;
2084
+ let dimensionNote;
2085
+ const autoResizeImages = this.settingsManager.getImageAutoResize();
2086
+ if (autoResizeImages) {
2087
+ const resized = await resizeImage(image.bytes, image.mimeType).catch(() => null);
2088
+ if (!resized) {
2089
+ this.showWarning("Clipboard image attached without resize");
2090
+ attachment = { type: "image", mimeType: image.mimeType, data: Buffer.from(image.bytes).toString("base64") };
2091
+ }
2092
+ else {
2093
+ dimensionNote = formatDimensionNote(resized);
2094
+ attachment = { type: "image", mimeType: resized.mimeType, data: resized.data };
2095
+ }
2091
2096
  }
2092
2097
  else {
2093
- dimensionNote = formatDimensionNote(resized);
2094
- attachment = {
2095
- type: "image",
2096
- mimeType: resized.mimeType,
2097
- data: resized.data,
2098
- };
2098
+ attachment = { type: "image", mimeType: image.mimeType, data: Buffer.from(image.bytes).toString("base64") };
2099
2099
  }
2100
+ this.pendingInputImages.push(attachment);
2101
+ this.editor.insertTextAtCursor?.(dimensionNote ? `${filePath} ${dimensionNote}` : filePath);
2102
+ this.ui.requestRender();
2103
+ return;
2100
2104
  }
2101
- else {
2102
- attachment = {
2103
- type: "image",
2104
- mimeType: image.mimeType,
2105
- data: Buffer.from(image.bytes).toString("base64"),
2106
- };
2105
+ const text = await readClipboardText();
2106
+ if (text) {
2107
+ this.editor.insertTextAtCursor?.(text);
2108
+ this.ui.requestRender();
2107
2109
  }
2108
- this.pendingInputImages.push(attachment);
2109
- const label = dimensionNote ? `${filePath} ${dimensionNote}` : filePath;
2110
- this.editor.insertTextAtCursor?.(label);
2111
- this.ui.requestRender();
2112
2110
  }
2113
2111
  catch (error) {
2114
- this.showWarning(`Failed to paste image: ${error instanceof Error ? error.message : String(error)}`);
2112
+ this.showWarning(`Failed to paste clipboard: ${error instanceof Error ? error.message : String(error)}`);
2115
2113
  }
2116
2114
  }
2117
2115
  consumePendingInputImages() {
@@ -2583,6 +2581,28 @@ export class InteractiveMode {
2583
2581
  this.ui.requestRender();
2584
2582
  break;
2585
2583
  }
2584
+ case "summarization_retry_scheduled": {
2585
+ this.showError(event.errorMessage);
2586
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2587
+ this.ui.requestRender();
2588
+ break;
2589
+ }
2590
+ case "summarization_retry_attempt_start": {
2591
+ this.clearStatusIndicator("retry");
2592
+ if (event.source === "branchSummary") {
2593
+ this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
2594
+ }
2595
+ else {
2596
+ this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2597
+ }
2598
+ this.ui.requestRender();
2599
+ break;
2600
+ }
2601
+ case "summarization_retry_finished": {
2602
+ this.clearStatusIndicator("retry");
2603
+ this.ui.requestRender();
2604
+ break;
2605
+ }
2586
2606
  }
2587
2607
  }
2588
2608
  /** Extract text content from a user message */
@@ -2719,7 +2739,7 @@ export class InteractiveMode {
2719
2739
  // Cache-miss notices are not persisted; re-derive them from the full entry
2720
2740
  // list and re-inject them after the assistant messages that paid for them.
2721
2741
  const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2722
- ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2742
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
2723
2743
  : new Map();
2724
2744
  if (options.updateFooter) {
2725
2745
  this.footer.invalidate();
@@ -2810,7 +2830,7 @@ export class InteractiveMode {
2810
2830
  if (!this.settingsManager.getShowCacheMissNotices())
2811
2831
  return;
2812
2832
  // Entries don't contain `message` yet: message_end fires before persistence.
2813
- const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2833
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
2814
2834
  if (miss)
2815
2835
  this.addCacheMissNotice(miss);
2816
2836
  }
@@ -3388,8 +3408,10 @@ export class InteractiveMode {
3388
3408
  for (const message of preCommands) {
3389
3409
  await this.session.prompt(message.text, { images: message.images });
3390
3410
  }
3391
- // Send first prompt (starts streaming)
3392
- const promptPromise = this.session.prompt(firstPrompt.text, { images: firstPrompt.images }).catch((error) => {
3411
+ // Start a prompt when idle, or queue it into a run still finishing compaction.
3412
+ const promptPromise = this.session
3413
+ .prompt(firstPrompt.text, { streamingBehavior: firstPrompt.mode, images: firstPrompt.images })
3414
+ .catch((error) => {
3393
3415
  restoreQueue(error);
3394
3416
  });
3395
3417
  // Queue remaining messages
@@ -3654,18 +3676,20 @@ export class InteractiveMode {
3654
3676
  if (this.session.scopedModels.length > 0) {
3655
3677
  return this.session.scopedModels.map((scoped) => scoped.model);
3656
3678
  }
3657
- this.session.modelRegistry.refresh();
3658
3679
  try {
3659
- return await this.session.modelRegistry.getAvailable();
3680
+ await this.session.modelRuntime.refresh();
3681
+ return [...(await this.session.modelRuntime.getAvailable())];
3660
3682
  }
3661
3683
  catch {
3662
3684
  return [];
3663
3685
  }
3664
3686
  }
3665
- /** Update the footer's available provider count from current model candidates */
3666
- async updateAvailableProviderCount() {
3667
- const models = await this.getModelCandidates();
3668
- const uniqueProviders = new Set(models.map((m) => m.provider));
3687
+ /** Update the footer's available provider count from the current snapshot without refreshing catalogs. */
3688
+ updateAvailableProviderCount() {
3689
+ const models = this.session.scopedModels.length > 0
3690
+ ? this.session.scopedModels.map((scoped) => scoped.model)
3691
+ : this.session.modelRuntime.getAvailableSnapshot();
3692
+ const uniqueProviders = new Set(models.map((model) => model.provider));
3669
3693
  this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
3670
3694
  }
3671
3695
  async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
@@ -3678,14 +3702,13 @@ export class InteractiveMode {
3678
3702
  if (!model || model.provider !== "anthropic") {
3679
3703
  return;
3680
3704
  }
3681
- const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
3682
- if (storedCredential?.type === "oauth") {
3683
- this.anthropicSubscriptionWarningShown = true;
3684
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3685
- return;
3686
- }
3687
3705
  try {
3688
- const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
3706
+ if ((await this.session.modelRuntime.checkAuth("anthropic"))?.type === "oauth") {
3707
+ this.anthropicSubscriptionWarningShown = true;
3708
+ this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3709
+ return;
3710
+ }
3711
+ const apiKey = (await this.session.modelRuntime.getAuth(model.provider))?.auth.apiKey;
3689
3712
  if (!isAnthropicSubscriptionAuthKey(apiKey)) {
3690
3713
  return;
3691
3714
  }
@@ -3743,7 +3766,7 @@ export class InteractiveMode {
3743
3766
  }
3744
3767
  showModelSelector(initialSearchInput) {
3745
3768
  this.showSelector((done) => {
3746
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3769
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRuntime, this.session.scopedModels, async (model) => {
3747
3770
  try {
3748
3771
  await this.session.setModel(model);
3749
3772
  this.footer.invalidate();
@@ -3766,8 +3789,8 @@ export class InteractiveMode {
3766
3789
  }
3767
3790
  async showModelsSelector() {
3768
3791
  // Get all available models
3769
- this.session.modelRegistry.refresh();
3770
- const allModels = this.session.modelRegistry.getAvailable();
3792
+ await this.session.modelRuntime.refresh();
3793
+ const allModels = [...(await this.session.modelRuntime.getAvailable())];
3771
3794
  if (allModels.length === 0) {
3772
3795
  this.showStatus("No models available");
3773
3796
  return;
@@ -3785,7 +3808,7 @@ export class InteractiveMode {
3785
3808
  // Fall back to settings
3786
3809
  const patterns = this.settingsManager.getEnabledModels();
3787
3810
  if (patterns !== undefined && patterns.length > 0) {
3788
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3811
+ const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
3789
3812
  currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3790
3813
  }
3791
3814
  }
@@ -3793,7 +3816,7 @@ export class InteractiveMode {
3793
3816
  const updateSessionModels = async (enabledIds) => {
3794
3817
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3795
3818
  if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3796
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3819
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3797
3820
  this.session.setScopedModels(newScopedModels.map((sm) => ({
3798
3821
  model: sm.model,
3799
3822
  thinkingLevel: sm.thinkingLevel,
@@ -3976,6 +3999,19 @@ export class InteractiveMode {
3976
3999
  this.sessionManager.appendLabelChange(entryId, label);
3977
4000
  this.ui.requestRender();
3978
4001
  }, initialSelectedId, initialFilterMode);
4002
+ selector.onCopy = async (text) => {
4003
+ if (!text) {
4004
+ this.showError("Selected entry has no text to copy");
4005
+ return;
4006
+ }
4007
+ try {
4008
+ await copyToClipboard(text);
4009
+ this.showStatus("Copied selected message to clipboard");
4010
+ }
4011
+ catch (error) {
4012
+ this.showError(error instanceof Error ? error.message : String(error));
4013
+ }
4014
+ };
3979
4015
  return { component: selector, focus: selector };
3980
4016
  });
3981
4017
  }
@@ -4040,44 +4076,46 @@ export class InteractiveMode {
4040
4076
  }
4041
4077
  }
4042
4078
  getLoginProviderOptions(authType) {
4043
- const authStorage = this.session.modelRegistry.authStorage;
4044
- const oauthProviders = authStorage.getOAuthProviders();
4045
- const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
4046
- const options = oauthProviders.map((provider) => ({
4047
- id: provider.id,
4048
- name: provider.name,
4049
- authType: "oauth",
4050
- }));
4051
- const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
4052
- for (const providerId of modelProviders) {
4053
- if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
4054
- continue;
4055
- }
4056
- options.push({
4057
- id: providerId,
4058
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4059
- authType: "api_key",
4060
- });
4061
- }
4062
- const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
4063
- return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
4064
- }
4065
- getLogoutProviderOptions() {
4066
- const authStorage = this.session.modelRegistry.authStorage;
4067
4079
  const options = [];
4068
- for (const providerId of authStorage.list()) {
4069
- const credential = authStorage.get(providerId);
4070
- if (!credential) {
4071
- continue;
4080
+ for (const provider of this.session.modelRuntime.getProviders()) {
4081
+ const authStatus = this.session.modelRuntime.getProviderAuthStatus(provider.id);
4082
+ const status = authStatus.configured
4083
+ ? {
4084
+ type: this.session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
4085
+ source: authStatus.label ?? authStatus.source,
4086
+ }
4087
+ : undefined;
4088
+ if ((!authType || authType === "oauth") && provider.auth.oauth) {
4089
+ options.push({
4090
+ id: provider.id,
4091
+ name: provider.name,
4092
+ authType: "oauth",
4093
+ method: provider.auth.oauth,
4094
+ status,
4095
+ });
4096
+ }
4097
+ if ((!authType || authType === "api_key") && provider.auth.apiKey) {
4098
+ options.push({
4099
+ id: provider.id,
4100
+ name: provider.name,
4101
+ authType: "api_key",
4102
+ method: provider.auth.apiKey,
4103
+ status,
4104
+ });
4072
4105
  }
4073
- options.push({
4074
- id: providerId,
4075
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4076
- authType: credential.type,
4077
- });
4078
4106
  }
4079
4107
  return options.sort((a, b) => a.name.localeCompare(b.name));
4080
4108
  }
4109
+ async getLogoutProviderOptions() {
4110
+ return (await this.session.modelRuntime.listCredentials())
4111
+ .map(({ providerId, type }) => ({
4112
+ id: providerId,
4113
+ name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
4114
+ authType: type,
4115
+ status: { type, source: "stored credential" },
4116
+ }))
4117
+ .sort((a, b) => a.name.localeCompare(b.name));
4118
+ }
4081
4119
  findLoginProviderOptions(providerRef) {
4082
4120
  const normalizedProviderRef = providerRef.trim().toLowerCase();
4083
4121
  if (!normalizedProviderRef) {
@@ -4087,6 +4125,7 @@ export class InteractiveMode {
4087
4125
  provider.name.toLowerCase() === normalizedProviderRef);
4088
4126
  }
4089
4127
  async handleLoginCommand(providerRef) {
4128
+ await this.session.modelRuntime.getAvailable();
4090
4129
  if (!providerRef) {
4091
4130
  this.showLoginAuthTypeSelector();
4092
4131
  return;
@@ -4109,16 +4148,18 @@ export class InteractiveMode {
4109
4148
  if (providerOption.authType === "oauth") {
4110
4149
  await this.showLoginDialog(providerOption.id, providerOption.name);
4111
4150
  }
4112
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4113
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4151
+ else if (providerOption.method?.login) {
4152
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4114
4153
  }
4115
4154
  else {
4116
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4155
+ this.showAmbientAuthDialog(providerOption);
4117
4156
  }
4118
4157
  }
4119
4158
  showLoginAuthTypeSelector(providerOptions) {
4120
- const subscriptionLabel = "Use a subscription";
4121
- const apiKeyLabel = "Use an API key";
4159
+ const oauthProvider = providerOptions?.find((provider) => provider.authType === "oauth");
4160
+ const oauthLoginLabel = oauthProvider?.method && "loginLabel" in oauthProvider.method ? oauthProvider.method.loginLabel : undefined;
4161
+ const subscriptionLabel = oauthLoginLabel ?? "Sign in with an account";
4162
+ const apiKeyLabel = "Sign in with an API key";
4122
4163
  const availableAuthTypes = providerOptions
4123
4164
  ? new Set(providerOptions.map((provider) => provider.authType))
4124
4165
  : new Set(["oauth", "api_key"]);
@@ -4174,7 +4215,7 @@ export class InteractiveMode {
4174
4215
  return;
4175
4216
  }
4176
4217
  this.showSelector((done) => {
4177
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
4218
+ const selector = new OAuthSelectorComponent("login", providerOptions, async (providerId, selectedAuthType) => {
4178
4219
  done();
4179
4220
  const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
4180
4221
  if (!providerOption) {
@@ -4189,7 +4230,7 @@ export class InteractiveMode {
4189
4230
  else {
4190
4231
  this.ui.requestRender();
4191
4232
  }
4192
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
4233
+ }, initialSearchInput);
4193
4234
  return { component: selector, focus: selector };
4194
4235
  });
4195
4236
  }
@@ -4198,21 +4239,20 @@ export class InteractiveMode {
4198
4239
  this.showLoginAuthTypeSelector();
4199
4240
  return;
4200
4241
  }
4201
- const providerOptions = this.getLogoutProviderOptions();
4242
+ const providerOptions = await this.getLogoutProviderOptions();
4202
4243
  if (providerOptions.length === 0) {
4203
4244
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4204
4245
  return;
4205
4246
  }
4206
4247
  this.showSelector((done) => {
4207
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4248
+ const selector = new OAuthSelectorComponent(mode, providerOptions, async (providerId) => {
4208
4249
  done();
4209
4250
  const providerOption = providerOptions.find((provider) => provider.id === providerId);
4210
4251
  if (!providerOption) {
4211
4252
  return;
4212
4253
  }
4213
4254
  try {
4214
- this.session.modelRegistry.authStorage.logout(providerOption.id);
4215
- this.session.modelRegistry.refresh();
4255
+ await this.session.modelRuntime.logout(providerOption.id);
4216
4256
  await this.updateAvailableProviderCount();
4217
4257
  const message = providerOption.authType === "oauth"
4218
4258
  ? `Logged out of ${providerOption.name}`
@@ -4230,12 +4270,12 @@ export class InteractiveMode {
4230
4270
  });
4231
4271
  }
4232
4272
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4233
- this.session.modelRegistry.refresh();
4273
+ await this.session.modelRuntime.getAvailable();
4234
4274
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4235
4275
  let selectedModel;
4236
4276
  let selectionError;
4237
4277
  if (isUnknownModel(previousModel)) {
4238
- const availableModels = this.session.modelRegistry.getAvailable();
4278
+ const availableModels = await this.session.modelRuntime.getAvailable();
4239
4279
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4240
4280
  if (!hasDefaultModelProvider(providerId)) {
4241
4281
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4279,20 +4319,15 @@ export class InteractiveMode {
4279
4319
  }
4280
4320
  }
4281
4321
  }
4282
- showBedrockSetupDialog(providerId, providerName) {
4322
+ showAmbientAuthDialog(providerOption) {
4283
4323
  const restoreEditor = () => {
4284
4324
  this.editorContainer.clear();
4285
4325
  this.editorContainer.addChild(this.editor);
4286
4326
  this.ui.setFocus(this.editor);
4287
4327
  this.ui.requestRender();
4288
4328
  };
4289
- const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
4290
- dialog.showInfo([
4291
- theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
4292
- theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
4293
- theme.fg("muted", "See:"),
4294
- theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4295
- ]);
4329
+ const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
4330
+ dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true);
4296
4331
  this.editorContainer.clear();
4297
4332
  this.editorContainer.addChild(dialog);
4298
4333
  this.ui.setFocus(dialog);
@@ -4303,6 +4338,13 @@ export class InteractiveMode {
4303
4338
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4304
4339
  // Completion handled below
4305
4340
  }, providerName);
4341
+ if (providerId === "amazon-bedrock") {
4342
+ dialog.showDetails([
4343
+ theme.fg("text", "You can also use an AWS profile, IAM keys, or role-based credentials."),
4344
+ theme.fg("muted", "See:"),
4345
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4346
+ ]);
4347
+ }
4306
4348
  this.editorContainer.clear();
4307
4349
  this.editorContainer.addChild(dialog);
4308
4350
  this.ui.setFocus(dialog);
@@ -4314,11 +4356,7 @@ export class InteractiveMode {
4314
4356
  this.ui.requestRender();
4315
4357
  };
4316
4358
  try {
4317
- const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
4318
- if (!apiKey) {
4319
- throw new Error("API key cannot be empty.");
4320
- }
4321
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
4359
+ await this.loginProvider(dialog, providerId, "api_key");
4322
4360
  restoreEditor();
4323
4361
  await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
4324
4362
  }
@@ -4330,8 +4368,8 @@ export class InteractiveMode {
4330
4368
  }
4331
4369
  }
4332
4370
  }
4333
- showOAuthLoginSelect(dialog, prompt) {
4334
- return new Promise((resolve) => {
4371
+ showAuthSelect(dialog, prompt) {
4372
+ return new Promise((resolve, reject) => {
4335
4373
  const restoreDialog = () => {
4336
4374
  this.editorContainer.clear();
4337
4375
  this.editorContainer.addChild(dialog);
@@ -4341,10 +4379,14 @@ export class InteractiveMode {
4341
4379
  const labels = prompt.options.map((option) => option.label);
4342
4380
  const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
4343
4381
  restoreDialog();
4344
- resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
4382
+ const id = prompt.options.find((option) => option.label === optionLabel)?.id;
4383
+ if (id)
4384
+ resolve(id);
4385
+ else
4386
+ reject(new Error("Login cancelled"));
4345
4387
  }, () => {
4346
4388
  restoreDialog();
4347
- resolve(undefined);
4389
+ reject(new Error("Login cancelled"));
4348
4390
  });
4349
4391
  this.editorContainer.clear();
4350
4392
  this.editorContainer.addChild(selector);
@@ -4352,30 +4394,64 @@ export class InteractiveMode {
4352
4394
  this.ui.requestRender();
4353
4395
  });
4354
4396
  }
4397
+ async showAuthPrompt(dialog, prompt) {
4398
+ let response;
4399
+ if (prompt.type === "select") {
4400
+ response = this.showAuthSelect(dialog, prompt);
4401
+ }
4402
+ else if (prompt.type === "manual_code") {
4403
+ response = dialog.showManualInput(prompt.message);
4404
+ }
4405
+ else {
4406
+ response = dialog.showPrompt(prompt.message, prompt.placeholder);
4407
+ }
4408
+ if (!prompt.signal)
4409
+ return response;
4410
+ if (prompt.signal.aborted)
4411
+ throw new Error("Login cancelled");
4412
+ const signal = prompt.signal;
4413
+ let onAbort;
4414
+ const aborted = new Promise((_resolve, reject) => {
4415
+ onAbort = () => reject(new Error("Login cancelled"));
4416
+ signal.addEventListener("abort", onAbort, { once: true });
4417
+ });
4418
+ try {
4419
+ return await Promise.race([response, aborted]);
4420
+ }
4421
+ finally {
4422
+ if (onAbort)
4423
+ signal.removeEventListener("abort", onAbort);
4424
+ }
4425
+ }
4426
+ notifyAuthDialog(dialog, event) {
4427
+ if (event.type === "auth_url") {
4428
+ dialog.showAuth(event.url, event.instructions);
4429
+ }
4430
+ else if (event.type === "device_code") {
4431
+ dialog.showDeviceCode(event);
4432
+ dialog.showWaiting("Waiting for authentication...");
4433
+ }
4434
+ else if (event.type === "info") {
4435
+ dialog.showInfo(event.message, event.links);
4436
+ }
4437
+ else {
4438
+ dialog.showProgress(event.message);
4439
+ }
4440
+ }
4441
+ async loginProvider(dialog, providerId, method) {
4442
+ await this.session.modelRuntime.login(providerId, method, {
4443
+ signal: dialog.signal,
4444
+ prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
4445
+ notify: (event) => this.notifyAuthDialog(dialog, event),
4446
+ });
4447
+ }
4355
4448
  async showLoginDialog(providerId, providerName) {
4356
- const providerInfo = this.session.modelRegistry.authStorage
4357
- .getOAuthProviders()
4358
- .find((provider) => provider.id === providerId);
4359
4449
  const previousModel = this.session.model;
4360
- // Providers that use callback servers (can paste redirect URL)
4361
- const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
4362
- // Create login dialog component
4363
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4364
- // Completion handled below
4365
- }, providerName);
4366
- // Show dialog in editor container
4450
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => { }, providerName);
4367
4451
  this.editorContainer.clear();
4368
4452
  this.editorContainer.addChild(dialog);
4369
4453
  this.ui.setFocus(dialog);
4370
4454
  this.ui.requestRender();
4371
- // Promise for manual code input (racing with callback server)
4372
- let manualCodeResolve;
4373
- let manualCodeReject;
4374
- const manualCodePromise = new Promise((resolve, reject) => {
4375
- manualCodeResolve = resolve;
4376
- manualCodeReject = reject;
4377
- });
4378
- // Restore editor helper
4379
4455
  const restoreEditor = () => {
4380
4456
  this.editorContainer.clear();
4381
4457
  this.editorContainer.addChild(this.editor);
@@ -4383,43 +4459,7 @@ export class InteractiveMode {
4383
4459
  this.ui.requestRender();
4384
4460
  };
4385
4461
  try {
4386
- await this.session.modelRegistry.authStorage.login(providerId, {
4387
- onAuth: (info) => {
4388
- dialog.showAuth(info.url, info.instructions);
4389
- if (usesCallbackServer) {
4390
- // Show input for manual paste, racing with callback
4391
- dialog
4392
- .showManualInput("Paste redirect URL below, or complete login in browser:")
4393
- .then((value) => {
4394
- if (value && manualCodeResolve) {
4395
- manualCodeResolve(value);
4396
- manualCodeResolve = undefined;
4397
- }
4398
- })
4399
- .catch(() => {
4400
- if (manualCodeReject) {
4401
- manualCodeReject(new Error("Login cancelled"));
4402
- manualCodeReject = undefined;
4403
- }
4404
- });
4405
- }
4406
- // For Anthropic: onPrompt is called immediately after
4407
- },
4408
- onDeviceCode: (info) => {
4409
- dialog.showDeviceCode(info);
4410
- dialog.showWaiting("Waiting for authentication...");
4411
- },
4412
- onPrompt: async (prompt) => {
4413
- return dialog.showPrompt(prompt.message, prompt.placeholder);
4414
- },
4415
- onProgress: (message) => {
4416
- dialog.showProgress(message);
4417
- },
4418
- onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
4419
- onManualCodeInput: () => manualCodePromise,
4420
- signal: dialog.signal,
4421
- });
4422
- // Success
4462
+ await this.loginProvider(dialog, providerId, "oauth");
4423
4463
  restoreEditor();
4424
4464
  await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
4425
4465
  }
@@ -4507,7 +4547,7 @@ export class InteractiveMode {
4507
4547
  showDiagnosticsWhenQuiet: true,
4508
4548
  });
4509
4549
  const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
4510
- const modelsJsonError = this.session.modelRegistry.getError();
4550
+ const modelsJsonError = this.session.modelRuntime.getError();
4511
4551
  if (modelsJsonError) {
4512
4552
  this.showError(`models.json error: ${modelsJsonError}`);
4513
4553
  }
@@ -4735,25 +4775,11 @@ export class InteractiveMode {
4735
4775
  const stats = this.session.getSessionStats();
4736
4776
  const sessionName = this.sessionManager.getSessionName();
4737
4777
  const entries = this.sessionManager.getEntries();
4738
- const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4778
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
4739
4779
  // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4740
- // resolves to a concrete responseModel), sorted by cost descending.
4741
- const perModelMap = new Map();
4742
- for (const entry of entries) {
4743
- if (entry.type !== "message" || entry.message.role !== "assistant")
4744
- continue;
4745
- const message = entry.message;
4746
- const usage = message.usage;
4747
- const key = `${message.provider}/${message.responseModel ?? message.model}`;
4748
- let bucket = perModelMap.get(key);
4749
- if (!bucket) {
4750
- bucket = { key, cost: 0, tokens: 0 };
4751
- perModelMap.set(key, bucket);
4752
- }
4753
- bucket.cost += usage.cost.total;
4754
- bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
4755
- }
4756
- const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
4780
+ // resolves to a concrete responseModel). Usage without model attribution is
4781
+ // grouped separately so the breakdown reconciles with the session total.
4782
+ const usageBreakdown = getUsageCostBreakdown(entries);
4757
4783
  let info = `${theme.bold("Session Info")}\n\n`;
4758
4784
  if (sessionName) {
4759
4785
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4784,8 +4810,8 @@ export class InteractiveMode {
4784
4810
  if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4785
4811
  info += `\n${theme.bold("Cost")}\n`;
4786
4812
  info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
4787
- if (perModel.length > 1) {
4788
- for (const entry of perModel) {
4813
+ if (usageBreakdown.length > 1) {
4814
+ for (const entry of usageBreakdown) {
4789
4815
  info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
4790
4816
  }
4791
4817
  }
@@ -4868,6 +4894,7 @@ export class InteractiveMode {
4868
4894
  const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
4869
4895
  const externalEditor = this.getAppKeyDisplay("app.editor.external");
4870
4896
  const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
4897
+ const copyMessage = this.getAppKeyDisplay("app.message.copy");
4871
4898
  const followUp = this.getAppKeyDisplay("app.message.followUp");
4872
4899
  const dequeue = this.getAppKeyDisplay("app.message.dequeue");
4873
4900
  const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
@@ -4910,9 +4937,10 @@ export class InteractiveMode {
4910
4937
  | \`${expandTools}\` | Toggle tool output expansion |
4911
4938
  | \`${toggleThinking}\` | Toggle thinking block visibility |
4912
4939
  | \`${externalEditor}\` | Edit message in external editor |
4940
+ | \`${copyMessage}\` | Copy last assistant message |
4913
4941
  | \`${followUp}\` | Queue follow-up message |
4914
4942
  | \`${dequeue}\` | Restore queued messages |
4915
- | \`${pasteImage}\` | Paste image from clipboard |
4943
+ | \`${pasteImage}\` | Paste image or text from clipboard |
4916
4944
  | \`/\` | Slash commands |
4917
4945
  | \`!\` | Run bash command |
4918
4946
  | \`!!\` | Run bash command (excluded from context) |