Skip to content
370 changes: 355 additions & 15 deletions src/chrome/src/agent/agent.js

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions src/chrome/src/agent/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export const AGENT_TOOLS = [
type: 'function',
function: {
name: 'navigate',
description: 'Navigate the current tab to a URL. NOTE: leaving a page discards unsaved form state — re-navigating to a page like GitHub\'s "New release" resets the tag, title, and any attached files. If the current page has attached files or filled fields, this is blocked and returns blockedUnsavedChanges; finish the current action first, or pass force:true to discard the changes intentionally.',
description: 'Navigate the current tab to a URL and verify that the browser commits the navigation, including same-URL reloads. NOTE: leaving a page discards unsaved form state — re-navigating to a page like GitHub\'s "New release" resets the tag, title, and any attached files. If the current page has attached files or filled fields, this is blocked and returns blockedUnsavedChanges; finish the current action first, or pass force:true to discard the changes intentionally. A native browser leave-page confirmation cannot be accepted automatically: while it is open, this returns navigationPending/confirmationPossible instead of success.',
parameters: {
type: 'object',
properties: {
Expand Down Expand Up @@ -923,11 +923,12 @@ export const AGENT_TOOLS = [
type: 'function',
function: {
name: 'upload_file',
description: 'Attach a file directly to an existing file input without opening the page or OS file-picker dialog. This only proves that the page input received or consumed the file; it does NOT prove a remote upload, form submission, or repository commit. Do NOT click "Choose file", "Select a file", an upload drop zone, or the input first when the input already exists. Provide EITHER downloadId (preferred — the id from download_files/list_downloads; you do not need to recall the path) OR filePath (absolute local path). Never guess a downloadId. If both are accidentally provided, a valid downloadId is preferred; if that id cannot resolve, the supplied filePath is used as a fallback. If the selector is ambiguous, call get_interactive_elements and use the exact selector on the intended file-input record before retrying. If no file input exists because the widget creates it lazily, one guarded click on its add-files control may initialize the widget; then retry upload_file with the exact selector returned or discovered. The file must exist on the local filesystem.',
description: 'Attach a file directly to an existing file input without opening the page or OS file-picker dialog. This proves only that the page input received or consumed the file; it does NOT prove a remote upload, form submission, or repository commit. Do NOT click "Choose file", "Select a file", an upload drop zone, or the input first when the input already exists. Provide ONE source: attachmentId from the current user-attachment notice, downloadId from download_files/list_downloads, or an absolute filePath. Never guess an id. attachmentId is valid only during the current run and reuses the exact file the user already attached. If both downloadId and filePath are accidentally provided, a valid downloadId is preferred and filePath is the fallback. If the selector is ambiguous, call get_interactive_elements and use the exact selector on the intended file-input record before retrying. If no file input exists because the widget creates it lazily, one guarded click on its add-files control may initialize it; then retry upload_file with the exact selector returned or discovered.',
parameters: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector for the file input element' },
attachmentId: { type: 'string', description: 'Opaque id from the current [UNTRUSTED USER ATTACHMENTS] notice. Reuses that exact user-selected file without another picker. Valid only during the current agent run.' },
downloadId: { type: 'number', description: 'Id of a previously downloaded file (from download_files or list_downloads). Preferred over filePath: it resolves to the real saved path automatically, so you never have to remember it. Survives context compaction via the scratchpad.' },
filePath: { type: 'string', description: 'Absolute path to the local file. Optional if downloadId is given.' },
},
Expand Down Expand Up @@ -1616,7 +1617,7 @@ CLICKING — read this:
- For buttons and links you can SEE, click by visible text: \`click({text: "Publish release"})\`. Default matching is EXACT (case-insensitive). If exact fails (no match), the system automatically tries prefix then substring matching — but if multiple elements match at any level, it returns an ambiguity error instead of guessing.
- If you get an ambiguity error, use a more specific text string, switch to \`click({index: N})\` from \`get_interactive_elements\`, or use a selector.
- You can explicitly control matching with \`textMatch\`: \`"exact"\` (default), \`"prefix"\`, or \`"contains"\`.
- FILE UPLOADS: when the page already has an \`<input type="file">\`, do not click "Choose file", "Select a file", "Browse", the upload drop zone, or the input first. Call \`get_interactive_elements\` when needed and use the exact \`selector\` returned on the intended file-input record, then call \`upload_file({selector, downloadId})\`. \`attachmentState\` proves only local input attachment/page consumption; it does NOT prove a remote upload or submit. Verify the filename/status in the page, then activate and verify the required Submit/Commit control. If \`upload_file\` reports an ambiguous selector, a fresh \`get_interactive_elements\` call is required before retrying. Exception: if no input exists because the widget creates it lazily, make one guarded click on its add-files control to initialize it.
- FILE UPLOADS: when the page already has an \`<input type="file">\`, do not click "Choose file", "Select a file", "Browse", the upload drop zone, or the input first. Call \`get_interactive_elements\` when needed and use the exact \`selector\` returned on the intended file-input record, then call \`upload_file\` with the current user-attachment \`attachmentId\` or a prior download's \`downloadId\`. \`attachmentState\` proves only local input attachment/page consumption; it does NOT prove a remote upload or submit. Verify the filename/status in the page, then activate and verify the required Submit/Commit control. If \`upload_file\` reports an ambiguous selector, a fresh \`get_interactive_elements\` call is required before retrying; never guess a selector variant or use generic \`input[type="file"]\` when multiple inputs exist. If no input exists because the widget creates it lazily, make one guarded click on its add-files control to initialize it, then retry with the exact returned selector.
- Order of preference:
1. \`click_ax({ref_id: "ref_N"})\` — ref_id from get_accessibility_tree. Most reliable; carries role+name so you always know what you're clicking, and ref_ids are stable across calls.
2. \`click({text: "..."})\` — visible button/link text. Good fallback if the tree didn't surface the element cleanly.
Expand Down Expand Up @@ -1815,7 +1816,7 @@ TOOLS — use only these:
- schedule_resume({after_seconds|run_at, reason, resume_instruction}): terminal durable pause for this current task.
- schedule_task({title, prompt, schedule, target, mode}): create one-shot or fixed-minute-interval future work only when explicitly requested by the user. Calendar/cron recurrence is unsupported and must not be approximated. Prefer target.type:"url" for monitors/repeatable automations; use current_tab only for exact current-tab state.
- iframe_read / iframe_click / iframe_type ({urlFilter, selector, text}): interact inside cross-origin iframes (Stripe, payment widgets, embeds).
- fetch_url({url}) / research_url({url}): read OTHER URLs (not the active tab). list_downloads, download_files, download_resource_from_page, read_downloaded_file, upload_file({selector, downloadId}): file workflows. Use download_files for direct URLs and download_resource_from_page when the resource is attached to a visible page element or a blob: URL. Successful downloads auto-pin each file's downloadId to the scratchpad as an \`[auto]\` line — attach with upload_file({downloadId, selector}) and re-read with read_downloaded_file({downloadId}); no need to recall the path.
- fetch_url({url}) / research_url({url}): read OTHER URLs (not the active tab). list_downloads, download_files, download_resource_from_page, read_downloaded_file, upload_file({selector, attachmentId}) or upload_file({selector, downloadId}): file workflows. Use attachmentId for a current user-supplied file; use downloadId for a downloaded file. Use download_files for direct URLs and download_resource_from_page when the resource is attached to a visible page element or a blob: URL. Successful downloads auto-pin each file's downloadId to the scratchpad as an \`[auto]\` line — attach with upload_file({downloadId, selector}) and re-read with read_downloaded_file({downloadId}); no need to recall the path.
- download_public_media (if enabled) / download_social_media: one-shot image/video download from supported public social sites; purpose-built download tools should be tried before manual DOM/resource workflows.
- verify_form: check a form's field values before submitting. scratchpad_write({text}): pin facts that survive context summarization. progress_update/progress_read: track repeated item/action progress.
- clarify({question, options?}): ask the user only when materially blocked/ambiguous (budget 1-2 per run). Unanswered clarifies auto-select options[0] after timeout (source=timeout is not user approval for high-risk steps; source=auto Instant is intentional auto-approve). solve_captcha: once, only when CapSolver is configured.
Expand Down
40 changes: 40 additions & 0 deletions src/chrome/src/cdp/cdp-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,46 @@ export class CDPClient {
return { success: true };
}

/**
* Attach in-memory user-selected bytes to an existing file input. Unlike
* DOM.setFileInputFiles this needs no local path: the File and DataTransfer
* are created in the page realm from a run-scoped attachment handle.
*/
async setFileInputData(tabId, objectId, { base64, filename, mimeType }) {
await this.sendCommand(tabId, 'Runtime.enable');
const res = await this.sendCommand(tabId, 'Runtime.callFunctionOn', {
functionDeclaration: `function (base64, filename, mimeType) {
if (!(this instanceof HTMLInputElement) || this.type !== 'file') {
return { success: false, dispatched: false, error: 'Target is not an <input type=file>.' };
}
let dispatched = false;
try {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const file = new File([bytes], filename, { type: mimeType || 'application/octet-stream' });
const transfer = new DataTransfer();
transfer.items.add(file);
this.files = transfer.files;
dispatched = true;
this.dispatchEvent(new Event('input', { bubbles: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
return { success: true, dispatched: true, name: file.name, size: file.size, type: file.type };
} catch (error) {
return { success: false, dispatched, error: error?.message || String(error) };
}
}`,
objectId,
arguments: [
{ value: String(base64 ?? '') },
{ value: String(filename || 'attachment') },
{ value: String(mimeType || 'application/octet-stream') },
],
returnByValue: true,
});
return res?.result?.value || { success: false, dispatched: false, error: 'The page did not return an upload result.' };
}

async _disarmProtocolFileChooserGuard(tabId) {
const state = this.fileChooserGuards.get(tabId);
if (!state) return;
Expand Down
15 changes: 13 additions & 2 deletions src/chrome/src/ui/sidepanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10993,9 +10993,20 @@ async function handleAttachedFiles(fileList, tabId = renderedTabId ?? currentTab
}
try {
if (isTextFile) {
const textContent = await readFileAsText(file);
// Keep the decoded text for model context and the original bytes for
// an exact upload_file replay (encoding/BOM and MIME must survive).
const [textContent, dataUrl] = await Promise.all([
readFileAsText(file),
readFileAsDataUrl(file),
]);
if (generation !== getAttachmentGeneration(numericTabId)) continue;
getPendingAttachmentsForTab(numericTabId).push({ kind: 'text', name: file.name, textContent });
getPendingAttachmentsForTab(numericTabId).push({
kind: 'text',
name: file.name,
textContent,
dataUrl,
mimeType: file.type || '',
});
} else {
const dataUrl = await readFileAsDataUrl(file);
if (generation !== getAttachmentGeneration(numericTabId)) continue;
Expand Down
Loading
Loading