@earendil-works/pi-coding-agent 0.80.6 → 0.80.8

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 (172) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +9 -7
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +2 -0
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/cli/args.d.ts.map +1 -1
  7. package/dist/cli/args.js +1 -1
  8. package/dist/cli/args.js.map +1 -1
  9. package/dist/cli/list-models.d.ts +2 -2
  10. package/dist/cli/list-models.d.ts.map +1 -1
  11. package/dist/cli/list-models.js +3 -3
  12. package/dist/cli/list-models.js.map +1 -1
  13. package/dist/core/agent-session-services.d.ts +3 -6
  14. package/dist/core/agent-session-services.d.ts.map +1 -1
  15. package/dist/core/agent-session-services.js +10 -9
  16. package/dist/core/agent-session-services.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +6 -7
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +61 -34
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/auth-storage.d.ts +15 -99
  22. package/dist/core/auth-storage.d.ts.map +1 -1
  23. package/dist/core/auth-storage.js +46 -259
  24. package/dist/core/auth-storage.js.map +1 -1
  25. package/dist/core/cache-stats.d.ts +2 -2
  26. package/dist/core/cache-stats.d.ts.map +1 -1
  27. package/dist/core/cache-stats.js +1 -1
  28. package/dist/core/cache-stats.js.map +1 -1
  29. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  30. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  31. package/dist/core/compaction/branch-summarization.js.map +1 -1
  32. package/dist/core/extensions/loader.d.ts.map +1 -1
  33. package/dist/core/extensions/loader.js +8 -2
  34. package/dist/core/extensions/loader.js.map +1 -1
  35. package/dist/core/extensions/runner.d.ts +2 -0
  36. package/dist/core/extensions/runner.d.ts.map +1 -1
  37. package/dist/core/extensions/runner.js +8 -0
  38. package/dist/core/extensions/runner.js.map +1 -1
  39. package/dist/core/extensions/types.d.ts +9 -2
  40. package/dist/core/extensions/types.d.ts.map +1 -1
  41. package/dist/core/extensions/types.js.map +1 -1
  42. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  43. package/dist/core/extensions/wrapper.js +22 -3
  44. package/dist/core/extensions/wrapper.js.map +1 -1
  45. package/dist/core/keybindings.d.ts +8 -3
  46. package/dist/core/keybindings.d.ts.map +1 -1
  47. package/dist/core/keybindings.js +7 -3
  48. package/dist/core/keybindings.js.map +1 -1
  49. package/dist/core/model-config.d.ts +507 -0
  50. package/dist/core/model-config.d.ts.map +1 -0
  51. package/dist/core/model-config.js +242 -0
  52. package/dist/core/model-config.js.map +1 -0
  53. package/dist/core/model-registry.d.ts +13 -123
  54. package/dist/core/model-registry.d.ts.map +1 -1
  55. package/dist/core/model-registry.js +44 -731
  56. package/dist/core/model-registry.js.map +1 -1
  57. package/dist/core/model-resolver.d.ts +6 -6
  58. package/dist/core/model-resolver.d.ts.map +1 -1
  59. package/dist/core/model-resolver.js +18 -17
  60. package/dist/core/model-resolver.js.map +1 -1
  61. package/dist/core/model-runtime.d.ts +77 -0
  62. package/dist/core/model-runtime.d.ts.map +1 -0
  63. package/dist/core/model-runtime.js +418 -0
  64. package/dist/core/model-runtime.js.map +1 -0
  65. package/dist/core/models-store.d.ts +17 -0
  66. package/dist/core/models-store.d.ts.map +1 -0
  67. package/dist/core/models-store.js +45 -0
  68. package/dist/core/models-store.js.map +1 -0
  69. package/dist/core/package-manager.d.ts.map +1 -1
  70. package/dist/core/package-manager.js +7 -2
  71. package/dist/core/package-manager.js.map +1 -1
  72. package/dist/core/provider-composer.d.ts +55 -0
  73. package/dist/core/provider-composer.d.ts.map +1 -0
  74. package/dist/core/provider-composer.js +375 -0
  75. package/dist/core/provider-composer.js.map +1 -0
  76. package/dist/core/radius.d.ts +2 -0
  77. package/dist/core/radius.d.ts.map +1 -0
  78. package/dist/core/radius.js +2 -0
  79. package/dist/core/radius.js.map +1 -0
  80. package/dist/core/remote-catalog-provider.d.ts +5 -0
  81. package/dist/core/remote-catalog-provider.d.ts.map +1 -0
  82. package/dist/core/remote-catalog-provider.js +83 -0
  83. package/dist/core/remote-catalog-provider.js.map +1 -0
  84. package/dist/core/runtime-credentials.d.ts +15 -0
  85. package/dist/core/runtime-credentials.d.ts.map +1 -0
  86. package/dist/core/runtime-credentials.js +36 -0
  87. package/dist/core/runtime-credentials.js.map +1 -0
  88. package/dist/core/sdk.d.ts +3 -6
  89. package/dist/core/sdk.d.ts.map +1 -1
  90. package/dist/core/sdk.js +14 -25
  91. package/dist/core/sdk.js.map +1 -1
  92. package/dist/core/system-prompt.d.ts.map +1 -1
  93. package/dist/core/system-prompt.js +1 -11
  94. package/dist/core/system-prompt.js.map +1 -1
  95. package/dist/index.d.ts +2 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +2 -2
  98. package/dist/index.js.map +1 -1
  99. package/dist/main.d.ts.map +1 -1
  100. package/dist/main.js +11 -13
  101. package/dist/main.js.map +1 -1
  102. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/assistant-message.js +22 -10
  104. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  105. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  106. package/dist/modes/interactive/components/custom-editor.js +1 -1
  107. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  108. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/footer.js +1 -1
  110. package/dist/modes/interactive/components/footer.js.map +1 -1
  111. package/dist/modes/interactive/components/login-dialog.d.ts +5 -5
  112. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/login-dialog.js +17 -8
  114. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  115. package/dist/modes/interactive/components/model-selector.d.ts +11 -4
  116. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/model-selector.js +72 -41
  118. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  119. package/dist/modes/interactive/components/oauth-selector.d.ts +4 -4
  120. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/oauth-selector.js +14 -27
  122. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  123. package/dist/modes/interactive/components/tree-selector.d.ts +5 -0
  124. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/tree-selector.js +49 -12
  126. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  127. package/dist/modes/interactive/interactive-mode.d.ts +7 -5
  128. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  129. package/dist/modes/interactive/interactive-mode.js +187 -171
  130. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  131. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  132. package/dist/modes/rpc/rpc-mode.js +2 -2
  133. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  134. package/dist/package-manager-cli.d.ts.map +1 -1
  135. package/dist/package-manager-cli.js +67 -5
  136. package/dist/package-manager-cli.js.map +1 -1
  137. package/dist/utils/clipboard-native.d.ts +1 -0
  138. package/dist/utils/clipboard-native.d.ts.map +1 -1
  139. package/dist/utils/clipboard-native.js.map +1 -1
  140. package/dist/utils/clipboard.d.ts +2 -0
  141. package/dist/utils/clipboard.d.ts.map +1 -1
  142. package/dist/utils/clipboard.js +13 -0
  143. package/dist/utils/clipboard.js.map +1 -1
  144. package/docs/custom-provider.md +8 -11
  145. package/docs/extensions.md +160 -1
  146. package/docs/keybindings.md +1 -0
  147. package/docs/models.md +3 -0
  148. package/docs/packages.md +1 -0
  149. package/docs/providers.md +16 -1
  150. package/docs/quickstart.md +1 -1
  151. package/docs/sdk.md +40 -50
  152. package/docs/usage.md +2 -0
  153. package/examples/extensions/custom-provider-anthropic/index.ts +1 -1
  154. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  155. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  156. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  157. package/examples/extensions/gondolin/package-lock.json +2 -2
  158. package/examples/extensions/gondolin/package.json +1 -1
  159. package/examples/extensions/sandbox/package-lock.json +2 -2
  160. package/examples/extensions/sandbox/package.json +1 -1
  161. package/examples/extensions/with-deps/package-lock.json +2 -2
  162. package/examples/extensions/with-deps/package.json +1 -1
  163. package/examples/sdk/02-custom-model.ts +5 -8
  164. package/examples/sdk/09-api-keys-and-oauth.ts +13 -31
  165. package/examples/sdk/12-full-control.ts +7 -12
  166. package/examples/sdk/README.md +14 -18
  167. package/npm-shrinkwrap.json +12 -12
  168. package/package.json +4 -4
  169. package/dist/core/provider-display-names.d.ts +0 -2
  170. package/dist/core/provider-display-names.d.ts.map +0 -1
  171. package/dist/core/provider-display-names.js +0 -36
  172. package/dist/core/provider-display-names.js.map +0 -1
@@ -6,7 +6,6 @@ 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 { getProviders, } from "@earendil-works/pi-ai/compat";
10
9
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
11
10
  import chalk from "chalk";
12
11
  import { spawn, spawnSync } from "child_process";
@@ -20,14 +19,13 @@ import { KeybindingsManager } from "../../core/keybindings.js";
20
19
  import { createCompactionSummaryMessage } from "../../core/messages.js";
21
20
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
22
21
  import { DefaultPackageManager } from "../../core/package-manager.js";
23
- import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
24
22
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
25
23
  import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.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";
29
27
  import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
30
- import { copyToClipboard } from "../../utils/clipboard.js";
28
+ import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
31
29
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
32
30
  import { parseGitUrl } from "../../utils/git.js";
33
31
  import { getCwdRelativePath } from "../../utils/paths.js";
@@ -94,7 +92,7 @@ function isDeadTerminalError(error) {
94
92
  const code = error.code;
95
93
  return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
96
94
  }
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.";
95
+ 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.";
98
96
  function isAnthropicSubscriptionAuthKey(apiKey) {
99
97
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
100
98
  }
@@ -125,17 +123,6 @@ export function formatResumeCommand(sessionManager) {
125
123
  function hasDefaultModelProvider(providerId) {
126
124
  return providerId in defaultModelPerProvider;
127
125
  }
128
- const BEDROCK_PROVIDER_ID = "amazon-bedrock";
129
- const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
130
- export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
131
- if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
132
- return true;
133
- }
134
- if (builtInProviderIds.has(providerId)) {
135
- return false;
136
- }
137
- return !oauthProviderIds.has(providerId);
138
- }
139
126
  const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
140
127
  function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
141
128
  const filtered = fuzzyFilter(items, prefix, getSearchText);
@@ -362,11 +349,11 @@ export class InteractiveMode {
362
349
  }));
363
350
  const modelCommand = slashCommands.find((command) => command.name === "model");
364
351
  if (modelCommand) {
365
- modelCommand.getArgumentCompletions = (prefix) => {
352
+ modelCommand.getArgumentCompletions = async (prefix) => {
366
353
  // Get available models (scoped or from registry)
367
354
  const models = this.session.scopedModels.length > 0
368
355
  ? this.session.scopedModels.map((s) => s.model)
369
- : this.session.modelRegistry.getAvailable();
356
+ : await this.session.modelRuntime.getAvailable();
370
357
  if (models.length === 0)
371
358
  return null;
372
359
  // Create items with provider/id format
@@ -532,7 +519,7 @@ export class InteractiveMode {
532
519
  rawKeyHint("!!", "to run bash (no context)"),
533
520
  hint("app.message.followUp", "to queue follow-up"),
534
521
  hint("app.message.dequeue", "to edit all queued messages"),
535
- hint("app.clipboard.pasteImage", "to paste image"),
522
+ hint("app.clipboard.pasteImage", "to paste image (with text fallback)"),
536
523
  rawKeyHint("drop files", "to attach"),
537
524
  ].join("\n");
538
525
  const compactInstructions = [
@@ -599,10 +586,18 @@ export class InteractiveMode {
599
586
  }
600
587
  });
601
588
  // Start package update check asynchronously
602
- this.checkForPackageUpdates().then((updates) => {
589
+ this.checkForPackageUpdates()
590
+ .then((updates) => {
603
591
  if (updates.length > 0) {
604
592
  this.showPackageUpdateNotification(updates);
605
593
  }
594
+ })
595
+ .finally(() => {
596
+ // On Windows, npm can overwrite the shared console title while checking
597
+ // extension package versions. Restore Pi's title after the startup check.
598
+ if (process.platform === "win32" && this.isInitialized) {
599
+ this.updateTerminalTitle();
600
+ }
606
601
  });
607
602
  // Check tmux keyboard setup asynchronously
608
603
  this.checkTmuxKeyboardSetup().then((warning) => {
@@ -615,7 +610,7 @@ export class InteractiveMode {
615
610
  if (migratedProviders && migratedProviders.length > 0) {
616
611
  this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
617
612
  }
618
- const modelsJsonError = this.session.modelRegistry.getError();
613
+ const modelsJsonError = this.session.modelRuntime.getError();
619
614
  if (modelsJsonError) {
620
615
  this.showError(`models.json error: ${modelsJsonError}`);
621
616
  }
@@ -1346,7 +1341,7 @@ export class InteractiveMode {
1346
1341
  hasUI: true,
1347
1342
  cwd: this.sessionManager.getCwd(),
1348
1343
  sessionManager: this.sessionManager,
1349
- modelRegistry: this.session.modelRegistry,
1344
+ modelRegistry: extensionRunner.getModelRegistry(),
1350
1345
  model: this.session.model,
1351
1346
  isIdle: () => this.session.isIdle,
1352
1347
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
@@ -2034,6 +2029,7 @@ export class InteractiveMode {
2034
2029
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2035
2030
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2036
2031
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2032
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2037
2033
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2038
2034
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2039
2035
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -2047,26 +2043,30 @@ export class InteractiveMode {
2047
2043
  this.updateEditorBorderColor();
2048
2044
  }
2049
2045
  };
2050
- // Handle clipboard image paste (triggered on Ctrl+V)
2046
+ // Handle clipboard paste (triggered on Ctrl+V). Images are attached by path;
2047
+ // otherwise, paste plain text from the system clipboard.
2051
2048
  this.defaultEditor.onPasteImage = () => {
2052
- this.handleClipboardImagePaste();
2049
+ void this.handleClipboardPaste();
2053
2050
  };
2054
2051
  }
2055
- async handleClipboardImagePaste() {
2052
+ async handleClipboardPaste() {
2056
2053
  try {
2057
2054
  const image = await readClipboardImage();
2058
- if (!image) {
2055
+ if (image) {
2056
+ const tmpDir = os.tmpdir();
2057
+ const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2058
+ const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
2059
+ const filePath = path.join(tmpDir, fileName);
2060
+ fs.writeFileSync(filePath, Buffer.from(image.bytes));
2061
+ this.editor.insertTextAtCursor?.(filePath);
2062
+ this.ui.requestRender();
2059
2063
  return;
2060
2064
  }
2061
- // Write to temp file
2062
- const tmpDir = os.tmpdir();
2063
- const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2064
- const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
2065
- const filePath = path.join(tmpDir, fileName);
2066
- fs.writeFileSync(filePath, Buffer.from(image.bytes));
2067
- // Insert file path directly
2068
- this.editor.insertTextAtCursor?.(filePath);
2069
- this.ui.requestRender();
2065
+ const text = await readClipboardText();
2066
+ if (text) {
2067
+ this.editor.insertTextAtCursor?.(text);
2068
+ this.ui.requestRender();
2069
+ }
2070
2070
  }
2071
2071
  catch {
2072
2072
  // Silently ignore clipboard errors (may not have permission, etc.)
@@ -2647,7 +2647,7 @@ export class InteractiveMode {
2647
2647
  // Cache-miss notices are not persisted; re-derive them from the full entry
2648
2648
  // list and re-inject them after the assistant messages that paid for them.
2649
2649
  const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2650
- ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2650
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
2651
2651
  : new Map();
2652
2652
  if (options.updateFooter) {
2653
2653
  this.footer.invalidate();
@@ -2738,7 +2738,7 @@ export class InteractiveMode {
2738
2738
  if (!this.settingsManager.getShowCacheMissNotices())
2739
2739
  return;
2740
2740
  // Entries don't contain `message` yet: message_end fires before persistence.
2741
- const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2741
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
2742
2742
  if (miss)
2743
2743
  this.addCacheMissNotice(miss);
2744
2744
  }
@@ -3579,9 +3579,9 @@ export class InteractiveMode {
3579
3579
  if (this.session.scopedModels.length > 0) {
3580
3580
  return this.session.scopedModels.map((scoped) => scoped.model);
3581
3581
  }
3582
- this.session.modelRegistry.refresh();
3583
3582
  try {
3584
- return await this.session.modelRegistry.getAvailable();
3583
+ await this.session.modelRuntime.refresh();
3584
+ return [...(await this.session.modelRuntime.getAvailable())];
3585
3585
  }
3586
3586
  catch {
3587
3587
  return [];
@@ -3603,14 +3603,13 @@ export class InteractiveMode {
3603
3603
  if (!model || model.provider !== "anthropic") {
3604
3604
  return;
3605
3605
  }
3606
- const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
3607
- if (storedCredential?.type === "oauth") {
3608
- this.anthropicSubscriptionWarningShown = true;
3609
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3610
- return;
3611
- }
3612
3606
  try {
3613
- const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
3607
+ if ((await this.session.modelRuntime.checkAuth("anthropic"))?.type === "oauth") {
3608
+ this.anthropicSubscriptionWarningShown = true;
3609
+ this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3610
+ return;
3611
+ }
3612
+ const apiKey = (await this.session.modelRuntime.getAuth(model.provider))?.auth.apiKey;
3614
3613
  if (!isAnthropicSubscriptionAuthKey(apiKey)) {
3615
3614
  return;
3616
3615
  }
@@ -3668,7 +3667,7 @@ export class InteractiveMode {
3668
3667
  }
3669
3668
  showModelSelector(initialSearchInput) {
3670
3669
  this.showSelector((done) => {
3671
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3670
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRuntime, this.session.scopedModels, async (model) => {
3672
3671
  try {
3673
3672
  await this.session.setModel(model);
3674
3673
  this.footer.invalidate();
@@ -3691,8 +3690,8 @@ export class InteractiveMode {
3691
3690
  }
3692
3691
  async showModelsSelector() {
3693
3692
  // Get all available models
3694
- this.session.modelRegistry.refresh();
3695
- const allModels = this.session.modelRegistry.getAvailable();
3693
+ await this.session.modelRuntime.refresh();
3694
+ const allModels = [...(await this.session.modelRuntime.getAvailable())];
3696
3695
  if (allModels.length === 0) {
3697
3696
  this.showStatus("No models available");
3698
3697
  return;
@@ -3710,7 +3709,7 @@ export class InteractiveMode {
3710
3709
  // Fall back to settings
3711
3710
  const patterns = this.settingsManager.getEnabledModels();
3712
3711
  if (patterns !== undefined && patterns.length > 0) {
3713
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3712
+ const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
3714
3713
  currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3715
3714
  }
3716
3715
  }
@@ -3718,7 +3717,7 @@ export class InteractiveMode {
3718
3717
  const updateSessionModels = async (enabledIds) => {
3719
3718
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3720
3719
  if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3721
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3720
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3722
3721
  this.session.setScopedModels(newScopedModels.map((sm) => ({
3723
3722
  model: sm.model,
3724
3723
  thinkingLevel: sm.thinkingLevel,
@@ -3901,6 +3900,19 @@ export class InteractiveMode {
3901
3900
  this.sessionManager.appendLabelChange(entryId, label);
3902
3901
  this.ui.requestRender();
3903
3902
  }, initialSelectedId, initialFilterMode);
3903
+ selector.onCopy = async (text) => {
3904
+ if (!text) {
3905
+ this.showError("Selected entry has no text to copy");
3906
+ return;
3907
+ }
3908
+ try {
3909
+ await copyToClipboard(text);
3910
+ this.showStatus("Copied selected message to clipboard");
3911
+ }
3912
+ catch (error) {
3913
+ this.showError(error instanceof Error ? error.message : String(error));
3914
+ }
3915
+ };
3904
3916
  return { component: selector, focus: selector };
3905
3917
  });
3906
3918
  }
@@ -3965,44 +3977,46 @@ export class InteractiveMode {
3965
3977
  }
3966
3978
  }
3967
3979
  getLoginProviderOptions(authType) {
3968
- const authStorage = this.session.modelRegistry.authStorage;
3969
- const oauthProviders = authStorage.getOAuthProviders();
3970
- const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
3971
- const options = oauthProviders.map((provider) => ({
3972
- id: provider.id,
3973
- name: provider.name,
3974
- authType: "oauth",
3975
- }));
3976
- const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
3977
- for (const providerId of modelProviders) {
3978
- if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
3979
- continue;
3980
- }
3981
- options.push({
3982
- id: providerId,
3983
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
3984
- authType: "api_key",
3985
- });
3986
- }
3987
- const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
3988
- return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
3989
- }
3990
- getLogoutProviderOptions() {
3991
- const authStorage = this.session.modelRegistry.authStorage;
3992
3980
  const options = [];
3993
- for (const providerId of authStorage.list()) {
3994
- const credential = authStorage.get(providerId);
3995
- if (!credential) {
3996
- continue;
3981
+ for (const provider of this.session.modelRuntime.getProviders()) {
3982
+ const authStatus = this.session.modelRuntime.getProviderAuthStatus(provider.id);
3983
+ const status = authStatus.configured
3984
+ ? {
3985
+ type: this.session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
3986
+ source: authStatus.label ?? authStatus.source,
3987
+ }
3988
+ : undefined;
3989
+ if ((!authType || authType === "oauth") && provider.auth.oauth) {
3990
+ options.push({
3991
+ id: provider.id,
3992
+ name: provider.name,
3993
+ authType: "oauth",
3994
+ method: provider.auth.oauth,
3995
+ status,
3996
+ });
3997
+ }
3998
+ if ((!authType || authType === "api_key") && provider.auth.apiKey) {
3999
+ options.push({
4000
+ id: provider.id,
4001
+ name: provider.name,
4002
+ authType: "api_key",
4003
+ method: provider.auth.apiKey,
4004
+ status,
4005
+ });
3997
4006
  }
3998
- options.push({
3999
- id: providerId,
4000
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4001
- authType: credential.type,
4002
- });
4003
4007
  }
4004
4008
  return options.sort((a, b) => a.name.localeCompare(b.name));
4005
4009
  }
4010
+ async getLogoutProviderOptions() {
4011
+ return (await this.session.modelRuntime.listCredentials())
4012
+ .map(({ providerId, type }) => ({
4013
+ id: providerId,
4014
+ name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
4015
+ authType: type,
4016
+ status: { type, source: "stored credential" },
4017
+ }))
4018
+ .sort((a, b) => a.name.localeCompare(b.name));
4019
+ }
4006
4020
  findLoginProviderOptions(providerRef) {
4007
4021
  const normalizedProviderRef = providerRef.trim().toLowerCase();
4008
4022
  if (!normalizedProviderRef) {
@@ -4012,6 +4026,7 @@ export class InteractiveMode {
4012
4026
  provider.name.toLowerCase() === normalizedProviderRef);
4013
4027
  }
4014
4028
  async handleLoginCommand(providerRef) {
4029
+ await this.session.modelRuntime.getAvailable();
4015
4030
  if (!providerRef) {
4016
4031
  this.showLoginAuthTypeSelector();
4017
4032
  return;
@@ -4034,16 +4049,16 @@ export class InteractiveMode {
4034
4049
  if (providerOption.authType === "oauth") {
4035
4050
  await this.showLoginDialog(providerOption.id, providerOption.name);
4036
4051
  }
4037
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4038
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4052
+ else if (providerOption.method?.login) {
4053
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4039
4054
  }
4040
4055
  else {
4041
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4056
+ this.showAmbientAuthDialog(providerOption);
4042
4057
  }
4043
4058
  }
4044
4059
  showLoginAuthTypeSelector(providerOptions) {
4045
- const subscriptionLabel = "Use a subscription";
4046
- const apiKeyLabel = "Use an API key";
4060
+ const subscriptionLabel = "Sign in with an account";
4061
+ const apiKeyLabel = "Sign in with an API key";
4047
4062
  const availableAuthTypes = providerOptions
4048
4063
  ? new Set(providerOptions.map((provider) => provider.authType))
4049
4064
  : new Set(["oauth", "api_key"]);
@@ -4099,7 +4114,7 @@ export class InteractiveMode {
4099
4114
  return;
4100
4115
  }
4101
4116
  this.showSelector((done) => {
4102
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
4117
+ const selector = new OAuthSelectorComponent("login", providerOptions, async (providerId, selectedAuthType) => {
4103
4118
  done();
4104
4119
  const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
4105
4120
  if (!providerOption) {
@@ -4114,7 +4129,7 @@ export class InteractiveMode {
4114
4129
  else {
4115
4130
  this.ui.requestRender();
4116
4131
  }
4117
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
4132
+ }, initialSearchInput);
4118
4133
  return { component: selector, focus: selector };
4119
4134
  });
4120
4135
  }
@@ -4123,21 +4138,20 @@ export class InteractiveMode {
4123
4138
  this.showLoginAuthTypeSelector();
4124
4139
  return;
4125
4140
  }
4126
- const providerOptions = this.getLogoutProviderOptions();
4141
+ const providerOptions = await this.getLogoutProviderOptions();
4127
4142
  if (providerOptions.length === 0) {
4128
4143
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4129
4144
  return;
4130
4145
  }
4131
4146
  this.showSelector((done) => {
4132
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4147
+ const selector = new OAuthSelectorComponent(mode, providerOptions, async (providerId) => {
4133
4148
  done();
4134
4149
  const providerOption = providerOptions.find((provider) => provider.id === providerId);
4135
4150
  if (!providerOption) {
4136
4151
  return;
4137
4152
  }
4138
4153
  try {
4139
- this.session.modelRegistry.authStorage.logout(providerOption.id);
4140
- this.session.modelRegistry.refresh();
4154
+ await this.session.modelRuntime.logout(providerOption.id);
4141
4155
  await this.updateAvailableProviderCount();
4142
4156
  const message = providerOption.authType === "oauth"
4143
4157
  ? `Logged out of ${providerOption.name}`
@@ -4155,12 +4169,12 @@ export class InteractiveMode {
4155
4169
  });
4156
4170
  }
4157
4171
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4158
- this.session.modelRegistry.refresh();
4172
+ await this.session.modelRuntime.getAvailable();
4159
4173
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4160
4174
  let selectedModel;
4161
4175
  let selectionError;
4162
4176
  if (isUnknownModel(previousModel)) {
4163
- const availableModels = this.session.modelRegistry.getAvailable();
4177
+ const availableModels = await this.session.modelRuntime.getAvailable();
4164
4178
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4165
4179
  if (!hasDefaultModelProvider(providerId)) {
4166
4180
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4204,20 +4218,15 @@ export class InteractiveMode {
4204
4218
  }
4205
4219
  }
4206
4220
  }
4207
- showBedrockSetupDialog(providerId, providerName) {
4221
+ showAmbientAuthDialog(providerOption) {
4208
4222
  const restoreEditor = () => {
4209
4223
  this.editorContainer.clear();
4210
4224
  this.editorContainer.addChild(this.editor);
4211
4225
  this.ui.setFocus(this.editor);
4212
4226
  this.ui.requestRender();
4213
4227
  };
4214
- const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
4215
- dialog.showInfo([
4216
- theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
4217
- theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
4218
- theme.fg("muted", "See:"),
4219
- theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4220
- ]);
4228
+ const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
4229
+ dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true);
4221
4230
  this.editorContainer.clear();
4222
4231
  this.editorContainer.addChild(dialog);
4223
4232
  this.ui.setFocus(dialog);
@@ -4228,6 +4237,13 @@ export class InteractiveMode {
4228
4237
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4229
4238
  // Completion handled below
4230
4239
  }, providerName);
4240
+ if (providerId === "amazon-bedrock") {
4241
+ dialog.showDetails([
4242
+ theme.fg("text", "You can also use an AWS profile, IAM keys, or role-based credentials."),
4243
+ theme.fg("muted", "See:"),
4244
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4245
+ ]);
4246
+ }
4231
4247
  this.editorContainer.clear();
4232
4248
  this.editorContainer.addChild(dialog);
4233
4249
  this.ui.setFocus(dialog);
@@ -4239,11 +4255,7 @@ export class InteractiveMode {
4239
4255
  this.ui.requestRender();
4240
4256
  };
4241
4257
  try {
4242
- const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
4243
- if (!apiKey) {
4244
- throw new Error("API key cannot be empty.");
4245
- }
4246
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
4258
+ await this.loginProvider(dialog, providerId, "api_key");
4247
4259
  restoreEditor();
4248
4260
  await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
4249
4261
  }
@@ -4255,8 +4267,8 @@ export class InteractiveMode {
4255
4267
  }
4256
4268
  }
4257
4269
  }
4258
- showOAuthLoginSelect(dialog, prompt) {
4259
- return new Promise((resolve) => {
4270
+ showAuthSelect(dialog, prompt) {
4271
+ return new Promise((resolve, reject) => {
4260
4272
  const restoreDialog = () => {
4261
4273
  this.editorContainer.clear();
4262
4274
  this.editorContainer.addChild(dialog);
@@ -4266,10 +4278,14 @@ export class InteractiveMode {
4266
4278
  const labels = prompt.options.map((option) => option.label);
4267
4279
  const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
4268
4280
  restoreDialog();
4269
- resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
4281
+ const id = prompt.options.find((option) => option.label === optionLabel)?.id;
4282
+ if (id)
4283
+ resolve(id);
4284
+ else
4285
+ reject(new Error("Login cancelled"));
4270
4286
  }, () => {
4271
4287
  restoreDialog();
4272
- resolve(undefined);
4288
+ reject(new Error("Login cancelled"));
4273
4289
  });
4274
4290
  this.editorContainer.clear();
4275
4291
  this.editorContainer.addChild(selector);
@@ -4277,30 +4293,64 @@ export class InteractiveMode {
4277
4293
  this.ui.requestRender();
4278
4294
  });
4279
4295
  }
4296
+ async showAuthPrompt(dialog, prompt) {
4297
+ let response;
4298
+ if (prompt.type === "select") {
4299
+ response = this.showAuthSelect(dialog, prompt);
4300
+ }
4301
+ else if (prompt.type === "manual_code") {
4302
+ response = dialog.showManualInput(prompt.message);
4303
+ }
4304
+ else {
4305
+ response = dialog.showPrompt(prompt.message, prompt.placeholder);
4306
+ }
4307
+ if (!prompt.signal)
4308
+ return response;
4309
+ if (prompt.signal.aborted)
4310
+ throw new Error("Login cancelled");
4311
+ const signal = prompt.signal;
4312
+ let onAbort;
4313
+ const aborted = new Promise((_resolve, reject) => {
4314
+ onAbort = () => reject(new Error("Login cancelled"));
4315
+ signal.addEventListener("abort", onAbort, { once: true });
4316
+ });
4317
+ try {
4318
+ return await Promise.race([response, aborted]);
4319
+ }
4320
+ finally {
4321
+ if (onAbort)
4322
+ signal.removeEventListener("abort", onAbort);
4323
+ }
4324
+ }
4325
+ notifyAuthDialog(dialog, event) {
4326
+ if (event.type === "auth_url") {
4327
+ dialog.showAuth(event.url, event.instructions);
4328
+ }
4329
+ else if (event.type === "device_code") {
4330
+ dialog.showDeviceCode(event);
4331
+ dialog.showWaiting("Waiting for authentication...");
4332
+ }
4333
+ else if (event.type === "info") {
4334
+ dialog.showInfo(event.message, event.links);
4335
+ }
4336
+ else {
4337
+ dialog.showProgress(event.message);
4338
+ }
4339
+ }
4340
+ async loginProvider(dialog, providerId, method) {
4341
+ await this.session.modelRuntime.login(providerId, method, {
4342
+ signal: dialog.signal,
4343
+ prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
4344
+ notify: (event) => this.notifyAuthDialog(dialog, event),
4345
+ });
4346
+ }
4280
4347
  async showLoginDialog(providerId, providerName) {
4281
- const providerInfo = this.session.modelRegistry.authStorage
4282
- .getOAuthProviders()
4283
- .find((provider) => provider.id === providerId);
4284
4348
  const previousModel = this.session.model;
4285
- // Providers that use callback servers (can paste redirect URL)
4286
- const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
4287
- // Create login dialog component
4288
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4289
- // Completion handled below
4290
- }, providerName);
4291
- // Show dialog in editor container
4349
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => { }, providerName);
4292
4350
  this.editorContainer.clear();
4293
4351
  this.editorContainer.addChild(dialog);
4294
4352
  this.ui.setFocus(dialog);
4295
4353
  this.ui.requestRender();
4296
- // Promise for manual code input (racing with callback server)
4297
- let manualCodeResolve;
4298
- let manualCodeReject;
4299
- const manualCodePromise = new Promise((resolve, reject) => {
4300
- manualCodeResolve = resolve;
4301
- manualCodeReject = reject;
4302
- });
4303
- // Restore editor helper
4304
4354
  const restoreEditor = () => {
4305
4355
  this.editorContainer.clear();
4306
4356
  this.editorContainer.addChild(this.editor);
@@ -4308,43 +4358,7 @@ export class InteractiveMode {
4308
4358
  this.ui.requestRender();
4309
4359
  };
4310
4360
  try {
4311
- await this.session.modelRegistry.authStorage.login(providerId, {
4312
- onAuth: (info) => {
4313
- dialog.showAuth(info.url, info.instructions);
4314
- if (usesCallbackServer) {
4315
- // Show input for manual paste, racing with callback
4316
- dialog
4317
- .showManualInput("Paste redirect URL below, or complete login in browser:")
4318
- .then((value) => {
4319
- if (value && manualCodeResolve) {
4320
- manualCodeResolve(value);
4321
- manualCodeResolve = undefined;
4322
- }
4323
- })
4324
- .catch(() => {
4325
- if (manualCodeReject) {
4326
- manualCodeReject(new Error("Login cancelled"));
4327
- manualCodeReject = undefined;
4328
- }
4329
- });
4330
- }
4331
- // For Anthropic: onPrompt is called immediately after
4332
- },
4333
- onDeviceCode: (info) => {
4334
- dialog.showDeviceCode(info);
4335
- dialog.showWaiting("Waiting for authentication...");
4336
- },
4337
- onPrompt: async (prompt) => {
4338
- return dialog.showPrompt(prompt.message, prompt.placeholder);
4339
- },
4340
- onProgress: (message) => {
4341
- dialog.showProgress(message);
4342
- },
4343
- onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
4344
- onManualCodeInput: () => manualCodePromise,
4345
- signal: dialog.signal,
4346
- });
4347
- // Success
4361
+ await this.loginProvider(dialog, providerId, "oauth");
4348
4362
  restoreEditor();
4349
4363
  await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
4350
4364
  }
@@ -4432,7 +4446,7 @@ export class InteractiveMode {
4432
4446
  showDiagnosticsWhenQuiet: true,
4433
4447
  });
4434
4448
  const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
4435
- const modelsJsonError = this.session.modelRegistry.getError();
4449
+ const modelsJsonError = this.session.modelRuntime.getError();
4436
4450
  if (modelsJsonError) {
4437
4451
  this.showError(`models.json error: ${modelsJsonError}`);
4438
4452
  }
@@ -4660,7 +4674,7 @@ export class InteractiveMode {
4660
4674
  const stats = this.session.getSessionStats();
4661
4675
  const sessionName = this.sessionManager.getSessionName();
4662
4676
  const entries = this.sessionManager.getEntries();
4663
- const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4677
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
4664
4678
  // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4665
4679
  // resolves to a concrete responseModel), sorted by cost descending.
4666
4680
  const perModelMap = new Map();
@@ -4793,6 +4807,7 @@ export class InteractiveMode {
4793
4807
  const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
4794
4808
  const externalEditor = this.getAppKeyDisplay("app.editor.external");
4795
4809
  const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
4810
+ const copyMessage = this.getAppKeyDisplay("app.message.copy");
4796
4811
  const followUp = this.getAppKeyDisplay("app.message.followUp");
4797
4812
  const dequeue = this.getAppKeyDisplay("app.message.dequeue");
4798
4813
  const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
@@ -4835,9 +4850,10 @@ export class InteractiveMode {
4835
4850
  | \`${expandTools}\` | Toggle tool output expansion |
4836
4851
  | \`${toggleThinking}\` | Toggle thinking block visibility |
4837
4852
  | \`${externalEditor}\` | Edit message in external editor |
4853
+ | \`${copyMessage}\` | Copy last assistant message |
4838
4854
  | \`${followUp}\` | Queue follow-up message |
4839
4855
  | \`${dequeue}\` | Restore queued messages |
4840
- | \`${pasteImage}\` | Paste image from clipboard |
4856
+ | \`${pasteImage}\` | Paste image or text from clipboard |
4841
4857
  | \`/\` | Slash commands |
4842
4858
  | \`!\` | Run bash command |
4843
4859
  | \`!!\` | Run bash command (excluded from context) |