Addresses review feedback from https://github.com/manaflow-ai/cmux/pull/219 by resolving read-screen targets against requested workspace/surface instead of the selected workspace.
* chore(claude-opus-4-6): From HN feedback: https://news.ycombinator.com/item?id=47...
* Centralize workspace auto-reorder into addNotification
Move moveTabToTop into TerminalNotificationStore.addNotification so all
notification paths (Ghostty actions, v2 API, control socket) respect the
reorder-on-notification setting, not just the two Ghostty action sites.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the server returns a plain-text error (e.g., "ERROR: Access denied
...") before the JSON protocol starts, sendV2() would pass it through
JSONSerialization which throws a confusing NSCocoaErrorDomain 3840 error.
Now sendV2() checks for "ERROR:" prefix and surfaces the real message.
Also includes the raw response in the fallback error for easier debugging.
Fixes https://github.com/manaflow-ai/cmux/issues/188
Fixes https://github.com/manaflow-ai/cmux/issues/180 by enabling NSAllowsArbitraryLoadsInWebContent for WKWebView and adding a regression test that asserts ATS web-content override exists in Resources/Info.plist.
* Sidebar ports on own line, wider sidebar, CMUX_PORT env vars
- Move listening ports to dedicated sidebar row (removed from branch/directory line)
- Allow sidebar to resize up to 2/3 of screen width (was capped at 360px)
- Add CMUX_PORT, CMUX_PORT_END, CMUX_PORT_RANGE env vars per workspace
- Each workspace gets a dedicated port range (default: base 9100, range 10)
- Add settings UI for port base and range size
- Add portOrdinal to Workspace, monotonic counter in TabManager
Closes#129
* Make port ordinal counter static to avoid overlap across windows
Each window creates its own TabManager, so a per-instance counter
would reset and reuse port ranges. Making it static ensures unique
ranges across all windows.
* Fix portOrdinal race: pass through Workspace init instead of setting after
The first TerminalPanel is created inside Workspace.init, so setting
portOrdinal after init returns meant the initial terminal always got
ordinal 0. Pass portOrdinal as an init parameter and set it before
the TerminalPanel is created.
* Fix P2/P3: snapshot port settings at surface creation, use window screen for sidebar cap
P2: Port base/range are now snapshotted on TerminalSurface when the
panel is created, so changing settings mid-session won't cause
inconsistent CMUX_PORT values across terminals in the same workspace.
P3: Sidebar max width now uses NSApp.keyWindow?.screen instead of
NSScreen.main, so multi-monitor setups get the correct 2/3 cap for
the display the window is actually on.
* Fix P1: snapshot port base/range once per app session, not per panel
Port base and range size are now static properties on TerminalSurface,
initialized once from UserDefaults at first access. This prevents
overlapping port ranges across workspaces when settings are changed
mid-session (e.g., workspace 1 with range=10 at 9110-9119, then
range changed to 5, workspace 2 would overlap at 9110).
The decide job already skips when main HEAD matches the nightly tag,
so this only builds when there are actual changes. Hourly means users
get nightly updates within an hour of merging to main.
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).