Files
oh-my-pi/packages/coding-agent/src/modes/controllers/selector-controller.ts
T
can1357 acf4b45062 fix(coding-agent): backported pi-mono changes (5133697..15e0957b0)
packages/ai:
- fix: improve GitHub Copilot OAuth polling and Codex stream recovery
- fix: allow google-vertex authentication via GOOGLE_CLOUD_API_KEY
- fix: send Gemini/Claude provider-specific thinking headers and thought-signature fallbacks correctly
- test: added coverage for Gemini CLI alignment, Codex streaming, and stream edge cases

packages/coding-agent:
- feat: add treeFilterMode setting for the session tree selector default
- fix: prefer later-loaded explicit extensions when commands conflict
- fix: truncate serialized tool results during compaction summarization to prevent overflow
- fix: normalize CRLF in write tool previews
- fix: use shell-based external editor launch on Windows
- test: added compaction serialization and extension runner precedence coverage

packages/tui:
- fix: chain slash-command argument autocomplete after tab-completing the command name
- fix: normalize pasted tabs in Input using configured indentation
- fix: render blockquote lists, tables, and fenced code blocks correctly
- fix: ignore unsupported Kitty CSI-u modifiers and enable modifyOtherKeys fallback cleanup
- test: added editor, input, keys, and markdown regression coverage
2026-03-10 07:27:30 +01:00

784 lines
25 KiB
TypeScript

import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
import { MODEL_ROLES } from "../../config/model-registry";
import { settings } from "../../config/settings";
import { DebugSelectorComponent } from "../../debug";
import { disableProvider, enableProvider } from "../../discovery";
import {
getAvailableThemes,
getSymbolTheme,
previewTheme,
setColorBlindMode,
setSymbolPreset,
setTheme,
theme,
} from "../../modes/theme/theme";
import type { InteractiveModeContext } from "../../modes/types";
import { SessionManager } from "../../session/session-manager";
import { setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
import { AgentDashboard } from "../components/agent-dashboard";
import { AssistantMessageComponent } from "../components/assistant-message";
import { ExtensionDashboard } from "../components/extensions";
import { HistorySearchComponent } from "../components/history-search";
import { ModelSelectorComponent } from "../components/model-selector";
import { OAuthSelectorComponent } from "../components/oauth-selector";
import { SessionSelectorComponent } from "../components/session-selector";
import { SettingsSelectorComponent } from "../components/settings-selector";
import { ToolExecutionComponent } from "../components/tool-execution";
import { TreeSelectorComponent } from "../components/tree-selector";
import { UserMessageSelectorComponent } from "../components/user-message-selector";
const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
"anthropic",
"openai-codex",
"gitlab-duo",
"google-gemini-cli",
"google-antigravity",
]);
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
export class SelectorController {
constructor(private ctx: InteractiveModeContext) {}
async #refreshOAuthProviderAuthState(): Promise<void> {
const oauthProviders = getOAuthProviders();
await Promise.all(
oauthProviders.map(provider =>
this.ctx.session.modelRegistry
.getApiKeyForProvider(provider.id, this.ctx.session.sessionId)
.catch(() => undefined),
),
);
}
/**
* Shows a selector component in place of the editor.
* @param create Factory that receives a `done` callback and returns the component and focus target
*/
showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
const done = () => {
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(this.ctx.editor);
this.ctx.ui.setFocus(this.ctx.editor);
};
const { component, focus } = create(done);
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(component);
this.ctx.ui.setFocus(focus);
this.ctx.ui.requestRender();
}
showSettingsSelector(): void {
getAvailableThemes().then(availableThemes => {
this.showSelector(done => {
const selector = new SettingsSelectorComponent(
{
availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
thinkingLevel: this.ctx.session.thinkingLevel,
availableThemes,
cwd: getProjectDir(),
},
{
onChange: (id, value) => this.handleSettingChange(id, value),
onThemePreview: async themeName => {
const result = await previewTheme(themeName);
if (result.success) {
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
this.ctx.ui.requestRender();
}
},
onStatusLinePreview: previewSettings => {
// Update status line with preview settings
this.ctx.statusLine.updateSettings({
preset: settings.get("statusLine.preset"),
leftSegments: settings.get("statusLine.leftSegments"),
rightSegments: settings.get("statusLine.rightSegments"),
separator: settings.get("statusLine.separator"),
showHookStatus: settings.get("statusLine.showHookStatus"),
...previewSettings,
});
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
},
getStatusLinePreview: () => {
// Return the rendered status line for inline preview
const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
return this.ctx.statusLine.getTopBorder(availableWidth).content;
},
onPluginsChanged: () => {
this.ctx.ui.requestRender();
},
onCancel: () => {
done();
// Restore status line to saved settings
this.ctx.statusLine.updateSettings({
preset: settings.get("statusLine.preset"),
leftSegments: settings.get("statusLine.leftSegments"),
rightSegments: settings.get("statusLine.rightSegments"),
separator: settings.get("statusLine.separator"),
showHookStatus: settings.get("statusLine.showHookStatus"),
});
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
},
},
);
return { component: selector, focus: selector };
});
});
}
showHistorySearch(): void {
const historyStorage = this.ctx.historyStorage;
if (!historyStorage) return;
this.showSelector(done => {
const component = new HistorySearchComponent(
historyStorage,
prompt => {
done();
this.ctx.editor.setText(prompt);
this.ctx.ui.requestRender();
},
() => {
done();
this.ctx.ui.requestRender();
},
);
return { component, focus: component };
});
}
/**
* Show the Extension Control Center dashboard.
* Replaces /status with a unified view of all providers and extensions.
*/
async showExtensionsDashboard(): Promise<void> {
const dashboard = await ExtensionDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows);
this.showSelector(done => {
dashboard.onClose = () => {
done();
this.ctx.ui.requestRender();
};
return { component: dashboard, focus: dashboard };
});
}
/**
* Show the Agent Control Center dashboard.
*/
async showAgentsDashboard(): Promise<void> {
const activeModel = this.ctx.session.model;
const activeModelPattern = activeModel ? `${activeModel.provider}/${activeModel.id}` : undefined;
const defaultModelPattern = this.ctx.settings.getModelRole("default");
const dashboard = await AgentDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows, {
modelRegistry: this.ctx.session.modelRegistry,
activeModelPattern,
defaultModelPattern,
});
this.showSelector(done => {
dashboard.onClose = () => {
done();
this.ctx.ui.requestRender();
};
dashboard.onRequestRender = () => {
this.ctx.ui.requestRender();
};
return { component: dashboard, focus: dashboard };
});
}
/**
* Handle setting changes from the settings selector.
* Most settings are saved directly via SettingsManager in the definitions.
* This handles side effects and session-specific settings.
*/
handleSettingChange(id: string, value: unknown): void {
// Discovery provider toggles
if (id.startsWith("discovery.")) {
const providerId = id.replace("discovery.", "");
if (value) {
enableProvider(providerId);
} else {
disableProvider(providerId);
}
return;
}
switch (id) {
// Session-managed settings (not in SettingsManager)
case "autoCompact":
this.ctx.session.setAutoCompactionEnabled(value as boolean);
this.ctx.statusLine.setAutoCompactEnabled(value as boolean);
break;
case "steeringMode":
this.ctx.session.setSteeringMode(value as "all" | "one-at-a-time");
break;
case "followUpMode":
this.ctx.session.setFollowUpMode(value as "all" | "one-at-a-time");
break;
case "interruptMode":
this.ctx.session.setInterruptMode(value as "immediate" | "wait");
break;
case "thinkingLevel":
case "defaultThinkingLevel":
this.ctx.session.setThinkingLevel(value as ThinkingLevel, true);
this.ctx.statusLine.invalidate();
this.ctx.updateEditorBorderColor();
break;
case "clearOnShrink":
this.ctx.ui.setClearOnShrink(value as boolean);
break;
case "autocompleteMaxVisible":
this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
break;
// Settings with UI side effects
case "showImages":
for (const child of this.ctx.chatContainer.children) {
if (child instanceof ToolExecutionComponent) {
child.setShowImages(value as boolean);
}
}
break;
case "hideThinking":
this.ctx.hideThinkingBlock = value as boolean;
for (const child of this.ctx.chatContainer.children) {
if (child instanceof AssistantMessageComponent) {
child.setHideThinkingBlock(value as boolean);
}
}
this.ctx.chatContainer.clear();
this.ctx.rebuildChatFromMessages();
break;
case "theme": {
setTheme(value as string, true).then(result => {
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
if (!result.success) {
this.ctx.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
}
});
break;
}
case "symbolPreset": {
setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
});
break;
}
case "colorBlindMode": {
setColorBlindMode(value === "true" || value === true).then(() => {
this.ctx.ui.invalidate();
});
break;
}
case "temperature": {
const temp = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.temperature = temp >= 0 ? temp : undefined;
break;
}
case "topP": {
const topP = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.topP = topP >= 0 ? topP : undefined;
break;
}
case "topK": {
const topK = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.topK = topK >= 0 ? topK : undefined;
break;
}
case "minP": {
const minP = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.minP = minP >= 0 ? minP : undefined;
break;
}
case "presencePenalty": {
const presencePenalty = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.presencePenalty = presencePenalty >= 0 ? presencePenalty : undefined;
break;
}
case "repetitionPenalty": {
const repetitionPenalty = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.repetitionPenalty = repetitionPenalty >= 0 ? repetitionPenalty : undefined;
break;
}
case "statusLinePreset":
case "statusLineSeparator":
case "statusLineShowHooks":
case "statusLineSegments":
case "statusLineModelThinking":
case "statusLinePathAbbreviate":
case "statusLinePathMaxLength":
case "statusLinePathStripWorkPrefix":
case "statusLineGitShowBranch":
case "statusLineGitShowStaged":
case "statusLineGitShowUnstaged":
case "statusLineGitShowUntracked":
case "statusLineTimeFormat":
case "statusLineTimeShowSeconds": {
const statusLineSettings = {
preset: settings.get("statusLine.preset"),
leftSegments: settings.get("statusLine.leftSegments"),
rightSegments: settings.get("statusLine.rightSegments"),
separator: settings.get("statusLine.separator"),
showHookStatus: settings.get("statusLine.showHookStatus"),
segmentOptions: settings.get("statusLine.segmentOptions"),
};
this.ctx.statusLine.updateSettings(statusLineSettings);
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
break;
}
// Provider settings - update runtime preferences
case "providers.webSearch":
setPreferredSearchProvider(
value as
| "auto"
| "exa"
| "brave"
| "jina"
| "kimi"
| "zai"
| "perplexity"
| "anthropic"
| "gemini"
| "codex"
| "kagi"
| "synthetic",
);
break;
case "providers.image":
setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
break;
// MCP update injection - live subscribe/unsubscribe
case "mcp.notifications":
this.ctx.mcpManager?.setNotificationsEnabled(value as boolean);
break;
// All other settings are handled by the definitions (get/set on SettingsManager)
// No additional side effects needed
}
}
showModelSelector(options?: { temporaryOnly?: boolean }): void {
this.showSelector(done => {
const selector = new ModelSelectorComponent(
this.ctx.ui,
this.ctx.session.model,
this.ctx.settings,
this.ctx.session.modelRegistry,
this.ctx.session.scopedModels,
async (model, role, thinkingLevel) => {
try {
if (role === null) {
// Temporary: update agent state but don't persist to settings
await this.ctx.session.setModelTemporary(model);
this.ctx.statusLine.invalidate();
this.ctx.updateEditorBorderColor();
this.ctx.showStatus(`Temporary model: ${model.id}`);
done();
this.ctx.ui.requestRender();
} else if (role === "default") {
// Default: update agent state and persist
await this.ctx.session.setModel(model, role);
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
this.ctx.session.setThinkingLevel(thinkingLevel);
}
this.ctx.statusLine.invalidate();
this.ctx.updateEditorBorderColor();
this.ctx.showStatus(`Default model: ${model.id}`);
// Don't call done() - selector stays open for role assignment
} else {
// Other roles (smol, slow): just update settings, not current model
const roleInfo = MODEL_ROLES[role];
const roleLabel = roleInfo?.name ?? role;
this.ctx.showStatus(`${roleLabel} model: ${model.id}`);
// Don't call done() - selector stays open
}
} catch (error) {
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
},
() => {
done();
this.ctx.ui.requestRender();
},
options,
);
return { component: selector, focus: selector };
});
}
showUserMessageSelector(): void {
const userMessages = this.ctx.session.getUserMessagesForBranching();
if (userMessages.length === 0) {
this.ctx.showStatus("No messages to branch from");
return;
}
this.showSelector(done => {
const selector = new UserMessageSelectorComponent(
userMessages.map(m => ({ id: m.entryId, text: m.text })),
async entryId => {
const result = await this.ctx.session.branch(entryId);
if (result.cancelled) {
// Hook cancelled the branch
done();
this.ctx.ui.requestRender();
return;
}
this.ctx.chatContainer.clear();
this.ctx.renderInitialMessages();
this.ctx.editor.setText(result.selectedText);
done();
this.ctx.showStatus("Branched to new session");
},
() => {
done();
this.ctx.ui.requestRender();
},
);
return { component: selector, focus: selector.getMessageList() };
});
}
showTreeSelector(): void {
const tree = this.ctx.sessionManager.getTree();
const realLeafId = this.ctx.sessionManager.getLeafId();
if (tree.length === 0) {
this.ctx.showStatus("No entries in session");
return;
}
this.showSelector(done => {
const selector = new TreeSelectorComponent(
tree,
realLeafId,
this.ctx.ui.terminal.rows,
async entryId => {
// Selecting the current leaf is a no-op (already there)
if (entryId === realLeafId) {
done();
this.ctx.showStatus("Already at this point");
return;
}
// Ask about summarization
done(); // Close selector first
// Loop until user makes a complete choice or cancels to tree
let wantsSummary = false;
let customInstructions: string | undefined;
const branchSummariesEnabled = settings.get("branchSummary.enabled");
while (branchSummariesEnabled) {
const summaryChoice = await this.ctx.showHookSelector("Summarize branch?", [
"No summary",
"Summarize",
"Summarize with custom prompt",
]);
if (summaryChoice === undefined) {
// User pressed escape - re-show tree selector
this.showTreeSelector();
return;
}
wantsSummary = summaryChoice !== "No summary";
if (summaryChoice === "Summarize with custom prompt") {
customInstructions = await this.ctx.showHookEditor("Custom summarization instructions");
if (customInstructions === undefined) {
// User cancelled - loop back to summary selector
continue;
}
}
// User made a complete choice
break;
}
// Set up escape handler and loader if summarizing
let summaryLoader: Loader | undefined;
const originalOnEscape = this.ctx.editor.onEscape;
if (wantsSummary) {
this.ctx.editor.onEscape = () => {
this.ctx.session.abortBranchSummary();
};
this.ctx.chatContainer.addChild(new Spacer(1));
summaryLoader = new Loader(
this.ctx.ui,
spinner => theme.fg("accent", spinner),
text => theme.fg("muted", text),
"Summarizing branch... (esc to cancel)",
getSymbolTheme().spinnerFrames,
);
this.ctx.statusContainer.addChild(summaryLoader);
this.ctx.ui.requestRender();
}
try {
const result = await this.ctx.session.navigateTree(entryId, {
summarize: wantsSummary,
customInstructions,
});
if (result.aborted) {
// Summarization aborted - re-show tree selector
this.ctx.showStatus("Branch summarization cancelled");
this.showTreeSelector();
return;
}
if (result.cancelled) {
this.ctx.showStatus("Navigation cancelled");
return;
}
// Update UI
this.ctx.chatContainer.clear();
this.ctx.renderInitialMessages();
await this.ctx.reloadTodos();
if (result.editorText && !this.ctx.editor.getText().trim()) {
this.ctx.editor.setText(result.editorText);
}
this.ctx.showStatus("Navigated to selected point");
} catch (error) {
this.ctx.showError(error instanceof Error ? error.message : String(error));
} finally {
if (summaryLoader) {
summaryLoader.stop();
this.ctx.statusContainer.clear();
}
this.ctx.editor.onEscape = originalOnEscape;
}
},
() => {
done();
this.ctx.ui.requestRender();
},
(entryId, label) => {
this.ctx.sessionManager.appendLabelChange(entryId, label);
this.ctx.ui.requestRender();
},
settings.get("treeFilterMode"),
);
return { component: selector, focus: selector };
});
}
async showSessionSelector(): Promise<void> {
const sessions = await SessionManager.list(
this.ctx.sessionManager.getCwd(),
this.ctx.sessionManager.getSessionDir(),
);
this.showSelector(done => {
const selector = new SessionSelectorComponent(
sessions,
async sessionPath => {
done();
await this.handleResumeSession(sessionPath);
},
() => {
done();
this.ctx.ui.requestRender();
},
() => {
void this.ctx.shutdown();
},
);
return { component: selector, focus: selector.getSessionList() };
});
}
async handleResumeSession(sessionPath: string): Promise<void> {
// Stop loading animation
if (this.ctx.loadingAnimation) {
this.ctx.loadingAnimation.stop();
this.ctx.loadingAnimation = undefined;
}
this.ctx.statusContainer.clear();
// Clear UI state
this.ctx.pendingMessagesContainer.clear();
this.ctx.compactionQueuedMessages = [];
this.ctx.streamingComponent = undefined;
this.ctx.streamingMessage = undefined;
this.ctx.pendingTools.clear();
// Switch session via AgentSession (emits hook and tool session events)
await this.ctx.session.switchSession(sessionPath);
// Clear and re-render the chat
this.ctx.chatContainer.clear();
this.ctx.renderInitialMessages();
await this.ctx.reloadTodos();
this.ctx.showStatus("Resumed session");
}
async #handleOAuthLogin(providerId: string): Promise<void> {
this.ctx.showStatus(`Logging in to ${providerId}…`);
const manualInput = this.ctx.oauthManualInput;
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
try {
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
onAuth: (info: { url: string; instructions?: string }) => {
this.ctx.chatContainer.addChild(new Spacer(1));
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
if (info.instructions) {
this.ctx.chatContainer.addChild(new Spacer(1));
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
}
if (useManualInput) {
this.ctx.chatContainer.addChild(new Spacer(1));
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
}
this.ctx.ui.requestRender();
this.ctx.openInBrowser(info.url);
},
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
this.ctx.chatContainer.addChild(new Spacer(1));
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
if (prompt.placeholder) {
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
}
this.ctx.ui.requestRender();
const { promise, resolve } = Promise.withResolvers<string>();
const codeInput = new Input();
codeInput.onSubmit = () => {
const code = codeInput.getValue();
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(this.ctx.editor);
this.ctx.ui.setFocus(this.ctx.editor);
resolve(code);
};
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(codeInput);
this.ctx.ui.setFocus(codeInput);
this.ctx.ui.requestRender();
return promise;
},
onProgress: (message: string) => {
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
this.ctx.ui.requestRender();
},
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
});
await this.ctx.session.modelRegistry.refresh();
this.ctx.chatContainer.addChild(new Spacer(1));
this.ctx.chatContainer.addChild(
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
);
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
this.ctx.ui.requestRender();
} catch (error: unknown) {
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
} finally {
if (useManualInput) {
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
}
}
}
async #handleOAuthLogout(providerId: string): Promise<void> {
try {
await this.ctx.session.modelRegistry.authStorage.logout(providerId);
await this.ctx.session.modelRegistry.refresh();
this.ctx.chatContainer.addChild(new Spacer(1));
this.ctx.chatContainer.addChild(
new Text(theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`), 1, 0),
);
this.ctx.chatContainer.addChild(
new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0),
);
this.ctx.ui.requestRender();
} catch (error: unknown) {
this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
if (providerId) {
if (mode === "login") {
await this.#handleOAuthLogin(providerId);
} else {
await this.#handleOAuthLogout(providerId);
}
return;
}
if (mode === "logout") {
await this.#refreshOAuthProviderAuthState();
const oauthProviders = getOAuthProviders();
const loggedInProviders = oauthProviders.filter(provider =>
this.ctx.session.modelRegistry.authStorage.hasAuth(provider.id),
);
if (loggedInProviders.length === 0) {
this.ctx.showStatus("No OAuth providers logged in. Use /login first.");
return;
}
}
this.showSelector(done => {
let selector: OAuthSelectorComponent;
selector = new OAuthSelectorComponent(
mode,
this.ctx.session.modelRegistry.authStorage,
async (selectedProviderId: string) => {
selector.stopValidation();
done();
if (mode === "login") {
await this.#handleOAuthLogin(selectedProviderId);
} else {
await this.#handleOAuthLogout(selectedProviderId);
}
},
() => {
selector.stopValidation();
done();
this.ctx.ui.requestRender();
},
{
validateAuth: async (selectedProviderId: string) => {
const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
selectedProviderId,
this.ctx.session.sessionId,
);
return !!apiKey;
},
requestRender: () => {
this.ctx.ui.requestRender();
},
},
);
return { component: selector, focus: selector };
});
}
showDebugSelector(): void {
this.showSelector(done => {
const selector = new DebugSelectorComponent(this.ctx, done);
return { component: selector, focus: selector };
});
}
}