|
|
|
@@ -8,15 +8,8 @@ import * as fs from "node:fs";
|
|
|
|
|
import * as os from "node:os";
|
|
|
|
|
import * as path from "node:path";
|
|
|
|
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
|
|
|
import {
|
|
|
|
|
type AssistantMessage,
|
|
|
|
|
getProviders,
|
|
|
|
|
type ImageContent,
|
|
|
|
|
type Message,
|
|
|
|
|
type Model,
|
|
|
|
|
type OAuthProviderId,
|
|
|
|
|
type OAuthSelectPrompt,
|
|
|
|
|
} from "@earendil-works/pi-ai/compat";
|
|
|
|
|
import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai";
|
|
|
|
|
import type { AssistantMessage, ImageContent, Message, Model } from "@earendil-works/pi-ai/compat";
|
|
|
|
|
import type {
|
|
|
|
|
AutocompleteItem,
|
|
|
|
|
AutocompleteProvider,
|
|
|
|
@@ -54,7 +47,6 @@ import {
|
|
|
|
|
getAgentDir,
|
|
|
|
|
getAuthPath,
|
|
|
|
|
getDebugLogPath,
|
|
|
|
|
getDocsPath,
|
|
|
|
|
getShareViewerUrl,
|
|
|
|
|
VERSION,
|
|
|
|
|
} from "../../config.ts";
|
|
|
|
@@ -85,7 +77,6 @@ import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.t
|
|
|
|
|
import { createCompactionSummaryMessage } from "../../core/messages.ts";
|
|
|
|
|
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts";
|
|
|
|
|
import { DefaultPackageManager } from "../../core/package-manager.ts";
|
|
|
|
|
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.ts";
|
|
|
|
|
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
|
|
|
|
|
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
|
|
|
|
|
import { type SessionEntry, SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.ts";
|
|
|
|
@@ -212,7 +203,7 @@ function isDeadTerminalError(error: unknown): boolean {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.";
|
|
|
|
|
"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.";
|
|
|
|
|
|
|
|
|
|
function isAnthropicSubscriptionAuthKey(apiKey: string | undefined): boolean {
|
|
|
|
|
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
|
|
@@ -248,24 +239,6 @@ function hasDefaultModelProvider(providerId: string): providerId is keyof typeof
|
|
|
|
|
return providerId in defaultModelPerProvider;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
|
|
|
|
|
|
|
|
|
const BUILT_IN_MODEL_PROVIDERS = new Set<string>(getProviders());
|
|
|
|
|
|
|
|
|
|
export function isApiKeyLoginProvider(
|
|
|
|
|
providerId: string,
|
|
|
|
|
oauthProviderIds: ReadonlySet<string>,
|
|
|
|
|
builtInProviderIds: ReadonlySet<string> = BUILT_IN_MODEL_PROVIDERS,
|
|
|
|
|
): boolean {
|
|
|
|
|
if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (builtInProviderIds.has(providerId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return !oauthProviderIds.has(providerId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LoginProviderCompletionOption = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
@@ -571,12 +544,12 @@ export class InteractiveMode {
|
|
|
|
|
|
|
|
|
|
const modelCommand = slashCommands.find((command) => command.name === "model");
|
|
|
|
|
if (modelCommand) {
|
|
|
|
|
modelCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null => {
|
|
|
|
|
modelCommand.getArgumentCompletions = async (prefix: string): Promise<AutocompleteItem[] | null> => {
|
|
|
|
|
// Get available models (scoped or from registry)
|
|
|
|
|
const models =
|
|
|
|
|
this.session.scopedModels.length > 0
|
|
|
|
|
? this.session.scopedModels.map((s) => s.model)
|
|
|
|
|
: this.session.modelRegistry.getAvailable();
|
|
|
|
|
: await this.session.modelRuntime.getAvailable();
|
|
|
|
|
|
|
|
|
|
if (models.length === 0) return null;
|
|
|
|
|
|
|
|
|
@@ -879,7 +852,7 @@ export class InteractiveMode {
|
|
|
|
|
this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const modelsJsonError = this.session.modelRegistry.getError();
|
|
|
|
|
const modelsJsonError = this.session.modelRuntime.getError();
|
|
|
|
|
if (modelsJsonError) {
|
|
|
|
|
this.showError(`models.json error: ${modelsJsonError}`);
|
|
|
|
|
}
|
|
|
|
@@ -1779,7 +1752,7 @@ export class InteractiveMode {
|
|
|
|
|
hasUI: true,
|
|
|
|
|
cwd: this.sessionManager.getCwd(),
|
|
|
|
|
sessionManager: this.sessionManager,
|
|
|
|
|
modelRegistry: this.session.modelRegistry,
|
|
|
|
|
modelRegistry: extensionRunner.getModelRegistry(),
|
|
|
|
|
model: this.session.model,
|
|
|
|
|
isIdle: () => this.session.isIdle,
|
|
|
|
|
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
|
|
|
|
@@ -3288,7 +3261,7 @@ export class InteractiveMode {
|
|
|
|
|
// Cache-miss notices are not persisted; re-derive them from the full entry
|
|
|
|
|
// list and re-inject them after the assistant messages that paid for them.
|
|
|
|
|
const cacheMisses = this.settingsManager.getShowCacheMissNotices()
|
|
|
|
|
? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
|
|
|
|
|
? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
|
|
|
|
|
: new Map<AssistantMessage, CacheMiss>();
|
|
|
|
|
|
|
|
|
|
if (options.updateFooter) {
|
|
|
|
@@ -3392,7 +3365,7 @@ export class InteractiveMode {
|
|
|
|
|
if (!this.settingsManager.getShowCacheMissNotices()) return;
|
|
|
|
|
|
|
|
|
|
// Entries don't contain `message` yet: message_end fires before persistence.
|
|
|
|
|
const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
|
|
|
|
|
const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
|
|
|
|
|
if (miss) this.addCacheMissNotice(miss);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4325,9 +4298,9 @@ export class InteractiveMode {
|
|
|
|
|
return this.session.scopedModels.map((scoped) => scoped.model);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.session.modelRegistry.refresh();
|
|
|
|
|
try {
|
|
|
|
|
return await this.session.modelRegistry.getAvailable();
|
|
|
|
|
await this.session.modelRuntime.refresh();
|
|
|
|
|
return [...(await this.session.modelRuntime.getAvailable())];
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
@@ -4353,15 +4326,13 @@ export class InteractiveMode {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
|
|
|
|
|
if (storedCredential?.type === "oauth") {
|
|
|
|
|
this.anthropicSubscriptionWarningShown = true;
|
|
|
|
|
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
|
|
|
|
|
if ((await this.session.modelRuntime.checkAuth("anthropic"))?.type === "oauth") {
|
|
|
|
|
this.anthropicSubscriptionWarningShown = true;
|
|
|
|
|
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const apiKey = (await this.session.modelRuntime.getAuth(model.provider))?.auth.apiKey;
|
|
|
|
|
if (!isAnthropicSubscriptionAuthKey(apiKey)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -4429,7 +4400,7 @@ export class InteractiveMode {
|
|
|
|
|
this.ui,
|
|
|
|
|
this.session.model,
|
|
|
|
|
this.settingsManager,
|
|
|
|
|
this.session.modelRegistry,
|
|
|
|
|
this.session.modelRuntime,
|
|
|
|
|
this.session.scopedModels,
|
|
|
|
|
async (model) => {
|
|
|
|
|
try {
|
|
|
|
@@ -4457,8 +4428,8 @@ export class InteractiveMode {
|
|
|
|
|
|
|
|
|
|
private async showModelsSelector(): Promise<void> {
|
|
|
|
|
// Get all available models
|
|
|
|
|
this.session.modelRegistry.refresh();
|
|
|
|
|
const allModels = this.session.modelRegistry.getAvailable();
|
|
|
|
|
await this.session.modelRuntime.refresh();
|
|
|
|
|
const allModels = [...(await this.session.modelRuntime.getAvailable())];
|
|
|
|
|
|
|
|
|
|
if (allModels.length === 0) {
|
|
|
|
|
this.showStatus("No models available");
|
|
|
|
@@ -4479,7 +4450,7 @@ export class InteractiveMode {
|
|
|
|
|
// Fall back to settings
|
|
|
|
|
const patterns = this.settingsManager.getEnabledModels();
|
|
|
|
|
if (patterns !== undefined && patterns.length > 0) {
|
|
|
|
|
const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
|
|
|
|
|
const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
|
|
|
|
|
currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -4488,7 +4459,7 @@ export class InteractiveMode {
|
|
|
|
|
const updateSessionModels = async (enabledIds: string[] | null) => {
|
|
|
|
|
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
|
|
|
|
if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
|
|
|
|
|
const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
|
|
|
|
|
const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
|
|
|
|
|
this.session.setScopedModels(
|
|
|
|
|
newScopedModels.map((sm) => ({
|
|
|
|
|
model: sm.model,
|
|
|
|
@@ -4790,48 +4761,46 @@ export class InteractiveMode {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
|
|
|
|
|
continue;
|
|
|
|
|
const options: AuthSelectorProvider[] = [];
|
|
|
|
|
for (const provider of this.session.modelRuntime.getProviders()) {
|
|
|
|
|
const authStatus = this.session.modelRuntime.getProviderAuthStatus(provider.id);
|
|
|
|
|
const status = authStatus.configured
|
|
|
|
|
? {
|
|
|
|
|
type: this.session.modelRuntime.isUsingOAuth(provider.id) ? ("oauth" as const) : ("api_key" as const),
|
|
|
|
|
source: authStatus.label ?? authStatus.source,
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
if ((!authType || authType === "oauth") && provider.auth.oauth) {
|
|
|
|
|
options.push({
|
|
|
|
|
id: provider.id,
|
|
|
|
|
name: provider.name,
|
|
|
|
|
authType: "oauth",
|
|
|
|
|
method: provider.auth.oauth,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if ((!authType || authType === "api_key") && provider.auth.apiKey) {
|
|
|
|
|
options.push({
|
|
|
|
|
id: provider.id,
|
|
|
|
|
name: provider.name,
|
|
|
|
|
authType: "api_key",
|
|
|
|
|
method: provider.auth.apiKey,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
options.push({
|
|
|
|
|
id: providerId,
|
|
|
|
|
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
|
|
|
|
authType: "api_key",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
|
|
|
|
|
return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
return options.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getLogoutProviderOptions(): AuthSelectorProvider[] {
|
|
|
|
|
const authStorage = this.session.modelRegistry.authStorage;
|
|
|
|
|
const options: AuthSelectorProvider[] = [];
|
|
|
|
|
|
|
|
|
|
for (const providerId of authStorage.list()) {
|
|
|
|
|
const credential = authStorage.get(providerId);
|
|
|
|
|
if (!credential) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
options.push({
|
|
|
|
|
private async getLogoutProviderOptions(): Promise<AuthSelectorProvider[]> {
|
|
|
|
|
return (await this.session.modelRuntime.listCredentials())
|
|
|
|
|
.map(({ providerId, type }) => ({
|
|
|
|
|
id: providerId,
|
|
|
|
|
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
|
|
|
|
authType: credential.type,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return options.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
|
|
|
|
|
authType: type,
|
|
|
|
|
status: { type, source: "stored credential" },
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findLoginProviderOptions(providerRef: string): AuthSelectorProvider[] {
|
|
|
|
@@ -4848,6 +4817,7 @@ export class InteractiveMode {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleLoginCommand(providerRef?: string): Promise<void> {
|
|
|
|
|
await this.session.modelRuntime.getAvailable();
|
|
|
|
|
if (!providerRef) {
|
|
|
|
|
this.showLoginAuthTypeSelector();
|
|
|
|
|
return;
|
|
|
|
@@ -4873,10 +4843,10 @@ export class InteractiveMode {
|
|
|
|
|
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 {
|
|
|
|
|
} else if (providerOption.method?.login) {
|
|
|
|
|
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
|
|
|
|
} else {
|
|
|
|
|
this.showAmbientAuthDialog(providerOption);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4951,7 +4921,6 @@ export class InteractiveMode {
|
|
|
|
|
this.showSelector((done) => {
|
|
|
|
|
const selector = new OAuthSelectorComponent(
|
|
|
|
|
"login",
|
|
|
|
|
this.session.modelRegistry.authStorage,
|
|
|
|
|
providerOptions,
|
|
|
|
|
async (providerId, selectedAuthType) => {
|
|
|
|
|
done();
|
|
|
|
@@ -4973,7 +4942,6 @@ export class InteractiveMode {
|
|
|
|
|
this.ui.requestRender();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId),
|
|
|
|
|
initialSearchInput,
|
|
|
|
|
);
|
|
|
|
|
return { component: selector, focus: selector };
|
|
|
|
@@ -4986,7 +4954,7 @@ export class InteractiveMode {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const providerOptions = this.getLogoutProviderOptions();
|
|
|
|
|
const providerOptions = await this.getLogoutProviderOptions();
|
|
|
|
|
if (providerOptions.length === 0) {
|
|
|
|
|
this.showStatus(
|
|
|
|
|
"No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.",
|
|
|
|
@@ -4997,7 +4965,6 @@ export class InteractiveMode {
|
|
|
|
|
this.showSelector((done) => {
|
|
|
|
|
const selector = new OAuthSelectorComponent(
|
|
|
|
|
mode,
|
|
|
|
|
this.session.modelRegistry.authStorage,
|
|
|
|
|
providerOptions,
|
|
|
|
|
async (providerId: string) => {
|
|
|
|
|
done();
|
|
|
|
@@ -5008,8 +4975,7 @@ export class InteractiveMode {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.session.modelRegistry.authStorage.logout(providerOption.id);
|
|
|
|
|
this.session.modelRegistry.refresh();
|
|
|
|
|
await this.session.modelRuntime.logout(providerOption.id);
|
|
|
|
|
await this.updateAvailableProviderCount();
|
|
|
|
|
const message =
|
|
|
|
|
providerOption.authType === "oauth"
|
|
|
|
@@ -5035,14 +5001,14 @@ export class InteractiveMode {
|
|
|
|
|
authType: "oauth" | "api_key",
|
|
|
|
|
previousModel: Model<any> | undefined,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
this.session.modelRegistry.refresh();
|
|
|
|
|
await this.session.modelRuntime.getAvailable();
|
|
|
|
|
|
|
|
|
|
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 availableModels = await this.session.modelRuntime.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.`;
|
|
|
|
@@ -5082,7 +5048,7 @@ export class InteractiveMode {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private showBedrockSetupDialog(providerId: string, providerName: string): void {
|
|
|
|
|
private showAmbientAuthDialog(providerOption: AuthSelectorProvider): void {
|
|
|
|
|
const restoreEditor = () => {
|
|
|
|
|
this.editorContainer.clear();
|
|
|
|
|
this.editorContainer.addChild(this.editor);
|
|
|
|
@@ -5092,17 +5058,12 @@ export class InteractiveMode {
|
|
|
|
|
|
|
|
|
|
const dialog = new LoginDialogComponent(
|
|
|
|
|
this.ui,
|
|
|
|
|
providerId,
|
|
|
|
|
providerOption.id,
|
|
|
|
|
() => restoreEditor(),
|
|
|
|
|
providerName,
|
|
|
|
|
"Amazon Bedrock setup",
|
|
|
|
|
providerOption.name,
|
|
|
|
|
`${providerOption.name} setup`,
|
|
|
|
|
);
|
|
|
|
|
dialog.showInfo([
|
|
|
|
|
theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
|
|
|
|
|
theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
|
|
|
|
|
theme.fg("muted", "See:"),
|
|
|
|
|
theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
|
|
|
|
|
]);
|
|
|
|
|
dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true);
|
|
|
|
|
|
|
|
|
|
this.editorContainer.clear();
|
|
|
|
|
this.editorContainer.addChild(dialog);
|
|
|
|
@@ -5135,13 +5096,7 @@ export class InteractiveMode {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
|
|
|
|
|
await this.loginProvider(dialog, providerId, "api_key");
|
|
|
|
|
restoreEditor();
|
|
|
|
|
await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
@@ -5153,8 +5108,11 @@ export class InteractiveMode {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private showOAuthLoginSelect(dialog: LoginDialogComponent, prompt: OAuthSelectPrompt): Promise<string | undefined> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
private showAuthSelect(
|
|
|
|
|
dialog: LoginDialogComponent,
|
|
|
|
|
prompt: Extract<AuthPrompt, { type: "select" }>,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const restoreDialog = () => {
|
|
|
|
|
this.editorContainer.clear();
|
|
|
|
|
this.editorContainer.addChild(dialog);
|
|
|
|
@@ -5167,11 +5125,13 @@ export class InteractiveMode {
|
|
|
|
|
labels,
|
|
|
|
|
(optionLabel) => {
|
|
|
|
|
restoreDialog();
|
|
|
|
|
resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
|
|
|
|
|
const id = prompt.options.find((option) => option.label === optionLabel)?.id;
|
|
|
|
|
if (id) resolve(id);
|
|
|
|
|
else reject(new Error("Login cancelled"));
|
|
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
restoreDialog();
|
|
|
|
|
resolve(undefined);
|
|
|
|
|
reject(new Error("Login cancelled"));
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
this.editorContainer.clear();
|
|
|
|
@@ -5181,40 +5141,63 @@ export class InteractiveMode {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async showAuthPrompt(dialog: LoginDialogComponent, prompt: AuthPrompt): Promise<string> {
|
|
|
|
|
let response: Promise<string>;
|
|
|
|
|
if (prompt.type === "select") {
|
|
|
|
|
response = this.showAuthSelect(dialog, prompt);
|
|
|
|
|
} else if (prompt.type === "manual_code") {
|
|
|
|
|
response = dialog.showManualInput(prompt.message);
|
|
|
|
|
} else {
|
|
|
|
|
response = dialog.showPrompt(prompt.message, prompt.placeholder);
|
|
|
|
|
}
|
|
|
|
|
if (!prompt.signal) return response;
|
|
|
|
|
if (prompt.signal.aborted) throw new Error("Login cancelled");
|
|
|
|
|
const signal = prompt.signal;
|
|
|
|
|
let onAbort: (() => void) | undefined;
|
|
|
|
|
const aborted = new Promise<string>((_resolve, reject) => {
|
|
|
|
|
onAbort = () => reject(new Error("Login cancelled"));
|
|
|
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
return await Promise.race([response, aborted]);
|
|
|
|
|
} finally {
|
|
|
|
|
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private notifyAuthDialog(dialog: LoginDialogComponent, event: AuthEvent): void {
|
|
|
|
|
if (event.type === "auth_url") {
|
|
|
|
|
dialog.showAuth(event.url, event.instructions);
|
|
|
|
|
} else if (event.type === "device_code") {
|
|
|
|
|
dialog.showDeviceCode(event);
|
|
|
|
|
dialog.showWaiting("Waiting for authentication...");
|
|
|
|
|
} else if (event.type === "info") {
|
|
|
|
|
dialog.showInfo(event.message, event.links);
|
|
|
|
|
} else {
|
|
|
|
|
dialog.showProgress(event.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loginProvider(
|
|
|
|
|
dialog: LoginDialogComponent,
|
|
|
|
|
providerId: string,
|
|
|
|
|
method: "api_key" | "oauth",
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await this.session.modelRuntime.login(providerId, method, {
|
|
|
|
|
signal: dialog.signal,
|
|
|
|
|
prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
|
|
|
|
|
notify: (event) => this.notifyAuthDialog(dialog, event),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
providerName,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Show dialog in editor container
|
|
|
|
|
const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {}, providerName);
|
|
|
|
|
this.editorContainer.clear();
|
|
|
|
|
this.editorContainer.addChild(dialog);
|
|
|
|
|
this.ui.setFocus(dialog);
|
|
|
|
|
this.ui.requestRender();
|
|
|
|
|
|
|
|
|
|
// Promise for manual code input (racing with callback server)
|
|
|
|
|
let manualCodeResolve: ((code: string) => void) | undefined;
|
|
|
|
|
let manualCodeReject: ((err: Error) => void) | undefined;
|
|
|
|
|
const manualCodePromise = new Promise<string>((resolve, reject) => {
|
|
|
|
|
manualCodeResolve = resolve;
|
|
|
|
|
manualCodeReject = reject;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Restore editor helper
|
|
|
|
|
const restoreEditor = () => {
|
|
|
|
|
this.editorContainer.clear();
|
|
|
|
|
this.editorContainer.addChild(this.editor);
|
|
|
|
@@ -5223,51 +5206,7 @@ export class InteractiveMode {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.session.modelRegistry.authStorage.login(providerId as OAuthProviderId, {
|
|
|
|
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
|
|
|
dialog.showAuth(info.url, info.instructions);
|
|
|
|
|
|
|
|
|
|
if (usesCallbackServer) {
|
|
|
|
|
// Show input for manual paste, racing with callback
|
|
|
|
|
dialog
|
|
|
|
|
.showManualInput("Paste redirect URL below, or complete login in browser:")
|
|
|
|
|
.then((value) => {
|
|
|
|
|
if (value && manualCodeResolve) {
|
|
|
|
|
manualCodeResolve(value);
|
|
|
|
|
manualCodeResolve = undefined;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
if (manualCodeReject) {
|
|
|
|
|
manualCodeReject(new Error("Login cancelled"));
|
|
|
|
|
manualCodeReject = undefined;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// For Anthropic: onPrompt is called immediately after
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onDeviceCode: (info) => {
|
|
|
|
|
dialog.showDeviceCode(info);
|
|
|
|
|
dialog.showWaiting("Waiting for authentication...");
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
|
|
|
return dialog.showPrompt(prompt.message, prompt.placeholder);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onProgress: (message: string) => {
|
|
|
|
|
dialog.showProgress(message);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onSelect: (prompt: OAuthSelectPrompt) => this.showOAuthLoginSelect(dialog, prompt),
|
|
|
|
|
|
|
|
|
|
onManualCodeInput: () => manualCodePromise,
|
|
|
|
|
|
|
|
|
|
signal: dialog.signal,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Success
|
|
|
|
|
await this.loginProvider(dialog, providerId, "oauth");
|
|
|
|
|
restoreEditor();
|
|
|
|
|
await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
@@ -5368,7 +5307,7 @@ export class InteractiveMode {
|
|
|
|
|
showDiagnosticsWhenQuiet: true,
|
|
|
|
|
});
|
|
|
|
|
const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
|
|
|
|
|
const modelsJsonError = this.session.modelRegistry.getError();
|
|
|
|
|
const modelsJsonError = this.session.modelRuntime.getError();
|
|
|
|
|
if (modelsJsonError) {
|
|
|
|
|
this.showError(`models.json error: ${modelsJsonError}`);
|
|
|
|
|
}
|
|
|
|
@@ -5613,7 +5552,7 @@ export class InteractiveMode {
|
|
|
|
|
const stats = this.session.getSessionStats();
|
|
|
|
|
const sessionName = this.sessionManager.getSessionName();
|
|
|
|
|
const entries = this.sessionManager.getEntries();
|
|
|
|
|
const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
|
|
|
|
|
const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
|
|
|
|
|
|
|
|
|
|
// Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
|
|
|
|
|
// resolves to a concrete responseModel), sorted by cost descending.
|
|
|
|
|