Skip to content

Fix sidebar edge fade background - #4610

Merged
lawrencecchen merged 1 commit into
mainfrom
fix-sidebar-edge-gradient
May 23, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
fix-sidebar-edge-gradient

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

  • Replace sidebar edge scrim overlays with masks on sidebar scroll content.
  • Let faded rows reveal the actual sidebar background/material underneath instead of drawing a separate visual effect layer.
  • Apply the same mask to the default workspace list and extension sidebar list.

Tests:

  • git diff --check
  • xcrun swiftc -parse Sources/SidebarScrim.swift Sources/ContentView.swift cmuxTests/WindowAppearanceSnapshotTests.swift
  • ./scripts/reload.sh --tag sidegrad
  • GitHub checks pass on commit 8c429e2d0 after rerunning transient web-typecheck setup failure.

Dogfood:

  • sidegrad is built from commit 8c429e2d0 for visual approval before merge.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented May 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 May 23, 2026 9:00am
cmux-staging Building Building Preview, Comment May 23, 2026 9:00am

@coderabbitai

coderabbitai Bot commented May 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces AppKit-backed scrim/blur overlays with a SwiftUI gradient-based edge fade mask and applies it to the workspace list and extension sidebar timeline scroll areas, using configured top and bottom scrim heights.

Changes

Edge Fade Mask Replacement

Layer / File(s) Summary
Edge fade mask implementation
Sources/SidebarScrim.swift
New SidebarWorkspaceScrollEdgeFadeMask View composed of top and bottom SidebarEdgeFadeGradient segments with fixed heights and a central divider; private gradient defines edge enum and color/opacity stops and reverses stops for bottom edge.
Apply mask to scroll areas
Sources/ContentView.swift
Replace SidebarTopScrim/SidebarBottomScrim overlays with .mask(SidebarWorkspaceScrollEdgeFadeMask(...)) on the workspace list and the extension sidebar timeline scroll areas, passing sidebarTopScrimHeight and sidebarBottomScrimHeight.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

"A rabbit hops and paints the rim,
gradients whisper, edges dim,
scrims retired, a softer mask,
SwiftUI hums — no AppKit task. 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 13

❌ Failed checks (1 warning, 12 inconclusive)

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.
Cmux Swift Actor Isolation ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Blocking Runtime ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux No Hacky Sleeps ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Concurrency ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift @Concurrent ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift File And Package Boundaries ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Logging ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux User-Facing Error Privacy ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Full Internationalization ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swiftui State Layout ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Architecture Rethink ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Auxiliary Window Close Shortcuts ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title 'Fix sidebar edge fade background' clearly summarizes the main change: replacing scrim overlays with edge fade masks in the sidebar.
Description check ✅ Passed The description covers the key changes, testing approach, and dogfooding validation, though it omits the optional demo video and formal review trigger request sections from the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix-sidebar-edge-gradient

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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 and usage tips.

@greptile-apps

greptile-apps Bot commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the two-layer overlay approach (blur NSVisualEffectView + gradient mask) with a single SwiftUI .mask() applied directly to the scroll content, letting the real sidebar material show through at the edges instead of drawing a separate effect layer.

  • SidebarScrim.swift is rewritten: SidebarTopScrim, SidebarBottomScrim, and SidebarEdgeBlurEffect (an NSViewRepresentable) are deleted; a new SidebarWorkspaceScrollEdgeFadeMask composes a top-fade gradient, a fully-opaque middle Rectangle, and a bottom-fade gradient into a single mask view.
  • ContentView.swift swaps the pair of .overlay scrim calls for .mask(SidebarWorkspaceScrollEdgeFadeMask(...)) in both sidebar scroll sites; gradient stop opacities are correctly inverted (nearly transparent at the edge, opaque toward center) to match mask semantics.

Confidence Score: 5/5

Safe to merge — the change removes an NSViewRepresentable and the observer machinery it required, replacing both call sites with a pure SwiftUI mask. No logic is at risk.

The diff deletes more than it adds: the NSVisualEffectView representable, the separate top/bottom scrim overlay views, and the associated layout are all gone. The replacement mask view is a straightforward VStack of two gradients and a solid Rectangle with correctly inverted opacity stops for mask semantics. No async work, no shared state, no actor crossings, and no user-facing strings are introduced.

No files require special attention.

Important Files Changed

Filename Overview
Sources/SidebarScrim.swift Full rewrite removes NSViewRepresentable blur layer; new SidebarWorkspaceScrollEdgeFadeMask is a clean VStack mask with correctly inverted gradient stops.
Sources/ContentView.swift Two overlay/scrim call sites replaced with .mask(SidebarWorkspaceScrollEdgeFadeMask(...)); remaining overlays (drag handle, drop target) unaffected.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["VerticalTabsSidebar scroll content\n(.mask applied)"]

    subgraph OLD["Old approach (removed)"]
        B1["SidebarTopScrim overlay\n(SidebarEdgeBlurEffect + gradient mask)"]
        B2["SidebarBottomScrim overlay\n(SidebarEdgeBlurEffect + gradient mask)"]
    end

    subgraph NEW["New approach"]
        C["SidebarWorkspaceScrollEdgeFadeMask\n(SwiftUI .mask)"]
        C1["SidebarEdgeFadeGradient(edge: .top)\n.frame(height: topHeight)\nopacity: 0.05 → 1.0"]
        C2["Rectangle().fill(.black)\n(fully opaque — content visible)"]
        C3["SidebarEdgeFadeGradient(edge: .bottom)\n.frame(height: bottomHeight)\nopacity: 1.0 → 0.05"]
        C --> C1
        C --> C2
        C --> C3
    end

    A --> NEW
Loading

Reviews (3): Last reviewed commit: "Fix sidebar edge fade background" | Re-trigger Greptile

Comment thread Sources/SidebarScrim.swift Outdated
Comment on lines +114 to +119
func updateNSView(_ nsView: SidebarScrollEdgeObserverView, context: Context) {
nsView.configure(currentVisibility: visibility) { newVisibility in
visibility = newVisibility
}
nsView.resolveScrollView()
}

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 updateNSView calls resolveScrollView() on every SwiftUI pass, which enqueues a DispatchQueue.main.async each time the parent redraws (including every scroll-triggered state update). The view-lifecycle overrides viewDidMoveToSuperview and viewDidMoveToWindow already handle initial discovery and any subsequent move to a different scroll view, so the extra dispatch in updateNSView only adds churn. If the concern is a SwiftUI hierarchy rebuild that leaves the view in place while swapping the enclosing scroll view, a targeted resolveScrollView() could be guarded to only run once the observer isn't already installed.

Suggested change
func updateNSView(_ nsView: SidebarScrollEdgeObserverView, context: Context) {
nsView.configure(currentVisibility: visibility) { newVisibility in
visibility = newVisibility
}
nsView.resolveScrollView()
}
func updateNSView(_ nsView: SidebarScrollEdgeObserverView, context: Context) {
nsView.configure(currentVisibility: visibility) { newVisibility in
visibility = newVisibility
}
// resolveScrollView is already called from viewDidMoveToSuperview/viewDidMoveToWindow.
// Only re-run discovery if no scroll view has been found yet (e.g. first pass
// before the hierarchy stabilises), not on every SwiftUI invalidation.
if !nsView.hasObservedScrollView {
nsView.resolveScrollView()
}
}

Comment thread Sources/SidebarScrim.swift Outdated
Comment on lines +183 to +218
let clipView = scrollView.contentView
clipView.postsBoundsChangedNotifications = true
clipView.postsFrameChangedNotifications = true
observers.append(NotificationCenter.default.addObserver(
forName: NSView.boundsDidChangeNotification,
object: clipView,
queue: .main
) { [weak self] _ in
self?.updateVisibility()
})
observers.append(NotificationCenter.default.addObserver(
forName: NSView.frameDidChangeNotification,
object: clipView,
queue: .main
) { [weak self] _ in
self?.updateVisibility()
})

if let documentView {
documentView.postsBoundsChangedNotifications = true
documentView.postsFrameChangedNotifications = true
observers.append(NotificationCenter.default.addObserver(
forName: NSView.boundsDidChangeNotification,
object: documentView,
queue: .main
) { [weak self] _ in
self?.updateVisibility()
})
observers.append(NotificationCenter.default.addObserver(
forName: NSView.frameDidChangeNotification,
object: documentView,
queue: .main
) { [weak self] _ in
self?.updateVisibility()
})
}

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 postsBoundsChangedNotifications not restored on cleanup

installObservers enables postsBoundsChangedNotifications = true and postsFrameChangedNotifications = true on both the clip view and the document view, but removeObservers() only removes the NSObjectProtocol tokens — it never resets these flags. If the sidebar's scroll view is replaced (triggering removeObservers for the old one), the old clip and document views keep posting notifications to any future listener. If another component in the codebase assumes those flags are false for those views, this becomes a silent source of unexpected callbacks. Capture the previous values before setting them and restore them in removeObservers.

Comment on lines +320 to +383
}

final class SidebarScrollEdgeVisibilityTests: XCTestCase {
func testHiddenWhenDocumentDoesNotOverflowViewport() {
let visibility = SidebarScrollEdgeVisibility.resolve(
visibleRect: CGRect(x: 0, y: 0, width: 200, height: 500),
documentBounds: CGRect(x: 0, y: 0, width: 200, height: 500),
documentIsFlipped: true
)

XCTAssertEqual(visibility, .hidden)
}

func testFlippedDocumentShowsOnlyBottomScrimAtTop() {
let visibility = SidebarScrollEdgeVisibility.resolve(
visibleRect: CGRect(x: 0, y: 0, width: 200, height: 400),
documentBounds: CGRect(x: 0, y: 0, width: 200, height: 1_000),
documentIsFlipped: true
)

XCTAssertFalse(visibility.showsTopScrim)
XCTAssertTrue(visibility.showsBottomScrim)
}

func testFlippedDocumentShowsBothScrimsInMiddle() {
let visibility = SidebarScrollEdgeVisibility.resolve(
visibleRect: CGRect(x: 0, y: 200, width: 200, height: 400),
documentBounds: CGRect(x: 0, y: 0, width: 200, height: 1_000),
documentIsFlipped: true
)

XCTAssertTrue(visibility.showsTopScrim)
XCTAssertTrue(visibility.showsBottomScrim)
}

func testFlippedDocumentShowsOnlyTopScrimAtBottom() {
let visibility = SidebarScrollEdgeVisibility.resolve(
visibleRect: CGRect(x: 0, y: 600, width: 200, height: 400),
documentBounds: CGRect(x: 0, y: 0, width: 200, height: 1_000),
documentIsFlipped: true
)

XCTAssertTrue(visibility.showsTopScrim)
XCTAssertFalse(visibility.showsBottomScrim)
}

func testNonFlippedDocumentUsesOppositeYAxisOrigin() {
let topVisibility = SidebarScrollEdgeVisibility.resolve(
visibleRect: CGRect(x: 0, y: 600, width: 200, height: 400),
documentBounds: CGRect(x: 0, y: 0, width: 200, height: 1_000),
documentIsFlipped: false
)
let bottomVisibility = SidebarScrollEdgeVisibility.resolve(
visibleRect: CGRect(x: 0, y: 0, width: 200, height: 400),
documentBounds: CGRect(x: 0, y: 0, width: 200, height: 1_000),
documentIsFlipped: false
)

XCTAssertFalse(topVisibility.showsTopScrim)
XCTAssertTrue(topVisibility.showsBottomScrim)
XCTAssertTrue(bottomVisibility.showsTopScrim)
XCTAssertFalse(bottomVisibility.showsBottomScrim)
}
}

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 Unrelated test class appended to existing file

SidebarScrollEdgeVisibilityTests tests SidebarScrollEdgeVisibility.resolve(...), which lives in SidebarScrim.swift and has no connection to window-appearance snapshots. Placing it in WindowAppearanceSnapshotTests.swift grows the file's scope and makes the tests harder to discover. A dedicated SidebarScrollEdgeVisibilityTests.swift (or SidebarScrimTests.swift) alongside the other unit tests would be more consistent with the repository's naming conventions.

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!

@lawrencecchen
lawrencecchen force-pushed the fix-sidebar-edge-gradient branch from b1b5a5f to 8c429e2 Compare May 23, 2026 08:48
@lawrencecchen lawrencecchen changed the title Fix sidebar edge fade visibility Fix sidebar edge fade background May 23, 2026
@lawrencecchen
lawrencecchen merged commit fb3d443 into main May 23, 2026
25 of 26 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 23, 2026
3 of 6 tasks

This branch was successfully deployed

1 active deployment
Preview – cmux — 8c429e2d Deployed May 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.

1 participant