The browser omnibar's updateNSView and controlTextDidEndEditing
were both dispatching makeFirstResponder calls without any guard
against re-dispatch. Each makeFirstResponder triggers SwiftUI's
FirstResponderObserver, which re-evaluates the view graph, which
calls updateNSView again, creating an infinite loop via the main
dispatch queue.
Fix: Add a pendingFocusRequest flag on the coordinator to prevent
re-dispatching while a focus/blur request is already in flight.
Also add nsView.currentEditor() != nil to the isFirstResponder
check so the field is recognized as focused during the transition
when the field editor (not the field itself) is first responder.
* Remove index-based CLI APIs and make commands workspace-relative
Migrate 14 CLI commands from v1 text protocol to v2 JSON-RPC, making them
workspace-relative via CMUX_WORKSPACE_ID env var fallback. Update 7 more
commands to use normalize functions instead of legacy resolvers. Remove dead
code (5 structs, 5 parsers, 3 functions). Add regression tests.
Commands migrated v1→v2: send, send-key, send-panel, send-key-panel,
new-split, new-pane, new-surface, close-surface, list-panes,
list-pane-surfaces, list-panels, surface-health, focus-pane, focus-panel.
Commands updated: move-workspace-to-window, list-workspaces,
close-workspace, select-workspace, trigger-flash, resolveWorkspaceId,
resolveSurfaceId.
* Fix CMUX_SURFACE_ID env fallback when --workspace is overridden
When --workspace is explicitly passed, don't fall back to CMUX_SURFACE_ID
from the caller's environment. The caller's surface belongs to a different
workspace, causing "surface not found" errors. Only use the env var fallback
when the workspace is implicit (from CMUX_WORKSPACE_ID or server default).
Affects: send, send-key, new-split, close-surface, trigger-flash, identify,
notify, claude-hook.
* Validate surface before close and respect -- option terminator in send
P1: close-surface now validates the surface handle exists before sending
surface.close, preventing silent fallback to focused surface when a stale
or mistyped ref is provided.
P2: parseOption now respects -- as an option terminator. send, send-key,
send-panel, send-key-panel strip the -- marker from payload args. This
prevents payload tokens like --workspace from being consumed as routing
flags (e.g. `cmux send -- echo --workspace foo` sends all text correctly).
* Fix test_send_workspace_relative: add to main() and send valid text
The test was never called from main() and sent empty string which would
raise immediately via _run_cli. Send a space character instead and add
the test to the main() call list.
* Respect --id-format in text output and resolve workspace refs across windows
Text-mode list commands (list-workspaces, list-panes, list-panels,
list-pane-surfaces, surface-health) now honor --id-format for plain output,
not just --json. Added textHandle() helper that picks ref/id/both based on
the selected format.
resolveWorkspaceId now enumerates all windows when resolving a workspace
ref, so notify/claude-hook work correctly in multi-window sessions where
the target workspace may be in a non-active window.
* Preserve escape-sequence semantics for send text
The v1 server unescaped \n, \r, \t in send payloads before injecting into
the terminal. The v2 surface.send_text handler sends text verbatim. Add
CLI-side unescapeSendText() to restore the same behavior: \n and \r map
to carriage return (Enter key), \t maps to tab. Applied to send and
send-panel commands.
* Reject malformed handles instead of passing through to server
All four normalize functions (window, workspace, pane, surface) now throw
a clear error for unrecognized handle formats instead of passing them
through. Previously, a typo like `--panel foo` would forward `foo` to the
server which would silently fall back to the focused surface.
* Allow cross-workspace surface refs in close-surface and strip plural ID arrays
Remove workspace-scoped pre-validation from close-surface so explicit
surface refs/UUIDs from other workspaces work without requiring
--workspace. The server resolves workspace from surface_id directly.
Malformed handles are already caught by normalizeSurfaceHandle.
Extend formatIDs to strip plural _ids/_refs array pairs (e.g.
surface_ids/surface_refs in pane.list output) based on --id-format,
matching the existing singular _id/_ref stripping behavior.
* Honor explicit --window over CMUX_WORKSPACE_ID env fallback
When --window is passed globally, skip the CMUX_WORKSPACE_ID env var
fallback so commands operate on the targeted window's selected workspace
instead of the caller's workspace from a different window. Affects all
migrated commands that use workspaceFromArgsOrEnv or inline env fallback.
Remove -Dxcframework-target=native from CI and release workflows,
defaulting to universal (matching upstream ghostty). The native target
produces a macos-arm64 xcframework slice that causes Xcode to link the
final binary differently (~70KB more __text), resulting in menubar and
right-click lag on M1 Max. The arm64 static libraries are byte-for-byte
identical between native and universal builds - the difference is purely
in how Xcode resolves the xcframework slice.
* Fix browser panel opening new tabs on every link click
Navigate target=_blank and window.open() links in the current webview
instead of spawning new browser tabs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Preserve cmd+click new tab behavior in createWebViewWith
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The -Dxcframework-target flag controls platform slices (native=macOS
only, universal=macOS+iOS), not CPU microarchitecture. Removing it
caused CI to attempt iOS builds which fail due to missing Metal
iOS toolchain on the runner.
* Fix zsh git branch refresh race after cwd change
* Clarify intentional duplicate cwd check in git refresh path
* Add Metal Toolchain download step to CI and release workflows
Fixes build failure when compiling Metal shaders for iOS xcframework
targets — the self-hosted runner needs `xcodebuild -downloadComponent
MetalToolchain` installed before `xcrun metal` can run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The self-hosted runner (M4 Mac Mini) was building GhosttyKit with
-Dxcframework-target=native, producing M4-tuned binaries. This caused
menubar and right-click lag on M1 machines. Dropping the flag defaults
to universal, which works well across all Apple Silicon chips.
- New /release-nightly: end-to-end version bump + local build + release
- Fix /release-local: source secrets directly, use signing hash to avoid
keychain ambiguity, correct create-dmg --codesign flag, export
SPARKLE_PRIVATE_KEY for appcast generation
- Add `say` notification on completion/failure to all release skills
The hide-send-unhide pattern in forwardEvent() can recurse infinitely
when gesture recognizer routing re-delivers the event despite isHidden.
Add a re-entrancy guard to break the cycle.
Fixes EXC_BAD_ACCESS (stack overflow) crash in production.
The shim always injected --session-id with a fresh UUID, which broke
`claude --resume <id>` and `claude --continue` by conflicting with the
user's session flag. Now scans args and skips injection when the user
already specifies a session/resume flag.
Also passes through subcommands (mcp, config, api-key) without injecting
hooks or session flags since they don't support them.
* Fix sidebar drag-and-drop broken by FileDropOverlayView
The FileDropOverlayView (added in 9fd3cc2) sits on the window's theme
frame above the content view. Its hitTest returned self for all events,
causing AppKit to route drag sessions to the overlay instead of the
content view where SwiftUI lives. AppKit walks UP the superview chain
from the hit-tested view, never checking siblings — so SwiftUI's
.onDrop handlers for sidebar tab reordering were never reached.
Three changes fix this:
1. Smart hitTest: check NSPasteboard(name: .drag) for .fileURL and only
return self during Finder file drags. Return nil otherwise so mouse
events and internal drags pass through to the content view.
2. Custom UTType for sidebar drags: replace the fragile UTType.plainText
hack with a proper com.cmux.sidebar-tab-reorder type registered in
Info.plist. Uses visibility: .ownProcess since it's internal-only.
3. Narrow overlay registration: only register for .fileURL instead of
.fileURL + .URL + .string. The broad .string type collided with
text-based drag payloads.
* Add custom UTType Info.plist pitfall to CLAUDE.md
- Delete docs-site/ (superseded by web/app/docs)
- Add posthog-js with Vercel reverse proxy at /cdata to bypass adblockers
- Track pageviews (SPA-aware), download clicks (hero/navbar/mobile_drawer),
and GitHub link clicks (hero/navbar/mobile_drawer/footer)
Three bonsplit commits (429af82, 2ff740d, b1948ab) adding the tab bar
+ button were lost when ad159da moved the submodule pointer to a
branch that forked before those commits. Merge origin/term-browser-icons
back into bonsplit main to restore them.
migrateMode() had no case for "allowAll" rawValue, so it fell
through to the default branch which returned .cmuxOnly. This
silently downgraded any persisted allowAll setting.
Replace the hero screenshot and add a visual features table showing
notification rings, notification panel, in-app browser, and vertical
tabs & splits with per-feature screenshots.
* Add features table with images to README
Replace the hero screenshot and add a visual features table showing
notification rings, notification panel, in-app browser, and vertical
tabs & splits with per-feature screenshots.
* Rename feature heading to "Vertical + horizontal tabs"
* Remove parenthetical from notification rings description
* Socket access control: process ancestry check + file permissions
Redesign socket control modes from (off, notifications, full) to
(off, cmuxOnly, allowAll):
- cmuxOnly (default): uses LOCAL_PEERPID + sysctl process tree walk to
verify the connecting process is a descendant of cmux. External
processes (SSH, other terminals) are rejected.
- allowAll: hidden mode accessible only via CMUX_SOCKET_MODE=allowAll
env var, skips ancestry check. Legacy "full"/"notifications" env
values map here for backward compat.
- off: disables socket entirely.
Security hardening:
- Server: chmod 0600 on socket after bind (owner-only access)
- CLI: stat() ownership check before connect (reject fake sockets)
Removes per-command allow-list (isCommandAllowed) — once a process
passes the ancestry check, all commands are available.
Includes migration for persisted UserDefaults values and env var
aliases (cmux_only, cmux-only, allow_all, allow-all).
* Add /sync-branch skill for submodule + main sync