diff --git a/src/main/browser/agent-browser-bridge-test-harness.ts b/src/main/browser/agent-browser-bridge-test-harness.ts index 5a861f3f552..c4c28006e82 100644 --- a/src/main/browser/agent-browser-bridge-test-harness.ts +++ b/src/main/browser/agent-browser-bridge-test-harness.ts @@ -32,7 +32,6 @@ export function mockBrowserManager( getBrowserPageLoadError: vi.fn(() => null), getBrowserPageCertificateFailure: vi.fn(() => null), unregisterGuest: vi.fn(), - ensureWebviewVisible: vi.fn(async () => () => {}), ...overrides } as unknown as BrowserManager } diff --git a/src/main/browser/browser-manager-guest-visibility.test.ts b/src/main/browser/browser-manager-guest-visibility.test.ts index 17e90143a44..6b5b80ed127 100644 --- a/src/main/browser/browser-manager-guest-visibility.test.ts +++ b/src/main/browser/browser-manager-guest-visibility.test.ts @@ -52,8 +52,7 @@ const { guestSetBackgroundThrottlingMock, guestSetWindowOpenHandlerMock, guestOpenDevToolsMock, - webContentsFromIdMock, - browserWindowFromWebContentsMock + webContentsFromIdMock } = browserMocks describe('browserManager', () => { @@ -66,69 +65,6 @@ describe('browserManager', () => { vi.useRealTimers() }) - it('activates the owning browser workspace when ensuring a page-backed guest is visible', async () => { - const rendererExecuteJavaScriptMock = vi - .fn() - .mockResolvedValueOnce({ - prevTabType: 'terminal', - prevActiveWorktreeId: 'wt-1', - prevActiveBrowserWorkspaceId: 'workspace-prev', - prevActiveBrowserPageId: 'page-prev', - prevFocusedGroupTabId: 'tab-prev', - targetWorktreeId: 'wt-1', - targetBrowserWorkspaceId: 'workspace-1', - targetBrowserPageId: 'page-1' - }) - .mockResolvedValueOnce(undefined) - const guest = { - id: 707, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock - } - const renderer = { - id: rendererWebContentsId, - isDestroyed: vi.fn(() => false), - executeJavaScript: rendererExecuteJavaScriptMock - } - browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) }) - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === rendererWebContentsId) { - return renderer - } - return null - }) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'page-1', - workspaceId: 'workspace-1', - worktreeId: 'wt-1', - webContentsId: guest.id, - rendererWebContentsId - }) - - const restore = await browserManager.ensureWebviewVisible(guest.id) - - const activationScript = rendererExecuteJavaScriptMock.mock.calls[0]?.[0] - expect(activationScript).toContain('var browserWorkspaceId = "workspace-1";') - expect(activationScript).toContain('var browserPageId = "page-1";') - expect(activationScript).toContain('state.setActiveBrowserTab(browserWorkspaceId);') - expect(activationScript).toContain( - 'state.setActiveBrowserPage(browserWorkspaceId, browserPageId);' - ) - expect(activationScript).toContain('var targetWorktreeId = "wt-1";') - - restore() - }) - it('holds capture paint one-way and keeps the renderer unthrottled until the last release', () => { const guest = { id: 1707, @@ -192,230 +128,4 @@ describe('browserManager', () => { const release = browserManager.holdPaintForCapture(424242) expect(() => release()).not.toThrow() }) - - it('restores the previously focused browser workspace after screenshot prep changes tabs', async () => { - const rendererExecuteJavaScriptMock = vi - .fn() - .mockResolvedValueOnce({ - prevTabType: 'browser', - prevActiveWorktreeId: 'wt-prev', - prevActiveBrowserWorkspaceId: 'workspace-prev', - prevActiveBrowserPageId: 'page-prev', - prevFocusedGroupTabId: 'tab-prev', - targetWorktreeId: 'wt-target', - targetBrowserWorkspaceId: 'workspace-target', - targetBrowserPageId: 'page-target' - }) - .mockResolvedValueOnce(undefined) - const guest = { - id: 708, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock - } - const renderer = { - id: rendererWebContentsId, - isDestroyed: vi.fn(() => false), - executeJavaScript: rendererExecuteJavaScriptMock - } - browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) }) - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === rendererWebContentsId) { - return renderer - } - return null - }) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'page-target', - workspaceId: 'workspace-target', - worktreeId: 'wt-target', - webContentsId: guest.id, - rendererWebContentsId - }) - - const restore = await browserManager.ensureWebviewVisible(guest.id) - restore() - - const restoreScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0] - expect(restoreScript).toContain('state.setActiveWorktree("wt-prev");') - expect(restoreScript).toContain('state.setActiveBrowserTab("workspace-prev");') - }) - - it('restores the previously active page when screenshot prep switches pages inside one workspace', async () => { - const rendererExecuteJavaScriptMock = vi - .fn() - .mockResolvedValueOnce({ - prevTabType: 'browser', - prevActiveWorktreeId: 'wt-target', - prevActiveBrowserWorkspaceId: 'workspace-target', - prevActiveBrowserPageId: 'page-prev', - prevFocusedGroupTabId: null, - targetWorktreeId: 'wt-target', - targetBrowserWorkspaceId: 'workspace-target', - targetBrowserPageId: 'page-target' - }) - .mockResolvedValueOnce(undefined) - const guest = { - id: 709, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock - } - const renderer = { - id: rendererWebContentsId, - isDestroyed: vi.fn(() => false), - executeJavaScript: rendererExecuteJavaScriptMock - } - browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) }) - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === rendererWebContentsId) { - return renderer - } - return null - }) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'page-target', - workspaceId: 'workspace-target', - worktreeId: 'wt-target', - webContentsId: guest.id, - rendererWebContentsId - }) - - const restore = await browserManager.ensureWebviewVisible(guest.id) - restore() - - const restoreScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0] - expect(restoreScript).toContain('state.setActiveBrowserPage(') - expect(restoreScript).toContain('"workspace-target"') - expect(restoreScript).toContain('"page-prev"') - }) - - it('restores remembered browser workspace/page even when the visible pane was terminal', async () => { - const rendererExecuteJavaScriptMock = vi - .fn() - .mockResolvedValueOnce({ - prevTabType: 'terminal', - prevActiveWorktreeId: 'wt-target', - prevActiveBrowserWorkspaceId: 'workspace-prev', - prevActiveBrowserPageId: 'page-prev', - prevFocusedGroupTabId: 'tab-prev', - targetWorktreeId: 'wt-target', - targetBrowserWorkspaceId: 'workspace-target', - targetBrowserPageId: 'page-target' - }) - .mockResolvedValueOnce(undefined) - const guest = { - id: 7091, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock - } - const renderer = { - id: rendererWebContentsId, - isDestroyed: vi.fn(() => false), - executeJavaScript: rendererExecuteJavaScriptMock - } - browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) }) - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === rendererWebContentsId) { - return renderer - } - return null - }) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'page-target', - workspaceId: 'workspace-target', - worktreeId: 'wt-target', - webContentsId: guest.id, - rendererWebContentsId - }) - - const restore = await browserManager.ensureWebviewVisible(guest.id) - restore() - - const restoreScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0] - expect(restoreScript).toContain('state.setActiveBrowserTab("workspace-prev");') - expect(restoreScript).toContain('state.setActiveBrowserPage(') - expect(restoreScript).toContain('"workspace-prev"') - expect(restoreScript).toContain('"page-prev"') - expect(restoreScript).toContain('state.activateTab("tab-prev");') - expect(restoreScript).toContain('state.setActiveTabType("terminal");') - }) - - it('does not focus the Orca window while preparing a screenshot', async () => { - const rendererExecuteJavaScriptMock = vi.fn().mockResolvedValueOnce({ - prevTabType: 'terminal', - prevActiveWorktreeId: 'wt-1', - prevActiveBrowserWorkspaceId: 'workspace-prev', - prevActiveBrowserPageId: 'page-prev', - prevFocusedGroupTabId: 'tab-prev', - targetWorktreeId: 'wt-1', - targetBrowserWorkspaceId: 'workspace-1', - targetBrowserPageId: 'page-1' - }) - const guest = { - id: 710, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock - } - const renderer = { - id: rendererWebContentsId, - isDestroyed: vi.fn(() => false), - executeJavaScript: rendererExecuteJavaScriptMock - } - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === rendererWebContentsId) { - return renderer - } - return null - }) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'page-1', - workspaceId: 'workspace-1', - worktreeId: 'wt-1', - webContentsId: guest.id, - rendererWebContentsId - }) - - await browserManager.ensureWebviewVisible(guest.id) - - expect(browserWindowFromWebContentsMock).not.toHaveBeenCalled() - }) }) diff --git a/src/main/browser/browser-manager-visibility.ts b/src/main/browser/browser-manager-visibility.ts index c51324b70db..45356cdb8dc 100644 --- a/src/main/browser/browser-manager-visibility.ts +++ b/src/main/browser/browser-manager-visibility.ts @@ -2,217 +2,6 @@ import { rendererPublicationThrottle } from '../window/renderer-publication-thro import { BrowserManagerState } from './browser-manager-state' export abstract class BrowserManagerVisibility extends BrowserManagerState { - // Why: screenshots target page ids but visible chrome is keyed by workspace id; activate by workspace or the webview stays hidden and capture times out. - async ensureWebviewVisible(guestWebContentsId: number): Promise<() => void> { - const browserPageId = this.resolveBrowserTabIdForGuestWebContentsId(guestWebContentsId) - if (!browserPageId) { - return () => {} - } - const browserWorkspaceId = this.workspaceIdByPageId.get(browserPageId) ?? browserPageId - const worktreeId = this.worktreeIdByTabId.get(browserPageId) ?? null - const renderer = this.resolveRendererForBrowserTab(browserPageId) - if (!renderer || renderer.isDestroyed()) { - return () => {} - } - - const prev = await renderer - .executeJavaScript( - `(function() { - var store = window.__store; - if (!store) return null; - var state = store.getState(); - var prevTabType = state.activeTabType; - var prevActiveWorktreeId = state.activeWorktreeId || null; - var prevActiveBrowserWorkspaceId = state.activeBrowserTabId || null; - var prevActiveBrowserPageId = null; - var prevFocusedGroupTabId = null; - var targetWorktreeId = ${JSON.stringify(worktreeId)}; - var browserWorkspaceId = ${JSON.stringify(browserWorkspaceId)}; - var browserPageId = ${JSON.stringify(browserPageId)}; - var browserTabsByWorktree = state.browserTabsByWorktree || {}; - - if (prevActiveWorktreeId) { - var prevFocusedGroupId = (state.activeGroupIdByWorktree || {})[prevActiveWorktreeId]; - var prevGroups = (state.groupsByWorktree || {})[prevActiveWorktreeId] || []; - for (var pg = 0; pg < prevGroups.length; pg++) { - if (prevGroups[pg].id === prevFocusedGroupId) { - prevFocusedGroupTabId = prevGroups[pg].activeTabId; - break; - } - } - } - - if (prevActiveBrowserWorkspaceId) { - for (var prevWtId in browserTabsByWorktree) { - var prevBrowserTabs = browserTabsByWorktree[prevWtId] || []; - for (var pbt = 0; pbt < prevBrowserTabs.length; pbt++) { - if (prevBrowserTabs[pbt].id === prevActiveBrowserWorkspaceId) { - prevActiveBrowserPageId = prevBrowserTabs[pbt].activePageId || null; - break; - } - } - if (prevActiveBrowserPageId) break; - } - } - - if ( - targetWorktreeId && - prevActiveWorktreeId !== targetWorktreeId && - typeof state.setActiveWorktree === 'function' - ) { - state.setActiveWorktree(targetWorktreeId); - state = store.getState(); - } - - var foundWorkspace = null; - for (var wtId in browserTabsByWorktree) { - var tabs = browserTabsByWorktree[wtId] || []; - for (var i = 0; i < tabs.length; i++) { - if (tabs[i].id === browserWorkspaceId) { - foundWorkspace = tabs[i]; - if (!targetWorktreeId) { - targetWorktreeId = wtId; - } - break; - } - } - if (foundWorkspace) break; - } - - var hasTargetPage = false; - var targetPages = (state.browserPagesByWorkspace || {})[browserWorkspaceId] || []; - for (var pageIndex = 0; pageIndex < targetPages.length; pageIndex++) { - if (targetPages[pageIndex].id === browserPageId) { - hasTargetPage = true; - break; - } - } - - if (foundWorkspace) { - if (typeof state.setActiveBrowserTab === 'function') { - state.setActiveBrowserTab(browserWorkspaceId); - state = store.getState(); - } else { - var allTabs = state.unifiedTabsByWorktree || {}; - var found = null; - for (var unifiedWtId in allTabs) { - var unifiedTabs = allTabs[unifiedWtId] || []; - for (var unifiedIndex = 0; unifiedIndex < unifiedTabs.length; unifiedIndex++) { - if ( - unifiedTabs[unifiedIndex].contentType === 'browser' && - unifiedTabs[unifiedIndex].entityId === browserWorkspaceId - ) { - found = unifiedTabs[unifiedIndex]; - break; - } - } - if (found) break; - } - if (found) { - state.activateTab(found.id); - } - state.setActiveTabType('browser'); - state = store.getState(); - } - // Why: activating the workspace alone is not enough for screenshot - // capture when a browser workspace contains multiple pages. The - // compositor only paints the currently mounted page guest. - if ( - hasTargetPage && - foundWorkspace.activePageId !== browserPageId && - typeof state.setActiveBrowserPage === 'function' - ) { - state.setActiveBrowserPage(browserWorkspaceId, browserPageId); - state = store.getState(); - } - } - - return { - prevTabType: prevTabType, - prevActiveWorktreeId: prevActiveWorktreeId, - prevActiveBrowserWorkspaceId: prevActiveBrowserWorkspaceId, - prevActiveBrowserPageId: prevActiveBrowserPageId, - prevFocusedGroupTabId: prevFocusedGroupTabId, - targetWorktreeId: targetWorktreeId, - targetBrowserWorkspaceId: foundWorkspace ? browserWorkspaceId : null, - targetBrowserPageId: foundWorkspace && hasTargetPage ? browserPageId : null - }; - })()` - ) - .catch(() => null) - - const needsRestore = - prev && - (prev.prevTabType !== 'browser' || - prev.prevActiveWorktreeId !== prev.targetWorktreeId || - prev.prevFocusedGroupTabId !== null || - prev.prevActiveBrowserWorkspaceId !== prev.targetBrowserWorkspaceId || - prev.prevActiveBrowserPageId !== prev.targetBrowserPageId) - - if (!needsRestore) { - return () => {} - } - - return () => { - if (!prev || !renderer || renderer.isDestroyed()) { - return - } - renderer - .executeJavaScript( - `(function() { - var store = window.__store; - if (!store) return; - var state = store.getState(); - if ( - ${JSON.stringify(prev?.prevActiveWorktreeId)} && - ${JSON.stringify(prev?.prevActiveWorktreeId)} !== - ${JSON.stringify(prev?.targetWorktreeId)} && - typeof state.setActiveWorktree === 'function' - ) { - state.setActiveWorktree(${JSON.stringify(prev?.prevActiveWorktreeId)}); - state = store.getState(); - } - if ( - ${JSON.stringify(prev?.prevActiveBrowserWorkspaceId)} && - ${JSON.stringify(prev?.prevActiveBrowserWorkspaceId)} !== - ${JSON.stringify(prev?.targetBrowserWorkspaceId)} && - typeof state.setActiveBrowserTab === 'function' - ) { - state.setActiveBrowserTab(${JSON.stringify(prev?.prevActiveBrowserWorkspaceId)}); - state = store.getState(); - } - if ( - ${JSON.stringify(prev?.prevActiveBrowserWorkspaceId)} && - ${JSON.stringify(prev?.prevActiveBrowserPageId)} && - ${JSON.stringify(prev?.prevActiveBrowserPageId)} !== - ${JSON.stringify(prev?.targetBrowserPageId)} && - typeof state.setActiveBrowserPage === 'function' - ) { - // Why: Orca remembers the last browser workspace/page even when - // the user is currently in terminal/editor view. Screenshot prep - // temporarily switches that hidden browser selection state, so - // restore it independently of the visible tab type. - state.setActiveBrowserPage( - ${JSON.stringify(prev?.prevActiveBrowserWorkspaceId)}, - ${JSON.stringify(prev?.prevActiveBrowserPageId)} - ); - state = store.getState(); - } - if ( - ${JSON.stringify(prev?.prevTabType)} !== 'browser' && - ${JSON.stringify(prev?.prevFocusedGroupTabId)} - ) { - state.activateTab(${JSON.stringify(prev?.prevFocusedGroupTabId)}); - } - if (${JSON.stringify(prev?.prevTabType)} !== 'browser') { - state.setActiveTabType(${JSON.stringify(prev?.prevTabType)}); - } - })()` - ) - .catch(() => {}) - } - } - // Why: page id -> active capture count; the renderer hears only the first hold and the last release. private readonly capturePaintHolds = new Map()