Skip to content

Use workspace color for notification ring and selection bar - #664

Merged
lawrencecchen merged 1 commit into
mainfrom
issue-557-notification-ring-color
Feb 28, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
issue-557-notification-ring-color

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Notification/focus flash now uses workspace customColor instead of hardcoded systemBlue (falls back to app accent color)
  • Sidebar selection bar/indicator uses workspace color when set
  • Flash color propagated through Panel.triggerFlash(color:) API
  • Browser panel flash overlay uses workspace color

Fixes #557

What's NOT included

Test plan

  • Set a workspace color via right-click → Workspace Color
  • Trigger flash (Cmd+Shift+H) — ring should use workspace color
  • Verify selection bar in sidebar uses workspace color
  • Clear workspace color — verify fallback to accent color

Summary by CodeRabbit

  • New Features
    • Added support for custom tab selection colors—tabs now use per-tab colors when available
    • Added customizable focus flash animation colors
  • Improvements
    • Enhanced visual consistency for focus and notification indicators throughout the application

- Notification/focus flash uses workspace customColor (fallback: accent)
- Selection bar/indicator uses workspace customColor when set
- Flash color propagated through Panel.triggerFlash(color:) API
- Browser panel flash overlay uses workspace color
- Regression tests for flash color resolution

Fixes #557
@vercel

vercel Bot commented Feb 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Building Building Preview, Comment Feb 28, 2026 1:49am

@coderabbitai

coderabbitai Bot commented Feb 28, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The pull request adds optional color customization for focus/notification flash visuals throughout the application. Color parameters are threaded through the Panel protocol and concrete implementations (TerminalPanel, BrowserPanel), enabling runtime color selection for flash animations. Workspace-level color resolution provides hex string validation and fallback logic.

Changes

Cohort / File(s) Summary
Focus Flash Protocol & Panel Updates
Sources/Panels/Panel.swift, Sources/Panels/TerminalPanel.swift, Sources/Panels/BrowserPanel.swift
Updated triggerFlash() method signatures to accept optional NSColor parameter. BrowserPanel adds @Published focusFlashColor property and resolves color with fallback to cmuxAccentNSColor().
Terminal View Flash Color Support
Sources/GhosttyTerminalView.swift
Enhanced triggerFlash(color:) signature to accept optional color override. Added resolvedWorkspaceFocusFlashColor() and setFocusFlashColor(_:) helpers to manage notification ring and flash layer colors via CATransaction.
Browser Panel UI Integration
Sources/Panels/BrowserPanelView.swift
Introduced private focusFlashColor computed property that applies color to focus ring overlay stroke and shadow instead of hardcoded cmuxAccentColor().
Workspace-Level Color Resolution
Sources/Workspace.swift
Added static resolvedFocusFlashColor(customColorHex:fallback:) to validate and normalize hex strings. New private focusFlashColor property. Enhanced triggerNotificationFocusFlash() with requiresSplit and shouldFocus parameters for conditional flashing.
Tab Selection Color
Sources/ContentView.swift
Added activeSelectionColor computed property in TabItemView to prefer custom per-tab colors when available, falling back to existing sidebar colors.
Color Validation Tests
cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Added two unit tests validating hex string normalization (valid and invalid cases) in Workspace.resolvedFocusFlashColor() logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A color for each flash so bright,
Hex strings now guide the visual light,
From workspace down through panel's glow,
Custom hues in a rainbow flow,
Focus rings dressed in chosen pride! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR partially addresses issue #557 by enabling workspace color support for notification ring and flash, but does not implement the requested CLI flags (--color for notify and trigger-flash commands). Clarify whether CLI command flag implementation is deferred or if this PR is incomplete; the socket/command layer changes mentioned in #557 appear absent from the code changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: workspace color is now used for notification ring flash and tab selection bar instead of hardcoded system colors.
Out of Scope Changes check ✅ Passed The PR introduces workspace color support for multiple UI surfaces (notification ring, flash overlay, selection bar) and adds test coverage; all changes directly support the workspace color feature objective.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-557-notification-ring-color

Comment @coderabbitai help to get the list of available commands and usage tips.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Workspace.swift (1)

2956-2970: ⚠️ Potential issue | 🟠 Major

Notification flash API still blocks per-notification color and non-terminal flash paths.

Line 2961 hard-restricts this flow to TerminalPanel, and Line 2969 always uses workspace-derived color. That bypasses the new panel-agnostic triggerFlash(color:) path and leaves no entry point for CLI/socket-supplied notification colors.

💡 Proposed fix
-    func triggerNotificationFocusFlash(
-        panelId: UUID,
-        requiresSplit: Bool = false,
-        shouldFocus: Bool = true
-    ) {
-        guard let terminalPanel = terminalPanel(for: panelId) else { return }
+    func triggerNotificationFocusFlash(
+        panelId: UUID,
+        requiresSplit: Bool = false,
+        shouldFocus: Bool = true,
+        colorHex: String? = nil
+    ) {
+        guard let panel = panels[panelId] else { return }
         if shouldFocus {
             focusPanel(panelId)
         }
         let isSplit = bonsplitController.allPaneIds.count > 1 || panels.count > 1
         if requiresSplit && !isSplit {
             return
         }
-        terminalPanel.triggerFlash(color: focusFlashColor)
+        let resolvedColor = Self.resolvedFocusFlashColor(
+            customColorHex: colorHex ?? customColor
+        )
+        panel.triggerFlash(color: resolvedColor)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 2956 - 2970, The current
triggerNotificationFocusFlash function restricts flashes to TerminalPanel and
always uses workspace focusFlashColor; change it so it locates the generic panel
(use the panel lookup used elsewhere instead of terminalPanel(for:)), add an
optional color parameter (e.g., color: Color? = nil) and pass (color ??
focusFlashColor) to the panel's triggerFlash(color:) method, preserve the
requiresSplit/isSplit check and focusPanel(panelId) behavior, and remove the
TerminalPanel-specific casting so both terminal and non-terminal panels (and
CLI/socket-supplied colors) go through the panel-agnostic triggerFlash(color:)
path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 4158-4166: setFocusFlashColor(_:) is overwriting explicit flash
colors because setNotificationRing(visible:) calls it with nil; update
setFocusFlashColor(_:updateFlash:) (or create separate setRingColor(_:) and
setFlashColor(_:) methods) so ring updates can opt out of mutating flashLayer;
modify setNotificationRing(visible:) to call the ring-only path (e.g.,
setRingColor(resolved) or setFocusFlashColor(resolved, updateFlash: false)) and
keep triggerFlash(color:) writing the flashLayer color directly so explicit
flashes are not clobbered.

---

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 2956-2970: The current triggerNotificationFocusFlash function
restricts flashes to TerminalPanel and always uses workspace focusFlashColor;
change it so it locates the generic panel (use the panel lookup used elsewhere
instead of terminalPanel(for:)), add an optional color parameter (e.g., color:
Color? = nil) and pass (color ?? focusFlashColor) to the panel's
triggerFlash(color:) method, preserve the requiresSplit/isSplit check and
focusPanel(panelId) behavior, and remove the TerminalPanel-specific casting so
both terminal and non-terminal panels (and CLI/socket-supplied colors) go
through the panel-agnostic triggerFlash(color:) path.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1815745 and a64e828.

📒 Files selected for processing (8)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Panels/Panel.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/Workspace.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift

Comment on lines +4158 to +4166
private func setFocusFlashColor(_ color: NSColor?) {
let resolved = color ?? resolvedWorkspaceFocusFlashColor()
CATransaction.begin()
CATransaction.setDisableActions(true)
notificationRingLayer.strokeColor = resolved.cgColor
notificationRingLayer.shadowColor = resolved.cgColor
flashLayer.strokeColor = resolved.cgColor
flashLayer.shadowColor = resolved.cgColor
CATransaction.commit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setNotificationRing can unintentionally overwrite explicit flash colors.

setFocusFlashColor(_:) updates both ring and flash layers. At Line 4194, setNotificationRing(visible:) calls it with nil, so frequent SwiftUI updates can reset flashLayer to workspace/accent and clobber the explicit color passed by triggerFlash(color:) at Line 4425.

Consider splitting ring and flash color application, or adding a flag so ring updates do not mutate flashLayer color.

Suggested fix
-    private func setFocusFlashColor(_ color: NSColor?) {
+    private func setFocusFlashColor(_ color: NSColor?, includeFlashLayer: Bool = true, includeRingLayer: Bool = true) {
         let resolved = color ?? resolvedWorkspaceFocusFlashColor()
         CATransaction.begin()
         CATransaction.setDisableActions(true)
-        notificationRingLayer.strokeColor = resolved.cgColor
-        notificationRingLayer.shadowColor = resolved.cgColor
-        flashLayer.strokeColor = resolved.cgColor
-        flashLayer.shadowColor = resolved.cgColor
+        if includeRingLayer {
+            notificationRingLayer.strokeColor = resolved.cgColor
+            notificationRingLayer.shadowColor = resolved.cgColor
+        }
+        if includeFlashLayer {
+            flashLayer.strokeColor = resolved.cgColor
+            flashLayer.shadowColor = resolved.cgColor
+        }
         CATransaction.commit()
     }

     func setNotificationRing(visible: Bool) {
         if !Thread.isMainThread {
             DispatchQueue.main.async { [weak self] in
                 self?.setNotificationRing(visible: visible)
             }
             return
         }

-        setFocusFlashColor(nil)
+        setFocusFlashColor(nil, includeFlashLayer: false, includeRingLayer: true)
         CATransaction.begin()
         CATransaction.setDisableActions(true)
         notificationRingOverlayView.isHidden = !visible
         notificationRingLayer.opacity = visible ? 1 : 0
         CATransaction.commit()
     }

     func triggerFlash(color: NSColor? = nil) {
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
-            self.setFocusFlashColor(color)
+            self.setFocusFlashColor(color, includeFlashLayer: true, includeRingLayer: false)
             self.updateFlashPath()
             ...
         }
     }

Also applies to: 4194-4194, 4417-4426

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 4158 - 4166,
setFocusFlashColor(_:) is overwriting explicit flash colors because
setNotificationRing(visible:) calls it with nil; update
setFocusFlashColor(_:updateFlash:) (or create separate setRingColor(_:) and
setFlashColor(_:) methods) so ring updates can opt out of mutating flashLayer;
modify setNotificationRing(visible:) to call the ring-only path (e.g.,
setRingColor(resolved) or setFocusFlashColor(resolved, updateFlash: false)) and
keep triggerFlash(color:) writing the flashLayer color directly so explicit
flashes are not clobbered.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a64e82858a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/ContentView.swift
switch activeTabIndicatorStyle {
case .leftRail:
if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) }
if isActive { return activeSelectionColor }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep active tab text legible with custom selection colors

Switching the active background to activeSelectionColor means an active workspace can now use arbitrary custom hex colors, but the active foreground still comes from sidebarSelectedWorkspaceForegroundNSColor (hardcoded white in ContentView.swift). For light custom colors (for example #F5E663), the title/metadata/icons become very low-contrast and hard to read, which is a regression from the previous fixed accent background. Please derive active foreground from the resolved custom color (or compute contrast) when isActive uses resolvedCustomTabColor.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces hardcoded systemBlue flash colors with workspace-specific custom colors throughout the application.

  • Core changes: Added Workspace.resolvedFocusFlashColor() to resolve custom workspace colors with proper fallback to accent color
  • Terminal panels: Flash and notification ring colors now dynamically update based on workspace customColor setting
  • Browser panels: Focus flash overlay uses workspace color via published property
  • Sidebar: Selection bar/indicator uses workspace custom color when available
  • Test coverage: Added tests validating color resolution for valid and invalid hex values

The implementation properly propagates colors through the Panel.triggerFlash(color:) API and includes appropriate fallback handling when custom colors are not set. Color updates are correctly dispatched to the main thread for CALayer modifications.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Clean implementation with proper fallback handling, good test coverage, and consistent changes across all affected components. Only minor style issue found (extra blank line).
  • No files require special attention

Important Files Changed

Filename Overview
Sources/Workspace.swift Added static method to resolve workspace focus flash color from custom hex, with proper fallback handling
Sources/GhosttyTerminalView.swift Changed initialization from hardcoded systemBlue to cmuxAccentNSColor, added workspace color resolution and dynamic color updates
Sources/Panels/Panel.swift Updated protocol to add optional color parameter to triggerFlash method
Sources/ContentView.swift Added activeSelectionColor property to use workspace custom color for sidebar tab selection indicator

Sequence Diagram

sequenceDiagram
    participant User
    participant Workspace
    participant Panel
    participant TerminalPanel
    participant GhosttySurfaceScrollView
    participant BrowserPanel
    participant BrowserPanelView

    User->>Workspace: Trigger flash (Cmd+Shift+H)
    Workspace->>Workspace: resolvedFocusFlashColor(customColorHex)
    alt Custom color set
        Workspace-->>Workspace: NSColor from hex
    else No custom color
        Workspace-->>Workspace: cmuxAccentNSColor()
    end
    
    Workspace->>Panel: triggerFlash(color: focusFlashColor)
    
    alt Terminal Panel
        Panel->>TerminalPanel: triggerFlash(color)
        TerminalPanel->>GhosttySurfaceScrollView: triggerFlash(color)
        GhosttySurfaceScrollView->>GhosttySurfaceScrollView: setFocusFlashColor(color)
        alt color is nil
            GhosttySurfaceScrollView->>GhosttySurfaceScrollView: resolvedWorkspaceFocusFlashColor()
        end
        GhosttySurfaceScrollView->>GhosttySurfaceScrollView: Update CALayer colors
    else Browser Panel
        Panel->>BrowserPanel: triggerFlash(color)
        BrowserPanel->>BrowserPanel: focusFlashColor = color ?? accent
        BrowserPanel->>BrowserPanelView: Updates @Published property
        BrowserPanelView->>BrowserPanelView: Render overlay with color
    end
Loading

Last reviewed commit: a64e828

@greptile-apps greptile-apps Bot left a comment

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.

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread Sources/ContentView.swift
.foregroundColor(activeSecondaryColor(0.8))
}


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.

Extra blank line

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 merged commit 4bfe95d into main Feb 28, 2026
9 of 11 checks passed
@lawrencecchen
lawrencecchen deleted the issue-557-notification-ring-color branch February 28, 2026 02:14
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…-ai#664)

- Notification/focus flash uses workspace customColor (fallback: accent)
- Selection bar/indicator uses workspace customColor when set
- Flash color propagated through Panel.triggerFlash(color:) API
- Browser panel flash overlay uses workspace color
- Regression tests for flash color resolution

Fixes manaflow-ai#557
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026

This branch was successfully deployed

1 active deployment
Preview — a64e8285 Deployed Feb 28, 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.

Feature: Per-notification ring color via CLI/socket

1 participant