Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion apps/desktop/src/main/browser-agent/driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,18 @@ describe('executeTool', () => {
expect(driver.migrateBrowserScope('pending:other-chat', 'chat-occupied')).toBe(false)
})

it('keeps a durable destination adoptable after an empty restore', async () => {
await driver.executeTool('pending:new-chat', 'browser_open_tab', {})
driver.activateBrowserScope('chat-real')
expect(driver.restoreBrowserScope('chat-real')).toMatchObject({ tabs: [] })

expect(driver.migrateBrowserScope('pending:new-chat', 'chat-real')).toBe(true)
await expect(driver.executeTool('chat-real', 'browser_list_tabs', {})).resolves.toMatchObject({
ok: true,
result: { scopeId: 'chat-real', tabs: [{ tabId: '1' }] },
})
})

it('cancels only the replaced destination authorizations during migration', async () => {
await driver.executeTool('pending:new-chat', 'browser_open_tab', {})
driver.activateBrowserScope('chat-real')
Expand Down Expand Up @@ -1395,7 +1407,7 @@ describe('executeTool', () => {
it('keeps activation lazy, then restores and disposes through the driver API', async () => {
const snapshot: BrowserSessionSnapshot = {
v: 1,
tabs: [{ url: 'https://restored.example/', pinned: false }],
tabs: [{ url: 'https://restored.example/' }],
activeIndex: 0,
downloads: [],
}
Expand Down
24 changes: 8 additions & 16 deletions apps/desktop/src/main/browser-agent/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,10 +794,13 @@ export function restoreBrowserScope(scopeId: string): BrowserTabsState {
return session.withBrowserScope(resolved, () => session.peekTabsState())
}
const state = driverScopeState(resolved)
state.activationOnly = false
return session.withBrowserScope(resolved, () => {
session.restoreBrowserSession()
return session.peekTabsState()
const tabs = session.peekTabsState()
// Only a scope that actually holds pages is material; one restored empty
// stays adoptable by a pending chat migrating onto its id.
if (tabs.tabs.length > 0) state.activationOnly = false
return tabs
})
}

Expand Down Expand Up @@ -905,7 +908,7 @@ export async function clearBrowserProfile(
retireAllDriverScopeStates()
const settingsCleared = knownSessions?.clear() !== false
const outcomes = await Promise.allSettled([session.clearProfileStorage(), clearCredentials()])
// Last, covering the pinned-tab list `clearProfileStorage` just emptied.
// Last, covering the saved tab list `clearProfileStorage` just emptied.
// Settings writes coalesce, and an erasure that is still sitting in that
// window when the process dies leaves the previous account's data on disk
// after sign-out already told the user it was gone.
Expand Down Expand Up @@ -2368,8 +2371,7 @@ async function executeToolInner(
}
}
assertCurrentExecution()
// The agent chose to open this page to work in, so the panel follows it.
const tab = session.addAutomationTab({ reveal: true })
const tab = session.addAutomationTab()
const contents = tab.view.webContents
if (url) {
assertCurrentExecution()
Expand Down Expand Up @@ -4782,19 +4784,9 @@ export async function handlePanelAction(
}
return
}
if (action.action === 'new-tab') {
session.addTab()
return
}
if (action.action === 'duplicate-tab') {
if (typeof action.tabId === 'string') {
session.duplicateTab(action.tabId)
}
return
}
if (action.action === 'switch-tab') {
if (typeof action.tabId === 'string') {
session.switchTab(action.tabId)
session.switchTab(action.tabId, { claim: action.claim !== false })
}
return
}
Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/src/main/browser-agent/panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function freshPanel(): PanelModule {
getMainWindow: () => null,
activeTab: () => null,
backgroundColor: () => '#ffffff',
ensureInitialTab: () => {},
restoreActiveScope: () => {},
onViewDetached: () => {},
})
panelModule.activatePanelScope('chat-test')
Expand All @@ -33,12 +33,12 @@ const PANEL_RECT = { x: 400, y: 64, width: 600, height: 800 }
function showPanel(panel: PanelModule) {
const win = new BrowserWindow()
const view = new WebContentsView()
const active = { id: 'tab-1', scopeId: 'chat-test', view, pinned: false }
const active = { id: 'tab-1', scopeId: 'chat-test', view }
panel.initPanel({
getMainWindow: () => win,
activeTab: () => active,
backgroundColor: () => '#0c0c0c',
ensureInitialTab: () => {},
restoreActiveScope: () => {},
onViewDetached: () => {},
})
panel.activatePanelScope('chat-test')
Expand All @@ -56,13 +56,13 @@ describe('panel chat scope', () => {
it('returns keyboard focus to the renderer when attaching a view steals it mid-typing', () => {
const win = new BrowserWindow()
const view = new WebContentsView()
const active = { id: 'tab-1', scopeId: 'chat-test', view, pinned: false }
const active = { id: 'tab-1', scopeId: 'chat-test', view }
vi.mocked(win.webContents.isFocused).mockReturnValue(true)
panel.initPanel({
getMainWindow: () => win,
activeTab: () => active,
backgroundColor: () => '#0c0c0c',
ensureInitialTab: () => {},
restoreActiveScope: () => {},
onViewDetached: () => {},
})
panel.activatePanelScope('chat-test')
Expand Down Expand Up @@ -327,12 +327,12 @@ describe('panel chat scope', () => {
it('applies a forced hide before the panel reports its first bounds', () => {
const win = new BrowserWindow()
const view = new WebContentsView()
const active = { id: 'tab-1', scopeId: 'chat-test', view, pinned: false }
const active = { id: 'tab-1', scopeId: 'chat-test', view }
panel.initPanel({
getMainWindow: () => win,
activeTab: () => active,
backgroundColor: () => '#0c0c0c',
ensureInitialTab: () => {},
restoreActiveScope: () => {},
onViewDetached: () => {},
})
panel.activatePanelScope('chat-test')
Expand Down
12 changes: 4 additions & 8 deletions apps/desktop/src/main/browser-agent/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,8 @@ export interface PanelHost {
activeTab: () => AgentTab | null
/** Native backdrop used by a blank tab before its first page paint. */
backgroundColor: () => string
/**
* Materializes the initial tab when the panel first becomes visible: a
* visible browser resource always represents one open browser window, and
* the tab strip, omnibox, and native session must not disagree about that.
*/
ensureInitialTab: () => void
/** Hydrates the active scope's saved pages when the panel first becomes visible. */
restoreActiveScope: () => void
/** Lets the session drop focus tracking for a view that is no longer attached. */
onViewDetached: (view: WebContentsView | null) => void
}
Expand All @@ -55,7 +51,7 @@ let host: PanelHost = {
getMainWindow: () => null,
activeTab: () => null,
backgroundColor: () => '#ffffff',
ensureInitialTab: () => {},
restoreActiveScope: () => {},
onViewDetached: () => {},
}

Expand Down Expand Up @@ -834,7 +830,7 @@ export function setPanelBounds(
panelBounds = bounds
panelAnchor = bounds === null ? null : (anchor ?? null)
if (bounds !== null) {
host.ensureInitialTab()
host.restoreActiveScope()
} else {
resetOcclusion()
}
Expand Down
Loading
Loading