Files
oh-my-pi/packages/coding-agent/src/modes/setup-wizard/scenes/web-search.ts
T
can1357 e7b7c3cc31 feat(coding-agent): added setup providers tabs for sign-in/web-search
- Added `SetupTab` abstractions and wired Sign-In/Web-Search tabs under the new providers onboarding scene.
- Added `providersSetupScene` and swapped `providerSetupScene` for `providers` in setup-wizard scene registration.
- Added `WebSearchTab` option loading, provider availability checks, and persisted selected search-provider preference.
- Changed sign-in flow handling to keep the scene active across providers and support cancellation during authentication.
- Changed glyph-mode picker initialization and rendering to preselect current preset and show live symbol previews.
- Updated setup wizard tests to use the `providers` scene id and cover the new web search tab path.
2026-05-31 00:29:48 +02:00

129 lines
4.4 KiB
TypeScript

import { type SelectItem, SelectList, truncateToWidth } from "@oh-my-pi/pi-tui";
import { SETTINGS_SCHEMA } from "../../../config/settings-schema";
import { getSearchProvider, setPreferredSearchProvider } from "../../../web/search/provider";
import { isSearchProviderPreference, type SearchProviderId } from "../../../web/search/types";
import { getSelectListTheme, theme } from "../../theme/theme";
import type { SetupSceneHost, SetupTab } from "./types";
const MAX_VISIBLE = 8;
/** Reuse the settings schema as the single source of truth for labels/descriptions. */
const WEB_SEARCH_ITEMS: readonly SelectItem[] = SETTINGS_SCHEMA["providers.webSearch"].ui.options.map(option => ({
value: option.value,
label: option.label,
description: option.description,
}));
type Availability = "checking" | boolean;
/**
* "Web search" panel: picks the provider the web_search tool should prefer and
* reports whether the highlighted provider is ready to use given current
* credentials (env keys or OAuth sign-ins from the Sign in tab).
*/
export class WebSearchTab implements SetupTab {
readonly id = "web-search";
readonly label = "Web search";
readonly modal = false;
#list: SelectList;
#availability = new Map<SearchProviderId, Availability>();
#status: string[] = [];
#disposed = false;
constructor(private readonly host: SetupSceneHost) {
this.#list = new SelectList(WEB_SEARCH_ITEMS, MAX_VISIBLE, getSelectListTheme());
const current = host.ctx.settings.get("providers.webSearch");
const index = WEB_SEARCH_ITEMS.findIndex(item => item.value === current);
if (index >= 0) this.#list.setSelectedIndex(index);
this.#list.onSelectionChange = item => this.#onHighlight(item.value);
this.#list.onSelect = item => this.#apply(item.value);
this.#list.onCancel = () => host.finish("skipped");
}
onActivate(): void {
// Auth may have changed in the Sign in tab; re-check from scratch.
this.#availability.clear();
this.#status = [];
const selected = this.#list.getSelectedItem();
if (selected) this.#onHighlight(selected.value);
this.host.requestRender();
}
handleInput(data: string): void {
this.#list.handleInput(data);
}
invalidate(): void {
this.#list.invalidate();
}
dispose(): void {
this.#disposed = true;
}
render(width: number): string[] {
const lines = [
theme.fg("muted", "Choose the provider the web_search tool should prefer."),
"",
...this.#list.render(width),
];
const selected = this.#list.getSelectedItem();
if (selected) {
lines.push("", ...this.#readinessLines(selected.value).map(line => truncateToWidth(line, width)));
}
if (this.#status.length > 0) {
lines.push("", ...this.#status.map(line => truncateToWidth(line, width)));
}
return lines;
}
#onHighlight(value: string): void {
this.#status = [];
if (value !== "auto") this.#checkAvailability(value as SearchProviderId);
this.host.requestRender();
}
#checkAvailability(id: SearchProviderId): void {
if (this.#availability.has(id)) return;
this.#availability.set(id, "checking");
void (async () => {
let ready = false;
try {
const provider = await getSearchProvider(id);
ready = await provider.isAvailable(this.host.ctx.session.modelRegistry.authStorage);
} catch {
ready = false;
}
if (this.#disposed) return;
this.#availability.set(id, ready);
this.host.requestRender();
})();
}
#apply(value: string): void {
if (!isSearchProviderPreference(value)) return;
this.host.ctx.settings.set("providers.webSearch", value);
setPreferredSearchProvider(value);
const label = WEB_SEARCH_ITEMS.find(item => item.value === value)?.label ?? value;
this.#status = [theme.fg("success", `${theme.status.success} Web search set to ${label}`)];
if (value !== "auto" && this.#availability.get(value as SearchProviderId) === false) {
this.#status.push(theme.fg("dim", "Not configured yet — add its API key or sign in to enable it."));
}
this.host.requestRender();
}
#readinessLines(value: string): string[] {
if (value === "auto") {
return [theme.fg("dim", "Automatically uses the first configured provider.")];
}
const state = this.#availability.get(value as SearchProviderId);
if (state === undefined || state === "checking") {
return [theme.fg("dim", "Checking availability…")];
}
return state
? [theme.fg("success", `${theme.status.success} Ready to use`)]
: [theme.fg("warning", `${theme.status.pending} Needs credentials`)];
}
}