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