fix: allow closing browser tabs during active requests

Allow users to close leased browser tabs without cancelling the AI request or revoking its other tools. Closed-page operations report a recoverable error; explicit navigation creates a dedicated replacement tab for direct-model and MCP calls.

Add regression coverage and real Electron lifecycle checks, and update browser behavior documentation.

Release note: 浏览器标签页被 AI 请求占用时也可关闭,其他任务继续运行;再次导航会打开独立新页。
This commit is contained in:
mesalogo
2026-09-13 11:21:37 +08:00
parent 032ebc313f
commit b84250f62a
14 changed files with 534 additions and 72 deletions
@@ -6,7 +6,7 @@
| --- | --- |
| 状态 | 基础多实例已实施,Agent 多 Tab 工具待需求 |
| 版本 | 1.0 |
| 日期 | 2026-09-10 |
| 日期 | 2026-09-13 |
| 关联 PRD | [通用助手工作栏与执行空间 PRD](./prd.md) |
| 页签策略 | [助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) |
@@ -86,7 +86,7 @@ Record<ConversationId, Record<BrowserTabId, BrowserLiveState>>
默认或恢复实例可在首次使用时取得当前会话绑定。
Tab 的跨会话可见性与 MCP 授权分开处理。B 的请求不能选择属于 A 的可见 Tab;A 已取得的
capability 在用户切到 B 后仍固定路由至 A 的原 Tab,使用租约也继续阻止关闭该 Tab。
capability 在用户切到 B 后仍固定路由至 A 的原 Tab;使用租约不阻止用户关闭该 Tab。
内部点击实例只同步应用类型,不再次按当前 Conversation 改选实例。Main 状态携带对应的
`workbarInstanceId`,Renderer 据此绑定请求创建的同一 Tab,而不是另建空白页。收到 `stopped`
后清除该实例的旧 Tab 绑定,下一次使用通过原逻辑实例重新创建,保留地址草稿。
@@ -136,11 +136,16 @@ Conversation 归属,不新建租约或把页面重新归属到子级对话。
直连模型上下文和请求级 MCP capability 同时保存 `conversationId` 与
`boundBrowserTabId`。请求执行期间,用户切换、创建或关闭其他工作栏 Tab 不改变工具目标;
模型请求使用某个 Tab 时禁用该 Tab 的工作栏关闭操作,并说明请求仍在执行。用户先取消请求
后才能关闭;系统不得把进行中的操作改投其他 Tab。
用户可直接关闭模型正在使用的 Tab。关闭释放该 Tab 的页面和使用租约,中止其浏览器操作,
不取消整个模型请求、不撤销 MCP capability,也不关闭同级 Tab。系统不得把进行中的操作改投其他 Tab。
关闭后,快照等非导航工具返回“浏览器标签页已关闭,请调用 browser_navigate 打开新标签页”,
不自动创建页面或使用现有 primary/sibling。只有显式且参数有效的 `browser_navigate` 才创建
专用替代 Tab 并绑定后续工具;旧 Tab 身份与元素引用不复用。Gateway 与直连 Provider 均隔离
单次工具取消信号、Tab 使用租约信号和请求信号,取消一次工具调用不终止后续调用。
该阶段 MCP transport、endpoint 和 token 生命周期不变,只扩展服务端 capability 内容和内部
路由。旧工具调用继续使用被冻结的 Tab。
路由。工具调用使用当前请求绑定的 Tab,只有上述显式导航恢复会更换绑定。
### 6.2 Agent 多 Tab 阶段
@@ -189,6 +194,8 @@ primary, including one opened by an Agent, is not adopted by a new instance. New
- 一个 Tab 的元素 `ref` 不能在另一个 Tab 中执行。
- 用户切换工作栏 Tab 不改变进行中模型请求的 MCP 浏览器目标。
- 关闭一个 Tab 不释放 Conversation Runtime,也不关闭同一 Conversation 的其他 Tab。
- 关闭已租用 Tab 后,同一 MCP 会话仍可列出工具;旧目标快照报错且不创建页面,显式导航创建
不同身份的替代 Tab,同级页面、导航状态与其他 Conversation 的可见 viewport 保持不变。
- 旧版无 `tabId` 的模型工具调用仍操作请求绑定的默认 Tab。
## 10. 实施顺序
+4
View File
@@ -483,6 +483,10 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
改变绑定到同一 Tab 的 Agent 后续操作。所有请求都进入 `BrowserService`;Renderer 不建立
第二套导航队列、抢占规则或恢复状态机。
用户可关闭 Agent 正在使用的标签页,无需先取消整轮请求。关闭只中止该页的浏览器操作,
其他标签页和请求的其他工具继续可用。后续快照等操作应提示标签页已关闭;Agent 显式导航
时打开专用新标签页并继续操作,不能自动接管用户的其他页面。
浏览器工具栏读取的权威状态至少包含:
```ts
+28 -3
View File
@@ -1,5 +1,30 @@
# 工作栏实现与验证进度
## 2026-09-13 浏览器租用标签页关闭与显式恢复
- 更新 `tests/browser-tabs-electron-e2e.ts`,移除两处租用期间拒绝关闭的旧断言。
当前行为见 [Agent 与 MCP 路由](./browser-tabs-technical-design.md#6-agent-与-mcp-路由)
和 [浏览器需求](./prd.md#77-浏览器)。同步修改 BrowserService、IPC、Gateway 和直连模型工具。
- Windows 真实 Electron/Chromium 与 loopback HTTP/MCP 验证通过:关闭已物化的预留页和
已租用主标签页,等待原 WebContents 的 `destroyed` 事件,确认租约结束而请求信号未取消。
同一 token 和 MCP session 仍可列出工具;旧目标快照返回关闭提示且不创建页面。
显式 `browser_navigate` 创建不同身份的专用替代页,后续 MCP 快照读取该页。
- 同 Conversation 的 sibling 仅在主标签页关闭后取得 `primary` 标记,页面 URL、导航状态、
时间戳、工作栏身份和 Cookie 内容保持不变;另一 Conversation 的页面与可见 viewport
保持不变。撤销 capability 后完成替代页、同级页和 Context 清理。
- 已先检查 runner:esbuild 输出临时 CJS,启动真实 Electron,子进程上限 120 秒,结束后
删除临时 bundle。`node build/run-browser-tabs-electron-e2e.cjs` 最终退出码 0;
前三轮分别修正测试对销毁事件时序、服务旧 ID 错误文案和 sibling 主标签标记的假设。
测试文件 ESLint 与限定文件的 `git diff --check` 通过。
- 覆盖边界:本轮通过 Main BrowserService 和真实 MCP transport 操作页面,未点击完整
Renderer/Preload 关闭入口,未注入执行中的工具取消,也未运行直连模型循环或其他共享工具。
浏览器、Gateway、直连 Provider、Model Runtime 和 IPC 回归共 359 项通过、1 项跳过,
包括执行中关闭、请求继续完成和其他工具继续可用。全库 typecheck、lint 和 diff 检查通过。
全量 `npm test` 在 Agent 离线依赖安装用例失败后达到 200 秒执行上限;无外部模型调用。
- 本轮未修改 Agent、桥接或 Runtime 产品源码,也未连接远程 Host。远程调用若进入同一桌面
Gateway,会使用上述服务端路由;远端 Runtime 到桌面的实际传输、取消传播及恢复仍需真实
Host 验证,本记录不将本机 loopback 结果算作远程覆盖。
## 2026-09-12 浏览器刷新与关闭后的错误清理
- 按 [浏览器工具栏规则](./prd.md#77-浏览器) 修复空白标签页允许刷新的问题;
@@ -83,7 +108,7 @@
132 项通过。随后新增的 Ask 指令、继承 Runtime 用量持久化及计划模式 App 场景,3 项通过。
- 扩展真实 Electron/MCP E2E 在独立临时 profile 通过:四个未使用的请求预留不创建会话、
WebContents 或 UI 事件;首次 MCP 导航物化相同 Tab/工作栏身份,恢复不额外创建页面,
并验证独立页面、共享 Cookie、请求租约关闭保护和清理。
并验证独立页面、共享 Cookie、当时的请求租约关闭保护和清理;关闭保护已于 2026-09-13 移除。
- 当前源码的共享 Linux x64 Host 验证已通过,覆盖工作区目标保护和重命名/Git Diff,
并通过桌面托管 ACP → 隔离 Agent → OpenCode 完成 Ask/Execute 和继承 Runtime 指标持久化;
该组共 3 次真实模型调用,清理后无所属进程残留。完整记录见
@@ -120,13 +145,13 @@
- Renderer 使用工作栏实例 UUID 原子创建或恢复 Browser Tab,并以 UUID viewport token 防止
延迟 cleanup 隐藏新活动 Tab。关闭单个实例只释放对应 Tab。
- 直连模型和请求级 MCP capability 在请求开始时优先绑定可见 Tab,其次绑定 primary Tab,
并持有使用租约。请求结束或 capability 撤销前,绑定 Tab 拒绝关闭;工具参数未增加模型可见
并持有使用租约。当时绑定 Tab 拒绝关闭,该行为已于 2026-09-13 改为允许关闭和显式导航恢复;工具参数未增加模型可见
`tabId`,已有浏览器工具 schema 保持不变。
- 聚焦回归命令覆盖 Shared、BrowserService、Electron Session、IPC、Preload、MCP、Renderer
和 Workbar,13 个测试文件共 453 项通过;`npm run lint` 与 Web TypeScript 检查通过。
- `node build/run-browser-tabs-electron-e2e.cjs` 通过。该测试启动真实 Electron/Chromium 和本地
HTTP 页面,创建两个 `WebContentsView`,验证页面隔离、同 Conversation Cookie 共享、真实
loopback MCP 的固定 Tab 导航与快照、使用租约关闭保护,以及单 Tab 和最终 Context 清理。
loopback MCP 的固定 Tab 导航与快照、当时的使用租约关闭保护,以及单 Tab 和最终 Context 清理。
- `npm run typecheck`、`npm run lint`、`npm run build:bundle` 和 `git diff --check` 通过。
- 本轮整库 `npm test` 为 3686 项通过、61 项跳过、4 项失败。失败位于 portable/release 的
ASAR 元数据 fixture、Runtime 版本探测和 DeepSeek Harness 的本地 MCP fixture;定向复跑仍