Skip to content

Commit 857f985

Browse files
committed
updates
1 parent 84e3b66 commit 857f985

54 files changed

Lines changed: 1801 additions & 1024 deletions

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.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ function frameMatches(candidate: ProtocolFrame, frame: WebFrameMain): boolean {
210210
)
211211
}
212212

213-
function sameWebFrame(left: WebFrameMain, right: WebFrameMain): boolean {
213+
export function sameWebFrame(left: WebFrameMain, right: WebFrameMain): boolean {
214214
if (left === right) return true
215215
if (Number.isSafeInteger(left.frameTreeNodeId) && Number.isSafeInteger(right.frameTreeNodeId)) {
216216
return left.frameTreeNodeId === right.frameTreeNodeId

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

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ describe('executeTool', () => {
625625
})
626626
}
627627
if (isPageCall(expression, 'focusElementForTyping')) {
628-
return Promise.resolve({ focused: true, kind: 'input' })
628+
return Promise.resolve({ focused: true, kind: 'input', x: 24, y: 48 })
629629
}
630630
if (isPageCall(expression, 'activeElementSecrecy')) return Promise.resolve('safe')
631631
if (isPageCall(expression, 'readActiveElementState')) return Promise.resolve({})
@@ -869,11 +869,17 @@ describe('credential protection', () => {
869869

870870
it('aborts a type when focus moves to a password field before the insert', async () => {
871871
const contents = await openPage()
872-
// The element passed the guard, then the page advanced focus — what a
873-
// login form does between the username and password steps.
874-
respondWith(contents, {
875-
focusElementForTyping: { focused: true, kind: 'input' },
876-
activeElementSecrecy: 'secret',
872+
let focusReads = 0
873+
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
874+
if (isPageCall(expression, 'focusElementForTyping')) {
875+
focusReads++
876+
return Promise.resolve(
877+
focusReads === 1 ? { focused: true, kind: 'input', x: 24, y: 48 } : { error: 'password' }
878+
)
879+
}
880+
if (isPageCall(expression, 'readActiveElementState')) return Promise.resolve({})
881+
if (isPageCall(expression, 'readPageActionState')) return Promise.resolve({})
882+
return Promise.resolve(undefined)
877883
})
878884

879885
const result = await driver.executeTool('chat-test', 'browser_type', {
@@ -886,16 +892,15 @@ describe('credential protection', () => {
886892
expect(cdpCalls(contents, 'Input.insertText')).toHaveLength(0)
887893
})
888894

889-
it('aborts when a surface check moves focus to a password field at the final guard', async () => {
895+
it('aborts when the suggestions surface steals focus at the final guard', async () => {
890896
const contents = await openPage()
891-
let secrecyReads = 0
897+
let focusReads = 0
892898
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
893899
if (isPageCall(expression, 'focusElementForTyping')) {
894-
return Promise.resolve({ focused: true, kind: 'input' })
895-
}
896-
if (isPageCall(expression, 'activeElementSecrecy')) {
897-
secrecyReads++
898-
return Promise.resolve(secrecyReads === 1 ? 'safe' : 'secret')
900+
focusReads++
901+
return Promise.resolve(
902+
focusReads === 1 ? { focused: true, kind: 'input', x: 24, y: 48 } : { error: 'different' }
903+
)
899904
}
900905
if (isPageCall(expression, 'clickElement')) {
901906
return Promise.resolve({ dispatched: false, x: 24, y: 48, element: 'Test' })
@@ -921,16 +926,16 @@ describe('credential protection', () => {
921926
text: 'hunter2',
922927
})
923928

924-
expect(secrecyReads).toBe(2)
929+
expect(focusReads).toBe(2)
925930
expect(result.ok).toBe(false)
926-
expect(result.error).toMatch(/Refusing to act on a password field/)
931+
expect(result.error).toMatch(/different field took focus/)
927932
expect(cdpCalls(contents, 'Input.insertText')).toHaveLength(0)
928933
})
929934

930935
it('warns when acknowledged text produces no observable field change', async () => {
931936
const contents = await openPage()
932937
respondWith(contents, {
933-
focusElementForTyping: { focused: true, kind: 'input' },
938+
focusElementForTyping: { focused: true, kind: 'input', x: 24, y: 48 },
934939
activeElementSecrecy: 'safe',
935940
readActiveElementState: { activeElement: 'input', valueLength: 7 },
936941
})
@@ -950,6 +955,42 @@ describe('credential protection', () => {
950955
expect(cdpCalls(contents, 'Input.insertText')).toHaveLength(1)
951956
})
952957

958+
it('types through a focused combobox suggestions popup without pointer probing', async () => {
959+
const contents = await openPage()
960+
respondWith(contents, {
961+
focusElementForTyping: {
962+
focused: true,
963+
kind: 'input',
964+
x: 24,
965+
y: 48,
966+
coveredByRelatedPopup: true,
967+
},
968+
readActiveElementState: { activeElement: 'input', valueLength: 0 },
969+
readPageActionState: {
970+
url: 'https://example.com/login',
971+
title: 'Compose',
972+
focus: 'input:combobox:::To:',
973+
mutationRevision: 0,
974+
dialogs: [],
975+
popups: ['Contact list'],
976+
scroll: [0],
977+
},
978+
})
979+
980+
const result = await driver.executeTool('chat-test', 'browser_type', {
981+
elementId: 0,
982+
text: 'Mondu',
983+
})
984+
985+
expect(result).toMatchObject({ ok: true, result: { dispatched: true, trusted: true } })
986+
expect(cdpCalls(contents, 'Input.insertText')).toHaveLength(1)
987+
expect(
988+
vi
989+
.mocked(contents.executeJavaScript)
990+
.mock.calls.some(([expression]) => isPageCall(String(expression), 'clickElement'))
991+
).toBe(false)
992+
})
993+
953994
it('confirms typing only after the field readback changes', async () => {
954995
const contents = await openPage()
955996
let inserted = false
@@ -960,7 +1001,7 @@ describe('credential protection', () => {
9601001
})
9611002
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
9621003
if (isPageCall(expression, 'focusElementForTyping')) {
963-
return Promise.resolve({ focused: true, kind: 'input' })
1004+
return Promise.resolve({ focused: true, kind: 'input', x: 24, y: 48 })
9641005
}
9651006
if (isPageCall(expression, 'activeElementSecrecy')) return Promise.resolve('safe')
9661007
if (isPageCall(expression, 'clickElement')) {
@@ -1031,6 +1072,22 @@ describe('credential protection', () => {
10311072
expect(result.error).toMatch(/Refusing to act on a password field/)
10321073
})
10331074

1075+
it('guides typing through owned suggestions without dispatching a pointer click', async () => {
1076+
const contents = await openPage()
1077+
respondWith(contents, {
1078+
clickElement: { error: 'suggestions-open', blocker: 'Contact list' },
1079+
})
1080+
1081+
const result = await driver.executeTool('chat-test', 'browser_click', { elementId: 0 })
1082+
1083+
expect(result).toMatchObject({
1084+
ok: false,
1085+
error: expect.stringContaining('Use browser_type on the same element'),
1086+
})
1087+
expect(result.error).toContain('do not dismiss the popup')
1088+
expect(cdpCalls(contents, 'Input.dispatchMouseEvent')).toHaveLength(0)
1089+
})
1090+
10341091
it('uses trusted CDP mouse input for element clicks', async () => {
10351092
const contents = await openPage()
10361093
respondWith(contents, {
@@ -1126,10 +1183,52 @@ describe('credential protection', () => {
11261183
})
11271184
})
11281185

1186+
it('confirms a panel close when the clicked target semantically disappears', async () => {
1187+
const contents = await openPage()
1188+
let actionReads = 0
1189+
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
1190+
if (isPageCall(expression, 'clickElement')) {
1191+
return Promise.resolve({ dispatched: false, x: 24, y: 48, element: 'Close thread' })
1192+
}
1193+
if (isPageCall(expression, 'readActiveElementState')) return Promise.resolve({})
1194+
if (isPageCall(expression, 'readPageActionState')) {
1195+
actionReads++
1196+
return Promise.resolve({
1197+
url: 'https://example.com/thread',
1198+
title: 'Thread',
1199+
focus: 'body',
1200+
mutationRevision: actionReads === 1 ? 0 : 2,
1201+
dialogs: [],
1202+
popups: [],
1203+
scroll: [0],
1204+
targetState:
1205+
actionReads === 1
1206+
? { present: true, rendered: true }
1207+
: { present: false, rendered: false },
1208+
})
1209+
}
1210+
return Promise.resolve(undefined)
1211+
})
1212+
1213+
const result = await driver.executeTool('chat-test', 'browser_click', { elementId: 0 })
1214+
1215+
expect(result).toMatchObject({
1216+
ok: true,
1217+
result: {
1218+
effectObserved: true,
1219+
possibleEffectObserved: true,
1220+
effect: { targetChanged: true },
1221+
},
1222+
})
1223+
expect(result).not.toMatchObject({
1224+
result: { note: expect.stringContaining('background DOM/title churn') },
1225+
})
1226+
})
1227+
11291228
it('reports failed submit dispatch separately from a completed text write', async () => {
11301229
const contents = await openPage()
11311230
respondWith(contents, {
1132-
focusElementForTyping: { focused: true, kind: 'input' },
1231+
focusElementForTyping: { focused: true, kind: 'input', x: 24, y: 48 },
11331232
activeElementSecrecy: 'safe',
11341233
readActiveElementState: { activeElement: 'input', valueLength: 5 },
11351234
readPageActionState: {
@@ -1172,7 +1271,7 @@ describe('credential protection', () => {
11721271
it('does not retry text when Chromium loses the insert acknowledgement', async () => {
11731272
const contents = await openPage()
11741273
respondWith(contents, {
1175-
focusElementForTyping: { focused: true, kind: 'input' },
1274+
focusElementForTyping: { focused: true, kind: 'input', x: 24, y: 48 },
11761275
activeElementSecrecy: 'safe',
11771276
readActiveElementState: { activeElement: 'input', valueLength: 5 },
11781277
})

0 commit comments

Comments
 (0)