feat(coding-agent): support provider arguments for login
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added `/login <provider>` support with provider autocomplete.
|
||||||
- Added public SDK exports for CLI-equivalent model and scoped-model resolution ([#6201](https://github.com/earendil-works/pi/issues/6201)).
|
- Added public SDK exports for CLI-equivalent model and scoped-model resolution ([#6201](https://github.com/earendil-works/pi/issues/6201)).
|
||||||
- Added extension entry renderers for persisted display-only session entries that are rendered in interactive mode without being sent to the model context.
|
- Added extension entry renderers for persisted display-only session entries that are rendered in interactive mode without being sent to the model context.
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ export interface SlashCommandInfo {
|
|||||||
export interface BuiltinSlashCommand {
|
export interface BuiltinSlashCommand {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
argumentHint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
||||||
{ name: "settings", description: "Open settings menu" },
|
{ name: "settings", description: "Open settings menu" },
|
||||||
{ name: "model", description: "Select model (opens selector UI)" },
|
{ name: "model", description: "Select model (opens selector UI)", argumentHint: "<provider/model>" },
|
||||||
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
||||||
{ name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },
|
{ name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },
|
||||||
{ name: "import", description: "Import and resume a session from a JSONL file" },
|
{ name: "import", description: "Import and resume a session from a JSONL file" },
|
||||||
@@ -31,7 +32,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|||||||
{ name: "clone", description: "Duplicate the current session at the current position" },
|
{ name: "clone", description: "Duplicate the current session at the current position" },
|
||||||
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
||||||
{ name: "trust", description: "Save project trust decision for future sessions" },
|
{ name: "trust", description: "Save project trust decision for future sessions" },
|
||||||
{ name: "login", description: "Configure provider authentication" },
|
{ name: "login", description: "Configure provider authentication", argumentHint: "<provider>" },
|
||||||
{ name: "logout", description: "Remove provider authentication" },
|
{ name: "logout", description: "Remove provider authentication" },
|
||||||
{ name: "new", description: "Start a new session" },
|
{ name: "new", description: "Start a new session" },
|
||||||
{ name: "compact", description: "Manually compact the session context" },
|
{ name: "compact", description: "Manually compact the session context" },
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export type AuthSelectorProvider = {
|
|||||||
authType: "oauth" | "api_key";
|
authType: "oauth" | "api_key";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function formatAuthSelectorProviderType(authType: AuthSelectorProvider["authType"]): string {
|
||||||
|
return authType === "oauth" ? "subscription" : "API key";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that renders an auth provider selector
|
* Component that renders an auth provider selector
|
||||||
*/
|
*/
|
||||||
@@ -40,16 +44,18 @@ export class OAuthSelectorComponent extends Container implements Focusable {
|
|||||||
private mode: "login" | "logout";
|
private mode: "login" | "logout";
|
||||||
private authStorage: AuthStorage;
|
private authStorage: AuthStorage;
|
||||||
private getAuthStatus: (providerId: string) => AuthStatus;
|
private getAuthStatus: (providerId: string) => AuthStatus;
|
||||||
private onSelectCallback: (providerId: string) => void;
|
private onSelectCallback: (providerId: string, authType: AuthSelectorProvider["authType"]) => void;
|
||||||
private onCancelCallback: () => void;
|
private onCancelCallback: () => void;
|
||||||
|
private showAuthTypeLabels: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
mode: "login" | "logout",
|
mode: "login" | "logout",
|
||||||
authStorage: AuthStorage,
|
authStorage: AuthStorage,
|
||||||
providers: AuthSelectorProvider[],
|
providers: AuthSelectorProvider[],
|
||||||
onSelect: (providerId: string) => void,
|
onSelect: (providerId: string, authType: AuthSelectorProvider["authType"]) => void,
|
||||||
onCancel: () => void,
|
onCancel: () => void,
|
||||||
getAuthStatus?: (providerId: string) => AuthStatus,
|
getAuthStatus?: (providerId: string) => AuthStatus,
|
||||||
|
initialSearchInput?: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -58,6 +64,7 @@ export class OAuthSelectorComponent extends Container implements Focusable {
|
|||||||
this.getAuthStatus = getAuthStatus ?? ((providerId) => this.authStorage.getAuthStatus(providerId));
|
this.getAuthStatus = getAuthStatus ?? ((providerId) => this.authStorage.getAuthStatus(providerId));
|
||||||
this.allProviders = providers;
|
this.allProviders = providers;
|
||||||
this.filteredProviders = providers;
|
this.filteredProviders = providers;
|
||||||
|
this.showAuthTypeLabels = new Set(providers.map((provider) => provider.authType)).size > 1;
|
||||||
this.onSelectCallback = onSelect;
|
this.onSelectCallback = onSelect;
|
||||||
this.onCancelCallback = onCancel;
|
this.onCancelCallback = onCancel;
|
||||||
|
|
||||||
@@ -71,10 +78,13 @@ export class OAuthSelectorComponent extends Container implements Focusable {
|
|||||||
this.addChild(new Spacer(1));
|
this.addChild(new Spacer(1));
|
||||||
|
|
||||||
this.searchInput = new Input();
|
this.searchInput = new Input();
|
||||||
|
if (initialSearchInput) {
|
||||||
|
this.searchInput.setValue(initialSearchInput);
|
||||||
|
}
|
||||||
this.searchInput.onSubmit = () => {
|
this.searchInput.onSubmit = () => {
|
||||||
const selectedProvider = this.filteredProviders[this.selectedIndex];
|
const selectedProvider = this.filteredProviders[this.selectedIndex];
|
||||||
if (selectedProvider) {
|
if (selectedProvider) {
|
||||||
this.onSelectCallback(selectedProvider.id);
|
this.onSelectCallback(selectedProvider.id, selectedProvider.authType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.addChild(this.searchInput);
|
this.addChild(this.searchInput);
|
||||||
@@ -90,7 +100,7 @@ export class OAuthSelectorComponent extends Container implements Focusable {
|
|||||||
this.addChild(new DynamicBorder());
|
this.addChild(new DynamicBorder());
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
this.filterProviders("");
|
this.filterProviders(initialSearchInput ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterProviders(query: string): void {
|
private filterProviders(query: string): void {
|
||||||
@@ -118,14 +128,17 @@ export class OAuthSelectorComponent extends Container implements Focusable {
|
|||||||
const isSelected = i === this.selectedIndex;
|
const isSelected = i === this.selectedIndex;
|
||||||
|
|
||||||
const statusIndicator = this.formatStatusIndicator(provider);
|
const statusIndicator = this.formatStatusIndicator(provider);
|
||||||
|
const authTypeLabel = this.showAuthTypeLabels
|
||||||
|
? theme.fg("muted", ` [${formatAuthSelectorProviderType(provider.authType)}]`)
|
||||||
|
: "";
|
||||||
let line = "";
|
let line = "";
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const prefix = theme.fg("accent", "→ ");
|
const prefix = theme.fg("accent", "→ ");
|
||||||
const text = theme.fg("accent", provider.name);
|
const text = theme.fg("accent", provider.name);
|
||||||
line = prefix + text + statusIndicator;
|
line = prefix + text + authTypeLabel + statusIndicator;
|
||||||
} else {
|
} else {
|
||||||
const text = ` ${theme.fg("text", provider.name)}`;
|
const text = ` ${theme.fg("text", provider.name)}`;
|
||||||
line = text + statusIndicator;
|
line = text + authTypeLabel + statusIndicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listContainer.addChild(new TruncatedText(line, 1, 0));
|
this.listContainer.addChild(new TruncatedText(line, 1, 0));
|
||||||
@@ -192,7 +205,7 @@ export class OAuthSelectorComponent extends Container implements Focusable {
|
|||||||
else if (kb.matches(keyData, "tui.select.confirm")) {
|
else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||||
const selectedProvider = this.filteredProviders[this.selectedIndex];
|
const selectedProvider = this.filteredProviders[this.selectedIndex];
|
||||||
if (selectedProvider) {
|
if (selectedProvider) {
|
||||||
this.onSelectCallback(selectedProvider.id);
|
this.onSelectCallback(selectedProvider.id, selectedProvider.authType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Escape or Ctrl+C
|
// Escape or Ctrl+C
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ import { FooterComponent } from "./components/footer.ts";
|
|||||||
import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.ts";
|
import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.ts";
|
||||||
import { LoginDialogComponent } from "./components/login-dialog.ts";
|
import { LoginDialogComponent } from "./components/login-dialog.ts";
|
||||||
import { ModelSelectorComponent } from "./components/model-selector.ts";
|
import { ModelSelectorComponent } from "./components/model-selector.ts";
|
||||||
import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.ts";
|
import {
|
||||||
|
type AuthSelectorProvider,
|
||||||
|
formatAuthSelectorProviderType,
|
||||||
|
OAuthSelectorComponent,
|
||||||
|
} from "./components/oauth-selector.ts";
|
||||||
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.ts";
|
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.ts";
|
||||||
import { SessionSelectorComponent } from "./components/session-selector.ts";
|
import { SessionSelectorComponent } from "./components/session-selector.ts";
|
||||||
import { SettingsSelectorComponent } from "./components/settings-selector.ts";
|
import { SettingsSelectorComponent } from "./components/settings-selector.ts";
|
||||||
@@ -255,6 +259,59 @@ export function isApiKeyLoginProvider(
|
|||||||
return !oauthProviderIds.has(providerId);
|
return !oauthProviderIds.has(providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginProviderCompletionOption = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
authTypes: AuthSelectorProvider["authType"][];
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 } satisfies Record<AuthSelectorProvider["authType"], number>;
|
||||||
|
|
||||||
|
function createFuzzyAutocompleteItems<T>(
|
||||||
|
items: T[],
|
||||||
|
prefix: string,
|
||||||
|
getSearchText: (item: T) => string,
|
||||||
|
toAutocompleteItem: (item: T) => AutocompleteItem,
|
||||||
|
): AutocompleteItem[] | null {
|
||||||
|
const filtered = fuzzyFilter(items, prefix, getSearchText);
|
||||||
|
if (filtered.length === 0) return null;
|
||||||
|
return filtered.map(toAutocompleteItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoginProviderCompletionOptions(
|
||||||
|
providerOptions: readonly AuthSelectorProvider[],
|
||||||
|
): LoginProviderCompletionOption[] {
|
||||||
|
const byId = new Map<string, LoginProviderCompletionOption>();
|
||||||
|
for (const provider of providerOptions) {
|
||||||
|
const existing = byId.get(provider.id);
|
||||||
|
if (existing) {
|
||||||
|
if (!existing.authTypes.includes(provider.authType)) {
|
||||||
|
existing.authTypes.push(provider.authType);
|
||||||
|
existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byId.set(provider.id, {
|
||||||
|
id: provider.id,
|
||||||
|
name: provider.name,
|
||||||
|
authTypes: [provider.authType],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoginProviderSearchText(provider: LoginProviderCompletionOption): string {
|
||||||
|
const authTypes = provider.authTypes
|
||||||
|
.map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
|
||||||
|
.join(" ");
|
||||||
|
return `${provider.id} ${provider.name} ${authTypes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLoginProviderCompletionDescription(provider: LoginProviderCompletionOption): string {
|
||||||
|
const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
|
||||||
|
return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for InteractiveMode initialization.
|
* Options for InteractiveMode initialization.
|
||||||
*/
|
*/
|
||||||
@@ -502,6 +559,7 @@ export class InteractiveMode {
|
|||||||
const slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.map((command) => ({
|
const slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.map((command) => ({
|
||||||
name: command.name,
|
name: command.name,
|
||||||
description: command.description,
|
description: command.description,
|
||||||
|
...(command.argumentHint && { argumentHint: command.argumentHint }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const modelCommand = slashCommands.find((command) => command.name === "model");
|
const modelCommand = slashCommands.find((command) => command.name === "model");
|
||||||
@@ -523,12 +581,7 @@ export class InteractiveMode {
|
|||||||
label: `${m.provider}/${m.id}`,
|
label: `${m.provider}/${m.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Fuzzy filter by model ID + provider in either order.
|
return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
|
||||||
const filtered = fuzzyFilter(items, prefix, getModelSearchText);
|
|
||||||
|
|
||||||
if (filtered.length === 0) return null;
|
|
||||||
|
|
||||||
return filtered.map((item) => ({
|
|
||||||
value: item.label,
|
value: item.label,
|
||||||
label: item.id,
|
label: item.id,
|
||||||
description: item.provider,
|
description: item.provider,
|
||||||
@@ -536,6 +589,18 @@ export class InteractiveMode {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginCommand = slashCommands.find((command) => command.name === "login");
|
||||||
|
if (loginCommand) {
|
||||||
|
loginCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null => {
|
||||||
|
const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
|
||||||
|
return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
|
||||||
|
value: provider.id,
|
||||||
|
label: provider.id,
|
||||||
|
description: formatLoginProviderCompletionDescription(provider),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Convert prompt templates to SlashCommand format for autocomplete
|
// Convert prompt templates to SlashCommand format for autocomplete
|
||||||
const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
|
const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
|
||||||
name: cmd.name,
|
name: cmd.name,
|
||||||
@@ -2638,9 +2703,10 @@ export class InteractiveMode {
|
|||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === "/login") {
|
if (text === "/login" || text.startsWith("/login ")) {
|
||||||
this.showOAuthSelector("login");
|
const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
|
await this.handleLoginCommand(providerRef);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === "/logout") {
|
if (text === "/logout") {
|
||||||
@@ -4717,16 +4783,96 @@ export class InteractiveMode {
|
|||||||
return options.sort((a, b) => a.name.localeCompare(b.name));
|
return options.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
private showLoginAuthTypeSelector(): void {
|
private findLoginProviderOptions(providerRef: string): AuthSelectorProvider[] {
|
||||||
|
const normalizedProviderRef = providerRef.trim().toLowerCase();
|
||||||
|
if (!normalizedProviderRef) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getLoginProviderOptions().filter(
|
||||||
|
(provider) =>
|
||||||
|
provider.id.toLowerCase() === normalizedProviderRef ||
|
||||||
|
provider.name.toLowerCase() === normalizedProviderRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleLoginCommand(providerRef?: string): Promise<void> {
|
||||||
|
if (!providerRef) {
|
||||||
|
this.showLoginAuthTypeSelector();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerOptions = this.findLoginProviderOptions(providerRef);
|
||||||
|
if (providerOptions.length === 1) {
|
||||||
|
await this.startProviderLogin(providerOptions[0]!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerOptions.length > 1) {
|
||||||
|
const providerIds = new Set(providerOptions.map((provider) => provider.id));
|
||||||
|
if (providerIds.size === 1) {
|
||||||
|
this.showLoginAuthTypeSelector(providerOptions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showLoginProviderSelector(undefined, providerRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startProviderLogin(providerOption: AuthSelectorProvider): Promise<void> {
|
||||||
|
if (providerOption.authType === "oauth") {
|
||||||
|
await this.showLoginDialog(providerOption.id, providerOption.name);
|
||||||
|
} else if (providerOption.id === BEDROCK_PROVIDER_ID) {
|
||||||
|
this.showBedrockSetupDialog(providerOption.id, providerOption.name);
|
||||||
|
} else {
|
||||||
|
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showLoginAuthTypeSelector(providerOptions?: AuthSelectorProvider[]): void {
|
||||||
const subscriptionLabel = "Use a subscription";
|
const subscriptionLabel = "Use a subscription";
|
||||||
const apiKeyLabel = "Use an API key";
|
const apiKeyLabel = "Use an API key";
|
||||||
|
const availableAuthTypes = providerOptions
|
||||||
|
? new Set(providerOptions.map((provider) => provider.authType))
|
||||||
|
: new Set<AuthSelectorProvider["authType"]>(["oauth", "api_key"]);
|
||||||
|
const options: string[] = [];
|
||||||
|
if (availableAuthTypes.has("oauth")) {
|
||||||
|
options.push(subscriptionLabel);
|
||||||
|
}
|
||||||
|
if (availableAuthTypes.has("api_key")) {
|
||||||
|
options.push(apiKeyLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length === 0) {
|
||||||
|
this.showStatus("No login methods available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerOptions && options.length === 1) {
|
||||||
|
const providerOption = providerOptions[0];
|
||||||
|
if (providerOption) {
|
||||||
|
void this.startProviderLogin(providerOption);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = providerOptions?.[0]
|
||||||
|
? `Select authentication method for ${providerOptions[0].name}:`
|
||||||
|
: "Select authentication method:";
|
||||||
this.showSelector((done) => {
|
this.showSelector((done) => {
|
||||||
const selector = new ExtensionSelectorComponent(
|
const selector = new ExtensionSelectorComponent(
|
||||||
"Select authentication method:",
|
title,
|
||||||
[subscriptionLabel, apiKeyLabel],
|
options,
|
||||||
(option) => {
|
(option) => {
|
||||||
done();
|
done();
|
||||||
const authType = option === subscriptionLabel ? "oauth" : "api_key";
|
const authType = option === subscriptionLabel ? "oauth" : "api_key";
|
||||||
|
if (providerOptions) {
|
||||||
|
const providerOption = providerOptions.find((provider) => provider.authType === authType);
|
||||||
|
if (providerOption) {
|
||||||
|
void this.startProviderLogin(providerOption);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.showLoginProviderSelector(authType);
|
this.showLoginProviderSelector(authType);
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
@@ -4738,12 +4884,16 @@ export class InteractiveMode {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private showLoginProviderSelector(authType: "oauth" | "api_key"): void {
|
private showLoginProviderSelector(authType?: AuthSelectorProvider["authType"], initialSearchInput?: string): void {
|
||||||
const providerOptions = this.getLoginProviderOptions(authType);
|
const providerOptions = this.getLoginProviderOptions(authType);
|
||||||
if (providerOptions.length === 0) {
|
if (providerOptions.length === 0) {
|
||||||
this.showStatus(
|
const message =
|
||||||
authType === "oauth" ? "No subscription providers available." : "No API key providers available.",
|
authType === "oauth"
|
||||||
);
|
? "No subscription providers available."
|
||||||
|
: authType === "api_key"
|
||||||
|
? "No API key providers available."
|
||||||
|
: "No login providers available.";
|
||||||
|
this.showStatus(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4752,27 +4902,28 @@ export class InteractiveMode {
|
|||||||
"login",
|
"login",
|
||||||
this.session.modelRegistry.authStorage,
|
this.session.modelRegistry.authStorage,
|
||||||
providerOptions,
|
providerOptions,
|
||||||
async (providerId: string) => {
|
async (providerId, selectedAuthType) => {
|
||||||
done();
|
done();
|
||||||
|
|
||||||
const providerOption = providerOptions.find((provider) => provider.id === providerId);
|
const providerOption = providerOptions.find(
|
||||||
|
(provider) => provider.id === providerId && provider.authType === selectedAuthType,
|
||||||
|
);
|
||||||
if (!providerOption) {
|
if (!providerOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerOption.authType === "oauth") {
|
await this.startProviderLogin(providerOption);
|
||||||
await this.showLoginDialog(providerOption.id, providerOption.name);
|
|
||||||
} else if (providerOption.id === BEDROCK_PROVIDER_ID) {
|
|
||||||
this.showBedrockSetupDialog(providerOption.id, providerOption.name);
|
|
||||||
} else {
|
|
||||||
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
done();
|
done();
|
||||||
this.showLoginAuthTypeSelector();
|
if (authType) {
|
||||||
|
this.showLoginAuthTypeSelector();
|
||||||
|
} else {
|
||||||
|
this.ui.requestRender();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId),
|
(providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId),
|
||||||
|
initialSearchInput,
|
||||||
);
|
);
|
||||||
return { component: selector, focus: selector };
|
return { component: selector, focus: selector };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { type Component, Container, type Focusable, TUI } from "../../tui/src/tu
|
|||||||
import { VirtualTerminal } from "../../tui/test/virtual-terminal.ts";
|
import { VirtualTerminal } from "../../tui/test/virtual-terminal.ts";
|
||||||
import type { AutocompleteProviderFactory } from "../src/core/extensions/types.ts";
|
import type { AutocompleteProviderFactory } from "../src/core/extensions/types.ts";
|
||||||
import type { SourceInfo } from "../src/core/source-info.ts";
|
import type { SourceInfo } from "../src/core/source-info.ts";
|
||||||
|
import type { AuthSelectorProvider } from "../src/modes/interactive/components/oauth-selector.ts";
|
||||||
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
|
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
|
||||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||||
|
|
||||||
@@ -423,8 +424,62 @@ describe("InteractiveMode.createBaseAutocompleteProvider", () => {
|
|||||||
"github-copilot/gpt-5.2-codex",
|
"github-copilot/gpt-5.2-codex",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
test("matches login command arguments by provider id and name", async () => {
|
||||||
|
type FakeInteractiveMode = {
|
||||||
|
session: {
|
||||||
|
scopedModels: [];
|
||||||
|
modelRegistry: { getAvailable: () => [] };
|
||||||
|
promptTemplates: [];
|
||||||
|
extensionRunner: { getRegisteredCommands: () => [] };
|
||||||
|
resourceLoader: { getSkills: () => { skills: [] } };
|
||||||
|
};
|
||||||
|
settingsManager: { getEnableSkillCommands: () => boolean };
|
||||||
|
skillCommands: Map<string, string>;
|
||||||
|
sessionManager: { getCwd: () => string };
|
||||||
|
fdPath: null;
|
||||||
|
getLoginProviderOptions: () => AuthSelectorProvider[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBaseAutocompleteProvider = (
|
||||||
|
InteractiveMode as unknown as {
|
||||||
|
prototype: { createBaseAutocompleteProvider(this: FakeInteractiveMode): AutocompleteProvider };
|
||||||
|
}
|
||||||
|
).prototype.createBaseAutocompleteProvider;
|
||||||
|
const fakeThis: FakeInteractiveMode = {
|
||||||
|
session: {
|
||||||
|
scopedModels: [],
|
||||||
|
modelRegistry: { getAvailable: () => [] },
|
||||||
|
promptTemplates: [],
|
||||||
|
extensionRunner: { getRegisteredCommands: () => [] },
|
||||||
|
resourceLoader: { getSkills: () => ({ skills: [] }) },
|
||||||
|
},
|
||||||
|
settingsManager: { getEnableSkillCommands: () => false },
|
||||||
|
skillCommands: new Map(),
|
||||||
|
sessionManager: { getCwd: () => "/tmp" },
|
||||||
|
fdPath: null,
|
||||||
|
getLoginProviderOptions: () => [
|
||||||
|
{ id: "anthropic", name: "Anthropic", authType: "oauth" },
|
||||||
|
{ id: "anthropic", name: "Anthropic", authType: "api_key" },
|
||||||
|
{ id: "openai", name: "OpenAI", authType: "api_key" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const provider = createBaseAutocompleteProvider.call(fakeThis);
|
||||||
|
const line = "/login subscription anthrop";
|
||||||
|
const suggestions = await provider.getSuggestions([line], 0, line.length, {
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(suggestions?.items).toEqual([
|
||||||
|
{
|
||||||
|
value: "anthropic",
|
||||||
|
label: "anthropic",
|
||||||
|
description: "Anthropic · subscription/API key",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe("InteractiveMode.showLoadedResources", () => {
|
describe("InteractiveMode.showLoadedResources", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
initTheme("dark");
|
initTheme("dark");
|
||||||
|
|||||||
Reference in New Issue
Block a user