* Add typing hot path timing diagnostics
* Add stress workspace debug menu item
* Restore stress workspace preload debug path
* Reduce typing lag from sidebar re-evaluation and hitTest overhead
hitTest: gate divider/sidebar/drag routing to pointer events only,
avoiding two full view-tree walks per non-pointer event.
forceRefresh: replace per-keystroke ISO8601DateFormatter + FileHandle
I/O with dlog() in DEBUG builds.
TabItemView: replace @EnvironmentObject subscriptions with plain refs
and precomputed parameters, add Equatable conformance to skip body
re-evaluation when parent rebuilds with unchanged values. @self changed
re-evaluations dropped from 668 to 1 during rapid typing.
* Add typing-latency guardrail comments and CLAUDE.md pitfalls
Strategic comments on hitTest, TabItemView, and forceRefresh to prevent
future regressions. Adds typing-latency-sensitive paths to CLAUDE.md
pitfalls section so agents know the constraints before editing.
* Add workspace palette actions and fix release autosave typing guard
Add Move Up/Down/Top, Close Other/Above/Below, Mark Read/Unread to
Cmd+Shift+P command palette and a Workspace submenu in the menu bar.
Fix recordTypingActivity() being gated behind #if DEBUG, which prevented
release builds from honoring the typing quiet period in autosave.
* Add cmux welcome command with ASCII logo and shortcuts
Adds `cmux welcome` CLI command that prints a blue-to-purple gradient
chevron logo, version info, and key shortcuts. Auto-runs once on first
workspace creation via UserDefaults. Adds "Welcome" to the ? help menu
in the sidebar footer, which opens a new workspace running the command.
* Polish welcome, feedback, and shortcuts flows
* Add regression test for terminal notification click dismissal
* Dismiss terminal notifications on direct clicks
* Add regression for focused terminal notification ring
* Keep focused terminal notifications unread until click
* Verify direct notification dismiss triggers flash
* Use focus-flash path for direct notification dismiss
* Align notification dismiss flash with ring geometry
* Add sidebar help menu
* Fix help menu test wiring
* Fix help menu accessibility
* Use native popup for help menu
* Use icon button for sidebar help
* Add feedback composer and feedback API
* Allow preview builds without feedback env
* Tighten feedback upload limits
* Adjust sidebar footer padding
* Tighten sidebar footer spacing
* Add link affordances to help menu
* Polish sidebar feedback composer
* Move feedback icon to trailing edge
* Normalize help menu trailing icon sizes
* Enlarge help menu trailing icons
* Reduce help menu link icon size
* Shrink help menu link arrow
* Reduce help menu link arrow again
* Fix feedback message editor focus
* Add send feedback keyboard shortcut
* Polish feedback launch and delivery
* Fix orphaned child processes when closing workspace tabs
When closing a workspace tab via the sidebar X button, child processes
(login → zsh → claude) survived as orphans because TabManager.closeWorkspace()
only removed the workspace from the tabs array without explicitly freeing
Ghostty surfaces. It relied on ARC to cascade deallocation, but SwiftUI views
and Combine publishers held references, delaying or preventing
ghostty_surface_free() (which sends SIGHUP) from ever running.
This adds explicit teardown on the workspace close path:
- TerminalSurface.teardownSurface(): idempotent method to free the Ghostty
runtime surface eagerly, matching the existing deinit logic
- TerminalPanel.close() now calls teardownSurface() to ensure SIGHUP is sent
- Workspace.teardownAllPanels() iterates all panels and closes them
- TabManager.closeWorkspace() calls teardownAllPanels() before removing
the workspace from the tabs array
* Harden workspace teardown and ownership checks
* Address follow-up teardown review feedback
---------
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
JavaScript-based find using TreeWalker + <mark> highlights with
match counter, next/previous navigation, and drag-to-corner overlay
matching the existing terminal find bar.
- BrowserFindJavaScript: JS generation for search/next/prev/clear
- BrowserSearchOverlay: SwiftUI overlay with IME-safe onSubmit
- BrowserSearchState: Observable state (needle/selected/total)
- TabManager routing: Cmd+F/G dispatches to browser when focused
- Visibility filter: skips script/style/hidden/aria-hidden elements
- Stale DOM guard: isConnected check in next/previous scripts
- Navigation cleanup: clears find on didFinish and didFailNavigation
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
* Add i18n infrastructure with String Catalog and Japanese translations
Introduce String Catalog (.xcstrings) for localization support:
- Localizable.xcstrings: 195 UI string entries with en and ja translations
- InfoPlist.xcstrings: Info.plist strings (microphone usage, Finder menu items)
- project.pbxproj: add xcstrings to build phase and ja to knownRegions
* Replace hardcoded UI strings with String(localized:defaultValue:)
Migrate all user-facing strings across 11 source files to use
String(localized:defaultValue:) API (macOS 13+). Each string references
a key in Localizable.xcstrings, with the English text preserved as
defaultValue for fallback.
Files modified:
- KeyboardShortcutSettings: 28 shortcut labels
- SocketControlSettings: mode names and descriptions
- TabManager: placement labels, color names, close dialogs
- BrowserPanel/BrowserPanelView: error pages, context menus, tooltips
- UpdateViewModel/UpdatePopoverView/UpdatePill: update UI states
- NotificationsPage: notification panel labels
- SurfaceSearchOverlay: search bar placeholder and tooltips
- AppDelegate: menus, dialogs, command palette items
* Fix localization gaps from review feedback
Address review comments from CodeRabbit, Greptile, and Cubic Dev AI:
- Use interpolated String(localized:) instead of concatenation for
version/progress strings in UpdateViewModel
- Localize remaining hardcoded strings in AppDelegate: window labels,
rename dialog, status menu items, unread notification count
- Localize insecure HTTP alert body in BrowserPanel
- Add 12 new entries to Localizable.xcstrings with Japanese translations
* Fix String(localized:defaultValue:) keys to use StaticString
The localized: parameter requires StaticString when defaultValue: is
used. Move string interpolation from the key to defaultValue only,
and revert maxWidthText to plain strings since they are only used for
layout width calculation.
* Localize remaining UI strings across all source files
Add String(localized:defaultValue:) to all user-facing strings in:
- cmuxApp.swift: settings screen, menus, about panel, dialogs (~180 strings)
- ContentView.swift: command palette, sidebar context menu, dialogs (~200 strings)
- Workspace.swift: rename/move/close tab dialogs, tooltips (~20 strings)
- UpdateTitlebarAccessory.swift: titlebar tooltips, notifications popover (~10 strings)
- TerminalNotificationStore.swift: notification permission dialog (4 strings)
- CmuxWebView.swift: browser context menu items (2 strings)
- AppDelegate.swift: CLI install/uninstall alerts (6 strings)
Add 418 new entries to Localizable.xcstrings with Japanese translations.
Extract sidebar context menu into separate @ViewBuilder to fix Swift
type-checker timeout in large body.
Fix xcstrings format specifiers for interpolated strings (%lld, %@).
Total: 624 localization entries covering the full UI.
* Address review feedback: fix missing localizations and terminology
- Localize javaScriptDialogTitle URL branch in BrowserPanel
- Localize cantReach error message in BrowserPanel
- Localize close other tabs dialog message in TabManager
- Localize workspace accessibility label in ContentView
- Fix unread notification singular/plural (split into two keys)
- Fix insecure connection apostrophe inconsistency (unify to U+2019)
- Rename socketControl.fullOpen.description to socketControl.allowAll.description
- Remove dead code: renameTargetNoun function
- Fix terminology inconsistencies in xcstrings:
- Unify "Developer Tools" to デベロッパツール
- Unify "Jump to Latest Unread" phrasing
- Unify "Flash Focused Panel" terminology
- Fix dialog.enableNotifications.notNow translation
* fix: address remaining PR 819 review feedback
* fix: use a single localized key for close-other-tabs
* fix: avoid inflection markup in close-other-tabs message
* Address review feedback: localize tooltip, fix subtitle concat, unify keys
- Localize menubar tooltip unread count (hardcoded English -> localized)
- Replace subtitle string concatenation anti-pattern with single localized
keys containing interpolation placeholders
- Unify workspace fallback key to workspace.displayName.fallback
- Remove unused workspace.defaultName key from xcstrings
- Add Japanese translations for new tooltip and subtitle keys
* Add debug logs for Cmd+F find bar focus/refocus state machine
Traces the full lifecycle: menu action, startSearch, overlay mount/unmount,
focus changes, window key/resign, applyFirstResponderIfNeeded guards, and
moveFocus calls. Helps reproduce the bug where Cmd+F fails to reopen after
switching away and back to the terminal window.
* Fix Cmd+F find bar focus loss after window switch
When the find bar is open and the user switches away and back, the
window's first responder was left as the NSWindow itself because
applyFirstResponderIfNeeded bailed on the searchState guard and nothing
refocused the find bar. This caused a dead state where neither the
search field nor the terminal accepted keyboard input.
Add a SearchFocusTarget state machine (.searchField / .terminal) to
GhosttySurfaceScrollView that tracks user intent. On window-become-key,
restoreSearchFocus() makes the correct view first responder based on
the target. Pressing Escape with a non-empty needle sets target to
.terminal so window reactivation preserves that intent. Cmd+F and
.ghosttySearchFocus notifications reset target to .searchField.
* Fix multi-surface focus stealing and NSHostingView responder issue
Two bugs found from debug logs:
1. Other surfaces in the same window (without search active) were calling
applyFirstResponderIfNeeded and stealing focus from the find bar's
surface. Added a check: if current first responder is inside a search
overlay NSHostingView, don't steal it.
2. window.makeFirstResponder(overlay) on the NSHostingView was wrong.
It made the hosting view itself the responder, which ate keystrokes
as performKeyEquivalent instead of routing them to the SwiftUI
TextField inside. Removed that call, now only posting the
.ghosttySearchFocus notification to let SwiftUI handle internal
focus via @FocusState.
* Use AppKit NSTextField focus instead of SwiftUI @FocusState for search restore
The notification-only approach fails because SwiftUI @FocusState can't
propagate to AppKit when the first responder is the NSWindow itself
(no view in the responder chain to anchor the change). And making the
NSHostingView first responder eats keys as performKeyEquivalent.
Now walks the hosting view's subview tree to find the actual editable
NSTextField backing the SwiftUI TextField, and calls
window.makeFirstResponder directly on it. Falls back to notification
if the text field isn't found.
* Two-phase focus restore: AppKit + SwiftUI sync, click-to-terminal fix
restoreSearchFocus now does both:
1. AppKit: makeFirstResponder(nsTextField) so typing works immediately
2. SwiftUI: post .ghosttySearchFocus so @FocusState syncs and
.onExitCommand (Escape) and .onKeyPress (Return) still work
Also: clicking the terminal while find bar is open now sets
searchFocusTarget to .terminal, so window reactivation correctly
restores terminal focus instead of jumping back to the search field.
* Replace SwiftUI TextField with NSViewRepresentable for find bar
The core issue: SwiftUI @FocusState does not sync with AppKit's
first responder after window resign/become-key cycles. This caused
the find bar to lose all keyboard input after switching windows.
Previous attempts to bridge SwiftUI and AppKit focus (notifications,
makeFirstResponder on the backing NSTextField, belt-and-suspenders
approaches) all failed because SwiftUI event handlers (.onExitCommand
for Escape, .onKeyPress for Return) require @FocusState to be set.
Fix: replace the SwiftUI TextField with an NSViewRepresentable-wrapped
NSTextField (SearchTextFieldRepresentable), following the proven
OmnibarNativeTextField pattern already in BrowserPanelView.swift.
- Escape and Return handled via control(_:textView:doCommandBy:)
at the AppKit delegate level, no @FocusState needed
- Focus restored via .ghosttySearchFocus notification observed
directly by the Coordinator, calling makeFirstResponder immediately
- hasMarkedText() guard preserves CJK IME composition (issue #118)
- isProgrammaticMutation guard prevents text binding cursor reset
- Removes findTextField(in:) subview walk hack
* Explicitly unfocus terminal surface when find bar takes focus
The Ghostty cursor kept blinking even when the search field was focused
because ghostty_surface_set_focus(false) was only called via
surfaceView.resignFirstResponder. After window switching, the surface
view may not have been the first responder, so resign was never called.
Fix: call surface.setFocus(false) in both the .ghosttySearchFocus
notification observer and directly in restoreSearchFocus. This ensures
the cursor stops blinking regardless of previous first-responder state.
* Address review findings: field-editor guard, NSLog→dlog, stale focus
1. isSearchOverlayOrDescendant now accepts NSResponder and follows
the field-editor delegate chain back to the owning NSTextField.
Previously, when the search field was being edited, the shared
NSTextView field editor was the first responder (outside the
overlay hierarchy), so the guard missed it and other surfaces
could steal focus.
2. Converted all NSLog calls in TabManager (startSearch, hideFind,
searchSelection), cmuxApp (Find menu), and GhosttyTerminalView
(searchState didSet) to dlog() wrapped in #if DEBUG. Avoids
leaking search needle text to system logs in release builds.
3. Added isFocused re-check inside the deferred focus block in
SearchTextFieldRepresentable to prevent stale focus requests
from stealing focus back after intent has changed.
* Guard against re-focusing already-focused search field
Every keystroke updated searchState.needle (@Published), which triggered
a SwiftUI re-render → ensureFocus → restoreSearchFocus → posted
.ghosttySearchFocus notification → Coordinator called makeFirstResponder
unconditionally. makeFirstResponder on an already-editing NSTextField
ends the editing session and restarts with all text selected, so the
next typed character replaced the previous one ("hi" → "i").
Fix: check if the field is already first responder before calling
makeFirstResponder in the notification handler.
* Address review findings: stale focus target, IME guard, tab/pane gating
- Add onFieldDidFocus callback so clicking back into the search field
after Escape updates searchFocusTarget = .searchField, fixing stale
focus restoration after window switches.
- Guard updateNSView text sync with !editor.hasMarkedText() to prevent
stomping active CJK IME composition.
- Move ensureFocus search state check after tab/pane selection guards
so search focus isn't restored on non-active tabs/panes.
- Clear surfaceView.onFocus when setFocusHandler(nil) is called.
* Add keyboard copy mode for terminal scrollback
* Show vim copy mode indicator in terminal
* Fix vi copy-mode symbol keys and pending yank handling
* Refine copy-mode badge wording and font
* Rename keyboard copy-mode badge to VI MODE
* Address PR feedback for copy-mode routing and keyup handling
* Refresh copy-mode viewport row after scrolling
Cmd+Shift+Enter toggles zoom on the focused pane, expanding it to fill
the workspace. Splitting or creating new tabs auto-unzooms. Zoom state
shown as icon in sidebar tab. Includes bonsplit zoom toggle support.
Closes https://github.com/manaflow-ai/cmux/issues/136
The app sometimes launched to a frozen blank state with an empty sidebar
and no terminal loaded. This was caused by restoreSessionSnapshot emitting
intermediate @Published states (empty tabs, nil selectedTabId) that left
SwiftUI's mountedWorkspaceIds empty.
Two fixes:
1. Make restoreSessionSnapshot atomic: build the new tab list locally
before assigning to @Published properties in a single batch, so
SwiftUI observers never see an intermediate empty state.
2. Add a startup recovery timer that detects and fixes broken state
(empty tabs, invalid selection, unmounted workspaces) 500ms after
the view appears, with Sentry breadcrumbs for diagnostics.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The equalize splits command was a no-op that always returned false.
Implement it by recursively walking the bonsplit tree and setting
every split divider position to 0.5. Also register the command in
the command palette with a "workspace has splits" precondition so
it only appears when there are multiple panes.
Adds a regression test that creates a nested split layout, skews
divider positions, equalizes, and verifies all dividers are at 0.5.
Fixes https://github.com/manaflow-ai/cmux/issues/571
TabManager.updateWindowTitle() used NSApp.keyWindow to find the target
window, meaning any terminal title change (e.g. a spinner) would update
whichever window happened to be focused, not the window that owns that
TabManager. This corrupted the macOS Accessibility title attribute and
caused visible title flapping in multi-window setups.
Add a weak back-reference from TabManager to its owning NSWindow, set
by AppDelegate.registerMainWindow(), and use it instead of keyWindow.
TabManager.updateWindowTitle() used NSApp.keyWindow to find the target
window, meaning any terminal title change (e.g. a spinner) would update
whichever window happened to be focused, not the window that owns that
TabManager. This corrupted the macOS Accessibility title attribute and
caused visible title flapping in multi-window setups.
Add a weak back-reference from TabManager to its owning NSWindow, set
by AppDelegate.registerMainWindow(), and use it instead of keyWindow.