Files
harness-tools/extensions/fast-browser/settings-view.ts
T
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

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);
}