pi-chrome 0.2.2 → 0.3.0

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/README.md CHANGED
@@ -10,6 +10,7 @@ Multiple Pi sessions can use Chrome at the same time. The first Pi session start
10
10
 
11
11
  - **Uses your existing Chrome profile** — works with the Chrome windows/tabs you are already using, including logged-in GitHub, admin dashboards, local apps, and internal tools.
12
12
  - **Full browser automation toolkit for Pi** — list/create/activate/close tabs, snapshot pages with usable CSS selectors, navigate, evaluate JavaScript, click, type, press keys, wait for page state, and capture screenshots.
13
+ - **Background by default** — agents can inspect, navigate, click, type, and snapshot without bringing Chrome to the foreground or interrupting whatever you are doing. Pass `foreground: true` to a tool only when you specifically need Chrome focused.
13
14
  - **Built-in setup and agent guidance** — `/chrome-onboard` walks users through installing the companion extension, `/chrome-status` checks connectivity, screenshots save to disk, and the prompt primer tells agents to inspect with `chrome_snapshot` before acting and avoid destructive actions unless explicitly requested.
14
15
 
15
16
  ## Install
@@ -111,20 +111,35 @@ async function dispatch(action, params) {
111
111
  return executeInTab(params, waitForPage, [params.kind, params.value, params.timeoutMs || 10000, params.intervalMs || 250]);
112
112
  case "page.navigate": {
113
113
  const tab = await getTabByParams(params);
114
+ if (params.foreground) await bringToFront(tab);
114
115
  const wait = params.waitUntilLoad !== false ? waitForTabComplete(tab.id, params.timeoutMs || 15000) : Promise.resolve(undefined);
115
- const updated = await chrome.tabs.update(tab.id, { url: params.url, active: true });
116
+ const updated = await chrome.tabs.update(tab.id, { url: params.url });
116
117
  await wait;
117
118
  return formatTab(await chrome.tabs.get(updated.id));
118
119
  }
119
120
  case "page.screenshot": {
120
121
  const tab = await getTabByParams(params);
121
- await chrome.windows.update(tab.windowId, { focused: true });
122
- await chrome.tabs.update(tab.id, { active: true });
123
- const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
124
- format: params.format || "png",
125
- quality: params.format === "jpeg" ? params.quality : undefined,
126
- });
127
- return { dataUrl, tab: formatTab(tab) };
122
+ if (params.foreground) await bringToFront(tab);
123
+ // captureVisibleTab requires the target tab to be the active tab in its window. Activate it
124
+ // without focusing the window so other apps don't get pushed behind Chrome, and restore the
125
+ // previous active tab afterwards to minimize disruption.
126
+ let previousActiveId;
127
+ if (!tab.active) {
128
+ const activeBefore = await chrome.tabs.query({ active: true, windowId: tab.windowId });
129
+ previousActiveId = activeBefore[0]?.id;
130
+ await chrome.tabs.update(tab.id, { active: true });
131
+ }
132
+ try {
133
+ const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
134
+ format: params.format || "png",
135
+ quality: params.format === "jpeg" ? params.quality : undefined,
136
+ });
137
+ return { dataUrl, tab: formatTab(tab) };
138
+ } finally {
139
+ if (previousActiveId !== undefined && previousActiveId !== tab.id) {
140
+ await chrome.tabs.update(previousActiveId, { active: true }).catch(() => undefined);
141
+ }
142
+ }
128
143
  }
129
144
  default:
130
145
  throw new Error(`Unknown action: ${action}`);
@@ -168,8 +183,7 @@ async function getTabByParams(params) {
168
183
 
169
184
  async function executeInTab(params, func, args) {
170
185
  const tab = await getTabByParams(params);
171
- await chrome.windows.update(tab.windowId, { focused: true });
172
- await chrome.tabs.update(tab.id, { active: true });
186
+ if (params.foreground) await bringToFront(tab);
173
187
  const results = await chrome.scripting.executeScript({
174
188
  target: { tabId: tab.id },
175
189
  world: "MAIN",
@@ -179,6 +193,11 @@ async function executeInTab(params, func, args) {
179
193
  return results?.[0]?.result;
180
194
  }
181
195
 
196
+ async function bringToFront(tab) {
197
+ await chrome.windows.update(tab.windowId, { focused: true });
198
+ await chrome.tabs.update(tab.id, { active: true });
199
+ }
200
+
182
201
  function waitForTabComplete(tabId, timeoutMs) {
183
202
  return new Promise((resolve, reject) => {
184
203
  const timer = setTimeout(() => {
@@ -337,7 +337,7 @@ export default function (pi: ExtensionAPI): void {
337
337
  <chrome-profile-bridge>
338
338
  Chrome control is available through the chrome_* tools via a companion Chrome extension installed in the user's normal Chrome profile.
339
339
  This is not CDP: it can use the user's existing Chrome windows and authenticated sessions after the user loads the companion browser extension.
340
- If chrome_* tools time out, ask the user to run /chrome-onboard, then load the bundled browser-extension folder in chrome://extensions. Prefer chrome_snapshot before clicking/typing. Avoid destructive actions unless explicitly requested.
340
+ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the bundled browser-extension folder in chrome://extensions. Prefer chrome_snapshot before clicking/typing. Avoid destructive actions unless explicitly requested. By default chrome_* tools run in the background and do not bring Chrome to the foreground; only pass foreground=true when you specifically need Chrome focused (for example, demoing a flow or capturing a screenshot the user should see).
341
341
  </chrome-profile-bridge>`;
342
342
  return { systemPrompt: event.systemPrompt + primer };
343
343
  });
@@ -448,13 +448,16 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
448
448
  name: "chrome_snapshot",
449
449
  label: "Chrome Snapshot",
450
450
  description:
451
- "Inspect a page in the user's existing Chrome profile: title, URL, visible body text, viewport, and clickable/focusable elements with CSS selectors.",
451
+ "Inspect a page in the user's existing Chrome profile: title, URL, visible body text, viewport, and clickable/focusable elements with CSS selectors. Runs in the background by default; pass foreground=true to bring Chrome to the front first.",
452
452
  promptSnippet: "Inspect the current Chrome page and get CSS selectors for browser automation.",
453
453
  parameters: Type.Object({
454
454
  targetId: Type.Optional(Type.String()),
455
455
  urlIncludes: Type.Optional(Type.String()),
456
456
  titleIncludes: Type.Optional(Type.String()),
457
457
  maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS })),
458
+ foreground: Type.Optional(
459
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before inspecting. Default false to avoid interrupting the user." }),
460
+ ),
458
461
  host: Type.Optional(Type.String()),
459
462
  port: Type.Optional(Type.Number()),
460
463
  }),
@@ -467,7 +470,8 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
467
470
  pi.registerTool({
468
471
  name: "chrome_navigate",
469
472
  label: "Chrome Navigate",
470
- description: "Navigate an existing Chrome tab to a URL via the companion extension. Optionally waits for load completion.",
473
+ description:
474
+ "Navigate an existing Chrome tab to a URL via the companion extension. Navigates in the background by default and does not change the user's currently active tab; pass foreground=true to also focus Chrome and activate the tab. Optionally waits for load completion.",
471
475
  promptSnippet: "Navigate a Chrome tab in the user's existing profile.",
472
476
  parameters: Type.Object({
473
477
  url: Type.String(),
@@ -476,6 +480,9 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
476
480
  titleIncludes: Type.Optional(Type.String()),
477
481
  waitUntilLoad: Type.Optional(Type.Boolean({ default: true })),
478
482
  timeoutMs: Type.Optional(Type.Number({ default: 15_000 })),
483
+ foreground: Type.Optional(
484
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before navigating. Default false." }),
485
+ ),
479
486
  host: Type.Optional(Type.String()),
480
487
  port: Type.Optional(Type.Number()),
481
488
  }),
@@ -489,7 +496,7 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
489
496
  name: "chrome_evaluate",
490
497
  label: "Chrome Evaluate",
491
498
  description:
492
- "Evaluate JavaScript in an existing Chrome tab through the companion extension. Runs in the page context and returns JSON-serializable values when possible.",
499
+ "Evaluate JavaScript in an existing Chrome tab through the companion extension. Runs in the page context (background) and returns JSON-serializable values when possible. Pass foreground=true to also focus Chrome and activate the tab.",
493
500
  promptSnippet: "Evaluate JavaScript in the active Chrome tab through the companion extension.",
494
501
  parameters: Type.Object({
495
502
  expression: Type.String(),
@@ -498,6 +505,9 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
498
505
  targetId: Type.Optional(Type.String()),
499
506
  urlIncludes: Type.Optional(Type.String()),
500
507
  titleIncludes: Type.Optional(Type.String()),
508
+ foreground: Type.Optional(
509
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before evaluating. Default false." }),
510
+ ),
501
511
  host: Type.Optional(Type.String()),
502
512
  port: Type.Optional(Type.Number()),
503
513
  }),
@@ -510,7 +520,8 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
510
520
  pi.registerTool({
511
521
  name: "chrome_click",
512
522
  label: "Chrome Click",
513
- description: "Click a CSS selector or viewport coordinate in an existing Chrome tab through the companion extension.",
523
+ description:
524
+ "Click a CSS selector or viewport coordinate in an existing Chrome tab through the companion extension. The click is dispatched as a synthetic DOM event in the background by default; pass foreground=true to focus Chrome and activate the tab first.",
514
525
  promptSnippet: "Click page elements in Chrome by selector or viewport coordinate.",
515
526
  parameters: Type.Object({
516
527
  selector: Type.Optional(Type.String({ description: "CSS selector to click. Prefer selectors from chrome_snapshot." })),
@@ -519,6 +530,9 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
519
530
  targetId: Type.Optional(Type.String()),
520
531
  urlIncludes: Type.Optional(Type.String()),
521
532
  titleIncludes: Type.Optional(Type.String()),
533
+ foreground: Type.Optional(
534
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before clicking. Default false." }),
535
+ ),
522
536
  host: Type.Optional(Type.String()),
523
537
  port: Type.Optional(Type.Number()),
524
538
  }),
@@ -531,7 +545,8 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
531
545
  pi.registerTool({
532
546
  name: "chrome_type",
533
547
  label: "Chrome Type",
534
- description: "Focus an optional CSS selector, then type text into an existing Chrome tab through the companion extension.",
548
+ description:
549
+ "Focus an optional CSS selector, then type text into an existing Chrome tab through the companion extension. Runs in the background by default; pass foreground=true to focus Chrome and activate the tab first.",
535
550
  promptSnippet: "Type text into Chrome, optionally focusing a selector first.",
536
551
  parameters: Type.Object({
537
552
  text: Type.String(),
@@ -540,6 +555,9 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
540
555
  targetId: Type.Optional(Type.String()),
541
556
  urlIncludes: Type.Optional(Type.String()),
542
557
  titleIncludes: Type.Optional(Type.String()),
558
+ foreground: Type.Optional(
559
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before typing. Default false." }),
560
+ ),
543
561
  host: Type.Optional(Type.String()),
544
562
  port: Type.Optional(Type.Number()),
545
563
  }),
@@ -552,13 +570,17 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
552
570
  pi.registerTool({
553
571
  name: "chrome_key",
554
572
  label: "Chrome Key",
555
- description: "Send a keyboard key to an existing Chrome tab (Enter, Escape, Tab, Backspace, Delete, ArrowUp/Down/Left/Right, or one character).",
573
+ description:
574
+ "Send a keyboard key to an existing Chrome tab (Enter, Escape, Tab, Backspace, Delete, ArrowUp/Down/Left/Right, or one character). Runs in the background by default; pass foreground=true to focus Chrome and activate the tab first.",
556
575
  promptSnippet: "Press keys in Chrome through the companion extension.",
557
576
  parameters: Type.Object({
558
577
  key: Type.String(),
559
578
  targetId: Type.Optional(Type.String()),
560
579
  urlIncludes: Type.Optional(Type.String()),
561
580
  titleIncludes: Type.Optional(Type.String()),
581
+ foreground: Type.Optional(
582
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before sending the key. Default false." }),
583
+ ),
562
584
  host: Type.Optional(Type.String()),
563
585
  port: Type.Optional(Type.Number()),
564
586
  }),
@@ -593,7 +615,8 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
593
615
  pi.registerTool({
594
616
  name: "chrome_screenshot",
595
617
  label: "Chrome Screenshot",
596
- description: "Capture a screenshot of an existing Chrome tab via the companion extension and save it to disk.",
618
+ description:
619
+ "Capture a screenshot of an existing Chrome tab via the companion extension and save it to disk. Chrome's extension screenshot API requires the target tab to be the active tab in its window, so this momentarily activates it (without focusing the window) and restores the previous active tab. Pass foreground=true to also bring the Chrome window to the front.",
597
620
  promptSnippet: "Capture Chrome screenshots and save them under .pi/chrome-screenshots by default.",
598
621
  parameters: Type.Object({
599
622
  path: Type.Optional(Type.String({ description: "Output path. Defaults to .pi/chrome-screenshots/<timestamp>.<format>." })),
@@ -603,6 +626,9 @@ If chrome_* tools time out, ask the user to run /chrome-onboard, then load the b
603
626
  targetId: Type.Optional(Type.String()),
604
627
  urlIncludes: Type.Optional(Type.String()),
605
628
  titleIncludes: Type.Optional(Type.String()),
629
+ foreground: Type.Optional(
630
+ Type.Boolean({ description: "If true, focus the Chrome window and activate the tab before capturing. Default false." }),
631
+ ),
606
632
  host: Type.Optional(Type.String()),
607
633
  port: Type.Optional(Type.Number()),
608
634
  }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.2.2",
4
- "description": "Control your existing authenticated Chrome profile from one or more Pi sessions with tabs, snapshots, clicks, typing, JS evaluation, waits, and screenshots.",
3
+ "version": "0.3.0",
4
+ "description": "Control your existing authenticated Chrome profile from one or more Pi sessions with tabs, snapshots, clicks, typing, JS evaluation, waits, and screenshots. Background-by-default so Chrome stops popping up when agents act.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",