feat(coding-agent): add searchable auth provider login flow (#3572)
This commit is contained in:
@@ -44,7 +44,7 @@ Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json`
|
||||
|
||||
### Environment Variables or Auth File
|
||||
|
||||
Set via environment variable:
|
||||
Use `/login` in interactive mode and select a provider to store an API key in `auth.json`, or set credentials via environment variable:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
@@ -30,8 +30,8 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
||||
{ name: "fork", description: "Create a new fork from a previous user message" },
|
||||
{ name: "clone", description: "Duplicate the current session at the current position" },
|
||||
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
||||
{ name: "login", description: "Login with OAuth provider" },
|
||||
{ name: "logout", description: "Logout from OAuth provider" },
|
||||
{ name: "login", description: "Configure provider authentication" },
|
||||
{ name: "logout", description: "Remove provider authentication" },
|
||||
{ name: "new", description: "Start a new session" },
|
||||
{ name: "compact", description: "Manually compact the session context" },
|
||||
{ name: "resume", description: "Resume a different session" },
|
||||
|
||||
@@ -30,12 +30,13 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
tui: TUI,
|
||||
providerId: string,
|
||||
private onComplete: (success: boolean, message?: string) => void,
|
||||
providerNameOverride?: string,
|
||||
) {
|
||||
super();
|
||||
this.tui = tui;
|
||||
|
||||
const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
|
||||
const providerName = providerInfo?.name || providerId;
|
||||
const providerName = providerNameOverride || providerInfo?.name || providerId;
|
||||
|
||||
// Top border
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
import type { OAuthProviderInterface } from "@mariozechner/pi-ai";
|
||||
import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
||||
import { Container, getKeybindings, Spacer, TruncatedText } from "@mariozechner/pi-tui";
|
||||
import {
|
||||
Container,
|
||||
type Focusable,
|
||||
fuzzyFilter,
|
||||
getKeybindings,
|
||||
Input,
|
||||
Spacer,
|
||||
TruncatedText,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import type { AuthStorage } from "../../../core/auth-storage.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
export type AuthSelectorProvider = {
|
||||
id: string;
|
||||
name: string;
|
||||
authType: "oauth" | "api_key";
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that renders an OAuth provider selector
|
||||
* Component that renders an auth provider selector
|
||||
*/
|
||||
export class OAuthSelectorComponent extends Container {
|
||||
export class OAuthSelectorComponent extends Container implements Focusable {
|
||||
private searchInput: Input;
|
||||
|
||||
// Focusable implementation - propagate to search input for IME cursor positioning
|
||||
private _focused = false;
|
||||
get focused(): boolean {
|
||||
return this._focused;
|
||||
}
|
||||
set focused(value: boolean) {
|
||||
this._focused = value;
|
||||
this.searchInput.focused = value;
|
||||
}
|
||||
|
||||
private listContainer: Container;
|
||||
private allProviders: OAuthProviderInterface[] = [];
|
||||
private allProviders: AuthSelectorProvider[];
|
||||
private filteredProviders: AuthSelectorProvider[];
|
||||
private selectedIndex: number = 0;
|
||||
private mode: "login" | "logout";
|
||||
private authStorage: AuthStorage;
|
||||
@@ -20,6 +45,7 @@ export class OAuthSelectorComponent extends Container {
|
||||
constructor(
|
||||
mode: "login" | "logout",
|
||||
authStorage: AuthStorage,
|
||||
providers: AuthSelectorProvider[],
|
||||
onSelect: (providerId: string) => void,
|
||||
onCancel: () => void,
|
||||
) {
|
||||
@@ -27,21 +53,30 @@ export class OAuthSelectorComponent extends Container {
|
||||
|
||||
this.mode = mode;
|
||||
this.authStorage = authStorage;
|
||||
this.allProviders = providers;
|
||||
this.filteredProviders = providers;
|
||||
this.onSelectCallback = onSelect;
|
||||
this.onCancelCallback = onCancel;
|
||||
|
||||
// Load all OAuth providers
|
||||
this.loadProviders();
|
||||
|
||||
// Add top border
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add title
|
||||
const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
|
||||
const title = mode === "login" ? "Select provider to configure:" : "Select provider to logout:";
|
||||
this.addChild(new TruncatedText(theme.bold(title)));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
this.searchInput = new Input();
|
||||
this.searchInput.onSubmit = () => {
|
||||
const selectedProvider = this.filteredProviders[this.selectedIndex];
|
||||
if (selectedProvider) {
|
||||
this.onSelectCallback(selectedProvider.id);
|
||||
}
|
||||
};
|
||||
this.addChild(this.searchInput);
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Create list container
|
||||
this.listContainer = new Container();
|
||||
this.addChild(this.listContainer);
|
||||
@@ -52,27 +87,38 @@ export class OAuthSelectorComponent extends Container {
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Initial render
|
||||
this.updateList();
|
||||
this.filterProviders("");
|
||||
}
|
||||
|
||||
private loadProviders(): void {
|
||||
this.allProviders = getOAuthProviders();
|
||||
private filterProviders(query: string): void {
|
||||
this.filteredProviders = query
|
||||
? fuzzyFilter(this.allProviders, query, (provider) => `${provider.name} ${provider.id} ${provider.authType}`)
|
||||
: this.allProviders;
|
||||
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, Math.max(0, this.filteredProviders.length - 1)));
|
||||
this.updateList();
|
||||
}
|
||||
|
||||
private updateList(): void {
|
||||
this.listContainer.clear();
|
||||
|
||||
for (let i = 0; i < this.allProviders.length; i++) {
|
||||
const provider = this.allProviders[i];
|
||||
const maxVisible = 8;
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredProviders.length - maxVisible),
|
||||
);
|
||||
const endIndex = Math.min(startIndex + maxVisible, this.filteredProviders.length);
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const provider = this.filteredProviders[i];
|
||||
if (!provider) continue;
|
||||
|
||||
const isSelected = i === this.selectedIndex;
|
||||
|
||||
// Check if user is logged in for this provider
|
||||
// Check if user is configured for this provider
|
||||
const credentials = this.authStorage.get(provider.id);
|
||||
const isLoggedIn = credentials?.type === "oauth";
|
||||
const statusIndicator = isLoggedIn ? theme.fg("success", " ✓ logged in") : "";
|
||||
|
||||
const statusIndicator = credentials
|
||||
? theme.fg("success", " ✓ configured")
|
||||
: theme.fg("muted", " • unconfigured");
|
||||
let line = "";
|
||||
if (isSelected) {
|
||||
const prefix = theme.fg("accent", "→ ");
|
||||
@@ -86,10 +132,19 @@ export class OAuthSelectorComponent extends Container {
|
||||
this.listContainer.addChild(new TruncatedText(line, 0, 0));
|
||||
}
|
||||
|
||||
if (startIndex > 0 || endIndex < this.filteredProviders.length) {
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredProviders.length})`);
|
||||
this.listContainer.addChild(new TruncatedText(scrollInfo, 0, 0));
|
||||
}
|
||||
|
||||
// Show "no providers" if empty
|
||||
if (this.allProviders.length === 0) {
|
||||
if (this.filteredProviders.length === 0) {
|
||||
const message =
|
||||
this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
|
||||
this.allProviders.length === 0
|
||||
? this.mode === "login"
|
||||
? "No providers available"
|
||||
: "No providers logged in. Use /login first."
|
||||
: "No matching providers";
|
||||
this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
|
||||
}
|
||||
}
|
||||
@@ -98,17 +153,19 @@ export class OAuthSelectorComponent extends Container {
|
||||
const kb = getKeybindings();
|
||||
// Up arrow
|
||||
if (kb.matches(keyData, "tui.select.up")) {
|
||||
if (this.filteredProviders.length === 0) return;
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.updateList();
|
||||
}
|
||||
// Down arrow
|
||||
else if (kb.matches(keyData, "tui.select.down")) {
|
||||
this.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);
|
||||
if (this.filteredProviders.length === 0) return;
|
||||
this.selectedIndex = Math.min(this.filteredProviders.length - 1, this.selectedIndex + 1);
|
||||
this.updateList();
|
||||
}
|
||||
// Enter
|
||||
else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const selectedProvider = this.allProviders[this.selectedIndex];
|
||||
const selectedProvider = this.filteredProviders[this.selectedIndex];
|
||||
if (selectedProvider) {
|
||||
this.onSelectCallback(selectedProvider.id);
|
||||
}
|
||||
@@ -117,5 +174,10 @@ export class OAuthSelectorComponent extends Container {
|
||||
else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancelCallback();
|
||||
}
|
||||
// Pass everything else to search input
|
||||
else {
|
||||
this.searchInput.handleInput(keyData);
|
||||
this.filterProviders(this.searchInput.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ import { FooterComponent } from "./components/footer.js";
|
||||
import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
|
||||
import { LoginDialogComponent } from "./components/login-dialog.js";
|
||||
import { ModelSelectorComponent } from "./components/model-selector.js";
|
||||
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
||||
import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.js";
|
||||
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
|
||||
import { SessionSelectorComponent } from "./components/session-selector.js";
|
||||
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
||||
@@ -168,6 +168,33 @@ function hasDefaultModelProvider(providerId: string): providerId is keyof typeof
|
||||
return providerId in defaultModelPerProvider;
|
||||
}
|
||||
|
||||
const API_KEY_PROVIDER_NAMES: Record<string, string> = {
|
||||
"azure-openai-responses": "Azure OpenAI Responses",
|
||||
cerebras: "Cerebras",
|
||||
fireworks: "Fireworks",
|
||||
google: "Google Gemini",
|
||||
"google-vertex": "Google Vertex AI",
|
||||
groq: "Groq",
|
||||
huggingface: "Hugging Face",
|
||||
"kimi-coding": "Kimi For Coding",
|
||||
mistral: "Mistral",
|
||||
minimax: "MiniMax",
|
||||
"minimax-cn": "MiniMax (China)",
|
||||
opencode: "OpenCode Zen",
|
||||
"opencode-go": "OpenCode Go",
|
||||
openai: "OpenAI",
|
||||
openrouter: "OpenRouter",
|
||||
"vercel-ai-gateway": "Vercel AI Gateway",
|
||||
xai: "xAI",
|
||||
zai: "ZAI",
|
||||
};
|
||||
|
||||
const API_KEY_LOGIN_PROVIDER_BLOCKLIST = new Set(["amazon-bedrock", "llama.cpp", "lmstudio", "ollama"]);
|
||||
|
||||
function getApiKeyProviderDisplayName(providerId: string): string {
|
||||
return API_KEY_PROVIDER_NAMES[providerId] ?? providerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for InteractiveMode initialization.
|
||||
*/
|
||||
@@ -3906,7 +3933,7 @@ export class InteractiveMode {
|
||||
},
|
||||
() => {
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
this.showLoginAuthTypeSelector();
|
||||
},
|
||||
initialSearchInput,
|
||||
);
|
||||
@@ -4259,42 +4286,149 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
||||
if (mode === "logout") {
|
||||
const providers = this.session.modelRegistry.authStorage.list();
|
||||
const loggedInProviders = providers.filter(
|
||||
(p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth",
|
||||
);
|
||||
if (loggedInProviders.length === 0) {
|
||||
this.showStatus("No OAuth providers logged in. Use /login first.");
|
||||
return;
|
||||
private getLoginProviderOptions(authType?: "oauth" | "api_key"): AuthSelectorProvider[] {
|
||||
const authStorage = this.session.modelRegistry.authStorage;
|
||||
const oauthProviders = authStorage.getOAuthProviders();
|
||||
const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
|
||||
const options: AuthSelectorProvider[] = oauthProviders.map((provider) => ({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
authType: "oauth",
|
||||
}));
|
||||
|
||||
const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
|
||||
for (const providerId of modelProviders) {
|
||||
if (oauthProviderIds.has(providerId) || API_KEY_LOGIN_PROVIDER_BLOCKLIST.has(providerId)) {
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
id: providerId,
|
||||
name: getApiKeyProviderDisplayName(providerId),
|
||||
authType: "api_key",
|
||||
});
|
||||
}
|
||||
|
||||
const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
|
||||
return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private getLogoutProviderOptions(): AuthSelectorProvider[] {
|
||||
const authStorage = this.session.modelRegistry.authStorage;
|
||||
const oauthNameById = new Map(authStorage.getOAuthProviders().map((provider) => [provider.id, provider.name]));
|
||||
const options: AuthSelectorProvider[] = [];
|
||||
|
||||
for (const providerId of authStorage.list()) {
|
||||
const credential = authStorage.get(providerId);
|
||||
if (!credential) {
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
id: providerId,
|
||||
name:
|
||||
credential.type === "oauth"
|
||||
? (oauthNameById.get(providerId) ?? providerId)
|
||||
: getApiKeyProviderDisplayName(providerId),
|
||||
authType: credential.type,
|
||||
});
|
||||
}
|
||||
|
||||
return options.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private showLoginAuthTypeSelector(): void {
|
||||
const subscriptionLabel = "Use a subscription";
|
||||
const apiKeyLabel = "Use an API key";
|
||||
this.showSelector((done) => {
|
||||
const selector = new ExtensionSelectorComponent(
|
||||
"Select authentication method:",
|
||||
[subscriptionLabel, apiKeyLabel],
|
||||
(option) => {
|
||||
done();
|
||||
const authType = option === subscriptionLabel ? "oauth" : "api_key";
|
||||
this.showLoginProviderSelector(authType);
|
||||
},
|
||||
() => {
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
},
|
||||
);
|
||||
return { component: selector, focus: selector };
|
||||
});
|
||||
}
|
||||
|
||||
private showLoginProviderSelector(authType: "oauth" | "api_key"): void {
|
||||
const providerOptions = this.getLoginProviderOptions(authType);
|
||||
if (providerOptions.length === 0) {
|
||||
this.showStatus(
|
||||
authType === "oauth" ? "No subscription providers available." : "No API key providers available.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSelector((done) => {
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
this.session.modelRegistry.authStorage,
|
||||
providerOptions,
|
||||
async (providerId: string) => {
|
||||
done();
|
||||
|
||||
const providerOption = providerOptions.find((provider) => provider.id === providerId);
|
||||
if (!providerOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (providerOption.authType === "oauth") {
|
||||
await this.showLoginDialog(providerOption.id, providerOption.name);
|
||||
} else {
|
||||
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
done();
|
||||
this.showLoginAuthTypeSelector();
|
||||
},
|
||||
);
|
||||
return { component: selector, focus: selector };
|
||||
});
|
||||
}
|
||||
|
||||
private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
||||
if (mode === "login") {
|
||||
this.showLoginAuthTypeSelector();
|
||||
return;
|
||||
}
|
||||
|
||||
const providerOptions = this.getLogoutProviderOptions();
|
||||
if (providerOptions.length === 0) {
|
||||
this.showStatus("No providers logged in. Use /login first.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSelector((done) => {
|
||||
const selector = new OAuthSelectorComponent(
|
||||
mode,
|
||||
this.session.modelRegistry.authStorage,
|
||||
providerOptions,
|
||||
async (providerId: string) => {
|
||||
done();
|
||||
|
||||
if (mode === "login") {
|
||||
await this.showLoginDialog(providerId);
|
||||
} else {
|
||||
// Logout flow
|
||||
const providerInfo = this.session.modelRegistry.authStorage
|
||||
.getOAuthProviders()
|
||||
.find((p) => p.id === providerId);
|
||||
const providerName = providerInfo?.name || providerId;
|
||||
const providerOption = providerOptions.find((provider) => provider.id === providerId);
|
||||
if (!providerOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.session.modelRegistry.authStorage.logout(providerId);
|
||||
this.session.modelRegistry.refresh();
|
||||
await this.updateAvailableProviderCount();
|
||||
this.showStatus(`Logged out of ${providerName}`);
|
||||
} catch (error: unknown) {
|
||||
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
try {
|
||||
this.session.modelRegistry.authStorage.logout(providerOption.id);
|
||||
this.session.modelRegistry.refresh();
|
||||
await this.updateAvailableProviderCount();
|
||||
const message =
|
||||
providerOption.authType === "oauth"
|
||||
? `Logged out of ${providerOption.name}`
|
||||
: `Removed API key for ${providerOption.name}`;
|
||||
this.showStatus(message);
|
||||
} catch (error: unknown) {
|
||||
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
@@ -4306,18 +4440,120 @@ export class InteractiveMode {
|
||||
});
|
||||
}
|
||||
|
||||
private async showLoginDialog(providerId: string): Promise<void> {
|
||||
const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
|
||||
const providerName = providerInfo?.name || providerId;
|
||||
private async completeProviderAuthentication(
|
||||
providerId: string,
|
||||
providerName: string,
|
||||
authType: "oauth" | "api_key",
|
||||
previousModel: Model<any> | undefined,
|
||||
): Promise<void> {
|
||||
this.session.modelRegistry.refresh();
|
||||
|
||||
const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
|
||||
|
||||
let selectedModel: Model<any> | undefined;
|
||||
let selectionError: string | undefined;
|
||||
if (isUnknownModel(previousModel)) {
|
||||
const availableModels = this.session.modelRegistry.getAvailable();
|
||||
const providerModels = availableModels.filter((model) => model.provider === providerId);
|
||||
if (!hasDefaultModelProvider(providerId)) {
|
||||
selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
|
||||
} else if (providerModels.length === 0) {
|
||||
selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
|
||||
} else {
|
||||
const defaultModelId = defaultModelPerProvider[providerId];
|
||||
selectedModel = providerModels.find((model) => model.id === defaultModelId);
|
||||
if (!selectedModel) {
|
||||
selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
|
||||
} else {
|
||||
try {
|
||||
await this.session.setModel(selectedModel);
|
||||
} catch (error: unknown) {
|
||||
selectedModel = undefined;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateAvailableProviderCount();
|
||||
this.footer.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
if (selectedModel) {
|
||||
this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
|
||||
this.checkDaxnutsEasterEgg(selectedModel);
|
||||
} else {
|
||||
this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
|
||||
if (selectionError) {
|
||||
this.showError(selectionError);
|
||||
} else {
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async showApiKeyLoginDialog(providerId: string, providerName: string): Promise<void> {
|
||||
const previousModel = this.session.model;
|
||||
|
||||
const dialog = new LoginDialogComponent(
|
||||
this.ui,
|
||||
providerId,
|
||||
(_success, _message) => {
|
||||
// Completion handled below
|
||||
},
|
||||
providerName,
|
||||
);
|
||||
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(dialog);
|
||||
this.ui.setFocus(dialog);
|
||||
this.ui.requestRender();
|
||||
|
||||
const restoreEditor = () => {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.editor);
|
||||
this.ui.setFocus(this.editor);
|
||||
this.ui.requestRender();
|
||||
};
|
||||
|
||||
try {
|
||||
const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
|
||||
if (!apiKey) {
|
||||
throw new Error("API key cannot be empty.");
|
||||
}
|
||||
|
||||
this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
|
||||
|
||||
restoreEditor();
|
||||
await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
|
||||
} catch (error: unknown) {
|
||||
restoreEditor();
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
if (errorMsg !== "Login cancelled") {
|
||||
this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async showLoginDialog(providerId: string, providerName: string): Promise<void> {
|
||||
const providerInfo = this.session.modelRegistry.authStorage
|
||||
.getOAuthProviders()
|
||||
.find((provider) => provider.id === providerId);
|
||||
const previousModel = this.session.model;
|
||||
|
||||
// Providers that use callback servers (can paste redirect URL)
|
||||
const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
|
||||
|
||||
// Create login dialog component
|
||||
const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
|
||||
// Completion handled below
|
||||
});
|
||||
const dialog = new LoginDialogComponent(
|
||||
this.ui,
|
||||
providerId,
|
||||
(_success, _message) => {
|
||||
// Completion handled below
|
||||
},
|
||||
providerName,
|
||||
);
|
||||
|
||||
// Show dialog in editor container
|
||||
this.editorContainer.clear();
|
||||
@@ -4384,51 +4620,7 @@ export class InteractiveMode {
|
||||
|
||||
// Success
|
||||
restoreEditor();
|
||||
this.session.modelRegistry.refresh();
|
||||
|
||||
let selectedModel: Model<any> | undefined;
|
||||
let selectionError: string | undefined;
|
||||
if (isUnknownModel(previousModel)) {
|
||||
const availableModels = this.session.modelRegistry.getAvailable();
|
||||
const providerModels = availableModels.filter((model) => model.provider === providerId);
|
||||
if (!hasDefaultModelProvider(providerId)) {
|
||||
selectionError = `Logged in to ${providerName}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
|
||||
} else if (providerModels.length === 0) {
|
||||
selectionError = `Logged in to ${providerName}, but no models are available for that provider. Use /model to select a model.`;
|
||||
} else {
|
||||
const defaultModelId = defaultModelPerProvider[providerId];
|
||||
selectedModel = providerModels.find((model) => model.id === defaultModelId);
|
||||
if (!selectedModel) {
|
||||
selectionError = `Logged in to ${providerName}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
|
||||
} else {
|
||||
try {
|
||||
await this.session.setModel(selectedModel);
|
||||
} catch (error: unknown) {
|
||||
selectedModel = undefined;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
selectionError = `Logged in to ${providerName}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateAvailableProviderCount();
|
||||
this.footer.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
if (selectedModel) {
|
||||
this.showStatus(
|
||||
`Logged in to ${providerName}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`,
|
||||
);
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
|
||||
this.checkDaxnutsEasterEgg(selectedModel);
|
||||
} else {
|
||||
this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
|
||||
if (selectionError) {
|
||||
this.showError(selectionError);
|
||||
} else {
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth();
|
||||
}
|
||||
}
|
||||
await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
|
||||
} catch (error: unknown) {
|
||||
restoreEditor();
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
Reference in New Issue
Block a user