@earendil-works/pi-coding-agent 0.80.7 → 0.80.9

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 (145) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +6 -6
  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-runtime.d.ts.map +1 -1
  14. package/dist/core/agent-session-runtime.js +3 -0
  15. package/dist/core/agent-session-runtime.js.map +1 -1
  16. package/dist/core/agent-session-services.d.ts +3 -6
  17. package/dist/core/agent-session-services.d.ts.map +1 -1
  18. package/dist/core/agent-session-services.js +10 -9
  19. package/dist/core/agent-session-services.js.map +1 -1
  20. package/dist/core/agent-session.d.ts +5 -6
  21. package/dist/core/agent-session.d.ts.map +1 -1
  22. package/dist/core/agent-session.js +57 -30
  23. package/dist/core/agent-session.js.map +1 -1
  24. package/dist/core/auth-storage.d.ts +15 -99
  25. package/dist/core/auth-storage.d.ts.map +1 -1
  26. package/dist/core/auth-storage.js +46 -259
  27. package/dist/core/auth-storage.js.map +1 -1
  28. package/dist/core/cache-stats.d.ts +2 -2
  29. package/dist/core/cache-stats.d.ts.map +1 -1
  30. package/dist/core/cache-stats.js +1 -1
  31. package/dist/core/cache-stats.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 +1 -0
  36. package/dist/core/extensions/runner.d.ts.map +1 -1
  37. package/dist/core/extensions/runner.js +3 -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/model-config.d.ts +512 -0
  43. package/dist/core/model-config.d.ts.map +1 -0
  44. package/dist/core/model-config.js +243 -0
  45. package/dist/core/model-config.js.map +1 -0
  46. package/dist/core/model-registry.d.ts +13 -123
  47. package/dist/core/model-registry.d.ts.map +1 -1
  48. package/dist/core/model-registry.js +44 -747
  49. package/dist/core/model-registry.js.map +1 -1
  50. package/dist/core/model-resolver.d.ts +6 -6
  51. package/dist/core/model-resolver.d.ts.map +1 -1
  52. package/dist/core/model-resolver.js +18 -18
  53. package/dist/core/model-resolver.js.map +1 -1
  54. package/dist/core/model-runtime.d.ts +77 -0
  55. package/dist/core/model-runtime.d.ts.map +1 -0
  56. package/dist/core/model-runtime.js +418 -0
  57. package/dist/core/model-runtime.js.map +1 -0
  58. package/dist/core/models-store.d.ts +17 -0
  59. package/dist/core/models-store.d.ts.map +1 -0
  60. package/dist/core/models-store.js +45 -0
  61. package/dist/core/models-store.js.map +1 -0
  62. package/dist/core/provider-composer.d.ts +55 -0
  63. package/dist/core/provider-composer.d.ts.map +1 -0
  64. package/dist/core/provider-composer.js +375 -0
  65. package/dist/core/provider-composer.js.map +1 -0
  66. package/dist/core/radius.d.ts +0 -19
  67. package/dist/core/radius.d.ts.map +1 -1
  68. package/dist/core/radius.js +0 -28
  69. package/dist/core/radius.js.map +1 -1
  70. package/dist/core/remote-catalog-provider.d.ts +5 -0
  71. package/dist/core/remote-catalog-provider.d.ts.map +1 -0
  72. package/dist/core/remote-catalog-provider.js +83 -0
  73. package/dist/core/remote-catalog-provider.js.map +1 -0
  74. package/dist/core/runtime-credentials.d.ts +15 -0
  75. package/dist/core/runtime-credentials.d.ts.map +1 -0
  76. package/dist/core/runtime-credentials.js +36 -0
  77. package/dist/core/runtime-credentials.js.map +1 -0
  78. package/dist/core/sdk.d.ts +3 -6
  79. package/dist/core/sdk.d.ts.map +1 -1
  80. package/dist/core/sdk.js +14 -25
  81. package/dist/core/sdk.js.map +1 -1
  82. package/dist/index.d.ts +2 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/main.d.ts.map +1 -1
  87. package/dist/main.js +11 -13
  88. package/dist/main.js.map +1 -1
  89. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/assistant-message.js +22 -10
  91. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  92. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  93. package/dist/modes/interactive/components/footer.js +1 -1
  94. package/dist/modes/interactive/components/footer.js.map +1 -1
  95. package/dist/modes/interactive/components/login-dialog.d.ts +4 -8
  96. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  97. package/dist/modes/interactive/components/login-dialog.js +14 -12
  98. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  99. package/dist/modes/interactive/components/model-selector.d.ts +11 -4
  100. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  101. package/dist/modes/interactive/components/model-selector.js +72 -41
  102. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  103. package/dist/modes/interactive/components/oauth-selector.d.ts +4 -4
  104. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/oauth-selector.js +14 -27
  106. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  107. package/dist/modes/interactive/interactive-mode.d.ts +6 -3
  108. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.js +157 -143
  110. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  112. package/dist/modes/rpc/rpc-mode.js +2 -2
  113. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  114. package/dist/package-manager-cli.d.ts.map +1 -1
  115. package/dist/package-manager-cli.js +67 -5
  116. package/dist/package-manager-cli.js.map +1 -1
  117. package/docs/custom-provider.md +6 -11
  118. package/docs/extensions.md +22 -1
  119. package/docs/models.md +2 -0
  120. package/docs/packages.md +1 -0
  121. package/docs/providers.md +13 -1
  122. package/docs/sdk.md +40 -50
  123. package/docs/usage.md +1 -0
  124. package/examples/extensions/README.md +1 -0
  125. package/examples/extensions/custom-provider-anthropic/index.ts +1 -1
  126. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  127. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  128. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  129. package/examples/extensions/gondolin/package-lock.json +2 -2
  130. package/examples/extensions/gondolin/package.json +1 -1
  131. package/examples/extensions/kimi-deferred-tools.ts +61 -0
  132. package/examples/extensions/sandbox/package-lock.json +2 -2
  133. package/examples/extensions/sandbox/package.json +1 -1
  134. package/examples/extensions/with-deps/package-lock.json +2 -2
  135. package/examples/extensions/with-deps/package.json +1 -1
  136. package/examples/sdk/02-custom-model.ts +5 -8
  137. package/examples/sdk/09-api-keys-and-oauth.ts +13 -31
  138. package/examples/sdk/12-full-control.ts +7 -12
  139. package/examples/sdk/README.md +14 -18
  140. package/npm-shrinkwrap.json +12 -12
  141. package/package.json +4 -4
  142. package/dist/core/provider-display-names.d.ts +0 -2
  143. package/dist/core/provider-display-names.d.ts.map +0 -1
  144. package/dist/core/provider-display-names.js +0 -37
  145. 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,7 +19,6 @@ 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";
@@ -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,16 +123,6 @@ export function formatResumeCommand(sessionManager) {
125
123
  function hasDefaultModelProvider(providerId) {
126
124
  return providerId in defaultModelPerProvider;
127
125
  }
128
- const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
129
- export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
130
- if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
131
- return true;
132
- }
133
- if (builtInProviderIds.has(providerId)) {
134
- return false;
135
- }
136
- return !oauthProviderIds.has(providerId);
137
- }
138
126
  const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
139
127
  function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
140
128
  const filtered = fuzzyFilter(items, prefix, getSearchText);
@@ -361,11 +349,11 @@ export class InteractiveMode {
361
349
  }));
362
350
  const modelCommand = slashCommands.find((command) => command.name === "model");
363
351
  if (modelCommand) {
364
- modelCommand.getArgumentCompletions = (prefix) => {
352
+ modelCommand.getArgumentCompletions = async (prefix) => {
365
353
  // Get available models (scoped or from registry)
366
354
  const models = this.session.scopedModels.length > 0
367
355
  ? this.session.scopedModels.map((s) => s.model)
368
- : this.session.modelRegistry.getAvailable();
356
+ : await this.session.modelRuntime.getAvailable();
369
357
  if (models.length === 0)
370
358
  return null;
371
359
  // Create items with provider/id format
@@ -598,10 +586,18 @@ export class InteractiveMode {
598
586
  }
599
587
  });
600
588
  // Start package update check asynchronously
601
- this.checkForPackageUpdates().then((updates) => {
589
+ this.checkForPackageUpdates()
590
+ .then((updates) => {
602
591
  if (updates.length > 0) {
603
592
  this.showPackageUpdateNotification(updates);
604
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
+ }
605
601
  });
606
602
  // Check tmux keyboard setup asynchronously
607
603
  this.checkTmuxKeyboardSetup().then((warning) => {
@@ -614,7 +610,7 @@ export class InteractiveMode {
614
610
  if (migratedProviders && migratedProviders.length > 0) {
615
611
  this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
616
612
  }
617
- const modelsJsonError = this.session.modelRegistry.getError();
613
+ const modelsJsonError = this.session.modelRuntime.getError();
618
614
  if (modelsJsonError) {
619
615
  this.showError(`models.json error: ${modelsJsonError}`);
620
616
  }
@@ -1345,7 +1341,7 @@ export class InteractiveMode {
1345
1341
  hasUI: true,
1346
1342
  cwd: this.sessionManager.getCwd(),
1347
1343
  sessionManager: this.sessionManager,
1348
- modelRegistry: this.session.modelRegistry,
1344
+ modelRegistry: extensionRunner.getModelRegistry(),
1349
1345
  model: this.session.model,
1350
1346
  isIdle: () => this.session.isIdle,
1351
1347
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
@@ -2651,7 +2647,7 @@ export class InteractiveMode {
2651
2647
  // Cache-miss notices are not persisted; re-derive them from the full entry
2652
2648
  // list and re-inject them after the assistant messages that paid for them.
2653
2649
  const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2654
- ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2650
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
2655
2651
  : new Map();
2656
2652
  if (options.updateFooter) {
2657
2653
  this.footer.invalidate();
@@ -2742,7 +2738,7 @@ export class InteractiveMode {
2742
2738
  if (!this.settingsManager.getShowCacheMissNotices())
2743
2739
  return;
2744
2740
  // Entries don't contain `message` yet: message_end fires before persistence.
2745
- const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2741
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
2746
2742
  if (miss)
2747
2743
  this.addCacheMissNotice(miss);
2748
2744
  }
@@ -3583,9 +3579,9 @@ export class InteractiveMode {
3583
3579
  if (this.session.scopedModels.length > 0) {
3584
3580
  return this.session.scopedModels.map((scoped) => scoped.model);
3585
3581
  }
3586
- this.session.modelRegistry.refresh();
3587
3582
  try {
3588
- return await this.session.modelRegistry.getAvailable();
3583
+ await this.session.modelRuntime.refresh();
3584
+ return [...(await this.session.modelRuntime.getAvailable())];
3589
3585
  }
3590
3586
  catch {
3591
3587
  return [];
@@ -3607,14 +3603,13 @@ export class InteractiveMode {
3607
3603
  if (!model || model.provider !== "anthropic") {
3608
3604
  return;
3609
3605
  }
3610
- const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
3611
- if (storedCredential?.type === "oauth") {
3612
- this.anthropicSubscriptionWarningShown = true;
3613
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3614
- return;
3615
- }
3616
3606
  try {
3617
- 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;
3618
3613
  if (!isAnthropicSubscriptionAuthKey(apiKey)) {
3619
3614
  return;
3620
3615
  }
@@ -3672,7 +3667,7 @@ export class InteractiveMode {
3672
3667
  }
3673
3668
  showModelSelector(initialSearchInput) {
3674
3669
  this.showSelector((done) => {
3675
- 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) => {
3676
3671
  try {
3677
3672
  await this.session.setModel(model);
3678
3673
  this.footer.invalidate();
@@ -3695,8 +3690,8 @@ export class InteractiveMode {
3695
3690
  }
3696
3691
  async showModelsSelector() {
3697
3692
  // Get all available models
3698
- this.session.modelRegistry.refresh();
3699
- const allModels = this.session.modelRegistry.getAvailable();
3693
+ await this.session.modelRuntime.refresh();
3694
+ const allModels = [...(await this.session.modelRuntime.getAvailable())];
3700
3695
  if (allModels.length === 0) {
3701
3696
  this.showStatus("No models available");
3702
3697
  return;
@@ -3714,7 +3709,7 @@ export class InteractiveMode {
3714
3709
  // Fall back to settings
3715
3710
  const patterns = this.settingsManager.getEnabledModels();
3716
3711
  if (patterns !== undefined && patterns.length > 0) {
3717
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3712
+ const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
3718
3713
  currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3719
3714
  }
3720
3715
  }
@@ -3722,7 +3717,7 @@ export class InteractiveMode {
3722
3717
  const updateSessionModels = async (enabledIds) => {
3723
3718
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3724
3719
  if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3725
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3720
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3726
3721
  this.session.setScopedModels(newScopedModels.map((sm) => ({
3727
3722
  model: sm.model,
3728
3723
  thinkingLevel: sm.thinkingLevel,
@@ -3982,44 +3977,46 @@ export class InteractiveMode {
3982
3977
  }
3983
3978
  }
3984
3979
  getLoginProviderOptions(authType) {
3985
- const authStorage = this.session.modelRegistry.authStorage;
3986
- const oauthProviders = authStorage.getOAuthProviders();
3987
- const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
3988
- const options = oauthProviders.map((provider) => ({
3989
- id: provider.id,
3990
- name: provider.name,
3991
- authType: "oauth",
3992
- }));
3993
- const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
3994
- for (const providerId of modelProviders) {
3995
- if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
3996
- continue;
3997
- }
3998
- options.push({
3999
- id: providerId,
4000
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4001
- authType: "api_key",
4002
- });
4003
- }
4004
- const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
4005
- return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
4006
- }
4007
- getLogoutProviderOptions() {
4008
- const authStorage = this.session.modelRegistry.authStorage;
4009
3980
  const options = [];
4010
- for (const providerId of authStorage.list()) {
4011
- const credential = authStorage.get(providerId);
4012
- if (!credential) {
4013
- 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
+ });
4014
4006
  }
4015
- options.push({
4016
- id: providerId,
4017
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4018
- authType: credential.type,
4019
- });
4020
4007
  }
4021
4008
  return options.sort((a, b) => a.name.localeCompare(b.name));
4022
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
+ }
4023
4020
  findLoginProviderOptions(providerRef) {
4024
4021
  const normalizedProviderRef = providerRef.trim().toLowerCase();
4025
4022
  if (!normalizedProviderRef) {
@@ -4029,6 +4026,7 @@ export class InteractiveMode {
4029
4026
  provider.name.toLowerCase() === normalizedProviderRef);
4030
4027
  }
4031
4028
  async handleLoginCommand(providerRef) {
4029
+ await this.session.modelRuntime.getAvailable();
4032
4030
  if (!providerRef) {
4033
4031
  this.showLoginAuthTypeSelector();
4034
4032
  return;
@@ -4051,12 +4049,17 @@ export class InteractiveMode {
4051
4049
  if (providerOption.authType === "oauth") {
4052
4050
  await this.showLoginDialog(providerOption.id, providerOption.name);
4053
4051
  }
4054
- else {
4052
+ else if (providerOption.method?.login) {
4055
4053
  await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4056
4054
  }
4055
+ else {
4056
+ this.showAmbientAuthDialog(providerOption);
4057
+ }
4057
4058
  }
4058
4059
  showLoginAuthTypeSelector(providerOptions) {
4059
- const subscriptionLabel = "Sign in with an account";
4060
+ const oauthProvider = providerOptions?.find((provider) => provider.authType === "oauth");
4061
+ const oauthLoginLabel = oauthProvider?.method && "loginLabel" in oauthProvider.method ? oauthProvider.method.loginLabel : undefined;
4062
+ const subscriptionLabel = oauthLoginLabel ?? "Sign in with an account";
4060
4063
  const apiKeyLabel = "Sign in with an API key";
4061
4064
  const availableAuthTypes = providerOptions
4062
4065
  ? new Set(providerOptions.map((provider) => provider.authType))
@@ -4113,7 +4116,7 @@ export class InteractiveMode {
4113
4116
  return;
4114
4117
  }
4115
4118
  this.showSelector((done) => {
4116
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
4119
+ const selector = new OAuthSelectorComponent("login", providerOptions, async (providerId, selectedAuthType) => {
4117
4120
  done();
4118
4121
  const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
4119
4122
  if (!providerOption) {
@@ -4128,7 +4131,7 @@ export class InteractiveMode {
4128
4131
  else {
4129
4132
  this.ui.requestRender();
4130
4133
  }
4131
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
4134
+ }, initialSearchInput);
4132
4135
  return { component: selector, focus: selector };
4133
4136
  });
4134
4137
  }
@@ -4137,21 +4140,20 @@ export class InteractiveMode {
4137
4140
  this.showLoginAuthTypeSelector();
4138
4141
  return;
4139
4142
  }
4140
- const providerOptions = this.getLogoutProviderOptions();
4143
+ const providerOptions = await this.getLogoutProviderOptions();
4141
4144
  if (providerOptions.length === 0) {
4142
4145
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4143
4146
  return;
4144
4147
  }
4145
4148
  this.showSelector((done) => {
4146
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4149
+ const selector = new OAuthSelectorComponent(mode, providerOptions, async (providerId) => {
4147
4150
  done();
4148
4151
  const providerOption = providerOptions.find((provider) => provider.id === providerId);
4149
4152
  if (!providerOption) {
4150
4153
  return;
4151
4154
  }
4152
4155
  try {
4153
- this.session.modelRegistry.authStorage.logout(providerOption.id);
4154
- this.session.modelRegistry.refresh();
4156
+ await this.session.modelRuntime.logout(providerOption.id);
4155
4157
  await this.updateAvailableProviderCount();
4156
4158
  const message = providerOption.authType === "oauth"
4157
4159
  ? `Logged out of ${providerOption.name}`
@@ -4169,12 +4171,12 @@ export class InteractiveMode {
4169
4171
  });
4170
4172
  }
4171
4173
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4172
- this.session.modelRegistry.refresh();
4174
+ await this.session.modelRuntime.getAvailable();
4173
4175
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4174
4176
  let selectedModel;
4175
4177
  let selectionError;
4176
4178
  if (isUnknownModel(previousModel)) {
4177
- const availableModels = this.session.modelRegistry.getAvailable();
4179
+ const availableModels = await this.session.modelRuntime.getAvailable();
4178
4180
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4179
4181
  if (!hasDefaultModelProvider(providerId)) {
4180
4182
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4218,6 +4220,20 @@ export class InteractiveMode {
4218
4220
  }
4219
4221
  }
4220
4222
  }
4223
+ showAmbientAuthDialog(providerOption) {
4224
+ const restoreEditor = () => {
4225
+ this.editorContainer.clear();
4226
+ this.editorContainer.addChild(this.editor);
4227
+ this.ui.setFocus(this.editor);
4228
+ this.ui.requestRender();
4229
+ };
4230
+ const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
4231
+ dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true);
4232
+ this.editorContainer.clear();
4233
+ this.editorContainer.addChild(dialog);
4234
+ this.ui.setFocus(dialog);
4235
+ this.ui.requestRender();
4236
+ }
4221
4237
  async showApiKeyLoginDialog(providerId, providerName) {
4222
4238
  const previousModel = this.session.model;
4223
4239
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
@@ -4241,11 +4257,7 @@ export class InteractiveMode {
4241
4257
  this.ui.requestRender();
4242
4258
  };
4243
4259
  try {
4244
- const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
4245
- if (!apiKey) {
4246
- throw new Error("API key cannot be empty.");
4247
- }
4248
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
4260
+ await this.loginProvider(dialog, providerId, "api_key");
4249
4261
  restoreEditor();
4250
4262
  await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
4251
4263
  }
@@ -4257,8 +4269,8 @@ export class InteractiveMode {
4257
4269
  }
4258
4270
  }
4259
4271
  }
4260
- showOAuthLoginSelect(dialog, prompt) {
4261
- return new Promise((resolve) => {
4272
+ showAuthSelect(dialog, prompt) {
4273
+ return new Promise((resolve, reject) => {
4262
4274
  const restoreDialog = () => {
4263
4275
  this.editorContainer.clear();
4264
4276
  this.editorContainer.addChild(dialog);
@@ -4268,10 +4280,14 @@ export class InteractiveMode {
4268
4280
  const labels = prompt.options.map((option) => option.label);
4269
4281
  const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
4270
4282
  restoreDialog();
4271
- resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
4283
+ const id = prompt.options.find((option) => option.label === optionLabel)?.id;
4284
+ if (id)
4285
+ resolve(id);
4286
+ else
4287
+ reject(new Error("Login cancelled"));
4272
4288
  }, () => {
4273
4289
  restoreDialog();
4274
- resolve(undefined);
4290
+ reject(new Error("Login cancelled"));
4275
4291
  });
4276
4292
  this.editorContainer.clear();
4277
4293
  this.editorContainer.addChild(selector);
@@ -4279,30 +4295,64 @@ export class InteractiveMode {
4279
4295
  this.ui.requestRender();
4280
4296
  });
4281
4297
  }
4298
+ async showAuthPrompt(dialog, prompt) {
4299
+ let response;
4300
+ if (prompt.type === "select") {
4301
+ response = this.showAuthSelect(dialog, prompt);
4302
+ }
4303
+ else if (prompt.type === "manual_code") {
4304
+ response = dialog.showManualInput(prompt.message);
4305
+ }
4306
+ else {
4307
+ response = dialog.showPrompt(prompt.message, prompt.placeholder);
4308
+ }
4309
+ if (!prompt.signal)
4310
+ return response;
4311
+ if (prompt.signal.aborted)
4312
+ throw new Error("Login cancelled");
4313
+ const signal = prompt.signal;
4314
+ let onAbort;
4315
+ const aborted = new Promise((_resolve, reject) => {
4316
+ onAbort = () => reject(new Error("Login cancelled"));
4317
+ signal.addEventListener("abort", onAbort, { once: true });
4318
+ });
4319
+ try {
4320
+ return await Promise.race([response, aborted]);
4321
+ }
4322
+ finally {
4323
+ if (onAbort)
4324
+ signal.removeEventListener("abort", onAbort);
4325
+ }
4326
+ }
4327
+ notifyAuthDialog(dialog, event) {
4328
+ if (event.type === "auth_url") {
4329
+ dialog.showAuth(event.url, event.instructions);
4330
+ }
4331
+ else if (event.type === "device_code") {
4332
+ dialog.showDeviceCode(event);
4333
+ dialog.showWaiting("Waiting for authentication...");
4334
+ }
4335
+ else if (event.type === "info") {
4336
+ dialog.showInfo(event.message, event.links);
4337
+ }
4338
+ else {
4339
+ dialog.showProgress(event.message);
4340
+ }
4341
+ }
4342
+ async loginProvider(dialog, providerId, method) {
4343
+ await this.session.modelRuntime.login(providerId, method, {
4344
+ signal: dialog.signal,
4345
+ prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
4346
+ notify: (event) => this.notifyAuthDialog(dialog, event),
4347
+ });
4348
+ }
4282
4349
  async showLoginDialog(providerId, providerName) {
4283
- const providerInfo = this.session.modelRegistry.authStorage
4284
- .getOAuthProviders()
4285
- .find((provider) => provider.id === providerId);
4286
4350
  const previousModel = this.session.model;
4287
- // Providers that use callback servers (can paste redirect URL)
4288
- const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
4289
- // Create login dialog component
4290
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4291
- // Completion handled below
4292
- }, providerName);
4293
- // Show dialog in editor container
4351
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => { }, providerName);
4294
4352
  this.editorContainer.clear();
4295
4353
  this.editorContainer.addChild(dialog);
4296
4354
  this.ui.setFocus(dialog);
4297
4355
  this.ui.requestRender();
4298
- // Promise for manual code input (racing with callback server)
4299
- let manualCodeResolve;
4300
- let manualCodeReject;
4301
- const manualCodePromise = new Promise((resolve, reject) => {
4302
- manualCodeResolve = resolve;
4303
- manualCodeReject = reject;
4304
- });
4305
- // Restore editor helper
4306
4356
  const restoreEditor = () => {
4307
4357
  this.editorContainer.clear();
4308
4358
  this.editorContainer.addChild(this.editor);
@@ -4310,43 +4360,7 @@ export class InteractiveMode {
4310
4360
  this.ui.requestRender();
4311
4361
  };
4312
4362
  try {
4313
- await this.session.modelRegistry.authStorage.login(providerId, {
4314
- onAuth: (info) => {
4315
- dialog.showAuth(info.url, info.instructions);
4316
- if (usesCallbackServer) {
4317
- // Show input for manual paste, racing with callback
4318
- dialog
4319
- .showManualInput("Paste redirect URL below, or complete login in browser:")
4320
- .then((value) => {
4321
- if (value && manualCodeResolve) {
4322
- manualCodeResolve(value);
4323
- manualCodeResolve = undefined;
4324
- }
4325
- })
4326
- .catch(() => {
4327
- if (manualCodeReject) {
4328
- manualCodeReject(new Error("Login cancelled"));
4329
- manualCodeReject = undefined;
4330
- }
4331
- });
4332
- }
4333
- // For Anthropic: onPrompt is called immediately after
4334
- },
4335
- onDeviceCode: (info) => {
4336
- dialog.showDeviceCode(info);
4337
- dialog.showWaiting("Waiting for authentication...");
4338
- },
4339
- onPrompt: async (prompt) => {
4340
- return dialog.showPrompt(prompt.message, prompt.placeholder);
4341
- },
4342
- onProgress: (message) => {
4343
- dialog.showProgress(message);
4344
- },
4345
- onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
4346
- onManualCodeInput: () => manualCodePromise,
4347
- signal: dialog.signal,
4348
- });
4349
- // Success
4363
+ await this.loginProvider(dialog, providerId, "oauth");
4350
4364
  restoreEditor();
4351
4365
  await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
4352
4366
  }
@@ -4434,7 +4448,7 @@ export class InteractiveMode {
4434
4448
  showDiagnosticsWhenQuiet: true,
4435
4449
  });
4436
4450
  const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
4437
- const modelsJsonError = this.session.modelRegistry.getError();
4451
+ const modelsJsonError = this.session.modelRuntime.getError();
4438
4452
  if (modelsJsonError) {
4439
4453
  this.showError(`models.json error: ${modelsJsonError}`);
4440
4454
  }
@@ -4662,7 +4676,7 @@ export class InteractiveMode {
4662
4676
  const stats = this.session.getSessionStats();
4663
4677
  const sessionName = this.sessionManager.getSessionName();
4664
4678
  const entries = this.sessionManager.getEntries();
4665
- const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4679
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
4666
4680
  // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4667
4681
  // resolves to a concrete responseModel), sorted by cost descending.
4668
4682
  const perModelMap = new Map();