cmux-next: agent question card like the Claude app's (Needs your input pill, ⌘↩, Esc returns to the composer)

Merged
TE
16d ago
+994-217

Summary

When an agent asks a question (Claude Code's AskUserQuestion, Codex user input, Grok or Cursor asks), the cmux-next agent pane now shows it the way the Claude app does, as Leo asked on 2026-10-19:

  • An amber Needs your input pill sits above the card. Its chevron folds the card away and back, and the person's picks survive the fold.
  • The question is the card's header strip. Options are radio rows (checkbox rows for multi-select), each with a bold label and a muted multi-line description, divided by hairlines. Other turns into an inline text field.
  • Skip and Submit sit at the bottom right. Submit shows a ⌘↩ hint.
  • A chat waiting on the person shows an amber Needs input label in the session sidebar, beside the existing glyph.

Keyboard:

  • The card takes the keyboard when it arrives if nothing else has it or the composer is empty, so no click is needed first. A half-written prompt keeps the keyboard.
  • ↑/↓ move between options, Enter picks, and 1–9 choose directly.
  • ⌘↩ submits. With nothing chosen yet, it answers the highlighted row the way Enter does.
  • Esc gives the keyboard back to the composer and never answers. Inside the Other field, Esc only leaves the field. Skip declines the ask.
  • One click on the last single-choice question sends it at once, as in Codex and Claude Code.
  • If acpmux refuses a reply, the card shows the error and its controls work again for a retry.

No new path: this reworks the existing QuestionCard in the agent pane. acpmux already normalizes every harness's ask into a permission with _meta.acpmux.question. The reply still goes chat.permission → _acpmux/permission_respond → agent, unchanged.

The native AgentQuestionCardView (CmuxNextAgentQuestion) is still unused. Home has no question producer or custom-row seam yet (plans/cmux-next/agent-questions.md §5–6), and agent tabs render through this web pane.

Skip declining follows the plan's §7 decision: a hidden pending ask blocks the turn. Esc does not decline; plans/cmux-next/agent-questions.md §6 and §7 now record that, the refused-reply retry and the single-click submit.

Testing

  • webviews/test/agent-question-card.test.ts (new) drives the agent pane preview in Chromium on the shared pending-single fixture. It checks four things:
    • the pill and the header;
    • fold and unfold keep the pick;
    • ↓ then ⌘↩ sends chat.permission {permissionId, optionId: "allow_once", answers: {"Which auth method should the API use?": "API keys"}};
    • Esc sends nothing and gives the keyboard to the composer, Skip sends the reject_once decline, and an Other answer typed after a digit key is sent with ⌘↩.
  • Red/green with CMUX_BROWSER_TESTS=1 bun test test/agent-question-card.test.ts:
  • The agent end of the round trip: cargo test -p acpmux --test hub_integration questions passed 4/4 on this branch. questions_incomplete_answers_are_refused_and_the_ask_stays answers with the same answers shape and asserts the fake agent received {"Which one?": "A"}.
  • tsc --noEmit, vp fmt --check and vp lint pass on the changed files. check:type-scale, check:layers, check:system-blue and check:text-focus pass, and the type-scale baseline is lowered by 9 because the card now uses --text-* tokens. These suites also pass: ui-rules, hairline-rims, theme-colors, stylesheets, session-rail, tooltip-rules, row-focus, selected-row-accent and hover-row-sweep.
  • The whole bun test run on this Linux VM has failures in unrelated files whether or not this change is applied. They pass when those files run alone, so they come from cross-file pollution in the single-process run.
  • Screenshots: from the agent-pane.question-card gallery entry, captured by the Linux gallery advisory run (run 39341076275, head 1425c5dcc1fd) in the app's real theme (Apple System Colors, dark and light), so the pill shows its amber. Not verified: a capture in a tagged macOS build. The fleet build of 2b0ad35 passed (cmux-ci job 80496fca94e0e6a1d048bbe3, tag qcard), but no capture mini was usable. Whether the ⌘↩ answer reaches the agent in the app is also not checked live; the acpmux hub question tests cover that reply path.

Localization: one new key, question.needsInput ("Needs your input"), in all 21 agent-pane locales (needs_review, the same state as the card's other strings). generated/strings.json is regenerated, and gen-strings --check agentPane passes. The session row reuses the existing sidebar.markInput.

Fixup (agent, per Leo)

Leo decided that Esc must not decline. Four commits on top of 3ab5658d2999:

  • Esc on the card sends nothing. Outside the Other field it gives the keyboard to the composer and keeps the answers so far. The card does not claim the key, so the pane's own listeners still see it. The exception is the floating chat, where an Esc nobody claimed would hide the panel, so the card claims it there. Skip is the only decline.
  • A refused chat.permission no longer locks the card. The card shows question.replyFailed ("Couldn't send your answer. Try again.", in all 21 locales, needs_review) with the host's reason, then takes input again. gen-strings --check agentPane passes.
  • One click on the last single-choice question still submits at once, and a test now pins it.
  • plans/cmux-next/agent-questions.md §6 and §7 record the three rules above.

Red/green with CMUX_BROWSER_TESTS=1 bun test test/agent-question-card.test.ts:

  • red at f0d0d47ce3d5: 8 pass / 2 fail (Esc answered and was claimed; a refused reply left the card locked). Green at 05985df7bbca: 10 pass.
  • red at 1c176bf49c95 (floating-chat Esc, from the review): 1 fail. Green at 851cc5b1f9ce: 11 pass.

Observed / Not run

  • Observed: the browser test above on Linux Chromium; prepush-gate.py has no new violations; scripts/web-lint.sh fails only on files this PR does not touch (settings and markdown formatting, findPill.css layers, Swift text-focus), the same as on the branch before.
  • Not run: a tagged macOS build, so the floating chat and a live acpmux refusal are not checked in the app.

Changelog

Changed: An agent's question in the agent pane now shows a "Needs your input" pill over a radio-option card; ⌘↩ submits, Esc returns to the composer, and a refused answer can be retried

Proof

Gallery advisory captures (Linux Chromium, agent-pane.question-card, width 420):

DarkLight
pending single, darkpending single, light
multi-select, darkOther typed, dark
four questions, darkwith preview, dark

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • UI, settings, menu, schema, help-text or user-facing docs change: localization audited, and the result is stated above
  • Reviewed with a subagent before merge (cmux-review), and all bot and human review comments resolved

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Reworks the agent pane's question card to match the Claude app's AskUserQuestion card: an amber "Needs your input" pill over a card with radio (or checkbox) option rows, an inline Other field, and Skip/Submit with a ⌘↩ hint. Esc no longer declines an ask — it returns focus to the composer and keeps the answers given so far (Skip is the only decline) — and a reply the host refuses now shows its error on the card and takes input again for a retry instead of locking the card.

  • The card takes the keyboard on arrival when nothing has it or the composer is empty (the emptiness check reads the editor's text, so a restored draft keeps the keyboard); ↑/↓ and 1–9 pick options, ⌘↩ submits, and one click on the last single-choice question sends it.
  • Esc is left to the pane's own listeners, except in the floating chat, where the card claims it so an unanswered Esc does not hide the panel.
  • The pill's chevron folds the card and keeps picks and Other text; each ask is keyed so a new ask arrives unfolded, and a chat waiting on the person shows an amber "Needs input" label in the session sidebar.
  • Adds question.needsInput and question.replyFailed in all 21 agent-pane locales, a gallery entry rendering the card on the shared CmuxAgentQuestion fixtures, and a Chromium test driving the pane preview and recording the chat.permission call it hands the host.