aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json2
-rw-r--r--extensions/chrome-profile-bridge/index.ts104
-rw-r--r--package.json2
3 files changed, 80 insertions, 28 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index 5313d22..0565eeb 100644
--- a/extensions/chrome-profile-bridge/browser-extension/manifest.json
+++ b/extensions/chrome-profile-bridge/browser-extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Pi Chrome Connector",
- "version": "0.13.0",
+ "version": "0.14.0",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 6e41363..2fa51b0 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -1,5 +1,7 @@
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
+import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
import { StringEnum } from "@earendil-works/pi-ai";
+import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
import { Type } from "typebox";
import { existsSync, readFileSync, statSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
@@ -679,6 +681,81 @@ Usage rules:
ctx.ui.notify(await statusSummary(), "info");
};
+ // Interactive dialog: each row is a setting whose value cycles with Space/Enter. Enter on
+ // the last value also saves; Esc / 'q' closes. The description below changes with the
+ // current value so users always see what the active setting means.
+ const openSettingsDialog = async (ctx: ExtensionContext): Promise<void> => {
+ // Read current click mode (might fail if extension permission missing).
+ let clicksMode: string = "auto";
+ let permissionGranted = false;
+ try {
+ const t = (await bridge.send("trusted.status", {}, 5_000)) as { mode?: string; permissionGranted?: boolean };
+ clicksMode = t.mode ?? "auto";
+ permissionGranted = !!t.permissionGranted;
+ } catch {}
+
+ const clicksItem: SettingItem = {
+ id: "clicks",
+ label: "Click realism",
+ currentValue: clicksMode,
+ values: ["auto", "on", "off"],
+ description: permissionGranted
+ ? (CLICKS_DESC[clicksMode] ?? "")
+ : "Real-looking clicks unavailable: reload the Chrome extension in chrome://extensions and accept the new permission prompt.",
+ };
+ const quietItem: SettingItem = {
+ id: "quiet",
+ label: "Quiet mode",
+ currentValue: backgroundDefault ? "on" : "off",
+ values: ["on", "off"],
+ description: QUIET_DESC[backgroundDefault ? "on" : "off"] ?? "",
+ };
+ const items: SettingItem[] = [clicksItem, quietItem];
+
+ await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
+ const container = new Container();
+ container.addChild(new Text(theme.fg("accent", theme.bold("pi-chrome settings")), 1, 1));
+ container.addChild(new Text(theme.fg("muted", "\u2191\u2193 navigate · space/enter cycle · esc close"), 1, 0));
+
+ let list: SettingsList;
+ list = new SettingsList(
+ items,
+ Math.min(items.length + 2, 8),
+ getSettingsListTheme(),
+ (id, newValue) => {
+ if (id === "clicks") {
+ if (!permissionGranted) {
+ ctx.ui.notify("Click mode locked: reload the Chrome extension first.", "warning");
+ // Revert by snapping back to the previous value.
+ list.updateValue("clicks", clicksItem.currentValue);
+ return;
+ }
+ // Mutate description so the help text matches the new value.
+ clicksItem.currentValue = newValue;
+ clicksItem.description = CLICKS_DESC[newValue] ?? "";
+ list.invalidate();
+ void bridge.send("trusted.mode", { mode: newValue }, 5_000).catch((err) => {
+ ctx.ui.notify(`Couldn't switch click mode: ${(err as Error).message}`, "warning");
+ });
+ } else if (id === "quiet") {
+ backgroundDefault = newValue === "on";
+ quietItem.currentValue = newValue;
+ quietItem.description = QUIET_DESC[newValue] ?? "";
+ list.invalidate();
+ }
+ },
+ () => done(undefined),
+ );
+ container.addChild(list);
+
+ return {
+ render: (w) => container.render(w),
+ invalidate: () => container.invalidate(),
+ handleInput: (data: string) => list.handleInput(data),
+ };
+ });
+ };
+
pi.registerCommand("chrome", {
description:
"All pi-chrome controls in one place.\n /chrome status — one-line snapshot of connection + current modes.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome clicks [auto|off|on|status] — how realistic should pi-chrome's clicks be.\n /chrome quiet [on|off|status|toggle] — whether Chrome pops to the front when pi-chrome acts.\nRun with no arguments for an interactive picker that shows current state.",
@@ -727,32 +804,7 @@ Usage rules:
handler: async (args, ctx) => {
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
if (tokens.length === 0) {
- const header = await statusSummary();
- // Compute next-cycle targets so the picker labels describe the toggle action.
- let clicksNow = "?";
- try {
- const t = (await bridge.send("trusted.status", {}, 3_000)) as { mode?: string };
- clicksNow = t.mode ?? "?";
- } catch {}
- const clicksNext = (() => {
- const idx = CLICKS_CYCLE.indexOf(clicksNow as typeof CLICKS_CYCLE[number]);
- return idx >= 0 ? CLICKS_CYCLE[(idx + 1) % CLICKS_CYCLE.length] : "auto";
- })();
- const quietNow = backgroundDefault ? "on" : "off";
- const quietNext = backgroundDefault ? "off" : "on";
- const picked = await ctx.ui.select(`pi-chrome — ${header}`, [
- `status — print the line above (so you can copy it).`,
- `doctor — full health check; explains anything broken.`,
- `onboard — install the Chrome companion extension (first-time setup).`,
- `clicks: ${clicksNow} → switch to ${clicksNext} — ${CLICKS_DESC[clicksNext] ?? ""}`,
- `quiet: ${quietNow} → switch to ${quietNext} — ${QUIET_DESC[quietNext] ?? ""}`,
- ]);
- if (!picked) return;
- if (picked.startsWith("status")) return statusHandler(ctx);
- if (picked.startsWith("doctor")) return doctorHandler(ctx);
- if (picked.startsWith("onboard")) return onboardHandler(ctx);
- if (picked.startsWith("clicks")) return trustedHandler(ctx, ""); // cycles
- if (picked.startsWith("quiet")) return backgroundHandler(ctx, ""); // toggles
+ await openSettingsDialog(ctx);
return;
}
const [head, ...rest] = tokens;
diff --git a/package.json b/package.json
index c8cccf2..1f155a3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pi-chrome",
- "version": "0.13.0",
+ "version": "0.14.0",
"description": "Drive your existing logged-in Chrome from Pi — no re-login, no throwaway profile, watch the agent work in real time (or toggle quiet background mode).",
"keywords": [
"pi-package",