Skip to content

(agents): a view of the sessions the claude daemon runs in the background - #374

Open
paulo-jay wants to merge 42 commits into
devsuitup:mainfrom
paulo-jay:worktree-background-agents-view-impl
Open

paulo-jay wants to merge 42 commits into
devsuitup:mainfrom
paulo-jay:worktree-background-agents-view-impl

Conversation

@paulo-jay

@paulo-jay paulo-jay commented Sep 30, 2026 •

Copy link
Copy Markdown

A graphical replacement for the claude agents TUI: a dedicated Agents view that lists the sessions the Claude daemon runs in the background, shows what each one does, and attaches to, stops, respawns, deletes or dispatches them.

What it does

  • List. The daemon's --bg sessions plus the interactive sessions running outside this Switchboard, read from ~/.claude/jobs/*/state.json and the CLI's session descriptors, reconciled by claude agents --json --all. Open it with the people icon in the sidebar filter row or Ctrl/Cmd+Shift+A (rebindable).
  • Grouping. A Group menu: by State (default), by Project, or none. State groups carry an emoji + label (⚙️ Working, ✋ Blocked, ✅ Done, ⏹️ Stopped, ❌ Failed, 🖥️ External, ❓ Unknown), and the same emoji starts each row's state column. Project mode merges all the worktrees of one git project under the main working tree (.claude/worktrees/<name> or any git worktree add); a Worktrees option (on by default) adds a second level per worktree for projects that have several.
  • Folding. Every group header (state, project, worktree) folds on click / Enter / Space; the folded set is remembered.
  • Attach / detach. Attach (the button, or a double click on a live row) opens claude attach <id> in a terminal tab keyed by the session's real id; closing it detaches (Ctrl+Z, 2 s grace, then kill), it never stops the job. A click in the sidebar on a session the daemon runs attaches instead of asking to resume; such sessions carry a bg badge.
  • Stop, respawn, delete, dispatch. Through the CLI via the login shell with a quoted argv. A live job (working or blocked) can only be stopped or attached to; it is never resumed or forked. New agent opens a dispatch dialog (prompt, name, project, agent, permission options).
  • Failure is silence. ~/.claude/jobs/ and the kind: "bg" descriptor are undocumented interfaces: if the daemon does not answer, the view falls back to the files and shows a banner. Canary tests pin the observed shapes (CLI 2.1.285).

Design notes: .ai/contexts/bg-agents.md; user doc: docs/background-agents.md
Context: .ai/contexts/bg-agents.md (includes "Known limits"); user doc: docs/background-agents.md

Found while building

  • The daemon reports job states beyond what was first assumed: blocked (a live job waiting on input, treated as live everywhere) and failed (finished).
  • claude --bg prints backgrounded · <id> · <name> with the id in ANSI colour even when piped; a never-trusted cwd makes it refuse ("Workspace not trusted").
  • The frameless window draws the system buttons over the top-right corner: the Agents header joins the window-frameless inset lists (right and left) so New agent stays clear of them, and its labels are no-drag.
  • A row's grid needs ~670 px, which made #main run past the window edge in narrow windows (it has no min-width); #main { min-width: 0 }, and the header wraps its controls.
  • The New background agent dialog reuses .new-session-dialog, which has no height limit; it gets its own class (max-height + overflow-y: auto) and scrolls on short screens. The shared class is left alone on purpose.

Known limits (details in the context doc)

dispatch errors can carry login-shell noise ahead of the CLI message; a noise line that is itself a valid JSON array can win the list parse; MAX_JOBS (200) truncates by id, not recency; interactive-descriptor liveness is pid-only; resolved project/worktree roots are cached until the window closes; a submodule or bare repo is its own project; runVerb's live-guard passes rm/respawn when the roster lacks the job (unreachable from the UI).

https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo

pjay added 30 commits September 30, 2026 16:40
A graphical replacement for the claude agents TUI: a dedicated view fed by
the daemon's job files and the session descriptors, reconciled by
claude agents --json, with attach/stop/rm/respawn/dispatch through the CLI.
…ose the agents roster

open-terminal runs `claude attach <id>` for a validated job id (no resume, sandbox, pre-launch or MCP), stop-session detaches an attach tab instead of killing it, and the roster module is wired to IPC, the preload API and the window teardown.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
… does not silence it

bgAgents.stop() clears its listeners when the window closes; a re-created window's next get-bg-agents now restores the bg-agents-changed push, idempotently.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
…anscript, stop, respawn and delete

Lists the roster the main process keeps, live jobs (working or blocked) first, with the verbs each state allows; opening the view is what arms the roster push. Ctrl/Cmd+Shift+A toggles it.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
…er viewer opens

A hide of an already-hidden view no longer clears the persisted flag, and the flag is read before the working-set restore runs. Memory, Work Files and Settings now close the view instead of stacking over it.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
The New agent button opens a dialog taking the prompt, project, name, agent and the New-Session permission options, and hands them to dispatchBgAgent. A refusal from main stays inline; a success closes the dialog and refreshes the roster, selecting the new row when the id is known.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
User doc, context doc with the known limits, and the rows in the README, shortcuts, IPC and cli-session-state docs.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
escapeHtml leaves double quotes alone, so a quote in a cwd, href or
session id could close the attribute and inject a data-verb that a click
would run.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
The CLI runs through an interactive login shell, so rc files that print
to stdout made every reconcile fail and the view blamed the daemon. The
list parse now falls back to the line-bounded JSON array inside the
noise, and a verb's error drops the shell's job-control warnings.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
A finished job kept its bg badge and its "click to attach" tooltip,
while a click on it resumes the session normally.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
The reattach branch of open-terminal did not say the live session was an
attach, so after a renderer reload the tab was treated as an ordinary
session.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
rm ran inside the job's cwd, which may be the directory it deletes;
Windows refuses to remove a live process's cwd. Respawn keeps the job
cwd, where its brief needs it.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
…tch dialog

Enter on Cancel both closed the dialog and started the agent.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
The CLI's --bg output and its untrusted-workspace refusal were measured
on 2.1.285; the stale unmeasured note goes, and the context doc now
covers the tolerant list parse, rm from home, the live-only badge, the
reattach flag and the attribute escaping.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
The daemon reports state failed for a job that ended in error (CLI 2.1.285, two live jobs); JOB_STATES dropped it to null and the row read '?'. It is finished like done and stopped: not live, filtered by Finished, Respawn and Delete enabled.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
A Group select in the view header (None / State / Project) splits the list into sections with a header and a count. The Finished filter and the sort apply first; headers are not rows, so selection and clicks are unchanged. The choice persists in localStorage.agentsGroupBy.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
Unset or invalid agentsGroupBy now means State; a stored none or project is kept. One AGENT_STATE_META map gives each state its emoji and label, used by the State headers and at the start of every row's state column. Section headers are larger and semi-bold, the count secondary.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
…ree sub-groups

Project mode keyed every cwd apart, so each worktree of a repo formed its own group. The main process now resolves projectRoot and worktreeRoot per cwd (the .claude/worktrees pattern, else one git rev-parse through execFile, cached, never blocking the roster) and Project mode groups by projectRoot. A Worktrees option, on by default and remembered, sub-groups a project by worktree when it spans more than one.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
Every group header (State, Project, worktree sub-group) is now a toggle: click, Enter or Space hides its rows, keeping the label and count. Keys are scoped by mode and level and persist in localStorage.agentsCollapsedGroups (capped at 200); roster pushes, regroups and the Finished filter keep them, and the selection stays.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
A double click on a working or blocked row attaches, like the Attach button, when the daemon answers. Finished and external rows, group headers, buttons and links are left alone.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
@paulo-jay

Copy link
Copy Markdown
Author

@devsuitup this PR is ready for review. I could not add you as a reviewer (my account has read-only access to this repository), so I am mentioning you here. The description explains the feature; a short demo GIF (synthetic data only) is at the top of docs/background-agents.md. The attach / detach behaviour has only been exercised in tests so far, not against a long-running daemon, so a quick manual pass on attach, detach and quit-with-an-attach-tab-open would be welcome.

@devsuitup

Copy link
Copy Markdown
Owner

Reviewing b903305 (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review at 012ac35 (feature content reviewed at b903305; the merge of main in 012ac35 was checked separately and only adds conflict resolutions plus a new UNRESOLVED_ALLOWED entry for runClaudeCommand, which is justified: it spawns the user's shell profile with claude …, the same exposure as runScheduleCommand).

Thanks for the detailed description and the "Known limits" section — they made this much faster to review. The renderer side holds up well: every disk-derived value I traced is escaped, job ids are hex-validated before any path.join or spawn, and the "a live job is never resumed" path is sound. Changes requested for the items below. CI has not run on this PR (fork workflows not approved yet), so everything here was measured locally on Windows 11.

Must fix

  1. test/bg-agents.test.js is timing-flaky on Windows. Four local runs of that file alone: 1 fail (a state.json rewrite reaches listeners once, coalesced…, actual ['two','two']), 2 fails (runVerb spawns claude <verb> <id>…, runVerb refuses rm and respawn…), 1 fail (runVerb spawns…), then green. Likely cause for the coalescing one: on Windows the non-recursive fs.watch(jobsDir) (bg-agents.js ~180) also fires when a job subdirectory's content changes, so syncJobWatchers + scheduleRebuild land after the first 250 ms flush and push a second time. Either have the dir watcher react only to added/removed entries, or emit only when the roster actually changed. The runVerb ones look like the same timer race.
  2. No CHANGELOG.md entry. Main requires one under ## Unreleased for user-visible changes since #365 (rule in docs/changelog.md); this PR adds a view, a shortcut and a sidebar badge.
  3. Dispatch with Additional Directories swallows the prompt. bg-agents-roster.js ~207-208 pushes the prompt right after the last --add-dir, and claude --help declares --add-dir <directories...> (variadic). Commander parses ['--add-dir','/a','do the thing'] as addDir: ['/a','do the thing'] with no prompt. The dialog pre-fills addDirs from the effective settings, so anyone with a global Additional Directories value dispatches a prompt-less job. Put -- before the prompt (or the prompt before the options); test/bg-agents-roster.test.js ~135 currently pins the broken order.
  4. Detach can kill twice. The stop-session attach branch (main.js ~1695) never checks stopRequested: each Stop (terminal Stop, agents-view stop, window close) writes Ctrl+Z again and arms another 2 s timer, and two timers can call killPty back to back before onExit sets exited. On Windows a second kill of a ConPTY is a heap double free that takes the whole app down (0xc0000374; root-caused in #405, guarded by #408, which makes killPty idempotent). Please return early when a detach is already pending, and have detachPty write through writePty rather than withPty so it inherits #408's no-write-after-kill guard once that lands.

Should fix

  1. Login-shell latency vs. the CLI timeouts. runClaudeCommand runs bash -l -i -c under Git Bash; measured idle here: echo hi 3.6–5.6 s, claude agents --json --all 12.1 s, against LIST_TIMEOUT_MS = 5000 / VERB_TIMEOUT_MS = 15000. The view therefore shows "daemon unreachable" permanently on this setup, and a dispatch that passes 15 s reports an error while the job probably still starts (a retry duplicates it). On timeout child.kill('SIGKILL') kills only the shell; on Windows claude.exe survives.
  2. cmd.exe quoting. spawnChild(shell, ['/C', cmd]) without windowsVerbatimArguments: Node re-escapes the /C argument and the program receives literal quotes — every verb fails under a cmd profile. With windowsVerbatimArguments: true, a & b still arrives as a ^& b, and a multi-line prompt is cut at the first line. PowerShell 5.1 drops the double quotes in say "hi" now. Consider passing the prompt on stdin or via a temp file rather than through the shell command line.
  3. Agents view stays on screen in grid mode. showSession's gridViewActive branch never calls hideAllViewers(); only attachBgAgent special-cases it. Clicking a sidebar session with grid + agents view open changes nothing visible.

Minor

  • Resolve on close rather than exit in runClaudeCommand (stdio may not be drained at exit; a truncated JSON list then marks the daemon unreachable).
  • e.state = cliEntry.state || e.state (bg-agents-roster.js ~145) lets the cached CLI snapshot override the live state.json for up to 30 s; runVerb's live-guard uses that stale state.
  • The attach cwd from the renderer is not existence-checked (main.js ~2403), unlike the resume path.
  • docs/superpowers/plans/… (3,209 lines) and docs/superpowers/specs/… duplicate .ai/contexts/bg-agents.md; repo convention is .ai/contexts/ or an ADR under docs/decisions/. Please drop them or fold the surviving rationale into an ADR.
  • build/screenshot-agents.gif is 806 KiB of permanent history; a PNG (like the other screenshots) or a much smaller asset would be better.
  • Comment sweep: the rationale comment above <script src="agents-view.js"> in index.html and the rationale line in resume-guard.js should become one-line pointers.
  • 5 of 6 tests in test/open-terminal-attach.test.js are regex pins on source text; they pass as long as the lines exist.
  • The canary tests carry "CLI 2.1.285" in their names and fail (rather than warn) on an unknown job state on a maintainer machine, which also blocks the pre-commit hook for unrelated commits.
  • UNRESOLVED_ALLOWED wording "attach-less bg commands": the function also runs agents, stop, respawn, rm.

Overlap heads-up: #402 adds a generation field on the same reattached return line in main.js, and #397 appends a dialog at the end of public/dialogs.js; both are queued ahead of this one, so a small rebase is likely.

Dispatch puts -- before the prompt so a variadic --add-dir cannot swallow it.
Detach is idempotent and writes through writePty. The jobs directory watcher
ignores content changes and the debounced flush emits only on a real change.
A job's own state.json state wins over the cached CLI snapshot. claude runs
directly under cmd.exe and PowerShell, a timeout kills the process tree, the
call resolves on close, and the CLI timeouts are 20 s and 60 s. Clicking a
session in grid mode closes the Agents view. Adds the CHANGELOG entry, drops
the plan and spec, replaces the GIF with a PNG, and trims comments.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
@devsuitup

Copy link
Copy Markdown
Owner

Reviewing 13bd51a (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 13bd51a (delta from 012ac35).

Fixed and checked: the -- before the prompt; the stopRequested early return plus the detaching flag and writePty in detachPty (and the new test); live state.json winning over the CLI snapshot; close instead of exit; the dir watcher ignoring change; the timeouts; the CHANGELOG entry; the plan/spec files dropped and the gif replaced by a 107 KiB PNG; the canary now warns on an unknown state; the grid-mode hide in showSession. Thanks for the quick turnaround.

Still blocking:

  1. The direct spawn on Windows cannot find an npm-installed claude. For cmd/PowerShell profiles runClaudeCommand now runs spawnChild('claude', claudeArgv) without a shell. When the CLI is installed through npm, which is the common case on Windows, where claude gives …\AppData\Roaming\npm\claude and claude.cmd. libuv only resolves .exe/.com, so the spawn fails: node -e "require('child_process').spawn('claude',['--version']).on('error',e=>console.log(e.code))" prints ENOENT on this machine. Every list, verb and dispatch then fails under a cmd or PowerShell profile. Resolve the real executable first. If it is a .cmd shim, either run the node + cli.js it points to, or go through cmd.exe /d /s /c with windowsVerbatimArguments and proper ^ escaping. Also add a test that covers the .cmd case.
  2. The runVerb tests fail on every Windows run. I ran bg-agents, pty-ops-detach and bg-agents-roster together 4 times at 13bd51a; each run had 1–2 failures out of 36. The failures are runVerb spawns…, runVerb runs rm from the home directory… and runVerb refuses rm and respawn…. The error is the cleanup fs.rmSync(dir, …) at test/bg-agents.test.js:172, which throws EPERM, Permission denied: …\Temp\sw-bg-agents-…. A handle is still open on the temp dir when it is removed, probably a fs.watch on jobs/<id> or the fake child's cwd. Stop the module (close the watchers) before the rmSync, or pass maxRetries to rmSync. Windows CI would hit this as well. The coalescing failure seen earlier did not reproduce in these 4 runs.

Worth a look (reasoning only, not measured):

  1. On POSIX the child is now detached and a timeout kills -pid, the whole process group. If claude --bg starts the daemon when none is running and the daemon stays in that group, a dispatch that times out would kill the daemon and every job it runs. Separately, if the daemon inherits the stdout pipe, close never fires and every first dispatch reports a timeout after 60 s even though it succeeded. Both depend on how the CLI daemonises, which I have not checked. A test with no daemon running would settle it.

@paulo-jay

Copy link
Copy Markdown
Author

Thanks for the review. Pushed 13bd51a; task check passes locally. By item:

Must fix

  1. Flaky tests: the jobs-directory watcher now ignores change events (only add/remove resync), and the debounced flush emits only when the roster actually changed. test/bg-agents.test.js is green three runs in a row here, but I could only run it on Linux, so please re-run it on Windows.
  2. CHANGELOG.md: entry added under ## Unreleased / New.
  3. Dispatch: dispatchArgs now emits -- before the prompt; the roster and dispatch tests pin the new order.
  4. Detach: stop-session returns early when a stop is already pending, detachPty is idempotent (session.detaching) and writes through writePty. New test for the second detach.

Should fix
5. Login-shell latency: timeouts raised to 20 s (list) and 60 s (verbs); a timeout kills the whole process tree (taskkill /T on Windows, the process group elsewhere); the call resolves on close.
6. cmd.exe / PowerShell: under those profiles claude is spawned directly with an argv array, so no shell touches the prompt. Not tested on Windows.
7. Grid mode: showSession's grid branch now closes the Agents view.

Minor

  • Resolve on close: done. A job's own state.json state now wins over the cached CLI snapshot (the old test pinning the opposite was reversed).
  • Attach cwd: already checked by the existing project directory no longer exists test further down in open-terminal; left as is.
  • Plan and spec removed (the PR body now points to .ai/contexts/bg-agents.md); GIF replaced by a 107 KB PNG; the two comments reduced to one-line pointers; UNRESOLVED_ALLOWED wording fixed; canaries: version dropped from a test name and an unknown job state is now a diagnostic, not a failure.
  • Not changed: the regex-pin tests in open-terminal-attach.test.js, and the rebase heads-up for (sidebar): end a dropped session's running state when main drops it (#375) #402 / (remote): send a prompt to an unattached remote session (#219) #397, which I will do when they merge.

…meout

claude is found on PATH and run as the .exe, as node plus the cli.js an npm
shim points to, or through cmd.exe. A timed-out --bg kills only the client,
and a call settles shortly after exit even if a daemon holds the pipes. The
bg-agents tests stop the module and retry before removing their temp dirs.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
@devsuitup

Copy link
Copy Markdown
Owner

Reviewing 784e5b9 (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 784e5b9 (delta from 13bd51a).

Not passing the client's --bg timeout to the process group, and settling one second after exit, answer my point 3. Two blockers remain, both measured on this machine (Windows 11, npm-installed CLI).

  1. resolveWindowsClaude returns an error for an npm install. I ran it against the real environment:
    resolveWindowsClaude(['agents','--json'], process.env) returns {"error":"cannot run C:\Users\…\AppData\Roaming\npm\claude"}. Two separate causes:
    • findOnPath tries the empty extension first. npm puts an extensionless claude (the POSIX sh shim) next to claude.cmd, so the sh shim wins and gets refused. On Windows, skip the bare name or try the PATHEXT extensions first.
    • The current npm shim no longer points to a cli.js. Its last line reads "%dp0%\node_modules\@anthropic-ai\claude-code\bin\claude.exe" %*, so the /"%dp0%\([^"]+\.js)"/ unwrap would miss it and fall back to cmd.exe, which refuses multi-line prompts. Unwrap an .exe target as well and run it directly.
      The test fixture uses the old cli.js shim, which is why test/claude-binary.test.js passes. Please add the current shim shape and an extensionless claude next to the .cmd.
  2. The runVerb tests still fail on Windows. bg-agents, pty-ops-detach, bg-agents-roster and claude-binary ran 3 times at 784e5b9: 1, 2 and 3 failures out of 42. The failures are runVerb spawns…, runVerb runs rm from the home directory… and runVerb refuses rm and respawn…. The error is still EPERM, Permission denied: …\Temp\sw-bg-agents-…, now thrown from rmTmp, so bgAgents.stop() plus maxRetries (10 × 50 ms) is not enough. The handle that is still open is probably not a watcher. A likely candidate is the project-root resolution, which spawns git with the temp dir as cwd (resolveMissingRoots / ROOT_CONCURRENCY) and can still be running when the test ends. Inject a no-op root resolver in these tests, or await the pending resolutions in stop(). This is an untested hypothesis; the error itself is measured.

@paulo-jay

Copy link
Copy Markdown
Author

Thanks. Pushed 784e5b9; task check passes locally on Linux. Nothing below was run on Windows.

  1. npm claude.cmd: new claude-binary.js finds claude on PATH and spawns it without a shell: the .exe itself, or, for a .cmd shim, node + the cli.js the shim points to, or cmd.exe /d /s /c with escaped arguments as a last resort (a multi-line argument is refused there with a message). test/claude-binary.test.js covers .exe, the unwrapped shim, the cmd.exe fallback, the multi-line refusal and a missing claude, against a fake file system.
  2. runVerb cleanup EPERM: the tests now call bgAgents.stop() before removing the temp dir, and rmSync retries (maxRetries: 10). I could not reproduce the failure on Linux, so please re-run on Windows.
  3. Process group / daemon: I have not checked how the CLI daemonises, so I took the safe side. A timed-out --bg kills only the client, never the group, and a call settles 1 s after the client exits even if a daemon keeps the pipes open. The other commands keep the whole-tree kill. I did not write the "no daemon running" test you suggest.

@devsuitup

Copy link
Copy Markdown
Owner

Reviewing e99a50f (adversarial review in progress).

@devsuitup

Copy link
Copy Markdown
Owner

Re-review at e99a50f: this head only merges main (#393, #395, #396), and the merge resolution is clean (main.js only gains the waitingFor copies from #395). The two blockers from the review at 784e5b9 still stand: resolveWindowsClaude picks the extensionless npm claude and misses the current bin\claude.exe shim, and the runVerb tests hit EPERM on Windows.

pjay added 2 commits October 2, 2026 12:00
…of the tests

Only PATHEXT names are looked up, so npm's extensionless sh shim never wins,
and a shim that points to bin/claude.exe runs that exe directly. The
bg-agents tests inject a no-op project-root resolver by default, so no git
child is still running in a temp dir when it is removed.

Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
@paulo-jay

Copy link
Copy Markdown
Author

Thanks. Pushed b0fcdc8 (this also merges main again: #393/#395/#396 earlier, then the three newer commits; two conflicts, in CHANGELOG.md and the reattached return in main.js, which keeps attach and takes #395's mcpState/mcpError). task check passes on Linux; nothing here was run on Windows.

  1. npm claude: findOnPath now only tries the PATHEXT names, so npm's extensionless sh shim is never picked. The shim unwrap also accepts a target ending in .exe and runs it directly (the current bin\claude.exe shape); the node + cli.js and cmd.exe paths stay as fallbacks. test/claude-binary.test.js now has the current shim shape and an extensionless claude next to the .cmd.
  2. runVerb EPERM: I took your hypothesis. The tests' boot() now injects a no-op project-root resolver by default, so no git child can still be running in the temp dir when it is removed; the few tests that check root resolution pass their own. I could not reproduce the failure on Linux, so please re-run the file on Windows.

@devsuitup

Copy link
Copy Markdown
Owner

Reviewing b0fcdc8 (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at b0fcdc8 (delta from 784e5b9: 930a0e1 plus a merge of main up to #401). Approving.

Both blockers are fixed, checked on Windows 11 with the CLI installed through npm:

  • resolveWindowsClaude(['agents','--json'], process.env) now returns …\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe, and that exe answers agents --json --all with valid JSON.
  • bg-agents, pty-ops-detach, bg-agents-roster and claude-binary: 44/44 passing in each of 3 runs.
  • Full local task check at b0fcdc8: lint 0 errors; 3074 tests, 0 failures (CI has not run on this fork PR).

Before merge: main has moved on since this head with #402 (pty generation on the open-terminal replies and in process-exited), #404, #406, #408 (kill-once guard in pty-ops.js) and #410. merge-tree shows no textual conflict, but main.js, pty-ops.js, public/app.js and public/sidebar.js change on both sides. Please merge main once more and re-run the tests. In particular, check that an attach tab keeps its attach flag through #402's generation and drop path, so that Stop still detaches rather than kills.

@devsuitup
devsuitup self-requested a review October 2, 2026 10:23
@devsuitup

Copy link
Copy Markdown
Owner

Reviewing 792481d (adversarial review in progress).

@paulo-jay

Copy link
Copy Markdown
Author

Thanks for the approval. I had already merged main again before it landed: 792481d has #402, #404, #406, #408 and #410, with no textual conflict, and task check passes locally on Linux.

On the attach flag through #402's generation path: an attach tab keeps it. The renderer sets entry.attach from the open options before the call and again from result.attach on a reattach (the reattached reply carries attach next to generation), and stop-session decides from session.isAttach in main. #402's drop path does not touch entry.attach. detachPty now sits on #408's idempotent writePty/killPty. I have not exercised this in a running Electron, only by reading the code and the tests.

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 792481d: a merge of current main (through #404). The resolution is right where both sides met. detachPty goes through writePty, so it inherits the #408 kill-once and no-write-after-kill guards, and the open-terminal replies carry both attach and generation. Full local task check at 792481d: lint 0 errors; 3136 tests, 0 failures. Approving.

@devsuitup

Copy link
Copy Markdown
Owner

CI and live test at 792481d.

CI: one test is red on Linux; Windows, lint and changelog are green. test/cli-session-state.test.js:467 "liveElsewhere reports the descriptor kind and jobId…" fails on both ubuntu jobs with Cannot read properties of null (reading 'pid') at line 473. The test writes a descriptor for pid 4242 and calls boot(dir, new Map()) without an isProcessAlive override, so the real liveness check runs. Pid 4242 does not exist on the runner and liveElsewhere returns null. Passing { isProcessAlive: () => true } to boot, as the readAllDescriptors test above it does, should fix it. Once CI is green this is ready to merge; the maintainer has approved merging on that condition.

Live test against the real daemon passed. Run on Windows 11 with CLI 2.1.287 and the real ~/.claude/jobs, in an isolated Switchboard instance:

  • The roster matches claude agents --json --all exactly: 7 bg jobs plus 3 interactive sessions, no banner.
  • Grouping by State, Project and none works; folding persists.
  • Dispatch through New agent works.
  • Attaching shows the live job.
  • Detaching by closing the tab leaves the job working and leaves no claude attach process behind.
  • Attach plus a double Stop returns {detached:true} twice; the app stays alive and the job keeps running.
  • Quitting with an attach tab open exits with code 0, and the job keeps running.
  • Stop and Delete from the view work.
  • The claude.cmd → bin\claude.exe path was exercised under a PowerShell profile: claude.exe ran as a direct child of the main process, and the list call took 729 ms.
  • No renderer or main-log errors.

Non-blocking follow-ups seen live (fine as separate issues):

  1. The attach tab renders the TUI misaligned. Text breaks at odd columns and the status line is drawn 2–5 times. This looks like a size mismatch between claude attach and the pty; it was not diagnosed.
  2. The state.json state can be stale in the other direction too. For several minutes the row showed blocked while the CLI said working, because state.json stayed at blocked. Preferring whichever source is newer, rather than always the file, would avoid this.
  3. The dispatch error keeps the login-shell noise. For example: bash: cannot set terminal process group (-1)… no job control in this shell Workspace not trusted…. stripShellNoise is applied to verb errors but not to dispatch errors.
  4. Delete took 33–37 s per job under the Git Bash profile, with no progress shown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants