@earendil-works/pi-coding-agent 0.99.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundle/chunks/anthropic.js +2 -2
  3. package/dist/bundle/chunks/{azure-openai-responses-TLHNIYZK.js → azure-openai-responses-AFMVM6NP.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-7QSPBJKW.js → chunk-2KTBZM5G.js} +14 -28
  5. package/dist/bundle/chunks/{chunk-R5J3MOBF.js → chunk-2VNGUWBO.js} +1 -1
  6. package/dist/bundle/chunks/chunk-33XOIQ5N.js +1204 -0
  7. package/dist/bundle/chunks/chunk-4KU537KD.js +2 -0
  8. package/dist/bundle/chunks/chunk-7FH3BDRG.js +82 -0
  9. package/dist/bundle/chunks/{chunk-45SXDISG.js → chunk-IBCPYVFK.js} +4 -4
  10. package/dist/bundle/chunks/chunk-KDN6RCEQ.js +4 -0
  11. package/dist/bundle/chunks/{chunk-G3UZ5FWD.js → chunk-LTXEMVMA.js} +11 -60
  12. package/dist/bundle/chunks/chunk-PRD6WYBT.js +16 -0
  13. package/dist/bundle/chunks/{cli-ALNBTL3U.js → cli-QOMC26IJ.js} +3 -3
  14. package/dist/bundle/chunks/codemode-worker.js +44 -5
  15. package/dist/bundle/chunks/execute-GVRSEGHL.js +367 -0
  16. package/dist/bundle/chunks/openai-chatgpt.js +1 -1
  17. package/dist/bundle/chunks/{openai-codex-responses-3GKGNXC4.js → openai-codex-responses-GK67DQQJ.js} +1 -1
  18. package/dist/bundle/chunks/openai-codex.js +1 -1
  19. package/dist/bundle/chunks/{openai-responses-S7GKCCDG.js → openai-responses-CQDDLDQF.js} +1 -1
  20. package/dist/bundle/chunks/openrouter.js +1 -1
  21. package/dist/bundle/chunks/pi-logo-animation-MPKZE4EK.js +2 -0
  22. package/dist/bundle/chunks/radius.js +1 -1
  23. package/dist/bundle/chunks/runtime-2SO2JDWM.js +2 -0
  24. package/dist/bundle/chunks/virtual-modules-6ZMK7GDO.js +2 -0
  25. package/dist/bundle/cli-runtime.js +1 -1
  26. package/dist/bundle/index.js +1 -1
  27. package/dist/bundle/rpc-entry.js +1 -1
  28. package/dist/cli/args.js +2 -2
  29. package/dist/cli/args.js.map +1 -1
  30. package/dist/core/agent-session.d.ts +8 -0
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +37 -8
  33. package/dist/core/agent-session.js.map +1 -1
  34. package/dist/core/mcp-servers.d.ts +6 -0
  35. package/dist/core/mcp-servers.d.ts.map +1 -1
  36. package/dist/core/mcp-servers.js +7 -0
  37. package/dist/core/mcp-servers.js.map +1 -1
  38. package/dist/core/model-registry.d.ts +3 -1
  39. package/dist/core/model-registry.d.ts.map +1 -1
  40. package/dist/core/model-registry.js +4 -0
  41. package/dist/core/model-registry.js.map +1 -1
  42. package/dist/core/radius.d.ts +2 -0
  43. package/dist/core/radius.d.ts.map +1 -1
  44. package/dist/core/radius.js +2 -0
  45. package/dist/core/radius.js.map +1 -1
  46. package/dist/core/settings-manager.d.ts +7 -5
  47. package/dist/core/settings-manager.d.ts.map +1 -1
  48. package/dist/core/settings-manager.js +3 -2
  49. package/dist/core/settings-manager.js.map +1 -1
  50. package/dist/core/system-prompt.js +1 -1
  51. package/dist/core/system-prompt.js.map +1 -1
  52. package/dist/core/tools/bash.d.ts.map +1 -1
  53. package/dist/core/tools/bash.js +3 -5
  54. package/dist/core/tools/bash.js.map +1 -1
  55. package/dist/extensions/codemode/execute.d.ts.map +1 -1
  56. package/dist/extensions/codemode/execute.js +154 -47
  57. package/dist/extensions/codemode/execute.js.map +1 -1
  58. package/dist/extensions/codemode/tool.d.ts +7 -7
  59. package/dist/extensions/codemode/tool.d.ts.map +1 -1
  60. package/dist/extensions/codemode/tool.js +55 -102
  61. package/dist/extensions/codemode/tool.js.map +1 -1
  62. package/dist/extensions/mcp/cli.js +2 -2
  63. package/dist/extensions/mcp/cli.js.map +1 -1
  64. package/dist/extensions/mcp/index.d.ts.map +1 -1
  65. package/dist/extensions/mcp/index.js +23 -12
  66. package/dist/extensions/mcp/index.js.map +1 -1
  67. package/dist/extensions/mcp/oauth.d.ts +8 -7
  68. package/dist/extensions/mcp/oauth.d.ts.map +1 -1
  69. package/dist/extensions/mcp/oauth.js +55 -30
  70. package/dist/extensions/mcp/oauth.js.map +1 -1
  71. package/dist/extensions/mcp/runtime.d.ts.map +1 -1
  72. package/dist/extensions/mcp/runtime.js +2 -1
  73. package/dist/extensions/mcp/runtime.js.map +1 -1
  74. package/dist/index.d.ts +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/main.d.ts.map +1 -1
  78. package/dist/main.js +6 -0
  79. package/dist/main.js.map +1 -1
  80. package/dist/modes/interactive/components/oauth-selector.d.ts +8 -2
  81. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/oauth-selector.js +24 -21
  83. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  84. package/dist/modes/interactive/components/pi-logo-animation.d.ts +68 -0
  85. package/dist/modes/interactive/components/pi-logo-animation.d.ts.map +1 -0
  86. package/dist/modes/interactive/components/pi-logo-animation.js +1086 -0
  87. package/dist/modes/interactive/components/pi-logo-animation.js.map +1 -0
  88. package/dist/modes/interactive/components/pi-logo-animation.lazy.d.ts +7 -0
  89. package/dist/modes/interactive/components/pi-logo-animation.lazy.d.ts.map +1 -0
  90. package/dist/modes/interactive/components/pi-logo-animation.lazy.js +12 -0
  91. package/dist/modes/interactive/components/pi-logo-animation.lazy.js.map +1 -0
  92. package/dist/modes/interactive/components/pi-logo.d.ts +7 -0
  93. package/dist/modes/interactive/components/pi-logo.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/pi-logo.js +13 -1
  95. package/dist/modes/interactive/components/pi-logo.js.map +1 -1
  96. package/dist/modes/interactive/components/radius-login-selector.d.ts +15 -0
  97. package/dist/modes/interactive/components/radius-login-selector.d.ts.map +1 -0
  98. package/dist/modes/interactive/components/radius-login-selector.js +85 -0
  99. package/dist/modes/interactive/components/radius-login-selector.js.map +1 -0
  100. package/dist/modes/interactive/components/settings-selector.d.ts +3 -3
  101. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/settings-selector.js +5 -5
  103. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  104. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/user-message.js +5 -4
  106. package/dist/modes/interactive/components/user-message.js.map +1 -1
  107. package/dist/modes/interactive/interactive-mode.d.ts +10 -0
  108. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.js +153 -40
  110. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/dist/modes/interactive/theme/system-theme.d.ts +2 -1
  112. package/dist/modes/interactive/theme/system-theme.d.ts.map +1 -1
  113. package/dist/modes/interactive/theme/system-theme.js +16 -5
  114. package/dist/modes/interactive/theme/system-theme.js.map +1 -1
  115. package/docs/cli.md +6 -16
  116. package/docs/codemode.md +207 -0
  117. package/docs/docs.json +5 -1
  118. package/docs/environment-variables.md +2 -2
  119. package/docs/index.md +1 -1
  120. package/docs/mcp.md +17 -0
  121. package/docs/models.md +22 -1
  122. package/docs/providers.md +16 -6
  123. package/docs/sdk.md +1 -1
  124. package/docs/settings.md +3 -3
  125. package/docs/usage.md +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/sandbox/package-lock.json +2 -2
  132. package/examples/extensions/sandbox/package.json +1 -1
  133. package/examples/extensions/with-deps/package-lock.json +2 -2
  134. package/examples/extensions/with-deps/package.json +1 -1
  135. package/npm-shrinkwrap.json +25 -30
  136. package/package.json +10 -10
  137. package/dist/bundle/chunks/chunk-3KYEVQKX.js +0 -82
  138. package/dist/bundle/chunks/chunk-3NVH2HQH.js +0 -4
  139. package/dist/bundle/chunks/chunk-3YAHQSW6.js +0 -1438
  140. package/dist/bundle/chunks/chunk-ECRK44UV.js +0 -2
  141. package/dist/bundle/chunks/execute-RC7W4ML3.js +0 -328
  142. package/dist/bundle/chunks/runtime-NJPX4ID2.js +0 -2
  143. package/dist/bundle/chunks/virtual-modules-5UGH4HRZ.js +0 -2
@@ -25,6 +25,7 @@ import { createCompactionSummaryMessage, createCustomMessage } from "../../core/
25
25
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScopeFromModels, } from "../../core/model-resolver.js";
26
26
  import { CredentialSynchronizationError } from "../../core/model-runtime.js";
27
27
  import { DefaultPackageManager } from "../../core/package-manager.js";
28
+ import { RADIUS_MCP_URL, RADIUS_PROVIDER_ID } from "../../core/radius.js";
28
29
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
29
30
  import { SessionManager, sessionEntryToContextMessages, } from "../../core/session-manager.js";
30
31
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -32,6 +33,7 @@ import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
32
33
  import { withBuiltInRenderers } from "../../core/tools/renderers/index.js";
33
34
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
34
35
  import { getUsageCostBreakdown } from "../../core/usage-totals.js";
36
+ import { addMcpServerConfig, loadMcpConfig } from "../../extensions/mcp/config.js";
35
37
  import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
36
38
  import { copyToClipboard, readClipboardFilePaths, readClipboardText } from "../../utils/clipboard.js";
37
39
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
@@ -63,8 +65,10 @@ import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./c
63
65
  import { LoginDialogComponent } from "./components/login-dialog.js";
64
66
  import { createMermaidMarkdownTransformer } from "./components/mermaid.js";
65
67
  import { ModelSelectorComponent } from "./components/model-selector.js";
66
- import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
67
- import { piLogoLines } from "./components/pi-logo.js";
68
+ import { formatAuthSelectorProviderStatus, formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
69
+ import { piLogoLines, piWordmark, supportsPiLogo } from "./components/pi-logo.js";
70
+ import { playPiLogoAnimation } from "./components/pi-logo-animation.lazy.js";
71
+ import { createLoginMenuSelector } from "./components/radius-login-selector.js";
68
72
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
69
73
  import { SessionSelectorComponent } from "./components/session-selector.js";
70
74
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
@@ -106,6 +110,16 @@ class ExpandableText extends ThemedText {
106
110
  this.invalidate();
107
111
  }
108
112
  }
113
+ /** The built-in header. Clicking its logo (the first two lines, after one column of padding) plays an easter egg. */
114
+ class BuiltInHeader extends ExpandableText {
115
+ onLogoClick;
116
+ handleMouse(event) {
117
+ if (event.type !== "click" || event.y > 1 || event.x < 1 || event.x > 4 || !this.onLogoClick)
118
+ return undefined;
119
+ this.onLogoClick(event.screenX - event.x + 1, event.screenY - event.y);
120
+ return { handled: true };
121
+ }
122
+ }
109
123
  function isCustomSessionEntry(item) {
110
124
  return "type" in item && item.type === "custom";
111
125
  }
@@ -175,6 +189,7 @@ function llamaCppPostLoginGuidance(actionLabel, loadedModelCount) {
175
189
  ? `${actionLabel}. No llama.cpp models are loaded. Use /llama to load a model, then /model to select it.`
176
190
  : `${actionLabel}. Use /model to select a loaded llama.cpp model, or /llama to manage models.`;
177
191
  }
192
+ const RADIUS_LOGIN_INTRO = "Radius is a service crafted for Pi by the builders of Pi, Earendil Works";
178
193
  const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
179
194
  function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
180
195
  const filtered = fuzzyFilter(items, prefix, getSearchText);
@@ -197,18 +212,21 @@ function getLoginProviderCompletionOptions(providerOptions) {
197
212
  id: provider.id,
198
213
  name: provider.name,
199
214
  authTypes: [provider.authType],
215
+ subscription: provider.subscription,
200
216
  });
201
217
  }
202
218
  return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
203
219
  }
204
220
  function getLoginProviderSearchText(provider) {
205
221
  const authTypes = provider.authTypes
206
- .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
222
+ .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType, provider.subscription)}`)
207
223
  .join(" ");
208
224
  return `${provider.id} ${provider.name} ${authTypes}`;
209
225
  }
210
226
  function formatLoginProviderCompletionDescription(provider) {
211
- const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
227
+ const authTypes = provider.authTypes
228
+ .map((authType) => formatAuthSelectorProviderType(authType, provider.subscription))
229
+ .join("/");
212
230
  return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
213
231
  }
214
232
  export class InteractiveMode {
@@ -635,7 +653,7 @@ export class InteractiveMode {
635
653
  this.registerSignalHandlers();
636
654
  // Load changelog (only show new entries, skip for resumed sessions)
637
655
  this.changelogMarkdown = this.getChangelogForDisplay();
638
- if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
656
+ if (this.session.scopedModels.length > 0 && this.shouldShowStartupDetails()) {
639
657
  const modelList = this.session.scopedModels
640
658
  .map((sm) => {
641
659
  const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
@@ -686,10 +704,15 @@ export class InteractiveMode {
686
704
  // reported its colors. This ends at the terminal's DA1 reply, or after 100 ms if it answers nothing.
687
705
  await this.themeController.waitForTerminalColors();
688
706
  // Add header with keybindings from config (unless silenced)
689
- if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
707
+ if (this.shouldShowStartupHeader()) {
708
+ const showDetails = this.shouldShowStartupDetails();
690
709
  // Built on demand so the header follows theme changes. The logo's first line carries the version,
691
- // its second line the first line of key hints.
710
+ // its second line the first line of key hints. Terminals that cannot render the logo get a
711
+ // "Pi vX" line instead, with the key hints below it.
712
+ const showLogo = supportsPiLogo();
692
713
  const withLogo = (hints) => {
714
+ if (!showLogo)
715
+ return `${piWordmark()} ${theme.fg("dim", `v${this.version}`)}\n${hints}`;
693
716
  const [top, bottom] = piLogoLines();
694
717
  return `${top} ${theme.fg("dim", `v${this.version}`)}\n${bottom} ${hints}`;
695
718
  };
@@ -723,9 +746,12 @@ export class InteractiveMode {
723
746
  rawKeyHint("!", "bash"),
724
747
  hint("app.tools.expand", "more"),
725
748
  ].join(theme.fg("muted", " · "));
726
- const compactOnboarding = () => theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
749
+ const compactOnboarding = () => theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help${showDetails ? " and loaded resources" : ""}.`);
727
750
  const onboarding = () => theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
728
- this.builtInHeader = new ExpandableText(() => `${withLogo(compactInstructions())}\n${compactOnboarding()}\n\n${onboarding()}`, () => `${withLogo(expandedInstructions())}\n\n${onboarding()}`, this.getStartupExpansionState(), 1, 0);
751
+ const header = new BuiltInHeader(() => `${withLogo(compactInstructions())}\n${compactOnboarding()}\n\n${onboarding()}`, () => `${withLogo(expandedInstructions())}\n\n${onboarding()}`, this.getStartupExpansionState(), 1, 0);
752
+ if (showLogo)
753
+ header.onLogoClick = (column, row) => playPiLogoAnimation(this.renderer, column, row);
754
+ this.builtInHeader = header;
729
755
  // Setup UI layout
730
756
  this.headerContainer.addChild(new Spacer(1));
731
757
  this.headerContainer.addChild(this.builtInHeader);
@@ -1024,6 +1050,14 @@ export class InteractiveMode {
1024
1050
  getStartupExpansionState() {
1025
1051
  return this.options.verbose || this.toolOutputExpanded;
1026
1052
  }
1053
+ /** Startup header (logo, version, key hints). Hidden only by quietStartup: true. */
1054
+ shouldShowStartupHeader() {
1055
+ return this.options.verbose === true || this.settingsManager.getQuietStartup() !== true;
1056
+ }
1057
+ /** Startup details (model scope, loaded resources). Hidden by quietStartup: true or "header". */
1058
+ shouldShowStartupDetails() {
1059
+ return this.options.verbose === true || this.settingsManager.getQuietStartup() === false;
1060
+ }
1027
1061
  /**
1028
1062
  * Get a short path relative to the package root for display.
1029
1063
  */
@@ -1285,7 +1319,7 @@ export class InteractiveMode {
1285
1319
  showLoadedResources(options) {
1286
1320
  // Resource rendering is idempotent; chat clears no longer clear this separate container.
1287
1321
  this.loadedResourcesContainer.clear();
1288
- const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
1322
+ const showListing = options?.force || this.shouldShowStartupDetails();
1289
1323
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
1290
1324
  if (!showListing && !showDiagnostics) {
1291
1325
  return;
@@ -4075,8 +4109,8 @@ export class InteractiveMode {
4075
4109
  onEnableInstallTelemetryChange: (enabled) => {
4076
4110
  this.settingsManager.setEnableInstallTelemetry(enabled);
4077
4111
  },
4078
- onQuietStartupChange: (enabled) => {
4079
- this.settingsManager.setQuietStartup(enabled);
4112
+ onQuietStartupChange: (quiet) => {
4113
+ this.settingsManager.setQuietStartup(quiet);
4080
4114
  },
4081
4115
  onDefaultProjectTrustChange: (defaultProjectTrust) => {
4082
4116
  this.settingsManager.setDefaultProjectTrust(defaultProjectTrust);
@@ -4728,6 +4762,7 @@ export class InteractiveMode {
4728
4762
  source: authStatus.label ?? authStatus.source,
4729
4763
  }
4730
4764
  : undefined;
4765
+ const subscription = provider.auth.oauth?.isSubscription === true;
4731
4766
  if ((!authType || authType === "oauth") && provider.auth.oauth) {
4732
4767
  options.push({
4733
4768
  id: provider.id,
@@ -4735,6 +4770,7 @@ export class InteractiveMode {
4735
4770
  authType: "oauth",
4736
4771
  method: provider.auth.oauth,
4737
4772
  status,
4773
+ subscription,
4738
4774
  });
4739
4775
  }
4740
4776
  if ((!authType || authType === "api_key") && provider.auth.apiKey) {
@@ -4744,6 +4780,7 @@ export class InteractiveMode {
4744
4780
  authType: "api_key",
4745
4781
  method: provider.auth.apiKey,
4746
4782
  status,
4783
+ subscription,
4747
4784
  });
4748
4785
  }
4749
4786
  }
@@ -4751,12 +4788,16 @@ export class InteractiveMode {
4751
4788
  }
4752
4789
  async getLogoutProviderOptions() {
4753
4790
  return (await this.session.modelRuntime.listCredentials({ signal: AbortSignal.timeout(15_000) }))
4754
- .map(({ providerId, type }) => ({
4755
- id: providerId,
4756
- name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
4757
- authType: type,
4758
- status: { type, source: "stored credential" },
4759
- }))
4791
+ .map(({ providerId, type }) => {
4792
+ const provider = this.session.modelRuntime.getProvider(providerId);
4793
+ return {
4794
+ id: providerId,
4795
+ name: provider?.name ?? providerId,
4796
+ authType: type,
4797
+ status: { type, source: "stored credential" },
4798
+ subscription: provider?.auth.oauth?.isSubscription === true,
4799
+ };
4800
+ })
4760
4801
  .sort((a, b) => a.name.localeCompare(b.name));
4761
4802
  }
4762
4803
  findLoginProviderOptions(providerRef) {
@@ -4786,18 +4827,25 @@ export class InteractiveMode {
4786
4827
  }
4787
4828
  this.showLoginProviderSelector(undefined, providerRef);
4788
4829
  }
4789
- async startProviderLogin(providerOption) {
4830
+ /** `onBack` reopens the selector the login was started from when the user cancels it. */
4831
+ async startProviderLogin(providerOption, onBack) {
4790
4832
  if (providerOption.authType === "oauth") {
4791
- await this.showLoginDialog(providerOption.id, providerOption.name);
4833
+ await this.showLoginDialog(providerOption.id, providerOption.name, onBack);
4792
4834
  }
4793
4835
  else if (providerOption.method?.login) {
4794
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4836
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name, onBack);
4795
4837
  }
4796
4838
  else {
4797
- this.showAmbientAuthDialog(providerOption);
4839
+ this.showAmbientAuthDialog(providerOption, onBack);
4798
4840
  }
4799
4841
  }
4800
4842
  showLoginAuthTypeSelector(providerOptions) {
4843
+ // The top-level selector offers Radius directly, as its last option.
4844
+ const radiusOption = providerOptions
4845
+ ? undefined
4846
+ : this.getLoginProviderOptions("oauth").find((provider) => provider.id === RADIUS_PROVIDER_ID);
4847
+ const radiusText = radiusOption ? `Sign in with ${radiusOption.name}` : undefined;
4848
+ const radiusLabel = radiusOption ? `${radiusText}${formatAuthSelectorProviderStatus(radiusOption)}` : undefined;
4801
4849
  const oauthProvider = providerOptions?.find((provider) => provider.authType === "oauth");
4802
4850
  const oauthLoginLabel = oauthProvider?.method && "loginLabel" in oauthProvider.method ? oauthProvider.method.loginLabel : undefined;
4803
4851
  const subscriptionLabel = oauthLoginLabel ?? "Sign in with an account";
@@ -4812,6 +4860,8 @@ export class InteractiveMode {
4812
4860
  if (availableAuthTypes.has("api_key")) {
4813
4861
  options.push(apiKeyLabel);
4814
4862
  }
4863
+ if (radiusLabel)
4864
+ options.push(radiusLabel);
4815
4865
  if (options.length === 0) {
4816
4866
  this.showStatus("No login methods available.");
4817
4867
  return;
@@ -4827,29 +4877,37 @@ export class InteractiveMode {
4827
4877
  ? `Select authentication method for ${providerOptions[0].name}:`
4828
4878
  : "Select authentication method:";
4829
4879
  this.showSelector((done) => {
4830
- const selector = new ExtensionSelectorComponent(title, options, (option) => {
4880
+ const onSelect = (option) => {
4831
4881
  done();
4882
+ if (radiusOption && option === radiusLabel) {
4883
+ void this.startProviderLogin(radiusOption, () => this.showLoginAuthTypeSelector());
4884
+ return;
4885
+ }
4832
4886
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4833
4887
  if (providerOptions) {
4834
4888
  const providerOption = providerOptions.find((provider) => provider.authType === authType);
4835
4889
  if (providerOption) {
4836
- void this.startProviderLogin(providerOption);
4890
+ void this.startProviderLogin(providerOption, () => this.showLoginAuthTypeSelector(providerOptions));
4837
4891
  }
4838
4892
  return;
4839
4893
  }
4840
4894
  this.showLoginProviderSelector(authType);
4841
- }, () => {
4895
+ };
4896
+ const onCancel = () => {
4842
4897
  done();
4843
4898
  this.ui.requestRender();
4844
- });
4845
- return { component: selector, focus: selector };
4899
+ };
4900
+ const selector = radiusLabel && radiusText
4901
+ ? createLoginMenuSelector(this.ui, title, options, { label: radiusLabel, text: radiusText }, onSelect, onCancel)
4902
+ : new ExtensionSelectorComponent(title, options, onSelect, onCancel);
4903
+ return { component: selector, focus: selector, dispose: () => selector.dispose() };
4846
4904
  });
4847
4905
  }
4848
4906
  showLoginProviderSelector(authType, initialSearchInput) {
4849
4907
  const providerOptions = this.getLoginProviderOptions(authType);
4850
4908
  if (providerOptions.length === 0) {
4851
4909
  const message = authType === "oauth"
4852
- ? "No subscription providers available."
4910
+ ? "No account providers available."
4853
4911
  : authType === "api_key"
4854
4912
  ? "No API key providers available."
4855
4913
  : "No login providers available.";
@@ -4863,7 +4921,7 @@ export class InteractiveMode {
4863
4921
  if (!providerOption) {
4864
4922
  return;
4865
4923
  }
4866
- await this.startProviderLogin(providerOption);
4924
+ await this.startProviderLogin(providerOption, () => this.showLoginProviderSelector(authType, initialSearchInput));
4867
4925
  }, () => {
4868
4926
  done();
4869
4927
  if (authType) {
@@ -5017,21 +5075,24 @@ export class InteractiveMode {
5017
5075
  })
5018
5076
  .finally(() => clearTimeout(timeout));
5019
5077
  }
5020
- showAmbientAuthDialog(providerOption) {
5078
+ showAmbientAuthDialog(providerOption, onBack) {
5021
5079
  const restoreEditor = () => {
5022
5080
  this.editorContainer.clear();
5023
5081
  this.editorContainer.addChild(this.editor);
5024
5082
  this.ui.setFocus(this.editor);
5025
5083
  this.ui.requestRender();
5026
5084
  };
5027
- const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
5085
+ const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => {
5086
+ restoreEditor();
5087
+ onBack?.();
5088
+ }, providerOption.name, `${providerOption.name} setup`);
5028
5089
  dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside ${APP_NAME}.`, [], true);
5029
5090
  this.editorContainer.clear();
5030
5091
  this.editorContainer.addChild(dialog);
5031
5092
  this.ui.setFocus(dialog);
5032
5093
  this.ui.requestRender();
5033
5094
  }
5034
- async showApiKeyLoginDialog(providerId, providerName) {
5095
+ async showApiKeyLoginDialog(providerId, providerName, onBack) {
5035
5096
  const previousModel = this.session.model;
5036
5097
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
5037
5098
  // Completion handled below
@@ -5064,12 +5125,15 @@ export class InteractiveMode {
5064
5125
  if (error instanceof CredentialSynchronizationError) {
5065
5126
  this.showError(`Saved API key for ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
5066
5127
  }
5067
- else if (errorMsg !== "Login cancelled") {
5128
+ else if (errorMsg === "Login cancelled") {
5129
+ onBack?.();
5130
+ }
5131
+ else {
5068
5132
  this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
5069
5133
  }
5070
5134
  }
5071
5135
  }
5072
- showAuthSelect(dialog, prompt) {
5136
+ showAuthSelect(dialog, prompt, providerId) {
5073
5137
  return new Promise((resolve, reject) => {
5074
5138
  const restoreDialog = () => {
5075
5139
  this.editorContainer.clear();
@@ -5088,17 +5152,17 @@ export class InteractiveMode {
5088
5152
  }, () => {
5089
5153
  restoreDialog();
5090
5154
  reject(new Error("Login cancelled"));
5091
- });
5155
+ }, { description: providerId === RADIUS_PROVIDER_ID ? RADIUS_LOGIN_INTRO : undefined });
5092
5156
  this.editorContainer.clear();
5093
5157
  this.editorContainer.addChild(selector);
5094
5158
  this.ui.setFocus(selector);
5095
5159
  this.ui.requestRender();
5096
5160
  });
5097
5161
  }
5098
- async showAuthPrompt(dialog, prompt) {
5162
+ async showAuthPrompt(dialog, prompt, providerId) {
5099
5163
  let response;
5100
5164
  if (prompt.type === "select") {
5101
- response = this.showAuthSelect(dialog, prompt);
5165
+ response = this.showAuthSelect(dialog, prompt, providerId);
5102
5166
  }
5103
5167
  else if (prompt.type === "manual_code") {
5104
5168
  response = dialog.showManualInput(prompt.message);
@@ -5142,11 +5206,11 @@ export class InteractiveMode {
5142
5206
  async loginProvider(dialog, providerId, method) {
5143
5207
  await this.session.modelRuntime.login(providerId, method, {
5144
5208
  signal: dialog.signal,
5145
- prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
5209
+ prompt: (prompt) => this.showAuthPrompt(dialog, prompt, providerId),
5146
5210
  notify: (event) => this.notifyAuthDialog(dialog, event),
5147
5211
  }, { getDeviceId: () => this.settingsManager.getOrCreateDeviceId() });
5148
5212
  }
5149
- async showLoginDialog(providerId, providerName) {
5213
+ async showLoginDialog(providerId, providerName, onBack) {
5150
5214
  const previousModel = this.session.model;
5151
5215
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => { }, providerName);
5152
5216
  this.editorContainer.clear();
@@ -5163,6 +5227,8 @@ export class InteractiveMode {
5163
5227
  await this.loginProvider(dialog, providerId, "oauth");
5164
5228
  restoreEditor();
5165
5229
  await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
5230
+ if (providerId === RADIUS_PROVIDER_ID)
5231
+ this.offerRadiusMcpServer(providerId, providerName);
5166
5232
  }
5167
5233
  catch (error) {
5168
5234
  restoreEditor();
@@ -5170,11 +5236,58 @@ export class InteractiveMode {
5170
5236
  if (error instanceof CredentialSynchronizationError) {
5171
5237
  this.showError(`Logged in to ${providerName}, but local model state could not be synchronized: ${errorMsg}`);
5172
5238
  }
5173
- else if (errorMsg !== "Login cancelled") {
5239
+ else if (errorMsg === "Login cancelled") {
5240
+ onBack?.();
5241
+ }
5242
+ else {
5174
5243
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
5175
5244
  }
5176
5245
  }
5177
5246
  }
5247
+ /**
5248
+ * Offer to point the Radius MCP server in the global mcp.json at the Radius login, adding the server
5249
+ * when missing. Nothing is asked when a global server already uses this login.
5250
+ */
5251
+ offerRadiusMcpServer(providerId, providerName) {
5252
+ const mcpPath = path.join(getAgentDir(), "mcp.json");
5253
+ const normalizeUrl = (url) => url.replace(/\/+$/u, "");
5254
+ const { servers } = loadMcpConfig({
5255
+ agentDir: getAgentDir(),
5256
+ cwd: this.sessionManager.getCwd(),
5257
+ projectTrusted: false,
5258
+ });
5259
+ const existing = servers.find((server) => "url" in server.config && normalizeUrl(server.config.url) === normalizeUrl(RADIUS_MCP_URL));
5260
+ if (existing && "url" in existing.config && existing.config.auth?.provider === providerId)
5261
+ return;
5262
+ let name = existing?.name ?? "radius";
5263
+ if (!existing && servers.some((server) => server.name === name))
5264
+ name = "radius-mcp";
5265
+ const config = existing && "url" in existing.config
5266
+ ? { ...existing.config, auth: { provider: providerId } }
5267
+ : { url: RADIUS_MCP_URL, auth: { provider: providerId } };
5268
+ // `auth` replaces the MCP OAuth sign-in.
5269
+ delete config.oauth;
5270
+ this.showSelector((done) => {
5271
+ const selector = new ExtensionSelectorComponent(`Configure ${providerName} MCP in ${mcpPath}?`, ["Yes", "No"], (option) => {
5272
+ done();
5273
+ if (option !== "Yes")
5274
+ return;
5275
+ try {
5276
+ addMcpServerConfig(mcpPath, name, config);
5277
+ }
5278
+ catch (error) {
5279
+ this.showError(`Could not update ${mcpPath}: ${error instanceof Error ? error.message : String(error)}`);
5280
+ return;
5281
+ }
5282
+ // The MCP extension reads mcp.json when the session starts.
5283
+ void this.handleReloadCommand();
5284
+ }, () => {
5285
+ done();
5286
+ this.ui.requestRender();
5287
+ });
5288
+ return { component: selector, focus: selector };
5289
+ });
5290
+ }
5178
5291
  // =========================================================================
5179
5292
  // Command handlers
5180
5293
  // =========================================================================