Skip to content

cmux-next iOS: a drag in a TUI scrolls the app; groups stay collapsed; bottom bounce after a fling - #24081

Merged
azooz2003-bit merged 15 commits into
feat-cmux-nextfrom
feat-cmux-next-ios-tui-scroll
Oct 22, 2026
Merged

azooz2003-bit merged 15 commits into
feat-cmux-nextfrom
feat-cmux-next-ios-tui-scroll

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Two bugs the user reported in the cmux-next iPhone app.

1. "I can't scroll in TUI mode because the TUI app controls the scroll."
In alternate-screen mode (less, vim, htop, Codex, Claude Code) the page is pinned, and nothing turned a drag into input for the app. Now a vertical drag on the TUI scrolls the app the way a Mac trackpad does in Ghostty. The drag becomes precise wheel events through Ghostty's own ghostty_surface_mouse_pos + ghostty_surface_mouse_scroll, 1:1 with the finger in the natural direction, with momentum after a fling (decay 0.95 per 1/60 s, stopped by a new touch). Ghostty's mirror surface already tracks the app's DEC modes, so it decides what the app gets:

  • an app that asked for the mouse (1000/1002/1003 + 1006) gets SGR wheel reports, 64 up and 65 down, at the touched cell;
  • the alternate screen with alternate scroll (1007, on by default) gets ESC [ A/B, or ESC O A/B in application cursor mode;
  • with 1007 off it gets nothing.

The page never scrolls in this mode. Taps still focus the terminal. A long press still selects, and a drag clears the selection. Selection handles still move. Keyboard and composer behavior is unchanged. ACCEPTANCE.md T-SCROLL-12 and T-AGENT-08 now describe this.

2. "Disclosure groups not being properly closable."

  • Cause: a workspace group's collapse shows at once, then goes to the Mac. Until the Mac answers and the list's cached arrangement is re-read, any tree refresh rebuilds the rows from the old arrangement. Refreshes come from terminal title changes, agent events and notifications, which arrive constantly with an active agent. So the group popped open again, and a second tap seemed needed. Two quick taps could also end on the first tap: a refused first tap rolled back over the second.
  • Fix: PendingGroupEdits holds a group edit (collapse, rename, color, delete, move) over AppStore.replaceWorkspaces. It holds while the request is in flight, then after the Mac's yes until a refresh shows the edit or 10 s pass. After that the Mac's state wins again, since another device can change it. Only the latest edit of a workspace rolls back on refusal.
  • The group header's tap target now covers its row, about 32 pt instead of the text line of about 17 pt. The layout is the same.
  • Agent chat:
    • A live "Thinking" row closes on the first tap. Before, the text stayed on screen while the chevron turned to closed.
    • A tool group's header wins the 6 pt overlap with its first call's hit target.
    • A running call's expansion no longer carries over to the next call.

Every DisclosureGroup or expandable section was surveyed. Feed, Settings and the sidebar showed no state-binding fault: Settings and sidebar chevrons navigate, and the Feed "Show more" is bound to a stable Set<String>. CmuxTabsKit and CmuxConversation are untouched.

3. "Sometimes the terminal doesn't have bottom content bounciness in its scroll."

  • Cause: a fling toward newer rows that reached the bottom stopped the deceleration dead (setContentOffset(contentOffset, animated: false)) so the page could switch to following. A fling into the end had no rubber band; a slow drag past it did.
  • Fix: the fling switches to following (TerminalScrollModel.flingReachedBottom()) and UIKit finishes the deceleration and its bounce. The end of the deceleration lands on the newest end, as before.

The review fixes are listed in the review comment.

Tests

Swift tests ran on mini-cmux11s in the iOS simulator: cmux-next-ios/verify/termqa/cmux_mobile_kit-test-ios.sh CmuxMobileTerminalTests CmuxMobileModelTests CmuxMobileAgentChatTests/DisclosureChoiceTests CmuxMobileWorkspacesTests.

  • TerminalTUIScrollSurfaceTests uses a real Ghostty mirror surface. It feeds the DEC modes, then checks the exact bytes onInput sends:
    • ESC[<64;4;6M ×3 with mouse reporting on;
    • ESC[A on the alternate screen;
    • ESC OA in application cursor mode;
    • nothing with 1007 off;
    • a fraction of a row waits for more.
  • TerminalTUIScrollTests covers drag deltas, scroll mods packing, the vertical-only rule, and momentum decay, end and stop.
  • WorkspaceGroupCreatorTests covers a refresh during a collapse, an unconfirmed edit expiring, quick taps, and a refused collapse.
  • DisclosureChoiceTests covers the live thought row.

Red on base:

  • The failing tests were committed first (778da28, 4feb6cc). The TUI tests did not build without the fix, because TerminalTUIScroll and scrollWheel were missing.
  • Behavioral red for groups: with groupEdits.apply disabled, aRefreshDuringACollapseKeepsTheGroupClosed, quickTapsEndOnTheLastTap and anUnconfirmedEditGivesWayToTheMacAfterAWhile fail. The first fails with "isCollapsed → false".

Green on head: all four targets passed, including every new test.

Observed / Not run

  • Observed: the focused suites above ran on mini-cmux11s. scripts/cmux-next/prepush-gate.py reported no new violations.
  • The live check over the real link (simulator plus a hidden Mac rig; less, htop and a real agent session; group collapse and expand) is reported in a PR comment.

Changelog

Fixed: In the iPhone app, dragging over a full-screen terminal app (less, htop, vim, Codex, Claude Code) now scrolls that app, workspace groups stay collapsed after a tap, and a fling into the bottom of a terminal bounces.

No capture: iOS app change (not the Mac app). Live simulator video and frames are in a PR comment.

🤖 Generated with Claude Code

azooz2003-bit and others added 5 commits October 21, 2026 19:16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fresh

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

TUI mode (alternate screen): a vertical drag becomes precise wheel events
through ghostty_surface_mouse_scroll, with momentum after a fling, so a
mouse-reporting app gets SGR wheel reports at the finger and the alternate
screen gets cursor keys (DECSET 1007, Ghostty's default). The page stays
pinned; taps, long-press selection and handles keep working.

Workspace groups: a collapse (or rename, color, delete, move) is held over
tree refreshes until the Mac's list shows it (PendingGroupEdits), and a
refused earlier tap no longer undoes a later one. The group header's tap
target covers its row. Agent chat: a live thought closes on the first tap;
a tool group's header wins over its first call; a running call's
expansion no longer carries over to the next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@azooz2003-bit azooz2003-bit added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review a6d6f76: HQ feed card · gallery before/after runs · checks

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=pr24081 -f ref=feat-cmux-next-ios-tui-scroll -f platform=macos -f backend_mode=local

@blacksmith-sh

blacksmith-sh Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
nightly cmux-tui resolve/nightly cmux-tui resolve View Logs
the cmux NIGHTLY-identity jobs (sign, verify, publish) must contain: - name: Skip a bui
ld older than the old feed's/
the cmux NIGHTLY-identity jobs (sign, verify, publish) must contain: - name: Skip a bui
ld older than the old feed's
View Logs

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator

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

… bounce

Review fixes:
- group requests go to a Mac one after another, so quick taps land in tap
  order;
- a new group supersedes an earlier pending move;
- an edit the Mac never answers gives way after 30 s;
- group edits reset at sign-in;
- a tap that stops a TUI fling does not focus the terminal;
- the wheel stays at the cell where the drag began;
- a slow drag is judged by its translation;
- surface tests cover 1002/1003 and the grid padding.

Bottom bounce: a fling toward newer rows that reached the bottom stopped
the deceleration dead (setContentOffset to itself) to switch to following,
so it had no rubber band while a slow drag did. It now switches to
following and lets UIKit finish the deceleration and its bounce.

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

Copy link
Copy Markdown
Collaborator Author

Correctness review (fresh review subagent) and resolution

The review covered the PR's own commits. It confirmed these are correct:

  • scroll direction (natural, wheel 64 for a finger moving down);
  • units (points × the surface's content scale, Ghostty's precision multiplier 1);
  • ScrollMods packing;
  • display-link lifetime;
  • the group header's layout height (unchanged);
  • that the live refresh path goes through AppStore.replaceWorkspaces.
# Finding Severity Resolution (4d1d4ba)
1 moveToNewGroup ignored and clobbered pending edits: an earlier pending move kept overriding the new group, and its refusal rolled back unconditionally must-fix It now begins a non-holding edit that supersedes earlier ones and takes part in latest-only rollback. Test aNewGroupSupersedesAnEarlierMove
2 Quick taps raced on the Mac: the Mac could apply expand before collapse and accept both must-fix Group requests go to each Mac one after another (groupRequestTail). Tests quickTapsReachTheMacInOrder (3 taps, the order checked) and quickTapsEndOnTheLastTap
3 A tap that stops a TUI fling also focused the terminal (keyboard up) should-fix The tap that stops a fling is ignored by the TUI tap (the controller is the tap's delegate)
4 An edit the Mac never answered never expired; edits survived sign-out should-fix Expires 30 s after it began; AppStore.resetGroupEdits() at sign-in. Test anUnansweredEditGivesWayToTheMac
5 Confirmation needs one more refresh nit Not changed: the 10 s window bounds it
6 The mouse position moved with every pan event, so any-motion apps got motion reports nit The wheel stays at the cell where the drag began
7 takesDrag used only velocity nit Uses the translation so far, falling back to velocity
8 zIndex wins only within the VStack nit Not changed
9 A live thought takes two taps to reach the full text nit Intended: the first tap closes
ACC T-SCROLL-12 claimed bytes "for each mode" doc Surface tests added for 1002 and 1003. The row now lists exactly what is covered
Tests The surface rig ignored grid padding nit Fixed

Also in this push, from the user: "sometimes the terminal doesn't have bottom content bounciness in its scroll."

  • Cause: a fling toward newer rows that reached the bottom called setContentOffset(contentOffset, animated: false) to stop the deceleration and switch to following. A fling into the end therefore stopped dead, while a slow drag past the end rubber-banded.
  • Fix: the fling now switches to following (TerminalScrollModel.flingReachedBottom()) and leaves UIKit's deceleration and bounce alone. applyScroll already leaves a decelerating page alone, and the end of the deceleration places the page at the newest end.
  • Tests: aFlingIntoTheBottomFollowsAndKeepsItsBounce, onlyAGestureReachesTheBottom. Whether the page bounces needs UIKit, so the live video check covers it.

Tests at 4d1d4ba on mini-cmux11s (iOS simulator) passed:

  • CmuxMobileTerminalTests
  • CmuxMobileModelTests
  • CmuxMobileLiveTests
  • CmuxMobileWorkspacesTests
  • CmuxMobileAgentChatTests/DisclosureChoiceTests

@azooz2003-bit azooz2003-bit changed the title cmux-next iOS: a drag in a TUI scrolls the app; workspace groups stay collapsed cmux-next iOS: a drag in a TUI scrolls the app; groups stay collapsed; bottom bounce after a fling Oct 22, 2026
- A new-group edit leaves the table once answered.
- Sign-in clears edits but keeps the generation counter.
- A queued edit's unanswered window starts when its request is sent.

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

cursor Bot commented Oct 22, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Re-review of 4d1d4ba by a fresh review subagent: SHIP at 4d1d4ba, with no must-fix findings. It confirmed:

  • the per-Mac request chain cannot deadlock;
  • the tap delegate works in either call order;
  • nothing moves the page while it is following during a deceleration, and the page lands on the newest end afterwards.

Nits fixed in 1078673:

  1. A new-group edit is dropped once answered (it never left the table).
  2. Sign-in clears edits but keeps the generation counter, so an old request can't match a new edit.
  3. A queued edit's 30 s unanswered window starts when its request is sent, not at the tap. Test aQueuedEditWaitsForItsTurn.

Not changed:

  • (4) a single jump to the newest end after a bounce over streaming output. This replaces the old dead stop and is checked in the live video.
  • (5) a rename or collapse of a group still waiting for its id from the Mac is refused. This existed before this PR.

CmuxMobileModelTests, CmuxMobileLiveTests and CmuxMobileWorkspacesTests pass on mini-cmux11s.

… swift test)

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

Copy link
Copy Markdown
Collaborator Author

CI at caf50f6:

  • CmuxMobileKit tests (macOS swift test and the iOS simulator) are green. The earlier red was this PR's own: DisclosureChoice was inside the iOS-only ToolViews.swift, so the macOS swift test could not see it. It now has its own file.
  • nightly-guards / workflow-guard-tests / release-notary is red with FAIL: the cmux NIGHTLY-identity jobs (sign, verify, publish) must contain: - name: Skip a build older than the old feed's. This is a base finding: the same check is red at the feat-cmux-next tip f76419f, and this PR touches no workflow. Guard status is its wrapper.

azooz2003-bit and others added 3 commits October 21, 2026 20:26
…ow the Mac's rows on the mirror

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ws on the mirror

less scrolls with a line feed on its last row and sets no margins. On the
phone's taller mirror the alternate screen's default margins were the
mirror's ~150 rows, so that line feed moved the cursor below the rows shown
instead of scrolling: the Mac's less moved on while the phone kept its
first page (drag or keys alike). Entering the alternate screen now sets the
margins to the Mac's rows (cursor kept), and a TUI's margin reset there
goes back to them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c's rows

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

Copy link
Copy Markdown
Collaborator Author

Live check finding: less (rig: a headless simulator plus the hidden mobilenaming Mac app on mini-cmux11s, over the real link, build 4d1d4ba).

  • What the run showed: every drag and swipe scrolled less on the Mac in the drag's direction (top line 1→26→51→27→98→175→99→74→1). The phone, however, kept showing less's first page. The j key showed the same split, so this was a mirror bug and not the scroll input.
  • Cause: less scrolls with a line feed on its last row and sets no margins. On the phone's taller mirror (about 150 rows), the alternate screen's default margins were the whole mirror, so that line feed moved the cursor below the Mac's rows instead of scrolling.
  • Fix (7d72cac, plus one more commit): TerminalRowShift sets the margins to the Mac's rows when the alternate screen is entered, keeping the cursor. On the alternate screen, every margin form a TUI sends stays within the Mac's rows: a reset, ;r, 0;0r, 1;0r, 5r, and a bottom past the screen. Test: TerminalRowShiftTests.theAlternateScreenScrollsAtTheMacsLastRow. It was red first (97d5ba6) and is green now.
  • Review: a fresh review subagent passed it (SHIP at 7d72cac). It checked Ghostty's per-screen saved cursor, the cursor homing that setting margins does, origin mode, mode lists, ESC c, and the replay after a Mac resize. Its nit 1, other reset forms, is fixed in f00e536. Its other nits are not changed: margins after a resize of the phone's mirror only, and the cursor and saved cursor on entering the alternate screen. They are follow-ups with no user-visible case known.

The less re-check on this build runs next.

azooz2003-bit and others added 3 commits October 21, 2026 20:56
… a full-screen app

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k (TUI capability F1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… full-screen apps; capability matrix

- A tap in an app that asked for the mouse (vim mouse=a, htop's header,
  mc, tmux panes) is a left click at that cell through Ghostty's encoder,
  then focuses the terminal as before.
- The composer's one-line draft is typed into a full-screen app (vim's
  normal mode took the bracketed paste of ':q' as text); several lines
  stay one paste.
- verify/tui-capabilities.md: the full TUI capability run and its resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t window; matrix re-verify

Review of e03153a: a one-line Send typed into an agent on the alternate
screen could hit Codex's paste-burst window (Codex QA F1) with Return 60 ms
later. Typed text now waits 250 ms before Return. The vim demo uses the
typed path. tui-capabilities.md records the live re-verify of e03153a.

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

Copy link
Copy Markdown
Collaborator Author

TUI capability run (the user's expanded scope) and its fixes

The full matrix is in cmux-next-ios/verify/tui-capabilities.md. It was run over the real link: a headless simulator, plus the hidden mobilenaming Mac app on mini-cmux11s, with real htop, less, man, vim, tmux, mc, Claude Code and cmatrix. Codex was not logged in on the mini, so it was not run.

Passed on the first run:

  • wheel scroll (htop, Claude Code while it streams, vim, tmux) and alternate-scroll arrows (less, man), with momentum and the right direction;
  • long press and Copy reaching the pasteboard;
  • every key-bar key and the sticky modifiers;
  • hardware Esc and F-keys;
  • bracketed paste into vim and into Claude Code;
  • OSC 52 reaching the pasteboard;
  • the shell's scrollback kept across the alternate screen;
  • the bottom row pinned to the composer when the key bar shows or hides;
  • 256-color, truecolor, CJK and emoji;
  • tmux prefix keys;
  • the screen and the wheel restored after a reconnect (background/foreground and cold relaunch);
  • cmatrix at about 43 fps.

Fixed in this PR, then re-verified live (all PASS on e03153a):

  • F1, taps never clicked. A tap in an app that asked for the mouse is now a left click at that cell through ghostty_surface_mouse_button, and it still focuses the terminal. Re-verify: in vim with mouse=a the ruler went 1,1 → 10,7 → 23,7 → 34,7; the htop CPU% header changed the sort; a tap on a tmux pane made it active. Tests: aTapIsAClickWhenTheAppAskedForTheMouse, aTapSendsNothingWithoutMouseReporting (committed red first, f156b28).
  • N1, the composer's :q was inserted into vim. A one-line draft is now typed into a full-screen app; several lines stay one paste. Re-verify: vim quit. Test: aFullScreenAppGetsAOneLineDraftTyped (red first, b9069f6).

Review of e03153a (fresh subagent): it confirmed the Ghostty mouse semantics:

  • mouse_captured is true when the app reports the mouse;
  • the release is not lost;
  • a click starts no selection and opens no link;
  • typed text goes out unbracketed, with UTF-8 intact.

Its findings and what changed in a6d6f76:

  • Must-fix: a one-line Send typed into an agent on the alternate screen could hit Codex's paste-burst window (Codex QA F1). Typed text now waits 250 ms before Return; a paste still waits 60 ms. A one-line Codex Send on the alternate screen stays unverified, because Codex is not logged in on the rig.
  • Fixed: the doc's missing re-verify section is now committed with its evidence.
  • Nits fixed:
    • the vim demo now uses the typed path;
    • the delaysEnter comment is corrected.
  • Nits not changed:
    • with 1003 (any-motion) on, a tap sends a motion report before the click, as a real mouse does;
    • click returns true when the Ghostty config turns mouse reporting off. Harmless, since the result is discarded.

Not fixed here, with reasons:

  • F2, hardware arrows, PgUp/PgDn, Home/End and forward Delete sent nothing under AXe-injected keys. They arrive through UIKeyCommands, and AXe may not reach those. Unverified until a real Bluetooth keyboard is used on a device; no code change without that evidence.
  • F3, OSC 8 links are not tappable. Opening a URL that a remote program printed is a trust decision. The recommendation is posted to Design and product calls (gallery: #15427), plus CI admin #13742 (a callout showing the host, with Open Link and Copy Link; http/https only).
  • Not offered: the key bar has no PgUp/PgDn, Home/End or F-keys (product scope).
  • Not run (simulator limits): drag-select inside a mouse-mode app (by design, a drag is the wheel; long press selects); rotation (no headless orientation command); bell haptics; the soft keyboard (the simulator has a hardware keyboard).

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Final review (fresh subagent): SHIP at a6d6f76. It found no must-fix and checked:

  • every caller of ComposerSubmit: the shell and main-screen path, attachments, and the demo;
  • that the docs match the code;
  • that CmuxTabsKit and CmuxConversation are untouched.

Nits not changed:

  • an unreachable ?? fallback;
  • the sendText doc comment;
  • a key-bar key tapped within 250 ms of a typed Send goes out before its Return (a pre-existing race, at 60 ms before).

CI at a6d6f76:

Merging (merge commit) per the user's direction for this work.

@azooz2003-bit
azooz2003-bit merged commit 70368f8 into feat-cmux-next Oct 22, 2026
@azooz2003-bit
azooz2003-bit deleted the feat-cmux-next-ios-tui-scroll branch October 22, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants