Skip to content

Keep iOS Mac avatar color slots stable during Mac switches - #7858

Merged
azooz2003-bit merged 9 commits into
mainfrom
issue-ios-mac-switch-color-flash
Jul 13, 2026
Merged

azooz2003-bit merged 9 commits into
mainfrom
issue-ios-mac-switch-color-flash

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes a one-frame color flash on iOS: switching the active Mac briefly shows every workspace/computer row in the wrong avatar color before snapping to the correct one.
  • Root cause: MobileWorkspaceAggregation.machineColorIndex ranked each Mac's color slot by alphabetical position among whatever keys currently existed in the live workspacesByMac dictionary. During a switch, dropStalePreviousForeground synchronously drops the old foreground Mac's key before the new Mac's siblings are re-added asynchronously, so the dictionary briefly holds exactly one key — collapsing that Mac's rank to slot 0 ([Color.blue, Color.cyan]), regardless of its real assigned color.
  • Fix: color slots are now assigned once per Mac, additively, and stick for the lifetime of the shell instance (MobileShellComposite.stableMacColorSlots), instead of being recomputed from the live key set on every recompute. MobileWorkspaceAggregation.machineColorIndex is now a pure (existingAssignments, adding) -> updatedAssignments function immune to live key-set churn.

Test plan

  • swift test in Packages/iOS/CmuxMobileShellModel — 95/95 passing, including a new regression test that reproduces the switch-churn pattern (mutating the live key set while asserting existing Macs' indices never change).
  • Two-commit red/green: d924896 adds the failing regression test only, c35d9dfe adds the fix.
  • Dogfood on a real two-Mac + iPhone setup (the actual repro requires two genuinely distinct paired Macs, which isn't reproducible in this environment — see PR discussion).

🤖 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
In-memory UI color mapping only; bounded resets on sign-out/team switch with dedicated tests. No auth, networking, or data persistence changes.

Overview
Fixes a one-frame avatar color flash when switching the active Mac on iOS. Per-Mac color slots are no longer derived from alphabetical rank among whatever keys are currently in workspacesByMac on each recompute (which briefly collapses to a single key mid-switch).

MobileShellComposite now keeps stableMacColorSlots for the shell lifetime: updateStableMacColorSlots() runs before workspace derivation, machineColorIndex exposes that map, and derivedWorkspaces takes the precomputed index instead of recomputing from the live key set. Sign-out clears all slots; team switch prunes to the foreground Mac only so old-team Macs do not leak into numbering while the foreground color stays stable.

MobileWorkspaceAggregation.machineColorIndex is now additive—(existingAssignments, adding) preserves prior slots and assigns new Macs deterministically. Regression tests cover switch churn, sign-out, and team-switch boundaries.

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


Summary by cubic

Fixes the one-frame avatar color flash on iOS when switching Macs by assigning stable per-Mac color slots for the shell lifetime, and resets those slots on sign-out/team switches to prevent color leakage.

  • Bug Fixes
    • Added stableMacColorSlots in MobileShellComposite and update it before deriving UI state.
    • Replaced alphabetical indexing with MobileWorkspaceAggregation.machineColorIndex(existingAssignments:adding:); skips empty/anonymous IDs.
    • Passed the precomputed machineColorIndex into derivedWorkspaces to keep previews consistent during switch churn.
    • Reset stableMacColorSlots on sign-out and on team switch; keep only the foreground Mac’s slot across a team switch to avoid a color flash.
    • Moved slot update/reset helpers to MobileShellComposite+MacSwitchState.swift and split color-slot reset tests into MobileShellCompositeColorSlotResetTests.swift to stay within CI’s file-length budget; no behavior change.

Written for commit 5a49608. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Mac color assignments now remain consistent as workspaces update, preventing colors from shifting unexpectedly.
    • Color assignments are correctly reset when signing out and appropriately retained or removed when switching teams.
  • Tests

    • Added coverage for color stability across workspace updates, account changes, and team switches.

@vercel

vercel Bot commented Jul 10, 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:52am
cmux-staging Building Building Preview, Comment Jul 13, 2026 3:52am

@greptile-apps

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR keeps iOS Mac avatar color slots stable while switching active Macs. The main changes are:

  • Adds stable in-memory Mac color slots on MobileShellComposite.
  • Updates workspace aggregation to preserve existing Mac-to-slot assignments.
  • Passes the stable slot map into derived workspace preview generation.
  • Clears or prunes color slots on sign-out and team switch.
  • Adds tests for switch churn, sign-out cleanup, and team-switch cleanup.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacSwitchState.swift Adds helper methods for updating, clearing, and pruning stable Mac color slots.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Stores stable Mac color slots and uses them when recomputing derived workspace state.
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceAggregation.swift Changes color-slot assignment to preserve existing mappings and accept a precomputed slot map.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeColorSlotResetTests.swift Adds tests for clearing slots on sign-out and pruning slots on team switch.
Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceAggregationTests.swift Updates aggregation tests for stable color slots and transient Mac-set changes.

Reviews (4): Last reviewed commit: "Drop stale budget entry for MobileShellC..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MobileWorkspaceAggregation now incrementally assigns Mac color slots, while MobileShellComposite caches and manages those assignments across workspace recomputation, sign-out, and team changes. Tests update derivation setup and cover lifecycle reset behavior.

Changes

Stable Mac color assignments

Layer / File(s) Summary
Incremental color index aggregation
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceAggregation.swift
Color slots are preserved from existing assignments, assigned to new non-empty Mac IDs, and passed explicitly when stamping derived workspaces.
Shell color cache integration
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacSwitchState.swift
The shell stores stable slots, refreshes them during derivation, prunes them on team changes, clears them on sign-out, and supplies them to workspace derivation.
Aggregation test migration
Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceAggregationTests.swift
Tests use shared helpers and explicitly provide color-index mappings to derived-workspace calculations.
Shell color lifecycle validation
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeColorSlotResetTests.swift
Tests verify slot clearing on sign-out and foreground-slot preservation during team changes.

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

Sequence Diagram(s)

sequenceDiagram
  participant MobileShellComposite
  participant MobileWorkspaceAggregation
  participant WorkspacePreviews
  MobileShellComposite->>MobileWorkspaceAggregation: update stable slots from existing assignments and Mac IDs
  MobileWorkspaceAggregation-->>MobileShellComposite: return updated slot map
  MobileShellComposite->>MobileWorkspaceAggregation: derive workspaces with stable machineColorIndex
  MobileWorkspaceAggregation->>WorkspacePreviews: stamp previews with owner Mac slots
  WorkspacePreviews-->>MobileShellComposite: return derived workspace previews
Loading

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 mutable color-slot cache lives inside @MainActor MobileShellComposite, and MobileWorkspaceAggregation stays a pure Sendable value helper; no new background-access debt.
Cmux Swift Blocking Runtime ✅ Passed PR adds only pure color-slot bookkeeping and tests; no new semaphores, waits, sleeps, polls, sync dispatches, or locks appear in the diff.
Cmux Browser Automation Off-Main ✅ Passed PASS: merge-base diff only changes iOS shell/model color-slot files; no edits to TerminalController.swift or ControlCommandExecutionPolicy.swift, and no browser.* routing code was touched.
Cmux Expensive Synchronous Load ✅ Passed Diff only adds in-memory color-slot caching/pruning; no RestorableAgentSessionIndex, transcript/JSONL loads, or filesystem scans were added to main-actor paths.
Cmux Cache Substitution Correctness ✅ Passed PASS: the cache is an in-memory UI color hint, not persistence/snapshot data; it’s event-updated, reset on sign-out/team switch, and tests cover churn.
Cmux No Hacky Sleeps ✅ Passed The PR diff is Swift-only and adds no fixed sleeps, timers, polling, or asyncAfter-based coordination; the touched hunks only change stable color-slot bookkeeping.
Cmux Algorithmic Complexity ✅ Passed No new asymptotic regression: color slots are cached incrementally, and the remaining scans are linear/sorted over the small Mac key set, matching prior complexity.
Cmux Swift Concurrency ✅ Passed Diff only adds synchronous MainActor state helpers and pure aggregation APIs; no new DispatchQueue, Combine, completion-handler, or unmanaged Task patterns appear.
Cmux Swift @Concurrent ✅ Passed Touched Swift changes are synchronous; no new nonisolated async/@Concurrent misuse, and color-slot recompute stays on @MainActor.
Cmux Swift File And Package Boundaries ✅ Passed Core color-slot logic is in the CmuxMobileShellModel package; the app target only adds small glue/cache resets, and the 7.5k-line shell file gets a tiny focused edit.
Cmux Swiftpm Lockfiles ✅ Passed PR changes only Swift sources/tests; no Package.swift, Package.resolved, .gitignore, xcodeproj, or workflow dependency files were modified.
Cmux Swift Logging ✅ Passed No added runtime logging found in the changed Swift files; the existing mobileShellLog logger predates this diff, and no print/debugPrint/dump/NSLog/ad hoc logs were introduced.
Cmux User-Facing Error Privacy ✅ Passed Changed production code only adjusts stable color-slot derivation/reset; no new user-facing errors, alerts, command output, or API bodies were added.
Cmux Full Internationalization ✅ Passed Diff only changes internal color-slot/state logic and tests; no user-facing Swift copy or locale catalog entries were added or changed.
Cmux Swiftui State Layout ✅ Passed Adds in-memory @Observable model state and helper methods only; no new ObservableObject/@published, GeometryReader, lazy-row store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed StableMacColorSlots has one MainActor owner, clear invariants, and no timing/observer/duplicate-lifecycle workaround; the fix is a local correctness change with tests.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The PR only changes color-slot logic/tests in shell/model files; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes were found.
Cmux Source Artifacts ✅ Passed Only changed path is .github/swift-file-length-budget.tsv, a checked-in CI/config budget file referenced by tests and workflow; no artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR adds only production lifecycle helpers and stable-color logic; no new #if DEBUG, ForTesting, or other test/debug seam was introduced in Sources/.
Cmux No Ambient Global State ✅ Passed PASS: stableMacColorSlots lives on MobileShellComposite, helpers are instance methods in an extension, and MobileWorkspaceAggregation remains constructable—no new globals/singletons.
Title check ✅ Passed The title accurately summarizes the main change: stabilizing iOS Mac avatar color slots during Mac switches.
Description check ✅ Passed The description covers the summary and testing clearly, with only non-critical template parts like demo video and checklist details missing.
✨ 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 issue-ios-mac-switch-color-flash

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.

@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.

🧹 Nitpick comments (1)
Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceAggregationTests.swift (1)

48-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor inconsistency: use the derivedWorkspaces helper for consistency.

Line 56 calls MobileWorkspaceAggregation().derivedWorkspaces(...) directly while all other tests in this suite were migrated to use the derivedWorkspaces(statesByMac:foregroundMacDeviceID:) helper. Consider using the helper here too for consistency.

♻️ Optional refactor for consistency
-        let derived = MobileWorkspaceAggregation().derivedWorkspaces(
-            statesByMac: states,
-            foregroundMacDeviceID: "mac-a",
-            machineColorIndex: idx
-        )
+        let derived = derivedWorkspaces(statesByMac: states, foregroundMacDeviceID: "mac-a")

Note: the helper computes its own color index from scratch, so if you need to assert against the explicit idx values, keep the direct call. The current assertions only check idx["mac-a"] and idx["mac-b"] against the derived results, so using the helper would still work if you compute idx via the machineColorIndex helper first.

🤖 Prompt for 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.

In
`@Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceAggregationTests.swift`
around lines 48 - 66, Use the suite’s
derivedWorkspaces(statesByMac:foregroundMacDeviceID:) helper instead of calling
MobileWorkspaceAggregation().derivedWorkspaces(...) directly in
distinctMacsGetDistinctColorIndicesAndSameMacShares. Preserve the explicit
machine color index assertions by computing idx with the existing
machineColorIndex helper and ensuring the helper-derived results remain
consistent with those values.
🤖 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.

Nitpick comments:
In
`@Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceAggregationTests.swift`:
- Around line 48-66: Use the suite’s
derivedWorkspaces(statesByMac:foregroundMacDeviceID:) helper instead of calling
MobileWorkspaceAggregation().derivedWorkspaces(...) directly in
distinctMacsGetDistinctColorIndicesAndSameMacShares. Preserve the explicit
machine color index assertions by computing idx with the existing
machineColorIndex helper and ensuring the helper-derived results remain
consistent with those values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53d68e8f-fd12-490a-b1b3-9ff61c7f12e1

📥 Commits

Reviewing files that changed from the base of the PR and between fa53bd0 and c35d9df.

📒 Files selected for processing (3)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceAggregation.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceAggregationTests.swift

MobileWorkspaceAggregation.machineColorIndex now takes a state parameter
that grew MobileShellComposite.swift by 15 lines, past the CI budget's
hard cap (files already over the 900-line default cap must have zero net
growth vs. origin/main, independent of the checked-in budget value).

Move updateStableMacColorSlots() into the existing
MobileShellComposite+MacSwitchState.swift extension file (same pattern
already used there for isMacSwitchInFlight) and combine two short
declarations with semicolons, matching existing precedent elsewhere in
the file (see effectiveViewportSizesBySurfaceID). Net line count vs.
origin/main is now zero; no behavior change.

@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.

There are 2 total unresolved issues (including 1 from previous review).

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 a246305. Configure here.

cmux reload-cloud added 2 commits July 12, 2026 19:07
`stableMacColorSlots` is additive-only, so sign-out and team switches
leak the previous account/team's Mac→color assignments instead of
resetting them. These tests currently fail without the fix.
updateStableMacColorSlots() only ever adds entries (that's the fix for
the switching-color-flash bug), so nothing previously cleared it. On
sign-out the next account inherited the old account's Mac→color
assignments; on a team switch the old team's background Macs lingered
in the slot map forever, permanently shifting new Macs' color numbers
upward.

Both resets have to run after every didSet-triggering assignment in
their function: signOut()'s earlier `foregroundMacDeviceID = nil` and
currentTeamDidChange()'s `workspacesByMac = ...filter` each trigger
updateStableMacColorSlots() against the still-stale workspacesByMac,
so a reset placed before them gets silently repopulated.
didSet { recomputeDerivedWorkspaceState() }
}
private let workspaceAggregation = MobileWorkspaceAggregation()
let workspaceAggregation = MobileWorkspaceAggregation(); var stableMacColorSlots: [String: Int] = [:] // see MobileShellComposite+MacSwitchState.swift

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 Seed initial slots stableMacColorSlots still starts empty and is only filled when workspacesByMac fires its didSet. The initializer assigns the initial workspace seed directly, so that observer does not run during construction. If a shell is created with real Mac workspace state, the first read of machineColorIndex can still return an empty map until a later workspace mutation happens, so the first Computers render can show missing or fallback avatar colors before the stable slots appear. Seed this map from the initial workspacesByMac before the shell is exposed.

cmux reload-cloud added 4 commits July 12, 2026 19:20
MobileShellComposite.swift grew by 12 lines for the sign-out/team-switch
reset, and MobileShellCompositePreviewTests.swift crossed the 500-line
tracking threshold with its new regression tests. Both are real,
reviewed growth, not incidental bloat, so refresh the budget rather
than split either file.
…color-flash

# Conflicts:
#	.github/swift-file-length-budget.tsv
MobileShellComposite.swift is already well past its 900-line hard cap on
main, which bans any growth at all regardless of the checked-in budget
number. Move the reset bodies into MobileShellComposite+MacSwitchState.swift
(already under threshold, and already used for exactly this purpose) and
collapse the two call sites onto existing lines, bringing the file back to
main's exact line count.

MobileShellCompositePreviewTests.swift crossing 500 lines for the first time
is also an unconditional failure, not something a budget entry can accept.
Split the two new color-slot regression tests into a sibling
MobileShellCompositeColorSlotResetTests.swift, matching the codebase's
established split-on-threshold-crossing convention, and revert the shared
signOutReturnsToPreviewHostState test to its pre-fix form.
The file dropped back to 488 lines (under the 500-line tracking
threshold) after the color-slot regression tests moved to their own
sibling file, so it no longer needs a budget row.
@azooz2003-bit
azooz2003-bit merged commit 989413b into main Jul 13, 2026
34 of 36 checks passed
@azooz2003-bit
azooz2003-bit deleted the issue-ios-mac-switch-color-flash branch July 13, 2026 03:40

This branch was successfully deployed

1 active deployment
Preview – cmux — 5a496083 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