pi-chrome 0.15.12 → 0.15.14

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable user-facing changes to `pi-chrome`.
4
4
 
5
+ ## 0.15.14 — 2026-05-14
6
+
7
+ - **Clearer consent wait state.** After the Chrome approval page opens, Pi now says “Approve or deny the Chrome approval page to continue” instead of looking stuck at the launch step.
8
+
9
+ ## 0.15.13 — 2026-05-14
10
+
11
+ - **Fix Chrome-side consent hang.** `/chrome authorize` now launches the browser consent page as a short command, then polls for the decision. This avoids holding one long extension command open while the user reads/clicks the page, which could leave Pi stuck at “Opening Chrome approval page…”.
12
+
5
13
  ## 0.15.12 — 2026-05-14
6
14
 
7
15
  - **Docs accuracy.** Clarified that the bundled Chrome extension currently polls `127.0.0.1:17318`; custom bridge ports are not supported without editing/reloading the extension source. Also softened the unpacked-extension rationale to avoid overstating Web Store limitations and fixed stale strict-CSP guidance for `chrome_evaluate`.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.12",
4
+ "version": "0.15.14",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -4,7 +4,8 @@ const POLL_ERROR_BACKOFF_MS = 2000;
4
4
  const CONSENT_TIMEOUT_MS = 5 * 60 * 1000;
5
5
  let polling = false;
6
6
  let nextConsentRequestId = 1;
7
- const pendingConsentRequests = new Map(); // id -> { request, resolve, timer, tabId }
7
+ const pendingConsentRequests = new Map(); // id -> { request, timer, tabId }
8
+ const completedConsentRequests = new Map(); // id -> { approved, reason, id, decidedAt }
8
9
 
9
10
  // =================== Chrome input (CDP) layer ===================
10
11
  // Tracks which tabs we have attached chrome.debugger to.
@@ -607,35 +608,45 @@ function consentRequestSnapshot(id, request) {
607
608
  };
608
609
  }
609
610
 
611
+ function completeBrowserConsent(id, approved, reason, closeTab = true) {
612
+ const pending = pendingConsentRequests.get(id);
613
+ if (!pending) return completedConsentRequests.get(id) || null;
614
+ pendingConsentRequests.delete(id);
615
+ clearTimeout(pending.timer);
616
+ if (closeTab && pending.tabId) chrome.tabs.remove(pending.tabId).catch(() => undefined);
617
+ const result = { approved, reason, id, decidedAt: Date.now() };
618
+ completedConsentRequests.set(id, result);
619
+ setTimeout(() => completedConsentRequests.delete(id), 10 * 60 * 1000);
620
+ return result;
621
+ }
622
+
610
623
  async function requestBrowserConsent(params) {
611
624
  const id = String(nextConsentRequestId++);
612
625
  const request = {
613
626
  ...params,
614
627
  requestedAt: Date.now(),
615
628
  };
629
+ const timer = setTimeout(() => completeBrowserConsent(id, false, "timed out waiting for browser approval"), CONSENT_TIMEOUT_MS);
630
+ pendingConsentRequests.set(id, { request, timer, tabId: null });
616
631
  const url = chrome.runtime.getURL(`consent.html?id=${encodeURIComponent(id)}`);
617
- const decision = new Promise((resolve) => {
618
- const finish = (approved, reason) => {
619
- const pending = pendingConsentRequests.get(id);
620
- if (!pending) return;
621
- pendingConsentRequests.delete(id);
622
- clearTimeout(pending.timer);
623
- if (pending.tabId) chrome.tabs.remove(pending.tabId).catch(() => undefined);
624
- resolve({ approved, reason, id, decidedAt: Date.now() });
625
- };
626
- const timer = setTimeout(() => finish(false, "timed out waiting for browser approval"), CONSENT_TIMEOUT_MS);
627
- pendingConsentRequests.set(id, { request, resolve: finish, timer, tabId: null });
628
- });
629
632
  try {
630
633
  const tab = await chrome.tabs.create({ url, active: true });
631
634
  if (tab.windowId !== undefined) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => undefined);
632
635
  const pending = pendingConsentRequests.get(id);
633
636
  if (pending) pending.tabId = tab.id;
634
637
  } catch (error) {
635
- const pending = pendingConsentRequests.get(id);
636
- if (pending) pending.resolve(false, `could not open consent tab: ${error?.message || error}`);
638
+ completeBrowserConsent(id, false, `could not open consent tab: ${error?.message || error}`, false);
637
639
  }
638
- return await decision;
640
+ return { id, opened: pendingConsentRequests.has(id), timeoutMs: CONSENT_TIMEOUT_MS };
641
+ }
642
+
643
+ function browserConsentStatus(params) {
644
+ const id = String(params?.id || "");
645
+ const completed = completedConsentRequests.get(id);
646
+ if (completed) return { state: "done", ...completed };
647
+ const pending = pendingConsentRequests.get(id);
648
+ if (pending) return { state: "pending", id, requestedAt: pending.request.requestedAt, timeoutMs: CONSENT_TIMEOUT_MS };
649
+ return { state: "missing", id, approved: false, reason: "consent request expired or not found" };
639
650
  }
640
651
 
641
652
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
@@ -648,12 +659,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
648
659
  }
649
660
  if (message.type === "piChromeConsentDecision") {
650
661
  const id = String(message.id || "");
651
- const pending = pendingConsentRequests.get(id);
652
- if (!pending) {
662
+ const result = completeBrowserConsent(id, message.approved === true, message.approved === true ? "approved in Chrome" : "denied in Chrome");
663
+ if (!result) {
653
664
  sendResponse({ ok: false, error: "Consent request expired or not found" });
654
665
  return true;
655
666
  }
656
- pending.resolve(message.approved === true, message.approved === true ? "approved in Chrome" : "denied in Chrome");
657
667
  sendResponse({ ok: true });
658
668
  return true;
659
669
  }
@@ -662,11 +672,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
662
672
 
663
673
  chrome.tabs.onRemoved.addListener((tabId) => {
664
674
  for (const [id, pending] of pendingConsentRequests) {
665
- if (pending.tabId === tabId) {
666
- pending.resolve(false, "consent tab closed");
667
- pendingConsentRequests.delete(id);
668
- clearTimeout(pending.timer);
669
- }
675
+ if (pending.tabId === tabId) completeBrowserConsent(id, false, "consent tab closed", false);
670
676
  }
671
677
  });
672
678
 
@@ -767,6 +773,8 @@ async function dispatch(action, params) {
767
773
  };
768
774
  case "consent.request":
769
775
  return requestBrowserConsent(params);
776
+ case "consent.status":
777
+ return browserConsentStatus(params);
770
778
  case "tab.list":
771
779
  return (await chrome.tabs.query({})).map(formatTab);
772
780
  case "tab.new": {
@@ -463,6 +463,8 @@ export default function (pi: ExtensionAPI): void {
463
463
  return bridge.send(action, params, timeoutMs);
464
464
  };
465
465
 
466
+ const sleep = (ms: number) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
467
+
466
468
  // Translate the public `background` parameter (default false = visible/foreground) into the
467
469
  // service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback.
468
470
  const withBackground = <T extends Record<string, unknown>>(params: T): T => {
@@ -610,14 +612,14 @@ Usage rules:
610
612
 
611
613
  const authorizeFor = async (ctx: ExtensionContext, label: string, until: number | "indefinite") => {
612
614
  ctx.ui.notify("Opening Chrome approval page…", "info");
613
- let consent: { approved?: boolean; reason?: string };
615
+ let request: { id?: string; opened?: boolean; timeoutMs?: number };
614
616
  try {
615
- consent = (await bridge.send("consent.request", {
617
+ request = (await bridge.send("consent.request", {
616
618
  durationLabel: label,
617
619
  workspace: workspaceCwd(ctx),
618
620
  pid: process.pid,
619
621
  piChromeVersion: PI_CHROME_VERSION,
620
- }, 5 * 60_000 + 5_000)) as { approved?: boolean; reason?: string };
622
+ }, 10_000)) as { id?: string; opened?: boolean; timeoutMs?: number };
621
623
  } catch (error) {
622
624
  const message = (error as Error).message;
623
625
  const hint = message.includes("Unknown action: consent.request")
@@ -626,12 +628,37 @@ Usage rules:
626
628
  ctx.ui.notify(`Chrome approval failed: ${message}\n${hint}`, "warning");
627
629
  return;
628
630
  }
629
- if (!consent.approved) {
630
- ctx.ui.notify(`Chrome control remains locked${consent.reason ? ` (${consent.reason})` : ""}.`, "info");
631
+ if (!request.id) {
632
+ ctx.ui.notify("Chrome approval failed: companion extension did not return a consent request id.", "warning");
633
+ return;
634
+ }
635
+ if (!request.opened) {
636
+ ctx.ui.notify("Chrome approval failed: companion extension could not open the consent page.", "warning");
631
637
  return;
632
638
  }
633
- chromeAuthorizedUntil = until;
634
- ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
639
+ ctx.ui.notify("Approve or deny the Chrome approval page to continue.", "info");
640
+
641
+ const deadline = Date.now() + (request.timeoutMs ?? 5 * 60_000);
642
+ while (Date.now() < deadline + 2_000) {
643
+ await sleep(700);
644
+ let status: { state?: string; approved?: boolean; reason?: string };
645
+ try {
646
+ status = (await bridge.send("consent.status", { id: request.id }, 5_000)) as { state?: string; approved?: boolean; reason?: string };
647
+ } catch (error) {
648
+ ctx.ui.notify(`Chrome approval failed: ${(error as Error).message}`, "warning");
649
+ return;
650
+ }
651
+ if (status.state !== "pending") {
652
+ if (!status.approved) {
653
+ ctx.ui.notify(`Chrome control remains locked${status.reason ? ` (${status.reason})` : ""}.`, "info");
654
+ return;
655
+ }
656
+ chromeAuthorizedUntil = until;
657
+ ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
658
+ return;
659
+ }
660
+ }
661
+ ctx.ui.notify("Chrome control remains locked (timed out waiting for browser approval).", "info");
635
662
  };
636
663
 
637
664
  const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.12",
3
+ "version": "0.15.14",
4
4
  "scripts": {
5
5
  "version": "node scripts/sync-manifest-version.js",
6
6
  "prepublishOnly": "node scripts/sync-manifest-version.js"