cmux NEXT composer: the caret stays put when the pane gets the keyboard back

Merged
TE
15d ago
+624-1

Summary

Leo: while typing in the agent pane's composer, the caret jumped to the start of the prompt.

Cause: when the agent pane's web view gives the keyboard to the app (another pane, tab or workspace, the palette) and gets it back, WebKit clears the selection and puts a contenteditable's caret at its start. The prompt editor stays the page's active element, so ProseMirror takes that start caret as the person's, and the next keys land at the front. A textarea gets its old selection back, which is why the classic composer never did this.

Fix (MarkdownField.tsx, keepCaret plugin): the field saves its selection when it or the window loses focus, and when focus comes back without a click it puts that selection back, also over a placement the browser makes in the 100 ms after. A click, any key, a document change or a programmatic caret/value write (setCaret, type, writeText, selectAll) drops the saved selection.

Tests, each typing into the real pane (stand-in acpmux daemon) and checking text, caret and focus after each step, in Chromium and WebKit:

  • agent-pane-inbound-keeps-caret.test.ts:
    • Another agent's queued message, its steer, the turn's end, a re-adopt and a reconnect.
    • Keys typed while the answer streams, then after each of these: a window refocus, the page hiding and showing, a model change, and the keyboard handed back (to the window only, and to the window and the prompt).
  • composer-inbound-keeps-caret.test.tsx (jsdom): the Composer re-rendered through inbound records.

Testing

bun test test/agent-pane-inbound-keeps-caret.test.ts (from webviews/), in CI's react-apps-check shards. Local test runs are blocked on this Mac.

Observed / Not run

  • Observed, red: on e5ce4362 (tests only), both engines failed the step "keyboard handed back to the window". Expected fix the abcderetry test with the caret at 13; got efix the abcdretry test with the caret at 1. That is the reported symptom.
  • Observed, green: on the same head, streaming, window refocus, the page hiding and showing, a model change, inbound queue and steer messages, the turn's end, a re-adopt and a reconnect all kept the caret.
  • Other reds on that head (composer placeholder, @ mentions, suggestions, queue layout, working counts) are base reds: the head added only these two test files to base.
  • Observed, green: on c25c2ed9 (fix + base merge) and 92c06367 both "keyboard handed back" steps and every earlier step pass in Chromium and WebKit. Remaining reds match base (the + tooltip from #23149, check-switch-fetches on base 1369f27 from keptAttachments.ts, stale action surfaces, and transcript timing tests in files this PR does not touch).
  • Observed, real app: in a fleet build of ad3e9b65 on a capture mini, "fix the retry test" was typed and the caret put after "fix the ". The keyboard went to the sidebar and back, without a click on the prompt, while a prompt sent into the chat streamed its reply. "XYZ" was then typed, and the prompt read "fix the XYZretry test". The clip is cropped to the chat (the Inspector showed the mini's name). The inbound message was the showcase chat's mock reply, not a real steer from another agent.

caret keeps its place

Full quality:

Changelog

Fixed: The composer's caret no longer jumps to the start of the prompt after you switch to another pane or tab and come back.

Agent session: cosmic-gray

🤖 Generated with Claude Code