Skip to content
Closed
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.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).
> **1.47.1** makes closing a Claude Code background session from the sidebar work when Claude Code's background service is not running — DevDeck starts it as Claude Code does and tries again, and a failure names the PC the command has to run on ([release notes](docs/releases/1.47.1.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).

<div align="center">
<img src="docs/screenshots/projects.png" width="820" alt="DevDeck project overview with repository and session navigation" />
Expand Down
11 changes: 11 additions & 0 deletions docs/releases/1.47.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# DevDeck 1.47.1

## Closing a background session works when Claude Code's background service is off

Closing a Claude Code background session from the sidebar (1.46.0) could fail with "Could not close the background session '…' — try claude rm <id> in a terminal", on this PC and on a paired one alike.

- **Cause:** `claude stop` and `claude rm` talk to Claude Code's background service, an on-demand daemon that exits 50 s after its last client — and a Claude Code update leaves none behind. With no service running, Claude Code 2.1.312 answers `couldn't remove <id> — the background service may be restarting. Try again in a moment.` and does not start one. Trying again in a moment does not help; opening `claude agents` once does, because that starts the service. DevDeck's log only said `failed`; the reason was on stderr, which it did not read.
- **Fix:** when `rm` refuses, DevDeck starts the service the way Claude Code itself does (`claude daemon run --origin transient` — it ends itself once idle, so nothing is left running), waits for `claude daemon status` to see it, and runs `stop` and `rm` once more. A session that is already gone counts as closed. What `rm` said is now in the diagnostics log.
- **The message names the right PC.** The failure toast told you to run `claude rm <id>` "in a terminal" even when the session was on another PC, where the command has to run; on this PC it answered `No job matching`. It now names that PC, and says that opening `claude agents` there once starts the service.

Verified on Windows with Claude Code 2.1.312: with the service stopped, `stop` and `rm` on a session waiting for input both refused with the message above; after `claude daemon run` they answered `stopped <id>` and `removed <id>`; the transient service exited on its own 50 s later. A paired PC gets the fix when its DevDeck updates.
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.47.0",
"version": "1.47.1",
"description": "Project command deck — at-a-glance state + Claude Code, Codex, and Antigravity resume",
"main": "dist/main/main.js",
"type": "commonjs",
Expand Down
3 changes: 2 additions & 1 deletion src/main/api/deckApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,8 @@ export function createDeckApi(cfg: DeckApiConfig): DeckApiBundle {
const folders = effFolders();
const listed = (await listBackgroundAgents()).filter((b) => isAllowedFilePath(folders, b.cwd));
const result = await closeBackgroundAgent(id, listed);
cfg.diagnostics?.write(result.ok ? 'info' : 'warn', 'agents', `close background ${String(id).slice(0, 16)}: ${result.ok ? 'removed' : result.error}`);
const how = result.ok ? `removed${result.serviceStarted ? ' (after starting the background service)' : ''}` : `${result.error}${result.detail ? ` (${result.detail})` : ''}`;
cfg.diagnostics?.write(result.ok ? 'info' : 'warn', 'agents', `close background ${String(id).slice(0, 16)}: ${how}`);
return result;
});

Expand Down
73 changes: 73 additions & 0 deletions src/main/backgroundAgents.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { EventEmitter } from 'node:events';
import { afterEach, describe, expect, it, vi } from 'vitest';

const spawn = vi.fn();
vi.mock('node:child_process', () => ({ spawn: (...a: unknown[]) => spawn(...a) }));
// A bare `claude` keeps cliInvocation from wrapping the arguments in cmd.exe on Windows, so the
// tests can read the argv as written.
vi.mock('./cliExecutable', () => ({ resolveAgentCliPath: () => 'claude' }));

type Answer = { code: number; stderr?: string };
/** A `claude` that answers each invocation by its first words, like the real one on the PC. */
function fakeClaude(answer: (args: string[]) => Answer): void {
spawn.mockImplementation((_file: string, args: string[]) => {
const child = Object.assign(new EventEmitter(), { stdout: new EventEmitter(), stderr: new EventEmitter(), pid: 4242, kill: () => true, unref: () => undefined });
const a = answer(args);
setTimeout(() => {
if (a.stderr) child.stderr.emit('data', Buffer.from(a.stderr));
child.emit('close', a.code);
}, 0);
return child;
});
}
const calls = (): string[] => spawn.mock.calls.map((c) => (c[1] as string[]).join(' '));

const LISTED = [{ id: '8e64da40', cwd: 'C:\\g\\shipwright', sessionId: '8e64da40-4c50-4e80-ae72-bddba02e6bcb', name: 'triage', state: 'blocked' as const, startedAt: 1 }];
const SERVICE_DOWN = "couldn't remove 8e64da40 — the background service may be restarting. Try again in a moment.";

describe('listing background sessions while Claude Code is being updated', () => {
afterEach(async () => { const m = await import('./backgroundAgents.js'); await m.pauseBackgroundAgents(false); m._clearBackgroundAgentCache(); spawn.mockReset(); });
Expand All @@ -13,3 +35,54 @@ describe('listing background sessions while Claude Code is being updated', () =>
expect(spawn).not.toHaveBeenCalled();
});
});

describe('closing a background session when the background service is not running', () => {
afterEach(() => { spawn.mockReset(); });

it('stops and removes in one go while the service is up — nothing else is started', async () => {
fakeClaude(() => ({ code: 0 }));
const m = await import('./backgroundAgents.js');
expect(await m.closeBackgroundAgent('8e64da40', LISTED)).toEqual({ ok: true });
expect(calls()).toEqual(['stop 8e64da40', 'rm 8e64da40']);
});

it('starts the service the way Claude Code does and tries again when rm says it may be restarting', async () => {
let serviceUp = false;
fakeClaude((args) => {
if (args[0] === 'daemon' && args[1] === 'run') { serviceUp = true; return { code: 0 }; }
if (args[0] === 'daemon' && args[1] === 'status') return { code: serviceUp ? 0 : 1 };
if (!serviceUp) return { code: 1, stderr: SERVICE_DOWN };
return { code: 0 };
});
const m = await import('./backgroundAgents.js');
expect(await m.closeBackgroundAgent('8e64da40', LISTED, { serviceWaitMs: 200 })).toEqual({ ok: true, serviceStarted: true });
expect(calls()).toEqual(['stop 8e64da40', 'rm 8e64da40', 'daemon run --origin transient', 'daemon status', 'stop 8e64da40', 'rm 8e64da40']);
// The service runs on after DevDeck, and on its own: detached, with no pipes to this process.
const run = spawn.mock.calls.find((c) => (c[1] as string[])[1] === 'run')!;
expect(run[2]).toMatchObject({ detached: true, stdio: 'ignore' });
});

it('reports what rm said when the service never comes up — the message goes to the log, not away', async () => {
fakeClaude((args) => (args[0] === 'daemon' && args[1] === 'run' ? { code: 0 } : { code: 1, stderr: SERVICE_DOWN }));
const m = await import('./backgroundAgents.js');
const result = await m.closeBackgroundAgent('8e64da40', LISTED, { serviceWaitMs: 50 });
expect(result).toMatchObject({ ok: false, error: 'failed' });
expect((result as { detail?: string }).detail).toContain('may be restarting');
expect(calls().filter((c) => c === 'rm 8e64da40')).toHaveLength(1); // no second try without a service
});

it('counts a session that is already gone as closed', async () => {
fakeClaude((args) => (args[0] === 'rm' ? { code: 1, stderr: "No job matching '8e64da40'" } : { code: 1 }));
const m = await import('./backgroundAgents.js');
expect(await m.closeBackgroundAgent('8e64da40', LISTED)).toEqual({ ok: true });
expect(calls()).toEqual(['stop 8e64da40', 'rm 8e64da40']);
});

it('acts only on an id the listing shows, and only on a short hex one', async () => {
fakeClaude(() => ({ code: 0 }));
const m = await import('./backgroundAgents.js');
expect(await m.closeBackgroundAgent('00cf1408', LISTED)).toEqual({ ok: false, error: 'not-listed' });
expect(await m.closeBackgroundAgent('8e64da40; rm -rf /', LISTED)).toEqual({ ok: false, error: 'bad-id' });
expect(spawn).not.toHaveBeenCalled();
});
});
Loading
Loading