aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-06-22 19:55:46 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-06-22 20:05:59 -0400
commitc1ed934dd20931b177e233ae0327a290a8001631 (patch)
tree384d7747031249f2f09fcfbbd5062ed175d435d0 /extensions
parentb8165967408be47e93b6a566270988177f8f6866 (diff)
Keep Pi-created tabs in one session group
Diffstat (limited to 'extensions')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js32
-rw-r--r--extensions/chrome-profile-bridge/index.ts27
2 files changed, 42 insertions, 17 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index c393090..2b5f96f 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -1073,8 +1073,10 @@ async function groupRecord(groupId) {
};
}
-// 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.
+// Find existing tab groups whose title matches `title` (case-insensitive).
+// Same-window lookup is used when grouping an already-created tab. Any-window lookup is used before
+// creating a new Pi tab so one Pi session keeps one tab group and new tabs are created in that
+// group's window (Chrome tab groups cannot span windows).
async function findGroupByTitle(windowId, title) {
if (!chrome.tabGroups) return null;
const wanted = cleanGroupTitle(title).toLowerCase();
@@ -1083,6 +1085,13 @@ async function findGroupByTitle(windowId, title) {
return match ? match.id : null;
}
+async function findGroupRecordByTitle(title) {
+ if (!chrome.tabGroups) return null;
+ const wanted = cleanGroupTitle(title).toLowerCase();
+ const groups = await chrome.tabGroups.query({}).catch(() => []);
+ return groups.find((g) => (g.title || "").trim().toLowerCase() === wanted) || 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) {
@@ -1115,11 +1124,20 @@ async function dispatch(action, params) {
return Promise.all(tabs.map(formatTab));
}
case "tab.new": {
- const tab = await chrome.tabs.create({ url: params.url || "about:blank", active: true });
- // 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);
+ // Every Pi-opened tab must join a tab group. There is intentionally no opt-out: an ungrouped
+ // Pi-created tab is easy to lose among user tabs. If grouping fails after creation, close the
+ // tab best-effort before surfacing the error so tab.new never leaves an ungrouped Pi tab.
+ const groupTitle = params.groupTitle || "Pi";
+ const existingGroup = await findGroupRecordByTitle(groupTitle);
+ const createParams = { url: params.url || "about:blank", active: true };
+ if (existingGroup && typeof existingGroup.windowId === "number") createParams.windowId = existingGroup.windowId;
+ const tab = await chrome.tabs.create(createParams);
+ try {
+ return await groupTab(tab, groupTitle, params.groupColor);
+ } catch (error) {
+ if (typeof tab.id === "number") await chrome.tabs.remove(tab.id).catch(() => {});
+ throw error;
+ }
}
case "tab.activate": {
// Management actions never auto-create an automation target (createOwnedTarget:false): with
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 7690f25..e626bef 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -844,13 +844,20 @@ export default function (pi: ExtensionAPI): void {
let wireParams: Record<string, unknown> = sessionKey !== undefined && params.sessionKey === undefined
? { ...params, sessionKey }
: params;
+ const sessionTitle = sessionCtx !== undefined ? sessionGroupTitle(sessionCtx) : undefined;
+ // Any tab Pi opens through tab.new/tab.group must use THIS session's group, even if a caller
+ // passes group:false or a custom groupTitle. This central guard covers chrome_tab plus internal
+ // callers such as chrome_launch(url).
+ if ((action === "tab.new" || action === "tab.group") && sessionTitle !== undefined) {
+ wireParams = { ...wireParams, groupTitle: sessionTitle };
+ }
// Any tab Pi *uses* (page.* interactions) should join this session's group, mirroring the
// auto-grouping that tab.new already does. Tagging the wire params lets getTabByParams pull
// the resolved tab into the session group on the service-worker side. We skip tab.* actions:
- // tab.new/group group explicitly, and activate/close/ungroup/list must not.
- const shouldJoinGroup = action.startsWith("page.") && sessionCtx !== undefined && params.sessionGroupTitle === undefined;
+ // tab.new/group are forced above, and activate/close/ungroup/list must not group tabs.
+ const shouldJoinGroup = action.startsWith("page.") && sessionTitle !== undefined && params.sessionGroupTitle === undefined;
if (shouldJoinGroup) {
- wireParams = { ...wireParams, sessionGroupTitle: sessionGroupTitle(sessionCtx as ExtensionContext), joinSessionGroup: true };
+ wireParams = { ...wireParams, sessionGroupTitle: sessionTitle, joinSessionGroup: true };
}
return bridge.send(action, wireParams, timeoutMs, signal);
};
@@ -1279,7 +1286,7 @@ Usage rules:
pi.registerTool({
name: "chrome_tab",
label: "Chrome Tab",
- description: "List, create, activate, close, group, ungroup, or inspect tabs in the user's existing Chrome profile via the companion extension. activate/close/group/ungroup require a target (targetId/urlIncludes/titleIncludes); with no target they act on this session's pi-chrome automation tab if one exists, and otherwise error rather than touching the user's active tab.",
+ description: "List, create, activate, close, group, ungroup, or inspect tabs in the user's existing Chrome profile via the companion extension. New/grouped tabs always use this session's Pi tab group. activate/close/group/ungroup require a target (targetId/urlIncludes/titleIncludes); with no target they act on this session's pi-chrome automation tab if one exists, and otherwise error rather than touching the user's active tab.",
promptSnippet: "List/open/activate/close/group existing Chrome tabs through the companion extension.",
parameters: Type.Object({
action: StringEnum(tabActionValues),
@@ -1287,18 +1294,18 @@ Usage rules:
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 this session's own tab group." })),
- groupTitle: Type.Optional(Type.String({ description: "Tab group title for action=group/new. Defaults to this Pi session's group ('Pi Session: <name-or-id>'). Pass an empty string on action=new to opt out of grouping." })),
+ group: Type.Optional(Type.Boolean({ description: "Deprecated; ignored. Pi-created tabs always join this session's own tab group." })),
+ groupTitle: Type.Optional(Type.String({ description: "Deprecated for action=new/group; ignored so one Pi session uses one tab group ('Pi Session: <name-or-id>')." })),
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, _onUpdate, ctx): Promise<ToolTextResult> {
const forwarded = { ...params } as typeof params & { groupTitle?: string };
- // Default every Pi-opened/explicitly-grouped tab into this session's own group,
- // named after the session display name (falling back to the session id), unless
- // the caller specified a group title or opted out with group:false.
- if ((params.action === "new" || params.action === "group") && params.groupTitle === undefined && params.group !== false) {
+ // Force every Pi-opened/explicitly-grouped tab into this session's own group,
+ // named after the session display name (falling back to the session id). There is
+ // intentionally no opt-out: one Pi session should create/use one tab group.
+ if (params.action === "new" || params.action === "group") {
forwarded.groupTitle = sessionGroupTitle(ctx);
}
const result = await authorizedBridgeSend(`tab.${params.action}`, forwarded, DEFAULT_TIMEOUT_MS, signal);