feat(browser): add paste-files command for clipboard-paste file uploads#1846
Open
Benjamin-eecs wants to merge 1 commit into
Open
feat(browser): add paste-files command for clipboard-paste file uploads#1846Benjamin-eecs wants to merge 1 commit into
Benjamin-eecs wants to merge 1 commit into
Conversation
5f0b71f to
16c1861
Compare
There was a problem hiding this comment.
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-filesCLI command plusIPage.pasteFiles/Page.pasteFilesAPI. - Extends daemon/extension protocol to carry base64-encoded clipboard file payloads.
- Adds test coverage for the new CLI command and
Page.pasteFilesbehavior.
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 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 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 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 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', | ||
| }); | ||
| }); |
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
16c1861 to
44b7fc9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a single new browser primitive that synthesizes a
ClipboardEvent('paste')with a DataTransfer payload built from local files, plus abrowser paste-filesCLI verb that drives it. Targets web apps whose upload flow only listens to clipboard paste, not to hidden<input type="file">or CDPDOM.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 asIPage.pasteFilesis the natural follow-up toIPage.setFileInputfrom a year ago. Same shape, different DOM contract.The implementation mirrors
setFileInputend-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>insrc/types.ts:138. Resolves on extension acknowledgement, throws on no-count fallback, matchingsetFileInput.Page.pasteFilesinsrc/browser/page.ts:340reads each file, infers MIME from a small extension map (falls back toapplication/octet-streamfor unknown types; paste handlers usually sniff content anyway), base64-encodes, and sends apaste-filesdaemon action with aclipboardFilespayload.DaemonCommandaction union andclipboardFilesfield added insrc/browser/daemon-client.ts:24,50.extension/src/protocol.ts.handlePasteFileshandler inextension/src/background.tsreuse the sameresolveCommandTabId/resolveTabId/pageScopedResultpattern ashandleSetFileInput.pasteClipboardFilesinextension/src/cdp.tsbuilds the DataTransfer construction expression inside a singleRuntime.evaluate, locates the target via the optionalselectoror falls back todocument.activeElement, and surfaces ano_targeterror when neither exists.browser paste-filesinsrc/cli.ts:2086sits next tobrowser upload, reuses the existingresolveUploadFilePathshelper for~-expansion and existence checks, and emits a{pasted, count, file_names, target}envelope that matches thebrowser uploadenvelope 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
Checklist
Screenshots / Output
opencli browser <session> paste-files /path/to/img.png --target #composerreturns:{ "pasted": true, "count": 1, "file_names": ["img.png"], "target": "#composer" }Without
--target, the envelope'stargetfield reads"focused"and the extension dispatches ondocument.activeElement(falling back todocument.bodyonly when nothing is focused).New tests cover the wire path at each layer:
src/browser/page.test.ts > Page.pasteFiles(3 cases): base64 encoding pluspaste-filesaction 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--targetis omitted,file_not_foundenvelope when the path does not exist.Full local runs:
npx tsc --noEmitclean (both root andextension/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 buildclean.extension/dist/background.jsrebuilt to include the new handler + action case.cli-manifest.jsonuntouched (no adapter commands were added; only a core browser verb).