Skip to content

Add configurable sidebar tint color with light/dark mode support#1465

Merged
lawrencecchen merged 1 commit intomanaflow-ai:mainfrom
rrva:sidebar-background-config
Mar 15, 2026
Merged

Add configurable sidebar tint color with light/dark mode support#1465
lawrencecchen merged 1 commit intomanaflow-ai:mainfrom
rrva:sidebar-background-config

Conversation

@rrva
Copy link
Copy Markdown
Contributor

@rrva rrva commented Mar 15, 2026

Summary

  • Adds sidebar-background and sidebar-tint-opacity config keys to the Ghostty config parser
  • Supports light:/dark: prefix syntax (e.g. sidebar-background = light:#ece3cc,dark:#184956)
  • Adds a Sidebar Appearance section to Settings with separate light/dark mode color pickers, opacity slider, and reset button
  • SidebarBackdrop automatically switches tint color when macOS appearance changes

Closes #1462

Config file usage

Settings go in the cmux-specific config (~/Library/Application Support/com.cmuxterm.app/config):

sidebar-background = light:#ece3cc,dark:#184956
sidebar-tint-opacity = 0.85

Or a single color for both modes:

sidebar-background = #103c48

Changes

File What
Sources/GhosttyConfig.swift Parse sidebar-background (with light:/dark: syntax) and sidebar-tint-opacity, sync to UserDefaults
Sources/ContentView.swift SidebarBackdrop observes colorScheme and resolves light/dark-specific tint hex
Sources/cmuxApp.swift New "Sidebar Appearance" section in SettingsView with light/dark color pickers, opacity slider, reset

Testing

  • Tested locally with 27 automated assertions (config parsing, stale key cleanup, dark mode toggle stability, opacity clamping, preset/reset flows)
  • Manual verification of Settings UI color pickers, dark/light mode transition, config reload behavior

Demo Video

Screen.Recording.2026-03-15.at.15.42.19.mov

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by CodeRabbit

  • New Features
    • Separate Light/Dark sidebar tint controls with color pickers and live preview; persisted per-mode settings and reset/preset behavior.
    • Configurable sidebar background (base/light/dark) and tint opacity via config, synced to app settings and persisted.
  • Documentation
    • Added localized "Sidebar Appearance" strings for settings UI.
  • Tests
    • Added tests for sidebar background/tint parsing, resolution, persistence, and opacity clamping.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 15, 2026

@rrva is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Adds per-appearance sidebar tint/background support: new AppStorage keys for light/dark tints and opacity, Settings UI color pickers and reset, GhosttyConfig parsing of sidebar-background and sidebar-tint-opacity with resolution and persistence, and color-scheme-aware tint resolution in the sidebar view.

Changes

Cohort / File(s) Summary
Sidebar View
Sources/ContentView.swift
Introduces @AppStorage keys sidebarTintHexLight, sidebarTintHexDark, sidebarTintHex, and sidebarTintOpacity; uses @Environment(\.colorScheme) to pick resolved hex (light → dark → fallback) and computes an NSColor preserving alpha; adds SidebarTintDefaults.
Configuration Model
Sources/GhosttyConfig.swift
Adds rawSidebarBackground, sidebarBackground, sidebarBackgroundLight, sidebarBackgroundDark, sidebarTintOpacity; adds resolveSidebarBackground(preferredColorScheme:) to parse single or light:...,dark:... syntax and applySidebarAppearanceToUserDefaults() to persist per-variant hex and opacity; invoked during loadFromDisk.
App State & Settings UI
Sources/cmuxApp.swift
Adds per-scheme @AppStorage keys and bindings (sidebarTintHexLight, sidebarTintHexDark, sidebarTintOpacity), exposes Light/Dark tint controls and opacity slider in Settings, updates reset/preset flows and debug payloads to include/clear per-scheme tint overrides.
Localization Resources
Resources/Localizable.xcstrings
Adds a new settings.section.sidebarAppearance group with localized strings for the Sidebar Appearance settings (labels, subtitles, descriptions for tint controls and reset).
Tests
cmuxTests/GhosttyConfigTests.swift
Adds tests covering parsing single and dual-mode sidebar-background, opacity clamping, resolution to light/dark variants, persistence to UserDefaults, and clearing stale keys; test class appears duplicated in the file.
Changelog / Docs
CHANGELOG.md
Adds release note describing configurable sidebar tint/background with light/dark support and new config keys sidebar-background and sidebar-tint-opacity.

Sequence Diagram

sequenceDiagram
    %% Components: User, Settings UI, AppStorage/UserDefaults, GhosttyConfig, Sidebar
    participant User as "User"
    participant Settings as "Settings UI"
    participant Storage as "AppStorage / UserDefaults"
    participant Config as "GhosttyConfig"
    participant Sidebar as "Sidebar (ContentView)"

    User->>Settings: choose Light/Dark tint & opacity
    Settings->>Storage: write `sidebarTintHexLight` / `sidebarTintHexDark` / `sidebarTintOpacity`
    Settings-->>User: show preview

    Config->>Config: loadFromDisk() / parse `sidebar-background`
    Config->>Config: resolveSidebarBackground(preferredColorScheme)
    Config->>Storage: applySidebarAppearanceToUserDefaults()

    Sidebar->>Storage: read tint keys + opacity
    Sidebar->>Sidebar: pick resolvedHex (scheme-specific → fallback)
    Sidebar->>Sidebar: compute NSColor (preserve alpha) and apply
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"I nibble hex codes in the burrow bright,
Light and dark I toggle through the night,
From config root to sidebar bloom,
I hop and paint the color room,
Tiny rabbit cheers — the theme feels right!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature added: configurable sidebar tint color with light/dark mode support.
Linked Issues check ✅ Passed The PR fulfills all coding requirements from #1462: config parsing (sidebar-background, sidebar-tint-opacity), Settings UI with light/dark color pickers and opacity slider, automatic theme switching, and UserDefaults persistence.
Out of Scope Changes check ✅ Passed All changes directly support the linked issue: GhosttyConfig parsing, ContentView sidebar tint resolution, cmuxApp Settings UI, localization strings, tests, and changelog update are all in scope.
Description check ✅ Passed The pull request description is comprehensive and follows the template structure with all major sections properly filled out including Summary, Testing, Demo Video, and Checklist.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue 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/GhosttyConfig.swift">

<violation number="1" location="Sources/GhosttyConfig.swift:169">
P2: Clear stale `sidebarTintHexLight`/`sidebarTintHexDark` values when no dual-mode color is resolved; otherwise old mode-specific values override new single-color config.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
Sources/cmuxApp.swift (1)

4037-4040: Avoid hardcoded sidebar tint defaults in reset action.

Line 4039 and Line 4040 duplicate default values that already exist in sidebar preset/default logic. Centralizing avoids drift.

♻️ Suggested refactor
-                                sidebarTintHex = "#000000"
-                                sidebarTintOpacity = 0.18
+                                let presetDefaults = SidebarPresetOption.nativeSidebar
+                                sidebarTintHex = presetDefaults.tintHex
+                                sidebarTintOpacity = presetDefaults.tintOpacity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 4037 - 4040, The reset action is
hardcoding sidebarTintHex and sidebarTintOpacity which duplicates central
preset/default logic; instead, remove the explicit assignments to sidebarTintHex
= "#000000" and sidebarTintOpacity = 0.18 and only clear per-variant overrides
(set sidebarTintHexLight and sidebarTintHexDark to nil) so the existing
centralized sidebar preset/default logic can supply the canonical defaults, or
if there is an existing helper (e.g. applySidebarPresetDefaults() or similar),
call that helper from the reset action rather than assigning literal values.
🤖 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/cmuxApp.swift`:
- Around line 3111-3114: The resetAllSettings() routine currently doesn't clear
the new sidebar tint AppStorage keys (sidebarTintHex, sidebarTintHexLight,
sidebarTintHexDark, sidebarTintOpacity); update resetAllSettings() to explicitly
reset these persisted settings by setting sidebarTintHex back to the default
("#000000"), sidebarTintHexLight and sidebarTintHexDark to nil (or their
defaults), and sidebarTintOpacity back to 0.18 (or its default), or remove those
keys from UserDefaults within that function so the Reset All Settings flow fully
restores the global defaults for the sidebar tint customization.

In `@Sources/ContentView.swift`:
- Around line 12577-12585: The per-theme override logic (resolvedHex) can return
an invalid hex and then NSColor(hex:) falls back to .black; change the resolver
so that when choosing sidebarTintHexLight or sidebarTintHexDark you first
validate that NSColor(hex: candidate) != nil (and non-empty) and only return the
candidate if valid, otherwise fall back to the shared sidebarTintHex; keep the
final tintColor construction using (NSColor(hex: resolvedHex) ??
.black).withAlphaComponent(sidebarTintOpacity) so opacity behavior is unchanged.

In `@Sources/GhosttyConfig.swift`:
- Around line 297-300: The case handling "sidebar-tint-opacity" currently
assigns any parsed Double to sidebarTintOpacity; clamp the parsed value to the
valid range 0.0...1.0 before assigning (or reject/ignore values outside that
range). Locate the switch case for "sidebar-tint-opacity" and replace the direct
assignment in that branch with a clamp operation (e.g., let clamped =
min(max(opacity, 0.0), 1.0); sidebarTintOpacity = clamped) so negative or >1
values cannot be persisted/used.
- Around line 167-184: When syncing sidebar appearance in
applySidebarAppearanceToUserDefaults(), ensure stale override keys are removed
to avoid old values winning: when setting a single sidebarBackground (the color)
call UserDefaults.standard.removeObject(forKey:) for "sidebarTintHexLight" and
"sidebarTintHexDark" so any previous light/dark overrides are cleared;
conversely, when setting sidebarBackgroundLight or sidebarBackgroundDark, remove
the single-key "sidebarTintHex" to prevent it from overriding the per-theme
values; keep the existing opacity writes as-is.

---

Nitpick comments:
In `@Sources/cmuxApp.swift`:
- Around line 4037-4040: The reset action is hardcoding sidebarTintHex and
sidebarTintOpacity which duplicates central preset/default logic; instead,
remove the explicit assignments to sidebarTintHex = "#000000" and
sidebarTintOpacity = 0.18 and only clear per-variant overrides (set
sidebarTintHexLight and sidebarTintHexDark to nil) so the existing centralized
sidebar preset/default logic can supply the canonical defaults, or if there is
an existing helper (e.g. applySidebarPresetDefaults() or similar), call that
helper from the reset action rather than assigning literal values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 450a024d-2305-4c91-b040-061fc5f87ee6

📥 Commits

Reviewing files that changed from the base of the PR and between 9bb2816 and 4f18d68.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/cmuxApp.swift

@rrva rrva force-pushed the sidebar-background-config branch 2 times, most recently from e89e307 to 5df5e35 Compare March 15, 2026 13:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
Sources/cmuxApp.swift (1)

3122-3125: ⚠️ Potential issue | 🟡 Minor

Reset All Settings still misses these new persisted keys.

resetAllSettings() around Line 4570 doesn't restore sidebarTintHex, sidebarTintHexLight, sidebarTintHexDark, or sidebarTintOpacity, so the global reset flow can leave sidebar tint customization behind.

🛠️ Proposed fix (outside this hunk)
         sidebarShowPorts = true
         sidebarShowLog = true
         sidebarShowProgress = true
         sidebarShowMetadata = true
+        sidebarTintHex = "#000000"
+        sidebarTintHexLight = nil
+        sidebarTintHexDark = nil
+        sidebarTintOpacity = 0.18
         showOpenAccessConfirmation = false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 3122 - 3125, resetAllSettings() does not
clear the new AppStorage keys for sidebar tinting, leaving user-customized
sidebar colors after a global reset; update resetAllSettings() to explicitly
restore sidebarTintHex, sidebarTintHexLight, sidebarTintHexDark, and
sidebarTintOpacity to their default values (e.g., "#000000" for sidebarTintHex,
nil for the Light/Dark variants, and 0.18 for sidebarTintOpacity) by setting
those AppStorage-backed properties inside resetAllSettings() so the reset flow
removes any custom sidebar tinting.
Sources/GhosttyConfig.swift (1)

313-316: ⚠️ Potential issue | 🟡 Minor

Clamp sidebar-tint-opacity before storing it.

Line 314 still accepts any Double; negative or > 1 values can leak into persisted alpha and downstream UI state.

🛠️ Proposed fix
                 case "sidebar-tint-opacity":
                     if let opacity = Double(value) {
-                        sidebarTintOpacity = opacity
+                        sidebarTintOpacity = min(1.0, max(0.0, opacity))
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyConfig.swift` around lines 313 - 316, The
"sidebar-tint-opacity" branch in GhosttyConfig.swift currently assigns any
Double to sidebarTintOpacity; clamp the parsed opacity to the 0...1 range before
storing to prevent negative or >1 values from persisting or affecting UI
state—locate the case "sidebar-tint-opacity" and replace the direct assignment
(where opacity is parsed) with a clamped value (e.g., using min/max or a range
clamp) and then assign sidebarTintOpacity.
🤖 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/cmuxApp.swift`:
- Around line 4027-4037: The Slider in SettingsCardRow (Slider(value:
$sidebarTintOpacity, in: 0...0.7)) is capped at 0.7 while GhosttyConfig.parse
accepts higher values (e.g., 0.85), causing a mismatch; fix by making the ranges
consistent: either increase the Slider max to match the config parser's allowed
max (e.g., 0...1 or the specific max used by GhosttyConfig.parse) so
$sidebarTintOpacity can reach values parsed from config, or clamp/validate the
parsed value inside GhosttyConfig.parse to a 0.7 ceiling and update docs—adjust
the Slider range or the parsing/clamping logic accordingly and ensure both
SettingsCardRow/$sidebarTintOpacity and GhosttyConfig.parse use the same max.

---

Duplicate comments:
In `@Sources/cmuxApp.swift`:
- Around line 3122-3125: resetAllSettings() does not clear the new AppStorage
keys for sidebar tinting, leaving user-customized sidebar colors after a global
reset; update resetAllSettings() to explicitly restore sidebarTintHex,
sidebarTintHexLight, sidebarTintHexDark, and sidebarTintOpacity to their default
values (e.g., "#000000" for sidebarTintHex, nil for the Light/Dark variants, and
0.18 for sidebarTintOpacity) by setting those AppStorage-backed properties
inside resetAllSettings() so the reset flow removes any custom sidebar tinting.

In `@Sources/GhosttyConfig.swift`:
- Around line 313-316: The "sidebar-tint-opacity" branch in GhosttyConfig.swift
currently assigns any Double to sidebarTintOpacity; clamp the parsed opacity to
the 0...1 range before storing to prevent negative or >1 values from persisting
or affecting UI state—locate the case "sidebar-tint-opacity" and replace the
direct assignment (where opacity is parsed) with a clamped value (e.g., using
min/max or a range clamp) and then assign sidebarTintOpacity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9be67cf4-b6ca-4c80-80bb-86d997292144

📥 Commits

Reviewing files that changed from the base of the PR and between 4f18d68 and e89e307.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
Sources/cmuxApp.swift (1)

4032-4033: ⚠️ Potential issue | 🟠 Major

Increase the tint opacity slider max (or clamp parser/docs to 0.7).

At Line 4032, the UI caps at 0.7, but this feature accepts higher opacity values (including the PR’s own 0.85 example), so loaded config can’t be edited faithfully in Settings.

🛠️ Suggested fix
-                                Slider(value: $sidebarTintOpacity, in: 0...0.7)
+                                Slider(value: $sidebarTintOpacity, in: 0...1.0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 4032 - 4033, The Slider for
sidebarTintOpacity in cmuxApp.swift is capped at 0.7 but configs (and the PR
example 0.85) allow higher values; update the Slider range to allow the full
accepted opacity (e.g., change the range from 0...0.7 to 0...1 or to a shared
max constant like maxSidebarTintOpacity) so loaded values can be edited
faithfully, and ensure any config parsing/clamping uses the same max constant
(e.g., references to sidebarTintOpacity, Slider(value: $sidebarTintOpacity, in:
...), and any loader/clamp logic) so UI and parsing remain consistent.
🤖 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/cmuxApp.swift`:
- Around line 3981-4057: The referenced localization keys used in
SettingsSectionHeader and SettingsCardRow (settings.section.sidebarAppearance,
settings.sidebarAppearance.tintColorLight,
settings.sidebarAppearance.tintColorLight.subtitle,
settings.sidebarAppearance.tintColorLight.picker,
settings.sidebarAppearance.tintColorDark,
settings.sidebarAppearance.tintColorDark.subtitle,
settings.sidebarAppearance.tintColorDark.picker,
settings.sidebarAppearance.tintOpacity,
settings.sidebarAppearance.tintOpacity.subtitle,
settings.sidebarAppearance.reset, settings.sidebarAppearance.reset.subtitle,
settings.sidebarAppearance.reset.button) are missing from the strings catalog;
add each key to the Localizable.xcstrings resource with proper English and
Japanese translations, matching the defaultValue text used in the code as the
English entries and appropriate Japanese equivalents, then save and re-run your
localization validation/build to ensure SwiftUI picks up the new entries.

In `@Sources/GhosttyConfig.swift`:
- Around line 32-35: The file's top comment about sidebar behavior contradicts
the implemented logic: code only writes sidebar tint values when the
"sidebar-background" config key exists and always writes sidebarTintOpacity (it
does not clear tint hex keys when "sidebar-background" is absent). Update the
documentation/comments around the sidebar settings (the block describing
"sidebar-background" and the nearby comments around the code that writes
sidebarTintOpacity and tint hex keys) to state that writes occur only if
"sidebar-background" exists, that sidebarTintOpacity is always written when
present, and that tint hex keys are not cleared when "sidebar-background" is
absent (so the UI controls remain authoritative). Ensure mentions of
"sidebar-background", "sidebarTintOpacity" and the sidebar tint hex keys match
the runtime behavior.

---

Duplicate comments:
In `@Sources/cmuxApp.swift`:
- Around line 4032-4033: The Slider for sidebarTintOpacity in cmuxApp.swift is
capped at 0.7 but configs (and the PR example 0.85) allow higher values; update
the Slider range to allow the full accepted opacity (e.g., change the range from
0...0.7 to 0...1 or to a shared max constant like maxSidebarTintOpacity) so
loaded values can be edited faithfully, and ensure any config parsing/clamping
uses the same max constant (e.g., references to sidebarTintOpacity,
Slider(value: $sidebarTintOpacity, in: ...), and any loader/clamp logic) so UI
and parsing remain consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8f532e4-8789-46b5-81b4-22e0cc6fba20

📥 Commits

Reviewing files that changed from the base of the PR and between e89e307 and 5df5e35.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/cmuxApp.swift

@rrva rrva force-pushed the sidebar-background-config branch 2 times, most recently from fe74d4e to be4db6c Compare March 15, 2026 13:34
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/cmuxApp.swift (1)

4031-4034: ⚠️ Potential issue | 🟠 Major

Opacity slider range is inconsistent with parser-accepted values.

Line 4031 caps UI input at 0...0.7, but GhosttyConfig accepts/clamps sidebar-tint-opacity to 0...1. Values like 0.85 can load from config but cannot be edited faithfully in Settings.

🛠️ Suggested fix
-                                Slider(value: $sidebarTintOpacity, in: 0...0.7)
+                                Slider(value: $sidebarTintOpacity, in: 0...1.0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 4031 - 4034, The UI slider for sidebar
opacity limits sidebarTintOpacity to 0...0.7 while GhosttyConfig accepts 0...1,
causing loaded values like 0.85 to be uneditable; update the Slider range bound
in the Settings UI (the Slider using the sidebarTintOpacity binding) to use
0...1 so the UI can represent and edit the full parser-accepted range for
sidebar-tint-opacity and keep the existing percentage Text formatting unchanged.
🤖 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/GhosttyConfig.swift`:
- Around line 182-185: The current code in the sidebarBackground handling
unconditionally writes a default opacity (0.7) when sidebarTintOpacity is nil,
which unintentionally overwrites stored settings; change the logic in the block
that handles sidebarBackground (referencing sidebarBackground,
sidebarTintOpacity and the defaults.set calls for "sidebarTintHex" and
"sidebarTintOpacity") so that you always set the hex value, but only call
defaults.set(..., forKey: "sidebarTintOpacity") when sidebarTintOpacity is
non-nil (e.g., if let opacity = sidebarTintOpacity { defaults.set(opacity,
forKey: "sidebarTintOpacity") }) so the existing stored/AppStorage value is not
clobbered when the config omits sidebar-tint-opacity.

---

Duplicate comments:
In `@Sources/cmuxApp.swift`:
- Around line 4031-4034: The UI slider for sidebar opacity limits
sidebarTintOpacity to 0...0.7 while GhosttyConfig accepts 0...1, causing loaded
values like 0.85 to be uneditable; update the Slider range bound in the Settings
UI (the Slider using the sidebarTintOpacity binding) to use 0...1 so the UI can
represent and edit the full parser-accepted range for sidebar-tint-opacity and
keep the existing percentage Text formatting unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6954ca0b-e01c-4f6e-a6d8-f7366975621e

📥 Commits

Reviewing files that changed from the base of the PR and between 5df5e35 and fe74d4e.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/cmuxApp.swift

@rrva rrva force-pushed the sidebar-background-config branch from be4db6c to 93d584a Compare March 15, 2026 13:37
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
Sources/cmuxApp.swift (2)

3121-3124: Centralize sidebar tint defaults to prevent drift.

"#000000" and 0.18 are repeated across AppStorage initialization and reset flows. A shared default constant would prevent accidental mismatch (especially since config sync logic currently uses a different fallback opacity path in GhosttyConfig).

♻️ Suggested refactor
+private enum SidebarTintDefaults {
+    static let hex = "#000000"
+    static let opacity = 0.18
+}
...
-    `@AppStorage`("sidebarTintHex") private var sidebarTintHex = "#000000"
+    `@AppStorage`("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex
...
-    `@AppStorage`("sidebarTintOpacity") private var sidebarTintOpacity = 0.18
+    `@AppStorage`("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
...
-                                sidebarTintHex = "#000000"
-                                sidebarTintOpacity = 0.18
+                                sidebarTintHex = SidebarTintDefaults.hex
+                                sidebarTintOpacity = SidebarTintDefaults.opacity
...
-        sidebarTintHex = "#000000"
-        sidebarTintOpacity = 0.18
+        sidebarTintHex = SidebarTintDefaults.hex
+        sidebarTintOpacity = SidebarTintDefaults.opacity

Also applies to: 4050-4051, 4621-4624

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

In `@Sources/cmuxApp.swift` around lines 3121 - 3124, The sidebar tint default
values are duplicated; define shared constants (e.g., SIDEBAR_TINT_HEX_DEFAULT
and SIDEBAR_TINT_OPACITY_DEFAULT) and replace the literal "#000000" and 0.18 in
the AppStorage declarations (sidebarTintHex, sidebarTintHexLight,
sidebarTintHexDark, sidebarTintOpacity) and in any reset/config-sync code paths
(including GhosttyConfig fallback) to use these constants so defaults stay
centralized and consistent across initialization and reset flows.

2427-2428: Prefer clearing @AppStorage bindings directly instead of raw UserDefaults calls.

Using the wrapped properties avoids key string duplication and keeps this path consistent with the rest of the view state writes.

♻️ Suggested refactor
-        UserDefaults.standard.removeObject(forKey: "sidebarTintHexLight")
-        UserDefaults.standard.removeObject(forKey: "sidebarTintHexDark")
+        sidebarTintHexLight = nil
+        sidebarTintHexDark = nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 2427 - 2428, Replace the raw
UserDefaults.removeObject calls with assignments to the corresponding
`@AppStorage-wrapped` properties so key strings aren't duplicated; instead of
UserDefaults.standard.removeObject(forKey: "sidebarTintHexLight") and
...("sidebarTintHexDark"), set the `@AppStorage` properties sidebarTintHexLight =
nil and sidebarTintHexDark = nil (or to their empty/default values if
non-optional) in the same function/block where those removes appear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/cmuxApp.swift`:
- Around line 3121-3124: The sidebar tint default values are duplicated; define
shared constants (e.g., SIDEBAR_TINT_HEX_DEFAULT and
SIDEBAR_TINT_OPACITY_DEFAULT) and replace the literal "#000000" and 0.18 in the
AppStorage declarations (sidebarTintHex, sidebarTintHexLight,
sidebarTintHexDark, sidebarTintOpacity) and in any reset/config-sync code paths
(including GhosttyConfig fallback) to use these constants so defaults stay
centralized and consistent across initialization and reset flows.
- Around line 2427-2428: Replace the raw UserDefaults.removeObject calls with
assignments to the corresponding `@AppStorage-wrapped` properties so key strings
aren't duplicated; instead of UserDefaults.standard.removeObject(forKey:
"sidebarTintHexLight") and ...("sidebarTintHexDark"), set the `@AppStorage`
properties sidebarTintHexLight = nil and sidebarTintHexDark = nil (or to their
empty/default values if non-optional) in the same function/block where those
removes appear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9e76d77-e11f-4ce4-b105-52dbca645936

📥 Commits

Reviewing files that changed from the base of the PR and between fe74d4e and be4db6c.

📒 Files selected for processing (4)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/cmuxApp.swift
✅ Files skipped from review due to trivial changes (1)
  • Resources/Localizable.xcstrings

@rrva rrva force-pushed the sidebar-background-config branch 3 times, most recently from 7307fd5 to e5baef7 Compare March 15, 2026 14:08
@rrva
Copy link
Copy Markdown
Contributor Author

rrva commented Mar 15, 2026

Re: review 3950173460 — both nitpicks addressed:

  1. Added SidebarTintDefaults enum with .hex and .opacity constants, used across all @AppStorage declarations, reset buttons, and resetAllSettings().
  2. applyPreset() now uses sidebarTintHexLight = nil / sidebarTintHexDark = nil instead of UserDefaults.standard.removeObject.

@rrva
Copy link
Copy Markdown
Contributor Author

rrva commented Mar 15, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rrva
Copy link
Copy Markdown
Contributor Author

rrva commented Mar 15, 2026

Demo Video

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
cmuxTests/GhosttyConfigTests.swift (1)

1680-1736: Consider centralizing sidebar defaults keys in test-local constants.

The same key literals are repeated across tests; extracting constants will reduce drift/typo risk.

♻️ Optional cleanup
+    private enum SidebarDefaultsKey {
+        static let hex = "sidebarTintHex"
+        static let hexLight = "sidebarTintHexLight"
+        static let hexDark = "sidebarTintHexDark"
+        static let opacity = "sidebarTintOpacity"
+    }
-        let testKey = "sidebarTintHex"
+        let testKey = SidebarDefaultsKey.hex
-        let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark"]
+        let keys = [SidebarDefaultsKey.hex, SidebarDefaultsKey.hexLight, SidebarDefaultsKey.hexDark]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/GhosttyConfigTests.swift` around lines 1680 - 1736, Extract
repeated UserDefaults key strings ("sidebarTintHex", "sidebarTintHexLight",
"sidebarTintHexDark") into test-local constants (e.g., let kSidebarTint =
"sidebarTintHex", kSidebarTintLight = "sidebarTintHexLight", kSidebarTintDark =
"sidebarTintHexDark") and use those constants in the tests
testApplyToUserDefaultsWritesHexWhenConfigSet,
testApplyToUserDefaultsClearsStaleKeysOnSwitchFromDualToSingle and the earlier
test that uses testKey; update the keys array, defaults.set/get calls, and
restore loop to reference the constants so the literals are defined once and
reduce duplication/typo risk.
Sources/cmuxApp.swift (1)

3199-3221: Consider deduplicating the light/dark tint Binding implementations.

These two bindings are structurally identical except for the target override key; a tiny helper would reduce repetition.

♻️ Proposed refactor
-    private var settingsSidebarTintLightBinding: Binding<Color> {
-        Binding(
-            get: {
-                Color(nsColor: NSColor(hex: sidebarTintHexLight ?? sidebarTintHex) ?? .black)
-            },
-            set: { newColor in
-                let nsColor = NSColor(newColor)
-                sidebarTintHexLight = nsColor.hexString()
-            }
-        )
-    }
-
-    private var settingsSidebarTintDarkBinding: Binding<Color> {
-        Binding(
-            get: {
-                Color(nsColor: NSColor(hex: sidebarTintHexDark ?? sidebarTintHex) ?? .black)
-            },
-            set: { newColor in
-                let nsColor = NSColor(newColor)
-                sidebarTintHexDark = nsColor.hexString()
-            }
-        )
-    }
+    private func settingsSidebarTintBinding(_ overrideHex: Binding<String?>) -> Binding<Color> {
+        Binding(
+            get: {
+                Color(nsColor: NSColor(hex: overrideHex.wrappedValue ?? sidebarTintHex) ?? .black)
+            },
+            set: { newColor in
+                overrideHex.wrappedValue = NSColor(newColor).hexString()
+            }
+        )
+    }
+
+    private var settingsSidebarTintLightBinding: Binding<Color> {
+        settingsSidebarTintBinding($sidebarTintHexLight)
+    }
+
+    private var settingsSidebarTintDarkBinding: Binding<Color> {
+        settingsSidebarTintBinding($sidebarTintHexDark)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 3199 - 3221, The two bindings
settingsSidebarTintLightBinding and settingsSidebarTintDarkBinding are identical
except for which stored-hex they read/write (sidebarTintHexLight vs
sidebarTintHexDark), so extract a small helper that builds a Binding<Color> from
two closures (one to get the optional override hex and one to set it). Implement
e.g. a private func makeSidebarTintBinding(getOverride: `@escaping` () -> String?,
setOverride: `@escaping` (String?) -> Void) -> Binding<Color> that performs the
Color(nsColor: NSColor(hex: override ?? sidebarTintHex) ?? .black) get and
converts Color -> NSColor -> hex string in the set; then replace
settingsSidebarTintLightBinding and settingsSidebarTintDarkBinding to call
makeSidebarTintBinding passing closures that get/set sidebarTintHexLight and
sidebarTintHexDark respectively.
Sources/GhosttyConfig.swift (1)

144-160: Reset resolved sidebar fields before recomputing.

At Line 145, early return and conditional assignments can leave stale sidebarBackground, sidebarBackgroundLight, and sidebarBackgroundDark when this method is called more than once on the same instance.

♻️ Proposed refactor
 mutating func resolveSidebarBackground(preferredColorScheme: ColorSchemePreference) {
-        guard let raw = rawSidebarBackground else { return }
+        sidebarBackground = nil
+        sidebarBackgroundLight = nil
+        sidebarBackgroundDark = nil
+
+        guard let raw = rawSidebarBackground,
+              !raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }

         let lightResolved = Self.resolveThemeName(from: raw, preferredColorScheme: .light)
         let darkResolved = Self.resolveThemeName(from: raw, preferredColorScheme: .dark)
         let hasDualMode = lightResolved != darkResolved
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyConfig.swift` around lines 144 - 160, In
resolveSidebarBackground(preferredColorScheme:), clear/reset sidebarBackground,
sidebarBackgroundLight, and sidebarBackgroundDark at the start (and when
rawSidebarBackground is nil) so repeated calls don't leave stale values; then
compute hasDualMode and only assign sidebarBackgroundLight/sidebarBackgroundDark
when dual-mode, otherwise ensure those dual fields remain nil, and finally set
sidebarBackground from the resolved color if available — refer to the function
resolveSidebarBackground and the properties rawSidebarBackground,
sidebarBackground, sidebarBackgroundLight, and sidebarBackgroundDark.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Around line 1738-1756: The test
testApplyToUserDefaultsOnlyWritesOpacityWhenExplicit only snapshots
"sidebarTintHex" and "sidebarTintOpacity" but
applySidebarAppearanceToUserDefaults() can also clear "sidebarTintHexLight" and
"sidebarTintHexDark"; expand the keys array in that test to include
"sidebarTintHexLight" and "sidebarTintHexDark", capture their originals (like
the existing originals map) and ensure the defer restore loop resets those keys
via restoreDefaultsValue so state isn't leaked to other tests.

In `@Sources/GhosttyConfig.swift`:
- Around line 182-184: The current code only sets
defaults.set(color.hexString(), forKey: "sidebarTintHex") when sidebarBackground
is non-nil, leaving stale values when sidebarBackground is nil; update the
handling around sidebarBackground in Sources/GhosttyConfig.swift so that when
sidebarBackground is nil you clear the stored value by calling
defaults.removeObject(forKey: "sidebarTintHex") (or set nil/empty appropriately)
instead of leaving the old hex; locate the block that references
sidebarBackground and defaults to apply this change.

---

Nitpick comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Around line 1680-1736: Extract repeated UserDefaults key strings
("sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark") into test-local
constants (e.g., let kSidebarTint = "sidebarTintHex", kSidebarTintLight =
"sidebarTintHexLight", kSidebarTintDark = "sidebarTintHexDark") and use those
constants in the tests testApplyToUserDefaultsWritesHexWhenConfigSet,
testApplyToUserDefaultsClearsStaleKeysOnSwitchFromDualToSingle and the earlier
test that uses testKey; update the keys array, defaults.set/get calls, and
restore loop to reference the constants so the literals are defined once and
reduce duplication/typo risk.

In `@Sources/cmuxApp.swift`:
- Around line 3199-3221: The two bindings settingsSidebarTintLightBinding and
settingsSidebarTintDarkBinding are identical except for which stored-hex they
read/write (sidebarTintHexLight vs sidebarTintHexDark), so extract a small
helper that builds a Binding<Color> from two closures (one to get the optional
override hex and one to set it). Implement e.g. a private func
makeSidebarTintBinding(getOverride: `@escaping` () -> String?, setOverride:
`@escaping` (String?) -> Void) -> Binding<Color> that performs the Color(nsColor:
NSColor(hex: override ?? sidebarTintHex) ?? .black) get and converts Color ->
NSColor -> hex string in the set; then replace settingsSidebarTintLightBinding
and settingsSidebarTintDarkBinding to call makeSidebarTintBinding passing
closures that get/set sidebarTintHexLight and sidebarTintHexDark respectively.

In `@Sources/GhosttyConfig.swift`:
- Around line 144-160: In resolveSidebarBackground(preferredColorScheme:),
clear/reset sidebarBackground, sidebarBackgroundLight, and sidebarBackgroundDark
at the start (and when rawSidebarBackground is nil) so repeated calls don't
leave stale values; then compute hasDualMode and only assign
sidebarBackgroundLight/sidebarBackgroundDark when dual-mode, otherwise ensure
those dual fields remain nil, and finally set sidebarBackground from the
resolved color if available — refer to the function resolveSidebarBackground and
the properties rawSidebarBackground, sidebarBackground, sidebarBackgroundLight,
and sidebarBackgroundDark.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69304419-033f-4932-984c-f6f55bc9c529

📥 Commits

Reviewing files that changed from the base of the PR and between fe74d4e and e5baef7.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/cmuxApp.swift
  • cmuxTests/GhosttyConfigTests.swift
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

✅ Actions performed

Reviews resumed.

- Config: sidebar-background supports plain hex (#336699) or
  light/dark syntax (light:#fbf3db,dark:#103c48)
- Config: sidebar-tint-opacity overrides tint opacity
- Settings UI: per-scheme color pickers, opacity slider (0-70%), reset
- SidebarBackdrop resolves light/dark hex based on @Environment colorScheme
- applySidebarAppearanceToUserDefaults guards on rawSidebarBackground presence
  so UI picks survive appearance toggles when no config is set
- Stale light/dark UserDefaults keys cleared when config switches from
  dual-mode to single or sidebar-background is removed
- applyPreset() and Reset Tint clear per-scheme overrides
- Debug snapshot (combinedPayload + copySidebarConfig) includes new keys
- ColorPicker labels use String(localized:) per localization policy
- Opacity slider capped at 0.7 to match debug view vibrancy constraint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rrva rrva force-pushed the sidebar-background-config branch from e5baef7 to adbe777 Compare March 15, 2026 14:32
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Sources/GhosttyConfig.swift (1)

162-190: Prefer shared constants for sidebar UserDefaults keys.

These key literals are now spread across multiple writes/removals. Reusing a single key namespace (same source as @AppStorage) would reduce drift risk during future renames.

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

In `@Sources/GhosttyConfig.swift` around lines 162 - 190, Replace the scattered
string literals in applySidebarAppearanceToUserDefaults with shared constants
from the same key namespace used by your `@AppStorage` declarations (or add a
single source-of-truth like SidebarKeys/sidebarUserDefaultsKeys), then use those
constants for all get/set/remove calls for "sidebarTintOpacity",
"sidebarTintHexLight", "sidebarTintHexDark", and "sidebarTintHex" to avoid
drift; update applySidebarAppearanceToUserDefaults to reference the new
constants (and remove the hardcoded strings) so all reads/writes use the unified
keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/GhosttyConfig.swift`:
- Around line 162-190: Replace the scattered string literals in
applySidebarAppearanceToUserDefaults with shared constants from the same key
namespace used by your `@AppStorage` declarations (or add a single source-of-truth
like SidebarKeys/sidebarUserDefaultsKeys), then use those constants for all
get/set/remove calls for "sidebarTintOpacity", "sidebarTintHexLight",
"sidebarTintHexDark", and "sidebarTintHex" to avoid drift; update
applySidebarAppearanceToUserDefaults to reference the new constants (and remove
the hardcoded strings) so all reads/writes use the unified keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ee169dd-51db-4d02-86f5-f47771cbff77

📥 Commits

Reviewing files that changed from the base of the PR and between e5baef7 and adbe777.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/GhosttyConfig.swift
  • Sources/cmuxApp.swift
  • cmuxTests/GhosttyConfigTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmuxTests/GhosttyConfigTests.swift
  • CHANGELOG.md

@rrva
Copy link
Copy Markdown
Contributor Author

rrva commented Mar 15, 2026

Re: review 3950235040 — the existing sidebar keys (sidebarMaterial, sidebarBlendMode, sidebarState, sidebarCornerRadius, sidebarBlurOpacity, sidebarPreset) all use inline string literals. Extracting only the new tint keys into constants while leaving the rest inline would be inconsistent. A full refactor of all sidebar keys into a shared namespace would be a worthwhile follow-up but is out of scope for this PR.

@lawrencecchen lawrencecchen merged commit a7cb968 into manaflow-ai:main Mar 15, 2026
10 of 12 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…aflow-ai#1465)

- Config: sidebar-background supports plain hex (#336699) or
  light/dark syntax (light:#fbf3db,dark:#103c48)
- Config: sidebar-tint-opacity overrides tint opacity
- Settings UI: per-scheme color pickers, opacity slider (0-70%), reset
- SidebarBackdrop resolves light/dark hex based on @Environment colorScheme
- applySidebarAppearanceToUserDefaults guards on rawSidebarBackground presence
  so UI picks survive appearance toggles when no config is set
- Stale light/dark UserDefaults keys cleared when config switches from
  dual-mode to single or sidebar-background is removed
- applyPreset() and Reset Tint clear per-scheme overrides
- Debug snapshot (combinedPayload + copySidebarConfig) includes new keys
- ColorPicker labels use String(localized:) per localization policy
- Opacity slider capped at 0.7 to match debug view vibrancy constraint

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 request: configurable sidebar background color

2 participants