diff options
| author | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-06-22 19:55:46 -0400 |
|---|---|---|
| committer | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-06-22 20:05:59 -0400 |
| commit | c1ed934dd20931b177e233ae0327a290a8001631 (patch) | |
| tree | 384d7747031249f2f09fcfbbd5062ed175d435d0 /extensions | |
| parent | b8165967408be47e93b6a566270988177f8f6866 (diff) | |
Keep Pi-created tabs in one session group
Diffstat (limited to 'extensions')
| -rw-r--r-- | extensions/chrome-profile-bridge/browser-extension/service_worker.js | 32 | ||||
| -rw-r--r-- | extensions/chrome-profile-bridge/index.ts | 27 |
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); |
