* Add tab color feature to sidebar workspaces
Lets users assign a custom background color to any sidebar workspace tab
via a right-click "Tab Color" submenu. The primary motivation is working
across multiple projects simultaneously — coloring tabs by project makes
it instant to visually locate the right workspace without reading the title.
- Workspace: adds `customColor: String?` (@Published hex string) and
`setCustomColor()` setter
- TabManager: adds `setTabColor(tabId:color:)` convenience method
- ContentView: 16-color dark palette (all luminance < 0.30, white text
always readable), `Color(hex:)` extension, `coloredCircleImage(hex:)`
helper to render bitmapped NSImage circles (needed because macOS menus
strip SwiftUI foregroundColor from SF Symbols), updated `backgroundColor`
to use custom color at full/70%/35% opacity for active/inactive/
multi-selected states, "Tab Color" submenu in context menu with
"Clear Color" option, and a 1.5pt `Color.primary` border overlay on
the active tab for clear selection indication when custom colors are set
* Add workspace tab color schemes with settings and debug toggles
* Remove Kelly scheme and keep only original tab color palette
* Preserve neutral grayscale when brightening tab colors
* Harden UpdatePill UI test polling timeouts
---------
Co-authored-by: Andreas Fruth <andreas.fruth@gmail.com>
* Remove border below titlebar
Remove the 1px separator line overlay at the bottom of the custom
titlebar and its associated fakeTitlebarSeparatorColor computed property.
* Remove tab hover background in bonsplit
Update bonsplit submodule to remove the background fill on hovered
non-selected tabs.
* Restore titlebar border with system separator color, hover bg on all tabs, browser theme bg
- Add back 1px bottom border on titlebar using NSColor.separatorColor
(matches bonsplit tab separator color)
- Tab hover background now applies to all tabs including the selected one
- Browser address bar and under-page background now use Ghostty theme
background color instead of window background
* Fix file drag-and-drop and file input in browser panel (#194)
Two fixes for the browser panel:
1. File drag-and-drop from Finder: CmuxWebView previously suppressed ALL
drag type registration as a no-op to prevent bonsplit tab drags from
being intercepted. Now it selectively filters out only the text-based
types that conflict with bonsplit (public.text, public.utf8-plain-text,
public.plain-text) and the custom tab transfer types, while allowing
file URL types through so Finder drops work.
2. File <input> elements: Added the WKUIDelegate runOpenPanelWith method
to BrowserUIDelegate so clicking a file input opens the native macOS
file picker (NSOpenPanel), with support for multiple selection and
directory picking as specified by the HTML element.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(claude-opus-4-6): take a look at https://github.com/manaflow-ai/cmux/issues...
* ok
* wok
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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 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.
* 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
Nested NSHostingController layers (from bonsplit's SinglePaneWrapper)
prevent AppKit's NSDraggingDestination routing from reaching terminal
views. Install a transparent FileDropOverlayView on the window's theme
frame that intercepts file drags and forwards drops to the GhosttyNSView
under the cursor. Mouse events pass through via a hide-send-unhide
pattern.
Fix y-axis inversion in split targeting: hitTest expects coordinates in
the receiver's superview's coordinate system, not the receiver's own.
Converting to contentView's coords flipped y because NSHostingView is
flipped, causing top/bottom split drops to land in the wrong terminal.
Also adds bonsplit onFileDrop API, PaneDragContainerView, and
drop_hit_test socket command for testing coordinate-to-terminal mapping.
In fullscreen mode, the NSTitlebarAccessoryViewController buttons are hidden
with the system titlebar. This adds SwiftUI-based fullscreen controls that
appear in the sidebar area (when visible) or inline in the custom titlebar
(when sidebar is hidden), reusing the existing TitlebarControlsView component.
- Track fullscreen state via window notifications and toggle controls visibility
- Hide original titlebar accessory (isHidden + alphaValue=0) in fullscreen
- Route notification popover anchoring through fullscreen controls view model
so both button clicks and keyboard shortcuts (Cmd+Shift+I) position correctly
- Add debug titlebar spacing slider for fine-tuning leading inset
* Fix terminal keys (arrows, Ctrl+N/P) swallowed after opening browser
After a browser panel is shown, SwiftUI's internal focus system activates
and its _NSHostingView starts consuming arrow keys and other non-Command
key events via performKeyEquivalent, preventing them from reaching the
terminal's keyDown handler.
Fix: In the NSWindow performKeyEquivalent swizzle, when GhosttyNSView is
the first responder and the event has no Command modifier, route directly
to the terminal's performKeyEquivalent — bypassing SwiftUI's view hierarchy
walk entirely.
Also clear stale browserAddressBarFocusedPanelId when a terminal surface
has focus, preventing Cmd+N from being eaten by omnibar selection logic
after focus transitions away from a browser.
Adds DEBUG-only keyboard event ring buffer (KeyDebugLog) that dumps to
/tmp/cmux-key-debug.log for diagnosing future key routing issues.
* Fix split focus and Cmd+Shift+N swallowed after opening browser
Split focus: capture the source terminal's hostedView before bonsplit
mutates focusedPaneId, so focusPanel moves focus FROM the old pane
instead of from the new pane to itself. Also retry ensureFocus when the
new terminal's view has no window yet (matching the existing retry
pattern for isVisibleInUI).
Cmd+Shift+N: after WKWebView has been in the responder chain, SwiftUI's
internal focus system can intercept Command-key events in the content
view hierarchy (returning true) without firing the CommandGroup action
closure. Fix by dispatching Command-key events directly to NSApp.mainMenu
when the terminal is first responder, bypassing the broken SwiftUI path.
Also add Cmd+Shift+N to handleCustomShortcut so it's customizable and
doesn't depend on SwiftUI menu dispatch at all.
* Unified debug event log: merge key/mouse/focus into /tmp/cmux-debug.log
- Delete KeyDebugLog, MouseDebugLog, klog(), mlog() from AppDelegate
- Replace all klog/mlog calls with dlog() (provided by bonsplit)
- Remove debugLogCallback wiring from Workspace
- Add focus change logging: focus.panel, focus.firstResponder,
split.created, focus.moveFocus
- Add import Bonsplit where needed for dlog access
- Fix stale drag state on cancelled tab drags (bonsplit submodule)
* Fix split focus stolen by re-entrant becomeFirstResponder during reparenting
During programmatic splits (Cmd+D / Cmd+Shift+D), SwiftUI reparents the old
terminal view, which fires becomeFirstResponder → onFocus → focusPanel for the
OLD panel, stealing focus from the newly created pane.
Add programmaticFocusTargetPanelId guard to suppress re-entrant focusPanel
calls for non-target panels during split creation.
Also document the unified debug event log in CLAUDE.md.
* Clear stale title/favicon when browser navigation fails
When a page fails to load (e.g. connection refused), the tab was still
showing the previous page's title and favicon. Now didFailProvisionalNavigation
resets pageTitle to the failed URL and clears faviconPNGData.
* Fix Cmd+N swallowed by browser omnibar and improve split focus suppression
- Only Ctrl+N/P trigger omnibar navigation, not Cmd+N/P (Cmd+N should
always create new workspace regardless of address bar focus)
- Move split focus suppression from workspace-level guard to source:
suppress becomeFirstResponder side-effects (onFocus + ghostty_surface_set_focus)
directly on the old GhosttyNSView during reparenting, preventing both
model-level and libghostty-level focus divergence
- Remove programmaticFocusTargetPanelId from Workspace.focusPanel
* Fix omnibar hang, WebView white flash, drag-over-browser, and idle CPU spin
- Omnibar: first click selects all without entering NSTextView tracking loop;
subsequent clicks have 3s synthetic mouseUp safety net to prevent hang
- WebView: set underPageBackgroundColor to match window so new browsers don't
flash white before content loads
- Drag/drop: register custom UTType (com.splittabbar.tabtransfer) in Info.plist
so WKWebView doesn't intercept tab drags; override registerForDraggedTypes
on CmuxWebView as belt-and-suspenders
- CPU: fix infinite makeFirstResponder loop in controlTextDidEndEditing by
checking both the text field and its field editor (the actual first responder)