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

  1. Target the actual `<input type=file>` (often hidden): use a selector like `input[type=file]` instead of the visible button.
  2. If the input is display:none, verify the bridge still allows dispatching on it; otherwise use drag-and-drop or clipboard actions if supported.
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/e890584b95877d75. Report an issue: GitHub.