@averyyy/pi-coding-agent 0.80.7-piclient.1 → 0.80.10-piclient.2

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 (144) hide show
  1. package/CHANGELOG.md +77 -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 +37 -33
  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/package.json +4 -4
  141. package/dist/core/provider-display-names.d.ts +0 -2
  142. package/dist/core/provider-display-names.d.ts.map +0 -1
  143. package/dist/core/provider-display-names.js +0 -37
  144. 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";
@@ -21,7 +20,6 @@ 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
22
  import { getPiClientUpdateMarkerPath, PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE, readPiClientUpdateMarker, writePiClientRuntimeReloadState, } from "../../core/pi-client-runtime-update.js";
24
- import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
25
23
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
26
24
  import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
27
25
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -95,7 +93,7 @@ function isDeadTerminalError(error) {
95
93
  const code = error.code;
96
94
  return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
97
95
  }
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.";
96
+ 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
97
  function isAnthropicSubscriptionAuthKey(apiKey) {
100
98
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
101
99
  }
@@ -126,16 +124,6 @@ export function formatResumeCommand(sessionManager) {
126
124
  function hasDefaultModelProvider(providerId) {
127
125
  return providerId in defaultModelPerProvider;
128
126
  }
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
127
  const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
140
128
  function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
141
129
  const filtered = fuzzyFilter(items, prefix, getSearchText);
@@ -362,11 +350,11 @@ export class InteractiveMode {
362
350
  }));
363
351
  const modelCommand = slashCommands.find((command) => command.name === "model");
364
352
  if (modelCommand) {
365
- modelCommand.getArgumentCompletions = (prefix) => {
353
+ modelCommand.getArgumentCompletions = async (prefix) => {
366
354
  // Get available models (scoped or from registry)
367
355
  const models = this.session.scopedModels.length > 0
368
356
  ? this.session.scopedModels.map((s) => s.model)
369
- : this.session.modelRegistry.getAvailable();
357
+ : await this.session.modelRuntime.getAvailable();
370
358
  if (models.length === 0)
371
359
  return null;
372
360
  // Create items with provider/id format
@@ -599,10 +587,18 @@ export class InteractiveMode {
599
587
  }
600
588
  });
601
589
  // Start package update check asynchronously
602
- this.checkForPackageUpdates().then((updates) => {
590
+ this.checkForPackageUpdates()
591
+ .then((updates) => {
603
592
  if (updates.length > 0) {
604
593
  this.showPackageUpdateNotification(updates);
605
594
  }
595
+ })
596
+ .finally(() => {
597
+ // On Windows, npm can overwrite the shared console title while checking
598
+ // extension package versions. Restore Pi's title after the startup check.
599
+ if (process.platform === "win32" && this.isInitialized) {
600
+ this.updateTerminalTitle();
601
+ }
606
602
  });
607
603
  // Check tmux keyboard setup asynchronously
608
604
  this.checkTmuxKeyboardSetup().then((warning) => {
@@ -615,7 +611,7 @@ export class InteractiveMode {
615
611
  if (migratedProviders && migratedProviders.length > 0) {
616
612
  this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
617
613
  }
618
- const modelsJsonError = this.session.modelRegistry.getError();
614
+ const modelsJsonError = this.session.modelRuntime.getError();
619
615
  if (modelsJsonError) {
620
616
  this.showError(`models.json error: ${modelsJsonError}`);
621
617
  }
@@ -1346,7 +1342,7 @@ export class InteractiveMode {
1346
1342
  hasUI: true,
1347
1343
  cwd: this.sessionManager.getCwd(),
1348
1344
  sessionManager: this.sessionManager,
1349
- modelRegistry: this.session.modelRegistry,
1345
+ modelRegistry: extensionRunner.getModelRegistry(),
1350
1346
  model: this.session.model,
1351
1347
  isIdle: () => this.session.isIdle,
1352
1348
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
@@ -2658,7 +2654,7 @@ export class InteractiveMode {
2658
2654
  // Cache-miss notices are not persisted; re-derive them from the full entry
2659
2655
  // list and re-inject them after the assistant messages that paid for them.
2660
2656
  const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2661
- ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2657
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
2662
2658
  : new Map();
2663
2659
  if (options.updateFooter) {
2664
2660
  this.footer.invalidate();
@@ -2749,7 +2745,7 @@ export class InteractiveMode {
2749
2745
  if (!this.settingsManager.getShowCacheMissNotices())
2750
2746
  return;
2751
2747
  // Entries don't contain `message` yet: message_end fires before persistence.
2752
- const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2748
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
2753
2749
  if (miss)
2754
2750
  this.addCacheMissNotice(miss);
2755
2751
  }
@@ -3590,9 +3586,9 @@ export class InteractiveMode {
3590
3586
  if (this.session.scopedModels.length > 0) {
3591
3587
  return this.session.scopedModels.map((scoped) => scoped.model);
3592
3588
  }
3593
- this.session.modelRegistry.refresh();
3594
3589
  try {
3595
- return await this.session.modelRegistry.getAvailable();
3590
+ await this.session.modelRuntime.refresh();
3591
+ return [...(await this.session.modelRuntime.getAvailable())];
3596
3592
  }
3597
3593
  catch {
3598
3594
  return [];
@@ -3614,14 +3610,13 @@ export class InteractiveMode {
3614
3610
  if (!model || model.provider !== "anthropic") {
3615
3611
  return;
3616
3612
  }
3617
- const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
3618
- if (storedCredential?.type === "oauth") {
3619
- this.anthropicSubscriptionWarningShown = true;
3620
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3621
- return;
3622
- }
3623
3613
  try {
3624
- const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
3614
+ if ((await this.session.modelRuntime.checkAuth("anthropic"))?.type === "oauth") {
3615
+ this.anthropicSubscriptionWarningShown = true;
3616
+ this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
3617
+ return;
3618
+ }
3619
+ const apiKey = (await this.session.modelRuntime.getAuth(model.provider))?.auth.apiKey;
3625
3620
  if (!isAnthropicSubscriptionAuthKey(apiKey)) {
3626
3621
  return;
3627
3622
  }
@@ -3679,7 +3674,7 @@ export class InteractiveMode {
3679
3674
  }
3680
3675
  showModelSelector(initialSearchInput) {
3681
3676
  this.showSelector((done) => {
3682
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3677
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRuntime, this.session.scopedModels, async (model) => {
3683
3678
  try {
3684
3679
  await this.session.setModel(model);
3685
3680
  this.footer.invalidate();
@@ -3702,8 +3697,8 @@ export class InteractiveMode {
3702
3697
  }
3703
3698
  async showModelsSelector() {
3704
3699
  // Get all available models
3705
- this.session.modelRegistry.refresh();
3706
- const allModels = this.session.modelRegistry.getAvailable();
3700
+ await this.session.modelRuntime.refresh();
3701
+ const allModels = [...(await this.session.modelRuntime.getAvailable())];
3707
3702
  if (allModels.length === 0) {
3708
3703
  this.showStatus("No models available");
3709
3704
  return;
@@ -3721,7 +3716,7 @@ export class InteractiveMode {
3721
3716
  // Fall back to settings
3722
3717
  const patterns = this.settingsManager.getEnabledModels();
3723
3718
  if (patterns !== undefined && patterns.length > 0) {
3724
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3719
+ const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
3725
3720
  currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3726
3721
  }
3727
3722
  }
@@ -3729,7 +3724,7 @@ export class InteractiveMode {
3729
3724
  const updateSessionModels = async (enabledIds) => {
3730
3725
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3731
3726
  if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3732
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3727
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3733
3728
  this.session.setScopedModels(newScopedModels.map((sm) => ({
3734
3729
  model: sm.model,
3735
3730
  thinkingLevel: sm.thinkingLevel,
@@ -3989,44 +3984,46 @@ export class InteractiveMode {
3989
3984
  }
3990
3985
  }
3991
3986
  getLoginProviderOptions(authType) {
3992
- const authStorage = this.session.modelRegistry.authStorage;
3993
- const oauthProviders = authStorage.getOAuthProviders();
3994
- const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
3995
- const options = oauthProviders.map((provider) => ({
3996
- id: provider.id,
3997
- name: provider.name,
3998
- authType: "oauth",
3999
- }));
4000
- const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
4001
- for (const providerId of modelProviders) {
4002
- if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
4003
- continue;
4004
- }
4005
- options.push({
4006
- id: providerId,
4007
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4008
- authType: "api_key",
4009
- });
4010
- }
4011
- const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
4012
- return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
4013
- }
4014
- getLogoutProviderOptions() {
4015
- const authStorage = this.session.modelRegistry.authStorage;
4016
3987
  const options = [];
4017
- for (const providerId of authStorage.list()) {
4018
- const credential = authStorage.get(providerId);
4019
- if (!credential) {
4020
- continue;
3988
+ for (const provider of this.session.modelRuntime.getProviders()) {
3989
+ const authStatus = this.session.modelRuntime.getProviderAuthStatus(provider.id);
3990
+ const status = authStatus.configured
3991
+ ? {
3992
+ type: this.session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
3993
+ source: authStatus.label ?? authStatus.source,
3994
+ }
3995
+ : undefined;
3996
+ if ((!authType || authType === "oauth") && provider.auth.oauth) {
3997
+ options.push({
3998
+ id: provider.id,
3999
+ name: provider.name,
4000
+ authType: "oauth",
4001
+ method: provider.auth.oauth,
4002
+ status,
4003
+ });
4004
+ }
4005
+ if ((!authType || authType === "api_key") && provider.auth.apiKey) {
4006
+ options.push({
4007
+ id: provider.id,
4008
+ name: provider.name,
4009
+ authType: "api_key",
4010
+ method: provider.auth.apiKey,
4011
+ status,
4012
+ });
4021
4013
  }
4022
- options.push({
4023
- id: providerId,
4024
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
4025
- authType: credential.type,
4026
- });
4027
4014
  }
4028
4015
  return options.sort((a, b) => a.name.localeCompare(b.name));
4029
4016
  }
4017
+ async getLogoutProviderOptions() {
4018
+ return (await this.session.modelRuntime.listCredentials())
4019
+ .map(({ providerId, type }) => ({
4020
+ id: providerId,
4021
+ name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
4022
+ authType: type,
4023
+ status: { type, source: "stored credential" },
4024
+ }))
4025
+ .sort((a, b) => a.name.localeCompare(b.name));
4026
+ }
4030
4027
  findLoginProviderOptions(providerRef) {
4031
4028
  const normalizedProviderRef = providerRef.trim().toLowerCase();
4032
4029
  if (!normalizedProviderRef) {
@@ -4036,6 +4033,7 @@ export class InteractiveMode {
4036
4033
  provider.name.toLowerCase() === normalizedProviderRef);
4037
4034
  }
4038
4035
  async handleLoginCommand(providerRef) {
4036
+ await this.session.modelRuntime.getAvailable();
4039
4037
  if (!providerRef) {
4040
4038
  this.showLoginAuthTypeSelector();
4041
4039
  return;
@@ -4058,12 +4056,17 @@ export class InteractiveMode {
4058
4056
  if (providerOption.authType === "oauth") {
4059
4057
  await this.showLoginDialog(providerOption.id, providerOption.name);
4060
4058
  }
4061
- else {
4059
+ else if (providerOption.method?.login) {
4062
4060
  await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4063
4061
  }
4062
+ else {
4063
+ this.showAmbientAuthDialog(providerOption);
4064
+ }
4064
4065
  }
4065
4066
  showLoginAuthTypeSelector(providerOptions) {
4066
- const subscriptionLabel = "Sign in with an account";
4067
+ const oauthProvider = providerOptions?.find((provider) => provider.authType === "oauth");
4068
+ const oauthLoginLabel = oauthProvider?.method && "loginLabel" in oauthProvider.method ? oauthProvider.method.loginLabel : undefined;
4069
+ const subscriptionLabel = oauthLoginLabel ?? "Sign in with an account";
4067
4070
  const apiKeyLabel = "Sign in with an API key";
4068
4071
  const availableAuthTypes = providerOptions
4069
4072
  ? new Set(providerOptions.map((provider) => provider.authType))
@@ -4120,7 +4123,7 @@ export class InteractiveMode {
4120
4123
  return;
4121
4124
  }
4122
4125
  this.showSelector((done) => {
4123
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
4126
+ const selector = new OAuthSelectorComponent("login", providerOptions, async (providerId, selectedAuthType) => {
4124
4127
  done();
4125
4128
  const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
4126
4129
  if (!providerOption) {
@@ -4135,7 +4138,7 @@ export class InteractiveMode {
4135
4138
  else {
4136
4139
  this.ui.requestRender();
4137
4140
  }
4138
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
4141
+ }, initialSearchInput);
4139
4142
  return { component: selector, focus: selector };
4140
4143
  });
4141
4144
  }
@@ -4144,21 +4147,20 @@ export class InteractiveMode {
4144
4147
  this.showLoginAuthTypeSelector();
4145
4148
  return;
4146
4149
  }
4147
- const providerOptions = this.getLogoutProviderOptions();
4150
+ const providerOptions = await this.getLogoutProviderOptions();
4148
4151
  if (providerOptions.length === 0) {
4149
4152
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4150
4153
  return;
4151
4154
  }
4152
4155
  this.showSelector((done) => {
4153
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4156
+ const selector = new OAuthSelectorComponent(mode, providerOptions, async (providerId) => {
4154
4157
  done();
4155
4158
  const providerOption = providerOptions.find((provider) => provider.id === providerId);
4156
4159
  if (!providerOption) {
4157
4160
  return;
4158
4161
  }
4159
4162
  try {
4160
- this.session.modelRegistry.authStorage.logout(providerOption.id);
4161
- this.session.modelRegistry.refresh();
4163
+ await this.session.modelRuntime.logout(providerOption.id);
4162
4164
  await this.updateAvailableProviderCount();
4163
4165
  const message = providerOption.authType === "oauth"
4164
4166
  ? `Logged out of ${providerOption.name}`
@@ -4176,12 +4178,12 @@ export class InteractiveMode {
4176
4178
  });
4177
4179
  }
4178
4180
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4179
- this.session.modelRegistry.refresh();
4181
+ await this.session.modelRuntime.getAvailable();
4180
4182
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4181
4183
  let selectedModel;
4182
4184
  let selectionError;
4183
4185
  if (isUnknownModel(previousModel)) {
4184
- const availableModels = this.session.modelRegistry.getAvailable();
4186
+ const availableModels = await this.session.modelRuntime.getAvailable();
4185
4187
  const providerModels = availableModels.filter((model) => model.provider === providerId);
4186
4188
  if (!hasDefaultModelProvider(providerId)) {
4187
4189
  selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
@@ -4225,6 +4227,20 @@ export class InteractiveMode {
4225
4227
  }
4226
4228
  }
4227
4229
  }
4230
+ showAmbientAuthDialog(providerOption) {
4231
+ const restoreEditor = () => {
4232
+ this.editorContainer.clear();
4233
+ this.editorContainer.addChild(this.editor);
4234
+ this.ui.setFocus(this.editor);
4235
+ this.ui.requestRender();
4236
+ };
4237
+ const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
4238
+ dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true);
4239
+ this.editorContainer.clear();
4240
+ this.editorContainer.addChild(dialog);
4241
+ this.ui.setFocus(dialog);
4242
+ this.ui.requestRender();
4243
+ }
4228
4244
  async showApiKeyLoginDialog(providerId, providerName) {
4229
4245
  const previousModel = this.session.model;
4230
4246
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
@@ -4248,11 +4264,7 @@ export class InteractiveMode {
4248
4264
  this.ui.requestRender();
4249
4265
  };
4250
4266
  try {
4251
- const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
4252
- if (!apiKey) {
4253
- throw new Error("API key cannot be empty.");
4254
- }
4255
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
4267
+ await this.loginProvider(dialog, providerId, "api_key");
4256
4268
  restoreEditor();
4257
4269
  await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
4258
4270
  }
@@ -4264,8 +4276,8 @@ export class InteractiveMode {
4264
4276
  }
4265
4277
  }
4266
4278
  }
4267
- showOAuthLoginSelect(dialog, prompt) {
4268
- return new Promise((resolve) => {
4279
+ showAuthSelect(dialog, prompt) {
4280
+ return new Promise((resolve, reject) => {
4269
4281
  const restoreDialog = () => {
4270
4282
  this.editorContainer.clear();
4271
4283
  this.editorContainer.addChild(dialog);
@@ -4275,10 +4287,14 @@ export class InteractiveMode {
4275
4287
  const labels = prompt.options.map((option) => option.label);
4276
4288
  const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
4277
4289
  restoreDialog();
4278
- resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
4290
+ const id = prompt.options.find((option) => option.label === optionLabel)?.id;
4291
+ if (id)
4292
+ resolve(id);
4293
+ else
4294
+ reject(new Error("Login cancelled"));
4279
4295
  }, () => {
4280
4296
  restoreDialog();
4281
- resolve(undefined);
4297
+ reject(new Error("Login cancelled"));
4282
4298
  });
4283
4299
  this.editorContainer.clear();
4284
4300
  this.editorContainer.addChild(selector);
@@ -4286,30 +4302,64 @@ export class InteractiveMode {
4286
4302
  this.ui.requestRender();
4287
4303
  });
4288
4304
  }
4305
+ async showAuthPrompt(dialog, prompt) {
4306
+ let response;
4307
+ if (prompt.type === "select") {
4308
+ response = this.showAuthSelect(dialog, prompt);
4309
+ }
4310
+ else if (prompt.type === "manual_code") {
4311
+ response = dialog.showManualInput(prompt.message);
4312
+ }
4313
+ else {
4314
+ response = dialog.showPrompt(prompt.message, prompt.placeholder);
4315
+ }
4316
+ if (!prompt.signal)
4317
+ return response;
4318
+ if (prompt.signal.aborted)
4319
+ throw new Error("Login cancelled");
4320
+ const signal = prompt.signal;
4321
+ let onAbort;
4322
+ const aborted = new Promise((_resolve, reject) => {
4323
+ onAbort = () => reject(new Error("Login cancelled"));
4324
+ signal.addEventListener("abort", onAbort, { once: true });
4325
+ });
4326
+ try {
4327
+ return await Promise.race([response, aborted]);
4328
+ }
4329
+ finally {
4330
+ if (onAbort)
4331
+ signal.removeEventListener("abort", onAbort);
4332
+ }
4333
+ }
4334
+ notifyAuthDialog(dialog, event) {
4335
+ if (event.type === "auth_url") {
4336
+ dialog.showAuth(event.url, event.instructions);
4337
+ }
4338
+ else if (event.type === "device_code") {
4339
+ dialog.showDeviceCode(event);
4340
+ dialog.showWaiting("Waiting for authentication...");
4341
+ }
4342
+ else if (event.type === "info") {
4343
+ dialog.showInfo(event.message, event.links);
4344
+ }
4345
+ else {
4346
+ dialog.showProgress(event.message);
4347
+ }
4348
+ }
4349
+ async loginProvider(dialog, providerId, method) {
4350
+ await this.session.modelRuntime.login(providerId, method, {
4351
+ signal: dialog.signal,
4352
+ prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
4353
+ notify: (event) => this.notifyAuthDialog(dialog, event),
4354
+ });
4355
+ }
4289
4356
  async showLoginDialog(providerId, providerName) {
4290
- const providerInfo = this.session.modelRegistry.authStorage
4291
- .getOAuthProviders()
4292
- .find((provider) => provider.id === providerId);
4293
4357
  const previousModel = this.session.model;
4294
- // Providers that use callback servers (can paste redirect URL)
4295
- const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
4296
- // Create login dialog component
4297
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4298
- // Completion handled below
4299
- }, providerName);
4300
- // Show dialog in editor container
4358
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => { }, providerName);
4301
4359
  this.editorContainer.clear();
4302
4360
  this.editorContainer.addChild(dialog);
4303
4361
  this.ui.setFocus(dialog);
4304
4362
  this.ui.requestRender();
4305
- // Promise for manual code input (racing with callback server)
4306
- let manualCodeResolve;
4307
- let manualCodeReject;
4308
- const manualCodePromise = new Promise((resolve, reject) => {
4309
- manualCodeResolve = resolve;
4310
- manualCodeReject = reject;
4311
- });
4312
- // Restore editor helper
4313
4363
  const restoreEditor = () => {
4314
4364
  this.editorContainer.clear();
4315
4365
  this.editorContainer.addChild(this.editor);
@@ -4317,43 +4367,7 @@ export class InteractiveMode {
4317
4367
  this.ui.requestRender();
4318
4368
  };
4319
4369
  try {
4320
- await this.session.modelRegistry.authStorage.login(providerId, {
4321
- onAuth: (info) => {
4322
- dialog.showAuth(info.url, info.instructions);
4323
- if (usesCallbackServer) {
4324
- // Show input for manual paste, racing with callback
4325
- dialog
4326
- .showManualInput("Paste redirect URL below, or complete login in browser:")
4327
- .then((value) => {
4328
- if (value && manualCodeResolve) {
4329
- manualCodeResolve(value);
4330
- manualCodeResolve = undefined;
4331
- }
4332
- })
4333
- .catch(() => {
4334
- if (manualCodeReject) {
4335
- manualCodeReject(new Error("Login cancelled"));
4336
- manualCodeReject = undefined;
4337
- }
4338
- });
4339
- }
4340
- // For Anthropic: onPrompt is called immediately after
4341
- },
4342
- onDeviceCode: (info) => {
4343
- dialog.showDeviceCode(info);
4344
- dialog.showWaiting("Waiting for authentication...");
4345
- },
4346
- onPrompt: async (prompt) => {
4347
- return dialog.showPrompt(prompt.message, prompt.placeholder);
4348
- },
4349
- onProgress: (message) => {
4350
- dialog.showProgress(message);
4351
- },
4352
- onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
4353
- onManualCodeInput: () => manualCodePromise,
4354
- signal: dialog.signal,
4355
- });
4356
- // Success
4370
+ await this.loginProvider(dialog, providerId, "oauth");
4357
4371
  restoreEditor();
4358
4372
  await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
4359
4373
  }
@@ -4460,7 +4474,7 @@ export class InteractiveMode {
4460
4474
  showDiagnosticsWhenQuiet: true,
4461
4475
  });
4462
4476
  const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
4463
- const modelsJsonError = this.session.modelRegistry.getError();
4477
+ const modelsJsonError = this.session.modelRuntime.getError();
4464
4478
  if (modelsJsonError) {
4465
4479
  this.showError(`models.json error: ${modelsJsonError}`);
4466
4480
  }
@@ -4688,7 +4702,7 @@ export class InteractiveMode {
4688
4702
  const stats = this.session.getSessionStats();
4689
4703
  const sessionName = this.sessionManager.getSessionName();
4690
4704
  const entries = this.sessionManager.getEntries();
4691
- const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4705
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
4692
4706
  // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4693
4707
  // resolves to a concrete responseModel), sorted by cost descending.
4694
4708
  const perModelMap = new Map();