aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-09-10 13:33:17 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-09-10 13:33:17 -0400
commit04e7b89f3525f770ee8eaefb983632f27afaf2f0 (patch)
treeca5aa848c0af8360b78bb905e1a0118180e3a549
parenta9b8023c2c1364244d4210a93c15f4ab3b284795 (diff)
Simplify Chrome status commands and release 0.15.51
-rw-r--r--CHANGELOG.md6
-rw-r--r--README.md6
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json2
-rw-r--r--extensions/chrome-profile-bridge/index.ts22
-rw-r--r--package-lock.json4
-rw-r--r--package.json4
-rw-r--r--test-suite/unit/chrome-command.test.mjs163
7 files changed, 187 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6222aaf..17def6c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable user-facing changes to `pi-chrome`.
+## 0.15.51 — 2026-09-10
+
+- **Fewer Chrome commands.** Removed `/chrome status`; use bare `/chrome` for the quick connection, authorization, and background dashboard plus controls. The dashboard remains lightweight and does not run page probes.
+- **Complete Doctor report.** `/chrome doctor` now includes authorization and background state alongside connection, version, page checks, and troubleshooting hints, even when Chrome is offline or outdated.
+- **Command regressions.** Added tests for command dispatch/completion, lightweight dashboard behavior, Doctor state reporting, and failure paths. `/chrome background status` remains available.
+
## 0.15.50 — 2026-09-10
- **README clarity.** Lead with workflow examples, correct setup ordering and platform-specific onboarding instructions, and clarify privacy guidance. Describe background behavior and the on/off/status controls directly.
diff --git a/README.md b/README.md
index 7ebe1eb..c863955 100644
--- a/README.md
+++ b/README.md
@@ -90,18 +90,20 @@ If page inspection or evaluation is blocked, use screenshots and coordinate inpu
## Commands
```text
+/chrome # quick connection/auth/background dashboard and controls
/chrome onboard # one-time companion setup
/chrome authorize # authorize this Pi session for 15 minutes
/chrome authorize 30m # choose a duration
/chrome authorize indefinite # no time limit; revoke when finished
/chrome revoke # lock tools and request session cleanup
-/chrome doctor # connection, version, and page checks
-/chrome status # connection, authorization, and background state
+/chrome doctor # full diagnostics, including authorization/background state
/chrome background on # default: block explicit focus/tab activation
/chrome background off # allow foreground/watch mode
/chrome background status
```
+Bare `/chrome` checks the connection without running page probes. Use `/chrome doctor` for version and page checks, troubleshooting hints, and authorization/background state.
+
Tool parameters are documented inline in Pi. See [architecture](./docs/ARCHITECTURE.md) for target ownership, screenshot behavior, and background-policy details.
### Updating and troubleshooting
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index 5f968a8..cad26fa 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.15.50",
+ "version": "0.15.51",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": [
"tabs",
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 40a353a..1917956 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -992,7 +992,11 @@ Usage rules:
// Shared handlers, dispatched by the unified /chrome command below.
const doctorHandler = async (ctx: ExtensionContext) => {
ctx.ui.notify("Checking pi-chrome…", "info");
- const lines: string[] = [`pi-chrome v${PI_CHROME_VERSION}`];
+ const lines: string[] = [
+ `pi-chrome v${PI_CHROME_VERSION}`,
+ `• Authorization: ${authSummary()}.`,
+ `• Background: ${backgroundEnabled ? "on (hard)" : "off (foreground/watch mode)"}.`,
+ ];
const status = bridge.status();
const roleLabel = status.mode === "client" ? "sharing another pi session's connection" : "running the Chrome connection for this machine";
lines.push(`• This pi session is ${roleLabel}.`);
@@ -1144,8 +1148,7 @@ Usage rules:
);
};
- // One-line snapshot of pi-chrome's current state. Used as a header in the bare-/chrome
- // picker and as the body of /chrome status.
+ // Lightweight connection/auth/background header for the bare-/chrome picker. No page probes.
const statusSummary = async (): Promise<string> => {
const parts: string[] = [];
try {
@@ -1163,11 +1166,6 @@ Usage rules:
return parts.join(" · ");
};
- const statusHandler = async (ctx: ExtensionContext) => {
- ctx.ui.notify("Checking Chrome connection…", "info");
- ctx.ui.notify(await statusSummary(), "info");
- };
-
const openAuthorizeMenu = async (ctx: ExtensionContext): Promise<void> => {
while (true) {
const choice = await ctx.ui.select("Authorize Chrome control", [
@@ -1225,7 +1223,7 @@ Usage rules:
pi.registerCommand("chrome", {
description:
- "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome status — one-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — enforce no explicit focus/tab activation, or allow foreground/watch mode.\nRun with no arguments for an interactive picker that shows current state.",
+ "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome doctor — full health check plus authorization and background state.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — enforce no explicit focus/tab activation, or allow foreground/watch mode.\nRun with no arguments for an interactive picker that shows current state.",
getArgumentCompletions: (prefix) => {
const raw = prefix;
const trimmedRight = raw.replace(/\s+$/, "");
@@ -1244,8 +1242,7 @@ Usage rules:
candidates = [
{ fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
{ fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
- { fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
- { fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
+ { fullValue: "doctor", label: "doctor", description: "Full diagnostics: connection, version, page checks, authorization, and background state." },
{ fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
{ fullValue: "background", label: "background", description: "Enforce hard background or allow foreground/watch mode." },
];
@@ -1279,7 +1276,6 @@ Usage rules:
switch (head) {
case "authorize": return authorizeHandler(ctx, subArgs);
case "revoke": return revokeHandler(ctx);
- case "status": return statusHandler(ctx);
case "doctor": return doctorHandler(ctx);
case "onboard": return onboardHandler(ctx);
case "background":
@@ -1292,7 +1288,7 @@ Usage rules:
return;
}
default:
- ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome authorize | revoke | status | doctor | onboard | background.`, "warning");
+ ctx.ui.notify(`Unknown subcommand '${head}'. Run /chrome for current state and controls, or try: /chrome authorize | revoke | doctor | onboard | background.`, "warning");
}
},
});
diff --git a/package-lock.json b/package-lock.json
index 425f2bd..930e3b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "pi-chrome",
- "version": "0.15.50",
+ "version": "0.15.51",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pi-chrome",
- "version": "0.15.50",
+ "version": "0.15.51",
"license": "MIT",
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
diff --git a/package.json b/package.json
index d1e872f..9f0af8c 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "pi-chrome",
- "version": "0.15.50",
+ "version": "0.15.51",
"scripts": {
- "test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && node test-suite/unit/session-cleanup.test.mjs && node test-suite/unit/background-policy.test.mjs && node test-suite/unit/input-reliability.test.mjs",
+ "test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && node test-suite/unit/session-cleanup.test.mjs && node test-suite/unit/background-policy.test.mjs && node test-suite/unit/input-reliability.test.mjs && node test-suite/unit/chrome-command.test.mjs",
"version": "node scripts/sync-manifest-version.js",
"prepublishOnly": "node scripts/sync-manifest-version.js"
},
diff --git a/test-suite/unit/chrome-command.test.mjs b/test-suite/unit/chrome-command.test.mjs
new file mode 100644
index 0000000..3fb4462
--- /dev/null
+++ b/test-suite/unit/chrome-command.test.mjs
@@ -0,0 +1,163 @@
+// Exercise the shipped /chrome command registration and handlers without opening Chrome or a bridge.
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import vm from "node:vm";
+import { stripTypeScriptTypes } from "node:module";
+import { test } from "node:test";
+
+const source = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url), "utf8");
+const { version } = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
+const now = 1_000_000;
+function section(start, end) {
+ const from = source.indexOf(start);
+ const to = source.indexOf(end, from);
+ assert.ok(from >= 0 && to > from, `Missing source section: ${start}`);
+ return source.slice(from, to);
+}
+const commandSource = stripTypeScriptTypes([
+ section("const authSummary =", "\n\tconst chromeControlAuthorized ="),
+ section("// Shared handlers,", "\n\tfunction registerChromeTools("),
+].join("\n"));
+
+function healthyResponse(action) {
+ switch (action) {
+ case "tab.version": return { extensionVersion: version };
+ case "page.evaluate": return 2;
+ case "page.probe": return { arithmetic: 2, location: "https://fixture.test/", webdriver: false };
+ default: throw new Error(`Unexpected bridge action: ${action}`);
+ }
+}
+
+function harness({ until, background = true, mode = "server", choices = [], send = healthyResponse } = {}) {
+ const calls = [], notices = [], menus = [];
+ let command;
+ const ctx = {
+ ui: {
+ notify: (...args) => notices.push(args),
+ async select(title, items) {
+ menus.push({ title, items: Array.from(items) });
+ return choices[menus.length - 1];
+ },
+ },
+ };
+ const sandbox = {
+ Date: { now: () => now }, PI_CHROME_VERSION: version,
+ chromeAuthorizedUntil: until, backgroundEnabled: background,
+ hostnameOf: (url) => new URL(url).hostname,
+ bridge: {
+ status: () => ({ mode }),
+ async send(action, params, timeout) {
+ calls.push({ action, params: JSON.parse(JSON.stringify(params)), timeout });
+ return send(action, params, timeout);
+ },
+ },
+ pi: { registerCommand(name, definition) { assert.equal(name, "chrome"); command = definition; } },
+ };
+ vm.runInNewContext(commandSource, sandbox);
+ return { command, calls, notices, menus, sandbox, run: (args = "") => command.handler(args, ctx) };
+}
+
+test("command help and root completion omit status; nested background status remains available", () => {
+ const h = harness();
+ assert.doesNotMatch(h.command.description, /\/chrome status\b/);
+ assert.match(h.command.description, /\/chrome doctor/);
+ assert.deepEqual(Array.from(h.command.getArgumentCompletions(""), (item) => item.value), [
+ "authorize", "revoke", "doctor", "onboard", "background",
+ ]);
+ assert.equal(h.command.getArgumentCompletions("sta"), null);
+ assert.equal(h.command.getArgumentCompletions("doctor")[0].value, "doctor");
+ assert.equal(h.command.getArgumentCompletions("background st")[0].value, "background status");
+ assert.equal(h.command.getArgumentCompletions("authorize 15")[0].value, "authorize 15m");
+});
+
+test("removed status command returns a warning without probing Chrome", async () => {
+ const h = harness();
+ await h.run("status");
+ assert.equal(h.calls.length, 0);
+ assert.equal(h.notices.length, 1);
+ assert.match(h.notices[0][0], /Unknown subcommand 'status'/);
+ assert.doesNotMatch(h.notices[0][0], /\| status \|/);
+ assert.equal(h.notices[0][1], "warning");
+});
+
+test("bare chrome shows loading immediately, then a lightweight dashboard without page probes", async () => {
+ let finish;
+ const h = harness({ send: () => new Promise((resolve) => { finish = resolve; }) });
+ const done = h.run();
+ assert.deepEqual(h.notices, [["Checking Chrome connection…", "info"]]);
+ assert.equal(h.menus.length, 0);
+ assert.deepEqual(h.calls, [{ action: "tab.version", params: {}, timeout: 5_000 }]);
+ finish({ extensionVersion: version });
+ await done;
+ assert.match(h.menus[0].title, /Chrome connected.*auth: locked.*background: on \(hard\)/);
+ assert.ok(h.menus[0].items.includes("Doctor / troubleshoot"));
+ assert.ok(!h.menus[0].items.some((item) => /status/i.test(item)));
+ assert.equal(h.calls.length, 1);
+});
+
+test("dashboard retains authorization/background state when Chrome is offline or outdated", async () => {
+ for (const [send, expected] of [
+ [() => { throw new Error("offline"); }, /Chrome not responding/],
+ [() => ({ extensionVersion: "0.0.0" }), /Chrome extension v0\.0\.0.*reload extension/],
+ ]) {
+ const h = harness({ until: "indefinite", send });
+ await h.run("background off");
+ await h.run("background status");
+ assert.match(h.notices.at(-1)[0], /background is off/);
+ await h.run();
+ assert.match(h.menus[0].title, expected);
+ assert.match(h.menus[0].title, /auth: authorized indefinitely.*background: off/);
+ assert.deepEqual(h.calls, [{ action: "tab.version", params: {}, timeout: 5_000 }]);
+ }
+});
+
+test("Doctor includes locked, timed, indefinite, and expired authorization plus background state", async () => {
+ for (const [until, expected] of [
+ [undefined, "locked"], [now + 15 * 60_000, "authorized for ~15m"],
+ ["indefinite", "authorized indefinitely"], [now, "locked"],
+ ]) {
+ for (const background of [true, false]) {
+ const h = harness({ until, background });
+ await h.run("doctor");
+ assert.equal(h.notices[0][0], "Checking pi-chrome…");
+ const report = h.notices.at(-1)[0];
+ assert.ok(report.includes(`pi-chrome v${version}`));
+ assert.ok(report.includes(`Authorization: ${expected}`));
+ assert.ok(report.includes(`Background: ${background ? "on (hard)" : "off"}`));
+ assert.match(report, /Chrome is connected/);
+ assert.match(report, /can run code/);
+ assert.match(report, /fixture\.test/);
+ assert.deepEqual(h.calls.map(({ action, timeout }) => [action, timeout]), [
+ ["tab.version", 35_000], ["page.evaluate", 10_000], ["page.probe", 10_000],
+ ]);
+ assert.ok(h.calls.filter((call) => call.action.startsWith("page.")).every((call) => call.params.foreground === false));
+ assert.equal(h.sandbox.chromeAuthorizedUntil, until, "diagnostics do not grant or change authorization");
+ assert.equal(h.sandbox.backgroundEnabled, background);
+ }
+ }
+});
+
+test("Doctor retains local state and repair hints when connection/version checks fail", async () => {
+ for (const [send, expected] of [
+ [() => { throw new Error("offline"); }, /Chrome isn't responding: offline/],
+ [() => ({ extensionVersion: "0.0.0" }), /old version \(0\.0\.0\)/],
+ ]) {
+ const h = harness({ until: "indefinite", background: false, mode: "client", send });
+ await h.run("doctor");
+ const report = h.notices.at(-1)[0];
+ assert.match(report, /Authorization: authorized indefinitely/);
+ assert.match(report, /Background: off/);
+ assert.match(report, /sharing another pi session's connection/);
+ assert.match(report, expected);
+ assert.match(report, /Fix:/);
+ assert.deepEqual(h.calls.map((call) => call.action), ["tab.version"]);
+ }
+});
+
+test("choosing Doctor explicitly from the dashboard runs full diagnostics", async () => {
+ const h = harness({ choices: ["Doctor / troubleshoot"] });
+ await h.run();
+ assert.deepEqual(h.calls.map((call) => call.action), ["tab.version", "tab.version", "page.evaluate", "page.probe"]);
+ assert.match(h.notices.at(-1)[0], /Authorization: locked/);
+ assert.match(h.notices.at(-1)[0], /Background: on \(hard\)/);
+});