226 lines
8.8 KiB
TypeScript
226 lines
8.8 KiB
TypeScript
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
import { SettingsList, truncateToWidth, type Component, type SettingItem } from "@earendil-works/pi-tui";
|
|
import type {
|
|
ConfigOverrides,
|
|
FastBrowserBrowserConfig,
|
|
FastBrowserConfig,
|
|
} from "./config.ts";
|
|
|
|
export const FAST_BROWSER_SETTING_IDS = {
|
|
cdpUrl: "cdpUrl",
|
|
allowedOrigins: "allowedOrigins",
|
|
python: "python",
|
|
textModel: "textModel",
|
|
textReasoning: "textReasoning",
|
|
typesafeKey: "typesafeKey",
|
|
browserMode: "browserMode",
|
|
browserProduct: "browserProduct",
|
|
browserExecutable: "browserExecutable",
|
|
browserHeadless: "browserHeadless",
|
|
browserStartUrl: "browserStartUrl",
|
|
browserProfile: "browserProfile",
|
|
} as const;
|
|
|
|
export type FastBrowserSettingId = typeof FAST_BROWSER_SETTING_IDS[keyof typeof FAST_BROWSER_SETTING_IDS];
|
|
|
|
export type FastBrowserSettingsViewEvent =
|
|
| { kind: "edit"; id: FastBrowserSettingId }
|
|
| { kind: "cancel" };
|
|
|
|
export interface FastBrowserSettingsViewOptions {
|
|
scope: "global" | "project";
|
|
draft: ConfigOverrides;
|
|
/** Effective values inherited by this scope (never a higher-precedence project override for global). */
|
|
inherited: ConfigOverrides;
|
|
onEvent: (event: FastBrowserSettingsViewEvent) => void;
|
|
}
|
|
|
|
const LABELS: Record<FastBrowserSettingId, string> = {
|
|
cdpUrl: "CDP endpoint",
|
|
allowedOrigins: "Allowed origins",
|
|
python: "Python executable",
|
|
textModel: "Auxiliary text model",
|
|
textReasoning: "Text reasoning",
|
|
typesafeKey: "Typesafe key source",
|
|
browserMode: "Browser mode",
|
|
browserProduct: "Browser product",
|
|
browserExecutable: "Browser executable",
|
|
browserHeadless: "Browser headless",
|
|
browserStartUrl: "Browser start URL",
|
|
browserProfile: "Browser profile",
|
|
};
|
|
|
|
export const FAST_BROWSER_SETTING_LABELS = LABELS;
|
|
|
|
const EDIT_IDS = [
|
|
FAST_BROWSER_SETTING_IDS.cdpUrl,
|
|
FAST_BROWSER_SETTING_IDS.allowedOrigins,
|
|
FAST_BROWSER_SETTING_IDS.python,
|
|
FAST_BROWSER_SETTING_IDS.textModel,
|
|
FAST_BROWSER_SETTING_IDS.textReasoning,
|
|
FAST_BROWSER_SETTING_IDS.typesafeKey,
|
|
FAST_BROWSER_SETTING_IDS.browserMode,
|
|
FAST_BROWSER_SETTING_IDS.browserProduct,
|
|
FAST_BROWSER_SETTING_IDS.browserExecutable,
|
|
FAST_BROWSER_SETTING_IDS.browserHeadless,
|
|
FAST_BROWSER_SETTING_IDS.browserStartUrl,
|
|
FAST_BROWSER_SETTING_IDS.browserProfile,
|
|
] as const;
|
|
|
|
const DEFAULT_BROWSER: FastBrowserBrowserConfig = { mode: "attach" };
|
|
const DEFAULT_REASONING = "off";
|
|
const OWN = Object.prototype.hasOwnProperty;
|
|
|
|
function ownValue(values: ConfigOverrides, key: keyof FastBrowserConfig): boolean {
|
|
return OWN.call(values, key) && values[key] !== undefined;
|
|
}
|
|
|
|
function browserValue(values: ConfigOverrides): FastBrowserBrowserConfig | undefined {
|
|
return values.browser;
|
|
}
|
|
|
|
function displayValue(key: FastBrowserSettingId, values: ConfigOverrides): string {
|
|
switch (key) {
|
|
case FAST_BROWSER_SETTING_IDS.cdpUrl:
|
|
return values.cdpUrl ?? "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.allowedOrigins:
|
|
return values.allowedOrigins === "*"
|
|
? "All web origins"
|
|
: values.allowedOrigins ? JSON.stringify(values.allowedOrigins) : "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.python:
|
|
return values.python ?? "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.textModel:
|
|
return values.textModel ?? "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.textReasoning:
|
|
return values.textReasoning ?? DEFAULT_REASONING;
|
|
case FAST_BROWSER_SETTING_IDS.typesafeKey: {
|
|
const reference = values.typesafeKey;
|
|
if (!reference) return "(unset)";
|
|
return "env" in reference
|
|
? `{ env: ${reference.env} }`
|
|
: `{ file: ${reference.file}${reference.key ? `, key: ${reference.key}` : ""} }`;
|
|
}
|
|
case FAST_BROWSER_SETTING_IDS.browserMode:
|
|
return browserValue(values)?.mode ?? DEFAULT_BROWSER.mode;
|
|
case FAST_BROWSER_SETTING_IDS.browserProduct:
|
|
return browserValue(values)?.product ?? "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.browserExecutable:
|
|
return browserValue(values)?.executablePath ?? "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.browserHeadless: {
|
|
const browser = browserValue(values);
|
|
return browser?.mode === "launch" ? String(browser.headless === true) : "(unset)";
|
|
}
|
|
case FAST_BROWSER_SETTING_IDS.browserStartUrl:
|
|
return browserValue(values)?.startUrl ?? "(unset)";
|
|
case FAST_BROWSER_SETTING_IDS.browserProfile:
|
|
return browserValue(values)?.profile ?? "(unset)";
|
|
}
|
|
}
|
|
|
|
function valueForSetting(
|
|
key: FastBrowserSettingId,
|
|
draft: ConfigOverrides,
|
|
inherited: ConfigOverrides,
|
|
): string {
|
|
const configKey: keyof FastBrowserConfig = key === FAST_BROWSER_SETTING_IDS.browserMode ||
|
|
key === FAST_BROWSER_SETTING_IDS.browserProduct ||
|
|
key === FAST_BROWSER_SETTING_IDS.browserExecutable ||
|
|
key === FAST_BROWSER_SETTING_IDS.browserHeadless ||
|
|
key === FAST_BROWSER_SETTING_IDS.browserStartUrl ||
|
|
key === FAST_BROWSER_SETTING_IDS.browserProfile
|
|
? "browser"
|
|
: key;
|
|
const isLocal = ownValue(draft, configKey);
|
|
const value = isLocal ? displayValue(key, draft) : displayValue(key, inherited);
|
|
if (isLocal) return value;
|
|
return value === "(unset)" ? value : `(inherited) ${value}`;
|
|
}
|
|
|
|
function description(id: FastBrowserSettingId, scope: "global" | "project"): string {
|
|
const scopeName = scope === "global" ? "global" : "project";
|
|
switch (id) {
|
|
case FAST_BROWSER_SETTING_IDS.cdpUrl: return `Loopback CDP endpoint; saved as a ${scopeName} override`;
|
|
case FAST_BROWSER_SETTING_IDS.allowedOrigins: return "All web origins by default, or an exact http(s) origin allowlist";
|
|
case FAST_BROWSER_SETTING_IDS.python: return "Python executable used by the local browser worker";
|
|
case FAST_BROWSER_SETTING_IDS.textModel: return "Optional provider/model used to compose text for fill steps";
|
|
case FAST_BROWSER_SETTING_IDS.textReasoning: return "Reasoning level for the optional auxiliary model";
|
|
case FAST_BROWSER_SETTING_IDS.typesafeKey: return "Credential reference only; secret contents are never read here";
|
|
case FAST_BROWSER_SETTING_IDS.browserMode: return "Attach to an existing browser or launch a new one";
|
|
case FAST_BROWSER_SETTING_IDS.browserProduct: return "Browser product used when launch mode is selected";
|
|
case FAST_BROWSER_SETTING_IDS.browserExecutable: return "Optional executable path used in launch mode";
|
|
case FAST_BROWSER_SETTING_IDS.browserHeadless: return "Whether launch mode starts without a visible window";
|
|
case FAST_BROWSER_SETTING_IDS.browserStartUrl: return "Initial launch URL; must use an allowed origin";
|
|
case FAST_BROWSER_SETTING_IDS.browserProfile: return "Persistent managed profile name; unset uses a disposable profile";
|
|
}
|
|
}
|
|
|
|
export function buildFastBrowserSettingItems(options: FastBrowserSettingsViewOptions): SettingItem[] {
|
|
return EDIT_IDS.map((id) => ({
|
|
id,
|
|
label: LABELS[id],
|
|
description: description(id, options.scope),
|
|
currentValue: valueForSetting(id, options.draft, options.inherited),
|
|
values: [valueForSetting(id, options.draft, options.inherited)],
|
|
}));
|
|
}
|
|
|
|
/** Native Pi SettingsList rendering for one scope's autosaved settings. */
|
|
export class FastBrowserSettingsView implements Component {
|
|
private readonly settingsList: SettingsList;
|
|
private readonly settingsTheme = getSettingsListTheme();
|
|
private readonly scope: "global" | "project";
|
|
private readonly onEvent: FastBrowserSettingsViewOptions["onEvent"];
|
|
|
|
constructor(options: FastBrowserSettingsViewOptions) {
|
|
this.scope = options.scope;
|
|
this.onEvent = options.onEvent;
|
|
const items = buildFastBrowserSettingItems(options);
|
|
this.settingsList = new SettingsList(
|
|
items,
|
|
10,
|
|
this.settingsTheme,
|
|
(id) => options.onEvent({ kind: "edit", id: id as FastBrowserSettingId }),
|
|
() => options.onEvent({ kind: "cancel" }),
|
|
);
|
|
}
|
|
|
|
render(width: number): string[] {
|
|
const lines = this.settingsList.render(width);
|
|
if (lines.length > 0) {
|
|
lines[lines.length - 1] = this.settingsTheme.hint(
|
|
truncateToWidth(" Changes save automatically · Esc to return", Math.max(1, width - 2), ""),
|
|
);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
handleInput(data: string): void {
|
|
this.settingsList.handleInput(data);
|
|
}
|
|
|
|
/** Refresh displayed values after the command applies an in-memory edit. */
|
|
updateValues(draft: ConfigOverrides, inherited: ConfigOverrides): void {
|
|
const updated = buildFastBrowserSettingItems({
|
|
scope: this.scope,
|
|
draft,
|
|
inherited,
|
|
onEvent: this.onEvent,
|
|
});
|
|
for (const item of updated) this.settingsList.updateValue(item.id, item.currentValue);
|
|
this.settingsList.invalidate();
|
|
}
|
|
|
|
invalidate(): void {
|
|
this.settingsList.invalidate();
|
|
}
|
|
}
|
|
|
|
/** Render-only helper used by offline tests and callers that need the exact native rows. */
|
|
export function fastBrowserSettingValue(
|
|
id: FastBrowserSettingId,
|
|
draft: ConfigOverrides,
|
|
inherited: ConfigOverrides,
|
|
): string {
|
|
return valueForSetting(id, draft, inherited);
|
|
}
|