Skip to content

Fix scrollback-limit parser to accept Ghostty K/M/G suffixes - #2952

Open
shaun0927 wants to merge 3 commits into
manaflow-ai:mainfrom
shaun0927:fix/2950-scrollback-limit-suffixes
Open

shaun0927 wants to merge 3 commits into
manaflow-ai:mainfrom
shaun0927:fix/2950-scrollback-limit-suffixes

Conversation

@shaun0927

@shaun0927 shaun0927 commented Apr 17, 2026 •

Copy link
Copy Markdown

Summary

  • accept K/M/G/T byte suffixes in scrollback-limit (case-insensitive, optional trailing B), matching upstream Ghostty's own parser
  • reject overflow on the suffix multiplication so a pathological literal can no longer trap
  • existing plain-integer and _-grouped literal behaviour is preserved at the call site (parse, line 256)

Closes #2950

Root Cause

parseIntegerLiteral (Sources/GhosttyConfig.swift:320-328) only stripped _ digit-group separators. Upstream Ghostty's configuration parser accepts byte-typed values like 10MB, so a user copying the recommended example from upstream Ghostty docs into their ~/.config/ghostty/config silently got cmux's default. README advertises Ghostty config compatibility (Reads your existing ~/.config/ghostty/config), so the silent fallback is surprising.

Fix

parseIntegerLiteral extended to handle the same set of suffixes Ghostty itself accepts, with overflow-safe multiplication:

private static func parseIntegerLiteral(_ value: String) -> Int? {
    var normalized = value
        .trimmingCharacters(in: .whitespaces)
        .replacingOccurrences(of: "_", with: "")
        .lowercased()

    if normalized.hasSuffix("b") {
        normalized.removeLast()
    }

    let multiplier: Int
    switch normalized.last {
    case "k": multiplier = 1 << 10
    case "m": multiplier = 1 << 20
    case "g": multiplier = 1 << 30
    case "t": multiplier = 1 << 40
    default:  multiplier = 1
    }
    if multiplier > 1 {
        normalized.removeLast()
    }

    guard !normalized.isEmpty,
          let magnitude = Int(normalized),
          magnitude >= 0 else {
        return nil
    }

    let (scaled, overflow) = magnitude.multipliedReportingOverflow(by: multiplier)
    return overflow ? nil : scaled
}

The call site in parse (line 256) is untouched — the existing if let limit = Self.parseIntegerLiteral(value) keeps the silent-default behaviour for genuinely unparseable input, so any existing config that already silently fell through (typos, gibberish) still does.

Tests

cmuxTests/GhosttyConfigTests.swift — new GhosttyConfigScrollbackLimitParseTests:

  • testAcceptsPlainIntegerValue — 12345 → 12345
  • testAcceptsUnderscoreSeparatedInteger — 10_000_000 → 10_000_000 (regression for the existing PR Fix scrollback-limit byte handling #2927 behaviour)
  • testAcceptsKilobyteSuffix — 256K → 262144
  • testAcceptsKilobyteSuffixWithB — 256KB → 262144
  • testAcceptsMegabyteSuffix — 10M → 10485760
  • testAcceptsMegabyteSuffixCaseInsensitive — 10mb → 10485760
  • testAcceptsGigabyteSuffix — 1G → 1073741824
  • testRejectsGibberishKeepsDefault — abc keeps the default in place

Commits split as test first, fix second per repo policy, so the GitHub PR Commits tab shows the red→green transition.

Test plan

  • new GhosttyConfigScrollbackLimitParseTests cases fail on the test-only commit, pass on the fix commit
  • existing GhosttyConfigTests cases continue to pass

Notes

  • Local tests were not run per repo policy.
  • This PR scopes itself to the parser to keep the diff small and reviewable. The companion CHANGELOG entry covering the bytes-vs-lines semantics from Fix scrollback-limit byte handling #2927 is left to the next /release pass since CHANGELOG.md currently has no [Unreleased] section.

Junghwan Shin added 2 commits April 17, 2026 17:00
Asserts that 'scrollback-limit' values written with Ghostty's K/M/G(B)
suffixes are parsed into the correct byte count. The current parser
('parseIntegerLiteral' in Sources/GhosttyConfig.swift:320-328) only
strips '_' separators, so each suffix-bearing test lands on the silent
default and fails. Documents the desired behaviour before the fix lands
in the next commit, per the project's regression-test commit policy.
Closes manaflow-ai#2950

## Root Cause

'parseIntegerLiteral' (Sources/GhosttyConfig.swift:320-328) only stripped
'_' digit-group separators. Upstream Ghostty's own configuration parser
accepts byte-typed values written with K, M, G suffixes (for example
'scrollback-limit = 10MB'); cmux returned nil and silently fell back to
the new 10 MB default introduced in manaflow-ai#2927. README advertises Ghostty
config compatibility, so the silent fallback is surprising.

## Fix

Extend 'parseIntegerLiteral' to:
- accept 'K', 'M', 'G', 'T' suffixes (case-insensitive, optional 'B')
  with the same 1024-base multipliers as Ghostty's own parser
- reject overflow on 'K/M/G/T' multiplication so a pathologically large
  value can no longer trap

The new shape preserves the previous behaviour for plain integers and
for underscore-grouped literals, so the existing call site in 'parse'
(line 256) is unchanged.

## Tests

The matching XCTest cases land in the previous commit (test-first) so
CI shows the regression coverage red->green progression.
@vercel

vercel Bot commented Apr 17, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Apr 17, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Enhanced GhosttyConfig's integer parser to accept Ghostty-style byte-size literals (whitespace trimming, _ separators, optional B, K/M/G/T multipliers with overflow detection). Added tests covering valid suffixes, separators, invalid input, and overflow behavior for scrollback-limit.

Changes

Cohort / File(s) Summary
Integer Literal Parser
Sources/GhosttyConfig.swift
Rewrote parseIntegerLiteral(_:) to: trim whitespace, remove _ digit separators, lowercase, optionally strip trailing b/B, apply K/M/G/T multipliers via bit shifts, detect and reject arithmetic overflow, and return nil for invalid/empty inputs.
Scrollback Limit Parsing Tests
cmuxTests/GhosttyConfigTests.swift
Added GhosttyConfigScrollbackLimitParseTests verifying parsing of raw integers, underscore-separated numbers, case-insensitive suffixed units (K/KB, M/MB, G, T/TB), B-only byte values, invalid inputs preservation of defaults, and overflow handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Fix scrollback-limit byte handling #2927 — Modified the same parseIntegerLiteral helper to add underscore-normalization and basic integer parsing; this PR extends that work with suffix handling and overflow checks.

Poem

🐰 I nibbled digits, stripped the underscores away,
I learned K, M, G, and T to hop and play.
No more silent defaults in the scrollback den,
Bytes counted true — hooray again! ✨

🚥 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 change: fixing the scrollback-limit parser to accept Ghostty K/M/G suffixes, which is the primary objective of the pull request.
Linked Issues check ✅ Passed The code changes fully address the primary coding-related requirements from issue #2950: accepting K/M/G/T byte suffixes (case-insensitive with optional B), using 1024 multipliers, and implementing overflow-safe multiplication.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the parser extension and corresponding unit tests as described in the PR objectives. No extraneous changes to unrelated code are present.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering summary, root cause, fix with code example, tests, test plan, and notes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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.

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

320-354: LGTM — parser logic is sound.

The walkthrough holds for the documented shapes ("10MB", "256K", "1024", "10_000_000", "abc"), the empty/"kb"/"b"-only cases correctly return nil via the guard, and multipliedReportingOverflow gives a clean overflow path. 1 << 40 is safe on Swift's 64-bit Int on macOS.

Two small notes worth considering (non-blocking):

  • Behavior change (intentional?): the magnitude >= 0 guard now rejects negative literals (e.g. "-100"), whereas the prior Int(value) path would have accepted them. Downstream clamps via max(config.scrollbackLimit, 0) anyway, so this is an improvement, but it's worth noting in the PR description / changelog since it's a small semantic shift beyond suffix support.
  • Unsupported variants: Ghostty's own parser also accepts the IEC KiB/MiB/GiB spellings on some byte-typed fields. If you want full parity, the i between digit and b will currently fall into the default branch and return nil. Easy to extend later if users report it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyConfig.swift` around lines 320 - 354, Update the PR/changelog
to note that parseIntegerLiteral now rejects negative literals (the magnitude >=
0 guard in parseIntegerLiteral) which is a behavioral change vs the previous
Int(value) path, and optionally extend parseIntegerLiteral's suffix handling
(the switch on normalized.last and the hasSuffix/removeLast logic) to accept
IEC-style "kib/mib/gib" spellings by stripping a trailing "ib"
(case-insensitive) or treating an 'i' before 'b' as part of the suffix so
"KiB"/"MiB"/etc. parse like "K"/"M"/"G"; keep overflow semantics using
multipliedReportingOverflow unchanged.
cmuxTests/GhosttyConfigTests.swift (1)

4044-4084: Consider adding overflow and bare-B coverage.

The added cases nicely document the happy paths and the gibberish fallback. Two small additions would make the suite more defensive against future regressions in parseIntegerLiteral:

♻️ Optional extra cases
+    func testRejectsOverflowingSuffixedValueKeepsDefault() {
+        // multipliedReportingOverflow must short-circuit pathological inputs
+        // like "999999999G" instead of wrapping to a negative byte count.
+        let defaultLimit = GhosttyConfig().scrollbackLimit
+        XCTAssertEqual(parsedScrollbackLimit(from: "999999999G"), defaultLimit)
+    }
+
+    func testAcceptsBareByteSuffix() {
+        // "1024B" should be treated as 1024 bytes (no K/M/G multiplier).
+        XCTAssertEqual(parsedScrollbackLimit(from: "1024B"), 1024)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/GhosttyConfigTests.swift` around lines 4044 - 4084, Add two
defensive tests to GhosttyConfigScrollbackLimitParseTests: one that verifies a
bare 'B' suffix is parsed as bytes (e.g.
XCTAssertEqual(parsedScrollbackLimit(from: "256B"), 256)) and one that verifies
an overflowing numeric literal does not change the default (use a very large
numeric string and assert it equals GhosttyConfig().scrollbackLimit). Add them
alongside the other test methods (referencing parsedScrollbackLimit and
GhosttyConfig) so parseIntegerLiteral regressions are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Around line 4044-4084: Add two defensive tests to
GhosttyConfigScrollbackLimitParseTests: one that verifies a bare 'B' suffix is
parsed as bytes (e.g. XCTAssertEqual(parsedScrollbackLimit(from: "256B"), 256))
and one that verifies an overflowing numeric literal does not change the default
(use a very large numeric string and assert it equals
GhosttyConfig().scrollbackLimit). Add them alongside the other test methods
(referencing parsedScrollbackLimit and GhosttyConfig) so parseIntegerLiteral
regressions are caught.

In `@Sources/GhosttyConfig.swift`:
- Around line 320-354: Update the PR/changelog to note that parseIntegerLiteral
now rejects negative literals (the magnitude >= 0 guard in parseIntegerLiteral)
which is a behavioral change vs the previous Int(value) path, and optionally
extend parseIntegerLiteral's suffix handling (the switch on normalized.last and
the hasSuffix/removeLast logic) to accept IEC-style "kib/mib/gib" spellings by
stripping a trailing "ib" (case-insensitive) or treating an 'i' before 'b' as
part of the suffix so "KiB"/"MiB"/etc. parse like "K"/"M"/"G"; keep overflow
semantics using multipliedReportingOverflow unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecd073bd-f138-4436-9bca-0e7065db6236

📥 Commits

Reviewing files that changed from the base of the PR and between 3f1ce4b and b6dcdee.

📒 Files selected for processing (2)
  • Sources/GhosttyConfig.swift
  • cmuxTests/GhosttyConfigTests.swift

@greptile-apps

greptile-apps Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Extends parseIntegerLiteral in GhosttyConfig.swift to accept Ghostty-style byte suffixes (K, M, G, T, with optional trailing B, case-insensitive) and adds overflow-safe multiplication, fixing silent fallback to the default when users copy configs that include values like scrollback-limit = 10MB from upstream Ghostty docs. The implementation logic is correct: the two-pass strip (b first, then k/m/g/t) handles all suffix combinations properly, overflow is guarded via multipliedReportingOverflow, and the existing if let call site preserves silent-default semantics for genuinely unparseable input.

Confidence Score: 5/5

Safe to merge; the only findings are missing test coverage for the T/TB suffix, bare-B suffix, and overflow path — none affect correctness of the shipped code.

All P0/P1 concerns are absent. The single comment is a P2 test-coverage gap for three edge cases that are implemented correctly but untested. Core logic and overflow protection are sound on the 64-bit macOS target.

cmuxTests/GhosttyConfigTests.swift — T/TB suffix, bare-B suffix, and overflow rejection are missing tests.

Important Files Changed

Filename Overview
Sources/GhosttyConfig.swift Extended parseIntegerLiteral to handle K/M/G/T byte-suffix parsing with overflow-safe multiplication; logic is correct for all covered cases
cmuxTests/GhosttyConfigTests.swift New GhosttyConfigScrollbackLimitParseTests covers the main suffix variants but is missing tests for the T/TB terabyte suffix, overflow rejection, and standalone B suffix

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["parseIntegerLiteral(value)"] --> B["trim whitespace\nstrip underscores\nlowercased()"]
    B --> C{"ends with 'b'?"}
    C -->|yes| D["removeLast() — strip 'b'"]
    C -->|no| E["unchanged"]
    D --> F{"last char?"}
    E --> F
    F -->|k| G["multiplier = 1<<10"]
    F -->|m| H["multiplier = 1<<20"]
    F -->|g| I["multiplier = 1<<30"]
    F -->|t| J["multiplier = 1<<40"]
    F -->|other| K["multiplier = 1"]
    G --> L["removeLast() — strip suffix"]
    H --> L
    I --> L
    J --> L
    K --> M["no strip"]
    L --> N{"normalized empty\nor Int() nil\nor magnitude < 0?"}
    M --> N
    N -->|yes| O["return nil → keep default"]
    N -->|no| P["multipliedReportingOverflow"]
    P -->|overflow| O
    P -->|ok| Q["return scaled Int"]
Loading

Reviews (1): Last reviewed commit: "fix: accept Ghostty K/M/G/T byte suffixe..." | Re-trigger Greptile

Comment on lines +4044 to +4085
final class GhosttyConfigScrollbackLimitParseTests: XCTestCase {
private func parsedScrollbackLimit(from value: String) -> Int {
var config = GhosttyConfig()
config.parse("scrollback-limit = \(value)\n")
return config.scrollbackLimit
}

func testAcceptsPlainIntegerValue() {
XCTAssertEqual(parsedScrollbackLimit(from: "12345"), 12345)
}

func testAcceptsUnderscoreSeparatedInteger() {
XCTAssertEqual(parsedScrollbackLimit(from: "10_000_000"), 10_000_000)
}

func testAcceptsKilobyteSuffix() {
XCTAssertEqual(parsedScrollbackLimit(from: "256K"), 256 * 1024)
}

func testAcceptsKilobyteSuffixWithB() {
XCTAssertEqual(parsedScrollbackLimit(from: "256KB"), 256 * 1024)
}

func testAcceptsMegabyteSuffix() {
XCTAssertEqual(parsedScrollbackLimit(from: "10M"), 10 * 1024 * 1024)
}

func testAcceptsMegabyteSuffixCaseInsensitive() {
XCTAssertEqual(parsedScrollbackLimit(from: "10mb"), 10 * 1024 * 1024)
}

func testAcceptsGigabyteSuffix() {
XCTAssertEqual(parsedScrollbackLimit(from: "1G"), 1024 * 1024 * 1024)
}

func testRejectsGibberishKeepsDefault() {
// Default `scrollbackLimit` is 10_000_000 (#2927). An unparseable value
// must not zero the limit out — it should leave the default in place.
let defaultLimit = GhosttyConfig().scrollbackLimit
XCTAssertEqual(parsedScrollbackLimit(from: "abc"), defaultLimit)
}
}

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 Missing test cases for T/TB suffix, overflow, and bare B suffix

The code adds explicit support for T/TB (terabyte), B-only (bare bytes), and overflow rejection via multipliedReportingOverflow, but no test exercises any of these three paths. A few additions would close the gap:

func testAcceptsTerabyteSuffix() {
    XCTAssertEqual(parsedScrollbackLimit(from: "1T"), 1 << 40)
}

func testAcceptsTerabyteSuffixWithB() {
    XCTAssertEqual(parsedScrollbackLimit(from: "1TB"), 1 << 40)
}

func testAcceptsBareBytesSuffix() {
    XCTAssertEqual(parsedScrollbackLimit(from: "100B"), 100)
}

func testOverflowKeepsDefault() {
    let defaultLimit = GhosttyConfig().scrollbackLimit
    // 9_999_999_999G overflows Int64 → should fall back to default
    XCTAssertEqual(parsedScrollbackLimit(from: "9999999999G"), defaultLimit)
}

@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="cmuxTests/GhosttyConfigTests.swift">

<violation number="1" location="cmuxTests/GhosttyConfigTests.swift:4044">
P2: New scrollback-limit parser branches (`T` suffix and overflow fallback) are added but not tested, leaving regression-prone paths unprotected.</violation>
</file>

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

Comment thread cmuxTests/GhosttyConfigTests.swift
Three reviewers (CodeRabbit, Greptile, cubic) converged on the same
gap: the new parseIntegerLiteral branches for the T suffix, the bare
'B' marker, and the multipliedReportingOverflow short-circuit were
all introduced in 'fix:' but had no XCTest coverage. Adding the
defensive cases here so a regression in any of the three branches is
caught by 'xcodebuild -scheme cmux-unit' on CI.
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
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>
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>
@teamleaderleo teamleaderleo added area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts S3: minor Wrong behavior with a workaround labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

scrollback-limit drops Ghostty unit suffixes (K/M/G); silent default after PR #2927

2 participants