From 5b916f6faec21a244407f202d3e7c22c2bc08ed1 Mon Sep 17 00:00:00 2001 From: Si Hyeong Lee Date: Tue, 27 Oct 2026 14:46:07 -0700 Subject: [PATCH] 1.47.1: close a background session when Claude Code's background service is off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `claude stop` and `claude rm` talk to Claude Code's on-demand background service, which exits 50 s after its last client and is absent after a Claude Code update. Without it, Claude Code 2.1.312 answers "couldn't remove — the background service may be restarting. Try again in a moment." and does not start one, so closing a background session from the sidebar failed — on this PC and on a paired one — and the log said only "failed". - closeBackgroundAgent: when rm refuses, start the service the way Claude Code does (`claude daemon run --origin transient`, which exits itself once idle), wait for `claude daemon status`, and run stop + rm once more. A session that is already gone counts as closed. What rm said on stderr goes to the log. - The failure toast names the PC the command has to run on; before, it sent the user to run `claude rm` on this PC for a session on another one. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LdbKDr2tTfCLAZv7WbV8kX --- README.md | 2 +- docs/releases/1.47.1.md | 11 ++++ package-lock.json | 4 +- package.json | 2 +- src/main/api/deckApi.ts | 3 +- src/main/backgroundAgents.test.ts | 73 ++++++++++++++++++++++++ src/main/backgroundAgents.ts | 92 +++++++++++++++++++++++++++---- src/renderer/cockpitView.ts | 5 +- src/renderer/locales/en.json | 3 +- src/renderer/locales/ja.json | 3 +- src/renderer/locales/ko.json | 3 +- src/renderer/locales/zh.json | 3 +- 12 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 docs/releases/1.47.1.md diff --git a/README.md b/README.md index 29db39b..7afe853 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.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).
DevDeck project overview with repository and session navigation diff --git a/docs/releases/1.47.1.md b/docs/releases/1.47.1.md new file mode 100644 index 0000000..df3f815 --- /dev/null +++ b/docs/releases/1.47.1.md @@ -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 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 — 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 ` "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 ` and `removed `; the transient service exited on its own 50 s later. A paired PC gets the fix when its DevDeck updates. diff --git a/package-lock.json b/package-lock.json index 9e28bcc..a33d9d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "devdeck", - "version": "1.47.0", + "version": "1.47.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "devdeck", - "version": "1.47.0", + "version": "1.47.1", "license": "MIT", "dependencies": { "@fontsource-variable/geist": "5.3.0", diff --git a/package.json b/package.json index 416aa17..6d33778 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/api/deckApi.ts b/src/main/api/deckApi.ts index 01091ce..30ee3b8 100644 --- a/src/main/api/deckApi.ts +++ b/src/main/api/deckApi.ts @@ -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; }); diff --git a/src/main/backgroundAgents.test.ts b/src/main/backgroundAgents.test.ts index 34be5d4..2873070 100644 --- a/src/main/backgroundAgents.test.ts +++ b/src/main/backgroundAgents.test.ts @@ -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(); }); @@ -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(); + }); +}); diff --git a/src/main/backgroundAgents.ts b/src/main/backgroundAgents.ts index 31bd6bb..1984e3a 100644 --- a/src/main/backgroundAgents.ts +++ b/src/main/backgroundAgents.ts @@ -48,7 +48,9 @@ function run(): Promise { return runClaude(['agents', '--json', '--all'], TIMEOUT_MS, (child) => { current = child; }); } -export type BackgroundCloseResult = { ok: true } | { ok: false; error: 'bad-id' | 'not-listed' | 'failed' }; +export type BackgroundCloseResult = + | { ok: true; serviceStarted?: boolean } + | { ok: false; error: 'bad-id' | 'not-listed' | 'failed'; detail?: string }; /** * Close one of Claude Code's background sessions: `claude stop ` (ends it if it is still running; @@ -60,34 +62,100 @@ export type BackgroundCloseResult = { ok: true } | { ok: false; error: 'bad-id' * (Claude Code 2.1.311): `stop` prints "stopped ", `rm` prints "removed " and the * `~/.claude/jobs/` folder is gone. * + * Both commands talk to Claude Code's background service (its on-demand daemon). When it is not + * running — the usual state: it exits 50 s after its last client, and a Claude Code update leaves + * none behind — `stop` and `rm` can answer "couldn't remove — the background service may be + * restarting. Try again in a moment." without starting one (seen on Claude Code 2.1.312, on this + * PC and on a paired one). Trying again in a moment does not help; opening `claude agents` once + * does, because that starts the service. So when `rm` refuses, DevDeck starts the service the way + * Claude Code itself does (`claude daemon run --origin transient`, which exits again on its own + * once idle) and runs the two commands once more. + * * Only an id the listing currently shows is acted on: the id is the one thing put on the command * line, and this keeps a remote deck from naming sessions it was never shown. */ -export async function closeBackgroundAgent(id: unknown, listed: readonly BackgroundAgent[]): Promise { +export async function closeBackgroundAgent(id: unknown, listed: readonly BackgroundAgent[], opts: { serviceWaitMs?: number } = {}): Promise { if (!isBackgroundAgentId(id)) return { ok: false, error: 'bad-id' }; if (!listed.some((b) => b.id === id)) return { ok: false, error: 'not-listed' }; - await runClaude(['stop', id], CLOSE_TIMEOUT_MS).catch(() => ''); // already exited → nothing to stop - const removed = await runClaude(['rm', id], CLOSE_TIMEOUT_MS).then(() => true, () => false); + let refusal = await stopAndRemove(id); + let serviceStarted = false; + if (refusal !== null && await startBackgroundService(opts.serviceWaitMs)) { + serviceStarted = true; + refusal = await stopAndRemove(id); + } cached = null; // the next listing must show it gone (or still there, if rm refused) - return removed ? { ok: true } : { ok: false, error: 'failed' }; + if (refusal !== null) return { ok: false, error: 'failed', detail: refusal }; + return serviceStarted ? { ok: true, serviceStarted } : { ok: true }; +} + +/** `claude stop` then `claude rm`; null once the session is gone, else what `rm` answered. */ +async function stopAndRemove(id: string): Promise { + await runClaude(['stop', id], CLOSE_TIMEOUT_MS).catch(() => ''); // already exited → nothing to stop + return runClaude(['rm', id], CLOSE_TIMEOUT_MS).then( + () => null, + // Listed a moment ago and gone now: that is the outcome asked for. + (e: Error) => (/no job matching/i.test(e.message) ? null : e.message), + ); +} + +/** + * Start Claude Code's background service as Claude Code does on demand, and wait until `claude + * daemon status` sees it (about a second; a few more on a busy PC). The transient service ends + * itself 50 s after its last client, so nothing is left running for DevDeck to clean up. False + * when it did not come up in time — the close is then reported as refused, with the reason. + */ +async function startBackgroundService(waitMs = SERVICE_WAIT_MS): Promise { + const how = cliInvocation(resolveAgentCliPath('claude'), ['daemon', 'run', '--origin', 'transient']); + try { + const child = spawn(how.file, how.args, { cwd: homedir(), env: claudeEnv(), detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: how.windowsVerbatimArguments }); + child.on('error', () => undefined); + child.unref(); + } catch { + return false; + } + const deadline = Date.now() + waitMs; + for (;;) { + if (await runClaude(['daemon', 'status'], SERVICE_STATUS_TIMEOUT_MS).then(() => true, () => false)) return true; + const left = deadline - Date.now(); + if (left <= 0) return false; + await new Promise((r) => setTimeout(r, Math.min(SERVICE_POLL_MS, left))); + } } const CLOSE_TIMEOUT_MS = 20_000; +const SERVICE_WAIT_MS = 10_000; +const SERVICE_POLL_MS = 500; +const SERVICE_STATUS_TIMEOUT_MS = 5_000; + +/** This process's environment without Claude Code's own session variables. */ +function claudeEnv(): NodeJS.ProcessEnv { + const env = { ...process.env }; + // A DevDeck started from inside Claude Code must not make this look like a nested session. + for (const k of Object.keys(env)) if (/^CLAUDE/i.test(k)) delete env[k]; + return env; +} -/** Run `claude ` detached from this session's own Claude environment; resolves to stdout on exit 0. */ +/** + * Run `claude ` detached from this session's own Claude environment; resolves to stdout on + * exit 0. On any other exit the error carries what the command said on stderr — that is where + * `rm` explains a refusal, and the diagnostics log is where it has to end up. + */ function runClaude(args: string[], timeoutMs: number, started?: (child: ChildProcess) => void): Promise { return new Promise((resolve, reject) => { - const env = { ...process.env }; - // A DevDeck started from inside Claude Code must not make this look like a nested session. - for (const k of Object.keys(env)) if (/^CLAUDE/i.test(k)) delete env[k]; const how = cliInvocation(resolveAgentCliPath('claude'), args); - const child = spawn(how.file, how.args, { cwd: homedir(), env, windowsHide: true, windowsVerbatimArguments: how.windowsVerbatimArguments, stdio: ['ignore', 'pipe', 'ignore'] }); + const child = spawn(how.file, how.args, { cwd: homedir(), env: claudeEnv(), windowsHide: true, windowsVerbatimArguments: how.windowsVerbatimArguments, stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; + let err = ''; const timer = setTimeout(() => { killChildTree(child); reject(new Error(`claude ${args[0]} timed out`)); }, timeoutMs); started?.(child); child.on('close', () => { if (current === child) current = null; }); - child.stdout.on('data', (b: Buffer) => { if (out.length < 2_000_000) out += b.toString('utf8'); }); + child.stdout?.on('data', (b: Buffer) => { if (out.length < 2_000_000) out += b.toString('utf8'); }); + child.stderr?.on('data', (b: Buffer) => { if (err.length < 4_000) err += b.toString('utf8'); }); child.on('error', (e) => { clearTimeout(timer); reject(e); }); - child.on('close', (code) => { clearTimeout(timer); if (code === 0) resolve(out); else reject(new Error(`claude ${args[0]} exited ${code}`)); }); + child.on('close', (code) => { + clearTimeout(timer); + if (code === 0) resolve(out); + else reject(new Error(`claude ${args[0]} exited ${code}${err.trim() ? `: ${err.trim().replace(/\s+/g, ' ').slice(0, 300)}` : ''}`)); + }); }); } diff --git a/src/renderer/cockpitView.ts b/src/renderer/cockpitView.ts index aac3fad..df6d983 100644 --- a/src/renderer/cockpitView.ts +++ b/src/renderer/cockpitView.ts @@ -668,8 +668,11 @@ async function closeBackgroundAgents(ids: readonly string[], ask: boolean): Prom toast(tr('cockpit.bg_closed', { name: nameOf(agent) })); } else if (result.error === 'host-too-old') { toast(tr('cockpit.bg_close_old_host', { machine: machineName(machineId) })); - } else { + } else if (machineId === LOCAL_MACHINE_ID) { toast(tr('cockpit.bg_close_failed', { name: nameOf(agent), id: agent.id })); + } else { + // The command has to run on the PC that has the session — the one named here, not this one. + toast(tr('cockpit.bg_close_failed_on_machine', { name: nameOf(agent), id: agent.id, machine: machineName(machineId) })); } } publishCockpitNavigation(); diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 1cdd133..a0fdaa7 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -646,7 +646,8 @@ "cockpit.bg_state_failed": "failed", "cockpit.bg_close_confirm": "Close the background session '{name}'? It is removed from Claude Code's list. Its conversation is kept and can be reopened later.", "cockpit.bg_closed": "Closed the background session '{name}' — its conversation is kept", - "cockpit.bg_close_failed": "Could not close the background session '{name}' — try claude rm {id} in a terminal", + "cockpit.bg_close_failed": "Could not close the background session '{name}' — open claude agents once in a terminal on this PC (it starts Claude Code's background service), then try again, or run claude rm {id} there", + "cockpit.bg_close_failed_on_machine": "Could not close the background session '{name}' on {machine} — open claude agents once in a terminal on {machine} (it starts Claude Code's background service), then try again, or run claude rm {id} there", "cockpit.bg_close_old_host": "DevDeck on {machine} is too old to close a background session — update it there", "whatsnew.f.bg-close.title": "Close background sessions", "whatsnew.f.bg-close.body": "A Claude Code background session (the sidebar's Background group) can now be closed from its row's ⋯ menu or with the group's bulk close. Finished or failed sessions no longer sit in the list forever. The conversation is kept.", diff --git a/src/renderer/locales/ja.json b/src/renderer/locales/ja.json index 0cba735..5c74546 100644 --- a/src/renderer/locales/ja.json +++ b/src/renderer/locales/ja.json @@ -646,7 +646,8 @@ "cockpit.bg_state_failed": "失敗", "cockpit.bg_close_confirm": "バックグラウンドセッション '{name}' を閉じますか? Claude Code の一覧から削除されます。会話の記録は残り、後で開き直せます。", "cockpit.bg_closed": "バックグラウンドセッション '{name}' を閉じました — 会話の記録は残っています", - "cockpit.bg_close_failed": "バックグラウンドセッション '{name}' を閉じられませんでした — ターミナルで claude rm {id} を試してください", + "cockpit.bg_close_failed": "バックグラウンドセッション '{name}' を閉じられませんでした — この PC のターミナルで claude agents を一度開いてから(Claude Code のバックグラウンドサービスが起動します)もう一度試すか、そこで claude rm {id} を実行してください", + "cockpit.bg_close_failed_on_machine": "{machine} のバックグラウンドセッション '{name}' を閉じられませんでした — {machine} のターミナルで claude agents を一度開いてから(Claude Code のバックグラウンドサービスが起動します)もう一度試すか、そこで claude rm {id} を実行してください", "cockpit.bg_close_old_host": "{machine} の DevDeck が古いためバックグラウンドセッションを閉じられません — そちらを更新してください", "whatsnew.f.bg-close.title": "バックグラウンドセッションを閉じる", "whatsnew.f.bg-close.body": "Claude Code のバックグラウンドセッション(サイドバーの「バックグラウンド」グループ)を、行の ⋯ メニューやグループの一括クローズで閉じられます。終了・失敗したセッションが一覧に残り続ける問題が解消されます。会話の記録は残ります。", diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index 493b8ed..2093bcf 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -646,7 +646,8 @@ "cockpit.bg_state_failed": "실패", "cockpit.bg_close_confirm": "'{name}' 백그라운드 세션을 닫을까요? Claude Code 목록에서 제거됩니다. 대화 기록은 남아 있어 나중에 다시 열 수 있습니다.", "cockpit.bg_closed": "'{name}' 백그라운드 세션을 닫았습니다 — 대화 기록은 남아 있습니다", - "cockpit.bg_close_failed": "'{name}' 백그라운드 세션을 닫지 못했습니다 — 터미널에서 claude rm {id} 를 실행해 보세요", + "cockpit.bg_close_failed": "'{name}' 백그라운드 세션을 닫지 못했습니다 — 이 PC의 터미널에서 claude agents 를 한 번 연 뒤(Claude Code 백그라운드 서비스가 시작됩니다) 다시 시도하거나, 거기서 claude rm {id} 를 실행해 보세요", + "cockpit.bg_close_failed_on_machine": "{machine}의 '{name}' 백그라운드 세션을 닫지 못했습니다 — {machine}의 터미널에서 claude agents 를 한 번 연 뒤(Claude Code 백그라운드 서비스가 시작됩니다) 다시 시도하거나, 거기서 claude rm {id} 를 실행해 보세요", "cockpit.bg_close_old_host": "{machine}의 DevDeck이 오래되어 백그라운드 세션을 닫을 수 없습니다 — 그쪽을 업데이트해 주세요", "whatsnew.f.bg-close.title": "백그라운드 세션 닫기", "whatsnew.f.bg-close.body": "Claude Code 백그라운드 세션(사이드바 「백그라운드」 그룹)을 행의 ⋯ 메뉴나 그룹 묶음 닫기로 닫을 수 있습니다. 끝났거나 실패한 세션이 목록에 영원히 남던 문제가 해결됩니다. 대화 기록은 남습니다.", diff --git a/src/renderer/locales/zh.json b/src/renderer/locales/zh.json index 8ffdb64..d035a5b 100644 --- a/src/renderer/locales/zh.json +++ b/src/renderer/locales/zh.json @@ -646,7 +646,8 @@ "cockpit.bg_state_failed": "失败", "cockpit.bg_close_confirm": "关闭后台会话 '{name}'?它将从 Claude Code 的列表中移除。对话记录会保留,之后仍可重新打开。", "cockpit.bg_closed": "已关闭后台会话 '{name}' — 对话记录已保留", - "cockpit.bg_close_failed": "无法关闭后台会话 '{name}' — 请在终端运行 claude rm {id}", + "cockpit.bg_close_failed": "无法关闭后台会话 '{name}' — 请在本机终端先打开一次 claude agents(这会启动 Claude Code 的后台服务),然后重试,或在那里运行 claude rm {id}", + "cockpit.bg_close_failed_on_machine": "无法关闭 {machine} 上的后台会话 '{name}' — 请在 {machine} 的终端先打开一次 claude agents(这会启动 Claude Code 的后台服务),然后重试,或在那里运行 claude rm {id}", "cockpit.bg_close_old_host": "{machine} 上的 DevDeck 版本过旧,无法关闭后台会话 — 请在那台电脑上更新", "whatsnew.f.bg-close.title": "关闭后台会话", "whatsnew.f.bg-close.body": "Claude Code 的后台会话(侧栏「后台」分组)现在可以通过行的 ⋯ 菜单或分组批量关闭来关闭。已结束或失败的会话不再永远留在列表中。对话记录会保留。",