can1357/oh-my-pi · error · Error
tab.uploadFile() requires an <input type=file> element
Error message
tab.uploadFile() requires an <input type=file> element
What it means
The `uploadFile` selector action requires an `<input type="file">` element: it checks `element.tagName !== "INPUT" || element.type !== "file"` before constructing a DataTransfer of File objects and assigning them to `element.files`. Throwing early avoids the silent no-op (or TypeError) of assigning files to a non-file input.
Source
Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:962
setValue(element, String(args.value || ""), false);
return true;
case "scrollIntoView":
return true;
case "select": {
const values = Array.isArray(args.values) ? args.values.map(String) : [String(args.value || "")];
if (element.tagName !== "SELECT") throw new Error("tab.select() requires a <select> element");
const wanted = new Set(values);
const selected = [];
for (const option of Array.from(element.options)) {
option.selected = wanted.has(option.value);
if (option.selected) selected.push(option.value);
}
inputEvent(element);
return selected;
}
case "uploadFile": {
if (element.tagName !== "INPUT" || element.type !== "file") {
throw new Error("tab.uploadFile() requires an <input type=file> element");
}
const transfer = new DataTransfer();
for (const file of args.files || []) {
const bytes = Uint8Array.from(atob(file.data), char => char.charCodeAt(0));
transfer.items.add(new File([bytes], file.name, { type: file.type || "application/octet-stream" }));
}
element.files = transfer.files;
inputEvent(element);
return true;
}
}
throw new Error("Unsupported selector action " + action);
})()`;
const result = (await this.#request("browser.eval", { script }, this.#runContext?.timeoutMs)) as CmuxEvalResult;
return result.value as TResult;
}
async #waitForSelector(selector: string, timeoutMs: number): Promise<void> {View on GitHub (pinned to 9690622007)
Solutions
- Target the actual `<input type=file>` (often hidden): use a selector like `input[type=file]` instead of the visible button.
- If the input is display:none, verify the bridge still allows dispatching on it; otherwise use drag-and-drop or clipboard actions if supported.
- Re-capture the element ref after the upload widget mounts; stale refs may point at wrappers.
Example fix
// before
tab.uploadFile('button.upload', files);
// after
tab.uploadFile('input[type=file]', files); // target the real input, not the styled button Defensive patterns
Strategy: validation
Validate before calling
const info = await tab.evaluate(`(() => { const el = document.querySelector(${JSON.stringify(sel)}); return el ? el.tagName + ':' + el.type : 'missing'; })()`);
if (info !== 'INPUT:file') throw new Error(`uploadFile needs input[type=file], got ${info}`); Type guard
function isFileInput(el): el is HTMLInputElement { return el instanceof HTMLInputElement && el.type === 'file'; } Prevention
- Always target `input[type=file]`, never the styled button/label that opens the picker.
- Capture element refs after the upload widget has mounted.
- Note hidden inputs still work when driven programmatically via DataTransfer.
When it happens
Trigger: Calling the uploadFile action against a selector that matches a text input, a styled button/div that wraps the real file input, or a ref captured before the file input was rendered.
Common situations: Upload widgets that hide the actual `<input type=file>` behind a styled label; clicking-to-open dialogs instead of driving the input; automation of drag-and-drop-only upload zones that never expose a file input at the targeted node.
Related errors
- tab.select() requires a <select> element
- tab.ariaSnapshot: selector ${__sel} matched no element
- Element handle selector no longer resolves
- No element matched ${spec.raw}
- Gemini Files API upload finalization failed with HTTP ${fina
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/e890584b95877d75.
Report an issue: GitHub.