When pressing Cmd+N for a workspace number that doesn't exist,
the event was not consumed and fell through to Ghostty's goto_tab
binding, which could create a new window. Now the event is always
consumed when the digit matches, preventing unintended window creation.
Fixes#1970
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Pre-launch app for browser UI test on headless CI runners
XCUIApplication.launch() blocks ~60s then fails on headless WarpBuild
runners because foreground activation requires a GUI login session.
Apply the same pre-launch strategy used for the display resolution test:
- CI shell launches the app with env vars before running xcodebuild
- Test detects pre-launched app via manifest, uses activate() instead of
launch() to avoid killing and relaunching the app
- Falls back to clicking the window for focus via accessibility framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert "Pre-launch app for browser UI test on headless CI runners"
This reverts commit a540e2fd99aaa1395b91a8d50caa797cdd7551b8.
* Fix browser portal leaking to other tabs on Bonsplit tab switch
When switching between Bonsplit tabs within a workspace, portal-hosted
WKWebViews from deselected browser panels could remain visible above the
newly selected tab. This happened because Bonsplit's keepAllAlive mode
hides non-selected tabs via SwiftUI .opacity(0), but the portal layer
renders at the AppKit window level and is not affected by SwiftUI
opacity changes.
Explicitly hide browser portals for deselected tabs in the pane during
tab selection, ensuring the portal visibility state is always in sync
regardless of SwiftUI re-render timing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: austinpower1258 <austinwang115@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix panel resize stuttering when tiled with browser panels (#1968)
During divider drag, the portal sync system was doing O(N²) work per
frame: each geometry callback synced ALL web views, and multiple
callbacks fired per layout pass (setFrameSize + setFrameOrigin + layout).
Two changes:
1. synchronizeWebViewForAnchor now only syncs the primary web view and
defers the all-sync. Each panel fires its own geometry callback, so
secondary syncs are redundant on the hot path.
2. HostContainerView.setFrameOrigin/setFrameSize use markGeometryDirtyIfNeeded
which defers the callback to layout(), coalescing 2-3 notifications
per frame into one. An async fallback ensures origin-only changes
(without a subsequent layout) are still delivered.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix premature geometryRevision increment in markGeometryDirtyIfNeeded
Address reviewer feedback (Greptile, CodeRabbit): geometryRevision and
lastReportedGeometryState are now only updated when the callback
actually fires, not eagerly. This prevents updateNSView from seeing a
premature revision delta and triggering a redundant synchronizeForAnchor
before the coalesced notification arrives.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent Japanese IME confirmation Enter from executing command
Korean IME commits a syllable and executes on a single Enter, but
Japanese/Chinese IME use Enter only to confirm conversion — a second
Enter is needed to execute. Restrict the extra Return forwarding in
shouldSendCommittedIMEConfirmKey to Korean input sources only.
* refactor: use case-insensitive check for Korean input source ID
* Fix dock icon not auto-switching with system dark mode
The automatic icon mode relied on the asset catalog to handle
appearance-based icon selection, but the compiled Assets.car does not
include dark variant renditions for AppIcon. This meant setting
applicationIconImage to nil had no effect — the icon stayed on the
light variant regardless of system appearance.
Replace the nil-reset approach with an active KVO observer on
NSApp.effectiveAppearance that programmatically swaps between
AppIconDark and AppIconLight images when in automatic mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback: fix async race and harden singleton
- Add guard in async callback to skip stale updates after stopObserving()
- Add private init() to prevent external instantiation of singleton
- Remove unused .new KVO option
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CHE-3 <schumannzheng@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When MainWindowContext.window (weak var) becomes nil after extended
uptime, resolvedWindow(for:) falls back to windowForMainWindowId()
which searches NSApp.windows by identifier. However, the recovered
window was only assigned back to context.window without reindexing
the mainWindowContexts dictionary — leaving the ObjectIdentifier key
stale. Subsequent lookups via contextForMainTerminalWindow() would
miss the context, causing addWorkspaceInPreferredMainWindow() to
return nil and Cmd+N to fall back to opening a new window.
Call reindexMainWindowContextIfNeeded() when re-resolving a window
so the dictionary key matches the current NSWindow object.
Fixes#1929
Co-authored-by: CHE-3 <schumannzheng@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Export CMUX_SOCKET alongside CMUX_SOCKET_PATH in terminal environment
The app only exported CMUX_SOCKET_PATH when setting up the terminal
environment, but some scripts and hooks (e.g. claude-hook) expect
CMUX_SOCKET. The CLI launcher code already exports both (cmux.swift
lines 9288-9289), but the app-side terminal setup was missing the
alias. This caused claude-hook stop to fail with 'TabManager not
available' when CMUX_SOCKET was empty.
Fixes#1905
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Cache socketPath to avoid redundant call
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix sidebar notification persisting after being read
latestNotification(forTabId:) fell back to latestByTabId when no
unread notifications existed, causing read notifications to persist
in the sidebar even after the user marked them as read, killed all
processes, or switched branches. The sidebar should only display
unread notifications.
Remove the fallback to latestByTabId so the sidebar notification
text clears once all notifications for a workspace are read.
Fixes#1642
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update test expectation for unread-only latestNotification semantics
After markRead, latestNotification(forTabId:) now returns nil since
it no longer falls back to read notifications. Update the test
assertion to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CHE-3 <schumannzheng@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(browser): use native value setter for React/Vue/Angular compatibility
fill, type, and select commands set input values via direct property
assignment (el.value = x), which does not trigger state updates in
frameworks that override the value setter on element instances.
Use Object.getOwnPropertyDescriptor on the prototype to call the
native setter, which bypasses the framework override and allows the
subsequent input/change events to propagate correctly through
React's synthetic event system.
Affects: browser.fill, browser.type, browser.select
* walk prototype chain instead of instanceof for cross-realm and web component support
---------
Co-authored-by: joshuaswanson <joshuaswanson@users.noreply.github.com>
* Fix all split panes appearing focused after layout restoration
Ghostty C surfaces default to focused=true (Terminal.zig), but
TerminalSurface.lastFocusState was initialized to false. During
restoration, unfocus() is called before C surfaces exist (views not in
window yet), so the ghostty_surface_set_focus(false) call was silently
dropped. When surfaces were later created, the dedup guard
(false != false) prevented the unfocus from ever reaching the renderer.
Three changes:
- Initialize lastFocusState=true to match Ghostty's default
- In setFocus(), update lastFocusState before the surface-nil guard so
the desired state is tracked even when the C surface doesn't exist yet
- In createSurface(), sync focus state after creation so surfaces that
were logically unfocused before the C surface existed get the correct
state applied
Fixes https://github.com/manaflow-ai/cmux/issues/2080
* Sync focus state unconditionally in createSurface
Per review feedback: always call ghostty_surface_set_focus(createdSurface,
lastFocusState) instead of only syncing when !lastFocusState. This avoids
coupling to Ghostty's default focused=true and is more robust if the
upstream default ever changes.
* Keep lastFocusState in sync with AppKit responder focus calls
The becomeFirstResponder, resignFirstResponder, and ctrl-key fast paths
call ghostty_surface_set_focus directly without updating lastFocusState.
This could cause the dedup guard in setFocus() to skip a needed call, or
createSurface() to replay a stale state on surface recreation.
Add recordExternalFocusState() and call it alongside each direct C focus
call so lastFocusState stays authoritative.
* Rename lastFocusState to desiredFocusState
The variable now tracks focus intent (may be set before the C surface
exists), not just the last-applied state. Update the name and doc
comment to reflect this.
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Add tty field to surface items in system.tree JSON response,
reading from existing surfaceTTYNames dictionary. Also show
tty= in the CLI text tree output for terminals that have a
registered TTY.
This enables external tools (e.g. CodeV) to cross-reference
claude process TTYs with cmux surfaces for accurate session
detection when multiple sessions share the same working directory.
Two fixes:
1. Use FileManager.temporaryDirectory for diagnostics path instead of
hardcoded /tmp/ — Process-spawned app inherits the test runner's
sandbox and can't write to /tmp/.
2. Add orderFrontRegardless() after activate() in the UI test window
creation path — on headless CI runners, activate() silently fails
and windows stay invisible, preventing terminal rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts cbb21872, 54ec524a, 10fd323b, 75375ab7, 82a16aa7 — all
attempts to fix display resolution UI test foreground activation
on CI that introduced regressions. Restores the state from the
last fully green CI run (56a4d258).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Double-clicking to select text in the terminal was causing unwanted cursor
movement because the mouse position was being updated on both the first and
second clicks. This disrupted the selection gesture and caused the cursor to
jump to a different position than intended.
Fix by only updating the mouse position on the first click (clickCount == 1),
allowing the terminal's selection logic to handle multi-click gestures without
cursor interference.
Fixesmanaflow-ai/cmux#1698
Co-authored-by: BillionClaw <267901332+BillionClaw@users.noreply.github.com>
When a pane split occurs, SwiftUI recreates host views and the portal
system rebinds the WKWebView to a new container. However, the bind path
never called BrowserWindowPortalRegistry.refresh(), so WebKit's internal
rendering state (_exitInWindow/_enterInWindow) was never cycled. This
left the WKWebView frozen in the original pane after a split.
Add refresh() calls after every portal bind that changes the host, in
three code paths: the main update path (shouldBindNow), onDidMoveToWindow,
and onGeometryChanged. The refresh is a no-op when no reattach is needed
(browserPortalNeedsRenderingStateReattach == false), so normal rendering
is unaffected.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use charactersIgnoringModifiers instead of characters when redispatching
Cmd-modified key events in performKeyEquivalent. Cmd-modified keys don't
produce text characters, so event.characters returns an empty string for
Cmd+Shift combos, preventing Ghostty from encoding them as kitty protocol
sequences. charactersIgnoringModifiers returns the actual key character
(e.g. "k" for Cmd+Shift+K) while modifiers are preserved in modifierFlags.
Fixes#1718
Co-authored-by: CHE-3 <schumannzheng@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `shouldConsumeSuppressedEscape` function had an early return that
unconditionally consumed all repeated Escape key events (`isARepeat`),
regardless of whether the suppression window had expired. This caused
Escape presses to be swallowed in TUI apps (e.g. lazygit) running in
panels, because the repeat events never reached the active responder.
Removing the `isARepeat` guard lets repeated Escapes fall through to
the existing time-based check (0.35s window), which correctly expires
and stops consuming events after the command palette is dismissed.
Fixes#1610
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Allow customizing numbered workspace and surface shortcuts
* Update bonsplit submodule to squashed main commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Publishing changes from background threads is not allowed in macOS 26
(Tahoe). Ghostty action callbacks run on I/O threads but were modifying
AppKit view properties and posting notifications without dispatching to
the main thread.
Fixes:
- GHOSTTY_ACTION_SCROLLBAR: wrap in DispatchQueue.main.async
- GHOSTTY_ACTION_CELL_SIZE: wrap in DispatchQueue.main.async
- GHOSTTY_ACTION_COLOR_CHANGE: wrap background color updates in main async
- GHOSTTY_ACTION_CONFIG_CHANGE: wrap background color clear in main async
- Add hasLiveSurface guard in TabManager.inheritedTerminalConfigForNewWorkspace
to protect the cmuxInheritedSurfaceConfig call site missed in the initial fix
- Expand malloc_size comment to clarify it is a best-effort heuristic, not a
lifetime guarantee (per Copilot review)
ghostty_surface_quicklook_font returns an unretained CTFont pointer that
can become stale on Intel Macs, leading to EXC_BAD_ACCESS (SIGSEGV) when
creating a split. This is a follow-up to the same crash pattern fixed in
#1496.
Add malloc_size validation in cmuxCurrentSurfaceFontSizePoints to detect
freed heap allocations before interpreting the pointer as a CTFont. Also
add hasLiveSurface guards in inheritedTerminalConfig and
rememberTerminalConfigInheritanceSource to skip surfaces whose native
state is closing or closed. All callers already handle nil gracefully by
falling back to inherited config values.
When a non-Latin input source like Korean 두벌식 is active,
event.charactersIgnoringModifiers returns Hangul characters (e.g. ㅅ
for T key) instead of Latin letters. This caused all character-based
shortcut matching to fail — Cmd+T, Cmd+D, Cmd+1-9, Ctrl+N/P, etc.
Root cause: KeyboardLayout.character(forKeyCode:modifierFlags:) assumed
CJK input sources lack kTISPropertyUnicodeKeyLayoutData, but Korean
두벌식 has it. UCKeyTranslate returned Korean characters and the ASCII
fallback was never reached.
Fix:
- KeyboardLayout.character(): check result is ASCII before accepting;
fall through to TISCopyCurrentASCIICapableKeyboardInputSource() when
the current source returns non-ASCII characters
- Add KeyboardLayout.normalizedCharacters(for:) helper that normalizes
event.charactersIgnoringModifiers for shortcut comparison
- Apply normalization in handleCustomShortcut (AppDelegate),
BrowserPanelView omnibar key handler, and BrowserPopupWindowController
Cmd+W handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>