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).
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/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');
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