cmux NEXT agent pane: a refused answer shows one message, not two glued together

Merged
TE
14d ago
+195-2

When an answer on a question card was refused, the card appended the refusal's raw message to its own sentence. With acpmux's person-required refusal that read "Couldn’t send your answer. Try again. approve this on the Mac app: allowing a permission is accepted only from the cmux app, never from the CLI, a script or an agent": two messages run together, the second in acpmux's CLI English.

The card now shows one plain message, chosen by where the error came from:

  • A refusal the pane has words for (a host refusal, isHostRefusal in transportErrors.ts) shows those words alone.
  • A message already in the person's language follows the card's sentence, as before. That covers the Swift host's userMessage (a NativeError, e.g. "The agent is busy." or "acpmux is not running. Open a new agent chat to start it.") and the pane's own native.* words ("The agent connection is not open.", timed out, interrupted).
  • Anything else is acpmux's English for its CLI. It shows only "Couldn’t send your answer. Try again." and goes to the console.

The review of the first head found that it also hid the localized messages, which broke agent-question-card.test.ts ("…Try again. The agent is busy."). This head keeps those messages; only acpmux's English is dropped.

acpmux's permission.person_required gets its own sentence in #23890; until that lands, this card shows "Couldn’t send your answer. Try again." for it.

Follow-up (not in this PR): a question-card reply that carries answers never gets a gesture ticket (direct.ts permission(), ~4026, asks for one only when answers === undefined). That is the likely root cause of the person-required refusal in the live report, so "Try again" there may hit the same refusal.

Risk: normal. One component's error line; no wire or behavior change.

Changelog

  • Fixed: A refused answer on an agent question card shows one plain message instead of two run together.

Test

webviews/test/question-card-reply-failed.test.tsx (jsdom): renders the card with a host that refuses the reply, answers it, and reads the alert line. Cases: acpmux's person-required refusal, acpmux's English for an unmapped reason (exact text), an unknown failure, a transport refusal, a NativeError host message, and the pane's error.notOpen.

Observed / Not run

Observed:

  • The fleet steps above.
  • scripts/cmux-next/prepush-gate.py: no new violations.
  • vp fmt left the changed files unchanged.

Not run:

  • agent-question-card.test.ts's browser-lane cases, observed. That file reported PASS on both the red and the green fleet step, so its Playwright cases most likely skipped there. CI's react-apps-check is what runs them. That its "The agent is busy." expectation now holds comes from reading the code: the stubbed { ok: false, error: { userMessage } } reply becomes a NativeError in native.ts.
  • The full webviews suite, bun run check, and a live refused answer in the app.

No capture: the error line keeps its look; only its text changes.

Agent session: cosmic-gray

🤖 Generated with Claude Code