fix(browser): preserve chat focus during agent browsing

This commit is contained in:
2026-09-27 15:23:23 +08:00
parent 220e89992b
commit a52e747a00
10 changed files with 323 additions and 16 deletions

View File

@@ -20,7 +20,7 @@ function isAllowedTopLevelUrl(value: string): boolean {
}
}
function wrapDebugger(contents: WebContents): AgentBrowserDebuggerPort {
function wrapDebugger(contents: WebContents, isVisible: () => boolean): AgentBrowserDebuggerPort {
const debuggerEvents = contents.debugger as unknown as {
on(event: string, listener: PortListener): void;
removeListener(event: string, listener: PortListener): void;
@@ -29,8 +29,14 @@ function wrapDebugger(contents: WebContents): AgentBrowserDebuggerPort {
attach: (version) => contents.debugger.attach(version),
detach: () => contents.debugger.detach(),
isAttached: () => contents.debugger.isAttached(),
sendCommand: (method, params, sessionRef) =>
contents.debugger.sendCommand(method, params, sessionRef),
sendCommand: async (method, params, sessionRef) => {
if (method === 'Page.captureScreenshot' && !isVisible()) {
// CDP capture can wait forever for a hidden view's compositor. Electron's
// native capture requests a frame without showing or focusing the view.
await contents.capturePage(undefined, { stayHidden: true, stayAwake: false });
}
return contents.debugger.sendCommand(method, params, sessionRef);
},
on: (event, listener) => debuggerEvents.on(event, listener),
removeListener: (event, listener) =>
debuggerEvents.removeListener(event, listener),
@@ -53,13 +59,13 @@ function wrapNavigation(contents: WebContents): AgentBrowserNavigationPort {
};
}
function wrapWebContents(contents: WebContents): AgentBrowserWebContentsPort {
function wrapWebContents(contents: WebContents, isVisible: () => boolean): AgentBrowserWebContentsPort {
const contentsEvents = contents as unknown as {
on(event: string, listener: PortListener): void;
removeListener(event: string, listener: PortListener): void;
};
return {
debugger: wrapDebugger(contents),
debugger: wrapDebugger(contents, isVisible),
navigationHistory: wrapNavigation(contents),
loadURL: (url) => contents.loadURL(url),
getURL: () => contents.getURL(),
@@ -111,7 +117,7 @@ export class ElectronAgentBrowserAdapter implements AgentBrowserAdapter {
this.guardSession(contents.session);
const view: AgentBrowserViewPort = {
webContents: wrapWebContents(contents),
webContents: wrapWebContents(contents, () => nativeView.getVisible()),
setBounds: (bounds: AgentBrowserBounds) =>
nativeView.setBounds(this.toNativeBounds(bounds)),
setVisible: (visible: boolean) => nativeView.setVisible(visible),

View File

@@ -615,7 +615,7 @@ export class AgentBrowserModule {
}
async readEvents(input: AgentBrowserReadEventsInput): Promise<AgentBrowserCdpEventPage> {
let record = this.requirePresented(input.projectPath);
let record = this.requireViewport(input.projectPath);
const after = normalizeNonNegativeInteger(input.after, 0, 'after');
const limit = normalizeIntegerRange(input.limit, 100, 1, 200, 'limit');
const waitMs = normalizeIntegerRange(input.waitMs, 0, 0, MAX_WAIT_MS, 'waitMs');
@@ -645,13 +645,13 @@ export class AgentBrowserModule {
|| current !== record
|| current.eventBuffer.cursor !== cursorBeforeWait;
});
record = this.requirePresented(input.projectPath);
record = this.requireViewport(input.projectPath);
page = record.eventBuffer.read(page.nextCursor, input.methods, limit);
return page;
}
async readPayload(input: AgentBrowserReadPayloadInput): Promise<AgentBrowserPayloadChunk> {
this.requirePresented(input.projectPath);
this.requireViewport(input.projectPath);
return this.payloadStore.read(input.handle, input.offset, input.maxBytes);
}
@@ -1589,7 +1589,7 @@ export class AgentBrowserModule {
}
private requireAttached(projectPath: string): BrowserRecord {
const record = this.requirePresented(projectPath);
const record = this.requireViewport(projectPath);
if (record.state === 'suspended_devtools') {
throw new AgentBrowserFault(
'DEVTOOLS_CONFLICT',
@@ -1617,12 +1617,14 @@ export class AgentBrowserModule {
return record;
}
private requirePresented(projectPath: string): BrowserRecord {
private requireViewport(projectPath: string): BrowserRecord {
const record = this.requireRecord(projectPath);
if (!record.visible || !record.bounds) {
// Switching to chat hides the native view without ending its debugging session.
// A new view still needs its first real viewport before tools can operate.
if (!record.bounds) {
throw new AgentBrowserFault(
'VIEWPORT_NOT_READY',
'开发浏览器已收起,智能体调试已暂停。',
'开发浏览器显示区域尚未准备好。',
true,
record.generation,
);