aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-05-31 14:38:38 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-31 14:38:38 -0400
commitd57dc6583953fcb652db5bd222698d58d3090e20 (patch)
treee7fedbbecbd13f8b24058f50febf9b5222bf8f43 /extensions
parentd21ebf7b96c90943ab48ffcd6d5b02c6f7438c04 (diff)
0.15.30: chrome_tab grouping — auto-group Pi tabs, group/ungroup actions, merge by title
Diffstat (limited to 'extensions')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json3
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js85
-rw-r--r--extensions/chrome-profile-bridge/index.ts17
3 files changed, 91 insertions, 14 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index 55e9152..01ac3b3 100644
--- a/extensions/chrome-profile-bridge/browser-extension/manifest.json
+++ b/extensions/chrome-profile-bridge/browser-extension/manifest.json
@@ -1,10 +1,11 @@
{
"manifest_version": 3,
"name": "Pi Chrome Connector",
- "version": "0.15.29",
+ "version": "0.15.30",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": [
"tabs",
+ "tabGroups",
"scripting",
"storage",
"activeTab",
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index 850784f..eb54dce 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -1,6 +1,9 @@
const BRIDGE_URL = "http://127.0.0.1:17318";
const CLIENT_NAME = `Pi Chrome Connector ${chrome.runtime.id}`;
const POLL_ERROR_BACKOFF_MS = 2000;
+const DEFAULT_GROUP_COLOR = "blue";
+const PI_GROUP_RE = /^Pi(\b|\s*-)/i;
+const VALID_GROUP_COLORS = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
let polling = false;
// =================== Chrome input (CDP) layer ===================
@@ -709,6 +712,58 @@ function isVersionOlder(a, b) {
return false;
}
+function cleanGroupTitle(value) {
+ const text = String(value || "Pi").replace(/\s+/g, " ").trim().slice(0, 80);
+ return text || "Pi";
+}
+
+function cleanGroupColor(value) {
+ const color = String(value || DEFAULT_GROUP_COLOR).toLowerCase();
+ return VALID_GROUP_COLORS.has(color) ? color : DEFAULT_GROUP_COLOR;
+}
+
+async function groupRecord(groupId) {
+ if (typeof groupId !== "number" || groupId < 0 || !chrome.tabGroups) return null;
+ const group = await chrome.tabGroups.get(groupId).catch(() => null);
+ if (!group) return null;
+ return {
+ id: group.id,
+ title: group.title || "",
+ color: group.color || "",
+ collapsed: Boolean(group.collapsed),
+ windowId: group.windowId,
+ piGroup: Boolean(group.title && PI_GROUP_RE.test(group.title)),
+ };
+}
+
+// Find an existing tab group in `windowId` whose title matches `title` (case-insensitive).
+// Used so all Pi-opened tabs collect into one group per window instead of spawning new ones.
+async function findGroupByTitle(windowId, title) {
+ if (!chrome.tabGroups) return null;
+ const wanted = cleanGroupTitle(title).toLowerCase();
+ const groups = await chrome.tabGroups.query({ windowId }).catch(() => []);
+ const match = groups.find((g) => (g.title || "").trim().toLowerCase() === wanted);
+ return match ? match.id : null;
+}
+
+// Add `tab` to a tab group, then set title/color. If the tab is ungrouped, reuse an
+// existing same-title group in its window when present, otherwise create a new group.
+async function groupTab(tab, title, color) {
+ if (!chrome.tabGroups) throw new Error("chrome.tabGroups API unavailable; reload the extension after granting the tabGroups permission");
+ if (!tab || typeof tab.id !== "number") throw new Error("No tab to group");
+ const groupTitle = cleanGroupTitle(title);
+ let groupId = tab.groupId;
+ if (typeof groupId !== "number" || groupId < 0) {
+ const existing = await findGroupByTitle(tab.windowId, groupTitle);
+ groupId = existing !== null
+ ? await chrome.tabs.group({ groupId: existing, tabIds: [tab.id] })
+ : await chrome.tabs.group({ tabIds: [tab.id] });
+ }
+ await chrome.tabGroups.update(groupId, { title: groupTitle, color: cleanGroupColor(color), collapsed: false });
+ const grouped = await chrome.tabs.get(tab.id);
+ return { tab: await formatTab(grouped), group: await groupRecord(groupId) };
+}
+
async function dispatch(action, params) {
switch (action) {
case "tab.version":
@@ -718,17 +773,31 @@ async function dispatch(action, params) {
bridgeUrl: BRIDGE_URL,
userAgent: navigator.userAgent,
};
- case "tab.list":
- return (await chrome.tabs.query({})).map(formatTab);
+ case "tab.list": {
+ const tabs = await chrome.tabs.query({});
+ return Promise.all(tabs.map(formatTab));
+ }
case "tab.new": {
const tab = await chrome.tabs.create({ url: params.url || "about:blank", active: true });
- return formatTab(tab);
+ // Every Pi-opened tab joins a group by default. Pass groupTitle:"" (or group:false) to opt out.
+ const optOut = params.groupTitle === "" || params.group === false;
+ if (optOut && !params.groupColor) return formatTab(tab);
+ return groupTab(tab, params.groupTitle || "Pi", params.groupColor);
}
case "tab.activate": {
const tab = await getTabByParams(params);
await chrome.windows.update(tab.windowId, { focused: true });
return formatTab(await chrome.tabs.update(tab.id, { active: true }));
}
+ case "tab.group": {
+ const tab = await getTabByParams(params);
+ return groupTab(tab, params.groupTitle || "Pi", params.groupColor);
+ }
+ case "tab.ungroup": {
+ const tab = await getTabByParams(params);
+ if (typeof tab.groupId === "number" && tab.groupId >= 0) await chrome.tabs.ungroup(tab.id);
+ return formatTab(await chrome.tabs.get(tab.id));
+ }
case "tab.close": {
const tab = await getTabByParams(params);
await chrome.tabs.remove(tab.id);
@@ -811,7 +880,7 @@ async function dispatch(action, params) {
} finally {
if (params.initScript) await unregisterInitScript(tab.id).catch(() => undefined);
}
- return formatTab(await chrome.tabs.get(updated.id));
+ return await formatTab(await chrome.tabs.get(updated.id));
}
case "page.screenshot":
return takeScreenshot(params);
@@ -820,7 +889,7 @@ async function dispatch(action, params) {
}
}
-function formatTab(tab) {
+async function formatTab(tab) {
return {
id: tab.id,
windowId: tab.windowId,
@@ -831,6 +900,8 @@ function formatTab(tab) {
status: tab.status,
pinned: tab.pinned,
incognito: tab.incognito,
+ groupId: typeof tab.groupId === "number" ? tab.groupId : -1,
+ group: await groupRecord(tab.groupId),
};
}
@@ -1095,7 +1166,7 @@ async function takeScreenshot(params) {
await executeInTab({ ...params, foreground: false }, scrollToY, [tiles.originalScrollY]);
return {
fullPage: true,
- tab: formatTab(tab),
+ tab: await formatTab(tab),
dimensions: { width: tiles.width, height: tiles.height, viewportHeight: tiles.viewportHeight, dpr: tiles.dpr },
tiles: captured,
};
@@ -1104,7 +1175,7 @@ async function takeScreenshot(params) {
format: params.format || "png",
quality: params.format === "jpeg" ? params.quality : undefined,
});
- return { dataUrl, tab: formatTab(tab) };
+ return { dataUrl, tab: await formatTab(tab) };
} finally {
if (previousActiveId !== undefined && previousActiveId !== tab.id) {
await chrome.tabs.update(previousActiveId, { active: true }).catch(() => undefined);
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 21dec6d..7e34887 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -500,7 +500,7 @@ class ChromeProfileBridge {
}
}
-const tabActionValues = ["list", "new", "activate", "close", "version"] as const;
+const tabActionValues = ["list", "new", "activate", "close", "group", "ungroup", "version"] as const;
const imageFormatValues = ["png", "jpeg"] as const;
const waitForValues = ["selector", "expression"] as const;
const CHROME_TOOL_NAMES = [
@@ -1029,20 +1029,25 @@ Usage rules:
pi.registerTool({
name: "chrome_tab",
label: "Chrome Tab",
- description: "List, create, activate, close, or inspect tabs in the user's existing Chrome profile via the companion extension.",
- promptSnippet: "List/open/activate/close existing Chrome tabs through the companion extension.",
+ description: "List, create, activate, close, group, ungroup, or inspect tabs in the user's existing Chrome profile via the companion extension.",
+ promptSnippet: "List/open/activate/close/group existing Chrome tabs through the companion extension.",
parameters: Type.Object({
action: StringEnum(tabActionValues),
url: Type.Optional(Type.String({ description: "URL for action=new." })),
- targetId: Type.Optional(Type.String({ description: "Chrome tab id for activate/close." })),
+ targetId: Type.Optional(Type.String({ description: "Chrome tab id for activate/close/group/ungroup." })),
+ urlIncludes: Type.Optional(Type.String({ description: "Match the target tab by URL substring for activate/close/group/ungroup." })),
+ titleIncludes: Type.Optional(Type.String({ description: "Match the target tab by title substring for activate/close/group/ungroup." })),
+ 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." })),
+ 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." })),
+ 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." })),
host: Type.Optional(Type.String()),
port: Type.Optional(Type.Number()),
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend(`tab.${params.action}`, params, DEFAULT_TIMEOUT_MS, signal);
if (params.action === "list") {
- const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number }>;
- const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
+ const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number; group?: { title?: string } | null }>;
+ 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.";
return { content: [{ type: "text", text }], details: { tabs } };
}
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };