pi-chrome 0.15.29 → 0.15.30
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,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes to `pi-chrome`.
|
|
4
4
|
|
|
5
|
+
## 0.15.30 — 2026-05-31
|
|
6
|
+
|
|
7
|
+
Tab grouping for `chrome_tab`.
|
|
8
|
+
|
|
9
|
+
- **Pi-opened tabs auto-group.** `action=new` now drops every tab into a shared `Pi` tab group per window by default (created once, then reused), so agent tabs stay visually separated from your own. Opt out per call with `groupTitle:""` or `group:false`.
|
|
10
|
+
- **`chrome_tab` can group/ungroup tabs.** New `action=group` (and `action=ungroup`) plus `groupTitle`/`groupColor` params. Grouping reuses an existing same-title group in the window instead of spawning duplicates. Defaults: title `Pi`, color `blue`; color validated against Chrome's 9 group colors. Target an existing tab with `targetId`/`urlIncludes`/`titleIncludes`.
|
|
11
|
+
- **Tab listings include group info.** `formatTab` now reports `groupId` and a `group` record (`title`, `color`, `collapsed`, `windowId`, `piGroup`), and `chrome_tab list` prefixes grouped tabs with `[Group Title]`.
|
|
12
|
+
- Requires the new `tabGroups` extension permission — reload the companion extension after updating.
|
|
13
|
+
|
|
5
14
|
## 0.15.29 — 2026-05-31
|
|
6
15
|
|
|
7
16
|
Strict-CSP support: `chrome_evaluate`, `chrome_snapshot`, `chrome_wait_for`, and `chrome_navigate initScript` now work on pages that block `unsafe-eval`.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Pi Chrome Connector",
|
|
4
|
-
"version": "0.15.
|
|
4
|
+
"version": "0.15.30",
|
|
5
5
|
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
8
|
+
"tabGroups",
|
|
8
9
|
"scripting",
|
|
9
10
|
"storage",
|
|
10
11
|
"activeTab",
|
|
@@ -1,6 +1,9 @@
|
|
|
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 DEFAULT_GROUP_COLOR = "blue";
|
|
5
|
+
const PI_GROUP_RE = /^Pi(\b|\s*-)/i;
|
|
6
|
+
const VALID_GROUP_COLORS = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
|
|
4
7
|
let polling = false;
|
|
5
8
|
|
|
6
9
|
// =================== Chrome input (CDP) layer ===================
|
|
@@ -709,6 +712,58 @@ function isVersionOlder(a, b) {
|
|
|
709
712
|
return false;
|
|
710
713
|
}
|
|
711
714
|
|
|
715
|
+
function cleanGroupTitle(value) {
|
|
716
|
+
const text = String(value || "Pi").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
717
|
+
return text || "Pi";
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function cleanGroupColor(value) {
|
|
721
|
+
const color = String(value || DEFAULT_GROUP_COLOR).toLowerCase();
|
|
722
|
+
return VALID_GROUP_COLORS.has(color) ? color : DEFAULT_GROUP_COLOR;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function groupRecord(groupId) {
|
|
726
|
+
if (typeof groupId !== "number" || groupId < 0 || !chrome.tabGroups) return null;
|
|
727
|
+
const group = await chrome.tabGroups.get(groupId).catch(() => null);
|
|
728
|
+
if (!group) return null;
|
|
729
|
+
return {
|
|
730
|
+
id: group.id,
|
|
731
|
+
title: group.title || "",
|
|
732
|
+
color: group.color || "",
|
|
733
|
+
collapsed: Boolean(group.collapsed),
|
|
734
|
+
windowId: group.windowId,
|
|
735
|
+
piGroup: Boolean(group.title && PI_GROUP_RE.test(group.title)),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Find an existing tab group in `windowId` whose title matches `title` (case-insensitive).
|
|
740
|
+
// Used so all Pi-opened tabs collect into one group per window instead of spawning new ones.
|
|
741
|
+
async function findGroupByTitle(windowId, title) {
|
|
742
|
+
if (!chrome.tabGroups) return null;
|
|
743
|
+
const wanted = cleanGroupTitle(title).toLowerCase();
|
|
744
|
+
const groups = await chrome.tabGroups.query({ windowId }).catch(() => []);
|
|
745
|
+
const match = groups.find((g) => (g.title || "").trim().toLowerCase() === wanted);
|
|
746
|
+
return match ? match.id : null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Add `tab` to a tab group, then set title/color. If the tab is ungrouped, reuse an
|
|
750
|
+
// existing same-title group in its window when present, otherwise create a new group.
|
|
751
|
+
async function groupTab(tab, title, color) {
|
|
752
|
+
if (!chrome.tabGroups) throw new Error("chrome.tabGroups API unavailable; reload the extension after granting the tabGroups permission");
|
|
753
|
+
if (!tab || typeof tab.id !== "number") throw new Error("No tab to group");
|
|
754
|
+
const groupTitle = cleanGroupTitle(title);
|
|
755
|
+
let groupId = tab.groupId;
|
|
756
|
+
if (typeof groupId !== "number" || groupId < 0) {
|
|
757
|
+
const existing = await findGroupByTitle(tab.windowId, groupTitle);
|
|
758
|
+
groupId = existing !== null
|
|
759
|
+
? await chrome.tabs.group({ groupId: existing, tabIds: [tab.id] })
|
|
760
|
+
: await chrome.tabs.group({ tabIds: [tab.id] });
|
|
761
|
+
}
|
|
762
|
+
await chrome.tabGroups.update(groupId, { title: groupTitle, color: cleanGroupColor(color), collapsed: false });
|
|
763
|
+
const grouped = await chrome.tabs.get(tab.id);
|
|
764
|
+
return { tab: await formatTab(grouped), group: await groupRecord(groupId) };
|
|
765
|
+
}
|
|
766
|
+
|
|
712
767
|
async function dispatch(action, params) {
|
|
713
768
|
switch (action) {
|
|
714
769
|
case "tab.version":
|
|
@@ -718,17 +773,31 @@ async function dispatch(action, params) {
|
|
|
718
773
|
bridgeUrl: BRIDGE_URL,
|
|
719
774
|
userAgent: navigator.userAgent,
|
|
720
775
|
};
|
|
721
|
-
case "tab.list":
|
|
722
|
-
|
|
776
|
+
case "tab.list": {
|
|
777
|
+
const tabs = await chrome.tabs.query({});
|
|
778
|
+
return Promise.all(tabs.map(formatTab));
|
|
779
|
+
}
|
|
723
780
|
case "tab.new": {
|
|
724
781
|
const tab = await chrome.tabs.create({ url: params.url || "about:blank", active: true });
|
|
725
|
-
|
|
782
|
+
// Every Pi-opened tab joins a group by default. Pass groupTitle:"" (or group:false) to opt out.
|
|
783
|
+
const optOut = params.groupTitle === "" || params.group === false;
|
|
784
|
+
if (optOut && !params.groupColor) return formatTab(tab);
|
|
785
|
+
return groupTab(tab, params.groupTitle || "Pi", params.groupColor);
|
|
726
786
|
}
|
|
727
787
|
case "tab.activate": {
|
|
728
788
|
const tab = await getTabByParams(params);
|
|
729
789
|
await chrome.windows.update(tab.windowId, { focused: true });
|
|
730
790
|
return formatTab(await chrome.tabs.update(tab.id, { active: true }));
|
|
731
791
|
}
|
|
792
|
+
case "tab.group": {
|
|
793
|
+
const tab = await getTabByParams(params);
|
|
794
|
+
return groupTab(tab, params.groupTitle || "Pi", params.groupColor);
|
|
795
|
+
}
|
|
796
|
+
case "tab.ungroup": {
|
|
797
|
+
const tab = await getTabByParams(params);
|
|
798
|
+
if (typeof tab.groupId === "number" && tab.groupId >= 0) await chrome.tabs.ungroup(tab.id);
|
|
799
|
+
return formatTab(await chrome.tabs.get(tab.id));
|
|
800
|
+
}
|
|
732
801
|
case "tab.close": {
|
|
733
802
|
const tab = await getTabByParams(params);
|
|
734
803
|
await chrome.tabs.remove(tab.id);
|
|
@@ -811,7 +880,7 @@ async function dispatch(action, params) {
|
|
|
811
880
|
} finally {
|
|
812
881
|
if (params.initScript) await unregisterInitScript(tab.id).catch(() => undefined);
|
|
813
882
|
}
|
|
814
|
-
return formatTab(await chrome.tabs.get(updated.id));
|
|
883
|
+
return await formatTab(await chrome.tabs.get(updated.id));
|
|
815
884
|
}
|
|
816
885
|
case "page.screenshot":
|
|
817
886
|
return takeScreenshot(params);
|
|
@@ -820,7 +889,7 @@ async function dispatch(action, params) {
|
|
|
820
889
|
}
|
|
821
890
|
}
|
|
822
891
|
|
|
823
|
-
function formatTab(tab) {
|
|
892
|
+
async function formatTab(tab) {
|
|
824
893
|
return {
|
|
825
894
|
id: tab.id,
|
|
826
895
|
windowId: tab.windowId,
|
|
@@ -831,6 +900,8 @@ function formatTab(tab) {
|
|
|
831
900
|
status: tab.status,
|
|
832
901
|
pinned: tab.pinned,
|
|
833
902
|
incognito: tab.incognito,
|
|
903
|
+
groupId: typeof tab.groupId === "number" ? tab.groupId : -1,
|
|
904
|
+
group: await groupRecord(tab.groupId),
|
|
834
905
|
};
|
|
835
906
|
}
|
|
836
907
|
|
|
@@ -1095,7 +1166,7 @@ async function takeScreenshot(params) {
|
|
|
1095
1166
|
await executeInTab({ ...params, foreground: false }, scrollToY, [tiles.originalScrollY]);
|
|
1096
1167
|
return {
|
|
1097
1168
|
fullPage: true,
|
|
1098
|
-
tab: formatTab(tab),
|
|
1169
|
+
tab: await formatTab(tab),
|
|
1099
1170
|
dimensions: { width: tiles.width, height: tiles.height, viewportHeight: tiles.viewportHeight, dpr: tiles.dpr },
|
|
1100
1171
|
tiles: captured,
|
|
1101
1172
|
};
|
|
@@ -1104,7 +1175,7 @@ async function takeScreenshot(params) {
|
|
|
1104
1175
|
format: params.format || "png",
|
|
1105
1176
|
quality: params.format === "jpeg" ? params.quality : undefined,
|
|
1106
1177
|
});
|
|
1107
|
-
return { dataUrl, tab: formatTab(tab) };
|
|
1178
|
+
return { dataUrl, tab: await formatTab(tab) };
|
|
1108
1179
|
} finally {
|
|
1109
1180
|
if (previousActiveId !== undefined && previousActiveId !== tab.id) {
|
|
1110
1181
|
await chrome.tabs.update(previousActiveId, { active: true }).catch(() => undefined);
|
|
@@ -500,7 +500,7 @@ class ChromeProfileBridge {
|
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
const tabActionValues = ["list", "new", "activate", "close", "version"] as const;
|
|
503
|
+
const tabActionValues = ["list", "new", "activate", "close", "group", "ungroup", "version"] as const;
|
|
504
504
|
const imageFormatValues = ["png", "jpeg"] as const;
|
|
505
505
|
const waitForValues = ["selector", "expression"] as const;
|
|
506
506
|
const CHROME_TOOL_NAMES = [
|
|
@@ -1029,20 +1029,25 @@ Usage rules:
|
|
|
1029
1029
|
pi.registerTool({
|
|
1030
1030
|
name: "chrome_tab",
|
|
1031
1031
|
label: "Chrome Tab",
|
|
1032
|
-
description: "List, create, activate, close, or inspect tabs in the user's existing Chrome profile via the companion extension.",
|
|
1033
|
-
promptSnippet: "List/open/activate/close existing Chrome tabs through the companion extension.",
|
|
1032
|
+
description: "List, create, activate, close, group, ungroup, or inspect tabs in the user's existing Chrome profile via the companion extension.",
|
|
1033
|
+
promptSnippet: "List/open/activate/close/group existing Chrome tabs through the companion extension.",
|
|
1034
1034
|
parameters: Type.Object({
|
|
1035
1035
|
action: StringEnum(tabActionValues),
|
|
1036
1036
|
url: Type.Optional(Type.String({ description: "URL for action=new." })),
|
|
1037
|
-
targetId: Type.Optional(Type.String({ description: "Chrome tab id for activate/close." })),
|
|
1037
|
+
targetId: Type.Optional(Type.String({ description: "Chrome tab id for activate/close/group/ungroup." })),
|
|
1038
|
+
urlIncludes: Type.Optional(Type.String({ description: "Match the target tab by URL substring for activate/close/group/ungroup." })),
|
|
1039
|
+
titleIncludes: Type.Optional(Type.String({ description: "Match the target tab by title substring for activate/close/group/ungroup." })),
|
|
1040
|
+
group: Type.Optional(Type.Boolean({ description: "action=new only: pass false to open an ungrouped tab. By default every Pi-opened tab joins the window's 'Pi' tab group." })),
|
|
1041
|
+
groupTitle: Type.Optional(Type.String({ description: "Tab group title for action=group (or action=new to open into a named group). Defaults to 'Pi'. Pass an empty string on action=new to opt out of grouping." })),
|
|
1042
|
+
groupColor: Type.Optional(Type.String({ description: "Tab group color for action=group/new: grey, blue, red, yellow, green, pink, purple, cyan, or orange. Defaults to blue." })),
|
|
1038
1043
|
host: Type.Optional(Type.String()),
|
|
1039
1044
|
port: Type.Optional(Type.Number()),
|
|
1040
1045
|
}),
|
|
1041
1046
|
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1042
1047
|
const result = await authorizedBridgeSend(`tab.${params.action}`, params, DEFAULT_TIMEOUT_MS, signal);
|
|
1043
1048
|
if (params.action === "list") {
|
|
1044
|
-
const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number }>;
|
|
1045
|
-
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
|
1049
|
+
const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number; group?: { title?: string } | null }>;
|
|
1050
|
+
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.group?.title ? `[${tab.group.title}] ` : ""}${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
|
1046
1051
|
return { content: [{ type: "text", text }], details: { tabs } };
|
|
1047
1052
|
}
|
|
1048
1053
|
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };
|