perf(browser): fill plain fields in one stdin edit (#20364)

This commit is contained in:
Neil
2026-09-12 01:36:59 -07:00
committed by GitHub
parent 4aa9329e99
commit 487d39e02e
2 changed files with 25 additions and 64 deletions
@@ -6,14 +6,10 @@ import type {
} from '../../shared/runtime-types'
import { assertClipboardTextWriteWithinLimitWithYield } from '../../shared/clipboard-text'
import { normalizeBrowserNavigationUrl } from '../../shared/browser-url'
import { iterateBrowserTextInsertionChunks } from './browser-text-insertion'
import { BrowserError } from './cdp-bridge'
import { ORCA_TAB_SESSION_PREFIX } from './agent-browser-orphan-sweep'
import { focusedValueSetExpression } from './agent-browser-bridge-input'
import {
AGENT_BROWSER_TEXT_ARGUMENT_MAX_BYTES,
EMBEDDED_NAVIGATION_TIMEOUT_MS
} from './agent-browser-bridge-types'
import { EMBEDDED_NAVIGATION_TIMEOUT_MS } from './agent-browser-bridge-types'
import {
isAbortedNavigationError,
waitForAbortedNavigationReplacement
@@ -157,23 +153,10 @@ export abstract class AgentBrowserBridgeCoreCommands extends AgentBrowserBridgeQ
async (sessionName) => {
if (!(await this.isExplicitContentEditableTarget(sessionName, element))) {
await this.execAgentBrowser(sessionName, ['focus', element])
await this.execAgentBrowser(sessionName, [
'eval',
focusedValueSetExpression(JSON.stringify(''))
])
for (const chunk of iterateBrowserTextInsertionChunks(
value,
AGENT_BROWSER_TEXT_ARGUMENT_MAX_BYTES
)) {
await this.execAgentBrowser(sessionName, [
'eval',
focusedValueSetExpression(JSON.stringify(chunk), { append: true })
])
}
await this.execAgentBrowser(sessionName, [
'eval',
focusedValueSetExpression(JSON.stringify(''), { append: true, dispatchEvents: true })
])
// One stdin edit avoids argv limits and repeated copying of the growing field value.
await this.execAgentBrowser(sessionName, ['eval', '--stdin'], {
stdinText: focusedValueSetExpression(JSON.stringify(value), { dispatchEvents: true })
})
return { filled: element } as BrowserFillResult
}
@@ -278,8 +278,9 @@ describe('AgentBrowserBridge', () => {
)
expect(evalCall).toBeDefined()
const args = evalCall![1] as string[]
const expression = args[args.indexOf('eval') + 1]
expect(() => new Function(expression)).not.toThrow()
expect(args[args.indexOf('eval') + 1]).toBe('--stdin')
expect(stdinWrites).toHaveLength(1)
expect(() => new Function(stdinWrites[0])).not.toThrow()
})
it('replaces contenteditable text through the browser editing pipeline', async () => {
@@ -359,12 +360,7 @@ describe('AgentBrowserBridge', () => {
await bridge.fill('@spinbutton', '200')
const expressions = execFileMock.mock.calls
.filter((call: unknown[]) => (call[1] as string[]).includes('eval'))
.map((call: unknown[]) => {
const args = call[1] as string[]
return args[args.indexOf('eval') + 1]
})
const expressions = stdinWrites
const input = createFillEvalNode({ tagName: 'INPUT' })
const wrapper = createFillEvalNode({
@@ -389,12 +385,7 @@ describe('AgentBrowserBridge', () => {
await bridge.fill('@spinbutton', '200')
const expressions = execFileMock.mock.calls
.filter((call: unknown[]) => (call[1] as string[]).includes('eval'))
.map((call: unknown[]) => {
const args = call[1] as string[]
return args[args.indexOf('eval') + 1]
})
const expressions = stdinWrites
const input = createFillEvalNode({ tagName: 'INPUT' })
const wrapper = createFillEvalNode({
@@ -419,12 +410,7 @@ describe('AgentBrowserBridge', () => {
await bridge.fill('@spinbutton', '200')
const expressions = execFileMock.mock.calls
.filter((call: unknown[]) => (call[1] as string[]).includes('eval'))
.map((call: unknown[]) => {
const args = call[1] as string[]
return args[args.indexOf('eval') + 1]
})
const expressions = stdinWrites
const input = createFillEvalNode({ tagName: 'INPUT' })
const controlled = createFillEvalNode({ tagName: 'DIV', descendant: input.node })
@@ -452,12 +438,7 @@ describe('AgentBrowserBridge', () => {
await bridge.fill('@spinbutton', '200')
const expressions = execFileMock.mock.calls
.filter((call: unknown[]) => (call[1] as string[]).includes('eval'))
.map((call: unknown[]) => {
const args = call[1] as string[]
return args[args.indexOf('eval') + 1]
})
const expressions = stdinWrites
const hiddenInput = createFillEvalNode({ tagName: 'INPUT', type: 'hidden' })
const numberInput = createFillEvalNode({ tagName: 'INPUT', type: 'number' })
@@ -485,12 +466,7 @@ describe('AgentBrowserBridge', () => {
await bridge.fill('@input', '200')
const expressions = execFileMock.mock.calls
.filter((call: unknown[]) => (call[1] as string[]).includes('eval'))
.map((call: unknown[]) => {
const args = call[1] as string[]
return args[args.indexOf('eval') + 1]
})
const expressions = stdinWrites
const input = createFillEvalNode({ tagName: 'INPUT' })
@@ -503,8 +479,8 @@ describe('AgentBrowserBridge', () => {
expect(input.events.map((event) => event.type)).toEqual(['input', 'change'])
})
it('chunks large agent-browser fill values before eval transport', async () => {
const text = ['x'.repeat(AGENT_BROWSER_TEXT_ARGUMENT_MAX_BYTES), 'tail'].join('')
it('fills large plain fields with one stdin edit and one event pair', async () => {
const text = `${'é\n'.repeat(512 * 1024)}tail'\\`
succeedWith({ ok: true })
await bridge.fill('@textarea', text)
@@ -512,15 +488,17 @@ describe('AgentBrowserBridge', () => {
const evalCalls = execFileMock.mock.calls.filter((call: unknown[]) =>
(call[1] as string[]).includes('eval')
)
const appendExpressions = evalCalls.slice(1, -1).map((call: unknown[]) => {
const args = call[1] as string[]
return args[args.indexOf('eval') + 1]
expect(evalCalls).toHaveLength(1)
expect(evalCalls[0][1]).toContain('--stdin')
expect(stdinWrites).toHaveLength(1)
expect((evalCalls[0][1] as string[]).join('')).not.toContain(text)
const input = createFillEvalNode({ tagName: 'TEXTAREA' })
runFillEvalExpressions(stdinWrites, {
activeElement: input.node,
getElementById: () => null
})
expect(appendExpressions).toHaveLength(2)
expect(appendExpressions.some((expression) => expression.includes(text))).toBe(false)
expect(appendExpressions[0]).toContain('x'.repeat(AGENT_BROWSER_TEXT_ARGUMENT_MAX_BYTES))
expect(appendExpressions[1]).toContain('tail')
expect(input.value).toBe(text)
expect(input.events.map((event) => event.type)).toEqual(['input', 'change'])
})
it.each([