Skip to content

Commit b5db795

Browse files
committed
feat(desktop): polish browser and terminal resources
1 parent 7d8c4b3 commit b5db795

61 files changed

Lines changed: 4765 additions & 991 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/main/browser-agent/cdp.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@ import { describe, expect, it, vi } from 'vitest'
33
vi.mock('electron', () => import('@/test/electron-mock'))
44

55
import { WebContentsView } from 'electron'
6-
import { setColorScheme } from '@/main/browser-agent/cdp'
6+
import { ensureInstrumented, setColorScheme } from '@/main/browser-agent/cdp'
7+
8+
describe('browser-agent CDP instrumentation', () => {
9+
it('leaves file chooser dialogs native so users can upload files', async () => {
10+
const contents = new WebContentsView().webContents
11+
12+
await ensureInstrumented(contents, { onDialog: vi.fn() })
13+
14+
expect(contents.debugger.sendCommand).toHaveBeenCalledWith('Page.enable', undefined)
15+
expect(contents.debugger.sendCommand).not.toHaveBeenCalledWith(
16+
'Page.setInterceptFileChooserDialog',
17+
expect.anything()
18+
)
19+
})
20+
})
721

822
describe('browser-agent CDP theme', () => {
923
it('emulates explicit light and dark preferences', async () => {

apps/desktop/src/main/browser-agent/cdp.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* CDP instrumentation for agent tabs via `webContents.debugger`: auto-handles
3-
* the page states that would otherwise wedge automation (JS dialogs, file
4-
* choosers), captures screenshots that work even while the view is hidden,
3+
* the page states that would otherwise wedge automation (JS dialogs),
4+
* captures screenshots that work even while the view is hidden,
55
* and dispatches TRUSTED input (key events, text insertion). Trusted input
66
* goes through Blink's real input pipeline — unlike synthetic DOM
77
* `KeyboardEvent`s, it triggers default actions (select-all, deletion, caret
@@ -24,8 +24,6 @@ export interface PageDialog {
2424
export interface CdpCallbacks {
2525
/** A JS dialog was auto-handled; the driver surfaces it to the model. */
2626
onDialog: (dialog: PageDialog) => void
27-
/** A file chooser was suppressed; the driver surfaces it to the model. */
28-
onFileChooser: () => void
2927
}
3028

3129
/** Per-tab callbacks, so a background tab's events reach ITS driver, not the
@@ -58,9 +56,6 @@ export async function ensureInstrumented(contents: WebContents, cb: CdpCallbacks
5856
}
5957

6058
await send(contents, 'Page.enable')
61-
// Suppress native file choosers: nothing can drive them from the panel,
62-
// and an open chooser blocks the page. Recorded and surfaced instead.
63-
await send(contents, 'Page.setInterceptFileChooserDialog', { enabled: true }).catch(() => {})
6459
}
6560

6661
/**
@@ -92,10 +87,6 @@ function handleDebuggerEvent(
9287
callbacks?.onDialog({ type, message })
9388
return
9489
}
95-
if (method === 'Page.fileChooserOpened') {
96-
logger.info('Suppressed file chooser in agent browser')
97-
callbacks?.onFileChooser()
98-
}
9990
}
10091

10192
/**

apps/desktop/src/main/browser-agent/context-menu.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ function params(overrides: Partial<Params> = {}): Params {
3333

3434
function page(overrides: Partial<Page> = {}): Page {
3535
// A fresh tab sits at the panel's baseline, which the menu reports as 100%.
36-
return { canGoBack: true, canGoForward: true, zoomFactor: BASE_ZOOM_FACTOR, ...overrides }
36+
return {
37+
canGoBack: true,
38+
canGoForward: true,
39+
zoomFactor: BASE_ZOOM_FACTOR,
40+
defaultZoomFactor: BASE_ZOOM_FACTOR,
41+
...overrides,
42+
}
3743
}
3844

3945
function handlers(): Handlers {
@@ -67,7 +73,7 @@ describe('buildAgentContextMenuTemplate', () => {
6773
'Reload',
6874
'Zoom In',
6975
'Zoom Out',
70-
'Actual Size (100%)',
76+
'Reset Zoom (100%)',
7177
])
7278
})
7379

@@ -137,7 +143,7 @@ describe('buildAgentContextMenuTemplate', () => {
137143
// against Chromium's native scale (where this factor would read 110%).
138144
const twoUp = steppedZoomFactor(steppedZoomFactor(BASE_ZOOM_FACTOR, 1), 1)
139145
const stepped = buildAgentContextMenuTemplate(params(), page({ zoomFactor: twoUp }), handlers())
140-
expect(item(stepped, 'Actual Size (121%)')?.enabled).toBe(true)
146+
expect(item(stepped, 'Reset Zoom (121%)')?.enabled).toBe(true)
141147

142148
const atMax = buildAgentContextMenuTemplate(params(), page({ zoomFactor: 3 }), handlers())
143149
expect(item(atMax, 'Zoom In')?.enabled).toBe(false)
@@ -148,21 +154,26 @@ describe('buildAgentContextMenuTemplate', () => {
148154

149155
// Nothing to reset to at 100%.
150156
expect(
151-
item(buildAgentContextMenuTemplate(params(), page(), handlers()), 'Actual Size (100%)')
157+
item(buildAgentContextMenuTemplate(params(), page(), handlers()), 'Reset Zoom (100%)')
152158
?.enabled
153159
).toBe(false)
154160
})
155161

156-
it('resets to exactly the baseline, undoing accumulated drift', () => {
162+
it('resets to the configured default, undoing accumulated drift', () => {
157163
const handled = handlers()
158164
// Three rungs of float multiplication up, so the factor no longer sits on a
159165
// clean value — reset has to restore the baseline exactly, not step back.
160166
const drifted = [1, 1, 1].reduce((factor) => steppedZoomFactor(factor, 1), BASE_ZOOM_FACTOR)
161-
const template = buildAgentContextMenuTemplate(params(), page({ zoomFactor: drifted }), handled)
167+
const configuredDefault = BASE_ZOOM_FACTOR * 1.25
168+
const template = buildAgentContextMenuTemplate(
169+
params(),
170+
page({ zoomFactor: drifted, defaultZoomFactor: configuredDefault }),
171+
handled
172+
)
162173

163-
item(template, 'Actual Size (133%)')?.click?.({} as never, undefined as never, {} as never)
174+
item(template, 'Reset Zoom (133%)')?.click?.({} as never, undefined as never, {} as never)
164175

165-
expect(handled.setZoomFactor).toHaveBeenCalledWith(BASE_ZOOM_FACTOR)
176+
expect(handled.setZoomFactor).toHaveBeenCalledWith(configuredDefault)
166177
})
167178

168179
it('never leaves a separator with nothing above it', () => {
@@ -190,7 +201,10 @@ describe('attachAgentContextMenu', () => {
190201
it('pops a menu built from the page that was right-clicked', () => {
191202
const contents = new WebContentsView().webContents
192203
vi.mocked(contents.navigationHistory.canGoBack).mockReturnValue(true)
193-
attachAgentContextMenu(contents, { openTab: vi.fn() })
204+
attachAgentContextMenu(contents, {
205+
openTab: vi.fn(),
206+
defaultZoomFactor: () => BASE_ZOOM_FACTOR,
207+
})
194208

195209
const listeners = vi.mocked(contents.on).mock.calls as unknown as [
196210
string,

apps/desktop/src/main/browser-agent/context-menu.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ const MAX_ZOOM_FACTOR = 3
3232
* The browser lives in a panel that is only ever a fraction of the window, so
3333
* it renders a rung below Chromium's native scale and treats THAT as its
3434
* baseline: the menu reads 100% there, and every other rung is reported
35-
* relative to it. Users get a zoom control that behaves the way one should —
36-
* starts at 100%, resets to 100% — over a page that is genuinely rendering at
37-
* ~91% of native.
35+
* relative to it. New installs start there; when a user chooses a different
36+
* default, Reset Zoom returns to that configured percentage. The initial 100%
37+
* is genuinely rendering at ~91% of native.
3838
*
3939
* Defined as one rung below native rather than as a round number so the ladder
4040
* still lands exactly on Chromium's 1.0 (the crispest rasterization, one step
@@ -76,6 +76,7 @@ interface AgentPageContext {
7676
canGoBack: boolean
7777
canGoForward: boolean
7878
zoomFactor: number
79+
defaultZoomFactor: number
7980
}
8081

8182
interface AgentContextMenuHandlers {
@@ -92,6 +93,8 @@ interface AgentContextMenuHandlers {
9293
export interface AgentContextMenuHost {
9394
/** Opens a link from the page in another tab of the same browser. */
9495
openTab(url: string): void
96+
/** Returns the device's current default page zoom factor. */
97+
defaultZoomFactor(): number
9598
}
9699

97100
/**
@@ -150,9 +153,9 @@ export function buildAgentContextMenuTemplate(
150153
click: () => handlers.setZoomFactor(zoomOut),
151154
},
152155
{
153-
label: `Actual Size (${zoomPercent}%)`,
154-
enabled: zoomPercent !== 100,
155-
click: () => handlers.setZoomFactor(BASE_ZOOM_FACTOR),
156+
label: `Reset Zoom (${zoomPercent}%)`,
157+
enabled: page.zoomFactor !== page.defaultZoomFactor,
158+
click: () => handlers.setZoomFactor(page.defaultZoomFactor),
156159
}
157160
)
158161

@@ -168,6 +171,7 @@ export function attachAgentContextMenu(contents: WebContents, host: AgentContext
168171
canGoBack: contents.navigationHistory.canGoBack(),
169172
canGoForward: contents.navigationHistory.canGoForward(),
170173
zoomFactor: contents.getZoomFactor(),
174+
defaultZoomFactor: host.defaultZoomFactor(),
171175
},
172176
{
173177
copy: () => contents.copy(),

apps/desktop/src/main/browser-agent/driver.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import type { MenuItemConstructorOptions } from 'electron'
12
import { beforeEach, describe, expect, it, vi } from 'vitest'
23

34
vi.mock('electron', () => import('@/test/electron-mock'))
45

5-
import { BrowserWindow } from 'electron'
6+
import { BrowserWindow, Menu } from 'electron'
67
import * as driverModule from '@/main/browser-agent/driver'
78
import * as session from '@/main/browser-agent/session'
89

@@ -65,6 +66,28 @@ describe('executeTool', () => {
6566
expect(second.result).toMatchObject({ tabs: [] })
6667
})
6768

69+
it('builds the native toolbar menu and routes renderer-owned actions back to its chat', async () => {
70+
await driver.executeTool('browser_open_tab', {})
71+
const win = new BrowserWindow()
72+
vi.mocked(Menu.buildFromTemplate).mockClear()
73+
74+
expect(driver.showToolbarMenu('legacy', win, { x: 20, y: 30 })).toBe(true)
75+
const template = vi.mocked(Menu.buildFromTemplate).mock.calls[0]?.[0] as
76+
| MenuItemConstructorOptions[]
77+
| undefined
78+
const labels = template?.filter((item) => item.type !== 'separator').map((item) => item.label)
79+
expect(labels).toEqual(['Find in Page', 'Zoom (110%)', 'Import Passwords', 'Browser Settings'])
80+
81+
const settings = template?.find((item) => item.label === 'Browser Settings')
82+
const openSettings = settings?.click as (() => void) | undefined
83+
openSettings?.()
84+
expect(win.webContents.send).toHaveBeenCalledWith(
85+
'browser-agent:toolbar-command',
86+
'browser-settings',
87+
'legacy'
88+
)
89+
})
90+
6891
it('keeps tool queues and tab state isolated by chat scope', async () => {
6992
await driver.executeTool('chat-a', 'browser_open_tab', {})
7093
await driver.executeTool('chat-a', 'browser_open_tab', {})

0 commit comments

Comments
 (0)