Skip to content

Fix #3075: accept named colors in cmux.json and degrade gracefully on parse errors - #3095

Closed
austinywang wants to merge 2 commits into
mainfrom
issue-3075-config-named-color-rejects
Closed

austinywang wants to merge 2 commits into
mainfrom
issue-3075-config-named-color-rejects

Conversation

@austinywang

@austinywang austinywang commented Apr 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #3075. Before this PR, a single command in cmux.json with a named color (e.g. "color": "Indigo") caused the entire config file to be silently rejected — no commands loaded, cmux reload-config returned OK, and the sidebar showed no error. The decoder only accepted #RRGGBB hex strings, and one rejected command failed the whole top-level JSONDecoder().decode(CmuxConfigFile.self, ...) call.

Two-part fix:

  • Accept named palette colors. CmuxWorkspaceDefinition now resolves the color field via a new resolveColor(_:) helper that accepts either a 6-digit hex (with/without #) or a case-insensitive match against WorkspaceTabColorSettings.defaultPalette (Red, Navy, Indigo, Purple, Charcoal, ...). "Indigo" now resolves to #283593.
  • Degrade gracefully on parse errors. New CmuxConfigFile.parseLenient(_:) decodes each command independently. One malformed command (bad color, bad layout, missing fields, etc.) no longer poisons its siblings — it is returned as a warning alongside the commands that did load. CmuxConfigStore.parseConfig now uses this path and logs each warning via NSLog so misconfigured entries are visible.

This matches the issue's expected behavior: "Both commands appear, or at minimum Echo still appears and a parse warning is logged."

Two-commit structure (per project policy)

  1. First commit adds failing regression tests only (named-color resolution + lenient parsing). CI should be red.
  2. Second commit adds the fix. CI should be green.

Repro (from the issue)

{
  "commands": [
    {
      "name": "Indigo Workspace",
      "workspace": { "name": "Indigo", "color": "Indigo" }
    },
    { "name": "Echo", "command": "echo hi" }
  ]
}
  • Before: neither command appeared; no error shown.
  • After: both commands appear; the Indigo workspace uses #283593. If the color were a typo like "Indigoo", only that command is skipped (with a logged warning), and Echo still appears.

Test plan

  • CI: unit tests in cmuxTests/CmuxConfigTests.swift pass on the fix commit.
  • CI: the first commit fails the new tests (proves the tests exercise the bug).
  • Manual: place the repro cmux.json in the project root, cmux reload-config, confirm both commands appear in the command palette with the Indigo color on the workspace.
  • Manual: swap "Indigo" for "NotARealColor" and confirm Echo still appears in the palette and the system log shows a [CmuxConfig] Skipped command warning.

Note

Medium Risk
Changes config decoding behavior and error handling, which can affect which commands load from user cmux.json files. Risk is mitigated by added unit tests covering named colors and partial-failure parsing.

Overview
Fixes cmux.json regressions by accepting named palette colors (e.g. "Indigo", case-insensitive) for workspace color, resolving them to normalized hex.

Updates config loading to degrade gracefully on per-command decode failures via CmuxConfigFile.parseLenient, so malformed commands are skipped with logged warnings while valid sibling commands still load, instead of rejecting the entire file.

Reviewed by Cursor Bugbot for commit 62b8741. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added support for named workspace colors (e.g., "Indigo", "Navy") with case-insensitive matching.
  • Bug Fixes

    • Configuration parsing is now more resilient; invalid individual commands no longer prevent the entire configuration from loading, with warnings logged for skipped items.
  • Tests

    • Added test coverage for named color resolution and lenient parsing behavior.

Summary by cubic

Fix #3075 by accepting named palette colors in cmux.json and making config loading tolerant. A bad command no longer drops the whole file; valid commands still load and a warning is logged.

  • Bug Fixes
    • Color resolution: CmuxWorkspaceDefinition.resolveColor accepts 6-digit hex (with/without #) and case-insensitive names from WorkspaceTabColorSettings.defaultPalette (e.g. "Indigo" -> #283593).
    • Lenient loading: CmuxConfigFile.parseLenient decodes commands independently and returns warnings; CmuxConfigStore.parseConfig uses it and logs each warning via NSLog.

Written for commit 62b8741. Summary will update on new commits.

austinywang and others added 2 commits April 21, 2026 22:36
Tests cover:
- Named color strings ("Indigo", "Navy", case-insensitive) in a workspace
  `color` field should decode to the palette hex.
- `CmuxConfigFile.parseLenient` should return sibling commands intact when
  one command fails to decode (invalid color, invalid layout), and surface
  a warning instead of silently rejecting the whole file.

Introduces a `parseLenient` entry point that currently falls back to strict
`JSONDecoder().decode`. That makes these tests red on purpose — the fix in
the follow-up commit turns parseLenient tolerant and teaches the workspace
color decoder to resolve named palette entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… parse errors

Before: a single command with a named color like "Indigo" in cmux.json caused
`JSONDecoder().decode(CmuxConfigFile.self, ...)` to throw, and `parseConfig`
returned nil — nuking every command in the file (even commands unrelated to
colors). The failure was only surfaced to the system log; the user saw
"reload-config" succeed with an empty sidebar and no visible error.

Two-part fix:

1. `CmuxWorkspaceDefinition.resolveColor` now accepts either a 6-digit hex
   string or a named entry from `WorkspaceTabColorSettings.defaultPalette`
   (case-insensitive), so values like "Indigo"/"Navy" resolve to their
   palette hex instead of failing the whole decode.

2. `CmuxConfigFile.parseLenient` decodes each command independently, so one
   malformed command (bad color, bad layout, etc.) no longer poisons its
   siblings. Failures are returned as warnings alongside the successfully
   parsed commands. `CmuxConfigStore.parseConfig` now uses `parseLenient`
   and logs each warning via NSLog so misconfigured entries are visible.

Strict `JSONDecoder().decode(CmuxConfigFile.self, ...)` still throws on
malformed commands — only the lenient path used by the runtime loader
tolerates per-command failures.

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

vercel Bot commented Apr 22, 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 Apr 22, 2026 5:44am

@coderabbitai

coderabbitai Bot commented Apr 22, 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: 7db3201a-180a-44ec-a0b1-5ed14f3614c4

📥 Commits

Reviewing files that changed from the base of the PR and between 3e84c16 and 62b8741.

📒 Files selected for processing (2)
  • Sources/CmuxConfig.swift
  • cmuxTests/CmuxConfigTests.swift

📝 Walkthrough

Walkthrough

The changes implement lenient JSON configuration parsing that preserves successfully decoded commands while capturing per-command failures as warnings. Additionally, a new color resolution helper is added to support both normalized hex values and case-insensitive named palette colors in workspace definitions.

Changes

Cohort / File(s) Summary
Lenient Config Parsing
Sources/CmuxConfig.swift
Added custom CodingKeys, init(commands:), init(from:), and encode(to:) methods to CmuxConfigFile. Introduced parseLenient(_:) static method that decodes the top-level structure, attempts per-command deserialization, and returns both successfully decoded commands and a warnings list. Updated CmuxConfigStore.parseConfig(at:) to use lenient parsing and log skip warnings.
Color Resolution
Sources/CmuxConfig.swift
Added resolveColor(_:) helper to CmuxWorkspaceDefinition for flexible color validation accepting normalized 6-digit hex (with or without #) or case-insensitive named palette entries.
Test Coverage
cmuxTests/CmuxConfigTests.swift
Added test cases verifying named color string resolution to palette hex values with case-insensitive matching, and comprehensive tests for parseLenient(_:) behavior including valid configurations with no warnings and individual command failures being isolated without affecting sibling commands.

Sequence Diagram

sequenceDiagram
    participant ConfigStore as CmuxConfigStore
    participant File as CmuxConfigFile
    participant Decoder as JSONDecoder
    participant Command as CmuxCommandDefinition
    participant Results as Result Buffer

    ConfigStore->>File: parseLenient(data)
    File->>Decoder: decode top-level structure
    Decoder-->>File: commands array
    File->>Results: initialize warnings list
    loop For each command
        File->>Decoder: decode CmuxCommandDefinition
        alt Success
            Decoder-->>Command: valid command
            Command-->>Results: add to successful commands
        else Failure
            Decoder-->>Results: capture error as warning
        end
    end
    File-->>ConfigStore: (file: CmuxConfigFile?, warnings: [String])
    ConfigStore->>ConfigStore: log warnings & parse result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Lenient parsing hops with grace,
Commands fail, yet keep their place,
Colors named in palette true,
Warnings whisper, warnings brew,
Invalid friends aren't turned away,
Just logged as warnings of the day! 🌈

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% 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 directly references issue #3075 and summarizes both main changes: accepting named colors and graceful error degradation on parse failures.
Description check ✅ Passed The description covers all required template sections: a detailed summary explaining the problem and two-part fix, testing instructions with CI/manual steps, and a complete checklist with all items addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 issue-3075-config-named-color-rejects

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 Apr 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes #3075 by (1) teaching CmuxWorkspaceDefinition to resolve named palette colors (e.g. \"Indigo\" → #283593) via the new resolveColor helper, and (2) replacing the all-or-nothing JSONDecoder call in CmuxConfigStore.parseConfig with CmuxConfigFile.parseLenient, which decodes each command independently so a single malformed entry no longer silently rejects the entire config file. The two-commit structure and regression tests follow the project's policy.

Confidence Score: 5/5

Safe to merge; all remaining feedback is non-blocking style.

The core logic is correct: lenient decoding properly isolates per-command failures, named-color resolution is case-insensitive and falls back gracefully, and the existing strict init(from:) path is preserved for callers that need it. The only finding is a minor duplicate log line on catastrophic JSON failure (P2). Tests exercise the bug regression end-to-end and follow the project's test quality policy.

No files require special attention.

Important Files Changed

Filename Overview
Sources/CmuxConfig.swift Adds parseLenient for per-command fault isolation and resolveColor for named-palette color support; one minor logging redundancy on top-level failure, no correctness issues.
cmuxTests/CmuxConfigTests.swift Adds regression tests for named-color decoding (Indigo, Navy, case-insensitive) and lenient parsing (bad color, bad layout, end-to-end named color); tests verify runtime behaviour, consistent with test quality policy.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[parseConfig at path] --> B{File exists & non-empty?}
    B -- No --> Z[return nil]
    B -- Yes --> C[parseLenient data]
    C --> D{Top-level JSON valid?}
    D -- No --> E[return nil + error warning]
    E --> F[log warning AND log generic message]
    F --> Z
    D -- Yes --> G[Decode each command independently]
    G --> H{Command decode OK?}
    H -- Yes --> I[append to commands]
    H -- No --> J[append to warnings with fallback name]
    I & J --> K[CmuxConfigFile with valid commands]
    K --> L{warnings empty?}
    L -- No --> M[NSLog each warning]
    L -- Yes --> N[return CmuxConfigFile]
    M --> N

    subgraph resolveColor
        RC1[raw string] --> RC2{normalizedHex succeeds?}
        RC2 -- Yes --> RC3[return hex]
        RC2 -- No --> RC4[trim whitespace]
        RC4 --> RC5{palette name match case-insensitive?}
        RC5 -- Yes --> RC6[return entry.hex]
        RC5 -- No --> RC7[return nil → DecodingError thrown]
    end
Loading

Reviews (1): Last reviewed commit: "Fix #3075: accept named colors in cmux.j..." | Re-trigger Greptile

Comment thread Sources/CmuxConfig.swift
Comment on lines +480 to 482
if file == nil {
NSLog("[CmuxConfig] parse error at %@: top-level JSON could not be decoded", path)
}

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 Redundant log on top-level decode failure

When parseLenient cannot parse the top-level JSON structure, it returns (nil, [String(describing: error)]). The warnings loop on lines 477–479 already logs that error with the full description. The if file == nil block then emits a second, less-informative message ("top-level JSON could not be decoded") for the same event, producing two log lines where the second adds no new information.

Consider either dropping the if file == nil block (the warning already contains the error), or — if you want a dedicated "top-level failure" message — skip logging the generic error string in the warnings loop when file is nil:

let (file, warnings) = CmuxConfigFile.parseLenient(data)
if file == nil {
    // Top-level failure: parseLenient already put the error in warnings[0]
    NSLog("[CmuxConfig] parse error at %@: %@", path, warnings.first ?? "unknown error")
} else {
    for warning in warnings {
        NSLog("[CmuxConfig] Skipped command in %@: %@", path, warning)
    }
}
return file

@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 2 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/CmuxConfig.swift">

<violation number="1" location="Sources/CmuxConfig.swift:480">
P3: Remove the extra top-level parse-error log here; top-level decode failures are already logged via `warnings`, so this produces duplicate entries and the second message is less informative.</violation>
</file>

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

Comment thread Sources/CmuxConfig.swift
for warning in warnings {
NSLog("[CmuxConfig] %@: %@", path, warning)
}
if file == nil {

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Remove the extra top-level parse-error log here; top-level decode failures are already logged via warnings, so this produces duplicate entries and the second message is less informative.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/CmuxConfig.swift, line 480:

<comment>Remove the extra top-level parse-error log here; top-level decode failures are already logged via `warnings`, so this produces duplicate entries and the second message is less informative.</comment>

<file context>
@@ -391,12 +473,14 @@ final class CmuxConfigStore: ObservableObject {
+        for warning in warnings {
+            NSLog("[CmuxConfig] %@: %@", path, warning)
+        }
+        if file == nil {
+            NSLog("[CmuxConfig] parse error at %@: top-level JSON could not be decoded", path)
         }
</file context>
Fix with Cubic

BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Config and CLI paths could supply a color as a human-readable name
like "Red" or "Blue" rather than a hex string. The executor passed
the raw string directly to setCustomColor, which normalizes only
valid 6-digit hex values and silently no-ops everything else, causing
the workspace tab to render with no color. Added resolveColorToHex,
a nonisolated static helper that first tries normalizedHex for valid
hex input and then does a case-insensitive lookup in
WorkspaceTabColorSettings.defaultPaletteWithOverrides. Strings that
are neither valid hex nor known palette names produce an ApplyFailure
with code "unknown_color_name" rather than a silent no-op.

Reported-by: @zacharygutt <upstream issue manaflow-ai/cmux#3075>
Reference: manaflow-ai/cmux#3095 by @austinywang
Reference: manaflow-ai/cmux#3149 by @lawrencecchen
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…cks) (#87)

* fix: propagate SSH_AUTH_SOCK through SSH subprocess environment

macOS GUI apps launched from Finder or Dock do not inherit
SSH_AUTH_SOCK or SSH_AGENT_PID because those are set by ssh-agent
in the user's shell, not in the launch environment. Previously,
WorkspaceRemoteDaemonRPCClient.start() and startReverseRelayLocked()
created Process objects without setting process.environment, leaving
it nil and relying on implicit inheritance. Explicit assignment of
ProcessInfo.processInfo.environment to both SSH Process objects ensures
agent socket variables pass through and SSH key authentication works
from the GUI app.

Reported-by: @knight42 <upstream issue manaflow-ai/cmux#3162>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: parse K/M/G byte-count suffixes in scrollback-limit config

The Ghostty config format documents that scrollback-limit accepts K, M,
and G suffixes (kibibytes, mebibytes, gibibytes). The previous parser
used plain Int(value) and silently discarded values like "1G", "512M",
or "100K", leaving the scrollback limit at the 10000-line default.
Added a private parseByteCount helper that strips a trailing
case-insensitive K/M/G suffix, parses the numeric prefix, and multiplies
by the appropriate power-of-1024 factor. Plain integers continue to work
unchanged. The Ghostty Zig layer expects a raw usize byte count, so
expansion is handled entirely in the Swift config reader.

Reported-by: @shaun0927 <upstream issue manaflow-ai/cmux#2950>
Reference: manaflow-ai/cmux#2952 by @shaun0927
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: resolve named palette colors in WorkspaceApplyPlan customColor

Config and CLI paths could supply a color as a human-readable name
like "Red" or "Blue" rather than a hex string. The executor passed
the raw string directly to setCustomColor, which normalizes only
valid 6-digit hex values and silently no-ops everything else, causing
the workspace tab to render with no color. Added resolveColorToHex,
a nonisolated static helper that first tries normalizedHex for valid
hex input and then does a case-insensitive lookup in
WorkspaceTabColorSettings.defaultPaletteWithOverrides. Strings that
are neither valid hex nor known palette names produce an ApplyFailure
with code "unknown_color_name" rather than a silent no-op.

Reported-by: @zacharygutt <upstream issue manaflow-ai/cmux#3075>
Reference: manaflow-ai/cmux#3095 by @austinywang
Reference: manaflow-ai/cmux#3149 by @lawrencecchen
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: invalidate GhosttyConfig cache on system appearance change

light:X,dark:Y paired themes resolve to the correct variant via
GhosttyConfig.currentColorSchemePreference(), but the load cache keyed
on ColorSchemePreference was never cleared when macOS switched between
light and dark mode. The stale cached config would be returned on the
first load after an appearance change, keeping the wrong theme active
until the next explicit config reload. Added a NSKeyValueObservation
on NSApp.effectiveAppearance in AppDelegate.applicationDidFinishLaunching
that calls GhosttyConfig.invalidateLoadCache() and then triggers
GhosttyApp.shared.reloadConfiguration so terminals pick up the correct
light or dark theme variant immediately.

Reported-by: @Corey-T1000 <upstream issue manaflow-ai/cmux#2922>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: include .badge in notification authorization request

requestAuthorizationIfNeeded called UNUserNotificationCenter.requestAuthorization
with options [.alert, .sound] but omitted .badge. On macOS, setting
NSApp.dockTile.badgeLabel requires .badge authorization — without it
the assignment silently no-ops and the badge counter never appears
in the Dock. The rest of the badge pipeline (isDockBadgeEnabled check,
dockBadgeLabel computation, dockTile.badgeLabel assignment) was already
correct. Adding .badge to the authorization options closes both #1764
and #2718 which report the same symptom.

Reported-by: @gilsiun <upstream issue manaflow-ai/cmux#1764>
Also-closes: manaflow-ai/cmux#2718 (reported by @tuzisang)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: remove .safeHelp from high-churn titlebar buttons to prevent UAF

NSToolTipManager installs per-view tooltip handlers via SwiftUI's .help()
modifier. When a hosting view is removed during split churn or workspace
teardown, the NSView backing the tooltip may be freed while
NSToolTipManager still holds a reference, causing a use-after-free crash
on the next mouse-enter event. Removed .safeHelp() from the three
high-churn titlebar control buttons (toggle sidebar, notifications, new
workspace) which remount frequently during tab management. The .accessibilityLabel()
calls already present on all three buttons preserve VoiceOver
discoverability without touching NSToolTipManager. The safeHelp()
extension remains available for use on stable views that do not remount
during normal workspace interactions.

Reported-by: @austinywang <upstream issue manaflow-ai/cmux#1036>
Reference: manaflow-ai/cmux#1038 by @lawrencecchen
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: subtract legacy scrollbar gutter from terminal width in .legacy mode

When a mouse is connected, macOS switches to legacy (always-visible)
scrollers. The scroll view was initialized with scrollerStyle = .overlay
but the system can override this, causing the vertical scrollbar to
occupy physical column space. The previous synchronizeCoreSurface()
used scrollView.contentSize.width directly, which does not account for
the scrollbar gutter in legacy mode, so the rightmost terminal columns
render under the scrollbar. The fix checks scrollView.scrollerStyle
and subtracts NSScroller.scrollerWidth(for:.regular, scrollerStyle:.legacy)
only when the effective style is .legacy. Overlay mode behavior is
unchanged — that path was a prior deliberate decision to prevent
column-flap during split churn (comment at line 8544 is preserved).

Reported-by: @Thinkscape <upstream issue manaflow-ai/cmux#2997>
Reference: manaflow-ai/cmux#3001 by @rdsciv
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: add coalescing guard to palette overlay update to reduce idle spin

The focus-reassert path (reassertTerminalSurfaceFocus, 0.05s throttle)
and first-responder scheduling (scheduleAutomaticFirstResponderApply,
pendingAutomaticFirstResponderApply flag) were already guarded in c11
against redundant work. The third idle runloop offender was the
WindowCommandPaletteOverlayController.update() path: SwiftUI calls it
on every parent render cycle, and when the palette is hidden each call
re-assigned hostingView.rootView = AnyView(EmptyView()), which drives
a redundant SwiftUI layout pass. Added an early-exit guard: when both
the current and new visibility state are false, skip the update entirely.
The transition from visible to hidden is preserved — the guard only
elides the no-op hidden-to-hidden calls.

Reported-by: @lawrencecchen <upstream issue manaflow-ai/cmux#2996>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: prevent white-on-white text in light mode and honor config palette entries

Two-part fix for the light-mode text regression introduced in 0.63.2:

1. Contrast fallback: added applyContrastFallbackIfNeeded(), a mutating
helper called at the end of loadFromDisk. When both backgroundColor and
foregroundColor have luminance > 0.5 (both near-white), the foreground is
replaced with #1A1A1A. This closes the case where a light theme loads the
background correctly but the foreground defaults to the near-white Monokai
value (#fdfff1) because the theme file does not explicitly set foreground.
This fix layers on Pick 5 (cache invalidation on appearance change) which
ensures the correct light-vs-dark config is loaded after a system switch.

2. Palette override: applyPalette(to:config:) now checks config.palette[i]
before falling back to the Monokai hardcoded default. When a theme file or
config key sets palette entries, those colors are used for the ANSI palette
instead of the defaults.

Reported-by: @exlaw <upstream issue manaflow-ai/cmux#2708>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ghostty: bump submodule to Stage-11-Agentics fork, fix PageList SIGSEGV race (#2738)

Point the ghostty submodule at the Stage-11-Agentics/ghostty fork (all
other c11 submodules already live under Stage-11-Agentics). The fork is
based on manaflow-ai/ghostty main and carries one additional fix:

  terminal: snapshot page rows in SlidingWindow.Meta to fix SIGSEGV race

SlidingWindow.highlight() was reading meta.node.data.size.rows directly
from the live page node without holding the terminal lock. The IO thread
calls resizeCols() (with the terminal lock) and frees old page nodes, so
a concurrent search thread calling next() could crash with a use-after-free
SIGSEGV. The fix snapshots the row count into Meta.rows during append()
(which IS called under the terminal lock) and uses that snapshot
throughout highlight().

Reported-by: @tmad4000 <upstream issue manaflow-ai/cmux#2738>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* localize: add workspace.apply.unknownColorName translations for 6 locales

Covers C11-22 pick 4 (#3075) error string: unknown named color in config.

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

* fix: guard parseByteCount against overflow and negative values

scrollback-limit = 9000000000G caused an Int multiplication trap at
config load. Add overflow check via multipliedReportingOverflow and
reject negative prefixes before the multiply.

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

* docs: update ghostty-fork.md for Stage-11-Agentics fork and PageList race fix

Document the submodule URL change (manaflow-ai -> Stage-11-Agentics),
the PageList SIGSEGV fix in sliding_window.zig, and conflict notes for
future upstream syncs. Required by CLAUDE.md: "Keep docs/ghostty-fork.md
up to date with any fork changes and conflict notes."

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

* fix: log when applyContrastFallbackIfNeeded overrides foreground color

Silent override produced confusing 'my foreground setting is not
applying' bugs. Log the old/new values so users can find this via
Console.app or debug output.

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

* docs: document named palette color support for customColor in schema

WorkspaceLayoutExecutor now accepts named palette colors (e.g. "Red")
in addition to hex strings. Unknown names produce an ApplyFailure with
code unknown_color_name. Schema previously said hex-only.

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

* refactor: make parseByteCount a static func (reads no instance state)

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

* ghostty: rebase PageList race fix onto c11 theme-picker fork tip (bc9be90a)

The previous submodule bump (df2a237) was based on a fresh
manaflow-ai/ghostty main, accidentally dropping 7 c11 theme-picker
commits that c11 requires. This commit rebases the PageList SIGSEGV
race fix onto bc9be90a (the existing c11 fork tip) so both the
theme-picker hooks and the PageList fix are present.

The 7 theme-picker commits modify src/cli/list_themes.zig to read
CMUX_THEME_PICKER_COLOR_SCHEME, CMUX_THEME_PICKER_INITIAL_LIGHT, and
CMUX_THEME_PICKER_INITIAL_DARK -- env vars set by c11 before calling
ghostty +list-themes. Without them the theme picker ignores c11's setup.

Reported-by: @tmad4000 <upstream issue manaflow-ai/cmux#2738>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: correct ghostty-fork.md ancestry and rename cmux to c11 in theme picker sections

The fork base is bc9be90a (c11 theme-picker tip), not manaflow-ai main.
Rename 'cmux theme picker' to 'c11 theme picker' per naming policy.
Update PageList fix SHA to c64952975 (cherry-picked commit).
Add upstream-sync note for list_themes.zig conflict guidance.

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

* fix: reject plain negative values in parseByteCount fallback path

scrollback-limit = -1 was accepted as -1 (no suffix branch, guard
did not apply). Guard the Int(s) fallback the same way.

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

* fix: substitute color name into ApplyFailure message for unknown_color_name

String(localized:defaultValue:"\(color)") returns the xcstrings value
verbatim in non-development builds; %@ was not substituted. Use
String(format:) to fill the placeholder.

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

* docs: fix customColor example to use a confirmed palette color name

"Ocean Blue" does not exist in the palette; use a name that does.

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

* fix: validate palette index is in 0...15 before assignment

Out-of-bounds indices were silently accepted and could write past
the 16-entry palette array.

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

* docs: update customColor doc comment to mention named palette colors

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

* ci: fix build-ghosttykit and download to use Stage-11-Agentics/ghostty

The workflow was publishing xcframework releases to manaflow-ai/ghostty
using a token that only has write access to Stage-11-Agentics repos,
causing exit code 4 on every PR. Similarly the download script was
fetching from manaflow-ai/ghostty where no release exists for the
Stage-11-Agentics fork's ghostty SHA.

Fixes:
- build-ghosttykit.yml: check-release and upload steps now target
  Stage-11-Agentics/ghostty
- build-ghosttykit.yml: add "Pin checksum" step that computes SHA256
  of the tarball post-upload and commits it to ghosttykit-checksums.txt,
  eliminating the manual step and the chicken-and-egg where the checksum
  guard fires before the release exists
- download-prebuilt-ghosttykit.sh: default DOWNLOAD_URL now points to
  Stage-11-Agentics/ghostty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: publish GhosttyKit releases to c11 repo using GITHUB_TOKEN

GHOSTTY_RELEASE_TOKEN is not configured on this fork. Switch the
build-ghosttykit workflow to publish xcframework releases to
Stage-11-Agentics/c11 instead, using GITHUB_TOKEN with an explicit
contents:write permission grant. Update download-prebuilt-ghosttykit.sh
to fetch from the same location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: checkout branch ref so git push works in Pin checksum step

Actions checks out a detached HEAD for PRs by default, causing
git push to fail with exit 128. Checking out the actual branch
ref (head_ref for PRs, ref_name for push events) puts us on a
real branch so the push succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: always run Pin checksum, download tarball when release pre-exists

If the release was created in a previous CI run (exists=true), the old
conditional skipped Pin checksum entirely, leaving ghosttykit-checksums.txt
unpopulated forever. Remove the condition and download the tarball from
the existing release when the local file isn't present, so the checksum
is always committed regardless of which run built the release.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: pin GhosttyKit checksum for ghostty c649529750b12e7fde7a33b74d5310a1b988cb67

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview — 62b87418 Deployed Apr 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cmux.json: named color strings silently reject entire config file

3 participants