From b8b63ea4d1bad9c4bbac6b05b03cce5f2cfa3f94 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Sun, 8 Nov 2026 17:11:15 +0000 Subject: tabs: back to one window per session; keep new tabs out of the user's window A window shows only one tab, so sessions sharing one hid each other's tabs: no real input and blank screenshots for every session but the one whose tab was active (castle-pirc-web, 2026-11-08). Time-sharing the active tab would flip tabs under the user while they sign in for an agent, so revert 66fbd30: a session gets its own unfocused window. Kept: chrome_tab new for a session with no window opens one for it instead of landing in the user's focused window. The primer and chrome_tab description now say why to reuse a tab. --- test-suite/unit/background-policy.test.mjs | 12 ++++++-- test-suite/unit/shared-window.test.mjs | 45 ++++++++++-------------------- 2 files changed, 24 insertions(+), 33 deletions(-) (limited to 'test-suite') diff --git a/test-suite/unit/background-policy.test.mjs b/test-suite/unit/background-policy.test.mjs index efbca78..ea08be0 100644 --- a/test-suite/unit/background-policy.test.mjs +++ b/test-suite/unit/background-policy.test.mjs @@ -297,6 +297,14 @@ function workerHarness({ withWindows = true } = {}) { }; return h; } +// A new Pi tab must not take over the user's window: it is either inactive, or the only tab of a +// new, unfocused window of the session's own (so it is visible to the agent without stealing focus). +function assertNewTabStaysOutOfTheWay(h, tab) { + if (tab.active === false) return; + assert.notEqual(tab.windowId, 1, "an active new tab must not be in the user's window"); + assert.ok(h.calls.some((c) => c.action === "windows.create" && c.params.focused === false), "its window was created unfocused"); +} + function assertNoFocus(h) { assert.equal(h.focusedWindow(), 1); assert.equal(h.tabs.get(1).active, true); @@ -325,7 +333,7 @@ test("worker advertises support, keeps new tabs inactive, and rejects activation assert.equal((await h.w.dispatch("tab.version", {})).capabilities.hardBackground, true); for (const flags of [{}, { foreground: false }, { background: true, foreground: true }]) { const result = await h.w.dispatch("tab.new", { ...flags, sessionKey: "alpha", groupTitle: "Pi Session: alpha" }); - assert.equal(result.tab.active, false); + assertNewTabStaysOutOfTheWay(h, result.tab); await assert.rejects(h.w.dispatch("tab.activate", { ...flags, targetId: "missing" }), /background mode/); } assertNoFocus(h); @@ -339,7 +347,7 @@ test("worker advertises support, keeps new tabs inactive, and rejects activation test("background-only worker aliases force safe behavior even with foreground flags", async () => { const h = workerHarness(); const opened = await h.w.dispatch("tab.new.background", { foreground: true, background: false }); - assert.equal(opened.tab.active, false); + assertNewTabStaysOutOfTheWay(h, opened.tab); const shot = await h.w.dispatch("page.screenshot.background", { targetId: "2", foreground: true, background: false }); assert.equal(shot.method, "cdp"); assertNoFocus(h); diff --git a/test-suite/unit/shared-window.test.mjs b/test-suite/unit/shared-window.test.mjs index 7afeb77..66041f4 100644 --- a/test-suite/unit/shared-window.test.mjs +++ b/test-suite/unit/shared-window.test.mjs @@ -1,5 +1,5 @@ -// Agent sessions share one window: a new session's tab goes into the window that already holds a -// "Pi Session:" group, and a window is created only when there is none. +// Each session has its own window: a window shows only one tab, so sessions sharing one hid each +// other's tabs (no real input, blank screenshots). A session's extra tabs go into its own window. import assert from "node:assert/strict"; import fs from "node:fs"; import vm from "node:vm"; @@ -35,44 +35,27 @@ function worker({ groups = [], windows = { 5: { id: 5, type: "normal" } } } = {} } const piGroup = (windowId, name = "Pi Session: claude-code 1 other") => ({ id: 1, windowId, title: name }); +const ops = (calls) => calls.filter((c) => c.method !== "storage.set").map((c) => c.method); -test("a new session's tab goes into the window that already holds another Pi session", async () => { +test("a new session gets its own window even when another session's window exists", async () => { const { w, calls } = worker({ groups: [piGroup(5)] }); await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine"); - assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["tabs.create"]); - assert.equal(calls[0].props.windowId, 5); + assert.deepEqual(ops(calls), ["windows.create"]); }); -test("with no Pi window yet, the first session creates one", async () => { - const { w, calls } = worker({ groups: [] }); +test("a session that already has a group adds its tab to that group's window", async () => { + const { w, calls } = worker({ groups: [piGroup(5, "Pi Session: claude-code 2 mine")] }); await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine"); - assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["windows.create"]); + assert.deepEqual(ops(calls), ["tabs.create"]); + assert.equal(calls.find((c) => c.method === "tabs.create").props.windowId, 5); }); -test("groups that are not Pi sessions do not attract agent tabs", async () => { - const { w, calls } = worker({ groups: [{ id: 2, windowId: 5, title: "Work" }] }); - await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine"); - assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["windows.create"]); -}); - -test("a Pi group whose window is gone, or is not a normal window, is skipped", async () => { - const { w, calls } = worker({ groups: [piGroup(7), piGroup(8)], windows: { 8: { id: 8, type: "popup" } } }); - await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine"); - assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["windows.create"]); -}); - -test("the shared window is never recorded as ours, so cleanup cannot close it", async () => { - const { w, calls } = worker({ groups: [piGroup(5)] }); - await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine"); - const stored = calls.filter((c) => c.method === "storage.set").map((c) => c.json).join(""); - assert.doesNotMatch(stored, /"windowId":5/); -}); - -test("chrome_tab new for a session with no group uses the shared window, not the focused one", async () => { +test("chrome_tab new for a session with no group goes to the session's own window, not the focused one", async () => { const { w, calls } = worker({ groups: [piGroup(5)] }); w.groupTab = async (tab) => tab; w.bringToFront = async () => {}; - await w.dispatch("tab.new", { url: "about:blank", groupTitle: "Pi Session: claude-code 2 mine", sessionKey: "claude-code 2 mine" }); - const created = calls.find((c) => c.method === "tabs.create"); - assert.equal(created.props.windowId, 5); + await w.dispatch("tab.new", { url: "https://x.test/", groupTitle: "Pi Session: claude-code 2 mine", sessionKey: "claude-code 2 mine" }); + assert.deepEqual(ops(calls), ["windows.create"]); + assert.equal(calls[0].props.url, "https://x.test/"); + assert.equal(calls[0].props.focused, false); }); -- cgit v1.2.3