Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ See what needs attention, resume the right conversation, and work on a paired PC

</div>

> **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.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).

<div align="center">
<img src="docs/screenshots/projects.png" width="820" alt="DevDeck project overview with repository and session navigation" />
Expand Down
2 changes: 1 addition & 1 deletion docs/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Results and screenshots are written to `qa/shots/maintenance/` and `qa/shots/res

The CI performance gate is `qa:perf-gate` (Windows, every push): a fixture agent instead of a real one, and units that do not depend on the runner's speed — running CSS animations and DOM mutation records on an idle deck (both must be ~0), DOM mutations outside the terminals while two tiles repaint at 10fps (a per-chunk sidebar rebuild is ~8,000 against a limit of 400), and the main process's longest event-loop stall while working and while 20,000 lines stream. Each limit was checked by injecting the regression it guards against: an infinite CSS animation, a sidebar render per PTY chunk, and a 700ms synchronous block in the main process all fail it. Results go to `qa/shots/perf-gate/results.json`.

`qa:images` (Windows) drives the cockpit image strip, full-size viewer and 📎 attach against an isolated home holding a transcript with a tool screenshot, a pasted image and a written image file; its fixture `claude` echoes what it is sent, so an attached path is read back off the terminal. `qa:update` (Windows) checks "Update all": a fixture `claude` prints Claude Code's failed-update footer, and the harness asserts that `claude update` ran with no session process alive and that every conversation was resumed by id. `qa:images-remote` (Windows) pairs a host and a viewer over real TLS and checks the same from the viewer: thumbnails and the full view cross the link, an attached JPEG lands on the host as PNG with the host path typed, and a non-image named `.png` or an unindexed id is refused over the link. `qa:restore-pacing` (Windows) checks that restored agents start one at a time: a fixture that draws for 2s while booting must not see the next one launched before it finished (measured before the fix: 26–49ms apart). `qa:image-rows` (Windows) ties images to the terminal: a fixture `claude` draws a screen the way Claude Code does (`● server - tool (MCP)` calls, `⎿ [Image]` results, shaded `❯` prompts, a composer between rules) over a transcript holding the same screenshots, and the harness checks that each placeholder is marked and opens its own image, that the viewer shows what led to an image and scrolls the terminal to its row, that copying puts an image on the clipboard (the person's clipboard is restored afterwards), that a new screenshot is previewed while the strip is closed, and that Ctrl+Up/Down steps between sent prompts without entering the composer. `qa:cockpit-tools` (Windows) uses a fixture `claude` that records every byte typed into it: a quick reply from the header menu and from Ctrl+Shift+<n> arrives as the phrase followed by Enter, phrases edited in Settings are saved and offered, the timeline lists each prompt with its time, reply and image count and scrolls the terminal to it without resizing it, and a screenshot marked up in the viewer reaches the session as a PNG that has the red marks in it. It also starts as an install updated from 1.41.0 with the Claude status signal on: the What's new window lists only what is newer and Show me rings the setting, NEW dots go once a button is used, and the fixture asks for status the way Claude Code does (OSC 7501) — the host answers on the bundled ConPTY and the header shows working, needs approval and done. It also starts a separate workspace from the header, checks its branch and that the project stays clean, sees a change counted, and cleans it up through the warning. Its fixture also answers `claude agents --json`, so the harness checks the Claude background sessions group, the waiting mark, and that a click opens the session with `claude attach <id>`. The release workflow runs it against the packaged app too. `qa:images-remote` also sends a log file from the viewer and checks it lands on the host under its own name. None of these starts a real agent.
`qa:images` (Windows) drives the cockpit image strip, full-size viewer and 📎 attach against an isolated home holding a transcript with a tool screenshot, a pasted image and a written image file; its fixture `claude` echoes what it is sent, so an attached path is read back off the terminal. `qa:update` (Windows) checks "Update all": a fixture `claude` prints Claude Code's failed-update footer, and the harness asserts that `claude update` ran with no session process alive and that every conversation was resumed by id. `qa:images-remote` (Windows) pairs a host and a viewer over real TLS and checks the same from the viewer: thumbnails and the full view cross the link, an attached JPEG lands on the host as PNG with the host path typed, and a non-image named `.png` or an unindexed id is refused over the link. `qa:restore-pacing` (Windows) checks that restored agents start one at a time: a fixture that draws for 2s while booting must not see the next one launched before it finished (measured before the fix: 26–49ms apart). `qa:image-rows` (Windows) ties images to the terminal: a fixture `claude` draws a screen the way Claude Code does (`● server - tool (MCP)` calls, `⎿ [Image]` results, shaded `❯` prompts, a composer between rules) over a transcript holding the same screenshots, and the harness checks that each placeholder is marked and opens its own image, that the viewer shows what led to an image and scrolls the terminal to its row, that copying puts an image on the clipboard (the person's clipboard is restored afterwards), that a new screenshot is previewed while the strip is closed, and that Ctrl+Up/Down steps between sent prompts without entering the composer. `qa:cockpit-tools` (Windows) uses a fixture `claude` that records every byte typed into it: a quick reply from the header menu and from Ctrl+Shift+<n> arrives as the phrase followed by Enter, phrases edited in Settings are saved and offered, the timeline lists each prompt with its time, reply and image count and scrolls the terminal to it without resizing it, and a screenshot marked up in the viewer reaches the session as a PNG that has the red marks in it. It also starts as an install updated from 1.41.0 with the Claude status signal on: the What's new window lists only what is newer and Show me rings the setting, NEW dots go once a button is used, and the fixture asks for status the way Claude Code does (OSC 7501) — the host answers on the bundled ConPTY and the header shows working, needs approval and done. It also starts a separate workspace from the header, checks its branch and that the project stays clean, sees a change counted, and cleans it up through the warning. Its fixture also answers `claude agents --json`, so the harness checks the Claude background sessions group, the waiting mark, and that a click opens the session with `claude attach <id>`. The release workflow runs it against the packaged app too. `qa:images-remote` also sends a log file from the viewer and checks it lands on the host under its own name. It also sends a 9 MB binary, which is past the old limit and of a kind the old list refused, and checks that it arrives in pieces, byte for byte, with its progress shown. None of these starts a real agent.

Do not run these against a live development conversation. Do not equate a short loopback test with sleep/resume, VPN changes or multi-day field validation.

Expand Down
29 changes: 29 additions & 0 deletions docs/releases/1.45.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# DevDeck 1.45.0

## Send any file to a session on another PC

Before, a session on a paired PC took images and 21 kinds of documents (text, data, logs, PDF) up to 5 MB each. A zip, a spreadsheet, a source file or a video was refused.

- **Any kind:** zip, xlsx, docx, source files, audio, video, binaries.
- **Up to 4 GB per file:** large files are sent in 2 MB pieces. A file over 8 MB shows its progress while it goes.
- **Drop it or use 📎:** both work on a remote session. The 📎 picker no longer filters by kind. The path where the file landed on the other PC is typed into the session, as before.
- **Images:** they still go as PNG, so the agent can look at them. An image too large to redraw as PNG within 5 MB now goes as it is, instead of being refused.
- **The other PC needs 1.45.0.** An older DevDeck there still takes what it did before (the 21 document kinds, up to 5 MB), and DevDeck says when something was skipped for that reason.

## What is still checked, on the PC receiving the file

Sending a file needs the same permission as typing into its sessions. A device that can type into a session can already have the agent there run anything, so checking the file's kind only got in the way. What is kept:

- **Where it goes:** the file is written only into the temp folder, under a name DevDeck chooses. It is never written to a path the sender picks. DevDeck removes it after a day.
- **Never opened:** DevDeck does not open or run a received file.
- **Marked as downloaded:** on Windows the file is marked as coming from the internet (Mark of the Web), so opening it from Explorer shows the usual warning first.
- **Disk space:** a file is refused if writing it would leave less than 2 GB free on that disk.
- **Limits:** at most 4 uploads are open at once. An upload with nothing new for 5 minutes is dropped and its partial file is deleted.

## Under the hood

- **New methods:** `cockpit:uploadStart`, `uploadChunk`, `uploadFinish` and `uploadAbort`, with the `control` permission. `cockpit:receiveFile` is kept for senders older than 1.45.0.
- **Tests:**
- On the receiving side, pieces are written in order. A piece that is out of order, too large or past the announced size is refused. A file with bytes missing never lands under its name. The size, disk-space and open-upload limits hold, and the Windows Mark of the Web is written.
- On the sending side, a file is split and reported in order. An empty file is sent. A refused piece abandons the upload. An older PC is told apart from a full disk.
- `qa:images-remote` sends a 9 MB binary between two real paired DevDecks and checks that it arrives byte for byte, with its progress shown.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "devdeck",
"version": "1.44.1",
"version": "1.45.0",
"description": "Project command deck — at-a-glance state + Claude Code, Codex, and Antigravity resume",
"main": "dist/main/main.js",
"type": "commonjs",
Expand Down
20 changes: 18 additions & 2 deletions qa/images-remote.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ try {
}, [join(root, 'build', 'icon.png'), jpg]);
});
const chooser = viewer.win.waitForEvent('filechooser');
await viewer.win.locator('#ck-header .ck-act').filter({ has: viewer.win.locator('svg') }).and(viewer.win.locator('[title*="이미지·문서 첨부"]')).click();
await viewer.win.locator('#ck-header .ck-act').filter({ has: viewer.win.locator('svg') }).and(viewer.win.locator('[title*="파일 첨부"]')).click();
await (await chooser).setFiles(jpg);
await rowsInclude(viewer.win, 'devdeck-paste-');
const typed = await host.win.evaluate(() => [...document.querySelectorAll('.ck-term.show .xterm-rows')].map((r) => r.textContent).join('\n'));
Expand All @@ -124,7 +124,7 @@ try {
const log = join(sandbox, 'build.log');
writeFileSync(log, 'step 1 ok\nstep 2 failed\n');
const chooser = viewer.win.waitForEvent('filechooser');
await viewer.win.locator('#ck-header .ck-act[title*="이미지·문서 첨부"]').click();
await viewer.win.locator('#ck-header .ck-act[title*="파일 첨부"]').click();
await (await chooser).setFiles(log);
await rowsInclude(viewer.win, 'build.log');
const typed = await host.win.evaluate(() => [...document.querySelectorAll('.ck-term.show .xterm-rows')].map((r) => r.textContent).join('\n'));
Expand All @@ -133,6 +133,22 @@ try {
assert.equal(readFileSync(m[1], 'utf8'), 'step 1 ok\nstep 2 failed\n');
});

await check('any kind of file past the old 5MB limit crosses in pieces, byte for byte, with its progress shown', async () => {
const big = join(sandbox, 'dataset.bin');
const bytes = Buffer.alloc(9 * 1024 * 1024 + 17); for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 31) % 256;
writeFileSync(big, bytes);
const chooser = viewer.win.waitForEvent('filechooser');
await viewer.win.locator('#ck-header .ck-act[title*="파일 첨부"]').click();
await (await chooser).setFiles(big);
await viewer.win.waitForFunction(() => [...document.querySelectorAll('#toast-host .toast')].some((t) => /dataset\.bin.*%/.test(t.textContent)), null, { timeout: 15000 });
await rowsInclude(viewer.win, 'dataset.bin');
const typed = await host.win.evaluate(() => [...document.querySelectorAll('.ck-term.show .xterm-rows')].map((r) => r.textContent).join('\n'));
const m = /GOT:(\S*devdeck-file-[0-9a-f-]{36}-dataset\.bin)/.exec(typed.replace(/\n/g, ''));
assert.ok(m, 'host typed the sent file path:\n' + typed);
assert.ok(readFileSync(m[1]).equals(bytes), 'the file on the host is the same bytes');
rmSync(m[1], { force: true });
});

await check('over the link, a real image is served and a fake .png or unindexed id is refused', async () => {
const r = await viewer.win.evaluate(async ([id, path]) => {
const m = window.devdeck.machine(id).cockpit;
Expand Down
18 changes: 18 additions & 0 deletions src/main/api/deckApi.surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,24 @@ describe('receiving a document from another machine', () => {
});
});

describe('receiving a file of any kind in pieces', () => {
it('needs control at every step, like a document', () => {
for (const m of ['cockpit:uploadStart', 'cockpit:uploadChunk', 'cockpit:uploadFinish', 'cockpit:uploadAbort']) {
expect(mayCallRemotely(api[m], ['observe']), m).toBe(false);
expect(mayCallRemotely(api[m], ['control']), m).toBe(true);
}
});

it('lands a kind receiveFile refuses in the temp directory, under its own name', async () => {
const start = await api['cockpit:uploadStart'].handler('tool.exe', 2) as { id: string };
expect(await api['cockpit:uploadChunk'].handler(start.id, 0, Buffer.from('MZ').toString('base64'))).toBe(2);
const written = await api['cockpit:uploadFinish'].handler(start.id) as string;
expect(dirname(written)).toBe(tmpdir());
expect(basename(written)).toMatch(/^devdeck-file-[0-9a-f-]{36}-tool\.exe$/);
rmSync(written);
});
});

it('reads a session timeline only for an allowed project, as an observer may', async () => {
expect(mayCallRemotely(api['cockpit:sessionTurns'], ['observe'])).toBe(true);
await expect(api['cockpit:sessionTurns'].handler(join(process.cwd(), 'outside'), '00000000-0000-0000-0000-000000000000', 'claude')).resolves.toEqual([]);
Expand Down
Loading
Loading