Skip to content

composer: picking another provider's model keeps the typed prompt - #21937

Merged
lawrencecchen merged 3 commits into
feat-cmux-nextfrom
composer-switch-keeps-draft
Oct 19, 2026
Merged

lawrencecchen merged 3 commits into
feat-cmux-nextfrom
composer-switch-keeps-draft

Conversation

@lucasr1b

@lucasr1b lucasr1b commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

picking a model from another provider in the composer (say gpt to claude) wiped whatever you had typed in the prompt.

that pick is a harness switch, so acpmux starts a new session. the composer keeps drafts per session, and when its sessionId changed it swapped in the new session's saved draft, which is empty for a brand new session. the old chat is also hidden while the new one starts, so the id goes old -> none -> new and each step cleared the text.

now the pane passes switching (a harness switch intent is live) to the composer. during a switch the typed prompt stays and becomes the new session's draft (the existing hydrate path then writes it durably). moving to another chat still shows that chat's own draft, same as before.

same-provider model picks never changed the session, so they weren't affected.

Testing

  • added webviews/test/composer-switch-keeps-draft.test.tsx: a switch (session a -> none -> b) keeps the prompt, and moving to another chat still shows that chat's draft.
  • red then green with the same command, bun test test/composer-switch-keeps-draft.test.tsx: on the test commit 9bfc08786a7 1 fail (prompt came back ""), with the fix 2 pass.
  • ran every draft / composer / harness switch / model picker test file (45 files), all pass. bun run typecheck clean, oxlint clean on the touched files.
  • checked live in a tagged dev build: typed a prompt, picked another provider's model, the text stayed.

Changelog

Fixed: Picking a model from another provider no longer clears the prompt you were typing

Proof

behavior fix in the composer, covered by the test above; checked live in a tagged build.

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • Reviewed with a subagent before merge (cmux-review), and all bot and human review comments resolved

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


Summary by cubic

Fixes the composer clearing the typed prompt when picking a model from another provider (say switching from gpt to claude).

Picking another provider's model is a harness switch that starts a new session. The composer keeps drafts per session, so when the session changed it swapped in the new session's empty draft. The pane now passes a switching flag while the harness switch is underway: the typed prompt stays and becomes the new session's draft. A chat's own saved draft wins over a carried prompt, so moving to another chat mid-switch still shows that chat's draft, as before.

Written for commit e086207. Summary will update on new commits.

View guided diff Turn on auto-fix

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: f576a870-2b79-4b4d-b644-04fdf444a30a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Receipt for e086207 (this PR's head; a new push replaces it)

  • Visual review: HQ feed card · gallery before/after runs
  • Checks: this head
  • App of this head (app.zip, kept 1 day): gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr21937 -f ref=composer-switch-keeps-draft -f platform=macos -f backend_mode=local

lucasr1b added a commit that referenced this pull request Oct 19, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Bead cx-bktsu created for this PR by the cmux coordinator.

@lucasr1b

Copy link
Copy Markdown
Collaborator Author

review (claude subagent, correctness first) on head 73c7d84b440, base c77b58ee93e, pr #21937, owner lucasr1b.

no p0 / p1. verified: during a shown switch the composer sees old -> "" -> new and switchView.intent is set at every render of it (leave() and changed() land in one render; attach() emits before the intent clears in run()); cancel paths (chat.select, chat.new, fork, editResend, handoff.prepare) clear the intent a commit before the session moves; the carried text reaches the new session's durable draft through the existing hydrate branch; the old chat keeping its copy is fine (it's the only copy if a switch fails). new tab deferred picks (undefined -> "") were also wiping the text and are covered now.

findings and resolution:

  • p2: a pending, not yet shown switch plus a session change through a path that skips cancel() (chat.adoptLive, chat.offer.open fallback, chat.handoff.start/discard) could let the carried prompt replace that chat's own saved draft. fixed in e086207: a chat's saved draft now wins (restored || carried || draft); carried text only fills a session with no draft, which a switch's new session always is. new test "moving to a chat with its own draft mid-switch keeps that chat's draft" fails on the old ordering and passes now.
  • p2: test used undefined where the app passes "" and missed the leak / same-batch cases. fixed in e086207: uses "", adds settle-then-navigate (expects "") and intent-clears-in-the-same-render (expects the prompt kept).
  • p2, already on feat-cmux-next: prompts handed back by a failed/cancelled switch go into the "" session and are lost if the user then moves to another chat. not changed here, separate fix.

after the fixes on e086207: bun test test/composer-switch-keeps-draft.test.tsx 4 pass; all 45 draft / composer / harness switch / model picker test files pass; bun run typecheck clean; oxlint clean on touched files.

@blacksmith-sh

blacksmith-sh Bot commented Oct 19, 2026

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmphjpyy69m/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpwbcg3ad0/summary-fail.json (live):/summary-fail.json (live): View Logs

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

@lawrencecchen
lawrencecchen merged commit 04fdf7e into feat-cmux-next Oct 19, 2026
@lawrencecchen
lawrencecchen deleted the composer-switch-keeps-draft branch October 19, 2026 06:19
teamleaderleo added a commit that referenced this pull request Oct 19, 2026
…t frame of a click (#21976)

* test(ui): a picker trigger is marked pressed from press to release (red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agent pane): a composer picker keeps its pressed tone on the first frame of a click

Opening a picker moves focus into its popup before the held frame paints,
which clears :active. The trigger carries data-press-active from press to
release, and the composer's picker buttons draw the open-chip tone under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* guards: drop the pull_request_target publish case removed with publish-pr-tree

17bb49c deleted the dead publish-pr-tree job, so release-notary's
'pull_request_target refs/heads/feat-cmux-next publishes once' cases failed
5 times (run 39205353024).

* ci(cmux-next media): rebuild a warm tour's prebuilt modules after a binary pin change (#21971)

* test(ci): a warm tour must rebuild after a GhosttyNextKit pin change

The cmux-next tour keeps each tag's DerivedData warm, and Xcode reused the
explicit module it had precompiled from the old pin's ghostty.h: "ghostty.h
has been modified since the module file ... was built" (job 117892119256).
This regression runs the tour's build step twice with a stand-in reload.sh
that fails the same way; it fails until the fix lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): drop a warm tour's prebuilt modules when a binary pin changes

A Mac that last built a pull request's tag with GhosttyNextKit e6a27a1fb
failed the next tour, which pinned ced99dcde: Xcode copied the new ghostty.h
into Build/Products/Debug/include but kept the explicit module it had
precompiled from the old one (job 117892119256).

scripts/ci/media_module_cache_stamp.py keys a tag's build on every sha256 in
the tracked Package.swift files and every Package.resolved. Before reload.sh
it removes SwiftExplicitPrecompiledModules, ExplicitPrecompiledModules and
ModuleCache.noindex when that key differs from the stamp of the tag's last
good build; after the build it writes the stamp. The rest of DerivedData
stays warm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* gallery advisory: sample the states a PR adds first (#21850)

* gallery advisory: sample the states a PR adds first

A PR that touches a shared file (strings, App.tsx) touches every gallery
entry, so the first 16 states in order came from unrelated entries and the
receipt never showed what the PR added. The manifests now list every touched
state and the sample puts the new ones (no base case with that id) first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* gallery advisory: the base keeps every sampled state before removed ones

The base filter cut at 16 in base order, so removed states early in that
order could push out sampled states that exist in base, which were then
compared as new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0283a51)

* gallery advisory: each stage renders its own manifest

Both stages rendered the joined case list, so the base reported the states a
PR adds as not ready (it has no such variant) and the capture stopped before
the head ran. Now that new states come first, every PR that adds a variant
hit this (#21851's first run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b6d825)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs(CLAUDE.md): merge freezes are issues; drop the WINDOW fallback (#21958)

* docs(CLAUDE.md): merge freezes are issues; drop the WINDOW fallback

The guarded fallback for when gh-merge-green couldn't read the coordinator's
WINDOW no longer applies: freezes are merge-freeze issues that every host
reads through the GitHub API (#21957).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(merge-freeze): gh-merge-green enforces the freeze check

Same line as #21957 on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* apps: preserve provider mutation context for owner handlers (#21406)

Replays #21406's change onto current feat-cmux-next as one commit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(sidebar): a dragged group drops between every group

Regression test for scrawl-1792382497882-1673e-10: on the sidebar
document path a dragged group found a drop only on the thin bands of
each group header. Fails without the fix.

* fix(sidebar): fluid group drag, a drop at every gap between groups

- SidebarDragGeometry: a drop that cannot go into a group (a dragged
  group, groups do not nest) goes before or after the whole group, by
  the half of its block the probe is in, over its header or any member
  row. Before, every member row refused and the gap closed.
- A dragged group's header, not the tall card's centre, decides the drop.
- A refused place keeps the last open gap instead of springing it shut
  and dimming the card, so rows stop jumping during the drag.

* test(layout): a divider drag keeps one daemon command in flight

Adds GestureCommandCoalescer and its tests: 125 live values while one
command is in flight send the first and the newest only; the ended value
goes last; a failed live value with a newer one waiting is superseded.

* fix(layout): stop flooding the daemon during divider and column drags

NIGHTLY sent one set-viewport-pane-width per mouse move (~125/s); each
timed out at 2 s and every other command (pane and tab moves, saves,
attaches) timed out behind them. Divider and column gestures now go
through GestureCommandCoalescer: one command in flight per gesture, only
the newest value waits, the ended value always goes last. A failed live
value with a newer one waiting no longer rolls the divider back.

* scripts(cmux-next): drag-lag-live.py, column/tab/sidebar drag timing on a fleet app

* scripts(cmux-next): drag-lag-live probes the daemon during drags, bigger scene

* ci(nightly-next): one-command rollback of the cmux NEXT feed (#21887)

* ci(nightly-next): one-command rollback of the cmux NEXT feed

nightly-next-rollback.yml takes a bad build, and every newer one, off
appcast-arm64.xml so the feed serves the previous signed build again. It
runs in release-next on branch nightly-next, guards the appcast and R2 key
like publish-nightly, checks the fallback DMG exists, refuses to empty the
feed, and stops when the feed changed while it planned.

docs/ci/nightly-next-canary.md maps the nightly pipeline and its holds and
says what a pull can and cannot do: Sparkle never offers a lower build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(nightly): the delta lane never puts a pulled build back on the feed

republish-nightly-deltas skipped its rewrite only when the live NEXT feed
served a newer build. After nightly-next-rollback.yml pulls build N, the
feed serves N-1 and the lane's two-item appcast would restore N. It now
skips whenever the feed serves any other build. The regression cases in
test_nightly_republish_delta_guard.py fail on the old guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(nightly-next): rollback runs-on keeps a fork branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(nightly-next): the rollback writes the feed only with If-Match

upload-r2-object.py --replace-sha256 HEX reads the object, requires the
bytes the caller planned from, and writes with If-Match on its ETag; a
changed object is not written (--conflict-exit-code). The rollback uses it
instead of a public re-read, so a publication that lands between the plan
and the write is never overwritten. Its summary no longer says "nothing
to pull" when the plan itself failed. (CodeRabbit on #21900.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: mark the sidebar card's Core Animation objects as reviewed motion exceptions (#21951)

* cmux-next: mark the sidebar card's Core Animation objects as reviewed motion exceptions

c5a1f90 built the update card's entrance, exit and crossfade from raw
CABasicAnimation, CASpringAnimation, CATransition and CAMediaTimingFunction, so
check-motion is red on feat-cmux-next (9 hits). Every duration and spring
already comes from the injected MotionPolicy's tokens; Motion.set would write
the model value and read the global policy instead. The 9 lines carry a
motion-allow and the type doc says why. Comments only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the sidebar card's motion-allow note names what stays literal

The curves and the spring's settle tolerance are literal, not tokens; say so,
so the allow does not overclaim (coord-2's review of #21951).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* optchat-chief: auto-resume subagents that stop on a model or network error

A subagent run that ends on an API/harness error is classified
(auto_resume.rs): transient errors (overloaded, 429, 5xx, did not start)
are told to continue after a 30/60/120 s backoff, at most 3 times in a
row; network errors (connection lost, fetch failed, DNS) are not retried
blind: a thread waits until a TCP probe of the model endpoint (the
subrouter from ANTHROPIC_BASE_URL or ~/.claude/settings.json) answers,
then continues. The Chief's chat gets a quiet '[id] stopped: <reason>'
note; the subagent's chat shows each '[auto-resume N]' continue. A stop
auto-resume gives up on is reported as usual. OPTCHAT_AUTO_RESUME=off
turns it off.

* optchat-chief: auto-resume agent CLIs in cmux terminals; fix clone_on_copy

terminal_watch.rs: every 30 s the Chief host lists cmux terminals; a
Claude Code or Codex terminal that reports 'error', or sits idle at an
empty prompt with an API error as its last output, is classified as in
auto_resume.rs. Transient errors get '[auto-resume N] ... continue' typed
at the prompt after a backoff; network errors wait until the model
endpoint answers. Working, blocked (a question) or half-typed prompts are
never touched. Each event reaches the Chief chat as
'[term_...] stopped: <reason>' (a notice while a continue is planned, a
Chief note when it gives up). OPTCHAT_TERMINAL_RESUME=off turns it off.

Also: a short reply that only mentions an error is not a stop, and the
clippy clone_on_copy in workspaces.rs (a75b0c3) is fixed.

* optchat-chief: module order in lib.rs (rustfmt)

* fix(acpmux): clear stop timer on client close (#21615)

* Markdown: cmux:// workspace, pane, tab and chat links open in the app

A cmux:// link in rendered Markdown did nothing or left the app: the
Markdown file page classified every non-web scheme as unsafe, Home only
linked a Chief subagent's chat, and the agent pane drew only http(s).

- One opener, CmuxAppLinks (was ChiefSubagentLinks): a workspace, pane,
  tab or session link in the `cmux` scheme or this build's scheme, or this
  Chief's subagent link, runs link.open. Terminals and pages reach it via
  LinkRouter.appLink.
- Markdown file page: `cmux://...` is link kind "app" (hover: "Opens in
  cmux", 21 locales); cmux.markdown.openLink kind "app" opens it in the app
  or refuses, never the system.
- Home: URL.isCmuxAppLink draws and routes the same links to onAppLink.
- Agent chat: replies draw cmux app links; a click after a real gesture
  goes to the app opener, never NSWorkspace.
- javascript:, file:, data:, vbscript:, ssh: and malformed ids stay text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next palette: Go to Workspace (Cmd-P) searches groups, panes, tabs and agent sessions

Cmd-P now lists sidebar groups, panes and every tab (terminal, browser,
markdown, file, agent chat) next to the workspaces once you type, each row
with its kind and its parent workspace and group. Titles, cwd, URL, group
names and session ids are searchable; whole-title and prefix matches rank
first, then the recently used.

- Picking a group shows its first workspace, expanding the group when
  collapsed (WorkspaceGroupFocus, shared with Next/Previous Workspace Group).
- Picking a pane or tab shows its workspace and focuses it (pane.focus,
  revealTab as Search Tabs).
- Cmd-K in the palette shows the row's Actions menu; agent rows offer Copy
  Session ID: acp:<session id> for acpmux chats (what cmux agent message
  takes), the harness session UUID for Claude Code / Codex terminals.
  Handle format documented in skills/cmux-acpmux/SKILL.md.

* acpmux: require version evidence for already-current updates (#21398)

Replays #21398's change onto current feat-cmux-next as one commit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cx-dgsp follow-ups: scope-bound wire tickets, hold a refused feed token (#21725)

* test(api): the forwarded wire upgrade carries no credential

A native header upgrade with stray bearer.x and ticket.y subprotocols
reaches UserDO with no Authorization header and only cmux.wire.v1.
Green on the current strip; red when wireCredential keeps bearer. or
forwardHeaders keeps Authorization.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(api): a wire ticket opens only the scope it was minted for (red)

POST /v1/wire/ticket takes {scope}; a feed ticket must not open the user
socket, a refused try spends it, an unscoped ticket is a user ticket and
an unknown scope mints nothing. Fails now: the feed ticket opens
/v1/wire/user (101).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud-client): openWire mints its ticket for the scope it opens (red)

Fails now: POST /v1/wire/ticket goes out with no body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): bind a wire ticket to the socket scope it was minted for

POST /v1/wire/ticket takes {scope} (user|team|feed|cloud|conv|mux; no
body or no scope means user, which the dashboard panel and any client
minted before this send). The ticket record keeps the scope; the router
entry still spends a ticket on any path, and wire() refuses one of
another scope with the usual 401. openWire and the dashboard's
wireCredential name their scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cmux-next): a 401 on the feed wire holds the refused token (red)

CloudFeedSource takes an internal Connect seam and reconnect Backoff so
the loop runs against a fake; the socket session throws Unauthorized
when the upgrade's response is 401. The test fails now: the loop opens a
socket for every repeat of the refused token (a, a, a, b), not a then b.
Compile-only on this Linux host; it runs in CmuxNextAppTests on macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): stop reconnecting the feed wire with a token the server refused

A 401 on the upgrade fed the outer reconnect loop, which opened a socket
with the same token every 500 ms..30 s. The loop now remembers the
refused token, reports a disconnected reason once, and opens no socket
with it again: it asks the token provider on the Backoff schedule (no
request reaches the server) until it returns another token. start and
reset still restart the loop on a sign-in or account change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(api): the expired-ticket test mints a feed ticket for the feed socket

With scope binding, an unscoped (user) ticket on /v1/wire/feed is refused
for its scope, so the test no longer reached the expiry check. Red when
WireTicketDO.spend ignores expires_at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the Desktop Wallpaper tile shows the desktop, never a broken image

macOS can report a desktop picture that is gone (a deleted file in /tmp),
a folder it rotates through, or a .madesktop description. The Settings
thumbnail and the window backdrop loaded that URL as is, so the tile drew
the webview's broken-image mark and choosing it showed nothing.

DesktopPicture resolves the reported URL to a picture that decodes: the
file, a picture in the folder, the still a description names, or else a
picture of the desktop fill color. The picker swaps a thumbnail that fails
to load for an empty framed box.

* Home dock: pinned column kind (pinned-dock-v1)

Lawrence 2026-10-18: "I should not be able to close Home inside the Home
thing ... I should not be able to create more tabs in the Home thing
unless I explicitly enable it."

Daemon (cmux-tui): a dock column may be pinned. Its first tab stays (no
close, move out or reorder away from the first place) and it takes no
other tab unless its allow_tabs is on (column.update {allow_tabs}). The
column is never removed, undocked or unpinned. Enforced on every path
(resource ops, JSON-lines, layout undo, terminal tab creation before the
spawn) with reason_code dock-column-pinned. set-column-dock takes pinned;
column.update takes pinned and allow_tabs. Spec, docs, bindings updated.

App: Home pins its left chat dock on connect (page tab first). Home's tab
shows no close button, the strip no new-tab button while the column
takes no tabs, Cmd-W keeps Home's tab, new agent chats open beside a
pinned dock, and a refused close says "Home stays open in its column."

* cmux NEXT: compact attachment strip in the composer (#21739)

* test(acpmux): the attachment strip is compact

Leo 2026-10-18: pasted screenshots took ~100 px off the composer. One row of
64 px tiles 6 px apart, 8 px under the composer's top, the text 6 px under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): compact attachment strip, like the Claude desktop composer

64 px tiles (from 80), a 6 px gap, an 8 px top inset, and the prompt starts
6 px under the strip instead of 15. The strip adds about 78 px, not ~100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): shell field follows the compact strip; tighten the gallery budget

Review: in shell mode under attachments the field and glyph kept the 15 px
inset, so typing ! moved the text 9 px. The attach-screenshot gallery budget
still allowed the old 96 px row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): wait for the thumbnails to load instead of decode()

CI: decode() rejected while a thumbnail's source was swapped for its copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the popup address field draws no focus ring (#21963)

fa8b3fc made the popup URL label selectable without focusRingType = .none, which turns check:text-focus red on every webviews job.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* composer: picking another provider's model keeps the typed prompt (#21937)

* test: a harness switch keeps the typed prompt (red)

* composer: picking another provider's model keeps the typed prompt through the harness switch

* composer: a chat's own saved draft wins over a carried prompt (review)

* fix(acpmux): the empty prompt mid-turn teaches ↵ and ⌘↵ (#21870)

* test(acpmux): a running turn's empty prompt teaches Return and ⌘Return

Leo 2026-10-18: like Claude's "↵ to send, ⌘↵ to queue for later", the
empty prompt mid-turn says "↵ to queue, ⌘↵ to steer" (or interrupt, for an
agent that cannot steer). The Quick Composer keeps "Queue a follow-up".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): the empty prompt mid-turn teaches ↵ and ⌘↵

Mid-turn the empty prompt says "↵ to queue, ⌘↵ to steer" (or "to interrupt"
when the agent cannot take steering), as Claude's composer does. The Quick
Composer keeps "Queue a follow-up", since its ⌘↵ opens the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): an attachment-only prompt mid-turn names only the queue key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): drop the ⌘↵ hint with only attachments, match the Steer button's verb

⌘Return needs typed text, so an attachment-only prompt mid-turn says only
"Queue a follow-up". The es, fr, it, ja, ko and uk hints now use the Steer
button's own verb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(acpmux): say when the queued placeholder still shows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* acpmux: schema doc names peer masking and the lane error cut (#21924)

* docs(acpmux): the schema names peer masking and the lane error cut

The SessionSummary lane note still said every other field stays raw;
since #21871 a peer daemon reads them masked, and since #21799 the lane
error is cut to 3072 characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(acpmux): raw for every client but a proven peer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: every markdown viewer is read-write; Cmd-=/-/0 text size and a default size setting

Root cause: a markdown file opened by cmux open, an agent or a link (not the
user's own Open File...) was writable only inside a files.roots folder. The
roots setting is empty by default and never holds home, so viewers opened
from the Home workspace (and any workspace) came up read only with an
"outside the workspace" note.

- Markdown pages are writable for every document they show; only a real
  reason keeps one read only (not UTF-8, binary, not writable, or a chat
  transcript the host opens read only), and the page names the reason.
  The code editor keeps its root rule.
- The markdown page handles the zoomIn/zoomOut/zoomReset page commands the
  app already sent: one pixel per step (8-40) in rich and source mode,
  Cmd-0 back to markdown.font.size. Cmd-Shift-= is a Zoom In alias.
- Settings > Appearance > Markdown Viewer > Default Font Size
  (markdown.font.size, 16, 8-40), localized.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(home): pasted pictures are small thumbnails in a row in the composer

The vendored MessagesLab compose strip copies macOS 27 Messages: every
attachment is a 177 x 118 pt tile stacked top to bottom above the text.
Two pasted screenshots made the field ~280 pt tall and pushed the text
line to the bottom. Images are now aspect-fit in 128 x 64 pt, files are
200 x 52 pt pills, and tiles flow left to right and wrap at the field's
right edge.

Tests: ComposeAttachmentLayoutTests (MessagesLabHome).

* test(home): qualify Attachment (Swift Testing has one too)

* cmux-next: page strings tables become build output (#21970)

* test(webviews): strings tables must not be tracked

Red on feat-cmux-next: every table gen-strings.mjs writes is committed, and
none is ignored (#21946).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: page strings tables become build output (#21946)

The generated strings tables (webviews/src/**/generated/strings.json, the diff
page's per-locale tables and loader, the home-channels pane table) are no longer
committed. bun install (postinstall, best effort), the webviews test preload
(only stale pages) and every bundle build (strict) write them from the
checkout's xcstrings catalogs and settings schema.

Committed copies conflicted in every pull request that changed a catalog or the
settings schema, and the PR autofix could rebuild one from a stale schema and
drop another branch's labels (#21746). A table made from the tree it sits in
can do neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: merge rules and atomic writes for every strings output (#21946)

The diff page's locale tables and loader and the home-channels pane table are
build output too: route them through cmux-generated-v1 and next_batch's
WEB_GENERATED, so a branch from before the change that edited one catches up
instead of stopping on a modify/delete conflict. gen-strings writes through a
temporary file (parallel test preloads), and --best-effort skips a page whose
catalog or schema is missing instead of failing bun install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at f2ef8f4

- regenerated schemas/settings-export/settings-schema.sources.sha256
- vp fmt webviews/scripts/pages/gen-strings.mjs
- vp fmt webviews/test/strings-build-output.test.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39205550635

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>

* cmux NEXT: a new terminal tab spawns at its pane's grid, so its first frame is final

The Mac's new-tab request sent no size. The daemon then spawned the PTY at
the grid of whichever terminal reported last, anywhere
(client_sizing_state.rs creation_size), the shell printed its first prompt
at that width, and the pane snapped when the view's first report resized it.

newTerminalTab now sends cols/rows (NewTerminalGrid): the fitted grid of the
terminal the pane shows (Ghostty's own fit of the same content host), else
the content host size, backing scale, a live terminal's cell size and the
configured padding through Ghostty's formula (TerminalGridEstimate; Cmd-T's
New Tab page, a page or agent tab). A zoomed terminal does not count. With
neither known the daemon's fallback applies as before.

docs/shared-terminal-sizing.md: a Creation size section (who sizes a new
terminal, and why a later attach never draws a wrong frame for others).

* cmux NEXT: NewTerminalGridTests builds its padding from the public default

* perf-10x daemon: request-latency bench (raw, 10 clients, CLI; 1 and 200 terminals)

* cmux-next: clear five base reds in the cmux-next checks lane (#21981)

The ErrorJournal (1597dc9) and browser popup (fa8b3fc) commits left
five checks red on feat-cmux-next, so every PR into it fails "cmux-next checks"
and its web lane skips its tests. Comments and baselines only, no behavior change:

- Main-thread concurrency: the harvest loop runs in a detached utility task and
  the flush delay in the ErrorJournal actor's task, so neither is on main;
  reviewed concurrency-allow and wakeup-allow comments.
- No literal state roots: the app's ~/.acpmux/daemon.log is only read; a
  keep row in the baseline.
- Package conventions: lint:allow on BrowserPopupWindowGeometry (pure geometry).
- Type scale: BrowserPopupPanel.swift has no raw font sizes left; drop its row.
- Text focus: the popup address field sets focusRingType = .none (was #21963).

Motion tokens (SidebarCardTransition) stays with #21951 for its owner.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cx-ncc.104: Debug toggle for the agent pane's in-session status (agent.sessionStatus) (#20953)

Recut of #20953 onto the current feat-cmux-next as one commit: the
In-session status Debug toggle, its sessionStatus page event, and
useSessionStatus() for the plan and recap cards. The base merge kept
both sides of the tunables list, the view's observations and the page
host's events (the sidebar preview variant landed meanwhile).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* acpmux: route/list says which routes serve the session's family (#21852)

* test(acpmux): route/list names a session's family and which routes serve it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(acpmux): route/list says which routes serve the session's family

With a sessionId, _acpmux/route/list now returns the session's family and a
serves flag on each route (Route::serves), so the composer's Account menu can
list only the routes that can run this chat without copying that logic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: tunable registries list one entry per line (#21972)

* cmux-next: tunable registries list one entry per line (#21946)

AgentPaneTunables.all and SidebarTunables.all were single lines, so every pull
request that added a tunable conflicted with every other one there (14 open
PRs on AgentPaneTunables, 4 on SidebarTunables). Each entry now has its own
line in alphabetical order, so two additions insert different lines and merge
cleanly; TunableCatalog.all lists one module per line. Debug Settings shows the
agent-pane and sidebar tunables alphabetically; nothing else depends on the
order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the TunableCatalog list as it was: CmuxNextApp may not grow

The one-per-line TunableCatalog added 20 source lines to CmuxNextApp, which
check-app-size.sh refuses. The conflict hotspots are the AgentPaneTunables and
SidebarTunables lists, which live in their own targets and stay one per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(next): cover dictation auto-send prompt routing (#21169)

* test(next): cover dictation auto-send prompt routing

* fix(next): satisfy dictation test formatting

* chore(next): PR autofix at 60f4a49

- regenerated Packages/macOS/CmuxNext/ci-target-graph.json
- vp fmt webviews/src/agent-session/acpmux/replyHref.ts
- vp fmt webviews/src/pages/markdown/README.md
- vp fmt webviews/src/pages/markdown/links.ts
- vp fmt webviews/test/markdown-cmux-app-links.test.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39212288176

* chore: drop the autofix bot's unrelated markdown-link and target-graph edits from this PR

They aren't part of this change; base carries its own version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agent pane): a pressed pill keeps the pill's own open tone

The model, effort and access pills open at --acpmux-pill-hover; under the
press marker alone they drew the brighter --acpmux-pill-open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): window blur, Escape and a drag-out release clear the press marker (red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): the press marker clears on window blur, Escape and unmount

A press whose pointerup never reaches the page (Cmd-Tab away, Escape)
left the pill tinted. The plain footer's resting and hover rules also
exclude the marker, so it can't flash there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: Lucas Ribeiro <lucascamribeiro@gmail.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
teamleaderleo added a commit that referenced this pull request Oct 19, 2026
* feat(agent-pane): accept new tab inline suggestions safely

* fix(agent-pane): complete visited URL suggestions

* test(new-tab): Tab without a highlighted row moves focus; history rows are not inline-accepted

Red: Tab is prevented whenever suggestions show, and Right/End inline-accepts
history rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(new-tab): Tab steps only a keyboard-highlighted row; inline completion skips history rows

Tab is taken only after Down/Up/Tab moved the highlight, so keyboard users can
Tab to Send. Right/End inline-accept only top-row open results (typed address
or top site), matching the comment that history rows stay explicit picks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* guards: drop the pull_request_target publish case removed with publish-pr-tree

17bb49c deleted the dead publish-pr-tree job, so release-notary's
'pull_request_target refs/heads/feat-cmux-next publishes once' cases failed
5 times (run 39205353024).

* ci(cmux-next media): rebuild a warm tour's prebuilt modules after a binary pin change (#21971)

* test(ci): a warm tour must rebuild after a GhosttyNextKit pin change

The cmux-next tour keeps each tag's DerivedData warm, and Xcode reused the
explicit module it had precompiled from the old pin's ghostty.h: "ghostty.h
has been modified since the module file ... was built" (job 117892119256).
This regression runs the tour's build step twice with a stand-in reload.sh
that fails the same way; it fails until the fix lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): drop a warm tour's prebuilt modules when a binary pin changes

A Mac that last built a pull request's tag with GhosttyNextKit e6a27a1fb
failed the next tour, which pinned ced99dcde: Xcode copied the new ghostty.h
into Build/Products/Debug/include but kept the explicit module it had
precompiled from the old one (job 117892119256).

scripts/ci/media_module_cache_stamp.py keys a tag's build on every sha256 in
the tracked Package.swift files and every Package.resolved. Before reload.sh
it removes SwiftExplicitPrecompiledModules, ExplicitPrecompiledModules and
ModuleCache.noindex when that key differs from the stamp of the tag's last
good build; after the build it writes the stamp. The rest of DerivedData
stays warm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* gallery advisory: sample the states a PR adds first (#21850)

* gallery advisory: sample the states a PR adds first

A PR that touches a shared file (strings, App.tsx) touches every gallery
entry, so the first 16 states in order came from unrelated entries and the
receipt never showed what the PR added. The manifests now list every touched
state and the sample puts the new ones (no base case with that id) first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* gallery advisory: the base keeps every sampled state before removed ones

The base filter cut at 16 in base order, so removed states early in that
order could push out sampled states that exist in base, which were then
compared as new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0283a51)

* gallery advisory: each stage renders its own manifest

Both stages rendered the joined case list, so the base reported the states a
PR adds as not ready (it has no such variant) and the capture stopped before
the head ran. Now that new states come first, every PR that adds a variant
hit this (#21851's first run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b6d825)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs(CLAUDE.md): merge freezes are issues; drop the WINDOW fallback (#21958)

* docs(CLAUDE.md): merge freezes are issues; drop the WINDOW fallback

The guarded fallback for when gh-merge-green couldn't read the coordinator's
WINDOW no longer applies: freezes are merge-freeze issues that every host
reads through the GitHub API (#21957).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(merge-freeze): gh-merge-green enforces the freeze check

Same line as #21957 on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* apps: preserve provider mutation context for owner handlers (#21406)

Replays #21406's change onto current feat-cmux-next as one commit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(sidebar): a dragged group drops between every group

Regression test for scrawl-1792382497882-1673e-10: on the sidebar
document path a dragged group found a drop only on the thin bands of
each group header. Fails without the fix.

* fix(sidebar): fluid group drag, a drop at every gap between groups

- SidebarDragGeometry: a drop that cannot go into a group (a dragged
  group, groups do not nest) goes before or after the whole group, by
  the half of its block the probe is in, over its header or any member
  row. Before, every member row refused and the gap closed.
- A dragged group's header, not the tall card's centre, decides the drop.
- A refused place keeps the last open gap instead of springing it shut
  and dimming the card, so rows stop jumping during the drag.

* test(layout): a divider drag keeps one daemon command in flight

Adds GestureCommandCoalescer and its tests: 125 live values while one
command is in flight send the first and the newest only; the ended value
goes last; a failed live value with a newer one waiting is superseded.

* fix(layout): stop flooding the daemon during divider and column drags

NIGHTLY sent one set-viewport-pane-width per mouse move (~125/s); each
timed out at 2 s and every other command (pane and tab moves, saves,
attaches) timed out behind them. Divider and column gestures now go
through GestureCommandCoalescer: one command in flight per gesture, only
the newest value waits, the ended value always goes last. A failed live
value with a newer one waiting no longer rolls the divider back.

* scripts(cmux-next): drag-lag-live.py, column/tab/sidebar drag timing on a fleet app

* scripts(cmux-next): drag-lag-live probes the daemon during drags, bigger scene

* ci(nightly-next): one-command rollback of the cmux NEXT feed (#21887)

* ci(nightly-next): one-command rollback of the cmux NEXT feed

nightly-next-rollback.yml takes a bad build, and every newer one, off
appcast-arm64.xml so the feed serves the previous signed build again. It
runs in release-next on branch nightly-next, guards the appcast and R2 key
like publish-nightly, checks the fallback DMG exists, refuses to empty the
feed, and stops when the feed changed while it planned.

docs/ci/nightly-next-canary.md maps the nightly pipeline and its holds and
says what a pull can and cannot do: Sparkle never offers a lower build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(nightly): the delta lane never puts a pulled build back on the feed

republish-nightly-deltas skipped its rewrite only when the live NEXT feed
served a newer build. After nightly-next-rollback.yml pulls build N, the
feed serves N-1 and the lane's two-item appcast would restore N. It now
skips whenever the feed serves any other build. The regression cases in
test_nightly_republish_delta_guard.py fail on the old guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(nightly-next): rollback runs-on keeps a fork branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(nightly-next): the rollback writes the feed only with If-Match

upload-r2-object.py --replace-sha256 HEX reads the object, requires the
bytes the caller planned from, and writes with If-Match on its ETag; a
changed object is not written (--conflict-exit-code). The rollback uses it
instead of a public re-read, so a publication that lands between the plan
and the write is never overwritten. Its summary no longer says "nothing
to pull" when the plan itself failed. (CodeRabbit on #21900.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: mark the sidebar card's Core Animation objects as reviewed motion exceptions (#21951)

* cmux-next: mark the sidebar card's Core Animation objects as reviewed motion exceptions

c5a1f90 built the update card's entrance, exit and crossfade from raw
CABasicAnimation, CASpringAnimation, CATransition and CAMediaTimingFunction, so
check-motion is red on feat-cmux-next (9 hits). Every duration and spring
already comes from the injected MotionPolicy's tokens; Motion.set would write
the model value and read the global policy instead. The 9 lines carry a
motion-allow and the type doc says why. Comments only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the sidebar card's motion-allow note names what stays literal

The curves and the spring's settle tolerance are literal, not tokens; say so,
so the allow does not overclaim (coord-2's review of #21951).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* optchat-chief: auto-resume subagents that stop on a model or network error

A subagent run that ends on an API/harness error is classified
(auto_resume.rs): transient errors (overloaded, 429, 5xx, did not start)
are told to continue after a 30/60/120 s backoff, at most 3 times in a
row; network errors (connection lost, fetch failed, DNS) are not retried
blind: a thread waits until a TCP probe of the model endpoint (the
subrouter from ANTHROPIC_BASE_URL or ~/.claude/settings.json) answers,
then continues. The Chief's chat gets a quiet '[id] stopped: <reason>'
note; the subagent's chat shows each '[auto-resume N]' continue. A stop
auto-resume gives up on is reported as usual. OPTCHAT_AUTO_RESUME=off
turns it off.

* optchat-chief: auto-resume agent CLIs in cmux terminals; fix clone_on_copy

terminal_watch.rs: every 30 s the Chief host lists cmux terminals; a
Claude Code or Codex terminal that reports 'error', or sits idle at an
empty prompt with an API error as its last output, is classified as in
auto_resume.rs. Transient errors get '[auto-resume N] ... continue' typed
at the prompt after a backoff; network errors wait until the model
endpoint answers. Working, blocked (a question) or half-typed prompts are
never touched. Each event reaches the Chief chat as
'[term_...] stopped: <reason>' (a notice while a continue is planned, a
Chief note when it gives up). OPTCHAT_TERMINAL_RESUME=off turns it off.

Also: a short reply that only mentions an error is not a stop, and the
clippy clone_on_copy in workspaces.rs (a75b0c3) is fixed.

* optchat-chief: module order in lib.rs (rustfmt)

* test(new-tab): a visited page completes inline on Right and opens nothing (red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(new-tab): a visited page completes inline again

The Tab fix also dropped history rows from inline completion to match an
older comment, undoing 1d8f0db. Accepting a completion only fills the field
on an explicit Right or End, and Enter or Send still opens it, so a visited
page completes as in Chrome. The comment now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): clear stop timer on client close (#21615)

* Markdown: cmux:// workspace, pane, tab and chat links open in the app

A cmux:// link in rendered Markdown did nothing or left the app: the
Markdown file page classified every non-web scheme as unsafe, Home only
linked a Chief subagent's chat, and the agent pane drew only http(s).

- One opener, CmuxAppLinks (was ChiefSubagentLinks): a workspace, pane,
  tab or session link in the `cmux` scheme or this build's scheme, or this
  Chief's subagent link, runs link.open. Terminals and pages reach it via
  LinkRouter.appLink.
- Markdown file page: `cmux://...` is link kind "app" (hover: "Opens in
  cmux", 21 locales); cmux.markdown.openLink kind "app" opens it in the app
  or refuses, never the system.
- Home: URL.isCmuxAppLink draws and routes the same links to onAppLink.
- Agent chat: replies draw cmux app links; a click after a real gesture
  goes to the app opener, never NSWorkspace.
- javascript:, file:, data:, vbscript:, ssh: and malformed ids stay text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next palette: Go to Workspace (Cmd-P) searches groups, panes, tabs and agent sessions

Cmd-P now lists sidebar groups, panes and every tab (terminal, browser,
markdown, file, agent chat) next to the workspaces once you type, each row
with its kind and its parent workspace and group. Titles, cwd, URL, group
names and session ids are searchable; whole-title and prefix matches rank
first, then the recently used.

- Picking a group shows its first workspace, expanding the group when
  collapsed (WorkspaceGroupFocus, shared with Next/Previous Workspace Group).
- Picking a pane or tab shows its workspace and focuses it (pane.focus,
  revealTab as Search Tabs).
- Cmd-K in the palette shows the row's Actions menu; agent rows offer Copy
  Session ID: acp:<session id> for acpmux chats (what cmux agent message
  takes), the harness session UUID for Claude Code / Codex terminals.
  Handle format documented in skills/cmux-acpmux/SKILL.md.

* acpmux: require version evidence for already-current updates (#21398)

Replays #21398's change onto current feat-cmux-next as one commit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cx-dgsp follow-ups: scope-bound wire tickets, hold a refused feed token (#21725)

* test(api): the forwarded wire upgrade carries no credential

A native header upgrade with stray bearer.x and ticket.y subprotocols
reaches UserDO with no Authorization header and only cmux.wire.v1.
Green on the current strip; red when wireCredential keeps bearer. or
forwardHeaders keeps Authorization.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(api): a wire ticket opens only the scope it was minted for (red)

POST /v1/wire/ticket takes {scope}; a feed ticket must not open the user
socket, a refused try spends it, an unscoped ticket is a user ticket and
an unknown scope mints nothing. Fails now: the feed ticket opens
/v1/wire/user (101).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud-client): openWire mints its ticket for the scope it opens (red)

Fails now: POST /v1/wire/ticket goes out with no body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): bind a wire ticket to the socket scope it was minted for

POST /v1/wire/ticket takes {scope} (user|team|feed|cloud|conv|mux; no
body or no scope means user, which the dashboard panel and any client
minted before this send). The ticket record keeps the scope; the router
entry still spends a ticket on any path, and wire() refuses one of
another scope with the usual 401. openWire and the dashboard's
wireCredential name their scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cmux-next): a 401 on the feed wire holds the refused token (red)

CloudFeedSource takes an internal Connect seam and reconnect Backoff so
the loop runs against a fake; the socket session throws Unauthorized
when the upgrade's response is 401. The test fails now: the loop opens a
socket for every repeat of the refused token (a, a, a, b), not a then b.
Compile-only on this Linux host; it runs in CmuxNextAppTests on macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): stop reconnecting the feed wire with a token the server refused

A 401 on the upgrade fed the outer reconnect loop, which opened a socket
with the same token every 500 ms..30 s. The loop now remembers the
refused token, reports a disconnected reason once, and opens no socket
with it again: it asks the token provider on the Backoff schedule (no
request reaches the server) until it returns another token. start and
reset still restart the loop on a sign-in or account change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(api): the expired-ticket test mints a feed ticket for the feed socket

With scope binding, an unscoped (user) ticket on /v1/wire/feed is refused
for its scope, so the test no longer reached the expiry check. Red when
WireTicketDO.spend ignores expires_at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the Desktop Wallpaper tile shows the desktop, never a broken image

macOS can report a desktop picture that is gone (a deleted file in /tmp),
a folder it rotates through, or a .madesktop description. The Settings
thumbnail and the window backdrop loaded that URL as is, so the tile drew
the webview's broken-image mark and choosing it showed nothing.

DesktopPicture resolves the reported URL to a picture that decodes: the
file, a picture in the folder, the still a description names, or else a
picture of the desktop fill color. The picker swaps a thumbnail that fails
to load for an empty framed box.

* Home dock: pinned column kind (pinned-dock-v1)

Lawrence 2026-10-18: "I should not be able to close Home inside the Home
thing ... I should not be able to create more tabs in the Home thing
unless I explicitly enable it."

Daemon (cmux-tui): a dock column may be pinned. Its first tab stays (no
close, move out or reorder away from the first place) and it takes no
other tab unless its allow_tabs is on (column.update {allow_tabs}). The
column is never removed, undocked or unpinned. Enforced on every path
(resource ops, JSON-lines, layout undo, terminal tab creation before the
spawn) with reason_code dock-column-pinned. set-column-dock takes pinned;
column.update takes pinned and allow_tabs. Spec, docs, bindings updated.

App: Home pins its left chat dock on connect (page tab first). Home's tab
shows no close button, the strip no new-tab button while the column
takes no tabs, Cmd-W keeps Home's tab, new agent chats open beside a
pinned dock, and a refused close says "Home stays open in its column."

* cmux NEXT: compact attachment strip in the composer (#21739)

* test(acpmux): the attachment strip is compact

Leo 2026-10-18: pasted screenshots took ~100 px off the composer. One row of
64 px tiles 6 px apart, 8 px under the composer's top, the text 6 px under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): compact attachment strip, like the Claude desktop composer

64 px tiles (from 80), a 6 px gap, an 8 px top inset, and the prompt starts
6 px under the strip instead of 15. The strip adds about 78 px, not ~100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): shell field follows the compact strip; tighten the gallery budget

Review: in shell mode under attachments the field and glyph kept the 15 px
inset, so typing ! moved the text 9 px. The attach-screenshot gallery budget
still allowed the old 96 px row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): wait for the thumbnails to load instead of decode()

CI: decode() rejected while a thumbnail's source was swapped for its copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the popup address field draws no focus ring (#21963)

fa8b3fc made the popup URL label selectable without focusRingType = .none, which turns check:text-focus red on every webviews job.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* composer: picking another provider's model keeps the typed prompt (#21937)

* test: a harness switch keeps the typed prompt (red)

* composer: picking another provider's model keeps the typed prompt through the harness switch

* composer: a chat's own saved draft wins over a carried prompt (review)

* fix(acpmux): the empty prompt mid-turn teaches ↵ and ⌘↵ (#21870)

* test(acpmux): a running turn's empty prompt teaches Return and ⌘Return

Leo 2026-10-18: like Claude's "↵ to send, ⌘↵ to queue for later", the
empty prompt mid-turn says "↵ to queue, ⌘↵ to steer" (or interrupt, for an
agent that cannot steer). The Quick Composer keeps "Queue a follow-up".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): the empty prompt mid-turn teaches ↵ and ⌘↵

Mid-turn the empty prompt says "↵ to queue, ⌘↵ to steer" (or "to interrupt"
when the agent cannot take steering), as Claude's composer does. The Quick
Composer keeps "Queue a follow-up", since its ⌘↵ opens the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): an attachment-only prompt mid-turn names only the queue key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): drop the ⌘↵ hint with only attachments, match the Steer button's verb

⌘Return needs typed text, so an attachment-only prompt mid-turn says only
"Queue a follow-up". The es, fr, it, ja, ko and uk hints now use the Steer
button's own verb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(acpmux): say when the queued placeholder still shows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* acpmux: schema doc names peer masking and the lane error cut (#21924)

* docs(acpmux): the schema names peer masking and the lane error cut

The SessionSummary lane note still said every other field stays raw;
since #21871 a peer daemon reads them masked, and since #21799 the lane
error is cut to 3072 characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(acpmux): raw for every client but a proven peer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: every markdown viewer is read-write; Cmd-=/-/0 text size and a default size setting

Root cause: a markdown file opened by cmux open, an agent or a link (not the
user's own Open File...) was writable only inside a files.roots folder. The
roots setting is empty by default and never holds home, so viewers opened
from the Home workspace (and any workspace) came up read only with an
"outside the workspace" note.

- Markdown pages are writable for every document they show; only a real
  reason keeps one read only (not UTF-8, binary, not writable, or a chat
  transcript the host opens read only), and the page names the reason.
  The code editor keeps its root rule.
- The markdown page handles the zoomIn/zoomOut/zoomReset page commands the
  app already sent: one pixel per step (8-40) in rich and source mode,
  Cmd-0 back to markdown.font.size. Cmd-Shift-= is a Zoom In alias.
- Settings > Appearance > Markdown Viewer > Default Font Size
  (markdown.font.size, 16, 8-40), localized.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(home): pasted pictures are small thumbnails in a row in the composer

The vendored MessagesLab compose strip copies macOS 27 Messages: every
attachment is a 177 x 118 pt tile stacked top to bottom above the text.
Two pasted screenshots made the field ~280 pt tall and pushed the text
line to the bottom. Images are now aspect-fit in 128 x 64 pt, files are
200 x 52 pt pills, and tiles flow left to right and wrap at the field's
right edge.

Tests: ComposeAttachmentLayoutTests (MessagesLabHome).

* test(home): qualify Attachment (Swift Testing has one too)

* cmux-next: page strings tables become build output (#21970)

* test(webviews): strings tables must not be tracked

Red on feat-cmux-next: every table gen-strings.mjs writes is committed, and
none is ignored (#21946).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: page strings tables become build output (#21946)

The generated strings tables (webviews/src/**/generated/strings.json, the diff
page's per-locale tables and loader, the home-channels pane table) are no longer
committed. bun install (postinstall, best effort), the webviews test preload
(only stale pages) and every bundle build (strict) write them from the
checkout's xcstrings catalogs and settings schema.

Committed copies conflicted in every pull request that changed a catalog or the
settings schema, and the PR autofix could rebuild one from a stale schema and
drop another branch's labels (#21746). A table made from the tree it sits in
can do neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: merge rules and atomic writes for every strings output (#21946)

The diff page's locale tables and loader and the home-channels pane table are
build output too: route them through cmux-generated-v1 and next_batch's
WEB_GENERATED, so a branch from before the change that edited one catches up
instead of stopping on a modify/delete conflict. gen-strings writes through a
temporary file (parallel test preloads), and --best-effort skips a page whose
catalog or schema is missing instead of failing bun install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at f2ef8f4

- regenerated schemas/settings-export/settings-schema.sources.sha256
- vp fmt webviews/scripts/pages/gen-strings.mjs
- vp fmt webviews/test/strings-build-output.test.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39205550635

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>

* cmux NEXT: a new terminal tab spawns at its pane's grid, so its first frame is final

The Mac's new-tab request sent no size. The daemon then spawned the PTY at
the grid of whichever terminal reported last, anywhere
(client_sizing_state.rs creation_size), the shell printed its first prompt
at that width, and the pane snapped when the view's first report resized it.

newTerminalTab now sends cols/rows (NewTerminalGrid): the fitted grid of the
terminal the pane shows (Ghostty's own fit of the same content host), else
the content host size, backing scale, a live terminal's cell size and the
configured padding through Ghostty's formula (TerminalGridEstimate; Cmd-T's
New Tab page, a page or agent tab). A zoomed terminal does not count. With
neither known the daemon's fallback applies as before.

docs/shared-terminal-sizing.md: a Creation size section (who sizes a new
terminal, and why a later attach never draws a wrong frame for others).

* cmux NEXT: NewTerminalGridTests builds its padding from the public default

* perf-10x daemon: request-latency bench (raw, 10 clients, CLI; 1 and 200 terminals)

* cmux-next: clear five base reds in the cmux-next checks lane (#21981)

The ErrorJournal (1597dc9) and browser popup (fa8b3fc) commits left
five checks red on feat-cmux-next, so every PR into it fails "cmux-next checks"
and its web lane skips its tests. Comments and baselines only, no behavior change:

- Main-thread concurrency: the harvest loop runs in a detached utility task and
  the flush delay in the ErrorJournal actor's task, so neither is on main;
  reviewed concurrency-allow and wakeup-allow comments.
- No literal state roots: the app's ~/.acpmux/daemon.log is only read; a
  keep row in the baseline.
- Package conventions: lint:allow on BrowserPopupWindowGeometry (pure geometry).
- Type scale: BrowserPopupPanel.swift has no raw font sizes left; drop its row.
- Text focus: the popup address field sets focusRingType = .none (was #21963).

Motion tokens (SidebarCardTransition) stays with #21951 for its owner.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cx-ncc.104: Debug toggle for the agent pane's in-session status (agent.sessionStatus) (#20953)

Recut of #20953 onto the current feat-cmux-next as one commit: the
In-session status Debug toggle, its sessionStatus page event, and
useSessionStatus() for the plan and recap cards. The base merge kept
both sides of the tunables list, the view's observations and the page
host's events (the sidebar preview variant landed meanwhile).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* acpmux: route/list says which routes serve the session's family (#21852)

* test(acpmux): route/list names a session's family and which routes serve it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(acpmux): route/list says which routes serve the session's family

With a sessionId, _acpmux/route/list now returns the session's family and a
serves flag on each route (Route::serves), so the composer's Account menu can
list only the routes that can run this chat without copying that logic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: tunable registries list one entry per line (#21972)

* cmux-next: tunable registries list one entry per line (#21946)

AgentPaneTunables.all and SidebarTunables.all were single lines, so every pull
request that added a tunable conflicted with every other one there (14 open
PRs on AgentPaneTunables, 4 on SidebarTunables). Each entry now has its own
line in alphabetical order, so two additions insert different lines and merge
cleanly; TunableCatalog.all lists one module per line. Debug Settings shows the
agent-pane and sidebar tunables alphabetically; nothing else depends on the
order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the TunableCatalog list as it was: CmuxNextApp may not grow

The one-per-line TunableCatalog added 20 source lines to CmuxNextApp, which
check-app-size.sh refuses. The conflict hotspots are the AgentPaneTunables and
SidebarTunables lists, which live in their own targets and stay one per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(next): cover dictation auto-send prompt routing (#21169)

* test(next): cover dictation auto-send prompt routing

* fix(next): satisfy dictation test formatting

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: Lucas Ribeiro <lucascamribeiro@gmail.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants