fix(coding-agent): show models.json auth in login selector

This commit is contained in:
Armin Ronacher
2026-04-24 17:27:53 +02:00
parent 3e0ee69b5e
commit 39db145495
8 changed files with 197 additions and 139 deletions

View File

@@ -1,44 +0,0 @@
import type { AuthCredential, AuthStatus } from "../../../core/auth-storage.js";
export type SelectorAuthType = "oauth" | "api_key";
export type AuthSelectorIndicator =
| { kind: "configured"; label: string }
| { kind: "configured-other"; label: string }
| { kind: "runtime" }
| { kind: "environment"; label: string }
| { kind: "fallback" }
| { kind: "unconfigured" };
function getConfiguredLabel(authType: SelectorAuthType): string {
return authType === "oauth" ? "subscription configured" : "api key configured";
}
export function getAuthSelectorIndicator(
authType: SelectorAuthType,
credential: AuthCredential | undefined,
authStatus?: AuthStatus,
): AuthSelectorIndicator {
if (credential) {
const label = getConfiguredLabel(credential.type);
return credential.type === authType ? { kind: "configured", label } : { kind: "configured-other", label };
}
if (authType === "oauth") {
return { kind: "unconfigured" };
}
if (authStatus?.source === "runtime") {
return { kind: "runtime" };
}
if (authStatus?.source === "environment") {
return { kind: "environment", label: authStatus.label ?? "API key" };
}
if (authStatus?.source === "fallback") {
return { kind: "fallback" };
}
return { kind: "unconfigured" };
}

View File

@@ -7,9 +7,8 @@ import {
Spacer,
TruncatedText,
} from "@mariozechner/pi-tui";
import type { AuthStorage } from "../../../core/auth-storage.js";
import type { AuthStatus, AuthStorage } from "../../../core/auth-storage.js";
import { theme } from "../theme/theme.js";
import { getAuthSelectorIndicator } from "./auth-selector-status.js";
import { DynamicBorder } from "./dynamic-border.js";
export type AuthSelectorProvider = {
@@ -40,6 +39,7 @@ export class OAuthSelectorComponent extends Container implements Focusable {
private selectedIndex: number = 0;
private mode: "login" | "logout";
private authStorage: AuthStorage;
private getAuthStatus: (providerId: string) => AuthStatus;
private onSelectCallback: (providerId: string) => void;
private onCancelCallback: () => void;
@@ -49,11 +49,13 @@ export class OAuthSelectorComponent extends Container implements Focusable {
providers: AuthSelectorProvider[],
onSelect: (providerId: string) => void,
onCancel: () => void,
getAuthStatus?: (providerId: string) => AuthStatus,
) {
super();
this.mode = mode;
this.authStorage = authStorage;
this.getAuthStatus = getAuthStatus ?? ((providerId) => this.authStorage.getAuthStatus(providerId));
this.allProviders = providers;
this.filteredProviders = providers;
this.onSelectCallback = onSelect;
@@ -147,29 +149,29 @@ export class OAuthSelectorComponent extends Container implements Focusable {
}
private formatStatusIndicator(provider: AuthSelectorProvider): string {
const status = provider.authType === "api_key" ? this.authStorage.getAuthStatus(provider.id) : undefined;
const indicator = getAuthSelectorIndicator(provider.authType, this.authStorage.get(provider.id), status);
const credential = this.authStorage.get(provider.id);
if (credential?.type === provider.authType) return theme.fg("success", " ✓ configured");
if (credential) {
const label = credential.type === "oauth" ? "subscription configured" : "API key configured";
return theme.fg("muted", " • ") + theme.fg("warning", label);
}
if (provider.authType !== "api_key") return theme.fg("muted", " • unconfigured");
if (indicator.kind === "configured") {
return theme.fg("success", `${indicator.label}`);
const status = this.getAuthStatus(provider.id);
switch (status.source) {
case "environment":
return theme.fg("success", ` ✓ env: ${status.label ?? "API key"}`);
case "runtime":
return theme.fg("success", " ✓ runtime API key");
case "fallback":
return theme.fg("success", " ✓ custom API key");
case "models_json_key":
return theme.fg("success", " ✓ key in models.json");
case "models_json_command":
return theme.fg("success", " ✓ command in models.json");
default:
return theme.fg("muted", " • unconfigured");
}
if (indicator.kind === "configured-other") {
return theme.fg("muted", " • ") + theme.fg("warning", indicator.label);
}
const base = theme.fg("muted", " • unconfigured");
if (indicator.kind === "environment") {
return base + theme.fg("success", ` · env: ${indicator.label}`);
}
if (indicator.kind === "runtime") {
return base + theme.fg("success", " · runtime API key");
}
if (indicator.kind === "fallback") {
return base + theme.fg("success", " · custom API key");
}
return base;
}
handleInput(keyData: string): void {

View File

@@ -4438,6 +4438,7 @@ export class InteractiveMode {
done();
this.showLoginAuthTypeSelector();
},
(providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId),
);
return { component: selector, focus: selector };
});
@@ -4451,7 +4452,9 @@ export class InteractiveMode {
const providerOptions = this.getLogoutProviderOptions();
if (providerOptions.length === 0) {
this.showStatus("No providers logged in. Use /login first.");
this.showStatus(
"No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.",
);
return;
}
@@ -4475,7 +4478,7 @@ export class InteractiveMode {
const message =
providerOption.authType === "oauth"
? `Logged out of ${providerOption.name}`
: `Removed API key for ${providerOption.name}`;
: `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
this.showStatus(message);
} catch (error: unknown) {
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);