cmux NEXT Inspector: Subagents shows the running limit, with − / + to change it

Merged
TE
9d ago
+2069-11

Leo's ask: "I want cmux next to be able to increase or decrease subagent amounts ad hoc, with the acpmux daemon and all that. Slide it, even per workspace. Right-click, edit subagent count."

The Inspector's Subagents card now says how many of the chats this chat's agent started are running a turn, against the most that may run at once, and lets a person change that limit for the chat: "Running 2 / 16 max · 1 queued" with − and + at the end of the line. Leo asked to "increase or decrease subagent amounts ad hoc". It builds on the daemon side in #23963, now merged (maxRunning, default 16, range 1 to 1000).

  • − and + send _acpmux/subsession/limits {sessionId, maxRunning: max ∓ 1} (clamped to 1..1000; − is disabled at 1, + at 1000). The number moves at once; acpmux's reply, or its limits record, then decides what holds, and a refused call goes back to acpmux's value.
  • A refused change says "Couldn't change the limit" for a few seconds beside the line. The value a click asked for is tied to its chat, so switching chats while acpmux hasn't answered never shows it on another chat.
  • The line only reads (no − / +) for a chat on another machine (summary.peer) and on a connection that isn't this Mac's app (origin other than local), where acpmux refuses the call with person_required.
  • When the chat has its own limit (maxRunningSource: "session"), a small Reset sends maxRunning: null, back to the inherited, workspace, settings or default limit.
  • Subagents shows when the chat has had subagents or sub-sessions (any row: running, queued or finished), or when its limit is not acpmux's default (maxRunningSource other than "default"), so a limit set by the settings, the workspace or an ancestor shows before any chat starts. A chat that never used them, at the default, shows no Subagents card. An acpmux without maxRunning (older daemons) draws no limit line.
  • Running is the started chats whose state is running or waiting; queued is those whose list summary has waitsForSlot: true. Both are live from the session list. acpmux's own running and queued stand in only while the list names none of the chat's children.

Where the data comes from: subsessionLimits is in the full session detail (_acpmux/info and the attach reply's session), not in the session list. The pane never sends _acpmux/info; its attach reply already carries the same detail, so direct.ts keeps it in the summary. The attach now asks for mux kind subsession (the eighth kind, the most a remote cmux-tui takes; the queue's kinds still go only to a local acpmux), so each limits record newer than the attach's lastSeq updates it, and so does the reply to the pane's own call. A record the page replays never takes it back. sessionList.ts reads waitsForSlot.

The pane relay had to allow the method. _acpmux/subsession/limits is added to the pane policy (policy.json, AcpmuxPaneMethods.swift, AcpmuxPaneMethods+Params.swift) as session-scoped, so only a chat in the pane's scope, with sessionId and maxRunning as its only params: maxChildren and maxDepth stay refused (new cases in the policy's params.json and sessions.json). No gesture: the call starts nothing new and widens no permission. acpmux itself accepts it only from a person-proven connection or an ancestor session (server/subsessions.rs), and the pane's LocalApp connection is person-proven.

The stepper is its own component (inspector/SubagentLimitStepper.tsx with its CSS, the parsing in subsessionLimits.ts), rendered above SubagentsSection, so it stays clear of #23916's row rework. It uses the Inspector's existing icon buttons (text color on hover, no box), secondary text and no transition. Strings are in the pane catalog in all 21 locales.

No capture: the UI is a webview (the Inspector's Subagents card), shown by the gallery states below; the Swift files touched are the pane relay's allowlist tables (AcpmuxPaneMethods*.swift), which draw nothing.

Gallery

Before/after: https://cmux-lawrences-mac-mini.tail137216.ts.net:18796/compare/session-panel-limit-stepper/?entry=agent-pane.chat-inspector (tailnet)

Captured on huge-blue with the gallery matrix runner (Chromium, light and dark, base 17a2acc96f7 and head d8dbed88aa79): the full state gains the quiet "Running 2 / 16 max" line with − / + above the avatars and nothing else moves; subagent-limit reads "Running 2 / 2 max · 1 queued" with Reset, − and +. (Those stills predate the rule that hides the card at the default when unused; the gallery compare link shows the current states.)

New states in agent-pane.chat-inspector: subagent-limit (the chat's own limit of 2, two running, one queued, Reset) and subagent-limit-hidden-default (never used, at the default: no Subagents card) and subagent-limit-set-no-rows (no chats yet, a settings limit of 4: only the limit line). full and the other states now show the default "16 max" line.

Changelog

Added: The Inspector's Subagents shows how many started chats run at once against the limit, with − and + to change it for the chat.

Observed / Not run

Ran at 0b9de12ef7c6bbcd61a3e47356e319bbf5dde7b7: cmux-ci compile-check --app (passed, job 800e014dabc); on cmux16, bun test --timeout 60000 on inspector-dead-clicks.test.tsx, subagent-limit-stepper.test.tsx, agent-pane-subsession-limits.test.ts and the 12 chat-inspector*.test.ts* files (72 pass, 0 fail); bun run check in webviews/ (pass, with the same change applied on 9ae88a3cfe6); prepush-gate.py (no new violations). 9ae88a3cfe6 was red in react-apps-check (1): base's inspector-dead-clicks.test.tsx makes every function prop a spy returning undefined, and the stepper called onSet(value).then(...), a TypeError; the file's second test failed only as a cascade of it. 0b9de12ef7c runs the handler through a promise chain, so a plain return settles the line and a throw shows the refusal. Both tests failed at 9ae88a3cfe6 and pass at 0b9de12ef7c. (cmux16 was at load 260, where bun's 5 s default timeout cut the dead-clicks sweep short, hence --timeout 60000; it took 4.8 s, and 2 s in CI.)

Earlier, at 9ae88a3cfe689c1a15b35e9ac75587fdd663b7f4 (base merged at 31afd27897e; the model.ts conflict was two additions at one spot, both kept: the PR's SubsessionLimitSource/SubsessionLimits beside base's MissingSessionReason), on cmux16: bun test on subagent-limit-stepper.test.tsx, agent-pane-subsession-limits.test.ts and all 12 chat-inspector*.test.ts* files (69 pass, 0 fail, 4 browser-lane tests skipped outside CI), bun run check in webviews/ (pass), scripts/cmux-next/build-agent-pane-web.sh --check (pass), and cmux-ci compile-check --app (passed, job c12a378f166). Locally: prepush-gate.py (no new violations); Localizable.xcstrings parses with no duplicate keys. The merge head cdc271160f2 was red in react-apps-check and the cmux-next web bundles job because build-agent-pane-web.sh did not ship the new inspector/SubagentLimitStepper.css (the stepper would have drawn unstyled in the app); 9ae88a3cfe6 adds it. Base reds at that head, not from this PR: cmux-next fast tier (AgentPaneTabRowHolesTests.swift: HoleRecordingPage does not conform to PageChromiumPage; the same failure on the base run 39735040423 at f61b6a13753) and cmux-next checks (namespace-type errors in AgentPaneNeedsYouSetting.swift and AttentionHighlightLook.swift, a fleet-host-name hit on a gallery fixture line that is base's, and a CI-wiring test; the job is red on base runs 39735040423 and 39736568768).

Earlier, on huge-blue at 132142d63e1 (after merging base with #23963): bun test test/subagent-limit-stepper.test.tsx test/agent-pane-subsession-limits.test.ts test/chat-inspector.test.tsx (26 pass, including no card at the default when unused, the card with a started chat at the default, and the limit alone for a settings limit with no chats), scripts/web-lint.sh (pass) and prepush-gate.py (no new violations). Earlier, at b9bb70619f7, the same three files (24 pass). agent-pane-subsession-limits.test.ts drives the real AcpmuxDirectClient over a fake socket: the attach asks for subsession within 8 non-queue kinds, a limits record at or below the attach's lastSeq leaves the detail's limit, a newer one moves it, and setRunningLimit sends {sessionId, maxRunning} and takes the reply's limit. scripts/whats-new/test_digest.py (OK) and tests/test_cmux_next_release_notes.py (16 OK); scripts/cmux-next/prepush-gate.py (no new violations). scripts/web-lint.sh fails only on test/composer-row-layout.test.ts (TS2339 Row.muted), which is identical at base 8bb848bbd0a (from #23763) and red there too.

Earlier, at d8dbed88aa79: the stepper tests (the new file covers parsing and clamping, the live counts, "Running N / M max · K queued", − / + params, optimistic then acpmux's value, a refused call, Reset sending null, nothing without maxRunning, and the up-front line), the catalog tests (52 pass), bun run check in webviews/ (pass), scripts/web-lint.sh (pass), scripts/cmux-next/prepush-gate.py (no new violations) and cargo test -p cmux-agent-pane-policy (pass, including the TypeScript and Swift parity checks against the new method).

Not run: the stepper against a live daemon with #23963 in the real app.

Agent session: cosmic-gray

🤖 Generated with Claude Code