Skip to content

iOS: dismissable alt-screen sizing notice in terminal toolbar - #7669

Merged
azooz2003-bit merged 7 commits into
mainfrom
feat-ios-altscreen-notice
Jul 9, 2026
Merged

azooz2003-bit merged 7 commits into
mainfrom
feat-ios-altscreen-notice

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 8, 2026 •

Copy link
Copy Markdown
Collaborator

When the selected terminal session is in alt-screen mode (full-screen TUIs like Codex, or Claude Code with alt-screen enabled), the phone mirrors the Mac grid and letterboxes instead of filling the frame. Users read this as a rendering bug. This adds an orange (!) button to the workspace terminal toolbar whenever the active surface is on the alternate screen; tapping it shows a popover explaining why the terminal may not fill the screen, with a Don't Show Again action persisted across launches.

The store already tracked per-surface active screen (terminalActiveScreenBySurfaceID, fed by every render-grid frame). This PR adds a public isAlternateScreen(surfaceID:) accessor in an extension (MobileShellComposite+AltScreenNotice.swift) and a standalone @MainActor @Observable AltScreenNoticeState with an injected UserDefaults for the dismissal flag, so MobileShellComposite.swift itself has zero line growth. WorkspaceDetailView conditionally emits a ToolbarItem (workspace-altscreen-notice) before the trailing cluster; the button and popover live in AltScreenNoticeButton.swift (value inputs + action closure only, no store reference).

Strings are mobile.altScreenNotice.* in ios/cmux/Resources/Localizable.xcstrings with English and Japanese entries, read via L10n.string.

Tests (MobileShellAltScreenNoticeTests): render-grid frames drive isAlternateScreen through the real delivery seam (.alternate true, .primary false, unknown surface false), and dismissal persists across AltScreenNoticeState instances on a scoped UserDefaults suite.

Verified on tag altwrn: simulator paired to the tagged Mac app, vim in the mirrored surface -> (!) appears in the toolbar; quitting alt-screen hides it. Package tests pass on host.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
UI and display preferences only; terminal delivery change is a no-op when screen mode is unchanged, with regression tests for observation.

Overview
Adds an orange toolbar warning when the selected terminal is in alternate (full-screen TUI) mode, so users understand letterboxing instead of treating it as a rendering bug.

MobileShellComposite exposes isAlternateScreen(surfaceID:) for the UI. Render-grid delivery now skips same-value writes to terminalActiveScreenBySurfaceID so SwiftUI observation does not re-fire on every frame (toolbar flicker fix).

AltScreenNoticeButton shows a popover explaining Mac-sized mirroring, with Don't Show Again wired to MobileDisplaySettings.showAltScreenNotice (UserDefaults, default on). WorkspaceDetailView inserts the toolbar item when alt-screen is active and the preference is on; Settings > Terminal adds a toggle to re-enable the notice. English and Japanese strings added.

Tests cover alt-screen tracking, observation behavior, and settings persistence.

Reviewed by Cursor Bugbot for commit 58e7e4b. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds a dismissable orange (!) notice in the iOS terminal toolbar when the selected session is on the alternate screen, explaining why full‑screen TUIs may not fill the phone. You can hide it with “Don’t Show Again” or re‑enable it in Settings; the popover now fits long text and the toolbar no longer flickers on unchanged frames.

  • New Features

    • Show a toolbar warning when isAlternateScreen(surfaceID:) is true and MobileDisplaySettings.showAltScreenNotice is on; the popover explains mirroring and offers “Don’t Show Again” persisted in UserDefaults.
    • Add a “Full‑Screen Sizing Notice” toggle under Settings → Terminal to turn the notice back on; WorkspaceDetailView reads MobileDisplaySettings from the environment; localized EN/JA strings and tests cover detection and preference persistence.
  • Bug Fixes

    • Skip same‑value writes to terminalActiveScreenBySurfaceID to prevent toolbar flicker; regression test confirms unchanged frames don’t notify observers.
    • Make the popover wrap and size to fit long text in compact width.

Written for commit 58e7e4b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an alternate-screen notice button to the iOS terminal toolbar, with a popover explanation and “Don’t Show Again” dismissal.
    • Added a Terminal settings toggle to show/hide the full-screen sizing notice.
    • Added localized notice text in English and Japanese.
  • Bug Fixes
    • The notice only appears when the selected terminal is in alternate-screen mode and honors the saved display preference.
    • Improved handling to avoid unnecessary notice state updates when alternate-screen status hasn’t changed.
  • Tests
    • Added coverage for alternate-screen behavior and persisted settings.

When the selected terminal session is in alt-screen mode (full-screen TUIs
like Codex), the mirrored terminal renders at the Mac grid size and may not
fill the phone frame. Show an orange (!) toolbar button whose popover
explains this, with a persisted Don't Show Again action.

The store already tracked per-surface active screen; this adds a public
isAlternateScreen(surfaceID:) accessor in an extension and a standalone
@observable AltScreenNoticeState (injected UserDefaults) so
MobileShellComposite.swift itself has zero growth.

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

vercel Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 9, 2026 8:12am
cmux-staging Building Building Preview, Comment Jul 9, 2026 8:12am

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds alternate-screen notice support to the iOS mobile shell. It exposes alternate-screen state from terminal render-grid tracking, persists a user-controlled notice preference, adds a dismissible notice button, wires it into workspace and settings UI, and adds tests and localized strings.

Changes

Alternate-screen notice feature

Layer / File(s) Summary
Alternate-screen detection and delivery tracking
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AltScreenNotice.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellAltScreenNoticeTests.swift
Adds isAlternateScreen(surfaceID:), updates tracked screen state only on value changes, and tests render-grid-driven state transitions and observer notifications.
Persisted notice preference
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDisplaySettings.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDisplaySettingsTests.swift
Adds the showAltScreenNotice setting, persists it in UserDefaults, and tests its default and round-trip behavior.
Notice button and workspace wiring
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AltScreenNoticeButton.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift, ios/cmux/Resources/Localizable.xcstrings
Adds the notice popover button, shows it in the workspace toolbar when the terminal is in alternate-screen mode, adds a settings toggle, and adds the localized strings used by both UI surfaces.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#7098: Both PRs modify MobileShellComposite+TerminalOutputDelivery’s render-grid delivery handling and tracked screen-state updates.
  • manaflow-ai/cmux#7172: Both PRs modify authoritative render-grid handling that updates the active/alternate screen tracking used by the notice state.

Suggested reviewers: lawrencecchen

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: the new accessor and settings model stay on @MainActor, UI changes are SwiftUI views, and no new background access or unisolated shared mutable Sendable types were introduced.
Cmux Swift Blocking Runtime ✅ Passed Full PR diff adds UI/state and conditional writes only; no semaphores, sleeps, main-syncs, polling, or locks were introduced in production Swift.
Cmux Browser Automation Off-Main ✅ Passed No browser-automation routing changed: only terminal alt-screen UI/state files were touched, and the rule’s target files (TerminalController, ControlCommandExecutionPolicy) are untouched.
Cmux Expensive Synchronous Load ✅ Passed PR only adds alt-screen UI/state and a small observer fix; no RestorableAgentSessionIndex.load, agent-history parsing, or other heavy sync loads appear in the diff.
Cmux Cache Substitution Correctness ✅ Passed PASS: the alt-screen read only drives a transient toolbar notice; terminalActiveScreenBySurfaceID is event-driven render-grid state, and the rule allows UI hints.
Cmux No Hacky Sleeps ✅ Passed PASS: The PR only touches Swift UI/store code and tests; no added sleeps/timers/polling or delayed dispatch appear in the changed files, and the runtime-sleeps rule is out of scope.
Cmux Algorithmic Complexity ✅ Passed New paths use O(1) dictionary/set lookups; the diff removes same-value writes and adds no scans, sorts, or per-target rescans.
Cmux Swift Concurrency ✅ Passed No new legacy async patterns were introduced; the patch is synchronous SwiftUI/Observation, and existing Task uses are preexisting UI-bound callbacks.
Cmux Swift @Concurrent ✅ Passed PASS: The PR adds only synchronous main-actor/UI state plumbing; no new @concurrent, nonisolated async, or heavy async UI call sites were introduced.
Cmux Swift File And Package Boundaries ✅ Passed New Swift files are small, single-purpose package/UI glue; touched large views only gained minor toolbar/toggle wiring, and the 451-line delivery file got a tiny same-value-write bug fix.
Cmux Swiftpm Lockfiles ✅ Passed HEAD only changes source/test files; no Package.swift, Package.resolved, .gitignore, or xcodeproj dependency edits are present.
Cmux Swift Logging ✅ Passed The PR’s commit diff adds no print/debugPrint/dump/NSLog or new Logger declarations; the only NSLog found is existing debug-only code unchanged by the patch.
Cmux User-Facing Error Privacy ✅ Passed The new alt-screen notice copy is generic and exposes no vendor/internal/provider details; other diff hunks only change state tracking and tests.
Cmux Full Internationalization ✅ Passed All new user-facing Swift text is localized via L10n/String(localized:), and the new catalog keys have en/ja translations matching the catalog's only supported locales.
Cmux Swiftui State Layout ✅ Passed Uses @Observable/@State, not ObservableObject/@published; no GeometryReader or lazy-list row store refs in changed views, and state writes are in actions/callbacks.
Cmux Architecture Rethink ✅ Passed The PR only exposes existing terminalActiveScreenBySurfaceID via a read accessor and adds a persisted UI preference; no new timing, polling, locks, or competing owner.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only adds a toolbar popover and settings/toggle UI; no NSWindow/WindowGroup/NSPanel/NSWindowController changes or cmuxAuxiliaryWindowIdentifiers edits.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source, tests, or localization catalogs; no logs, caches, build output, or scratch artifacts are present.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Production diff only makes same-value screen writes conditional to cut observer churn; no #if DEBUG, test-only naming, or widened test seam in Sources.
Cmux No Ambient Global State ✅ Passed No ambient-global-state violation: the new state lives in injectable instance-owned types, and the only new API is an extension method on MobileShellComposite.
Title check ✅ Passed The title is concise and accurately summarizes the main change: a dismissable alt-screen notice in the iOS terminal toolbar.
Description check ✅ Passed The description covers the summary and testing well, with minor template omissions like the demo video and checklist sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-altscreen-notice

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9bc8102. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a dismissable orange (!) toolbar notice to the iOS workspace detail view when the selected terminal session is on the alternate screen (e.g. vim, Codex), explaining to users why the terminal may not fill the screen. The dismissal preference is persisted via MobileDisplaySettings.showAltScreenNotice and can also be toggled back on in Settings → Terminal.

  • Adds isAlternateScreen(surfaceID:) on MobileShellComposite and guards same-value writes to terminalActiveScreenBySurfaceID at three call sites so @Observable re-renders are not fired on every delivered render-grid frame.
  • AltScreenNoticeButton is a value-type SwiftUI view with a closure seam for the dismiss action; WorkspaceDetailView conditionally emits the ToolbarItem based on the alt-screen state and user preference; EN/JA localization is complete for all four new string keys.

Confidence Score: 5/5

This PR is safe to merge — it touches only UI state, toolbar layout, and a UserDefaults preference; no auth, sync, or terminal rendering paths are changed in a breaking way.

The change is narrowly scoped: the same-value guard on terminalActiveScreenBySurfaceID prevents over-notification without altering delivery semantics, isAlternateScreen is a pure dictionary read, AltScreenNoticeButton holds no store reference, and showAltScreenNotice follows the established MobileDisplaySettings pattern with correct tri-state default handling. Tests cover alt-screen tracking, observation coalescing, and settings persistence. No blocking issues found.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift Adds same-value guards before writing to terminalActiveScreenBySurfaceID at three write sites, preventing spurious @observable change notifications on every render-grid frame.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AltScreenNoticeButton.swift New SwiftUI view with @State popover, closure-injected dismiss action, and L10n.string for all four user-facing strings; no store reference.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDisplaySettings.swift Adds showAltScreenNotice: Bool (defaulting to true via object-cast pattern) following the same @observable + UserDefaults write-through pattern as existing settings properties.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Adds @Environment(MobileDisplaySettings.self) and a conditional ToolbarItem that appears when alt-screen is active and showAltScreenNotice is true; closure passed to button correctly captures the environment object.
ios/cmux/Resources/Localizable.xcstrings Adds EN and JA translations for all four new string keys (mobile.altScreenNotice.* and mobile.settings.altScreenNotice), fully covering the two supported locales.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[render-grid frame arrives] --> B{same activeScreen as tracked?}
    B -- yes --> C[skip write - no Observable notification]
    B -- no --> D[write terminalActiveScreenBySurfaceID]
    D --> E[Observable notifies WorkspaceDetailView]
    E --> F{selectedTerminalID + isAlternateScreen + showAltScreenNotice?}
    F -- all true --> G[emit workspace-altscreen-notice ToolbarItem]
    G --> H[AltScreenNoticeButton shown]
    H --> I{user taps button}
    I --> J[popover appears]
    J --> K{user taps Don't Show Again}
    K --> L[showAltScreenNotice = false persisted to UserDefaults]
    L --> M[SwiftUI re-render: popover dismissed, ToolbarItem removed]
    F -- any false --> N[no ToolbarItem]
    Q[Settings Terminal toggle] -.-> L
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[render-grid frame arrives] --> B{same activeScreen as tracked?}
    B -- yes --> C[skip write - no Observable notification]
    B -- no --> D[write terminalActiveScreenBySurfaceID]
    D --> E[Observable notifies WorkspaceDetailView]
    E --> F{selectedTerminalID + isAlternateScreen + showAltScreenNotice?}
    F -- all true --> G[emit workspace-altscreen-notice ToolbarItem]
    G --> H[AltScreenNoticeButton shown]
    H --> I{user taps button}
    I --> J[popover appears]
    J --> K{user taps Don't Show Again}
    K --> L[showAltScreenNotice = false persisted to UserDefaults]
    L --> M[SwiftUI re-render: popover dismissed, ToolbarItem removed]
    F -- any false --> N[no ToolbarItem]
    Q[Settings Terminal toggle] -.-> L
Loading

Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@MainActor
@Observable
public final class AltScreenNoticeState {
static let dismissedDefaultsKey = "mobile.altScreenNotice.dismissed"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The constant is implicitly internal on this public class, exposing the UserDefaults key string to the rest of the module without a clear reason. Marking it private closes that surface.

Suggested change
static let dismissedDefaultsKey = "mobile.altScreenNotice.dismissed"
private static let dismissedDefaultsKey = "mobile.altScreenNotice.dismissed"

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Cursor review: per-view @State instances meant a detail view opened before
Don't Show Again could still show the notice. All views now read the same
shared instance, so dismissal hides the button everywhere immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AltScreenNoticeState.swift`:
- Around line 8-9: Remove the new singleton from AltScreenNoticeState and rely
on scoped injection instead. AltScreenNoticeState already has init(defaults:)
for dependency injection, so delete the static shared instance and pass an
AltScreenNoticeState from the parent coordinator or environment into
WorkspaceDetailView. Update any call sites that currently reference
AltScreenNoticeState.shared to use the injected instance, keeping the dismissal
state owned by the scoped type rather than global app state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a8d69ac8-4733-4aa8-838b-4c85ff12076b

📥 Commits

Reviewing files that changed from the base of the PR and between 9bc8102 and 615a92d.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AltScreenNoticeState.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift

Comment thread Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AltScreenNoticeState.swift Outdated
azooz2003-bit and others added 5 commits July 8, 2026 17:37
package-conventions-lint bans singletons; the shared instance now lives as
@State on WorkspaceShellView and flows through WorkspaceDetailContainer to
each WorkspaceDetailView, keeping one dismissal source of truth per
navigation tree without a static.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Don't Show Again had no way back. The persisted flag moves into
MobileDisplaySettings as showAltScreenNotice (default on, didSet
write-through like the other preferences), read via the environment;
Settings > Terminal gains a localized toggle bound to it. Deletes the
standalone AltScreenNoticeState and its root-to-detail plumbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recordTerminalRenderGridDelivery wrote the observation-tracked
terminalActiveScreenBySurfaceID dict on every render-grid frame; @observable
fires on same-value assignments, so the workspace toolbar (which reads
isAlternateScreen) re-evaluated at terminal frame rate and rebuilds racing
the popover-dismiss animation flickered the (!) item. All tracked-screen
writes are now change-conditional, with an observation-tracking regression
test proving same-value frames no longer notify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-line Texts in a popover report a single-line ideal height, so the
compact-width popover truncated the warning. fixedSize(horizontal: false,
vertical: true) on the title and explanation makes them report their full
wrapped height, and the popover sizes to fit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit merged commit 845395d into main Jul 9, 2026
34 of 36 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-ios-altscreen-notice branch July 9, 2026 04:38

This branch was successfully deployed

1 active deployment
Preview – cmux — 58e7e4b7 Deployed Jul 9, 2026 by vercel[bot]
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