From 18c4326f05f24fc3238a978c5f814a24b269a44e Mon Sep 17 00:00:00 2001 From: Si Hyeong Lee Date: Tue, 27 Oct 2026 12:09:11 -0700 Subject: [PATCH 1/2] 1.47.0: Electron 44.10.0 (Chromium 152, Node.js 24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime update on its own, so a regression has one cause. Electron 44 changed the clipboard module to the W3C shape: - clipboard.readText() is a Promise now → clipboardInvite() is async (interface, implementation, three tests). - clipboard.readImage() / writeImage() are gone → src/main/clipboardImage.ts reads the image through clipboard.read() + ClipboardItem.getType('image/png') and writes through clipboard.write(). Verified with a real Electron 44 on Windows: a bitmap put on the clipboard by PowerShell reads as image/png at 20x10; a 3x4 PNG written by DevDeck reads back at 3x4. The three handlers (clipboard:readImage, readImageBytes, writeImage) call the helper; the guard tests mock it. - macOS 13+ is now required (Electron 44 dropped macOS 12). x64-only builds, no openAsHidden, no electron import in the renderer: nothing else in the breaking-changes list applies. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016NLGwiBY5uEivJqERQzcoY --- README.md | 2 +- docs/releases/1.47.0.md | 10 ++++++++++ package-lock.json | 12 ++++++------ package.json | 4 ++-- src/main/api/deckApi.ts | 20 +++++++++---------- src/main/clipboardImage.ts | 32 +++++++++++++++++++++++++++++++ src/main/ipc.guard.test.ts | 9 ++++++--- src/main/link/linkService.test.ts | 8 ++++---- src/main/link/linkService.ts | 9 +++++---- 9 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 docs/releases/1.47.0.md create mode 100644 src/main/clipboardImage.ts diff --git a/README.md b/README.md index 62c0f66..29db39b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ See what needs attention, resume the right conversation, and work on a paired PC -> **1.46.0** lets you close Claude Code's background sessions from the sidebar — the row's ⋯ menu or the group's bulk close runs `claude stop` and `claude rm` for you, so a finished or failed session no longer sits in the Background group forever; its conversation is kept ([release notes](docs/releases/1.46.0.md)). 1.45.2 stops an update from doubling the sessions on a paired PC — the relaunch no longer restores remote tiles itself, it adopts them from that PC, and no restore starts a second conversation for one that is already open here ([release notes](docs/releases/1.45.2.md)). 1.45.1 stops offering Claude background sessions whose folder was deleted, and says so plainly when a session's folder is gone ([release notes](docs/releases/1.45.1.md)). 1.45.0 sends any file to a session on another PC — any kind, up to 4 GB, in pieces with its progress shown ([release notes](docs/releases/1.45.0.md)). 1.44.1 fixes the "Assertion failed: remove_pty_baton" dialog (and the occasional hang or crash) when quitting or running Update all with several sessions open ([release notes](docs/releases/1.44.1.md)). 1.44.0 lists Claude Code's own background sessions (`claude --bg`, the agent view) in the sidebar, marks the ones waiting for you, and opens one in a tile by attaching to it ([release notes](docs/releases/1.44.0.md)). 1.43.0 runs parallel sessions in separate workspaces — each in its own git worktree and branch, so two agents never edit the same files — with its changes in the header and a clean-up that warns before unmerged work is lost ([release notes](docs/releases/1.43.0.md)). 1.42.0 shows what changed — a What's new window after each update, with Show me, and NEW dots on new buttons until you try them — and, as an experimental setting on Windows, lets Claude Code report its own state (working, needs approval, asking, done) instead of DevDeck reading the screen ([release notes](docs/releases/1.42.0.md)). 1.41.0 adds quick replies (one click or Ctrl+Shift+1…9 for the phrases you keep typing), a session timeline of your prompts with their times, replies and images, red boxes and arrows drawn on a screenshot and sent straight back to the session, and text, log and PDF attachments for sessions on another PC ([release notes](docs/releases/1.41.0.md)). 1.40.0 ties each screenshot to the terminal row it appeared on — hover a highlighted `[Image]` for a thumbnail, click for the full view, and jump from the viewer back to the row — says what led to each image, previews new screenshots, copies images, and adds Ctrl+↑/↓ between your prompts ([release notes](docs/releases/1.40.0.md)). 1.39.0 shows the screenshots and images a Claude session worked with — Claude Desktop shows them inline; DevDeck puts them in a strip beside the terminal, also for sessions on another PC — adds 📎 / drag-and-drop attachments (images travel to a remote PC), **Update all** for Claude Code on Windows, staggered session restores that no longer stall a PC, and ends the daily false "Login expired" ([release notes](docs/releases/1.39.0.md)). 1.38.0 made closing a session reliable across paired PCs ([notes](docs/releases/1.38.0.md)). See [the maintenance review](docs/quality-review-2026-09-08.md) and [session synchronization review](docs/session-sync-review-2026-09-08.md). +> **1.47.0** moves to Electron 44 (Chromium 152, Node.js 24); macOS 13 or later is now required, and screenshot paste / image copy use Electron's new clipboard API ([release notes](docs/releases/1.47.0.md)). 1.46.0 lets you close Claude Code's background sessions from the sidebar — the row's ⋯ menu or the group's bulk close runs `claude stop` and `claude rm` for you, so a finished or failed session no longer sits in the Background group forever; its conversation is kept ([release notes](docs/releases/1.46.0.md)). 1.45.2 stops an update from doubling the sessions on a paired PC — the relaunch no longer restores remote tiles itself, it adopts them from that PC, and no restore starts a second conversation for one that is already open here ([release notes](docs/releases/1.45.2.md)). 1.45.1 stops offering Claude background sessions whose folder was deleted, and says so plainly when a session's folder is gone ([release notes](docs/releases/1.45.1.md)). 1.45.0 sends any file to a session on another PC — any kind, up to 4 GB, in pieces with its progress shown ([release notes](docs/releases/1.45.0.md)). 1.44.1 fixes the "Assertion failed: remove_pty_baton" dialog (and the occasional hang or crash) when quitting or running Update all with several sessions open ([release notes](docs/releases/1.44.1.md)). 1.44.0 lists Claude Code's own background sessions (`claude --bg`, the agent view) in the sidebar, marks the ones waiting for you, and opens one in a tile by attaching to it ([release notes](docs/releases/1.44.0.md)). 1.43.0 runs parallel sessions in separate workspaces — each in its own git worktree and branch, so two agents never edit the same files — with its changes in the header and a clean-up that warns before unmerged work is lost ([release notes](docs/releases/1.43.0.md)). 1.42.0 shows what changed — a What's new window after each update, with Show me, and NEW dots on new buttons until you try them — and, as an experimental setting on Windows, lets Claude Code report its own state (working, needs approval, asking, done) instead of DevDeck reading the screen ([release notes](docs/releases/1.42.0.md)). 1.41.0 adds quick replies (one click or Ctrl+Shift+1…9 for the phrases you keep typing), a session timeline of your prompts with their times, replies and images, red boxes and arrows drawn on a screenshot and sent straight back to the session, and text, log and PDF attachments for sessions on another PC ([release notes](docs/releases/1.41.0.md)). 1.40.0 ties each screenshot to the terminal row it appeared on — hover a highlighted `[Image]` for a thumbnail, click for the full view, and jump from the viewer back to the row — says what led to each image, previews new screenshots, copies images, and adds Ctrl+↑/↓ between your prompts ([release notes](docs/releases/1.40.0.md)). 1.39.0 shows the screenshots and images a Claude session worked with — Claude Desktop shows them inline; DevDeck puts them in a strip beside the terminal, also for sessions on another PC — adds 📎 / drag-and-drop attachments (images travel to a remote PC), **Update all** for Claude Code on Windows, staggered session restores that no longer stall a PC, and ends the daily false "Login expired" ([release notes](docs/releases/1.39.0.md)). 1.38.0 made closing a session reliable across paired PCs ([notes](docs/releases/1.38.0.md)). See [the maintenance review](docs/quality-review-2026-09-08.md) and [session synchronization review](docs/session-sync-review-2026-09-08.md).
DevDeck project overview with repository and session navigation diff --git a/docs/releases/1.47.0.md b/docs/releases/1.47.0.md new file mode 100644 index 0000000..9f458f0 --- /dev/null +++ b/docs/releases/1.47.0.md @@ -0,0 +1,10 @@ +# DevDeck 1.47.0 + +## Electron 44 + +DevDeck now runs on Electron 44.10.0 (Chromium 152, Node.js 24). No feature changes; this release exists so the runtime update gets its own regression run. + +- **macOS 13 (Ventura) or later is required** from this version on. Electron 44 dropped macOS 12. Windows and Linux requirements are unchanged (DevDeck ships x64 builds only, so the removal of 32-bit builds does not affect it). +- **Pasting a screenshot and copying an image** were moved to Electron 44's clipboard API (`clipboard.read()` / `write()` with `ClipboardItem`; `readImage()` / `writeImage()` are gone). Verified on Windows: a bitmap on the clipboard is read as `image/png` at its original size, and an image DevDeck copies can be read back. +- Reading a connection code from the clipboard (Settings → Machines) follows the same API, which is now asynchronous. +- Dependency advisories: none open (`npm audit --audit-level=high` clean). diff --git a/package-lock.json b/package-lock.json index 2484ddf..9e28bcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "devdeck", - "version": "1.46.0", + "version": "1.47.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "devdeck", - "version": "1.46.0", + "version": "1.47.0", "license": "MIT", "dependencies": { "@fontsource-variable/geist": "5.3.0", @@ -19,7 +19,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "axe-core": "^4.14.0", - "electron": "^43.8.4", + "electron": "^44.10.0", "electron-builder": "^26.15.3", "esbuild": "^0.28.2", "playwright": "^1.64.1", @@ -2788,9 +2788,9 @@ } }, "node_modules/electron": { - "version": "43.8.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.8.4.tgz", - "integrity": "sha512-Vt1SigFO1Oe5ZX5aJYtkAtGaB4XZFipr9l565/ghd4IA/FYJCnRbp5I2PhKqPKoDJESXBitaK6NMBkl+bTGy6Q==", + "version": "44.10.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-44.10.0.tgz", + "integrity": "sha512-ss4pCfrQKV4TWkuVYtWB4HmeysPZgB96cSYNKYZv1UVCRP4vQMGNW5oopFbvx6Bbfce1XtP5pm5RDuKehp0svQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a6f2289..416aa17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devdeck", - "version": "1.46.0", + "version": "1.47.0", "description": "Project command deck — at-a-glance state + Claude Code, Codex, and Antigravity resume", "main": "dist/main/main.js", "type": "commonjs", @@ -116,7 +116,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "axe-core": "^4.14.0", - "electron": "^43.8.4", + "electron": "^44.10.0", "electron-builder": "^26.15.3", "esbuild": "^0.28.2", "playwright": "^1.64.1", diff --git a/src/main/api/deckApi.ts b/src/main/api/deckApi.ts index ef41894..01091ce 100644 --- a/src/main/api/deckApi.ts +++ b/src/main/api/deckApi.ts @@ -5,6 +5,7 @@ import { join, resolve } from 'node:path'; import { readFile, realpath, stat } from 'node:fs/promises'; import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { randomUUID } from 'node:crypto'; +import { readClipboardPng, writeClipboardPng } from '../clipboardImage'; import type { Store } from '../store'; import type { PtyHost } from '../ptyHost'; import { PtyBatcher } from '../ptyBatch'; @@ -1191,29 +1192,28 @@ export function createDeckApi(cfg: DeckApiConfig): DeckApiBundle { send('clipboard:writeText', localOnly, (text: string) => clipboard.writeText(String(text ?? ''))); invoke('clipboard:readText', localOnly, () => clipboard.readText()); /** Put an image the viewer is showing on the clipboard. Only image data URLs; never a path. */ - invoke('clipboard:writeImage', localOnly, (dataUrl: string) => { + invoke('clipboard:writeImage', localOnly, async (dataUrl: string) => { if (typeof dataUrl !== 'string' || !/^data:image\/(?:png|jpeg);base64,/.test(dataUrl) || dataUrl.length > MAX_CLIPBOARD_IMAGE_CHARS) return false; const img = nativeImage.createFromDataURL(dataUrl); if (img.isEmpty()) return false; - clipboard.writeImage(img); + try { await writeClipboardPng(img.toPNG()); } catch { return false; } return true; }); // Paste a CLIPBOARD IMAGE (e.g. a screenshot) into the terminal: Claude Code can't read the OS // clipboard (esp. native Windows), but it DOES read an image off a file path. So on Ctrl+V with an // image on the clipboard, write it to a temp PNG and return the path for the renderer to inject as // text — the one image-input method that works cross-platform. null when the clipboard has no image. - invoke('clipboard:readImage', localOnly, () => { - const img = clipboard.readImage(); - if (img.isEmpty()) return null; + invoke('clipboard:readImage', localOnly, async () => { + const png = await readClipboardPng(); + if (!png) return null; const file = join(tmpdir(), `${PASTE_IMAGE_PREFIX}${randomUUID()}.png`); - try { writeFileSync(file, img.toPNG()); } catch { return null; } + try { writeFileSync(file, png); } catch { return null; } return file; }); /** The same clipboard image, as bytes, so it can be sent to the machine running the session. */ - invoke('clipboard:readImageBytes', localOnly, () => { - const img = clipboard.readImage(); - if (img.isEmpty()) return null; - const png = img.toPNG(); + invoke('clipboard:readImageBytes', localOnly, async () => { + const png = await readClipboardPng(); + if (!png) return null; // Bounded well under the protocol's frame ceiling, base64 expansion included. A screenshot is // ~1MB; anything past this is not a paste, and refusing beats a dropped connection. if (png.length > MAX_PASTE_IMAGE_BYTES) return { tooLarge: true, bytes: null }; diff --git a/src/main/clipboardImage.ts b/src/main/clipboardImage.ts new file mode 100644 index 0000000..6cbdbc0 --- /dev/null +++ b/src/main/clipboardImage.ts @@ -0,0 +1,32 @@ +// src/main/clipboardImage.ts +// The image on the OS clipboard, as PNG bytes — and the reverse. Electron 44 removed +// `clipboard.readImage()` / `writeImage()` in favour of the W3C-shaped `clipboard.read()` / +// `write()` (ClipboardItem + Blob), so the paste-a-screenshot and copy-an-image paths go through here. +import { ClipboardItem, clipboard, nativeImage } from 'electron'; + +/** + * PNG bytes of the image on the clipboard, or null when there is none. A screenshot on Windows is a + * bitmap; Chromium offers it as `image/png` through `read()`. Any other image type is converted. + */ +export async function readClipboardPng(): Promise { + let items: Electron.ClipboardItem[]; + try { items = await clipboard.read(); } catch { return null; } + for (const item of items) { + const type = item.types.includes('image/png') ? 'image/png' : item.types.find((t) => t.startsWith('image/')); + if (!type) continue; + try { + const blob = await item.getType(type) as Blob; + const bytes = Buffer.from(await blob.arrayBuffer()); + if (!bytes.length) continue; + if (type === 'image/png') return bytes; + const img = nativeImage.createFromBuffer(bytes); + if (!img.isEmpty()) return img.toPNG(); + } catch { /* try the next item */ } + } + return null; +} + +/** Put PNG bytes on the clipboard as an image. */ +export async function writeClipboardPng(png: Buffer): Promise { + await clipboard.write([new ClipboardItem({ 'image/png': new Blob([new Uint8Array(png)], { type: 'image/png' }) })]); +} diff --git a/src/main/ipc.guard.test.ts b/src/main/ipc.guard.test.ts index 96e1e02..8cecbb5 100644 --- a/src/main/ipc.guard.test.ts +++ b/src/main/ipc.guard.test.ts @@ -38,7 +38,10 @@ vi.mock('./projectMemory', () => ({ import { registerIpc, type IpcConfig } from './ipc'; import { getGitBranchDirty } from './gitInfo'; -import { dialog, clipboard } from 'electron'; +import { dialog } from 'electron'; +import { readClipboardPng } from './clipboardImage'; +// Electron 44 reads images through the W3C-shaped clipboard.read(); the helper is what the handlers call. +vi.mock('./clipboardImage', () => ({ readClipboardPng: vi.fn(), writeClipboardPng: vi.fn() })); const ALLOWED_ROOT = join(process.cwd(), 'allowed-root'); const sendError = vi.fn(); @@ -245,13 +248,13 @@ describe('settings:addFolder picker handshake', () => { describe('clipboard:readImage (paste a screenshot into the terminal)', () => { it('returns null when the clipboard holds no image', async () => { - vi.mocked(clipboard.readImage).mockReturnValue({ isEmpty: () => true } as never); + vi.mocked(readClipboardPng).mockResolvedValue(null); expect(await handlers.get('clipboard:readImage')!(null)).toBeNull(); }); it('writes the clipboard image to a temp .png under tmpdir and returns its path', async () => { const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG magic bytes - vi.mocked(clipboard.readImage).mockReturnValue({ isEmpty: () => false, toPNG: () => png } as never); + vi.mocked(readClipboardPng).mockResolvedValue(png); const p = (await handlers.get('clipboard:readImage')!(null)) as string; expect(p).toMatch(/devdeck-paste-.+\.png$/); expect(p.startsWith(tmpdir())).toBe(true); diff --git a/src/main/link/linkService.test.ts b/src/main/link/linkService.test.ts index 1cf7462..99b3bb3 100644 --- a/src/main/link/linkService.test.ts +++ b/src/main/link/linkService.test.ts @@ -188,13 +188,13 @@ describe('the clipboard banner', () => { const viewer = makeService('laptop', { machineId: '11111111-2222-4333-8444-555555555555' }); const status = await host.service.createInvite(); clipboardText.value = `여기 코드: ${status.invite!.code}`; - expect(viewer.service.clipboardInvite()).toEqual({ code: status.invite!.code, machineName: 'SIHYEONG-MAIN' }); + expect(await viewer.service.clipboardInvite()).toEqual({ code: status.invite!.code, machineName: 'SIHYEONG-MAIN' }); }); - it('stays quiet for unrelated clipboard contents', () => { + it('stays quiet for unrelated clipboard contents', async () => { const viewer = makeService('laptop'); clipboardText.value = 'git commit -m "wip"'; - expect(viewer.service.clipboardInvite()).toBeNull(); + expect(await viewer.service.clipboardInvite()).toBeNull(); }); it('stays quiet once that machine is already paired — re-adding it would be a confusing no-op', async () => { @@ -205,7 +205,7 @@ describe('the clipboard banner', () => { const second = await host.service.createInvite(); clipboardText.value = second.invite!.code; - expect(viewer.service.clipboardInvite()).toBeNull(); + expect(await viewer.service.clipboardInvite()).toBeNull(); }); }); diff --git a/src/main/link/linkService.ts b/src/main/link/linkService.ts index 43f73a3..d0d9141 100644 --- a/src/main/link/linkService.ts +++ b/src/main/link/linkService.ts @@ -187,7 +187,7 @@ export interface LinkService { revokeDevice(fingerprint: string): void; disconnectDevice(fingerprint: string): void; /** A pasteable code sitting in the clipboard right now, for the "add a machine" banner. */ - clipboardInvite(): { code: string; machineName: string } | null; + clipboardInvite(): Promise<{ code: string; machineName: string } | null>; call(machineId: string, method: string, args: unknown[]): Promise; notify(machineId: string, method: string, args: unknown[]): void; /** @@ -639,11 +639,12 @@ export function createLinkService(options: LinkServiceOptions): LinkService { disconnectDevice(fingerprint) { server?.disconnect(fingerprint); changed(); }, - clipboardInvite() { + async clipboardInvite() { // Read at the moment the screen asks, never stored: the clipboard is a shared surface and its - // contents are none of DevDeck's business a second later. + // contents are none of DevDeck's business a second later. (Async since Electron 44, which + // aligned `clipboard.readText` with the W3C API.) let text = ''; - try { text = clipboard.readText(); } catch { return null; } + try { text = await clipboard.readText(); } catch { return null; } const code = findInviteCodeInText(text); if (!code) return null; const parsed = parseInviteCode(code, now()); From ead3fd938297d494d44f2ab8d3586a3522b4a519 Mon Sep 17 00:00:00 2001 From: Si Hyeong Lee Date: Tue, 27 Oct 2026 12:18:55 -0700 Subject: [PATCH 2/2] qa: image-rows and link harnesses use Electron 44's clipboard API CI's Windows journeys failed on `clipboard.readImage is not a function`: the harnesses saved, checked and restored the clipboard image through the removed readImage()/writeImage(). They now go through read() / write() + ClipboardItem, as the app does. image-rows passes locally on 44.10.0; link.mjs is the local two-instance script (not run in CI). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016NLGwiBY5uEivJqERQzcoY --- qa/image-rows.mjs | 16 +++++++++++----- qa/link.mjs | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/qa/image-rows.mjs b/qa/image-rows.mjs index 37ba8ce..a037e26 100644 --- a/qa/image-rows.mjs +++ b/qa/image-rows.mjs @@ -100,7 +100,13 @@ const errors = []; let failed = false; const check = async (name, fn) => { await fn(); console.log('PASS ' + name); }; // The person's clipboard is put back afterwards: this harness copies an image onto it. -const savedClip = await app.evaluate(({ clipboard }) => ({ text: clipboard.readText(), image: clipboard.readImage().toDataURL() })); +// (Electron 44: the clipboard is read through the W3C-shaped read(); readImage() is gone.) +const savedClip = await app.evaluate(async ({ clipboard }) => { + const items = await clipboard.read().catch(() => []); + const item = items.find((i) => i.types.includes('image/png')); + const image = item ? Buffer.from(await (await item.getType('image/png')).arrayBuffer()).toString('base64') : null; + return { text: await clipboard.readText().catch(() => ''), image }; +}); try { const win = await app.firstWindow(); win.setDefaultTimeout(15000); @@ -185,7 +191,7 @@ try { await win.locator('.ck-img-bar button[aria-label="이미지 복사"]').click(); await win.waitForFunction(() => document.body.textContent.includes('이미지를 클립보드에 복사했습니다')); - assert.equal(await app.evaluate(({ clipboard }) => clipboard.readImage().isEmpty()), false, 'an image is on the clipboard'); + assert.equal(await app.evaluate(async ({ clipboard }) => (await clipboard.read()).some((i) => i.types.includes('image/png'))), true, 'an image is on the clipboard'); await win.locator('.ck-img-bar button[aria-label="터미널에서 위치 보기"]').click(); await win.waitForSelector('.ck-img-viewer', { state: 'detached' }); @@ -220,9 +226,9 @@ try { failed = true; try { const w = await app.firstWindow(); await w.screenshot({ path: join(shots, 'failure.png') }); } catch { /* window gone */ } } finally { - await app.evaluate(({ clipboard, nativeImage }, saved) => { - if (saved.image && saved.image !== 'data:image/png;base64,') clipboard.writeImage(nativeImage.createFromDataURL(saved.image)); - else clipboard.writeText(saved.text); + await app.evaluate(async ({ clipboard, ClipboardItem }, saved) => { + if (saved.image) await clipboard.write([new ClipboardItem({ 'image/png': new Blob([new Uint8Array(Buffer.from(saved.image, 'base64'))], { type: 'image/png' }) })]); + else await clipboard.writeText(saved.text); }, savedClip).catch(() => {}); await closeElectron(app).catch(() => {}); } diff --git a/qa/link.mjs b/qa/link.mjs index 7b6855e..74fe88f 100644 --- a/qa/link.mjs +++ b/qa/link.mjs @@ -322,10 +322,11 @@ try { // key handler is what decides, not the harness. // Watched at the PTY INPUT rather than on screen: what the paste types is sent to the agent, and // whether the agent happens to echo it back is its business, not this feature's. - await viewer.app.evaluate(({ ipcMain, clipboard, nativeImage }, png) => { + await viewer.app.evaluate(async ({ ipcMain, clipboard, ClipboardItem }, png) => { globalThis.__typed = ''; ipcMain.on('cockpit:input', (_e, _id, data) => { globalThis.__typed += String(data); }); - clipboard.writeImage(nativeImage.createFromBuffer(Buffer.from(png, 'base64'))); + // Electron 44: images go on the clipboard through write() + ClipboardItem (writeImage is gone). + await clipboard.write([new ClipboardItem({ 'image/png': new Blob([new Uint8Array(Buffer.from(png, 'base64'))], { type: 'image/png' }) })]); }, await viewer.win.screenshot({ type: 'png' }).then((buf) => buf.toString('base64'))); await viewer.win.evaluate(() => { const box = document.querySelector('.ck-term.show .xterm-helper-textarea');