Skip to content

Fix iOS terminal picker menu flicker and blocked scrolling - #7959

Merged
azooz2003-bit merged 2 commits into
mainfrom
feat-ios-menu-flicker
Jul 13, 2026
Merged

azooz2003-bit merged 2 commits into
mainfrom
feat-ios-menu-flicker

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

The terminal picker toolbar menu flickered while open: every live preview update from the Mac (~2Hz title/viewportFit churn in MobileTerminalPreview) re-rendered WorkspaceDetailView, re-evaluated the Menu content closure, and made iOS rebuild the presented UIMenu. Each rebuild cross-faded the menu text (the visible opacity pulse, measured at 3.5 luminance peak-to-peak with a dominant ~2Hz component) and cancelled in-flight scroll gestures, so an overflowing menu could not be scrolled.

Fix: the picker now lives in TerminalPickerMenu, an Equatable child view applied with .equatable(). Its only inputs are TerminalPickerMenuValue (row snapshot, selection resolved from that same snapshot, capability/browser/chat flags) and a TerminalPickerMenuActions closure bundle excluded from equality. Preview/title/viewport churn produces an equal value, so SwiftUI skips the child body and the presented UIMenu is never rebuilt; membership changes, selection changes, browser/chat mode, and the New Workspace capability still change the value and update the open menu once. The existing syncTerminalPickerRows event paths (tap, onAppear, membership onChange, selection onChange) are unchanged. DEBUG-only Logger/os_signpost diagnostics count content-builder evaluations and snapshot writes.

Commits follow the red/green regression policy: the first commit adds TerminalPickerMenuValueTests against the not-yet-existing value seam (CI red), the second adds the fix (green). Tests cover title-only vs membership changes, selection resolution from snapshot rows, and the empty-snapshot first-open fallback.

Simulator evidence (before/after video with per-frame luminance analysis) is being captured and will be posted on this PR.


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


Note

Low Risk
Localized SwiftUI toolbar refactor with tests and DEBUG-only instrumentation; menu actions and sync behavior are preserved.

Overview
Fixes iOS terminal picker flicker and broken scrolling when live MobileTerminalPreview updates (~2Hz title/viewport churn) kept rebuilding the native UIMenu from WorkspaceDetailView.

The inline toolbar Menu is replaced by TerminalPickerMenu, an Equatable child with .equatable(). It only compares TerminalPickerMenuValue (snapshot rows, selection resolved from those rows, browser/chat/capability flags); TerminalPickerMenuActions closures are excluded from equality. Title/viewport-only live churn leaves the value unchanged so SwiftUI skips the child body and the open menu is not rebuilt; membership, selection, mode, and capability changes still update once. Existing syncTerminalPickerRows triggers (tap, appear, membership/selection) are unchanged.

DEBUG adds Logger/os_signpost for menu content evaluation and snapshot writes. TerminalPickerMenuValueTests lock snapshot-vs-live equality and selection resolution.

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


Summary by cubic

Fixes the iOS terminal picker flicker and cancelled scrolling by moving the menu into an Equatable child with a stable snapshot. The menu no longer rebuilds during live preview/title/viewport updates, so scrolling stays smooth.

  • Bug Fixes
    • Extracted picker into TerminalPickerMenu and applied .equatable() so equal TerminalPickerMenuValue skips recompute.
    • Added TerminalPickerMenuValue (snapshot-based rows; selection resolved from the snapshot) and TerminalPickerMenuActions (excluded from equality).
    • Menu now updates only on membership/selection/browser/chat/capability changes; ignores preview churn, removing the ~2Hz cross-fade and preserving in-flight scroll.
    • Kept existing sync event paths; added DEBUG diagnostics and TerminalPickerMenuValueTests to cover title-only vs membership changes, selection resolution, and empty-snapshot fallback.

Written for commit 74daa96. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a native terminal picker menu for switching terminals and workspaces.
    • Added actions to create workspaces and terminals, open a browser, view text, copy debug logs, and send feedback.
    • Added visual indicators for the selected terminal and active browser.
  • Bug Fixes

    • Improved selection handling when terminal lists change or contain outdated selections.
  • Tests

    • Added coverage for terminal list updates, selection resolution, and empty-state behavior.

@vercel

vercel Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 13, 2026 3:34am
cmux-staging Building Building Preview, Comment Jul 13, 2026 3:34am

@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b71f7b9b-9ef1-44dd-b230-49053e660d2e

📥 Commits

Reviewing files that changed from the base of the PR and between 74e166a and 74daa96.

📒 Files selected for processing (7)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenu.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuActions.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuDiagnostics.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuValue.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+MenuState.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/TerminalPickerMenuValueTests.swift

📝 Walkthrough

Walkthrough

Adds a reusable SwiftUI terminal picker menu with derived state and action closures, integrates it into WorkspaceDetailView, adds DEBUG diagnostics, and tests selection, fallback, and equality behavior.

Changes

Terminal picker menu

Layer / File(s) Summary
Menu state and action contracts
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuValue.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuActions.swift
Defines immutable picker state, selection resolution, live-terminal fallback, feature flags, and closure-based menu actions.
Native menu rendering
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenu.swift
Renders terminal selection, workspace and terminal creation, browser, text, debug-log, and feedback actions with conditional states and accessibility metadata.
Workspace wiring and validation
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+MenuState.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuDiagnostics.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/TerminalPickerMenuValueTests.swift
Connects the extracted menu to the toolbar, records DEBUG row-write diagnostics, and tests value equality, selection resolution, and empty-state fallbacks.

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

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceDetailView
  participant TerminalPickerMenu
  participant MenuActionHandler
  WorkspaceDetailView->>TerminalPickerMenu: provide picker value and action closures
  TerminalPickerMenu->>MenuActionHandler: invoke selected terminal or menu action
  MenuActionHandler->>WorkspaceDetailView: execute configured operation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: iOS terminal picker flicker and blocked scrolling.
Description check ✅ Passed The description is mostly complete, with summary and testing covered; the demo video and checklist sections are still incomplete.
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 The new menu/value/helper types are top-level value types or SwiftUI views, and the diff adds no new MainActor-bound store access or shared mutable Sendable references.
Cmux Swift Blocking Runtime ✅ Passed Changed Swift files add no blocking waits, sync dispatch, locks, sleeps, or polling; the new menu uses pure state/value isolation and DEBUG-only logging.
Cmux Browser Automation Off-Main ✅ Passed PR only adds iOS SwiftUI menu/value/diagnostics files; it չի touches TerminalController or CmuxControlSocket browser-automation routing covered by the rule.
Cmux Expensive Synchronous Load ✅ Passed PASS: The picker refactor only snapshots rows/value and wires existing actions; no added RestorableAgentSessionIndex/load, JSON parsing, or other sync history loads appear in the menu path.
Cmux Cache Substitution Correctness ✅ Passed UI-only menu snapshot has a live-terminals fallback on cold start, and membership/selection updates are event-driven; the call site documents title-churn staleness as intentional.
Cmux No Hacky Sleeps ✅ Passed Rule is scoped to non-Swift runtime scripts; the PR changes only Swift menu/state files and I found no sleeps, timers, or polling.
Cmux Algorithmic Complexity ✅ Passed The patch uses linear snapshot mapping/equality only; it adds no nested scans, per-target rescans, or hot-path sorting/filtering, and the test file is exempt.
Cmux Swift Concurrency ✅ Passed New files are synchronous SwiftUI/state helpers only; no new DispatchQueue, Combine, completion APIs, or fire-and-forget Tasks were added, and existing Task helpers predate the PR.
Cmux Swift @Concurrent ✅ Passed New picker types are synchronous; the only async calls are existing UI-bound tasks, and helper reads already hop via @MainActor/actor sinks.
Cmux Swift File And Package Boundaries ✅ Passed The PR extracts the picker into small focused SwiftPM package files; no oversized/mixed-responsibility boundary violation is present.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift, Package.resolved, Xcode project, or workflow files changed; the touched CmuxMobileShellUI .gitignore only ignores .build/, not Package.resolved.
Cmux Swift Logging ✅ Passed Only DEBUG-gated Logger/signpost diagnostics and a DEBUG-only NSLog are added; no forbidden production print/debugPrint/dump logging appears.
Cmux User-Facing Error Privacy ✅ Passed Diff adds menu labels and DEBUG diagnostics only; no user-facing error, alert, or recovery copy exposes vendor, snapshot, or secret details.
Cmux Full Internationalization ✅ Passed All new user-facing menu strings use L10n.string/String(localized:), and ios/cmux/Resources/Localizable.xcstrings has en+ja entries for each touched key.
Cmux Swiftui State Layout ✅ Passed The PR uses value snapshots plus closures for the menu, with no new ObservableObject/@published, GeometryReader, store-bearing lazy rows, or render-time state writes.
Cmux Architecture Rethink ✅ Passed The menu is isolated behind one snapshot value and one action bundle; no sleeps, locks, polling, or duplicate lifecycle ownership were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only adds a SwiftUI Menu and related state/helpers; no NSWindow/NSPanel/WindowGroup code or cmux.* auxiliary-window identifiers were introduced or changed.
Cmux Source Artifacts ✅ Passed All changed paths are hand-written Swift source/tests; no logs, caches, screenshots, build output, or scratch artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR adds only real menu behavior plus isolated DEBUG diagnostics; it does not add a test-only or debugger-only production seam.
Cmux No Ambient Global State ✅ Passed No new ambient global state: the added APIs are instance-based structs/views, the helper is an extension method, and no new singleton or global mutable surface was introduced.
✨ 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-menu-flicker

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.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR isolates the iOS terminal picker from frequent preview updates. The main changes are:

  • Moves the native menu into an equatable child view.
  • Adds immutable menu state and a closure-based action bundle.
  • Preserves snapshot synchronization for taps, selection, and membership changes.
  • Adds focused value tests and DEBUG signpost diagnostics.

Confidence Score: 4/5

The equatable menu can retain actions from an old workspace when the compared snapshot stays equal.

  • The snapshot boundary addresses the reported menu rebuilds.
  • Selection and membership changes participate in the menu value.
  • Action ownership does not participate in equality, so workspace transitions can preserve stale closures.

TerminalPickerMenu.swift and WorkspaceDetailView.swift

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenu.swift Adds the equatable menu boundary, but its equality omits the action owner.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuActions.swift Adds a focused closure bundle that keeps mutable stores out of the menu subtree.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuValue.swift Adds an immutable snapshot with selection resolved from the displayed rows.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Replaces inline menu construction with the snapshot-driven child and existing synchronization events.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+MenuState.swift Keeps full snapshot writes for opening and selection while filtering title-only live churn.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenuDiagnostics.swift Adds DEBUG-only unified logging and signposts for menu evaluations and snapshot writes.
Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/TerminalPickerMenuValueTests.swift Tests preview-churn equality, membership changes, selection resolution, and empty snapshots.

Reviews (1): Last reviewed commit: "fix(ios): isolate terminal picker from p..." | Re-trigger Greptile

Comment on lines +12 to +14
nonisolated static func == (lhs: Self, rhs: Self) -> Bool {
lhs.value == rhs.value
}

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.

P1 Equal Values Retain Old Actions

The equality check ignores actions, although the rendered menu invokes those closures. If SwiftUI preserves this view identity while the workspace changes to one with an equal menu snapshot, .equatable() can retain closures from the previous WorkspaceDetailView; selecting a terminal can then close the browser or perform another action for the previous workspace. Include the action owner, such as the workspace ID, in the compared value.

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not reachable: terminal ids are globally unique surface ids, so a workspace change always changes the row ids and therefore the menu value; equal snapshots across workspaces cannot occur. The existing .onChange(of: terminalPickerLiveMembership) also resyncs the snapshot on any membership change, installing fresh closures. Within the single-render window the closures act via the live store reference keyed by unique terminal id, so they cannot target the wrong workspace.

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.

That's a sound argument. Globally unique terminal IDs ensure workspace switches always produce unequal TerminalPickerMenuValues, .equatable() never suppresses those updates, and the .onChange resync installs fresh closures before any interaction is possible. The stale-closure path I described isn't reachable given those invariants.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Re Greptile's stale-closure concern (actions excluded from Equatable): a workspace transition cannot keep the old closures live. Terminal ids are globally unique surface ids, so any workspace swap changes terminalPickerLiveMembership; the existing .onChange(of: terminalPickerLiveMembership) resyncs terminalPickerRows, which changes TerminalPickerMenuValue, re-evaluates the child body, and installs fresh closures. Inside the one-render window before that fires, the closures still act through the live store reference and select by globally-unique terminal id, so a tap either performs the correct selection or no-ops. No code change needed.

This branch was successfully deployed

1 active deployment
Preview – cmux — 74daa96d Deployed Jul 13, 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