Skip to content

iOS: a phone fills its screen in a terminal shared with its Mac - #2

Open
thingnoy wants to merge 7 commits into
fix/ios-composer-send-waits-for-reconnectfrom
fix/ios-phone-fills-screen-when-shared
Open

thingnoy wants to merge 7 commits into
fix/ios-composer-send-waits-for-reconnectfrom
fix/ios-phone-fills-screen-when-shared

Conversation

@thingnoy

@thingnoy thingnoy commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

When a terminal is open on both the iPhone and the same person's Mac, the iPhone now fills its screen while it shows that terminal. Previously it showed a hatched empty band under the grid.

Cause. Each terminal's sizing policy defaults to Fit everyone (.smallest), so the grid is the minimum of every viewer. A full-screen TUI such as Claude Code mirrors that grid exactly, so a phone next to a shorter Mac pane gets 72x44 and a blank band below.

Fix. This is phone-only, so it works against an unchanged Mac (0.65.2). When a size state shows that this phone is letterboxed under the untouched default policy, the phone does two things, once per terminal:

  1. It sends a viewport report with counts_override: true.
  2. It sends mobile.terminal.size_policy.set with mode priority and its own priority key (user/iphone/device).

The phone then owns the grid while it counts. When it leaves the terminal, or goes to the background, the Mac takes the grid back through the priority fallback.

Why not Follow latest. The first version (build 7) used latest. Any Mac keystroke then resized the grid under the phone, including cmux send from agents. I reproduced it on a real iPhone through iPhone Mirroring:

  • The phone went blank for about 2 seconds during streaming output.
  • It then showed a hatched "97x29 · scaled" grid, with stale text in the top-left corner.
  • It stayed stuck at line 124 of 150 for more than 45 seconds.
  • One tap on the phone restored it.

With priority, Mac input does not change the owner.

Decision. TerminalSizingState.fillScreenClaim(for:) returns what to send (countsThisPhone, policy), or nil. Three cases qualify:

  • The default policy, with no counts override, letterboxing the phone: counts and policy.
  • Follow latest with this phone counted (build 7's claim), so existing terminals move over: policy.
  • This phone's own priority policy with no counts override (back from a hand-off): counts.

Every other policy or counts choice is left alone. The store marks the terminal claimed before sending, so repeated pushes don't resend, and unmarks it if a request fails.

Hand off. Hand Off Terminal Sizing When Inactive now defaults to on. Without it, a backgrounded phone would keep the Mac at the phone's size. A stored choice is kept.

Back from a hand-off. The Mac treats a returning phone as a fresh attach, without its counts override, so under the phone's own priority policy it stopped counting and the Mac kept the grid (seen on build 8). The decision now also covers this state: this phone's priority policy with no counts choice re-sends counts_override: true and leaves the policy alone. That repeat is guarded only while its request is in flight.

Settings. Settings › Fill Screen in Shared Terminals turns the claim off. It is on by default.

Upstream manaflow-ai#27195 ("iPhone First") does the same thing with a new handheld_first policy field. That field needs a Mac update. Drop this once a release ships manaflow-ai#27195.

Testing

Focused commands, with Xcode 26.3:

  • swift test --filter TerminalFillScreenClaimTests in Packages/iOS/CmuxMobileShell.
    • Red on 43b16fd (tests only): ✘ Test run with 6 tests in 1 suite failed ... with 6 issues. The claim sent latest, and build 7's follow-latest state was not moved.
    • Green on 5629e62: ✔ Test run with 6 tests in 1 suite passed.
  • swift test in Packages/iOS/CmuxMobileShellModel.
    • On 43b16fd it does not compile, because fillScreenPolicy(for:) does not exist yet.
    • On 5629e62, 444 tests pass.
  • verify-local.py --affected: 16/16.

Observed / Not run

  • Observed: the build 7 failure above, on cmux connect 1.0.6 (7) against cmux 0.65.2, through iPhone Mirroring. A control run with the same output triggered from the phone stayed smooth, under 1 second behind the Mac, with no resize.
  • Observed on device through iPhone Mirroring, reading the Mac's cmux surface size:
    • Build 8: streaming output from the Mac plus Mac input stayed full screen (150/150 lines, no "scaled" chip). Home returned the Mac to 97x29 (priority-fallback). Coming back left the phone at counts=false under the Mac's grid.
    • Build 9 (60024f0): viewing gave 72x59 owned by the phone, Home gave 97x29, and coming back gave 72x59 with the phone counted again.
  • Back-from-hand-off test: red on c358d12 (Expectation failed: recounted), green on 60024f0 (14 tests across the two focused suites; model 445).
  • Not run: MobileDisplaySettingsTests (CmuxMobileShellUI is iOS-only and needs a simulator run).
  • Not fixed: the phone's render still stalls if the grid resizes mid-stream. This change only stops Mac input from causing that resize while the phone shows the terminal.
  • Known gap: the claimed-terminal set is kept in memory only. See the // debt: comment.

Changelog

Added: On iPhone, a terminal shared with your Mac fills the screen while you view it, and the Mac gets its size back when you leave (Settings › Fill Screen in Shared Terminals).

Stacked on #1.

🤖 Generated with Claude Code

A phone sharing a terminal with the same user's Mac under the default
fit-everyone policy gets min(Mac, phone) and shows an empty band. These
cases expect the phone to switch that terminal to follow the latest
device and count itself, once, unless the user turned the preference off
or already chose a policy or counts override, or the grid already fills
the phone. Adds the preference key and the store's defaults seam the
tests inject.
Under the default fit-everyone policy a phone next to the same user's Mac
gets min(Mac, phone) and shows an empty band under a full-screen agent.
When a size state shows this phone letterboxed under the untouched
default, with no counts choice of its own, the phone counts itself and
switches the terminal to follow the latest device, once per terminal.
The grid then fills whichever device was used last, through RPCs the Mac
already accepts. TerminalSizingState.isFillScreenClaimable(by:) holds the
decision; the cmux.mobile.fillScreenWhenShared preference (default on)
turns it off.
Settings > Terminal gets "Fill Screen in Shared Terminals", on by
default. It writes cmux.mobile.fillScreenWhenShared, which the shell
reads before it claims a letterboxed terminal. Strings in all nine
locales.
Follow latest resizes the grid under the phone on any Mac keystroke, and the
phone's render stalls mid-stream on that resize. The claim should put this
phone first in a priority policy, move build 7's follow-latest claims over,
and hand the grid back while the phone is inactive by default.
… latest

Follow latest gave the grid back to the Mac on any Mac keystroke, including
cmux send. The resize landed mid-stream and the phone went blank, then showed
a stale 97x29 scaled grid until the next phone input.

The phone now counts itself and comes first in a priority policy, keyed to its
own device, so Mac input no longer resizes the grid while the phone shows the
terminal. Build 7's follow-latest claims (latest with the phone counted) move
over. Hand Off Terminal Sizing When Inactive now defaults to on, so the Mac
takes the grid back when the phone goes to the background.
Seen on device with build 8: after Home and back, the Mac listed the phone
with counts=false and kept its own 97x29 grid under the phone's priority
claim. The reattached phone had lost its counts override, and the claimed-set
guard stopped the phone from sending it again.
The claim decision now returns what to send. Under this phone's own priority
policy with no counts choice, which is how the Mac sees a phone reattach after
Hand Off, the phone re-sends counts_override true and leaves the policy alone.
That repeat is guarded only while its request is in flight; the policy claim
stays once per terminal.

This branch has not been deployed

No deployments
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.

1 participant