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

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
27 changes: 27 additions & 0 deletions docs/releases/1.43.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# DevDeck 1.43.0

## Parallel sessions in separate workspaces

Running two agents on one project used to mean two agents editing the same files. A session can now start in a **separate workspace**: its own folder and git branch, so it never touches what the project's other sessions are doing.

- **Start one:** use the branch button in a session's header. DevDeck creates a git worktree from the project's current commit and opens a new session in it.
- The worktree lives in `.claude/worktrees/<name>`, the same place Claude Code's own `--worktree` uses.
- Its branch is `devdeck/<name>`.
- **See what it has done:** its header shows how many files it has changed and how many commits it has made.
- **Keep the project's status clean:** the workspace folders are listed in the repository's local `.git/info/exclude`, so they never show up as changes in the project.
- **Clean up:** the bin button in a workspace session's header closes the session and removes the folder and its branch.
- It first says what would be lost, and mentions only what is actually there: uncommitted changes, or commits the starting branch does not have.
- If there is unmerged work, it suggests asking a session on the starting branch to merge it first.
- Nothing is deleted without that confirmation.
- **On another PC:** this works for sessions on another PC as well. Creating or removing a workspace there needs the same permission as starting a session. A PC still on an older DevDeck says it needs updating.

The What's new window lists this, with Show me, and the button carries a NEW dot until you first use it.

## Under the hood

- **Folder permissions:** a project registered as a single folder admits its own workspaces, `<repo>/.claude/worktrees/<name>`, exactly one level down. Nothing else inside the repository is admitted.
- **What can be removed:** only a folder DevDeck created as a workspace, on a branch it named.
- **Windows folder leftovers:** git can drop a workspace from its list while Windows still holds the folder for a moment. DevDeck then finishes removing that one folder itself.
- **Tests:**
- New tests run against real temporary git repositories: creating, counting changes, refusing to drop unmerged work, and cleaning up after a half-removed workspace.
- `qa:cockpit-tools` creates a workspace from the header, sees its change counted, and cleans it up through the warning.
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.42.0",
"version": "1.43.0",
"description": "Project command deck — at-a-glance state + Claude Code, Codex, and Antigravity resume",
"main": "dist/main/main.js",
"type": "commonjs",
Expand Down
36 changes: 35 additions & 1 deletion qa/cockpit-tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Run with npm run qa:cockpit-tools.
import { _electron as electron } from 'playwright';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
Expand All @@ -24,6 +24,10 @@ const profile = join(sandbox, 'profile');
const home = join(profile, 'home');
for (const p of [repo, bin, home]) mkdirSync(p, { recursive: true });
execFileSync('git', ['init', '-q', '-b', 'qa-tools'], { cwd: repo });
// A first commit, so a separate workspace (a git worktree) can be started from it.
writeFileSync(join(repo, 'README.md'), '# tools\n');
execFileSync('git', ['-c', 'user.email=qa@example.invalid', '-c', 'user.name=QA', 'add', '.'], { cwd: repo });
execFileSync('git', ['-c', 'user.email=qa@example.invalid', '-c', 'user.name=QA', 'commit', '-q', '-m', 'first'], { cwd: repo });

const sid = '7a1c2b3d-2222-4333-8444-955566667777';
const tdir = join(home, '.claude', 'projects', repo.replace(/[^a-zA-Z0-9]/g, '-'));
Expand Down Expand Up @@ -229,6 +233,36 @@ try {
rmSync(path, { force: true });
});

await check('a separate workspace: its own branch and folder, its changes in the header, and a clean-up that warns', async () => {
const before = await win.locator('#shell-session-groups .shell-session').count();
await win.locator('#ck-header .ck-wt-new').click();
await win.waitForFunction((n) => document.querySelectorAll('#shell-session-groups .shell-session').length === n + 1, before, { timeout: 20000 });
await win.waitForSelector('#ck-header .ck-wt-pill');
const wtDir = join(repo, '.claude', 'worktrees');
const [name] = readdirSync(wtDir);
const wt = join(wtDir, name);
assert.equal(execFileSync('git', ['-C', wt, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).trim(), 'devdeck/' + name);
// The project itself does not see the workspace as a change.
assert.equal(execFileSync('git', ['-C', repo, 'status', '--porcelain'], { encoding: 'utf8' }).trim(), '');
// Work done in the workspace shows up in its header.
writeFileSync(join(wt, 'feature.txt'), 'new work\n');
await win.locator('#shell-session-groups .shell-session').first().click();
await win.locator('#shell-session-groups .shell-session', { hasText: name }).click();
await win.waitForFunction(() => document.querySelector('#ck-header .ck-wt-pill')?.textContent?.includes('파일 1 '), null, { timeout: 15000 });
await win.screenshot({ path: join(shots, '4-workspace.png') });
// Cleaning up says what would be lost before anything is.
await win.locator('#ck-header .ck-wt-clean').click();
await win.waitForSelector('.ck-confirm-msg');
const confirmText = await win.locator('.ck-confirm-msg').textContent();
assert.match(confirmText, /커밋하지 않은 변경 1개/);
assert.doesNotMatch(confirmText, /커밋 0개/, 'nothing that is not there is mentioned');
await win.screenshot({ path: join(shots, '4-workspace-cleanup.png') });
await win.locator('.ck-confirm-ok').click();
for (const until = Date.now() + 15000; existsSync(wt) && Date.now() < until;) await new Promise((r) => setTimeout(r, 250));
assert.equal(existsSync(wt), false, 'the workspace folder is gone');
assert.equal(execFileSync('git', ['-C', repo, 'branch', '--list', 'devdeck/' + name], { encoding: 'utf8' }).trim(), '', 'and its branch');
});

await check('a document sent from another machine lands in the temp directory under its own name', async () => {
const landed = await win.evaluate(() => window.devdeck.cockpit.receiveFile('build.log', btoa('step 1 ok\nstep 2 failed\n')));
assert.match(landed, /devdeck-file-[0-9a-f-]{36}-build\.log$/);
Expand Down
3 changes: 3 additions & 0 deletions qa/perf-gate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,6 @@ try {
await closeElectron(app).catch(() => {});
}
if (failures.length) { console.log(`\nFAIL: ${failures.join(', ')}`); process.exitCode = 1; } else console.log('\nPASS: performance gate');
// Exit explicitly: on a Windows runner a handle left by the closed app once kept node alive after
// the verdict was printed, until the job hit its 20-minute limit and the v1.42.0 release was cancelled.
process.exit(process.exitCode ?? 0);
12 changes: 11 additions & 1 deletion src/main/api/deckApi.surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ beforeAll(() => {
getTrayAlert: () => 'attention',
getShutdownIdleMinutes: () => 10,
getSessionSummary: () => true,
getAiSessionSummary: () => false, getProgramStatus: () => false,
getAiSessionSummary: () => false, getProgramStatus: () => false, getWorktrees: () => [],
},
sendError: vi.fn(),
defaultLanguage: 'en',
Expand Down Expand Up @@ -345,3 +345,13 @@ it('allows revision-checked task saves only to write-authorized devices', () =>
expect(() => api['project:setTodos'].handler(ALLOWED_ROOT, [])).toThrow('TASKS_CLIENT_OUTDATED');
return expect(Promise.resolve().then(() => api['project:saveTodos'].handler(join(process.cwd(), 'outside'), [], 0))).rejects.toThrow('allowed folders');
});

it('lets a paired device create or remove a workspace only with spawn, and look at one as an observer', async () => {
expect(mayCallRemotely(api['cockpit:createWorktree'], ['observe'])).toBe(false);
expect(mayCallRemotely(api['cockpit:createWorktree'], ['spawn'])).toBe(true);
expect(mayCallRemotely(api['cockpit:removeWorktree'], ['control'])).toBe(false);
expect(mayCallRemotely(api['cockpit:worktreeInfo'], ['observe'])).toBe(true);
await expect(api['cockpit:createWorktree'].handler(join(process.cwd(), 'outside'))).resolves.toEqual({ error: 'Path outside allowed folders' });
// A folder DevDeck did not make is not one it will remove.
await expect(api['cockpit:removeWorktree'].handler(join(process.cwd(), 'outside'), true)).resolves.toEqual({ removed: false, reason: 'unknown' });
});
Loading
Loading