mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(extension): keep user-opened tabs free & fix agent-tab leak on session stop (issue #57)
- session_stop: exclude tabs still tracked in agentCreatedTabs from the release decision so a tab that failed to close is no longer mistaken for a user tab (which previously kept the window open and leaked the tab). - Distinguish user-created tabs from agent-created tabs in the Agent Window via a pending-tab counter consumed by chrome.tabs.onCreated, so tabs the user opens themselves (new-tab button / Cmd+T / open in new tab) are never taken over by the BrowserSkill control mask. - background overlay: decide per-tab state BEFORE showing controls (overlayStateForTab + pushOverlayStateForTab) so a user tab receives hidden on its first overlay.ready ping instead of flashing control then hiding. - AgentWindowApi.ensureActiveTab now resolves with the activated/created tab id instead of void, so SessionManager can record homeTabId. This fixes the tsc failure flagged in review (the interface previously returned Promise<void> while callers treated the result as number | null). - Wire the production tabManagement.tabs / tabsQuery deps into tool.session_stop, both in the dispatcher and in the disconnect cleanup. They were never injected, which left the agent-tab cleanup and the window-release path as dead code outside tests (behaviour identical to main). Added a dispatcher-level integration test that fails without the wiring: a surviving user tab must release the window, not close it.
This commit is contained in:
@@ -6,6 +6,38 @@ export interface SessionContext {
|
||||
agentWindowId: number;
|
||||
refStore: RefStore;
|
||||
borrowedTabs: Map<number, BorrowedTab>;
|
||||
/**
|
||||
* Tabs created by the agent via `tool.tab_create` in this session's
|
||||
* Agent Window. Tracked so `session_stop` can close them before
|
||||
* releasing the window (design §3.1). User-created tabs (via Chrome UI)
|
||||
* never enter this set.
|
||||
*/
|
||||
agentCreatedTabs: Set<number>;
|
||||
/**
|
||||
* Tabs the user opened themselves inside the Agent Window via Chrome UI
|
||||
* (new-tab button, Cmd+T, right-click → open in new tab, …). These must
|
||||
* NOT be controlled by the agent overlay and must be left free for the
|
||||
* user to operate. Distinguishing them from agent-created tabs is done
|
||||
* by the `chrome.tabs.onCreated` listener in background.ts, which consults
|
||||
* `pendingAgentTabCount` to tell "opened via tool.tab_create" apart from
|
||||
* "opened by the user".
|
||||
*/
|
||||
userTabs: Set<number>;
|
||||
/**
|
||||
* Number of agent tabs currently being created via `tool.tab_create` but
|
||||
* whose `onCreated` event has not yet been observed. `handleTabCreate`
|
||||
* increments this *before* calling `chrome.tabs.create`; the
|
||||
* `onCreated` listener decrements it once per new tab in the Agent Window.
|
||||
* This lets us tell agent-created tabs from user-created tabs even though
|
||||
* both fire `onCreated` (design §3.1, user-tab freedom fix).
|
||||
*/
|
||||
pendingAgentTabCount: number;
|
||||
/**
|
||||
* Id of the home tab created/activated when the session started
|
||||
* (`ensureActiveTab`). Used to clean up the home tab precisely on
|
||||
* stop instead of matching by URL (design §3.2).
|
||||
*/
|
||||
homeTabId: number | null;
|
||||
createdAtMs: number;
|
||||
}
|
||||
|
||||
@@ -81,6 +113,13 @@ export class SessionManager {
|
||||
private readonly sessions = new Map<string, SessionContext>();
|
||||
private readonly windowIndex = new Map<number, string>();
|
||||
private readonly borrowReservations = new Map<number, string>();
|
||||
/**
|
||||
* Windows that are still being initialised by `start()` (the home tab is
|
||||
* being created). Tabs born into one of these windows via `onCreated`
|
||||
* belong to the agent (the home tab), not the user, and must not be
|
||||
* mistaken for user-created tabs.
|
||||
*/
|
||||
private readonly windowInitializing = new Set<number>();
|
||||
private readonly agentWindow: AgentWindowApi;
|
||||
private readonly now: () => number;
|
||||
|
||||
@@ -179,7 +218,10 @@ export class SessionManager {
|
||||
const { signal: _signal, ...createOptions } = opts;
|
||||
windowId = await this.agentWindow.create(AGENT_WINDOW_HOME, createOptions);
|
||||
throwIfSessionStartAborted(opts.signal);
|
||||
await this.agentWindow.ensureActiveTab(windowId, AGENT_WINDOW_HOME);
|
||||
// Mark the window as initialising so `onCreated` (driven by the home
|
||||
// tab) does not mistake the home tab for a user-created tab.
|
||||
this.windowInitializing.add(windowId);
|
||||
const homeTabId = await this.agentWindow.ensureActiveTab(windowId, AGENT_WINDOW_HOME);
|
||||
throwIfSessionStartAborted(opts.signal);
|
||||
|
||||
const ctx: SessionContext = {
|
||||
@@ -187,13 +229,19 @@ export class SessionManager {
|
||||
agentWindowId: windowId,
|
||||
refStore: new RefStore(),
|
||||
borrowedTabs: new Map(),
|
||||
agentCreatedTabs: new Set(),
|
||||
userTabs: new Set(),
|
||||
pendingAgentTabCount: 0,
|
||||
homeTabId,
|
||||
createdAtMs: this.now(),
|
||||
};
|
||||
this.sessions.set(sessionId, ctx);
|
||||
this.windowIndex.set(windowId, sessionId);
|
||||
this.windowInitializing.delete(windowId);
|
||||
return ctx;
|
||||
} catch (startupError) {
|
||||
if (windowId !== null) {
|
||||
this.windowInitializing.delete(windowId);
|
||||
try {
|
||||
await this.agentWindow.remove(windowId);
|
||||
} catch (cleanupError) {
|
||||
@@ -204,6 +252,40 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that a `tool.tab_create` is about to open a tab. Called *before*
|
||||
* `chrome.tabs.create` so the pending count is visible to the
|
||||
* `onCreated` listener that will fire for the new tab.
|
||||
*/
|
||||
markAgentTabPending(windowId: number): void {
|
||||
const ctx = this.findByWindowId(windowId);
|
||||
if (!ctx) return;
|
||||
ctx.pendingAgentTabCount += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the `onCreated` listener for each new tab in an Agent Window.
|
||||
* Returns `"agent"` when the tab corresponds to a pending `tool.tab_create`
|
||||
* (and registers it), `"user"` when the user opened it via Chrome UI, or
|
||||
* `"initializing"` when the window is still booting its home tab.
|
||||
*/
|
||||
classifyNewTab(tabId: number, windowId: number): "agent" | "user" | "initializing" | "unknown" {
|
||||
const ctx = this.findByWindowId(windowId);
|
||||
if (!ctx) return "unknown";
|
||||
if (this.windowInitializing.has(windowId)) {
|
||||
// Home tab / window boot — belongs to the agent, not the user.
|
||||
ctx.agentCreatedTabs.add(tabId);
|
||||
return "initializing";
|
||||
}
|
||||
if (ctx.pendingAgentTabCount > 0) {
|
||||
ctx.pendingAgentTabCount -= 1;
|
||||
ctx.agentCreatedTabs.add(tabId);
|
||||
return "agent";
|
||||
}
|
||||
ctx.userTabs.add(tabId);
|
||||
return "user";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down a session: close its Agent Window and drop the context.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user