pi-chrome 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Multiple Pi sessions can use Chrome at the same time. The first Pi session start
|
|
|
11
11
|
## Why try it?
|
|
12
12
|
|
|
13
13
|
- **Uses your existing Chrome profile** — works with the Chrome windows/tabs you are already using, including logged-in GitHub, admin dashboards, local apps, and internal tools.
|
|
14
|
-
- **Watch your authenticated Chrome work** — by default, `chrome_*` tool calls focus Chrome and activate the target tab so you can see the agent inspect, navigate, click, and type in real time. Switch to silent/background mode for the whole session with `/chrome
|
|
14
|
+
- **Watch your authenticated Chrome work** — by default, `chrome_*` tool calls focus Chrome and activate the target tab so you can see the agent inspect, navigate, click, and type in real time. Switch to silent/background mode for the whole session with `/chrome quiet`, or pass `background: true` on a single tool call when you want quiet.
|
|
15
15
|
- **Full browser automation toolkit for Pi** — list/create/activate/close tabs, snapshot pages with usable CSS selectors, navigate, evaluate JavaScript, click, type, press keys, wait for page state, and capture screenshots.
|
|
16
16
|
- **Built-in setup and agent guidance** — `/chrome onboard` walks users through installing the companion extension, `/chrome doctor` checks connectivity and version drift, screenshots save to disk, and the prompt primer tells agents to inspect with `chrome_snapshot` before acting and avoid destructive actions unless explicitly requested.
|
|
17
17
|
|
|
@@ -72,13 +72,13 @@ pi-chrome can drive Chrome two ways:
|
|
|
72
72
|
- **Quiet clicks** — fast and unobtrusive. They work on most sites, but some pages (sign-in flows, copy-to-clipboard buttons, file pickers, autoplay videos, fullscreen, paywalls) ignore them because they don't look like a real human action.
|
|
73
73
|
- **Real-looking clicks** — indistinguishable from a person clicking. They unlock the cases above, but Chrome shows a *"Pi Chrome Connector started debugging this browser"* banner at the top of every tab pi-chrome touches while it's working.
|
|
74
74
|
|
|
75
|
-
Pick a mode with `/chrome
|
|
75
|
+
Pick a mode with `/chrome clicks`:
|
|
76
76
|
|
|
77
77
|
```text
|
|
78
|
-
/chrome
|
|
79
|
-
/chrome
|
|
80
|
-
/chrome
|
|
81
|
-
/chrome
|
|
78
|
+
/chrome clicks auto # default; quiet by default, real-looking only when needed
|
|
79
|
+
/chrome clicks off # always quiet, no banner ever
|
|
80
|
+
/chrome clicks on # always real-looking, banner stays up the whole session
|
|
81
|
+
/chrome clicks status # show the current mode
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
For a one-off call, pass `trusted: true` (or `false`) on `chrome_click`, `chrome_type`, `chrome_fill`, `chrome_key`, `chrome_hover`, `chrome_drag`, or `chrome_scroll`. The per-call value wins over the global mode.
|
|
@@ -92,9 +92,9 @@ By default, `chrome_*` tools focus Chrome and activate the target tab so you can
|
|
|
92
92
|
When you want quiet (planner / audit / worker sessions running alongside your editor), turn background mode on for the whole Pi session:
|
|
93
93
|
|
|
94
94
|
```text
|
|
95
|
-
/chrome
|
|
96
|
-
/chrome
|
|
97
|
-
/chrome
|
|
95
|
+
/chrome quiet # toggle
|
|
96
|
+
/chrome quiet on # explicit
|
|
97
|
+
/chrome quiet off # explicit
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
For a single tool call, the agent can pass `background: true` directly. The per-call value always wins over the session toggle.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Pi Chrome Connector",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
|
|
6
6
|
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
|
|
7
7
|
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
|
+
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
3
5
|
import { Type } from "typebox";
|
|
4
6
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
7
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -440,7 +442,7 @@ Usage rules:
|
|
|
440
442
|
2. \`includeSnapshot=true\` on click/type/fill to verify in one round trip.
|
|
441
443
|
3. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm.
|
|
442
444
|
4. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now).
|
|
443
|
-
5. By default chrome_* tools focus Chrome so the user can watch; pass \`background=true\` or run /chrome
|
|
445
|
+
5. By default chrome_* tools focus Chrome so the user can watch; pass \`background=true\` or run /chrome quiet to silence the whole session.
|
|
444
446
|
6. If you hit an autoplay/clipboard/file-picker gate, tell the user; this bridge cannot satisfy it.
|
|
445
447
|
7. Run /chrome doctor when in doubt about connectivity or capabilities.
|
|
446
448
|
</chrome-profile-bridge>`;
|
|
@@ -523,7 +525,7 @@ Usage rules:
|
|
|
523
525
|
? " Clicks/keys are quiet by default; if a site rejects a quiet click, pi-chrome retries it once with a real-looking click. The Chrome banner shows only when that retry happens."
|
|
524
526
|
: status.mode === "on"
|
|
525
527
|
? " Every click and keystroke uses a real-looking event. The Chrome banner stays up on every tab pi-chrome touches."
|
|
526
|
-
: " All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore them.
|
|
528
|
+
: " All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore them. Run /chrome clicks if a site isn’t responding.";
|
|
527
529
|
const label = status.mode === "auto" ? "auto (smart upgrade)" : status.mode === "on" ? "on (always real-looking)" : "off (always quiet)";
|
|
528
530
|
lines.push(`✓ Click mode: ${label}${banner}.${note}`);
|
|
529
531
|
} else {
|
|
@@ -537,113 +539,98 @@ Usage rules:
|
|
|
537
539
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
538
540
|
};
|
|
539
541
|
|
|
542
|
+
// Click realism handler. With no args, cycles auto → on → off → auto. Explicit args jump
|
|
543
|
+
// directly. 'status' prints the current mode without changing it.
|
|
544
|
+
const CLICKS_CYCLE = ["auto", "on", "off"] as const;
|
|
545
|
+
const CLICKS_DESC: Record<string, string> = {
|
|
546
|
+
auto: "Quiet by default; pi-chrome retries once with a real-looking click if a site rejects the quiet one. The Chrome banner appears only when that retry happens.",
|
|
547
|
+
off: "All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore these clicks.",
|
|
548
|
+
on: "Every click and keystroke looks real to websites. Chrome shows a 'Pi Chrome Connector started debugging this browser' banner on every tab pi-chrome touches.",
|
|
549
|
+
};
|
|
550
|
+
const CLICKS_LABEL: Record<string, string> = {
|
|
551
|
+
auto: "auto (smart upgrade)",
|
|
552
|
+
off: "off (always quiet)",
|
|
553
|
+
on: "on (always real-looking)",
|
|
554
|
+
};
|
|
555
|
+
|
|
540
556
|
const trustedHandler = async (ctx: ExtensionContext, args: string) => {
|
|
541
|
-
|
|
557
|
+
const rawArg = (args || "").trim().toLowerCase();
|
|
542
558
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (!status) return;
|
|
559
|
+
let status: { mode: string; attachedTabs: number[]; permissionGranted: boolean } | undefined;
|
|
560
|
+
try {
|
|
561
|
+
status = (await bridge.send("trusted.status", {}, 5_000)) as typeof status;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
ctx.ui.notify(`Couldn't check current click mode: ${(error as Error).message}`, "warning");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!status) return;
|
|
552
567
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
568
|
+
if (!status.permissionGranted) {
|
|
569
|
+
ctx.ui.notify(
|
|
570
|
+
"pi-chrome can't drive real-looking clicks yet — the companion extension is missing a permission. Open chrome://extensions, click reload on 'Pi Chrome Connector', and accept the new permission prompt that appears.",
|
|
571
|
+
"warning",
|
|
572
|
+
);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
560
575
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
off: "off (always quiet)",
|
|
564
|
-
on: "on (always real-looking)",
|
|
565
|
-
};
|
|
566
|
-
const friendly = (m: string) => MODE_NAMES[m] ?? m;
|
|
567
|
-
const attached = status.attachedTabs?.length ? ` — banner currently up on ${status.attachedTabs.length} tab(s)` : "";
|
|
568
|
-
const current = status.mode;
|
|
576
|
+
const current = status.mode;
|
|
577
|
+
const attached = status.attachedTabs?.length ? ` (banner up on ${status.attachedTabs.length} tab(s))` : "";
|
|
569
578
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
if (!target) {
|
|
576
|
-
// Interactive picker. Plain-English descriptions; no jargon.
|
|
577
|
-
const options = [
|
|
578
|
-
`auto${current === "auto" ? " (current)" : ""} — quiet by default; if a site rejects a quiet click, retry it once with a real-looking click. Yellow banner appears only when needed. Recommended for everyday use.`,
|
|
579
|
-
`off${current === "off" ? " (current)" : ""} — always quiet. No banner, ever. Fast and unobtrusive, but some sites (sign-in pages, copy-to-clipboard buttons, file pickers, paywalls) will silently ignore the click.`,
|
|
580
|
-
`on${current === "on" ? " (current)" : ""} — every click and keystroke uses a real-looking event. A banner stays at the top of every Chrome window for the whole session, saying ‘Pi Chrome Connector started debugging this browser’. Best when working a stubborn site for a long stretch.`,
|
|
581
|
-
`status — just show me which mode is on right now.`,
|
|
582
|
-
];
|
|
583
|
-
const picked = await ctx.ui.select(
|
|
584
|
-
`How realistic should pi-chrome's clicks be? (current: ${friendly(current)}${attached})`,
|
|
585
|
-
options,
|
|
586
|
-
);
|
|
587
|
-
if (!picked) return; // cancelled
|
|
588
|
-
if (picked.startsWith("on")) target = "on";
|
|
589
|
-
else if (picked.startsWith("off")) target = "off";
|
|
590
|
-
else if (picked.startsWith("auto")) target = "auto";
|
|
591
|
-
else if (picked.startsWith("status")) {
|
|
592
|
-
ctx.ui.notify(`Current mode: ${friendly(current)}${attached}`, "info");
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
579
|
+
if (rawArg === "status") {
|
|
580
|
+
ctx.ui.notify(`Click mode is ${CLICKS_LABEL[current] ?? current}${attached}. ${CLICKS_DESC[current] ?? ""}`, "info");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
596
583
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
584
|
+
// No argument = cycle to the next mode.
|
|
585
|
+
let target = rawArg;
|
|
586
|
+
if (!target) {
|
|
587
|
+
const idx = CLICKS_CYCLE.indexOf(current as typeof CLICKS_CYCLE[number]);
|
|
588
|
+
target = CLICKS_CYCLE[(idx + 1 + CLICKS_CYCLE.length) % CLICKS_CYCLE.length];
|
|
589
|
+
}
|
|
601
590
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
591
|
+
if (!["on", "off", "auto"].includes(target)) {
|
|
592
|
+
ctx.ui.notify(`Unknown click mode '${rawArg}'. Pick one of: auto | off | on | status.`, "warning");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
606
595
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
"Every click and keystroke pi-chrome sends will now look like a real human action to websites. This unlocks copy-to-clipboard buttons, sign-in pages, file pickers, fullscreen, autoplay, and most bot-protected sites.\n\nThe tradeoff: Chrome will pin a banner at the top of every Chrome window saying ‘Pi Chrome Connector started debugging this browser’. The banner stays visible for the rest of your pi session (or until you switch back to auto/off). Clicking ‘Cancel’ on the banner interrupts pi-chrome.",
|
|
612
|
-
);
|
|
613
|
-
if (!ok) {
|
|
614
|
-
ctx.ui.notify("Mode unchanged.", "info");
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
596
|
+
if (target === current) {
|
|
597
|
+
ctx.ui.notify(`Click mode is already ${CLICKS_LABEL[current] ?? current}.`, "info");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
618
600
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"info",
|
|
625
|
-
);
|
|
626
|
-
} else if (result.mode === "off") {
|
|
627
|
-
ctx.ui.notify("Off. All clicks are quiet now, no banner. Note: some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore these clicks.", "info");
|
|
628
|
-
} else {
|
|
629
|
-
ctx.ui.notify("Auto. Clicks stay quiet by default; pi-chrome will only switch to real-looking clicks when a site rejects a quiet one. The Chrome banner will appear only when that retry happens.", "info");
|
|
630
|
-
}
|
|
631
|
-
} catch (error) {
|
|
632
|
-
ctx.ui.notify(`Couldn't switch mode: ${(error as Error).message}`, "warning");
|
|
601
|
+
try {
|
|
602
|
+
await bridge.send("trusted.mode", { mode: target }, 5_000);
|
|
603
|
+
ctx.ui.notify(`Click mode → ${CLICKS_LABEL[target] ?? target}. ${CLICKS_DESC[target] ?? ""}`, "info");
|
|
604
|
+
} catch (error) {
|
|
605
|
+
ctx.ui.notify(`Couldn't switch click mode: ${(error as Error).message}`, "warning");
|
|
633
606
|
}
|
|
634
607
|
};
|
|
635
608
|
|
|
609
|
+
// Quiet (Chrome focus) handler. No args = toggle. Explicit on/off/status.
|
|
610
|
+
const QUIET_DESC: Record<string, string> = {
|
|
611
|
+
on: "pi-chrome works in the background; Chrome won't pop up or steal focus.",
|
|
612
|
+
off: "Chrome pops to the front and switches tabs so you can watch what pi-chrome is doing.",
|
|
613
|
+
};
|
|
614
|
+
|
|
636
615
|
const backgroundHandler = async (ctx: ExtensionContext, args: string) => {
|
|
637
616
|
const arg = (args || "").trim().toLowerCase();
|
|
617
|
+
const currentLabel = backgroundDefault ? "on" : "off";
|
|
618
|
+
|
|
619
|
+
if (arg === "status") {
|
|
620
|
+
ctx.ui.notify(`Quiet mode is ${currentLabel}. ${QUIET_DESC[currentLabel]}`, "info");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
638
624
|
if (arg === "on" || arg === "true" || arg === "1") backgroundDefault = true;
|
|
639
625
|
else if (arg === "off" || arg === "false" || arg === "0") backgroundDefault = false;
|
|
640
|
-
else backgroundDefault = !backgroundDefault;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
626
|
+
else if (arg === "toggle" || arg === "") backgroundDefault = !backgroundDefault;
|
|
627
|
+
else {
|
|
628
|
+
ctx.ui.notify(`Unknown quiet mode '${arg}'. Pick one of: on | off | toggle | status.`, "warning");
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const nextLabel = backgroundDefault ? "on" : "off";
|
|
633
|
+
ctx.ui.notify(`Quiet mode → ${nextLabel}. ${QUIET_DESC[nextLabel]}`, "info");
|
|
647
634
|
};
|
|
648
635
|
|
|
649
636
|
const onboardHandler = async (ctx: ExtensionContext) => {
|
|
@@ -667,90 +654,181 @@ Usage rules:
|
|
|
667
654
|
);
|
|
668
655
|
};
|
|
669
656
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
const [head, ...sub] = rest;
|
|
685
|
-
const subArgs = sub.join(" ");
|
|
686
|
-
switch (head) {
|
|
687
|
-
case "background": return backgroundHandler(ctx, subArgs);
|
|
688
|
-
case "trusted": return trustedHandler(ctx, subArgs);
|
|
689
|
-
default:
|
|
690
|
-
ctx.ui.notify(`Unknown setting '${head}'. Try: /chrome settings background | trusted.`, "warning");
|
|
657
|
+
// One-line snapshot of pi-chrome's current state. Used as a header in the bare-/chrome
|
|
658
|
+
// picker and as the body of /chrome status.
|
|
659
|
+
const statusSummary = async (): Promise<string> => {
|
|
660
|
+
const parts: string[] = [];
|
|
661
|
+
try {
|
|
662
|
+
const version = (await bridge.send("tab.version", {}, 5_000)) as { extensionVersion?: string };
|
|
663
|
+
if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) {
|
|
664
|
+
parts.push(`⚠ Chrome extension v${version.extensionVersion} (pi-chrome v${PI_CHROME_VERSION}, reload extension)`);
|
|
665
|
+
} else {
|
|
666
|
+
parts.push(`✓ Chrome connected`);
|
|
667
|
+
}
|
|
668
|
+
} catch {
|
|
669
|
+
parts.push(`✗ Chrome not responding`);
|
|
691
670
|
}
|
|
671
|
+
try {
|
|
672
|
+
const t = (await bridge.send("trusted.status", {}, 3_000)) as { mode?: string; attachedTabs?: number[] };
|
|
673
|
+
const banner = t.attachedTabs?.length ? `, banner on ${t.attachedTabs.length} tab(s)` : "";
|
|
674
|
+
parts.push(`clicks: ${t.mode ?? "?"}${banner}`);
|
|
675
|
+
} catch {}
|
|
676
|
+
parts.push(`quiet: ${backgroundDefault ? "on" : "off"}`);
|
|
677
|
+
return parts.join(" · ");
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const statusHandler = async (ctx: ExtensionContext) => {
|
|
681
|
+
ctx.ui.notify(await statusSummary(), "info");
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// Interactive dialog: each row is a setting whose value cycles with Space/Enter. Enter on
|
|
685
|
+
// the last value also saves; Esc / 'q' closes. The description below changes with the
|
|
686
|
+
// current value so users always see what the active setting means.
|
|
687
|
+
const openSettingsDialog = async (ctx: ExtensionContext): Promise<void> => {
|
|
688
|
+
// Read current click mode (might fail if extension permission missing).
|
|
689
|
+
let clicksMode: string = "auto";
|
|
690
|
+
let permissionGranted = false;
|
|
691
|
+
try {
|
|
692
|
+
const t = (await bridge.send("trusted.status", {}, 5_000)) as { mode?: string; permissionGranted?: boolean };
|
|
693
|
+
clicksMode = t.mode ?? "auto";
|
|
694
|
+
permissionGranted = !!t.permissionGranted;
|
|
695
|
+
} catch {}
|
|
696
|
+
|
|
697
|
+
const clicksItem: SettingItem = {
|
|
698
|
+
id: "clicks",
|
|
699
|
+
label: "Click realism",
|
|
700
|
+
currentValue: clicksMode,
|
|
701
|
+
values: ["auto", "on", "off"],
|
|
702
|
+
description: permissionGranted
|
|
703
|
+
? (CLICKS_DESC[clicksMode] ?? "")
|
|
704
|
+
: "Real-looking clicks unavailable: reload the Chrome extension in chrome://extensions and accept the new permission prompt.",
|
|
705
|
+
};
|
|
706
|
+
const quietItem: SettingItem = {
|
|
707
|
+
id: "quiet",
|
|
708
|
+
label: "Quiet mode",
|
|
709
|
+
currentValue: backgroundDefault ? "on" : "off",
|
|
710
|
+
values: ["on", "off"],
|
|
711
|
+
description: QUIET_DESC[backgroundDefault ? "on" : "off"] ?? "",
|
|
712
|
+
};
|
|
713
|
+
const items: SettingItem[] = [clicksItem, quietItem];
|
|
714
|
+
|
|
715
|
+
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
|
716
|
+
const container = new Container();
|
|
717
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("pi-chrome settings")), 1, 1));
|
|
718
|
+
container.addChild(new Text(theme.fg("muted", "\u2191\u2193 navigate · space/enter cycle · esc close"), 1, 0));
|
|
719
|
+
|
|
720
|
+
let list: SettingsList;
|
|
721
|
+
list = new SettingsList(
|
|
722
|
+
items,
|
|
723
|
+
Math.min(items.length + 2, 8),
|
|
724
|
+
getSettingsListTheme(),
|
|
725
|
+
(id, newValue) => {
|
|
726
|
+
if (id === "clicks") {
|
|
727
|
+
if (!permissionGranted) {
|
|
728
|
+
ctx.ui.notify("Click mode locked: reload the Chrome extension first.", "warning");
|
|
729
|
+
// Revert by snapping back to the previous value.
|
|
730
|
+
list.updateValue("clicks", clicksItem.currentValue);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// Mutate description so the help text matches the new value.
|
|
734
|
+
clicksItem.currentValue = newValue;
|
|
735
|
+
clicksItem.description = CLICKS_DESC[newValue] ?? "";
|
|
736
|
+
list.invalidate();
|
|
737
|
+
void bridge.send("trusted.mode", { mode: newValue }, 5_000).catch((err) => {
|
|
738
|
+
ctx.ui.notify(`Couldn't switch click mode: ${(err as Error).message}`, "warning");
|
|
739
|
+
});
|
|
740
|
+
} else if (id === "quiet") {
|
|
741
|
+
backgroundDefault = newValue === "on";
|
|
742
|
+
quietItem.currentValue = newValue;
|
|
743
|
+
quietItem.description = QUIET_DESC[newValue] ?? "";
|
|
744
|
+
list.invalidate();
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
() => done(undefined),
|
|
748
|
+
);
|
|
749
|
+
container.addChild(list);
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
render: (w) => container.render(w),
|
|
753
|
+
invalidate: () => container.invalidate(),
|
|
754
|
+
handleInput: (data: string) => list.handleInput(data),
|
|
755
|
+
};
|
|
756
|
+
});
|
|
692
757
|
};
|
|
693
758
|
|
|
694
759
|
pi.registerCommand("chrome", {
|
|
695
760
|
description:
|
|
696
|
-
"All pi-chrome controls in one place
|
|
761
|
+
"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.",
|
|
697
762
|
getArgumentCompletions: (prefix) => {
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
];
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
else if (path[0] === "
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
763
|
+
const raw = prefix;
|
|
764
|
+
const trimmedRight = raw.replace(/\s+$/, "");
|
|
765
|
+
const tokens = trimmedRight ? trimmedRight.split(/\s+/) : [];
|
|
766
|
+
const endsWithSpace = raw.length > 0 && raw !== trimmedRight;
|
|
767
|
+
// Path = completed tokens; partial = the token currently being typed (or "" if cursor sits right after a space).
|
|
768
|
+
const partial = endsWithSpace ? "" : (tokens.pop() ?? "");
|
|
769
|
+
const path = tokens.map((t) => t.toLowerCase());
|
|
770
|
+
const partialLower = partial.toLowerCase();
|
|
771
|
+
|
|
772
|
+
// Build candidate set with FULL argument-text values so pi-tui's apply-completion
|
|
773
|
+
// (which replaces the entire argument) lands correctly even for nested paths.
|
|
774
|
+
type Item = { fullValue: string; label: string; description: string };
|
|
775
|
+
let candidates: Item[] = [];
|
|
776
|
+
if (path.length === 0) {
|
|
777
|
+
candidates = [
|
|
778
|
+
{ fullValue: "status", label: "status", description: "One-line summary: connection + click mode + quiet mode." },
|
|
779
|
+
{ fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
|
|
780
|
+
{ fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
|
|
781
|
+
{ fullValue: "clicks", label: "clicks", description: "How realistic should pi-chrome's clicks be? auto / off / on." },
|
|
782
|
+
{ fullValue: "quiet", label: "quiet", description: "Should Chrome pop to the front when pi-chrome acts, or work silently?" },
|
|
783
|
+
];
|
|
784
|
+
} else if (path[0] === "clicks" && path.length === 1) {
|
|
785
|
+
candidates = [
|
|
786
|
+
{ fullValue: "clicks auto", label: "auto", description: "Default. Quiet clicks; upgrade to real-looking ones only when a site rejects them." },
|
|
787
|
+
{ fullValue: "clicks off", label: "off", description: "Always quiet. No banner. Some sites won't accept the clicks." },
|
|
788
|
+
{ fullValue: "clicks on", label: "on", description: "Always real-looking. Chrome shows a banner. Best for stubborn sites." },
|
|
789
|
+
{ fullValue: "clicks status", label: "status", description: "Show the current click mode." },
|
|
790
|
+
];
|
|
791
|
+
} else if (path[0] === "quiet" && path.length === 1) {
|
|
792
|
+
candidates = [
|
|
793
|
+
{ fullValue: "quiet on", label: "on", description: "Work silently. Chrome stays in the background. Your editor keeps focus." },
|
|
794
|
+
{ fullValue: "quiet off", label: "off", description: "Bring Chrome to the front so you can watch (default)." },
|
|
795
|
+
{ fullValue: "quiet toggle", label: "toggle", description: "Flip whichever way it's currently set." },
|
|
796
|
+
{ fullValue: "quiet status", label: "status", description: "Show the current setting." },
|
|
797
|
+
];
|
|
798
|
+
}
|
|
799
|
+
if (candidates.length === 0) return null;
|
|
800
|
+
const filtered = candidates.filter((c) => c.label.toLowerCase().startsWith(partialLower));
|
|
801
|
+
if (filtered.length === 0) return null;
|
|
802
|
+
return filtered.map((c) => ({ value: c.fullValue, label: c.label, description: c.description }));
|
|
732
803
|
},
|
|
733
804
|
handler: async (args, ctx) => {
|
|
734
805
|
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
|
|
735
806
|
if (tokens.length === 0) {
|
|
736
|
-
|
|
737
|
-
"doctor — quick health check; tells you what's wrong if Chrome isn't responding",
|
|
738
|
-
"onboard — install the Chrome companion extension (first-time setup)",
|
|
739
|
-
"settings — change pi-chrome behaviour (background mode, click realism)",
|
|
740
|
-
]);
|
|
741
|
-
if (!picked) return;
|
|
742
|
-
if (picked.startsWith("doctor")) return doctorHandler(ctx);
|
|
743
|
-
if (picked.startsWith("onboard")) return onboardHandler(ctx);
|
|
744
|
-
if (picked.startsWith("settings")) return settingsHandler(ctx, []);
|
|
807
|
+
await openSettingsDialog(ctx);
|
|
745
808
|
return;
|
|
746
809
|
}
|
|
747
810
|
const [head, ...rest] = tokens;
|
|
811
|
+
const subArgs = rest.join(" ");
|
|
748
812
|
switch (head) {
|
|
813
|
+
case "status": return statusHandler(ctx);
|
|
749
814
|
case "doctor": return doctorHandler(ctx);
|
|
750
815
|
case "onboard": return onboardHandler(ctx);
|
|
751
|
-
case "
|
|
816
|
+
case "clicks":
|
|
817
|
+
case "trusted": // legacy alias
|
|
818
|
+
return trustedHandler(ctx, subArgs);
|
|
819
|
+
case "quiet":
|
|
820
|
+
case "background": // legacy alias
|
|
821
|
+
return backgroundHandler(ctx, subArgs);
|
|
822
|
+
case "settings": {
|
|
823
|
+
// Legacy nested form: /chrome settings background ... or /chrome settings trusted ...
|
|
824
|
+
const [setting, ...settingArgs] = rest;
|
|
825
|
+
if (setting === "background") return backgroundHandler(ctx, settingArgs.join(" "));
|
|
826
|
+
if (setting === "trusted") return trustedHandler(ctx, settingArgs.join(" "));
|
|
827
|
+
ctx.ui.notify(`'/chrome settings' was removed. Use /chrome clicks or /chrome quiet directly.`, "warning");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
752
830
|
default:
|
|
753
|
-
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome doctor | onboard |
|
|
831
|
+
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome status | doctor | onboard | clicks | quiet.`, "warning");
|
|
754
832
|
}
|
|
755
833
|
},
|
|
756
834
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-chrome",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"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).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|