Conversation
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.
…e agents view Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
…econciled by the CLI Claude-Session: https://claude.ai/code/session_01WriJRPX84KSTHVY9wuKWyo
…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
…fering to resume it 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
|
@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. |
|
Reviewing |
devsuitup
left a comment
There was a problem hiding this comment.
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
test/bg-agents.test.jsis 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-recursivefs.watch(jobsDir)(bg-agents.js~180) also fires when a job subdirectory's content changes, sosyncJobWatchers+scheduleRebuildland 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. TherunVerbones look like the same timer race.- No
CHANGELOG.mdentry. Main requires one under## Unreleasedfor user-visible changes since #365 (rule indocs/changelog.md); this PR adds a view, a shortcut and a sidebar badge. - Dispatch with Additional Directories swallows the prompt.
bg-agents-roster.js~207-208 pushes the prompt right after the last--add-dir, andclaude --helpdeclares--add-dir <directories...>(variadic). Commander parses['--add-dir','/a','do the thing']asaddDir: ['/a','do the thing']with no prompt. The dialog pre-fillsaddDirsfrom 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. - Detach can kill twice. The
stop-sessionattach branch (main.js~1695) never checksstopRequested: each Stop (terminal Stop, agents-view stop, window close) writes Ctrl+Z again and arms another 2 s timer, and two timers can callkillPtyback to back beforeonExitsetsexited. 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 makeskillPtyidempotent). Please return early when a detach is already pending, and havedetachPtywrite throughwritePtyrather thanwithPtyso it inherits #408's no-write-after-kill guard once that lands.
Should fix
- Login-shell latency vs. the CLI timeouts.
runClaudeCommandrunsbash -l -i -cunder Git Bash; measured idle here:echo hi3.6–5.6 s,claude agents --json --all12.1 s, againstLIST_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 timeoutchild.kill('SIGKILL')kills only the shell; on Windowsclaude.exesurvives. - cmd.exe quoting.
spawnChild(shell, ['/C', cmd])withoutwindowsVerbatimArguments: Node re-escapes the/Cargument and the program receives literal quotes — every verb fails under a cmd profile. WithwindowsVerbatimArguments: true,a & bstill arrives asa ^& b, and a multi-line prompt is cut at the first line. PowerShell 5.1 drops the double quotes insay "hi" now. Consider passing the prompt on stdin or via a temp file rather than through the shell command line. - Agents view stays on screen in grid mode.
showSession'sgridViewActivebranch never callshideAllViewers(); onlyattachBgAgentspecial-cases it. Clicking a sidebar session with grid + agents view open changes nothing visible.
Minor
- Resolve on
closerather thanexitinrunClaudeCommand(stdio may not be drained atexit; 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 livestate.jsonfor up to 30 s;runVerb's live-guard uses that stale state.- The attach
cwdfrom the renderer is not existence-checked (main.js~2403), unlike the resume path. docs/superpowers/plans/…(3,209 lines) anddocs/superpowers/specs/…duplicate.ai/contexts/bg-agents.md; repo convention is.ai/contexts/or an ADR underdocs/decisions/. Please drop them or fold the surviving rationale into an ADR.build/screenshot-agents.gifis 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">inindex.htmland the rationale line inresume-guard.jsshould become one-line pointers. - 5 of 6 tests in
test/open-terminal-attach.test.jsare 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_ALLOWEDwording "attach-less bg commands": the function also runsagents,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
|
Reviewing |
devsuitup
left a comment
There was a problem hiding this comment.
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:
- The direct spawn on Windows cannot find an npm-installed
claude. For cmd/PowerShell profilesrunClaudeCommandnow runsspawnChild('claude', claudeArgv)without a shell. When the CLI is installed through npm, which is the common case on Windows,where claudegives…\AppData\Roaming\npm\claudeandclaude.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))"printsENOENTon this machine. Every list, verb and dispatch then fails under a cmd or PowerShell profile. Resolve the real executable first. If it is a.cmdshim, either run thenode+cli.jsit points to, or go throughcmd.exe /d /s /cwithwindowsVerbatimArgumentsand proper^escaping. Also add a test that covers the.cmdcase. - The
runVerbtests fail on every Windows run. I ranbg-agents,pty-ops-detachandbg-agents-rostertogether 4 times at13bd51a; each run had 1–2 failures out of 36. The failures arerunVerb spawns…,runVerb runs rm from the home directory…andrunVerb refuses rm and respawn…. The error is the cleanupfs.rmSync(dir, …)attest/bg-agents.test.js:172, which throwsEPERM, Permission denied: …\Temp\sw-bg-agents-…. A handle is still open on the temp dir when it is removed, probably afs.watchonjobs/<id>or the fake child's cwd. Stop the module (close the watchers) before thermSync, or passmaxRetriestormSync. 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):
- On POSIX the child is now
detachedand a timeout kills-pid, the whole process group. Ifclaude --bgstarts 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,closenever 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.
|
Thanks for the review. Pushed Must fix
Should fix Minor
|
…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
|
Reviewing |
devsuitup
left a comment
There was a problem hiding this comment.
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).
resolveWindowsClaudereturns 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:findOnPathtries the empty extension first. npm puts an extensionlessclaude(the POSIX sh shim) next toclaude.cmd, so the sh shim wins and gets refused. On Windows, skip the bare name or try thePATHEXTextensions 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.exetarget as well and run it directly.
The test fixture uses the oldcli.jsshim, which is whytest/claude-binary.test.jspasses. Please add the current shim shape and an extensionlessclaudenext to the.cmd.
- The
runVerbtests still fail on Windows.bg-agents,pty-ops-detach,bg-agents-rosterandclaude-binaryran 3 times at784e5b9: 1, 2 and 3 failures out of 42. The failures arerunVerb spawns…,runVerb runs rm from the home directory…andrunVerb refuses rm and respawn…. The error is stillEPERM, Permission denied: …\Temp\sw-bg-agents-…, now thrown fromrmTmp, sobgAgents.stop()plusmaxRetries(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 spawnsgitwith 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 instop(). This is an untested hypothesis; the error itself is measured.
|
Thanks. Pushed
|
|
Reviewing |
|
Re-review at |
…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
|
Thanks. Pushed
|
|
Reviewing |
…-agents-view-impl
devsuitup
left a comment
There was a problem hiding this comment.
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 answersagents --json --allwith valid JSON.bg-agents,pty-ops-detach,bg-agents-rosterandclaude-binary: 44/44 passing in each of 3 runs.- Full local
task checkatb0fcdc8: 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.
|
Reviewing |
|
Thanks for the approval. I had already merged main again before it landed: On the attach flag through #402's generation path: an attach tab keeps it. The renderer sets |
devsuitup
left a comment
There was a problem hiding this comment.
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.
|
CI and live test at CI: one test is red on Linux; Windows, lint and changelog are green. Live test against the real daemon passed. Run on Windows 11 with CLI 2.1.287 and the real
Non-blocking follow-ups seen live (fine as separate issues):
|
A graphical replacement for the
claude agentsTUI: 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
--bgsessions plus the interactive sessions running outside this Switchboard, read from~/.claude/jobs/*/state.jsonand the CLI's session descriptors, reconciled byclaude agents --json --all. Open it with the people icon in the sidebar filter row orCtrl/Cmd+Shift+A(rebindable)..claude/worktrees/<name>or anygit worktree add); a Worktrees option (on by default) adds a second level per worktree for projects that have several.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 abgbadge.workingorblocked) 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).~/.claude/jobs/and thekind: "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.mdContext:
.ai/contexts/bg-agents.md(includes "Known limits"); user doc:docs/background-agents.mdFound while building
blocked(a live job waiting on input, treated as live everywhere) andfailed(finished).claude --bgprintsbackgrounded · <id> · <name>with the id in ANSI colour even when piped; a never-trusted cwd makes it refuse ("Workspace not trusted").window-framelessinset lists (right and left) so New agent stays clear of them, and its labels areno-drag.#mainrun past the window edge in narrow windows (it has nomin-width);#main { min-width: 0 }, and the header wraps its controls..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)
dispatcherrors 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