* Return browser screenshot image URL
* Make screenshot path/url best effort
* cli: omit screenshot png_base64 from json output
* browser wait: fail fast on js errors and include screenshot in help
* browser wait: avoid main-actor default world warning
* tests: scope contentWorld regression check to function signature
* browser screenshot: clean up output handling and tests
* browser wait: resolve snapshot refs in selector waits
Agents were following CLAUDE.md instructions to run bare xcodebuild
without -derivedDataPath, producing untagged cmux DEV.app that shares
the default debug socket and steals window focus.
Replace bare xcodebuild instruction with reload.sh --tag. Replace
E2E/Basic tests sections with a unified testing policy that forbids
local test runs and requires tagged builds.
* Fix sidebar branch refresh after checkout
* Fix bash PR probe not refreshing on checkout (PR review feedback)
When HEAD changes (e.g. git checkout), the bash integration now resets
_CMUX_PR_LAST_RUN=0 so the PR probe is forced to re-run immediately.
This matches the zsh integration which already sets _CMUX_PR_FORCE=1
on HEAD change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Locale initialization on the main thread (os.locale.ensureLocale /
NSLocale._preferredLanguages) can race with Sentry's background
init thread calling posix.getenv, causing a SIGSEGV and leaving
the SDK disabled.
Related to #836
* fix: avoid NSTextView tracking loop in omnibar mouseDown (#917)
Replace the synthetic mouseUp timeout workaround with direct cursor
positioning via NSTextView.characterIndexForInsertion(at:). The previous
approach posted a fake mouseUp event via NSApp.postEvent after 3 seconds,
but the NSTextView tracking loop does not always dequeue events from the
application event queue when stuck in an infinite
NSTextLayoutManager.enumerateTextLayoutFragments cycle, so the hang
persisted.
The new approach bypasses super.mouseDown entirely when the field editor
is already active, positioning the cursor (or extending the selection
with Shift+click) without entering the tracking loop. Drag-to-select is
not supported in this code path, but for a single-line omnibar this is
an acceptable trade-off.
* fix: handle double-click, UTF-16 length, and shift-click anchor
Address review feedback:
- Forward double/triple-click events to editor.mouseDown(with:) to
preserve word and line selection without entering NSTextField's
tracking loop
- Use (editor.string as NSString).length instead of String.count for
NSRange clamping (NSRange uses UTF-16 indices)
- Track shift-click anchor independently via shiftClickAnchor property
to correctly handle bidirectional selection extension
* fix: reset shiftClickAnchor on keyDown to prevent stale anchor
Clear the shift-click selection anchor whenever a key is pressed, so
that keyboard navigation (arrow keys, Shift+arrow, Home/End, etc.)
properly invalidates the mouse-originated anchor. A subsequent
Shift+click will then use the current selection position as anchor
instead of a stale value from a prior mouse interaction.
* fix: reset shiftClickAnchor in performKeyEquivalent and on re-focus
Key equivalents (Cmd+A, Cmd+V, etc.) bypass keyDown and go through
performKeyEquivalent, so the anchor must also be cleared there.
Similarly, re-focusing the field (currentEditor() == nil path) should
reset the anchor since selectAll changes the selection state.
During IME composition (e.g. Japanese input), Ctrl+H should delete
composing characters via the IME, not bypass it and send a backspace
directly to the terminal. Add a hasMarkedText() check so the fast path
is only taken when no IME composition is active, letting
interpretKeyEvents() handle the key instead.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
* Fix Claude wrapper hook injection when cmux socket is stale
* Harden socket listener lifecycle and rearm policy
* Unset CLAUDECODE in stale-socket passthrough
* Harden listener cleanup and bound claude ping probe
* Guard socket unlink during listener startup window
* Add localization for 16 new languages
Add translations for all 637 UI string keys and 3 InfoPlist keys in:
ar, bs, da, de, es, fr, it, ko, nb, pl, pt-BR, ru, th, tr, zh-Hans, zh-Hant
Update AppLanguage enum and knownRegions to include all 18 languages.
Total supported languages: en, ja + 16 new = 18.
* Reorder languages: English first, rest alphabetical, explicit display names
Use "Chinese Simplified" / "Chinese Traditional" naming. Show native script
with English name in parentheses for non-Latin languages.
* Add Chinese native characters to language display names
* Delay language restart dialog until picker dropdown closes
* Fix Arabic bidi rendering in language picker with LTR mark
* Defer AppleLanguages write to app launch, fix picker animation lag
Writing AppleLanguages to UserDefaults triggers synchronous locale
recalculation on the main thread, causing the picker dropdown dismiss
animation to stutter. Since a restart is already required, move the
AppleLanguages write to init() on next launch instead of onChange.
* Fix reset path and add apply() back to delayed onChange
- resetAllSettings() now calls LanguageSettings.apply(.system) and
shows restart alert if language was changed from launch value
- onChange also calls apply() inside the 0.3s delay block, so
AppleLanguages is set before restart (avoids two-restart issue)
- init() still calls apply() as belt-and-suspenders for launch
* Fix deferred alert race: re-check current language in closure
If user changes language then changes back within 0.3s, the stale
closure would fire with the old value. Now reads current appLanguage
inside the closure instead of capturing newValue.
* Fix Spanish and Danish translations: restore missing diacritics
Spanish was missing all áéíóúñ characters (now 315 diacritics).
Danish was missing all æøå characters (now 401 diacritics).
Both languages fully retranslated with correct orthography.
* Fix reset restart alert: compare new value against launch language
Was comparing previousLanguage against languageAtLaunch, which would
miss the case where user launched in Spanish and reset to System
(Spanish != Spanish = false, so no alert). Now compares the new
appLanguage (system) against languageAtLaunch.
* Add markdown viewer panel with live file watching
Introduce a new PanelType.markdown that renders .md files in a dedicated
panel using MarkdownUI (SwiftUI), with live file watching via DispatchSource
so content auto-updates when the file changes on disk.
- New MarkdownPanel class with file system watcher (write/delete/rename/extend)
- New MarkdownPanelView with custom cmux theme (headings, code blocks, tables,
blockquotes, inline code, lists, horizontal rules, light/dark mode)
- Full workspace integration: SurfaceKind, creation methods, tab subscription
- Session persistence: snapshot/restore across app restarts
- V2 socket command: markdown.open (validates path, resolves workspace, splits)
- CLI command: cmux markdown open <path> with routing flags and help text
- Agent skill: skills/cmux-markdown/ with SKILL.md, openai.yaml, and references
- Cross-link from skills/cmux/SKILL.md to the new markdown skill
- SPM dependency: gonzalezreal/swift-markdown-ui 2.4.1
* Fix unreachable guard in markdown subcommand dispatch
Use looksLikePath() to distinguish subcommands from path arguments
so the guard can catch unknown subcommands and future subcommands
are parsed correctly.
* Use .isoLatin1 fallback instead of .ascii for encoding recovery
ASCII is a strict subset of UTF-8, so falling back to .ascii after
UTF-8 fails is dead code. Use .isoLatin1 which accepts all 256 byte
values and covers legacy encodings like Windows-1252.
* Mark fileWatchSource as nonisolated(unsafe) for deinit safety
deinit is not guaranteed to run on the main actor, so accessing
@MainActor-isolated storage is a data race under strict concurrency.
DispatchSource.cancel() is thread-safe, so nonisolated(unsafe) is
sufficient with a documented invariant that writes only occur on main.
* Fix file watcher reattach: retry loop with cancellation guard
- Replace one-shot 500ms retry with up to 6 attempts (3s total window)
so files that reappear after a slow atomic replace are picked up
- Add isClosed flag checked before each retry to prevent restarting
the watcher after close()/deinit
* Harden path validation in markdown.open command
Reject directories and non-absolute paths before panel creation
to prevent ambiguous behavior and generic downstream failures.
* Always reattach file watcher on delete/rename events
After an atomic save (delete old + create new), the DispatchSource still
points to the old inode. Previously we only reattached when the file was
unreadable, so successful atomic saves left the watcher on a stale inode
and live updates silently stopped. Now we always stop and reattach:
immediately if the new file is readable, via retry loop if not.
* Restore markdown panels even when file is missing at launch
MarkdownPanel already handles unavailable files gracefully (shows
'file unavailable' UI and retries via the reattach loop). Dropping
the panel on restore lost the user's layout for files that may
reappear shortly after (network drives, build artifacts, etc.).
* Harden markdown CLI parsing and startup reconnect behavior
---------
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>
Swift's split(separator:) omits empty subsequences by default, so a
payload like "||" or "||body" produces an empty or misaligned array.
Accessing parts[0] unconditionally then triggers an out-of-bounds trap
(EXC_BREAKPOINT / SIGTRAP).
Two changes:
1. Pass omittingEmptySubsequences: false to preserve field positions
across the pipe delimiters, so "title||body" correctly yields
["title", "", "body"] instead of ["title", "body"].
2. Guard parts[0] with a bounds check, consistent with how parts[1]
and parts[2] are already accessed.
Reproduces when cmux notify is called with empty --title or via
Claude Code's Notification hook where env vars may be empty.
* Add Language setting to Settings for per-app locale override
Uses UserDefaults AppleLanguages to override locale without changing
macOS system language. Picker shows System/English/Japanese with a
restart prompt when the selection changes.
* Address review feedback: guard relaunch and reset behavior
- Guard relaunchApp() against Process launch failure (don't terminate
if the new instance didn't start)
- Prevent restart dialog from firing during Reset All Settings
* Add localization requirement to CLAUDE.md
All user-facing strings must use String(localized:) with keys in
Localizable.xcstrings and translations for all supported languages.
* Fix relaunch: use detached shell so open survives app exit
The previous approach spawned open as a child process that could get
killed when the parent terminated. Now spawns a shell with sleep+open
that outlives the current process.
* Fix shell injection risk and SwiftUI state batching in language setting
- Pass bundle path via environment variable instead of interpolating
into shell command string
- Defer isResettingSettings=false to next run loop tick so onChange
handler reliably sees the guard during Reset All Settings
WKWebView rejects all authentication challenges by default when
webView(_:didReceive:completionHandler:) is not implemented, using
.rejectProtectionSpace. This silently breaks TLS client-certificate
flows like Microsoft Entra ID Conditional Access, which verifies
device compliance via a certificate stored in the system keychain
by MDM enrollment.
By implementing the delegate method and returning
.performDefaultHandling, the system's standard URL-loading behaviour
takes over: the keychain is searched for matching client identities,
MDM-installed root CAs are trusted, and any configured SSO extensions
(e.g. Microsoft Enterprise SSO) can intercept the challenge.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Profiling shows the main thread spends ~24% of time in
AccessibilityViewGraph.needsUpdate walking invisible SwiftUI views
during every layout pass, blocking key event dequeue.
- Add .accessibilityHidden on inactive workspaces in the ZStack
(opacity-0 but still walked by the accessibility subsystem)
- Add .accessibilityHidden on NotificationsPage and the tabs
container when their respective selection is not active
- Mark GhosttyTerminalView's HostContainerView (empty portal
placeholder) as non-accessible since the terminal surface
lives in the AppKit portal layer above SwiftUI
Every settings picker needs .pickerStyle(.menu), controlWidth, and
.labelsHidden(). Missing any of these causes layout bugs (like the
notification sound picker rendering as an expanded control). This
new SettingsPickerRow component bakes in the correct modifiers so
future pickers get them by construction. Migrates all 8 existing
settings pickers.
* 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
The notification sound picker was missing .pickerStyle(.menu) and
controlWidth, causing it to render as an expanded picker with a large
empty area instead of a compact dropdown menu. Apply the same pattern
used by all other settings pickers.
* 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 prev/next navigation to blog posts, reduce index gap
* Add download/GitHub CTA to all blog posts via layout
* Track blog post slug in PostHog download/GitHub click events
* Add blog post about Cmd+Shift+U (Jump to Latest Unread)
* Rewrite blog post to remove AI rhetorical patterns
* Add video, trim post to two paragraphs
Ghostty's keybinding system intercepts Cmd+V and routes it through
read_clipboard_cb, which only reads text. The paste(_:) NSResponder
method with image handling was never reached.
Move clipboard image save logic into GhosttyPasteboardHelper and call
it from read_clipboard_cb when the clipboard has no text but has image
data. This makes image paste work regardless of whether paste is
triggered via Ghostty keybinding (Cmd+V) or menu action (Edit > Paste).
Fixes regression from https://github.com/manaflow-ai/cmux/pull/562