all repos — sz-chrome-remote-extension @ 43c62c2f338654394f82c86aa12b22be6a358d94

pi-chrome, patched to drive a signed-in browser on another host through a broker, with an MCP server

0.17.1: fix /chrome onboard stuck on 'Waiting for the extension to wake up'

0.17.0 made the daemon own the HTTP listener, but the Pi client's
bridge.status().connected still reflected the local Pi's lastSeenAt
(which is never updated in the daemon model). Onboard's connect-wait
loop spun forever even when the extension was polling the daemon.

Fix: new ChromeProfileBridge.daemonStatus() async helper that fetches
the daemon's /status. onboard, doctor, and status now query that for
the 'connected' flag instead of reading stale Pi-side state.
Tianren Dong tianrendong.dtr@gmail.com
Sat, 16 May 2026 12:56:24 -0400
commit

43c62c2f338654394f82c86aa12b22be6a358d94

parent

2ac665ec31fe156b372540e8ac38aa42765b3001

M CHANGELOG.md → CHANGELOG.md

@@ -2,6 +2,20 @@ # Changelog

All notable user-facing changes to `pi-chrome`. +## 0.17.1 — 2026-05-16 + +### Fix: `/chrome onboard` stuck on "Waiting for the extension to wake up…" + +0.17.0 made the daemon (not the Pi process) own the HTTP listener, but the Pi client's +`bridge.status().connected` still reflected the *local Pi*'s `lastSeenAt`, which never gets +updated in the daemon model. Result: onboard's connect-wait loop spun forever even when the +extension was happily polling the daemon. + +- New `ChromeProfileBridge.daemonStatus()` async method: fetches the daemon's `/status` and + returns its view (the only correct source of truth in 0.17+). +- `/chrome onboard`, `/chrome doctor`, `/chrome status` now query `daemonStatus()` for the + `connected` flag instead of reading stale Pi-side `lastSeenAt`. + ## 0.17.0 — 2026-05-16 ### Standalone daemon (no more owner-vs-client topology)
M extensions/chrome-profile-bridge/browser-extension/manifest.json → extensions/chrome-profile-bridge/browser-extension/manifest.json

@@ -1,7 +1,7 @@

{ "manifest_version": 3, "name": "Pi Chrome Connector", - "version": "0.17.0", + "version": "0.17.1", "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.", "permissions": [ "tabs",
M extensions/chrome-profile-bridge/index.ts → extensions/chrome-profile-bridge/index.ts

@@ -895,6 +895,22 @@ }

console.warn(`pi-chrome: killed PID ${pid}; will spawn the daemon next.`); } + // Probe the daemon's /status and return its view (which is the only source of truth for + // `connected` / `lastSeenAt` in 0.17+, since the extension polls the daemon, not Pi). + async daemonStatus(timeoutMs = 1_000): Promise<Record<string, unknown> | undefined> { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetch(`${this.url}/status`, { signal: controller.signal }); + if (!res.ok) return undefined; + return (await res.json()) as Record<string, unknown>; + } catch { + return undefined; + } finally { + clearTimeout(timer); + } + } + // Signed admin RPC to the daemon. Used for state mutations the daemon owns: arm-pair-window, // reset, status. Pre-pair (when the daemon has no state file), unauthenticated; once paired, // the daemon requires the broker-key envelope (same signature as /command).

@@ -1453,7 +1469,7 @@ // delivered) or no recent /next poll (extension absent/asleep). Otherwise we'd block

// the doctor command for the full 35s waiting for a response that will never come. if (!bridge.bridgeAuth.paired) { lines.push("✗ Skipping extension probe — bridge isn't paired yet. Run /chrome onboard."); - } else if (!status.connected) { + } else if (!(await bridge.daemonStatus(1_500))?.connected) { lines.push( "✗ Chrome extension hasn't polled the bridge recently.", " Fix: load the extension via /chrome onboard, keep that Chrome window open, then re-run /chrome doctor.",

@@ -1744,11 +1760,13 @@ // reload) lets the user proceed to step 3 only to have pairing silently fail. Active

// poll = lastSeenAt updates = `bridge.status().connected` flips true. ctx.ui.notify("Waiting for the extension to wake up and poll the bridge…", "info"); const connectStart = Date.now(); + let connected = false; while (Date.now() - connectStart < 15_000) { - if (bridge.status().connected) break; + const ds = await bridge.daemonStatus(1_500); + if (ds && ds.connected === true) { connected = true; break; } await new Promise((r) => setTimeout(r, 500)); } - if (!bridge.status().connected) { + if (!connected) { ctx.ui.notify( [ "✗ The Chrome extension didn't poll the bridge within 15 s.",

@@ -1837,7 +1855,8 @@ const probeExtension = async (timeoutMs = 3_000): Promise<string> => {

if (!bridge.bridgeAuth.paired) { return "✗ Chrome bridge not paired (run /chrome onboard)"; } - if (!bridge.status().connected) { + const ds = await bridge.daemonStatus(1_000); + if (!ds?.connected) { return "✗ Chrome extension not polling (load extension via /chrome onboard, keep Chrome open)"; } try {
M package.json → package.json

@@ -1,6 +1,6 @@

{ "name": "pi-chrome", - "version": "0.17.0", + "version": "0.17.1", "scripts": { "version": "node scripts/sync-manifest-version.js", "prepublishOnly": "node scripts/sync-manifest-version.js"