Skip to content

feat(browser): add paste-files command for clipboard-paste file uploads#1846

Open
Benjamin-eecs wants to merge 1 commit into
jackwener:mainfrom
Benjamin-eecs:feat/browser-paste-files
Open

feat(browser): add paste-files command for clipboard-paste file uploads#1846
Benjamin-eecs wants to merge 1 commit into
jackwener:mainfrom
Benjamin-eecs:feat/browser-paste-files

Conversation

@Benjamin-eecs
Copy link
Copy Markdown
Contributor

@Benjamin-eecs Benjamin-eecs commented Jun 3, 2026

Description

Adds a single new browser primitive that synthesizes a ClipboardEvent('paste') with a DataTransfer payload built from local files, plus a browser paste-files CLI verb that drives it. Targets web apps whose upload flow only listens to clipboard paste, not to hidden <input type="file"> or CDP DOM.setFileInputFiles. Chat composers, rich text editors, issue trackers, ticket forms are the #1843 use cases.

This is currently being reinvented in 13 adapter files (twitter quote / post / reply / utils, jike create / repost / comment, xiaohongshu publish, wechat-channels publish, claude utils, deepseek utils, chatgpt utils, instagram reel / post), 8 of which dispatch the exact ClipboardEvent('paste') shape this primitive provides. Each adapter currently inlines a 15-30 LOC block to base64-decode files, build a DataTransfer, and dispatch a paste event. Centralizing the flow as IPage.pasteFiles is the natural follow-up to IPage.setFileInput from a year ago. Same shape, different DOM contract.

The implementation mirrors setFileInput end-to-end so the new path slots into the existing protocol surface without inventing a new transport:

  • IPage.pasteFiles(files: string[], selector?: string): Promise<void> in src/types.ts:138. Resolves on extension acknowledgement, throws on no-count fallback, matching setFileInput.
  • Page.pasteFiles in src/browser/page.ts:340 reads each file, infers MIME from a small extension map (falls back to application/octet-stream for unknown types; paste handlers usually sniff content anyway), base64-encodes, and sends a paste-files daemon action with a clipboardFiles payload.
  • Daemon DaemonCommand action union and clipboardFiles field added in src/browser/daemon-client.ts:24,50.
  • Extension protocol mirrors the daemon shape in extension/src/protocol.ts.
  • Extension dispatch case + handlePasteFiles handler in extension/src/background.ts reuse the same resolveCommandTabId / resolveTabId / pageScopedResult pattern as handleSetFileInput.
  • Extension executor pasteClipboardFiles in extension/src/cdp.ts builds the DataTransfer construction expression inside a single Runtime.evaluate, locates the target via the optional selector or falls back to document.activeElement, and surfaces a no_target error when neither exists.
  • CLI verb browser paste-files in src/cli.ts:2086 sits next to browser upload, reuses the existing resolveUploadFilePaths helper for ~-expansion and existence checks, and emits a {pasted, count, file_names, target} envelope that matches the browser upload envelope shape (sibling consistency).

No existing adapter is migrated in this PR. The 13 inline DataTransfer blocks will land in follow-up PRs once the primitive is in main, one adapter at a time, so each migration stays small and bisectable.

Related issue: closes #1843.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🌐 New site adapter
  • 📝 Documentation
  • ♻️ Refactor
  • 🔧 CI / build / tooling

Checklist

  • I ran the checks relevant to this PR
  • I updated tests or docs if needed
  • I included output or screenshots when useful

Screenshots / Output

opencli browser <session> paste-files /path/to/img.png --target #composer returns:

{
  "pasted": true,
  "count": 1,
  "file_names": ["img.png"],
  "target": "#composer"
}

Without --target, the envelope's target field reads "focused" and the extension dispatches on document.activeElement (falling back to document.body only when nothing is focused).

New tests cover the wire path at each layer:

  • src/browser/page.test.ts > Page.pasteFiles (3 cases): base64 encoding plus paste-files action payload shape (real temp PNG fixture), empty-files rejection before reaching the daemon, no-count fallback when the extension reports an unsupported action.
  • src/cli.test.ts > browser click/type commands > paste-files (3 cases): happy path with --target, focused-default envelope when --target is omitted, file_not_found envelope when the path does not exist.

Full local runs:

  • npx tsc --noEmit clean (both root and extension/tsconfig.json).
  • npx vitest run --project unit src/: 1163 passed / 1 skipped (1 pre-existing skip).
  • npx vitest run --project extension extension/src/: 82 passed.
  • npm run build clean. extension/dist/background.js rebuilt to include the new handler + action case.
  • cli-manifest.json untouched (no adapter commands were added; only a core browser verb).

@Benjamin-eecs Benjamin-eecs force-pushed the feat/browser-paste-files branch 2 times, most recently from f7569b4 to 5f0b71f Compare June 3, 2026 12:11
Wires a new IPage.pasteFiles primitive plus a browser paste-files CLI verb so adapters can attach local files into chat composers and rich editors whose upload flow only listens to clipboard paste events.

Closes jackwener#1843
@Benjamin-eecs Benjamin-eecs force-pushed the feat/browser-paste-files branch from 5f0b71f to 16c1861 Compare June 3, 2026 12:14
@Benjamin-eecs Benjamin-eecs marked this pull request as ready for review June 3, 2026 12:21
Copilot AI review requested due to automatic review settings June 3, 2026 12:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds end-to-end support for pasting local files into web apps via a synthesized clipboard paste event (CLI → daemon protocol → extension CDP injection → Page API).

Changes:

  • Introduces browser paste-files CLI command plus IPage.pasteFiles/Page.pasteFiles API.
  • Extends daemon/extension protocol to carry base64-encoded clipboard file payloads.
  • Adds test coverage for the new CLI command and Page.pasteFiles behavior.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/types.ts Adds pasteFiles to the page interface contract.
src/cli.ts Adds browser paste-files command and JSON envelope output.
src/cli.test.ts Adds CLI tests covering target selector forwarding and missing-file validation.
src/browser/page.ts Implements Page.pasteFiles (read files → base64 → sendCommand).
src/browser/page.test.ts Adds unit tests for Page.pasteFiles encoding and error paths.
src/browser/daemon-client.ts Extends daemon command type union and payload types.
extension/src/protocol.ts Extends extension command protocol to include paste-files.
extension/src/cdp.ts Implements clipboard paste dispatch using Runtime.evaluate.
extension/src/background.ts Adds command handler wiring and executor delegation.
extension/dist/background.js Updates built extension bundle with new paste-files support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/browser/page.ts
Comment on lines +355 to +368
async pasteFiles(files: string[], selector?: string): Promise<void> {
if (!Array.isArray(files) || files.length === 0) {
throw new Error('pasteFiles requires at least one file path');
}
const clipboardFiles = files.map((filePath) => {
const absPath = path.resolve(filePath);
const buffer = fs.readFileSync(absPath);
const ext = path.extname(absPath).toLowerCase();
return {
name: path.basename(absPath),
mimeType: CLIPBOARD_MIME_BY_EXT[ext] ?? 'application/octet-stream',
base64: buffer.toString('base64'),
};
});
Comment thread src/browser/page.ts
Comment on lines +369 to +376
const result = await sendCommand('paste-files', {
clipboardFiles,
selector,
...this._cmdOpts(),
}) as { count?: number };
if (!result?.count) {
throw new Error('pasteFiles returned no count; command may not be supported by the extension');
}
Comment thread extension/src/cdp.ts
Comment on lines +529 to +553
const targetExpr = selector
? `document.querySelector(${JSON.stringify(selector)})`
: 'document.activeElement && document.activeElement !== document.body ? document.activeElement : document.body';
const expression = `
(() => {
const target = ${targetExpr};
if (!target) return { ok: false, reason: 'no_target' };
const files = ${JSON.stringify(files)};
const dt = new DataTransfer();
for (const f of files) {
const binary = atob(f.base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const blob = new Blob([bytes], { type: f.mimeType });
dt.items.add(new File([blob], f.name, { type: f.mimeType }));
}
const event = new ClipboardEvent('paste', {
clipboardData: dt,
bubbles: true,
cancelable: true,
});
const delivered = target.dispatchEvent(event);
return { ok: true, count: files.length, delivered };
})()
`;
Comment thread src/cli.test.ts
Comment on lines +3020 to +3036
it('paste-files: validates local files and delegates to page.pasteFiles with the optional --target selector', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-paste-'));
const file = path.join(dir, 'screenshot.png');
fs.writeFileSync(file, Buffer.from([0x89, 0x50, 0x4e, 0x47]));
(browserState.page!.pasteFiles as any).mockResolvedValueOnce(undefined);
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'paste-files', file, '--target', '#composer']);

expect(browserState.page!.pasteFiles).toHaveBeenCalledWith([file], '#composer');
expect(lastJsonLog()).toEqual({
pasted: true,
count: 1,
file_names: ['screenshot.png'],
target: '#composer',
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support pasting one or multiple local files into web pages

2 participants