pi-chrome 0.12.1 → 0.13.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 settings background`, or pass `background: true` on a single tool call when you want quiet.
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 settings trusted`:
75
+ Pick a mode with `/chrome clicks`:
76
76
 
77
77
  ```text
78
- /chrome settings trusted auto # default; quiet by default, real-looking only when needed
79
- /chrome settings trusted off # always quiet, no banner ever
80
- /chrome settings trusted on # always real-looking, banner stays up the whole session
81
- /chrome settings trusted status # show the current mode
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 settings background # toggle
96
- /chrome settings background on # explicit
97
- /chrome settings background off # explicit
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.12.1",
4
+ "version": "0.13.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/*"],
@@ -440,7 +440,7 @@ Usage rules:
440
440
  2. \`includeSnapshot=true\` on click/type/fill to verify in one round trip.
441
441
  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
442
  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 settings background to silence the whole session.
443
+ 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
444
  6. If you hit an autoplay/clipboard/file-picker gate, tell the user; this bridge cannot satisfy it.
445
445
  7. Run /chrome doctor when in doubt about connectivity or capabilities.
446
446
  </chrome-profile-bridge>`;
@@ -523,7 +523,7 @@ Usage rules:
523
523
  ? " 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
524
  : status.mode === "on"
525
525
  ? " 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. Switch to /chrome settings trusted auto if a site isn’t responding.";
526
+ : " 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
527
  const label = status.mode === "auto" ? "auto (smart upgrade)" : status.mode === "on" ? "on (always real-looking)" : "off (always quiet)";
528
528
  lines.push(`✓ Click mode: ${label}${banner}.${note}`);
529
529
  } else {
@@ -537,113 +537,98 @@ Usage rules:
537
537
  ctx.ui.notify(lines.join("\n"), "info");
538
538
  };
539
539
 
540
+ // Click realism handler. With no args, cycles auto → on → off → auto. Explicit args jump
541
+ // directly. 'status' prints the current mode without changing it.
542
+ const CLICKS_CYCLE = ["auto", "on", "off"] as const;
543
+ const CLICKS_DESC: Record<string, string> = {
544
+ 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.",
545
+ off: "All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore these clicks.",
546
+ 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.",
547
+ };
548
+ const CLICKS_LABEL: Record<string, string> = {
549
+ auto: "auto (smart upgrade)",
550
+ off: "off (always quiet)",
551
+ on: "on (always real-looking)",
552
+ };
553
+
540
554
  const trustedHandler = async (ctx: ExtensionContext, args: string) => {
541
- const rawArg = (args || "").trim().toLowerCase();
555
+ const rawArg = (args || "").trim().toLowerCase();
542
556
 
543
- // Resolve current status once for both branches (interactive picker + direct args).
544
- let status: { mode: string; attachedTabs: number[]; permissionGranted: boolean } | undefined;
545
- try {
546
- status = (await bridge.send("trusted.status", {}, 5_000)) as typeof status;
547
- } catch (error) {
548
- ctx.ui.notify(`Couldn't check current mode: ${(error as Error).message}`, "warning");
549
- return;
550
- }
551
- if (!status) return;
557
+ let status: { mode: string; attachedTabs: number[]; permissionGranted: boolean } | undefined;
558
+ try {
559
+ status = (await bridge.send("trusted.status", {}, 5_000)) as typeof status;
560
+ } catch (error) {
561
+ ctx.ui.notify(`Couldn't check current click mode: ${(error as Error).message}`, "warning");
562
+ return;
563
+ }
564
+ if (!status) return;
552
565
 
553
- if (!status.permissionGranted) {
554
- ctx.ui.notify(
555
- "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.",
556
- "warning",
557
- );
558
- return;
559
- }
566
+ if (!status.permissionGranted) {
567
+ ctx.ui.notify(
568
+ "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.",
569
+ "warning",
570
+ );
571
+ return;
572
+ }
560
573
 
561
- const MODE_NAMES: Record<string, string> = {
562
- auto: "auto (smart upgrade)",
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;
574
+ const current = status.mode;
575
+ const attached = status.attachedTabs?.length ? ` (banner up on ${status.attachedTabs.length} tab(s))` : "";
569
576
 
570
- let target = rawArg;
571
- if (target === "status") {
572
- ctx.ui.notify(`Current mode: ${friendly(current)}${attached}`, "info");
573
- return;
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
- }
577
+ if (rawArg === "status") {
578
+ ctx.ui.notify(`Click mode is ${CLICKS_LABEL[current] ?? current}${attached}. ${CLICKS_DESC[current] ?? ""}`, "info");
579
+ return;
580
+ }
596
581
 
597
- if (!["on", "off", "auto"].includes(target)) {
598
- ctx.ui.notify(`Unknown choice '${rawArg}'. Pick one of: auto | off | on | status, or run /chrome settings trusted with no argument.`, "warning");
599
- return;
600
- }
582
+ // No argument = cycle to the next mode.
583
+ let target = rawArg;
584
+ if (!target) {
585
+ const idx = CLICKS_CYCLE.indexOf(current as typeof CLICKS_CYCLE[number]);
586
+ target = CLICKS_CYCLE[(idx + 1 + CLICKS_CYCLE.length) % CLICKS_CYCLE.length];
587
+ }
601
588
 
602
- if (target === current) {
603
- ctx.ui.notify(`Already in ${friendly(current)} mode.`, "info");
604
- return;
605
- }
589
+ if (!["on", "off", "auto"].includes(target)) {
590
+ ctx.ui.notify(`Unknown click mode '${rawArg}'. Pick one of: auto | off | on | status.`, "warning");
591
+ return;
592
+ }
606
593
 
607
- // Extra confirmation only on first-time "on" (warn about persistent banner).
608
- if (target === "on" && current !== "on") {
609
- const ok = await ctx.ui.confirm(
610
- "Always use real-looking clicks?",
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
- }
594
+ if (target === current) {
595
+ ctx.ui.notify(`Click mode is already ${CLICKS_LABEL[current] ?? current}.`, "info");
596
+ return;
597
+ }
618
598
 
619
- try {
620
- const result = (await bridge.send("trusted.mode", { mode: target }, 5_000)) as { mode: string };
621
- if (result.mode === "on") {
622
- ctx.ui.notify(
623
- "On. Every click and keystroke now looks real to websites. A banner saying ‘Pi Chrome Connector started debugging this browser’ will appear on every tab pi-chrome touches.",
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");
599
+ try {
600
+ await bridge.send("trusted.mode", { mode: target }, 5_000);
601
+ ctx.ui.notify(`Click mode ${CLICKS_LABEL[target] ?? target}. ${CLICKS_DESC[target] ?? ""}`, "info");
602
+ } catch (error) {
603
+ ctx.ui.notify(`Couldn't switch click mode: ${(error as Error).message}`, "warning");
633
604
  }
634
605
  };
635
606
 
607
+ // Quiet (Chrome focus) handler. No args = toggle. Explicit on/off/status.
608
+ const QUIET_DESC: Record<string, string> = {
609
+ on: "pi-chrome works in the background; Chrome won't pop up or steal focus.",
610
+ off: "Chrome pops to the front and switches tabs so you can watch what pi-chrome is doing.",
611
+ };
612
+
636
613
  const backgroundHandler = async (ctx: ExtensionContext, args: string) => {
637
614
  const arg = (args || "").trim().toLowerCase();
615
+ const currentLabel = backgroundDefault ? "on" : "off";
616
+
617
+ if (arg === "status") {
618
+ ctx.ui.notify(`Quiet mode is ${currentLabel}. ${QUIET_DESC[currentLabel]}`, "info");
619
+ return;
620
+ }
621
+
638
622
  if (arg === "on" || arg === "true" || arg === "1") backgroundDefault = true;
639
623
  else if (arg === "off" || arg === "false" || arg === "0") backgroundDefault = false;
640
- else backgroundDefault = !backgroundDefault;
641
- ctx.ui.notify(
642
- backgroundDefault
643
- ? "Quiet mode on. pi-chrome will work in the background; Chrome won't steal focus."
644
- : "Watch mode on. Chrome will pop to the front and switch tabs so you can see what pi-chrome is doing.",
645
- "info",
646
- );
624
+ else if (arg === "toggle" || arg === "") backgroundDefault = !backgroundDefault;
625
+ else {
626
+ ctx.ui.notify(`Unknown quiet mode '${arg}'. Pick one of: on | off | toggle | status.`, "warning");
627
+ return;
628
+ }
629
+
630
+ const nextLabel = backgroundDefault ? "on" : "off";
631
+ ctx.ui.notify(`Quiet mode → ${nextLabel}. ${QUIET_DESC[nextLabel]}`, "info");
647
632
  };
648
633
 
649
634
  const onboardHandler = async (ctx: ExtensionContext) => {
@@ -667,90 +652,131 @@ Usage rules:
667
652
  );
668
653
  };
669
654
 
670
- const settingsHandler = async (ctx: ExtensionContext, rest: string[]) => {
671
- if (rest.length === 0) {
672
- const picked = await ctx.ui.select(
673
- "pi-chrome settings what would you like to change?",
674
- [
675
- "background should Chrome pop to the front when pi-chrome acts, or work silently?",
676
- "trusted how realistic should pi-chrome's clicks and keystrokes be?",
677
- ],
678
- );
679
- if (!picked) return;
680
- if (picked.startsWith("background")) return backgroundHandler(ctx, "");
681
- if (picked.startsWith("trusted")) return trustedHandler(ctx, "");
682
- return;
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");
655
+ // One-line snapshot of pi-chrome's current state. Used as a header in the bare-/chrome
656
+ // picker and as the body of /chrome status.
657
+ const statusSummary = async (): Promise<string> => {
658
+ const parts: string[] = [];
659
+ try {
660
+ const version = (await bridge.send("tab.version", {}, 5_000)) as { extensionVersion?: string };
661
+ if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) {
662
+ parts.push(`⚠ Chrome extension v${version.extensionVersion} (pi-chrome v${PI_CHROME_VERSION}, reload extension)`);
663
+ } else {
664
+ parts.push(`✓ Chrome connected`);
665
+ }
666
+ } catch {
667
+ parts.push(`✗ Chrome not responding`);
691
668
  }
669
+ try {
670
+ const t = (await bridge.send("trusted.status", {}, 3_000)) as { mode?: string; attachedTabs?: number[] };
671
+ const banner = t.attachedTabs?.length ? `, banner on ${t.attachedTabs.length} tab(s)` : "";
672
+ parts.push(`clicks: ${t.mode ?? "?"}${banner}`);
673
+ } catch {}
674
+ parts.push(`quiet: ${backgroundDefault ? "on" : "off"}`);
675
+ return parts.join(" · ");
676
+ };
677
+
678
+ const statusHandler = async (ctx: ExtensionContext) => {
679
+ ctx.ui.notify(await statusSummary(), "info");
692
680
  };
693
681
 
694
682
  pi.registerCommand("chrome", {
695
683
  description:
696
- "All pi-chrome controls in one place. Subcommands:\n /chrome doctor quick health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome settingschange how pi-chrome behaves (background mode, click realism).\nRun with no arguments for an interactive picker.",
684
+ "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
685
  getArgumentCompletions: (prefix) => {
698
- const rawTokens = prefix.split(/\s+/);
699
- const last = (rawTokens[rawTokens.length - 1] ?? "").toLowerCase();
700
- const path = rawTokens.slice(0, -1).filter(Boolean).map((t) => t.toLowerCase());
701
-
702
- const TOP = [
703
- { value: "doctor", description: "Quick health check. Tells you if Chrome is connected and what's wrong if it isn't." },
704
- { value: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
705
- { value: "settings", description: "Change pi-chrome behaviour: background mode, click realism." },
706
- ];
707
- const SETTINGS = [
708
- { value: "background", description: "Should Chrome pop to the front when pi-chrome acts, or work silently?" },
709
- { value: "trusted", description: "How realistic should pi-chrome's clicks and keystrokes be?" },
710
- ];
711
- const BG = [
712
- { value: "on", description: "Work silently. Chrome stays in the background. Your editor keeps focus." },
713
- { value: "off", description: "Bring Chrome to the front so you can watch (default)." },
714
- ];
715
- const TRUSTED = [
716
- { value: "auto", description: "Default. Quiet clicks; upgrade to real ones only when a site rejects them." },
717
- { value: "off", description: "Always quiet. No banner. Some sites won't accept the clicks." },
718
- { value: "on", description: "Always real-looking clicks. Banner stays up. Best for stubborn sites." },
719
- { value: "status", description: "Show the current click mode." },
720
- ];
721
-
722
- let pool: Array<{ value: string; description: string }> | null = null;
723
- if (path.length === 0) pool = TOP;
724
- else if (path[0] === "settings" && path.length === 1) pool = SETTINGS;
725
- else if (path[0] === "settings" && path[1] === "background" && path.length === 2) pool = BG;
726
- else if (path[0] === "settings" && path[1] === "trusted" && path.length === 2) pool = TRUSTED;
727
- if (!pool) return null;
728
-
729
- const items = pool.map((p) => ({ value: p.value, label: p.value, description: p.description }));
730
- const filtered = items.filter((i) => i.value.startsWith(last));
731
- return filtered.length > 0 ? filtered : null;
686
+ const raw = prefix;
687
+ const trimmedRight = raw.replace(/\s+$/, "");
688
+ const tokens = trimmedRight ? trimmedRight.split(/\s+/) : [];
689
+ const endsWithSpace = raw.length > 0 && raw !== trimmedRight;
690
+ // Path = completed tokens; partial = the token currently being typed (or "" if cursor sits right after a space).
691
+ const partial = endsWithSpace ? "" : (tokens.pop() ?? "");
692
+ const path = tokens.map((t) => t.toLowerCase());
693
+ const partialLower = partial.toLowerCase();
694
+
695
+ // Build candidate set with FULL argument-text values so pi-tui's apply-completion
696
+ // (which replaces the entire argument) lands correctly even for nested paths.
697
+ type Item = { fullValue: string; label: string; description: string };
698
+ let candidates: Item[] = [];
699
+ if (path.length === 0) {
700
+ candidates = [
701
+ { fullValue: "status", label: "status", description: "One-line summary: connection + click mode + quiet mode." },
702
+ { fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
703
+ { fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
704
+ { fullValue: "clicks", label: "clicks", description: "How realistic should pi-chrome's clicks be? auto / off / on." },
705
+ { fullValue: "quiet", label: "quiet", description: "Should Chrome pop to the front when pi-chrome acts, or work silently?" },
706
+ ];
707
+ } else if (path[0] === "clicks" && path.length === 1) {
708
+ candidates = [
709
+ { fullValue: "clicks auto", label: "auto", description: "Default. Quiet clicks; upgrade to real-looking ones only when a site rejects them." },
710
+ { fullValue: "clicks off", label: "off", description: "Always quiet. No banner. Some sites won't accept the clicks." },
711
+ { fullValue: "clicks on", label: "on", description: "Always real-looking. Chrome shows a banner. Best for stubborn sites." },
712
+ { fullValue: "clicks status", label: "status", description: "Show the current click mode." },
713
+ ];
714
+ } else if (path[0] === "quiet" && path.length === 1) {
715
+ candidates = [
716
+ { fullValue: "quiet on", label: "on", description: "Work silently. Chrome stays in the background. Your editor keeps focus." },
717
+ { fullValue: "quiet off", label: "off", description: "Bring Chrome to the front so you can watch (default)." },
718
+ { fullValue: "quiet toggle", label: "toggle", description: "Flip whichever way it's currently set." },
719
+ { fullValue: "quiet status", label: "status", description: "Show the current setting." },
720
+ ];
721
+ }
722
+ if (candidates.length === 0) return null;
723
+ const filtered = candidates.filter((c) => c.label.toLowerCase().startsWith(partialLower));
724
+ if (filtered.length === 0) return null;
725
+ return filtered.map((c) => ({ value: c.fullValue, label: c.label, description: c.description }));
732
726
  },
733
727
  handler: async (args, ctx) => {
734
728
  const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
735
729
  if (tokens.length === 0) {
736
- const picked = await ctx.ui.select("pi-chrome — what would you like to do?", [
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)",
730
+ const header = await statusSummary();
731
+ // Compute next-cycle targets so the picker labels describe the toggle action.
732
+ let clicksNow = "?";
733
+ try {
734
+ const t = (await bridge.send("trusted.status", {}, 3_000)) as { mode?: string };
735
+ clicksNow = t.mode ?? "?";
736
+ } catch {}
737
+ const clicksNext = (() => {
738
+ const idx = CLICKS_CYCLE.indexOf(clicksNow as typeof CLICKS_CYCLE[number]);
739
+ return idx >= 0 ? CLICKS_CYCLE[(idx + 1) % CLICKS_CYCLE.length] : "auto";
740
+ })();
741
+ const quietNow = backgroundDefault ? "on" : "off";
742
+ const quietNext = backgroundDefault ? "off" : "on";
743
+ const picked = await ctx.ui.select(`pi-chrome — ${header}`, [
744
+ `status — print the line above (so you can copy it).`,
745
+ `doctor — full health check; explains anything broken.`,
746
+ `onboard — install the Chrome companion extension (first-time setup).`,
747
+ `clicks: ${clicksNow} → switch to ${clicksNext} — ${CLICKS_DESC[clicksNext] ?? ""}`,
748
+ `quiet: ${quietNow} → switch to ${quietNext} — ${QUIET_DESC[quietNext] ?? ""}`,
740
749
  ]);
741
750
  if (!picked) return;
751
+ if (picked.startsWith("status")) return statusHandler(ctx);
742
752
  if (picked.startsWith("doctor")) return doctorHandler(ctx);
743
753
  if (picked.startsWith("onboard")) return onboardHandler(ctx);
744
- if (picked.startsWith("settings")) return settingsHandler(ctx, []);
754
+ if (picked.startsWith("clicks")) return trustedHandler(ctx, ""); // cycles
755
+ if (picked.startsWith("quiet")) return backgroundHandler(ctx, ""); // toggles
745
756
  return;
746
757
  }
747
758
  const [head, ...rest] = tokens;
759
+ const subArgs = rest.join(" ");
748
760
  switch (head) {
761
+ case "status": return statusHandler(ctx);
749
762
  case "doctor": return doctorHandler(ctx);
750
763
  case "onboard": return onboardHandler(ctx);
751
- case "settings": return settingsHandler(ctx, rest);
764
+ case "clicks":
765
+ case "trusted": // legacy alias
766
+ return trustedHandler(ctx, subArgs);
767
+ case "quiet":
768
+ case "background": // legacy alias
769
+ return backgroundHandler(ctx, subArgs);
770
+ case "settings": {
771
+ // Legacy nested form: /chrome settings background ... or /chrome settings trusted ...
772
+ const [setting, ...settingArgs] = rest;
773
+ if (setting === "background") return backgroundHandler(ctx, settingArgs.join(" "));
774
+ if (setting === "trusted") return trustedHandler(ctx, settingArgs.join(" "));
775
+ ctx.ui.notify(`'/chrome settings' was removed. Use /chrome clicks or /chrome quiet directly.`, "warning");
776
+ return;
777
+ }
752
778
  default:
753
- ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome doctor | onboard | settings.`, "warning");
779
+ ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome status | doctor | onboard | clicks | quiet.`, "warning");
754
780
  }
755
781
  },
756
782
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.12.1",
3
+ "version": "0.13.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",