Skip to content

Add Match Terminal Background sidebar setting - #2293

Merged
lawrencecchen merged 3 commits into
mainfrom
feat-sidebar-terminal-bg
Mar 28, 2026
Merged

lawrencecchen merged 3 commits into
mainfrom
feat-sidebar-terminal-bg

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds a "Match Terminal Background" toggle in Settings > Sidebar Appearance
  • When enabled, sidebar uses the same background color and transparency as the terminal
  • Uses layer-level opacity (1 - (1-alpha)² effective opacity) matching the terminal's two stacked semi-transparent layers (Bonsplit chrome + Ghostty Metal surface)
  • Adds 1px trailing border derived from terminal chrome color, matching bonsplit tab bar separator logic
  • Reacts to theme changes via .ghosttyDefaultBackgroundDidChange notification
  • Includes reset in resetAllSettings() and en/ja localization

Test plan

  • Enable "Match Terminal Background" in Settings > Sidebar Appearance
  • With background-opacity < 1.0, verify sidebar background matches terminal
  • Change Ghostty themes, verify sidebar updates live
  • Verify the trailing border matches tab bar borders
  • Reset all settings, verify toggle resets to off

Summary by cubic

Adds a "Match Terminal Background" sidebar setting that uses the terminal’s background color and effective opacity for a seamless look, plus a 1px trailing border that matches terminal chrome. Updates apply live on theme/background changes.

  • New Features

    • Sidebar renders with the terminal’s color and effective opacity using layer-level opacity (1 - (1 - alpha)^2).
    • Adds a 1px trailing border derived from terminal chrome, matching bonsplit tab bar separators.
    • Layout switches so the sidebar sits on the window background when matching is enabled.
    • Included in Reset All Settings; English and Japanese strings added, including the debug toggle label.
  • Bug Fixes

    • Fixed border color not updating until a theme change by recomputing on appear and on .ghosttyDefaultBackgroundDidChange.

Written for commit 315d670. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added a "Match Terminal Background" toggle in Sidebar Appearance (off by default). When enabled, the sidebar matches the terminal’s background, opacity, border, and layout for a more cohesive look. The setting is persisted and can be reset to default.
  • Localization

    • Added English and Japanese translations for the new setting title and subtitle.

Adds a toggle in Settings > Sidebar Appearance that makes the sidebar
use the same background color and transparency as the terminal area.

Uses layer-level opacity on a fully opaque background color (the same
technique as TitlebarLayerBackground) with effective opacity formula
`1 - (1-alpha)^2` to account for the terminal's two stacked
semi-transparent layers (Bonsplit chrome + Ghostty Metal surface).

Also adds a 1px trailing border derived from the terminal chrome color,
matching the bonsplit tab bar separator logic.
@vercel

vercel Bot commented Mar 28, 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 Mar 28, 2026 11:29am

@coderabbitai

coderabbitai Bot commented Mar 28, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2481c50-50fa-4010-9d94-0455e716f5f9

📥 Commits

Reviewing files that changed from the base of the PR and between c2ce66a and 315d670.

📒 Files selected for processing (2)
  • Sources/ContentView.swift
  • Sources/cmuxApp.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/ContentView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/cmuxApp.swift

📝 Walkthrough

Walkthrough

Adds a new "Match Terminal Background" persisted setting, localized strings, settings/debug toggles, conditional sidebar layout/backdrop rendering in ContentView, a native NSView-backed sidebar background component, and a 1px trailing sidebar border.

Changes

Cohort / File(s) Summary
Localization
Resources/Localizable.xcstrings
Added two keys: settings.sidebarAppearance.matchTerminalBackground and settings.sidebarAppearance.matchTerminalBackground.subtitle with English and Japanese values (manual extraction).
Sidebar UI & Rendering
Sources/ContentView.swift
Added @AppStorage sidebarMatchTerminalBackground; compute useWithinWindow to change layout path when enabled; introduced SidebarTerminalBackgroundView (NSViewRepresentable) for layer-backed opaque background with composited opacity; added SidebarTrailingBorder and integrated change notifications for background updates.
Settings & Debug UI
Sources/cmuxApp.swift
Exposed sidebarMatchTerminalBackground in SettingsView and SidebarDebugView toggles; added reset behavior in resetAllSettings() to clear the setting.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SettingsView
  participant AppStorage as Storage
  participant ContentView
  participant SidebarNSView as SidebarTerminalBackgroundView
  participant Ghostty as GhosttyApp.shared

  User->>SettingsView: Toggle "Match Terminal Background"
  SettingsView->>Storage: write sidebarMatchTerminalBackground = true
  Storage-->>ContentView: AppStorage binding updates
  ContentView->>Ghostty: read defaultBackgroundOpacity & color
  ContentView->>SidebarNSView: create/update with color + opacity
  SidebarNSView->>SidebarNSView: set CALayer backgroundColor & opacity
  ContentView->>ContentView: switch layout/backdrop path and render trailing border
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰
I found a toggle, soft and new,
Sidebar wears the terminal's hue,
A thin bright edge, an opaque glow,
I hop, I hum—designs align so,
Nibble a carrot, then I go!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a new sidebar setting for matching terminal background color and transparency.
Description check ✅ Passed The description includes a detailed summary and test plan with checkboxes, but lacks a demo video link and review trigger section as specified in 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 feat-sidebar-terminal-bg

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 Mar 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a "Match Terminal Background" toggle under Settings > Sidebar Appearance. When enabled, the sidebar uses the same background color and transparency as the Ghostty terminal by replicating its two-layer compositing model (1 - (1-α)² effective opacity), a 1 px trailing separator derived from the terminal chrome color, and live updates via .ghosttyDefaultBackgroundDidChange notifications.

Key changes:

  • SidebarBackdrop: correctly forks on matchTerminalBackground, uses SidebarTerminalBackgroundView (a CALayer-opacity-based NSViewRepresentable), and subscribes to theme-change notifications via @State + .onReceive to force re-renders.
  • SidebarTrailingBorder: adds the 1 px separator when the feature is on — but is missing the .ghosttyDefaultBackgroundDidChange subscription, so the border color will go stale after a Ghostty theme change while the backdrop correctly updates.
  • contentAndSidebarLayout: forces HStack mode when the feature is enabled to prevent intermediate compositing layers.
  • cmuxApp.swift: adds the UI toggle, localization wiring, and a resetAllSettings() reset — all correct.
  • Localizable.xcstrings: adds en + ja strings for the label and subtitle.

Confidence Score: 4/5

Safe to merge after fixing the stale border color on theme change in SidebarTrailingBorder.

One P1 defect: SidebarTrailingBorder lacks a .ghosttyDefaultBackgroundDidChange subscription, so the border color is not updated when the Ghostty theme changes, even though the PR description explicitly claims live theme reactivity. The backdrop itself is correctly wired. Everything else (settings UI, localization, reset, layout switch) looks correct.

Sources/ContentView.swift — specifically SidebarTrailingBorder around line 13771.

Important Files Changed

Filename Overview
Sources/ContentView.swift Adds SidebarTrailingBorder, SidebarTerminalBackgroundView, and updates SidebarBackdrop for the new "Match Terminal Background" mode. SidebarBackdrop correctly subscribes to theme-change notifications, but SidebarTrailingBorder.chromeSeparatorColor() lacks a notification subscription, leaving the border color stale after theme changes.
Sources/cmuxApp.swift Adds the toggle to SettingsView (Settings > Sidebar Appearance) with correct localization keys, and adds sidebarMatchTerminalBackground = false to resetAllSettings(). Also adds a debug toggle in SidebarDebugView. All additions look correct.
Resources/Localizable.xcstrings Adds settings.sidebarAppearance.matchTerminalBackground and its .subtitle key in both English and Japanese. Strings are accurate and well-placed alphabetically in the file.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SidebarBackdrop.body] --> B{matchTerminalBackground?}
    B -->|Yes| C[Compute effective opacity]
    C --> D[SidebarTerminalBackgroundView]
    D --> E[onReceive ghosttyDefaultBackgroundDidChange - triggers re-render OK]
    B -->|No| F[Existing blur / visual effect path]

    G[VerticalTabsSidebar] --> H[background SidebarBackdrop]
    G --> I[overlay SidebarTrailingBorder]
    I --> J{matchTerminalBackground?}
    J -->|Yes| K[Rectangle 1px - chromeSeparatorColor static call]
    K --> L[No notification subscription - border color goes STALE]
    J -->|No| M[EmptyView]

    N[Ghostty theme change] -->|ghosttyDefaultBackgroundDidChange| E
    N -.->|missing link| L
Loading

Reviews (1): Last reviewed commit: "Add "Match Terminal Background" sidebar ..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Comment on lines +13771 to +13784
/// 1px trailing border on the sidebar, derived from the terminal chrome background
/// using the same logic as bonsplit's TabBarColors.nsColorSeparator:
/// dark bg → lighten RGB by 0.16 at 0.36 alpha; light bg → darken by 0.12 at 0.26 alpha.
private struct SidebarTrailingBorder: View {
@AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false

var body: some View {
if matchTerminalBackground {
Rectangle()
.fill(Color(nsColor: Self.chromeSeparatorColor()))
.frame(width: 1)
.ignoresSafeArea()
}
}

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 SidebarTrailingBorder border color doesn't update on Ghostty theme change

chromeSeparatorColor() is a static call that reads GhosttyBackgroundTheme.currentColor() at render time, but SidebarTrailingBorder has only one reactive dependency: @AppStorage("sidebarMatchTerminalBackground"). SwiftUI will not re-render this view when the Ghostty theme changes, so the border color will become stale after a theme switch.

Compare with SidebarBackdrop: it has @State private var terminalBackgroundColor and an .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) that mutates that state to force a re-render. SidebarTrailingBorder has no equivalent mechanism.

The test plan item "Change Ghostty themes, verify sidebar updates live" would verify the backdrop updates (because SidebarBackdrop is correctly wired) but would miss the stale border.

The fix is the same pattern used in SidebarBackdrop:

private struct SidebarTrailingBorder: View {
    @AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false
    @State private var separatorColor: NSColor = chromeSeparatorColor()

    var body: some View {
        if matchTerminalBackground {
            Rectangle()
                .fill(Color(nsColor: separatorColor))
                .frame(width: 1)
                .ignoresSafeArea()
                .onReceive(
                    NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)
                ) { _ in
                    separatorColor = Self.chromeSeparatorColor()
                }
        }
    }
    ...
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already fixed in 315d670 - SidebarTrailingBorder already has @State separatorColor and .onReceive(.ghosttyDefaultBackgroundDidChange).

— Claude Code

Comment thread Sources/ContentView.swift Outdated
Comment on lines +13806 to +13820
/// fully opaque layer color + layer-level opacity. Also clears non-transparent
/// ancestor hosting view layers that SwiftUI may have added after the initial
/// makeViewHierarchyTransparent pass, preventing them from tinting the fill.
private struct SidebarTerminalBackgroundView: NSViewRepresentable {
let backgroundColor: NSColor
let opacity: CGFloat

func makeNSView(context: Context) -> NSView {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = backgroundColor.withAlphaComponent(1.0).cgColor
view.layer?.opacity = Float(opacity)
return view
}

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 Inaccurate doc comment on SidebarTerminalBackgroundView

The doc comment states: "Also clears non-transparent ancestor hosting view layers that SwiftUI may have added after the initial makeViewHierarchyTransparent pass, preventing them from tinting the fill."

However, the implementation is byte-for-byte identical to TitlebarLayerBackground (lines 309–324) and performs no such ancestor-layer clearing — it only sets layer.backgroundColor and layer.opacity on the view's own layer. Either the described clearing logic is missing from this implementation, or the comment was copy-pasted and describes intended (but unimplemented) behavior. The comment should be corrected to avoid misleading future maintainers.

Additionally, since SidebarTerminalBackgroundView and TitlebarLayerBackground are identical, consider consolidating them into a single shared private struct (e.g., LayerBackgroundView) to reduce duplication.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The doc comment is accurate for the current implementation. It describes the layer color + opacity technique, which matches what the code does.

— Claude Code

@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: 58bd0ba247

ℹ️ 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/cmuxApp.swift Outdated
Text("Sidebar Appearance")
.font(.headline)

Toggle("Match Terminal Background", isOn: $matchTerminalBackground)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Localize the new sidebar debug toggle label

SidebarDebugView introduces Toggle("Match Terminal Background", ...) as a hardcoded English string, which violates the localization rule in /workspace/cmux/AGENTS.md (all user-facing strings must use localization keys). In non-English locales (for example Japanese), this control will remain untranslated and creates an immediate localization regression in the settings UI.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already fixed in 315d670 - the debug toggle uses String(localized:) with a proper key.

— Claude Code

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/Localizable.xcstrings`:
- Around line 83201-83234: Add the missing locale entries for both
"settings.sidebarAppearance.matchTerminalBackground" and
"settings.sidebarAppearance.matchTerminalBackground.subtitle" to match the
project's existing pattern: include the full set of locales (ar, bs, da, de, en,
es, fr, it, ja, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant), copying
the English "value" for any locales that should use an English fallback and mark
their "stringUnit.state" as "translated"; ensure existing Japanese entries
remain unchanged and all new locale blocks follow the same structure as other
settings.sidebarAppearance.* keys in the catalog.

In `@Sources/cmuxApp.swift`:
- Line 2923: Replace the bare string in the SwiftUI Toggle with a localized
string: instead of Toggle("Match Terminal Background", isOn:
$matchTerminalBackground) use String(localized:
"settings.matchTerminalBackground", defaultValue: "Match Terminal Background")
for the label; update the Toggle call that references matchTerminalBackground
accordingly and add the corresponding key ("settings.matchTerminalBackground")
to your localization resources. Ensure you modify the Toggle invocation in
cmuxApp.swift where matchTerminalBackground is used so all user-facing text
follows the String(localized:..., defaultValue:...) pattern.

In `@Sources/ContentView.swift`:
- Around line 13775-13803: The separator's color isn't refreshed when Ghostty
theme changes because the Rectangle only depends on `@AppStorage`
matchTerminalBackground; add a listener for .ghosttyDefaultBackgroundDidChange
(the same notification used by the sibling backdrop) and force the separator to
recompute by updating a small `@State` trigger that's referenced by the Rectangle
(or by changing the view's id) so chromeSeparatorColor() is re-evaluated when
the notification fires; update the body around matchTerminalBackground/Rectangle
to include an onReceive(NotificationCenter.default.publisher(for:
.ghosttyDefaultBackgroundDidChange)) { ... } that flips the state trigger.
- Around line 2605-2609: The toggle that flips layout (computed as
useWithinWindow using sidebarBlendMode and sidebarMatchTerminalBackground) can
change portal-hosted surface frames without triggering a geometry resync; after
computing useWithinWindow and before switching into the HStack/ZStack branches,
call the same portal geometry resync hook used for sidebar-width changes (the
explicit resync invoked elsewhere for width updates) so portal frames are
updated immediately when sidebarBlendMode or sidebarMatchTerminalBackground
changes; locate the resync call used for width changes and invoke it here
(adjacent to useWithinWindow) to schedule a portal geometry resync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50bc75e0-cc6c-4e48-8376-3d8ee87e4b0d

📥 Commits

Reviewing files that changed from the base of the PR and between e9afc22 and 58bd0ba.

📒 Files selected for processing (3)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/cmuxApp.swift

Comment thread Resources/Localizable.xcstrings
Comment thread Sources/cmuxApp.swift Outdated
Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:13774">
P2: Make the trailing border observe Ghostty background changes so its color updates immediately with theme switches.</violation>

<violation number="2" location="Sources/ContentView.swift:13808">
P3: The doc comment says this view clears non-transparent ancestor hosting layers, but the implementation only updates its own layer color/opacity. Update the comment or implement the missing ancestor-clearing behavior so the docs match runtime behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift Outdated
Add @State + .onReceive(.ghosttyDefaultBackgroundDidChange) to
SidebarTrailingBorder so the separator color recomputes when the
Ghostty theme changes, matching the pattern used in SidebarBackdrop.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Fixed the stale border color issue flagged by Greptile in c2ce66a. Added @State separatorColor + .onReceive(.ghosttyDefaultBackgroundDidChange) to SidebarTrailingBorder, matching the pattern in SidebarBackdrop.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:13784">
P2: Refresh `separatorColor` when the border view appears; otherwise toggling “Match Terminal Background” on after a theme change can show an out-of-date border color.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/ContentView.swift
- Localize the debug panel toggle label (Codex P1)
- Add .onAppear to SidebarTrailingBorder for initial color (Cubic P2)
- Fix stale doc comment on SidebarTerminalBackgroundView (Cubic P3)
@lawrencecchen
lawrencecchen merged commit 550d98c into main Mar 28, 2026
14 checks passed
@lawrencecchen
lawrencecchen deleted the feat-sidebar-terminal-bg branch March 28, 2026 11:36
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Add "Match Terminal Background" sidebar setting

Adds a toggle in Settings > Sidebar Appearance that makes the sidebar
use the same background color and transparency as the terminal area.

Uses layer-level opacity on a fully opaque background color (the same
technique as TitlebarLayerBackground) with effective opacity formula
`1 - (1-alpha)^2` to account for the terminal's two stacked
semi-transparent layers (Bonsplit chrome + Ghostty Metal surface).

Also adds a 1px trailing border derived from the terminal chrome color,
matching the bonsplit tab bar separator logic.

* Fix sidebar border color not updating on theme change

Add @State + .onReceive(.ghosttyDefaultBackgroundDidChange) to
SidebarTrailingBorder so the separator color recomputes when the
Ghostty theme changes, matching the pattern used in SidebarBackdrop.

* Address review comments: localize debug toggle, fix separator refresh

- Localize the debug panel toggle label (Codex P1)
- Add .onAppear to SidebarTrailingBorder for initial color (Cubic P2)
- Fix stale doc comment on SidebarTerminalBackgroundView (Cubic P3)

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
@coderabbitai coderabbitai Bot mentioned this pull request Apr 6, 2026
9 tasks

This branch was successfully deployed

1 active deployment
Preview — 315d6703 Deployed Mar 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.

1 participant