Recolor the debug icon (orange DEV → purple NIGHTLY) instead of
pasting a banner onto the production icon. This preserves the exact
same chevron positioning, glow effects, and banner integration.
Follows the same pattern as AppIcon-Debug (orange DEV banner) but with
a purple banner and "NIGHTLY" text. The nightly CI workflow now passes
ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly to xcodebuild so
the nightly app gets its own distinct icon.
Includes scripts/generate_nightly_icon.py for regenerating the icons
from the production AppIcon source files.
The nightly build is now a distinct app called "cmux NIGHTLY" with
bundle ID com.cmuxterm.app.nightly, allowing side-by-side installation
with the stable release. The nightly appcast URL is baked into the
app's Info.plist by CI, so no in-app channel switching is needed.
- Nightly workflow: rename app to "cmux NIGHTLY", set bundle ID to
com.cmuxterm.app.nightly, hardcode nightly Sparkle feed URL, publish
DMG as cmux-nightly-macos.dmg
- Remove "Receive Nightly Builds" toggle from settings
- Remove UpdateChannelSettings enum and simplify feed URL resolution
to just use SUFeedURL from Info.plist
- Remove UpdateChannelSettingsTests (no longer applicable)
Replace @ObservedObject notificationStore in TerminalPanelView and
PanelContentView with a plain `let hasUnreadNotification: Bool`.
The parent WorkspaceContentView already subscribes to the store
via @EnvironmentObject; it now computes the per-panel Bool and
passes it down, so child views only re-render when their own
notification state actually changes.
TerminalPanelView and PanelContentView held notificationStore as a
plain `let` property. Since it's a reference type (class), SwiftUI's
structural diffing saw no change when notifications were added and
skipped re-evaluating the view body. Changed to @ObservedObject var
so the views properly subscribe to the store's @Published changes.
Closes#126
Tests run via SSH and aren't cmux descendants, so cmuxOnly mode
rejects the connection. Also fix socket path from /tmp/cmux.sock
to /tmp/cmux-debug.sock for debug builds.
Handle multi-button mouse events in the browser panel's WKWebView:
- Mouse back button (button 3) triggers goBack(), forward button
(button 4) triggers goForward(), enabling side-button navigation
on mice like Logitech
- Middle-click (button 2) on a link opens it in a new browser tab
by hit-testing the click position via JavaScript and routing through
the existing openLinkInNewTab mechanism
* Fix CJK IME input not working (#118)
CJK (Korean, Japanese, Chinese) IME input was completely broken because
cmux never forwarded preedit/composition state to Ghostty's libghostty.
Root causes and fixes:
1. Missing preedit sync: Added syncPreedit() that calls
ghostty_surface_preedit() to notify Ghostty about IME composition
text. Called from setMarkedText, unmarkText, and after
interpretKeyEvents in keyDown.
2. Wrong composing flag: The composing flag on key events now correctly
accounts for when composition just ended (markedTextBefore was true
but markedText is now empty), preventing spurious deletions when
canceling composition.
3. Event interception during IME: Added early exits in
performKeyEquivalent, the NSWindow swizzle, and the local event
monitor (handleCustomShortcut) to avoid stealing key events while
IME has active marked text.
4. IME popup positioning: firstRect(forCharacterRange:) now uses
ghostty_surface_ime_point() for accurate cursor-relative positioning
of the IME candidate window.
* Add regression tests for CJK IME composition (#118)
31 tests covering Korean, Japanese, and Chinese IME input scenarios:
- Korean jamo combining: ㅎ -> 하 -> 한 composition lifecycle
- Chinese pinyin: multi-letter marked text and candidate selection
- Japanese hiragana-to-kanji: romaji -> hiragana -> kanji conversion
- insertText correctly commits composed text and clears marked state
- unmarkText properly clears composition state (idempotent)
- performKeyEquivalent returns false during active composition for
all key types (plain, shift, space, return, escape)
- Shortcut bypass: hasMarkedText gates the handleCustomShortcut bypass
- Multi-syllable sequences, backspace correction, and rapid transitions
- keyTextAccumulator lifecycle tests
Also adds #if DEBUG test accessors for keyTextAccumulator on
GhosttyNSView to enable unit testing the accumulator path.
Previously, --help/-h was only checked at the top level before command
dispatch. Running e.g. `cmux new-workspace --help` would execute the
command instead of printing help.
Add per-subcommand help text for all commands that take arguments/flags.
The help check runs before socket connect so it works even when cmux is
not running. Commands without dedicated help (ping, help, list-windows,
etc.) fall through to normal behavior.
* CLI: add --command flag to new-workspace
Allows running an initial command in the new workspace's terminal:
cmux new-workspace --command "cd /path && claude"
After creating the workspace, waits 500ms for the shell to initialize,
then sends the command text via surface.send_text.
Closes#120
* CLI: error on unknown flags for new-workspace
Typos like `--comand` were silently ignored. Now reports the
unknown flag and lists known flags.
* CLI: skip --command send when workspace creation fails
If new_workspace returns an error instead of "OK <uuid>",
don't attempt to send the command (which would hit the wrong workspace).
Root cause: update-homebrew.yml triggered on release:published, which fires
before softprops/action-gh-release finishes uploading assets. The workflow
downloaded a 404 page instead of the DMG and committed its SHA.
Fix:
- Change trigger from release:published to workflow_run (fires after the
release workflow completes, guaranteeing assets are uploaded)
- Add download validation with retries and file size checks
- Add SHA verification step before committing to the cask
- Add homebrew cask update to build-sign-upload.sh for local releases
- Add regression test (tests/test_homebrew_sha.sh)
- Update /release and /release-local skills with homebrew verification steps
Fixes#110
- New /pull command: pulls main and updates submodules locally
- sync-branch: never pushes automatically, asks user first
- No pushing to submodules; all changes land via PRs
Use GhosttyFlashOverlayView (hitTest returns nil) instead of plain
NSView so the overlay doesn't steal drag/mouse routing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The drop placeholder rendered in SwiftUI was hidden behind the
portal-hosted terminal surface. Add an AppKit overlay directly on
GhosttySurfaceScrollView driven by a new paneDropZone environment key
from Bonsplit so it renders above the Metal layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move port scanning from shell to app-side with batching
Replace per-shell `ps -axo + lsof` scanning with a centralized
PortScanner singleton in the app. Each shell now sends lightweight
`report_tty` (once per session) and `ports_kick` (on preexec/precmd)
socket messages. The app coalesces kicks across all panels and runs a
single `ps -t <ttys> + lsof -p <pids>` covering every active panel.
Also fixes a macOS 26 Tahoe regression where `getsockopt(LOCAL_PEERPID)`
returns ENOTCONN on accepted sockets when the peer disconnects before
the handler thread starts. This was silently breaking ALL socket
commands sent via ncat --send-only. The fix captures the peer PID in
the accept loop immediately after accept(), and falls back to
LOCAL_PEERCRED (uid check) when the PID lookup fails.
* Fix PR review feedback: burst timing and auth comment clarity
- P2: burstDelays were accumulating (0.5+1.5+3+... = ~22.5s) instead of
firing at absolute offsets from burst start. Now uses burstStart anchor
so scans fire at 0.5s, 1.5s, 3s, 5s, 7.5s, 10s as intended.
- P1: Clarify LOCAL_PEERCRED fallback rationale — same security boundary
as socket file permissions (0600), does not widen attack surface.
Long-lived connections still get full descendant check via LOCAL_PEERPID.