Skip to content

Fix sidebar tab selection highlight timing - #6627

Merged
austinywang merged 1 commit into
mainfrom
issue-6626-tab-selection-double-highlight
Jun 23, 2026
Merged

austinywang merged 1 commit into
mainfrom
issue-6626-tab-selection-double-highlight

Conversation

@austinywang

@austinywang austinywang commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6626

Fixes the vertical-tab visual regression where selecting a workspace paints in two visible phases: first the translucent multi-selected background, then the solid active selected background a moment later.

Regression provenance

The last known-good tag is v0.64.16 (e647c3049c03786299d81f3c287571373d635269). Diff/blame against HEAD points to 5ea81da1a5 / PR #6460 (Fix title-churn beachball in transcript adoption and sidebar rows) as the selection-highlight regression source.

That PR intentionally moved active workspace selection out of the VerticalTabsSidebar parent and into row-local TabItemView state so a selected-workspace change only invalidates the previous and new rows instead of every row under the ForEach. The problem was the row-local state pipeline added .receive(on: RunLoop.main), while the click path still updates selectedTabIds synchronously. On a normal click, the row first rendered as merely multi-selected (opacity: 0.25), then the scheduled active-state update arrived and repainted the solid selected fill (opacity: 1).

Fix

TabManager is @MainActor and sends selectedTabIdPublisher from its selected-workspace will-change hook, so the row-local active-state update is already main-thread delivered. Removing the extra run-loop scheduler hop keeps #6460's scoped two-row invalidation, but lets the active-state change settle in the same interaction transaction as the sidebar selection set. The selected row now paints the solid selection highlight immediately in one phase.

Verification

  • git diff --check
  • Inspected v0.64.16..HEAD diffs/blame for ContentView.swift, SidebarAppearanceSupport.swift, and TabManager.swift.
  • No regression test added: this is a SwiftUI frame-ordering/visual timing bug, and a meaningful behavioral test would require an app/UI runtime build. A source-shape test would not prove the user-visible flash is fixed.
  • No local/cloud app build or launch was run per the issue handoff instructions; the sanctioned dev build is intentionally deferred until CI is green and the user explicitly asks for it.

Localization

No user-facing strings were added or changed.


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


Summary by cubic

Fixes the sidebar vertical-tab highlight lag that briefly shows a translucent highlight before the solid active state when selecting a workspace (fixes #6626). Removes the extra .receive(on: RunLoop.main) in TabItemView so TabManager’s main-thread selectedTabIdPublisher updates the active state in the same interaction, preventing the two-phase paint.

Written for commit e22a1ac. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved tab switching responsiveness by optimizing how selection state updates are processed, reducing unnecessary duplicate operations.

@vercel

vercel Bot commented Jun 23, 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 Jun 23, 2026 12:46am
cmux-staging Building Building Preview, Comment Jun 23, 2026 12:46am

@coderabbitai

coderabbitai Bot commented Jun 23, 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: 07d35142-04f6-4e03-98df-79119bb771e7

📥 Commits

Reviewing files that changed from the base of the PR and between c19767a and e22a1ac.

📒 Files selected for processing (1)
  • Sources/ContentView.swift
💤 Files with no reviewable changes (1)
  • Sources/ContentView.swift

📝 Walkthrough

Walkthrough

In TabItemView, the .onReceive pipeline for tab selection state is updated to map the incoming selectedTabId to a Bool ($0 == tab.id), apply .removeDuplicates(), and remove the .receive(on: RunLoop.main) step before calling updateObservedActiveState(isSelected).

Changes

Tab Selection Publisher Deduplication

Layer / File(s) Summary
TabItemView selection pipeline
Sources/ContentView.swift
Adds .map { $0 == tab.id } and .removeDuplicates() to the onReceive pipeline, and removes .receive(on: RunLoop.main). This prevents updateObservedActiveState from being called redundantly with the same isSelected value, eliminating the two-phase highlight.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 A tab once blushed twice — first shy, then bold,
A duplicate signal, a stutter of old.
One removeDuplicates() hop set things right,
The highlight now snaps with a single crisp bite.
No lag, no translucent ghost haunting the view —
Just one solid blue, clean, immediate, true! ✨


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Cmux Source Artifacts ❌ Error PR adds 28 files from hidden scratch directories (.claude/ and .greptile/) that violate source-control-artifacts.md rule, including .claude/scheduled_tasks.lock (session lock with PID/timestamp) an... Remove all files from .claude/ and .greptile/ directories, and add these patterns to .gitignore to prevent future commits of local development tool artifacts.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: removing the extra scheduler hop in the tab selection state pipeline to fix the two-phase highlight rendering bug.
Description check ✅ Passed The description provides comprehensive context including the root cause analysis, regression source, fix details, and verification steps. However, it does not include a demo video, which is specified in the template.
Linked Issues check ✅ Passed The PR directly addresses issue #6626 by removing the .receive(on: RunLoop.main) scheduler hop that caused the two-phase highlight rendering, restoring single-phase selection behavior as expected in version 0.64.16.
Out of Scope Changes check ✅ Passed The change is narrowly scoped to fix the identified regression in TabItemView's subscription handling. No unrelated modifications or out-of-scope changes are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed Change removes redundant .receive(on: RunLoop.main) from TabItemView's subscription to TabManager's @MainActor selectedTabIdPublisher; no new actor isolation issues introduced, only redundant synch...
Cmux Swift Blocking Runtime ✅ Passed PR removes .receive(on: RunLoop.main) scheduler hop, which decreases blocking/timing synchronization rather than introducing it. No blocking runtime primitives added.
Cmux Expensive Synchronous Load ✅ Passed PR removes .receive(on: RunLoop.main) from TabItemView.selectedTabIdPublisher subscription—a timing fix that improves performance, not adds expensive loads. No agent session loading, JSON parsing,...
Cmux Cache Substitution Correctness ✅ Passed PR removes .receive(on: RunLoop.main) scheduler hop from TabItemView's UI state pipeline, not a cache substitution in persistence/undo/snapshot/history path—no rule violation.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code changes (main fix in ContentView.swift) and prototype development scaffolding. The rule covers TypeScript/JavaScript/shell only; Swift timing is covered separately. Adde...
Cmux Algorithmic Complexity ✅ Passed PR removes one line (.receive(on: RunLoop.main)) from TabItemView's selectedTabIdPublisher subscription, reducing scheduler overhead. No algorithmic complexity issues; change improves efficiency...
Cmux Swift Concurrency ✅ Passed The PR removes a .receive(on: RunLoop.main) scheduler hop from TabItemView's selectedTabIdPublisher subscription, reducing legacy async patterns. No new Dispatch queues, Combine app state, comple...
Cmux Swift @Concurrent ✅ Passed PR removes .receive(on: RunLoop.main) from a main-actor-isolated publisher subscription. No async/concurrent work involved; closure is synchronous UI-state update only.
Cmux Swift File And Package Boundaries ✅ Passed PR removes 1 line from existing ContentView.swift (16092 vs 16117 budgeted), an oversized file already tracked. Change is a focused UI timing bug fix (removing scheduler hop), matching allowed case...
Cmux Swiftpm Lockfiles ✅ Passed PR complies with swiftpm-package-resolved.md: cmux-owned .gitignore files don't ignore Package.resolved, root Xcode lockfile included, vendored third-party preserves upstream policy, all non-vendor...
Cmux Swift Logging ✅ Passed No logging violations found: zero print/debugPrint/dump/NSLog calls in Sources/ContentView.swift; all cmuxDebugLog calls are guarded by #if DEBUG per swift-logging.md rules.
Cmux User-Facing Error Privacy ✅ Passed The PR change removes a scheduler hop (.receive(on:)) from TabItemView's selectedTabIdPublisher subscription for rendering optimization. No user-facing error messages, alerts, or privacy-sensitive...
Cmux Full Internationalization ✅ Passed PR fixes a visual regression by removing .receive(on: RunLoop.main) from TabItemView's subscription to tabManager.selectedTabIdPublisher and adding .removeDuplicates(). This is a pure technic...
Cmux Swiftui State Layout ✅ Passed PR removes .receive(on: RunLoop.main) from existing tabManager.selectedTabIdPublisher subscription in TabItemView—no new ObservableObject/@Published/@StateObject state introduced, only incident...
Cmux Architecture Rethink ✅ Passed PR removes unnecessary .receive(on: RunLoop.main) scheduler hop from TabItemView's selectedTabIdPublisher subscription. This is a small correctness fix with clear ownership (TabManager @MainActor...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only modifies TabItemView (a SwiftUI View for sidebar tab rows), not any standalone cmux-owned windows (NSWindow, NSPanel, NSWindowController, Window, or WindowGroup). Changes only remove a sche...
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR removes one line of code (.receive(on: RunLoop.main)) from Sources/ContentView.swift with no added lines. No #if DEBUG blocks, test/debug seams, or visibility widening for test access 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 issue-6626-tab-selection-double-highlight

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.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Removes the .receive(on: RunLoop.main) scheduler hop from the selectedTabIdPublisher Combine pipeline inside TabItemView, eliminating the two-phase repaint that caused a visible flash when selecting a sidebar workspace tab.

  • TabManager is @MainActor and selectedTabIdPublisher is a CurrentValueSubject, so the publisher already delivers synchronously on the main thread; the extra run-loop re-dispatch was causing observedIsActive to settle one cycle late, painting the translucent multi-selected background before the solid active fill.
  • The two other .receive(on: RunLoop.main) hops in the same view (sidebarImmediateObservationPublisher and sidebarObservationPublisher) are left in place; those publishers may fire from non-main contexts and sidebarObservationPublisher additionally feeds a .debounce scheduled on RunLoop.main.

Confidence Score: 5/5

Safe to merge — the one-line removal correctly eliminates an unnecessary scheduler hop from a publisher that already delivers on the main thread.

The change is a single-line deletion of .receive(on: RunLoop.main) from a Combine pipeline whose source (CurrentValueSubject on @MainActor TabManager) always fires synchronously on the main thread. Removing the hop is provably safe and closes the two-phase render race without touching any other state path. The two remaining .receive(on: RunLoop.main) calls in the same view are intentionally kept for publishers that can originate off-main.

No files require special attention.

Important Files Changed

Filename Overview
Sources/ContentView.swift Removes .receive(on: RunLoop.main) from the selectedTabIdPublisher pipeline; correct and minimal — TabManager is @mainactor so the publisher fires synchronously on main with no re-dispatch needed.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant TabManager as TabManager (@MainActor)
    participant Publisher as selectedTabIdPublisher (CurrentValueSubject)
    participant TabItemView

    Note over User,TabItemView: Before fix — two render phases
    User->>TabManager: click workspace
    TabManager->>Publisher: send(newId) [synchronous, on main]
    Publisher-->>TabItemView: .receive(on: RunLoop.main) schedules next cycle
    TabItemView->>TabItemView: render phase 1: multi-selected (opacity 0.25)
    Note over TabItemView: next RunLoop cycle
    Publisher-->>TabItemView: "callback fires, observedIsActive = true"
    TabItemView->>TabItemView: render phase 2: solid active fill (opacity 1.0)

    Note over User,TabItemView: After fix — single render phase
    User->>TabManager: click workspace
    TabManager->>Publisher: send(newId) [synchronous, on main]
    Publisher-->>TabItemView: onReceive fires in same transaction
    TabItemView->>TabItemView: "observedIsActive = true, single solid render"
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"}}}%%
sequenceDiagram
    participant User
    participant TabManager as TabManager (@MainActor)
    participant Publisher as selectedTabIdPublisher (CurrentValueSubject)
    participant TabItemView

    Note over User,TabItemView: Before fix — two render phases
    User->>TabManager: click workspace
    TabManager->>Publisher: send(newId) [synchronous, on main]
    Publisher-->>TabItemView: .receive(on: RunLoop.main) schedules next cycle
    TabItemView->>TabItemView: render phase 1: multi-selected (opacity 0.25)
    Note over TabItemView: next RunLoop cycle
    Publisher-->>TabItemView: "callback fires, observedIsActive = true"
    TabItemView->>TabItemView: render phase 2: solid active fill (opacity 1.0)

    Note over User,TabItemView: After fix — single render phase
    User->>TabManager: click workspace
    TabManager->>Publisher: send(newId) [synchronous, on main]
    Publisher-->>TabItemView: onReceive fires in same transaction
    TabItemView->>TabItemView: "observedIsActive = true, single solid render"
Loading

Reviews (1): Last reviewed commit: "Fix sidebar tab selection highlight timi..." | Re-trigger Greptile

@austinywang
austinywang merged commit a9cd035 into main Jun 23, 2026
30 checks passed
austinywang added a commit that referenced this pull request Jun 23, 2026
Integrate #6609 (Codex sidebar status lifecycle) and #6627 (sidebar tab
selection highlight timing). ContentView auto-merged (no overlap with the
font fix / pinResolutionContext). swift-file-length-budget.tsv regenerated
via --write-budget. Shard partition verified identical to origin/main
(weight 1267).

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

This branch was successfully deployed

1 active deployment
Preview – cmux — e22a1acb Deployed Jun 23, 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.

Vertical tab switch shows double selection highlight (translucent then solid blue), feels laggy — regression since 0.64.16

1 participant