Commit graph

1012 commits

Author SHA1 Message Date
Ismail Pelaseyed
d72b014d6d
feat: add markdown viewer panel with live file watching (#883)
* 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>
2026-03-04 17:48:28 -08:00
Qian Wan
6f210dd2c7
Fix voice dictation text insertion path in GhosttyNSView. (#857)
* Fix voice dictation text insertion path in GhosttyNSView.

* Fix AX selected text decoding to use explicit length
2026-03-04 17:25:39 -08:00
Lawrence Chen
80bbfdf206
Add custom file support for notification sounds (#869)
* Add custom-file notification sound option

* Add notification permission controls and test action

* Allow notification enable retries from settings

* Add notification permission flow debug logging
2026-03-04 17:18:07 -08:00
Lawrence Chen
28977c8e3b
Fix browser panel lifecycle after WebContent process termination (#892)
* Fix browser panel webview lifecycle after web content crashes

* Fix BrowserPanel observer lifecycle during webview replacement

* Fix WebKit termination delegate and harden lifecycle regression check
2026-03-04 16:23:22 -08:00
Yoshiki Agatsuma
76bdf7631a
Add find-in-page (Cmd+F) for browser panels (#837) (#875)
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>
2026-03-04 16:15:15 -08:00
Orkhan Rzazade
5baf0d1a3b
fix: prevent crash in parseNotificationPayload when fields are empty (#881)
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.
2026-03-04 16:05:43 -08:00
Lawrence Chen
b07532c522
Add Language setting for per-app locale override (#886)
* 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
2026-03-04 16:03:33 -08:00
Connor Callison
80eca0de48
Handle TLS authentication challenges to fix Microsoft device compliance (#806)
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>
2026-03-04 15:48:18 -08:00
Lawrence Chen
102931d975
Reduce typing lag by hiding invisible views from accessibility tree (#862)
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
2026-03-04 15:40:57 -08:00
Lawrence Chen
bd6fa9e8bc
Extract SettingsPickerRow to enforce correct picker style (#887)
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.
2026-03-04 15:04:59 -08:00
atani
2c330efb8a
feat: add Japanese localization with String Catalog (#819)
* 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
2026-03-04 14:58:28 -08:00
Lawrence Chen
422c86e822
Fix notification sound picker using wrong style in Settings (#885)
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.
2026-03-04 14:41:04 -08:00
Yoshiki Agatsuma
a4bf2214fe
Fix browser address bar Japanese IME input (#789) (#867)
* Fix browser address bar Japanese IME input (#789)

* Remove redundant comments
2026-03-04 14:32:52 -08:00
Lawrence Chen
e0ec448701
Fix Escape propagation when command palette is visible (#847)
* Fix command palette Escape propagation and add regressions

* Respect IME marked text for command palette Escape

* Harden command palette escape pending-open routing
2026-03-04 03:27:54 -08:00
Lawrence Chen
79cfe2d168
Fix cross-window theme background gating after jump-to-unread (#861)
* Fix cross-window theme background gating

* Handle owning-manager nil-selection theme edge case

* Simplify window background gating helper
2026-03-04 02:48:06 -08:00
Lawrence Chen
ace539326f
Add debug logs for Cmd+F find bar refocus (#840)
* 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.
2026-03-04 02:30:49 -08:00
Lawrence Chen
f5de515376
Add prev/next nav to blog posts, reduce index gap (#859)
* 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
2026-03-04 01:52:56 -08:00
Lawrence Chen
66384358b6
Fix video layout shift on blog post (#858) 2026-03-04 01:37:30 -08:00
Lawrence Chen
9b78ef4726
Add blog post: My Favorite Feature: Cmd+Shift+U (#852)
* 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
2026-03-04 01:35:11 -08:00
Lawrence Chen
e65bc65ac7
Fix Cmd+V clipboard image paste not working (#853)
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
2026-03-04 00:33:52 -08:00
Lawrence Chen
dad52b09d9
Cmd+P: show workspaces only (#844)
* Limit Cmd+P switcher to workspaces

* Fix command palette Enter/Escape handling and add regression test

* Scope command palette key handling to event window
2026-03-04 00:19:01 -08:00
Lawrence Chen
1f62d770c7
Skip keychain migration for DEV/staging builds (#845)
* Skip keychain migration for DEV/staging builds

Each tagged DEV build gets a unique bundle ID (com.cmuxterm.app.debug.<tag>)
with its own UserDefaults domain. This means the migration version key is
never set, so migrateLegacyKeychainPasswordIfNeeded runs on every launch.
The SecItemCopyMatching call then triggers a macOS keychain access prompt
because the ad-hoc re-signed binary doesn't match the ACL on the legacy
keychain item.

Guard the migration call so it only runs for production bundle IDs.

* Suppress keychain UI prompt in legacy password lookup

Add kSecUseAuthenticationUIFail to the SecItemCopyMatching query so macOS
fails silently instead of showing a keychain access dialog when the app's
code signing identity doesn't match the item's ACL. This closes the
remaining code path (lazy keychain fallback in configuredPassword) that
could trigger a prompt in DEV builds at runtime.

* Revert "Suppress keychain UI prompt in legacy password lookup"

This reverts commit 6453b7b5d70e713b324b0ffff7ecc4b22cc9fb5f.
2026-03-04 00:15:04 -08:00
Lawrence Chen
a5de92e9d6
Add customizable notification sound (#839)
* Add customizable notification sound setting

Adds a "Notification Sound" picker in Settings > App that lets users
choose from macOS system sounds (Default, Basso, Blow, Glass, etc.)
or silence notifications entirely with "None".

Closes https://github.com/manaflow-ai/cmux/issues/608

* Add custom notification command with env vars and sound preview

Users can set a shell command in Settings > App > Notification Command
that runs on every notification. CMUX_NOTIFICATION_TITLE,
CMUX_NOTIFICATION_SUBTITLE, and CMUX_NOTIFICATION_BODY env vars are
set. Also adds a play button to preview system sounds and docs.
2026-03-04 00:12:05 -08:00
Lawrence Chen
044a3dbb64
Vi mode: half-page scroll, visible cursor, gg fix (#851)
* Vi mode P0 improvements: half-page scroll, visible cursor, gg fix

Three changes to keyboard copy mode (vi mode):

1. Ctrl+U/D now scroll half-page (was full-page). Ctrl+B/F remain
   full-page. Uses Ghostty's scroll_page_fractional binding.

2. Entering copy mode now creates a 1-cell selection at the terminal
   cursor via select_cursor_cell, giving the user a visible cursor
   indicator. Visual mode (v) is tracked separately from Ghostty's
   has_selection so the cursor selection doesn't make every motion
   behave as visual. Exiting visual mode (v again) collapses back
   to the cursor cell.

3. Single 'g' is now a prefix key requiring 'gg' to scroll to top,
   matching standard vim behavior. Uses the same pendingG state
   machine pattern as pendingYankLine for 'yy'.

Part of https://github.com/manaflow-ai/cmux/issues/846

* Fix viewport row refresh with persistent cursor selection

The 1-cell cursor selection made refreshKeyboardCopyModeViewportRowFromVisibleAnchor
always bail (has_selection was always true). Guard on keyboardCopyModeVisualActive
instead so viewport row updates work after scrolling in non-visual mode. Also
re-creates the cursor cell after refresh to preserve visibility.

Additionally: use performBindingAction(_:repeatCount:) for scrollHalfPage,
add state-reset assertion to testGGWithSelectionAdjustsToHome.

Addresses review feedback from CodeRabbit, Cubic, Codex, and Greptile.
2026-03-03 23:56:01 -08:00
Austin Wang
c7bdd92df9
Fix Ghostty theme loading in debug builds (#830)
* Revert "Fix Cmd+Tab activation ordering for cmux windows (#744) (#766)"

This reverts commit a6f6485e3c.

* Fix debug Ghostty theme loading fallback
2026-03-03 19:14:55 -08:00
Lawrence Chen
2f6cb6ff38
Add keyboard copy mode for terminal scrollback (#792)
* 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
2026-03-03 19:01:21 -08:00
Lawrence Chen
bfe843f0bd
Clear sidebar notification when user submits prompt (#821)
* Clear sidebar notification when user submits prompt in Claude Code

Add UserPromptSubmit hook to the Claude Code wrapper that calls
`cmux claude-hook prompt-submit`. This clears the workspace notification
and sets status back to "Running" when the user addresses Claude's question,
so the "waiting for input" preview in the sidebar goes away.

Also adds --tab support to clear_notifications socket command and
--workspace support to the clear-notifications CLI command for
per-workspace notification clearing.

Closes https://github.com/manaflow-ai/cmux/issues/799

* Address review feedback: stricter error handling

- clear-notifications CLI: error on explicit --workspace failure instead of
  falling back to global clear. Env var still gracefully degrades.
- prompt-submit hook: propagate sendV1Command errors instead of swallowing
  with try?.
- clear_notifications socket: validate --tab flag is present before resolving,
  reject malformed args instead of falling back to selected tab.

* Gate env workspace fallback on windowId == nil in clear-notifications

Matches the pattern used by other CLI commands to avoid using
CMUX_WORKSPACE_ID from the caller shell when --window targets
a different window.
2026-03-03 18:48:32 -08:00
Austin Wang
355012b252
Revert "Fix Cmd+Tab activation ordering for cmux windows (#744) (#766)" (#827)
This reverts commit a6f6485e3c.
2026-03-03 18:38:24 -08:00
Lawrence Chen
a139c346f2
Fix Cmd+Shift+Enter pane zoom regression in browser focus (#826) 2026-03-03 18:37:46 -08:00
Lawrence Chen
fd31210ea4
Fix settings dropdown for Sidebar Branch Layout taking full width (#825)
The Picker was missing the controlWidth parameter that all other picker
rows in settings use, causing it to expand and hide its label.
2026-03-03 18:21:02 -08:00
Lawrence Chen
d1f4c66378
Move UNUserNotificationCenter remove calls off main thread (#820)
* Move UNUserNotificationCenter remove calls off main thread

removeDeliveredNotifications(withIdentifiers:) and
removePendingNotificationRequests(withIdentifiers:) perform synchronous XPC
to usernoted. When usernoted is slow or overwhelmed, this blocks the main
thread indefinitely, freezing the entire UI (confirmed via sample showing
__NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__ for the full sample
duration on both cmux and cmux NIGHTLY).

Add extension methods on UNUserNotificationCenter that dispatch these calls
to a background queue. All 13 call sites in TerminalNotificationStore are
fire-and-forget (void, no data flows back), so moving them off-main is safe.
The @Published property mutations and dock badge updates remain on @MainActor.

* Use dedicated serial queue for notification removal dispatch

Replaces DispatchQueue.global(qos: .utility) with a private static serial
queue. If usernoted stalls, concurrent dispatches to the global pool could
exhaust threads. A dedicated queue caps concurrency at 1.
2026-03-03 17:42:10 -08:00
Austin Wang
a12fb563ff
Fix Ghostty config reload theme refresh (#812) (#818) 2026-03-03 16:28:51 -08:00
Austin Wang
34c6250a37
Respect shell-integration=none in zsh wrapper (#816) 2026-03-03 16:22:38 -08:00
Austin Wang
bdfcc74df3
Fix Shift+backquote input regression in Ghostty key path (#815) 2026-03-03 15:57:36 -08:00
Austin Wang
a3681ede5b
ok (#717) 2026-03-03 15:53:39 -08:00
Lawrence Chen
4af2e6be30
Remove hourly cron and skipped job from nightly workflow (#817)
Every merge to main already triggers a nightly build, making the hourly
cron redundant. The skipped job was cosmetic (just echoed a message) and
caused confusing red X statuses when cancel-in-progress kicked in.
2026-03-03 15:42:09 -08:00
Lawrence Chen
5d463af122
Fix ghost terminal surface rebind after close (#808)
* Fix ghost terminal lifecycle rebind race

* Address review feedback on portal regression checks

* Address follow-up review feedback
2026-03-03 15:20:42 -08:00
Lawrence Chen
a086ebc0f3
Add @tonkotsuboy_com testimonial to wall of love (#785) 2026-03-03 00:48:01 -08:00
Lawrence Chen
37dc43a6de
Fix TCC dialog, trim black frames, add macos-26 runner option (#784)
- Grant kTCCServiceScreenCapture in system-level TCC database (sudo)
  and pre-date ScreenCaptureApprovals.plist to suppress Sequoia's
  private window picker dialog
- Move recording start to right before xcodebuild test (skip build time)
- Trim leading black frames from video using ffmpeg blackdetect
- Add runner input: macos-15 (Sequoia) or macos-26 (Tahoe)
2026-03-02 23:14:27 -08:00
Lawrence Chen
d77299c220
Clean up E2E ffmpeg device detection (#782)
* Install ffmpeg via brew for screen recording

macos-15 GitHub runners don't have ffmpeg pre-installed.

* Clean up ffmpeg device detection and add fallback

Suppress noisy device listing errors, add fallback to index 1 if
detected index fails, upgrade warning to error on total failure.
2026-03-02 22:55:03 -08:00
Lawrence Chen
63e7cc7faa
Install ffmpeg via brew for screen recording (#781)
macos-15 GitHub runners don't have ffmpeg pre-installed.
2026-03-02 22:45:22 -08:00
Lawrence Chen
3cb101f1c8
Switch screen recording from screencapture to ffmpeg AVFoundation (#780)
screencapture -v produces a 0-second file on GitHub Actions M-series
runners (IOServiceMatchingfailed for AppleM2ScalerParavirtDriver).
ffmpeg with avfoundation input handles virtual displays correctly.
2026-03-02 22:36:07 -08:00
Lawrence Chen
cd0c3cfa93
Add E2E test workflow with video recording (#778)
* Add E2E test workflow with video recording and issue posting

New workflow_dispatch workflow (test-e2e.yml) that runs XCUITests on
GitHub-hosted macos-15 runners, records the virtual display, uploads the
video as an artifact, and posts results as an issue on cmux-dev-artifacts.

Includes scripts/run-e2e.sh for convenient triggering from the terminal.

* Print issue URL in workflow annotation and run-e2e.sh output

- Capture gh issue create output URL, print as ::notice annotation
- Search issues by run ID instead of grabbing most recent
2026-03-02 22:16:38 -08:00
Lawrence Chen
fe3e2d06d9
Trigger nightly on push to main, switch to GitHub macos-15 runner (#779)
Build immediately on merge instead of waiting for the hourly cron.
Concurrency group cancels in-progress builds when new commits land.
Depot macos runner replaced with GitHub macos-15 (similar perf, simpler).
2026-03-02 22:13:03 -08:00
Lawrence Chen
56f184d02e
Add debug logging for cmd+click link handling (#777)
* Add debug logging for cmd+click link handling

Logs every decision point in the link opening pipeline:
- mouseDown/mouseUp: modifier keys, click count, position
- resolveTerminalOpenURLTarget: input URL, classification (external/embedded/fallback)
- OPEN_URL action handler: routing decision (cmuxBrowser disabled, external, whitelist miss, embedded browser pane)

All logs are #if DEBUG only via dlog().

* Update tagged build link format for Claude Code cmd+clickability

Claude Code gets a markdown link with the real derived-data path
(file:///tmp/cmux-<tag>/Build/Products/Debug/cmux%20DEV%20<tag>.app)
which is cmd+clickable in cmux. Codex keeps the original format.

* Add equal sign separators to Claude Code build link format

* Fix cmd+click URL buffer overread in OPEN_URL handler

cmux was using String(cString:) to decode the URL from
GHOSTTY_ACTION_OPEN_URL, which reads until a null terminator.
The C struct provides both a pointer and a length field (url + len),
and the pointer is not guaranteed to be null-terminated. This caused
cmux to read past the URL bytes into adjacent memory, producing
corrupted URLs like "https://example.comcom" and multi-URL
concatenations.

Fix: use Data(bytes:count:) with the length field, matching how
vanilla Ghostty decodes the same struct in Ghostty.Action.swift.

* Address review feedback: extract debugModifierString helper
2026-03-02 21:47:24 -08:00
Lawrence Chen
225588ccfc
Split CI: GitHub runners for tests, Depot for perf regression (#773)
* Split CI: GitHub runners for tests, Depot for perf regression

Unit/UI tests move to macos-15 (no queue wait, fast enough for test
suites). Typing-lag regression stays on Depot in a new tests-depot job
(needs stronger hardware). No duplicated test work between the two.

* Fix Xcode selection: add pipefail guard, use sort|tail for consistency

Address review comments:
- tests job: add || true to ls pipeline so fallback works under pipefail
- tests-depot job: use sort | tail -n 1 instead of head -n 1

* Move XCUITests from GitHub runner to Depot

tests (macos-15) now runs unit tests only. tests-depot (Depot) runs
UI tests and the typing-lag regression, reusing the same build.
2026-03-02 21:31:39 -08:00
Lawrence Chen
0cad7e0126
Reduce spacing under Changelog heading (#774)
Remove top padding on the first article and reduce the gap between
the heading and version list from 32px to 16px.
2026-03-02 20:11:47 -08:00
Lawrence Chen
919f77b6dc
Add setting to hide Cmd-hold shortcut hints (#765)
* Add setting to hide Cmd-hold shortcut hints

* Bump bonsplit for Cmd-hold pane hint toggle

* Document tagged app link format in agent notes

* Disable Ctrl pane hints when hold-hints toggle is off
2026-03-02 19:56:27 -08:00
Lawrence Chen
5bbdd87c29
Fix re-entrant exclusive-access crash in drag handle hit test (#771)
When sibling.hitTest() triggers a SwiftUI layout pass during the
drag handle's sibling walk, AppKit can call back into
windowDragHandleShouldCaptureHit before the outer invocation
finishes. This re-entry accesses SwiftUI view state that is already
held exclusively, causing a Swift runtime SIGABRT.

Add a module-level re-entrancy guard that bails out (returns false)
on nested calls to the sibling walk. Since hitTest is always called
on the main thread, a simple Bool flag is sufficient.

Crash was reproduced on macOS Sequoia 15.1.1 (24B91) in a UTM VM.
The crash stack: DraggableView.hitTest -> windowDragHandleShouldCaptureHit
-> sibling.hitTest -> SwiftUI body evaluation -> hitTest (re-entry)
-> exclusive-access violation -> SIGABRT.
2026-03-02 19:20:14 -08:00
Lawrence Chen
682a57d7db
Add workspace-churn typing lag regression and fix (#767)
* Add workspace-churn typing lag regression and fix

* Fix CI build for debug stress split calls

* Stabilize lag regression gate for low baseline latency
2026-03-02 19:06:50 -08:00