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
+88
View File
@@ -7739,6 +7739,94 @@ describe('registerIpcHandlers agent terminal state', () => {
await harness.dispose()
})
it('completes the runtime request and releases its lease after browser lease abort', async () => {
const requestId = '00000000-0000-4000-8000-000000000031'
const tabId = browserTabIdSchema.parse(
'00000000-0000-4000-8000-000000000423'
)
const leaseController = new AbortController()
const release = vi.fn()
const lease = {
conversationId: 'browser-lease-abort',
tabId,
owner: requestId,
signal: leaseController.signal,
release
}
let runtimeSignal: AbortSignal | undefined
const runtime = {
runtimeId: 'model',
capability: 'chat',
supportsToolExecution: true,
async *run(request: { requestId: string }, signal: AbortSignal) {
runtimeSignal = signal
leaseController.abort(new Error('browser tab closed'))
signal.throwIfAborted()
yield { requestId: request.requestId, type: 'done' }
}
}
let grantedLease: { release(): void } | undefined
const knowledgeGateway = {
grant: vi.fn((...args: unknown[]) => {
grantedLease = args[7] as { release(): void }
return 'browser-capability'
}),
getAvailableToolNames: vi.fn(() => ['browser_navigate']),
drainReferences: vi.fn(() => []),
revoke: vi.fn(() => grantedLease?.release())
}
const browserControl = {
listTabs: vi.fn(() => [{ tabId, primary: true }]),
getVisibleTabId: vi.fn(() => tabId),
acquireTabUsage: vi.fn(() => lease),
createTab: vi.fn(),
onState: vi.fn(() => () => undefined)
}
const harness = createHarness(
runtime,
undefined,
'always',
undefined,
false,
undefined,
undefined,
knowledgeGateway,
false,
undefined,
{ getEnabledBuiltinMcpServerIds: vi.fn(async () => ['builtin-browser']) },
browserControl
)
try {
await harness.handler?.(trustedEvent(harness.webContents), {
requestId,
conversationId: lease.conversationId,
prompt: 'Continue after the browser tab closes',
workMode: 'execute',
knowledgeLibraryIds: []
})
await vi.waitFor(() => expect(release).toHaveBeenCalledOnce())
expect(grantedLease).toBe(lease)
expect(lease.signal.aborted).toBe(true)
expect(runtimeSignal?.aborted).toBe(false)
expect(harness.assistantDatabase.updateTaskStatus).toHaveBeenCalledWith(
requestId,
'completed'
)
const publicEvents = harness.webContents.send.mock.calls
.filter(([channel]) => channel === ipcChannels.agentEvent)
.map(([, payload]) => payload)
expect(publicEvents).toContainEqual({ requestId, type: 'done' })
expect(publicEvents).not.toEqual(expect.arrayContaining([
expect.objectContaining({ type: 'error' })
]))
expect(knowledgeGateway.revoke).toHaveBeenCalledWith('browser-capability')
} finally {
await harness.dispose()
}
})
it.each(['error', 'cancel'] as const)(
'releases a request-created browser tab lease on runtime %s',
async (outcome) => {