diff options
| author | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-09-10 13:33:17 -0400 |
|---|---|---|
| committer | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-09-10 13:33:17 -0400 |
| commit | 04e7b89f3525f770ee8eaefb983632f27afaf2f0 (patch) | |
| tree | ca5aa848c0af8360b78bb905e1a0118180e3a549 | |
| parent | a9b8023c2c1364244d4210a93c15f4ab3b284795 (diff) | |
Simplify Chrome status commands and release 0.15.51
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | extensions/chrome-profile-bridge/browser-extension/manifest.json | 2 | ||||
| -rw-r--r-- | extensions/chrome-profile-bridge/index.ts | 22 | ||||
| -rw-r--r-- | package-lock.json | 4 | ||||
| -rw-r--r-- | package.json | 4 | ||||
| -rw-r--r-- | test-suite/unit/chrome-command.test.mjs | 163 |
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. @@ -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\)/); +}); |
