Skip to content

Reload the Ghostty config when its files change, and show config errors - #14859

Merged
teamleaderleo merged 3 commits into
mainfrom
feat/ghostty-config-live-reload
Sep 27, 2026
Merged

teamleaderleo merged 3 commits into
mainfrom
feat/ghostty-config-live-reload

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Editing ~/.config/ghostty/config (or any file it includes) now applies in cmux when you save, without pressing Cmd+Shift+, or restarting. When Ghostty rejects part of the config, release builds now tell you: a small notice in the corner of the window lists the first errors and opens the file at the offending line. Before this, those diagnostics were only written to a DEBUG log, so a typo or a missing theme silently fell back to defaults (#1304, #570).

Closes #1047.

Live reload

GhosttyConfigLiveReloadCoordinator (CmuxTerminalCore) watches:

  • every top-level config cmux can load, including ones that don't exist yet: ~/.config/ghostty/config, config.ghostty, ~/Library/Application Support/com.mitchellh.ghostty/config.ghostty and legacy config, and the cmux Application Support configs;
  • every config-file include (relative, ?-optional, nested), re-armed when the include set changes;
  • user theme files named by theme: an absolute path, or $XDG_CONFIG_HOME/ghostty/themes/<name> for each side of a light:…,dark:… pair. Bundled themes aren't watched.

Each path uses the existing FileWatcher, which also watches the nearest existing ancestor and reattaches after every event, so atomic saves, Vim-style move-aside saves and files created after launch keep working. A change arms a 300 ms trailing debounce; the evaluation reads a snapshot off the main thread and calls the existing AppDelegate.reloadConfiguration path (same as Cmd+Shift+,) only when file contents differ from the last applied snapshot. When the include or theme set changes, the watchers are re-armed and the files re-read once, so a write that lands before the new watcher attaches still reloads.

Reloads cmux starts itself (cmux themes set, Settings, the menu) refresh that baseline through .ghosttyConfigDidReload. cmux writes the file before reloading, so the write's event is normally still debouncing when the reload finishes; the refreshed baseline turns it into a no-op instead of a second reload with the generic source (which could flash a single-theme change through app appearance). The trade-off: a user edit saved after Ghostty read the files but before that baseline refresh is picked up on the next save rather than immediately.

There's no setting to turn it off; this matches cmux.json, which already reloads on save without a toggle.

Config error notice

After each full config load (launch and reload; not the soft or surface-only paths), GhosttyApp records ghostty_config_get_diagnostic messages. GhosttyConfigDiagnosticsNoticePolicy shows the notice once per distinct set of errors, stays quiet on reloads that keep the same errors (appearance changes, font zoom), hides it when a clean load arrives, and shows it again if an error comes back. Diagnostics from cmux's own inline fragments (/__cmux_inline__/…) are dropped. The notice is a borderless, non-activating child panel in the top-right of the key (or frontmost) main terminal window: it never takes focus, dismisses after 20 s, and waits for a window if the launch-time load happens before one exists. It's placed once and doesn't follow a later window resize. "Open Config" opens the first file at its line through the preferred-editor service.

cmux config doctor still validates cmux.json only: it runs without the app socket, and the CLI doesn't link GhosttyKit, so exposing Ghostty diagnostics there needs a socket method. Left as a follow-up.

Not in this PR

Single-sided light:X / dark:Y theme values (#10068) are already handled by #10358, so this PR leaves encodedThemeValue alone.

Verification

Nothing here has been compiled or run locally (this machine is at ~140 load; builds are left to CI). What ran:

  • python3 scripts/verify-local.py --affected mf/main --swift-changed mf/main: swift-syntax (22 files), xcstrings, localization parity, project normalization, test wiring, package groups, feature-flag policy all pass.
  • xcrun swiftc -parse on every changed Swift file.
  • ./scripts/localize-changes: 5 new keys with de, fr, ar, es, zh-Hant, zh-Hans, ko, ja translations; localization_catalog.py check reports 0 parity errors. No web copy changed.

Tests added (CmuxTerminalCore package, Swift Testing), expected to run in CI:

  • GhosttyConfigLiveReloadCoordinatorTests: baseline and watch set at start, reload on content change, no reload on a content-free event, a burst debounced into one evaluation (gated clock, and the superseded debounce doesn't evaluate later), cmux's own write absorbed whether its event arrives before or after the reload notification, re-arming when an include is added, and a write that lands while re-arming.
  • GhosttyConfigLiveReloadSnapshotTests: missing candidates watched, relative/optional/nested includes, include cycles, both theme sides, absolute theme paths, theme edits change contents.
  • FileWatcherGhosttyConfigChangeSourceTests: real temp files for a move-aside-and-rewrite save, an include created later in a new directory, and cancel. These are smoke checks; inode reattachment itself is covered by the existing FileWatcherTests.
  • GhosttyConfigDiagnosticsNoticePolicyTests: path/line parsing, dedupe across reloads, re-present on change, dismiss and re-present after a fix, inline/duplicate filtering, three-item cap.

A review subagent read the diff for compile and concurrency errors and found none; its findings (the self-write ordering, the re-arm window, host-window choice, first-click on the non-activating panel) are fixed in the second commit.

Unverified: app-target compilation of the AppDelegate wiring, the notice panel and its layout, and live behavior (saving a config in a running tagged build, the notice appearing, its buttons responding to a first click, and Open Config jumping to the line). Those need the tagged build and dogfood. Watching the parent directory also means unrelated writes there (for example browser_history.json next to the cmux config) cost a debounced off-main read that ends without a reload.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Editing the Ghostty config (or any file it includes) now reloads in cmux automatically when you save. When Ghostty rejects part of the config, release builds now show a notice in the window corner listing the first errors; previously these diagnostics only went to a DEBUG log, so a typo or missing theme silently fell back to defaults (#1304, #570).

Closes #1047.

Live reload

  • Watches every top-level config cmux can load (including ones that don't exist yet), every config-file include, and user themes named by theme.
  • A 300 ms trailing debounce triggers a reload only when file contents differ from the last applied snapshot.
  • Atomic saves, move-aside saves, and files created after launch are all picked up.
  • Reloads cmux starts itself (Settings, cmux themes set) refresh the baseline so their own write doesn't cause a second reload.
  • Enabled by default, matching cmux.json; there's no toggle.

Config error notice

  • Shows once per distinct error set, hides when a clean load arrives, and returns if the errors come back.
  • Non-activating borderless panel in the top-right of the main terminal window; auto-dismisses after 20 s and never takes focus.
  • "Open Config" opens the first file at the offending line in the preferred editor.
  • Diagnostics from cmux's own inline config fragments are dropped.

Written for commit 2ba2681. Summary will update on new commits.

Review in cubic

teamleaderleo and others added 2 commits September 26, 2026 12:27
Watch every file the Ghostty config reads (top-level configs, config-file
includes, user theme files) and reload through the existing Reload
Configuration path after a 300 ms trailing debounce. Evaluations compare
file contents off the main thread, so directory noise and cmux's own
writes (cmux themes set, Settings) do not trigger a second reload, while an
edit pending during a cmux-started reload is never absorbed. Watchers re-arm
when the include/theme set changes; FileWatcher already follows atomic and
move-aside saves.

After each full config load, Ghostty's diagnostics feed a policy that shows
a small non-activating notice once per distinct error set (first three
errors, Open Config jumps to the line), hides it when the errors are fixed,
and drops cmux's own inline-fragment diagnostics.

Refs #1047, #1304, #570.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…to main windows

cmux writes the config before reloading, so the write's event is usually
still debouncing when .ghosttyConfigDidReload arrives. Always refresh the
baseline on that notification so the pending evaluation is a no-op instead
of a second reload. After re-arming on a new include/theme set, read the
files once more so a write that landed before the new watcher attached
still reloads. The notice now attaches only to main terminal windows and
its buttons accept the first click on the non-activating panel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 23 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 432bf400-b315-4a10-ad33-429bd652d971

📥 Commits

Reviewing files that changed from the base of the PR and between e7f1c40 and 2ba2681.

📒 Files selected for processing (27)
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiagnostics/GhosttyConfigDiagnostic.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiagnostics/GhosttyConfigDiagnosticsNotice.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiagnostics/GhosttyConfigDiagnosticsNoticeDecision.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiagnostics/GhosttyConfigDiagnosticsNoticePolicy.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiscovery/GhosttyConfigDiscovery.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/DiscoveryGhosttyConfigLiveReloadSnapshotReader.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/FileWatcherGhosttyConfigChangeSource.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigChangeSource.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigChangeSubscription.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigDiscovery+LiveReload.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigLiveReloadCoordinator.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigLiveReloadOutcome.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigLiveReloadSnapshot.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigLiveReload/GhosttyConfigLiveReloadSnapshotReading.swift
  • Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/ConfigDiagnostics/GhosttyConfigDiagnosticsNoticePolicyTests.swift
  • Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/ConfigLiveReload/FileWatcherGhosttyConfigChangeSourceTests.swift
  • Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/ConfigLiveReload/GhosttyConfigLiveReloadCoordinatorTests.swift
  • Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/ConfigLiveReload/GhosttyConfigLiveReloadSnapshotTests.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/GhosttyApp+ConfigDiagnostics.swift
  • Sources/GhosttyConfigDiagnosticsNoticePresenter.swift
  • Sources/GhosttyConfigDiagnosticsNoticeView.swift
  • Sources/GhosttyTerminalView.swift
  • cmux.xcodeproj/project.pbxproj
  • docs/configuration.md
  • scripts/lint_auxiliary_window_close_shortcuts.py

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.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

The notice is a non-activating child panel of a main window and never
becomes key, so Cmd+W must keep closing the main window's panel rather
than the notice. Document it in the lint's ignore list like the textbox
completion popup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@teamleaderleo
teamleaderleo merged commit 3a64e0e into main Sep 27, 2026
106 of 110 checks passed
@teamleaderleo
teamleaderleo deleted the feat/ghostty-config-live-reload branch September 27, 2026 18:14
@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for 2ba2681359: every check was green at merge (22 verified; 16 skipped by policy). Full suite runs on main after merge.

rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 27, 2026
a64d59b tools: ui-lab renders view code in seconds; wire-app-sources.py (manaflow-ai#15049)
4e03ed2 fix(events): harden durable replay recovery (manaflow-ai#15054)
ac51546 Settings: native terminal theme gallery (manaflow-ai#14996)
867e7a0 Add native Ghostty option rows to Settings > Terminal (manaflow-ai#15005)
7f97b0d ui-tests: wait for static preflight when a reused compile skips the gate (manaflow-ai#15051)
c708e0c Add a chat view for the terminal's agent session (Claude Code, Codex) (manaflow-ai#14965)
b762a3d ci: the picker fetches kept bases' trees, not just checks their commits (manaflow-ai#15053)
2570eed docs: refresh and trim contributor build guidance (manaflow-ai#15050)
20019d3 ci: re-run by cause: host faults to Blacksmith, code failures back to the minis (manaflow-ai#15045)
36ee3e9 Add Warn Before Closing Workspace setting (manaflow-ai#14979)
4df2317 CI: run changed UI test classes in PRs, keep UI runs off Blacksmith, probe the GUI session (manaflow-ai#14964)
9efe05e Owned-pool sweeper: page the marker listing back to the runs it adopts (manaflow-ai#15033)
6361554 fix(events): restore durable replay across restarts (manaflow-ai#15030)
0bc5145 ci: place side lanes on the light minis one per idle side runner (manaflow-ai#15047)
9d4e92b ci: the E2E rule's queue-round reason names the owned pools the run may take (manaflow-ai#15044)
9e6e216 Dogfood the app from CI with JSON tours (manaflow-ai#14928)
fd3dcf6 ci: retry the picker's kept-base fetch and record how it went (manaflow-ai#15040)
3a64e0e Reload the Ghostty config when its files change, and show config errors (manaflow-ai#14859)
f412b05 test: hit-test the browser portal tab strip with its own click (manaflow-ai#15031)
64d5235 test: route the reopen-last-closed shortcut through the test's own window (manaflow-ai#15036)
7037079 ci: take the gui token in the E2E test job's step, not at job start (manaflow-ai#15037)

# Conflicts:
#	.github/workflows/ci-macos.yml
#	.github/workflows/ci.yml
#	.github/workflows/remote-daemon.yml
#	.github/workflows/test-e2e.yml
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.

Hot reload of Ghostty config (reload_config action / SIGUSR2)?

1 participant