Files
pi_harness/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts
T
Mario Zechner 8d1229b5ec fix(coding-agent): use key helpers for arrow keys and Enter
Fixed arrow key and Enter detection in selector components to work
with Kitty protocol when Caps Lock or Num Lock is enabled.

Updated: oauth-selector, user-message-selector, hook-selector,
hook-input, model-selector, session-selector
2025-12-19 21:53:04 +01:00

115 lines
3.5 KiB
TypeScript

import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, TruncatedText } from "@mariozechner/pi-tui";
import { getOAuthProviders, type OAuthProviderInfo } from "../../../core/oauth/index.js";
import { loadOAuthCredentials } from "../../../core/oauth/storage.js";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
/**
* Component that renders an OAuth provider selector
*/
export class OAuthSelectorComponent extends Container {
private listContainer: Container;
private allProviders: OAuthProviderInfo[] = [];
private selectedIndex: number = 0;
private mode: "login" | "logout";
private onSelectCallback: (providerId: string) => void;
private onCancelCallback: () => void;
constructor(mode: "login" | "logout", onSelect: (providerId: string) => void, onCancel: () => void) {
super();
this.mode = mode;
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:";
this.addChild(new TruncatedText(theme.bold(title)));
this.addChild(new Spacer(1));
// Create list container
this.listContainer = new Container();
this.addChild(this.listContainer);
this.addChild(new Spacer(1));
// Add bottom border
this.addChild(new DynamicBorder());
// Initial render
this.updateList();
}
private loadProviders(): void {
this.allProviders = getOAuthProviders();
this.allProviders = this.allProviders.filter((p) => p.available);
}
private updateList(): void {
this.listContainer.clear();
for (let i = 0; i < this.allProviders.length; i++) {
const provider = this.allProviders[i];
if (!provider) continue;
const isSelected = i === this.selectedIndex;
const isAvailable = provider.available;
// Check if user is logged in for this provider
const credentials = loadOAuthCredentials(provider.id);
const isLoggedIn = credentials !== null;
const statusIndicator = isLoggedIn ? theme.fg("success", " ✓ logged in") : "";
let line = "";
if (isSelected) {
const prefix = theme.fg("accent", "→ ");
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
line = prefix + text + statusIndicator;
} else {
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
line = text + statusIndicator;
}
this.listContainer.addChild(new TruncatedText(line, 0, 0));
}
// Show "no providers" if empty
if (this.allProviders.length === 0) {
const message =
this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
}
}
handleInput(keyData: string): void {
// Up arrow
if (isArrowUp(keyData)) {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
this.updateList();
}
// Down arrow
else if (isArrowDown(keyData)) {
this.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);
this.updateList();
}
// Enter
else if (isEnter(keyData)) {
const selectedProvider = this.allProviders[this.selectedIndex];
if (selectedProvider?.available) {
this.onSelectCallback(selectedProvider.id);
}
}
// Escape
else if (isEscape(keyData)) {
this.onCancelCallback();
}
}
}