pi-chrome 0.15.14 → 0.15.16

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.16 — 2026-05-14
6
+
7
+ - **Visible `/chrome` loading state.** Bare `/chrome` and `/chrome status` now immediately say “Checking Chrome connection…” before probing the companion extension, so a slow Chrome bridge no longer looks like the command did nothing.
8
+
9
+ ## 0.15.15 — 2026-05-14
10
+
11
+ - **Terminal authorization restored.** `/chrome authorize` is back to terminal-based confirmation. Removed the browser-side Chrome consent page and companion-extension consent polling.
12
+
5
13
  ## 0.15.14 — 2026-05-14
6
14
 
7
15
  - **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.
package/README.md CHANGED
@@ -30,11 +30,11 @@ Then in Pi:
30
30
 
31
31
  On macOS this opens `chrome://extensions`, reveals the bundled `browser-extension/` folder in Finder, and copies its path to your clipboard. In Chrome: **Developer mode** → **Load unpacked** → paste the path. Done.
32
32
 
33
- Verify, then authorize current Pi session in Chrome:
33
+ Verify, then authorize current Pi session in Pi:
34
34
 
35
35
  ```text
36
36
  /chrome doctor
37
- /chrome authorize # opens a Chrome approval page
37
+ /chrome authorize
38
38
  ```
39
39
 
40
40
  ```text
@@ -162,7 +162,7 @@ Each tool is documented inline in Pi — agents see the parameters and gotchas (
162
162
 
163
163
  ### Authorization
164
164
 
165
- Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session. `/chrome authorize` opens a browser-side approval page in Chrome; control unlocks only after you approve there.
165
+ Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session from the terminal with `/chrome authorize`.
166
166
 
167
167
  ```text
168
168
  /chrome authorize # default: authorize for 15 minutes
@@ -173,7 +173,7 @@ Chrome control is locked by default. Before any agent can use `chrome_*` tools,
173
173
  /chrome status # shows connection + auth + background
174
174
  ```
175
175
 
176
- This protects your signed-in Chrome profile from accidental agent use and makes the approval happen inside the browser profile being controlled. The loopback bridge also rejects browser-origin command requests so arbitrary web pages cannot call into `127.0.0.1:17318` through CORS.
176
+ This protects your signed-in Chrome profile from accidental agent use. The loopback bridge also rejects browser-origin command requests so arbitrary web pages cannot call into `127.0.0.1:17318` through CORS.
177
177
 
178
178
  ### Run in background / watch modes
179
179
 
@@ -239,7 +239,7 @@ If you build a competing tool, please open a PR with your scores. We benchmark i
239
239
 
240
240
  **Unpacked on purpose.** pi-chrome ships as an inspectable, MIT-licensed extension folder you load once with Developer Mode, so the local bridge and browser permissions are easy to audit and update without a Web Store release cycle. Every line is yours to read in [`extensions/chrome-profile-bridge/browser-extension/`](./extensions/chrome-profile-bridge/browser-extension). `/chrome doctor` reports the loaded extension version and warns when it drifts from your installed `pi-chrome`.
241
241
 
242
- The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi and approve the browser-side consent page in Chrome. Use `/chrome revoke` to lock them again.
242
+ The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi. Use `/chrome revoke` to lock them again.
243
243
 
244
244
  The Pi side listens on `127.0.0.1:17318` and rejects browser-origin command requests; ordinary web pages cannot use CORS to drive the bridge. The bundled Chrome extension currently polls that default port, so custom bridge ports are not supported without editing the extension source and reloading it.
245
245
 
package/SECURITY.md CHANGED
@@ -9,13 +9,13 @@ Open a GitHub issue prefixed with `[security]` at https://github.com/tianrendong
9
9
  `pi-chrome` is a developer tool you install knowingly. It is **not** designed to defend against:
10
10
 
11
11
  - Hostile pages running in your Chrome trying to detect or escape automation. (Standard browser security boundaries still apply, but a hostile page that already runs in your tab can do anything that page can already do.)
12
- - Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` plus browser-side consent in Chrome, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
12
+ - Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` inside Pi, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
13
13
 
14
14
  `pi-chrome` **is** designed to:
15
15
 
16
16
  - Never exfiltrate page state to the network. All communication is loopback (`127.0.0.1`).
17
17
  - Surface every action with an honest result envelope so the agent can't silently do the wrong thing.
18
- - Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session and approves the Chrome-side consent page.
18
+ - Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session.
19
19
  - Reject browser-origin command requests to the loopback bridge so ordinary web pages cannot use CORS to drive Chrome.
20
20
 
21
21
  ## The companion extension
@@ -26,7 +26,7 @@ The Chrome extension under `extensions/chrome-profile-bridge/browser-extension/`
26
26
 
27
27
  - Loopback bridge only. No remote port. No telemetry.
28
28
  - Chrome real input layer for interactive controls.
29
- - Chrome control locked by default; `/chrome authorize` opens a Chrome consent page, approval unlocks current Pi session, `/chrome revoke` locks it again.
29
+ - Chrome control locked by default; `/chrome authorize` unlocks current Pi session after terminal confirmation, `/chrome revoke` locks it again.
30
30
  - Run-in-background optional; tab/window focus is observable by default (the user can see Pi acting).
31
31
 
32
32
  ## Custom ports
@@ -126,7 +126,7 @@ Yes — you can export cookies and replay them, or point Playwright at your exis
126
126
  Different security boundary, not strictly safer.
127
127
 
128
128
  - **CDP-based tools** require `chrome --remote-debugging-port=...`. That port is unauthenticated and exposes the whole browser to any local process. Easy to misconfigure.
129
- - **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session and approved on the Chrome-side consent page. **Only install the bundled extension if you trust the source you got the npm package from.**
129
+ - **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session. **Only install the bundled extension if you trust the source you got the npm package from.**
130
130
 
131
131
  If your threat model excludes extensions with broad permissions, neither approach is a fit — you want a sandboxed CI runner.
132
132
 
package/docs/EXAMPLES.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-chrome examples
2
2
 
3
- Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`, then `/chrome authorize` and approving in Chrome. Each one uses Chrome tabs and accounts you already have.
3
+ Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`, then `/chrome authorize`. Each one uses Chrome tabs and accounts you already have.
4
4
 
5
5
  ## Daily workflow
6
6
 
package/docs/FAQ.md CHANGED
@@ -26,11 +26,11 @@ That's Chrome's built-in warning when an extension uses `chrome.debugger`. pi-ch
26
26
 
27
27
  No — pages cannot directly talk to extensions. Commands flow agent → local bridge (`127.0.0.1:17318`) → extension → tab. The bridge binds to loopback only and rejects browser-origin command requests, so ordinary web pages cannot use CORS to drive it.
28
28
 
29
- Chrome control is also locked per Pi session until you run `/chrome authorize` and approve the Chrome-side consent page; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account.
29
+ Chrome control is also locked per Pi session until you run `/chrome authorize`; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account.
30
30
 
31
31
  ## Can multiple Pi sessions use it at once?
32
32
 
33
- Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` and approved in Chrome before its chrome_* tools work.
33
+ Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` before its chrome_* tools work.
34
34
 
35
35
  ## Why can't this be on the Chrome Web Store?
36
36
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.14",
4
+ "version": "0.15.16",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -1,11 +1,7 @@
1
1
  const BRIDGE_URL = "http://127.0.0.1:17318";
2
2
  const CLIENT_NAME = `Pi Chrome Connector ${chrome.runtime.id}`;
3
3
  const POLL_ERROR_BACKOFF_MS = 2000;
4
- const CONSENT_TIMEOUT_MS = 5 * 60 * 1000;
5
4
  let polling = false;
6
- let nextConsentRequestId = 1;
7
- const pendingConsentRequests = new Map(); // id -> { request, timer, tabId }
8
- const completedConsentRequests = new Map(); // id -> { approved, reason, id, decidedAt }
9
5
 
10
6
  // =================== Chrome input (CDP) layer ===================
11
7
  // Tracks which tabs we have attached chrome.debugger to.
@@ -595,87 +591,6 @@ async function chromeInputUpload(params) {
595
591
  // ===============================================================
596
592
 
597
593
 
598
- function consentRequestSnapshot(id, request) {
599
- return {
600
- id,
601
- extensionVersion: chrome.runtime.getManifest().version,
602
- extensionId: chrome.runtime.id,
603
- workspace: String(request.workspace || ""),
604
- pid: request.pid ?? null,
605
- durationLabel: String(request.durationLabel || "15 minutes"),
606
- requestedAt: request.requestedAt || Date.now(),
607
- piChromeVersion: String(request.piChromeVersion || ""),
608
- };
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
-
623
- async function requestBrowserConsent(params) {
624
- const id = String(nextConsentRequestId++);
625
- const request = {
626
- ...params,
627
- requestedAt: Date.now(),
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 });
631
- const url = chrome.runtime.getURL(`consent.html?id=${encodeURIComponent(id)}`);
632
- try {
633
- const tab = await chrome.tabs.create({ url, active: true });
634
- if (tab.windowId !== undefined) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => undefined);
635
- const pending = pendingConsentRequests.get(id);
636
- if (pending) pending.tabId = tab.id;
637
- } catch (error) {
638
- completeBrowserConsent(id, false, `could not open consent tab: ${error?.message || error}`, false);
639
- }
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" };
650
- }
651
-
652
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
653
- if (!message || typeof message !== "object") return false;
654
- if (message.type === "piChromeConsentGet") {
655
- const id = String(message.id || "");
656
- const pending = pendingConsentRequests.get(id);
657
- sendResponse(pending ? { ok: true, request: consentRequestSnapshot(id, pending.request) } : { ok: false, error: "Consent request expired or not found" });
658
- return true;
659
- }
660
- if (message.type === "piChromeConsentDecision") {
661
- const id = String(message.id || "");
662
- const result = completeBrowserConsent(id, message.approved === true, message.approved === true ? "approved in Chrome" : "denied in Chrome");
663
- if (!result) {
664
- sendResponse({ ok: false, error: "Consent request expired or not found" });
665
- return true;
666
- }
667
- sendResponse({ ok: true });
668
- return true;
669
- }
670
- return false;
671
- });
672
-
673
- chrome.tabs.onRemoved.addListener((tabId) => {
674
- for (const [id, pending] of pendingConsentRequests) {
675
- if (pending.tabId === tabId) completeBrowserConsent(id, false, "consent tab closed", false);
676
- }
677
- });
678
-
679
594
  function armKeepaliveAlarm() {
680
595
  chrome.alarms.create("pi-bridge-keepalive", { periodInMinutes: 0.5 });
681
596
  }
@@ -771,10 +686,6 @@ async function dispatch(action, params) {
771
686
  bridgeUrl: BRIDGE_URL,
772
687
  userAgent: navigator.userAgent,
773
688
  };
774
- case "consent.request":
775
- return requestBrowserConsent(params);
776
- case "consent.status":
777
- return browserConsentStatus(params);
778
689
  case "tab.list":
779
690
  return (await chrome.tabs.query({})).map(formatTab);
780
691
  case "tab.new": {
@@ -463,8 +463,6 @@ 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
-
468
466
  // Translate the public `background` parameter (default false = visible/foreground) into the
469
467
  // service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback.
470
468
  const withBackground = <T extends Record<string, unknown>>(params: T): T => {
@@ -611,54 +609,16 @@ Usage rules:
611
609
  };
612
610
 
613
611
  const authorizeFor = async (ctx: ExtensionContext, label: string, until: number | "indefinite") => {
614
- ctx.ui.notify("Opening Chrome approval page…", "info");
615
- let request: { id?: string; opened?: boolean; timeoutMs?: number };
616
- try {
617
- request = (await bridge.send("consent.request", {
618
- durationLabel: label,
619
- workspace: workspaceCwd(ctx),
620
- pid: process.pid,
621
- piChromeVersion: PI_CHROME_VERSION,
622
- }, 10_000)) as { id?: string; opened?: boolean; timeoutMs?: number };
623
- } catch (error) {
624
- const message = (error as Error).message;
625
- const hint = message.includes("Unknown action: consent.request")
626
- ? "Open chrome://extensions and reload 'Pi Chrome Connector', then try /chrome authorize again."
627
- : "Run /chrome doctor if the companion extension is not responding.";
628
- ctx.ui.notify(`Chrome approval failed: ${message}\n${hint}`, "warning");
629
- return;
630
- }
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");
612
+ const ok = await ctx.ui.confirm(
613
+ "Authorize pi-chrome control?",
614
+ `This Pi session will be allowed to inspect and control your existing Chrome profile for ${label}.\n\nChrome actions use your signed-in browser state and real input. Only approve if you trust the current agent/task.`,
615
+ );
616
+ if (!ok) {
617
+ ctx.ui.notify("Chrome control remains locked.", "info");
637
618
  return;
638
619
  }
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");
620
+ chromeAuthorizedUntil = until;
621
+ ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
662
622
  };
663
623
 
664
624
  const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => {
@@ -724,6 +684,7 @@ Usage rules:
724
684
  };
725
685
 
726
686
  const statusHandler = async (ctx: ExtensionContext) => {
687
+ ctx.ui.notify("Checking Chrome connection…", "info");
727
688
  ctx.ui.notify(await statusSummary(), "info");
728
689
  };
729
690
 
@@ -763,6 +724,7 @@ Usage rules:
763
724
 
764
725
  const openCommandMenu = async (ctx: ExtensionContext): Promise<void> => {
765
726
  while (true) {
727
+ ctx.ui.notify("Checking Chrome connection…", "info");
766
728
  const choice = await ctx.ui.select(`pi-chrome\n${await statusSummary()}`, [
767
729
  "Authorize Chrome control…",
768
730
  "Lock Chrome control",
@@ -783,7 +745,7 @@ Usage rules:
783
745
 
784
746
  pi.registerCommand("chrome", {
785
747
  description:
786
- "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — open Chrome approval and 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] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.",
748
+ "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] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.",
787
749
  getArgumentCompletions: (prefix) => {
788
750
  const raw = prefix;
789
751
  const trimmedRight = raw.replace(/\s+$/, "");
@@ -800,7 +762,7 @@ Usage rules:
800
762
  let candidates: Item[] = [];
801
763
  if (path.length === 0) {
802
764
  candidates = [
803
- { fullValue: "authorize", label: "authorize", description: "Open Chrome approval and allow this Pi session to use chrome_* tools." },
765
+ { fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
804
766
  { fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
805
767
  { fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
806
768
  { fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.14",
3
+ "version": "0.15.16",
4
4
  "scripts": {
5
5
  "version": "node scripts/sync-manifest-version.js",
6
6
  "prepublishOnly": "node scripts/sync-manifest-version.js"
@@ -1,141 +0,0 @@
1
- :root {
2
- color-scheme: light dark;
3
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
- background: #0f172a;
5
- color: #e5e7eb;
6
- }
7
-
8
- * { box-sizing: border-box; }
9
-
10
- body {
11
- margin: 0;
12
- min-height: 100vh;
13
- display: grid;
14
- place-items: center;
15
- padding: 32px;
16
- background:
17
- radial-gradient(circle at 20% 10%, rgba(79, 70, 229, 0.35), transparent 30%),
18
- radial-gradient(circle at 80% 80%, rgba(14, 165, 233, 0.2), transparent 32%),
19
- #0f172a;
20
- }
21
-
22
- .card {
23
- width: min(680px, 100%);
24
- background: rgba(15, 23, 42, 0.9);
25
- border: 1px solid rgba(148, 163, 184, 0.25);
26
- border-radius: 24px;
27
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.4);
28
- padding: 32px;
29
- }
30
-
31
- .badge {
32
- display: inline-flex;
33
- align-items: center;
34
- border: 1px solid rgba(129, 140, 248, 0.45);
35
- border-radius: 999px;
36
- padding: 6px 12px;
37
- color: #c7d2fe;
38
- background: rgba(79, 70, 229, 0.18);
39
- font-size: 13px;
40
- font-weight: 700;
41
- }
42
-
43
- h1 {
44
- margin: 18px 0 8px;
45
- font-size: clamp(32px, 6vw, 48px);
46
- line-height: 1;
47
- }
48
-
49
- .lead {
50
- margin: 0 0 24px;
51
- color: #cbd5e1;
52
- font-size: 18px;
53
- }
54
-
55
- .details {
56
- display: grid;
57
- gap: 12px;
58
- margin: 24px 0;
59
- }
60
-
61
- .details div {
62
- display: grid;
63
- grid-template-columns: 140px 1fr;
64
- gap: 16px;
65
- align-items: start;
66
- padding: 14px 16px;
67
- border-radius: 14px;
68
- background: rgba(30, 41, 59, 0.72);
69
- border: 1px solid rgba(148, 163, 184, 0.14);
70
- }
71
-
72
- .details span {
73
- color: #94a3b8;
74
- font-size: 13px;
75
- font-weight: 700;
76
- text-transform: uppercase;
77
- letter-spacing: 0.05em;
78
- }
79
-
80
- .details strong {
81
- color: #f8fafc;
82
- font-size: 15px;
83
- overflow-wrap: anywhere;
84
- }
85
-
86
- .warning {
87
- margin: 0 0 24px;
88
- padding: 16px;
89
- border-radius: 14px;
90
- background: rgba(245, 158, 11, 0.12);
91
- border: 1px solid rgba(245, 158, 11, 0.35);
92
- color: #fde68a;
93
- }
94
-
95
- .actions {
96
- display: flex;
97
- justify-content: flex-end;
98
- gap: 12px;
99
- }
100
-
101
- button {
102
- border: 0;
103
- border-radius: 12px;
104
- padding: 12px 18px;
105
- font: inherit;
106
- font-weight: 800;
107
- cursor: pointer;
108
- }
109
-
110
- button:focus-visible {
111
- outline: 3px solid #93c5fd;
112
- outline-offset: 2px;
113
- }
114
-
115
- .primary {
116
- background: #4f46e5;
117
- color: white;
118
- }
119
-
120
- .primary:hover { background: #4338ca; }
121
-
122
- .secondary {
123
- background: rgba(148, 163, 184, 0.16);
124
- color: #e2e8f0;
125
- }
126
-
127
- .secondary:hover { background: rgba(148, 163, 184, 0.26); }
128
-
129
- .status {
130
- min-height: 20px;
131
- margin: 18px 0 0;
132
- color: #cbd5e1;
133
- }
134
-
135
- @media (max-width: 560px) {
136
- body { padding: 16px; }
137
- .card { padding: 22px; }
138
- .details div { grid-template-columns: 1fr; gap: 6px; }
139
- .actions { flex-direction: column-reverse; }
140
- button { width: 100%; }
141
- }
@@ -1,33 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Authorize pi-chrome</title>
7
- <link rel="stylesheet" href="consent.css">
8
- </head>
9
- <body>
10
- <main class="card">
11
- <div class="badge">Pi Chrome Connector</div>
12
- <h1>Authorize Chrome control?</h1>
13
- <p class="lead">Pi is asking to inspect and control this Chrome profile.</p>
14
-
15
- <section class="details" aria-label="Request details">
16
- <div><span>Duration</span><strong id="duration">—</strong></div>
17
- <div><span>Workspace</span><strong id="workspace">—</strong></div>
18
- <div><span>Pi process</span><strong id="pid">—</strong></div>
19
- <div><span>Extension</span><strong id="extension">—</strong></div>
20
- </section>
21
-
22
- <p class="warning">Approve only if you trust this Pi session and current task. Approved actions use your signed-in browser state and real Chrome input.</p>
23
-
24
- <div class="actions">
25
- <button id="deny" class="secondary" type="button">Deny</button>
26
- <button id="approve" class="primary" type="button" autofocus>Authorize</button>
27
- </div>
28
-
29
- <p id="status" class="status" role="status"></p>
30
- </main>
31
- <script src="consent.js"></script>
32
- </body>
33
- </html>
@@ -1,75 +0,0 @@
1
- const params = new URLSearchParams(location.search);
2
- const id = params.get("id") || "";
3
-
4
- const els = {
5
- duration: document.getElementById("duration"),
6
- workspace: document.getElementById("workspace"),
7
- pid: document.getElementById("pid"),
8
- extension: document.getElementById("extension"),
9
- approve: document.getElementById("approve"),
10
- deny: document.getElementById("deny"),
11
- status: document.getElementById("status"),
12
- };
13
-
14
- function setStatus(text) {
15
- els.status.textContent = text || "";
16
- }
17
-
18
- function setDisabled(disabled) {
19
- els.approve.disabled = disabled;
20
- els.deny.disabled = disabled;
21
- }
22
-
23
- function render(request) {
24
- els.duration.textContent = request.durationLabel || "—";
25
- els.workspace.textContent = request.workspace || "unknown workspace";
26
- els.pid.textContent = request.pid ? String(request.pid) : "unknown";
27
- const versions = [];
28
- if (request.extensionVersion) versions.push(`extension ${request.extensionVersion}`);
29
- if (request.piChromeVersion) versions.push(`pi-chrome ${request.piChromeVersion}`);
30
- els.extension.textContent = versions.join(" · ") || request.extensionId || "—";
31
- }
32
-
33
- async function send(message) {
34
- return await chrome.runtime.sendMessage(message);
35
- }
36
-
37
- async function decide(approved) {
38
- setDisabled(true);
39
- setStatus(approved ? "Authorizing…" : "Denying…");
40
- try {
41
- const response = await send({ type: "piChromeConsentDecision", id, approved });
42
- if (!response?.ok) throw new Error(response?.error || "Consent request failed");
43
- setStatus(approved ? "Authorized. You can close this tab." : "Denied. You can close this tab.");
44
- window.close();
45
- } catch (error) {
46
- setDisabled(false);
47
- setStatus(error?.message || String(error));
48
- }
49
- }
50
-
51
- async function init() {
52
- if (!id) {
53
- setDisabled(true);
54
- setStatus("Missing consent request id.");
55
- return;
56
- }
57
- setStatus("Loading request…");
58
- try {
59
- const response = await send({ type: "piChromeConsentGet", id });
60
- if (!response?.ok) throw new Error(response?.error || "Consent request not found");
61
- render(response.request || {});
62
- setStatus("");
63
- } catch (error) {
64
- setDisabled(true);
65
- setStatus(error?.message || String(error));
66
- }
67
- }
68
-
69
- els.approve.addEventListener("click", () => decide(true));
70
- els.deny.addEventListener("click", () => decide(false));
71
- document.addEventListener("keydown", (event) => {
72
- if (event.key === "Escape") decide(false);
73
- });
74
-
75
- void init();