* feat(sidebar): make listening ports clickable to open in browser
Wrap each sidebar port in a Button that opens http://localhost:{port}
in the cmux built-in browser (or system browser as fallback), matching
the existing PR link click behavior.
Fixes#1602
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address bot review feedback on port clickability
- Localize port label text with String(localized:) instead of bare literal
- Add sidebar.port.label and sidebar.port.openTooltip keys to
Localizable.xcstrings with English and Japanese translations
- Respect openSidebarPullRequestLinksInCmuxBrowser user preference in
openPortLink, matching the openPullRequestLink pattern exactly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: keyboard shortcuts not working with Russian and other non-Latin layouts
When a non-Latin input source (Russian, etc.) is active, event characters
are non-ASCII. The ANSI keyCode fallback was blocked when the layout-based
translation resolved a character, leaving no safety net. Now the keyCode
fallback is always available for non-Latin layouts, matching the physical
key position — similar to Ghostty's `physical:` keybinding behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add unit tests for Russian keyboard layout shortcut matching
Two tests for Cmd+T with non-Latin (Russian) keyboard layout:
1. Layout provider returns "t" (normal ASCII fallback) — verifies
the layout-based matching path works with Cyrillic event chars.
2. Layout provider returns nil (translation failure) — verifies the
ANSI keyCode fallback catches the shortcut by physical key position.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: scope ANSI keyCode fallback to non-ASCII events, add Russian to comments
Address review feedback:
- Split !hasUsableEventChars into two precise conditions:
(hasEventChars && !eventCharsAreASCII) for non-Latin layouts, and
(!hasEventChars && layoutCharacter empty) for synthetic/empty-char events.
This prevents unintended keyCode fallback on Dvorak/Colemak with empty
synthetic events.
- Add "Russian" to the non-Latin layout list in the guard comment at line 10626.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow naming a workspace at creation time instead of requiring a
separate rename-workspace call afterward.
Threads a title parameter through:
- CLI: --name <title> flag parsed and sent as "title" in v2 params
- V2 handler: extracts title, passes to TabManager.addWorkspace()
- TabManager: uses provided title instead of auto-generated
"Terminal N" and calls setCustomTitle() to persist it
- V1 handler: accepts optional name argument
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously, GHOSTTY_ACTION_PWD used AppDelegate.shared?.tabManager to
update the current working directory, which only returns the key window's
TabManager. In multi-window scenarios, cwd updates from non-key windows
were written to the wrong TabManager, causing panelDirectories to be
empty at save time and falling back to $HOME on restore.
Fix by using tabManagerFor(tabId:) to precisely route the update to
whichever window owns the tab, ensuring all windows persist their
working directories correctly.
Fixes#2125
The Korean CJK font mapping added in PR #1017 was removed in PR #1700,
but the koreanRanges static property was left behind as dead code.
Related: #1700, #1693
applicationShouldTerminate was returning .terminateNow unconditionally,
bypassing QuitWarningSettings. Now it shows the same confirmation alert
used by handleQuitShortcutWarning, with an isQuitWarningConfirmed flag
to prevent a double dialog when the Cmd+Q shortcut path already confirmed.
Fixes#2139
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix SEO indexing: add hreflang, canonicals, sitemap per-locale entries
Google Search Console showed 380 not-indexed vs 86 indexed pages.
Root causes: missing hreflang tags on rendered pages (only in sitemap),
no canonical on homepage, inconsistent canonicals wiping parent hreflang,
sitemap only listing English URLs, trailing slash duplicates, and
_next/static chunks being crawled as pages.
Changes:
- Add buildAlternates() utility for consistent canonical + hreflang
- Add hreflang tags to all pages via alternates.languages in metadata
- Add self-referencing canonical URLs to every page (homepage had none)
- Expand sitemap to emit separate entries for each locale
- Add missing /docs/custom-commands to sitemap
- Remove skipTrailingSlashRedirect to normalize trailing slashes
- Block /_next/ in robots.txt to stop chunk crawling
* Add per-page alternates to docs sub-pages and blog index
Docs sub-pages and blog index only returned title/description in
generateMetadata, so they inherited the parent layout's alternates
(pointing to /docs or /blog). Now each page sets its own
buildAlternates() with the correct path so canonical and hreflang
point to the actual page URL.
* Derive openGraph.url from buildAlternates to avoid drift
* Redirect non-English legal pages to English, remove from sitemap
Legal pages (privacy policy, TOS, EULA) are untranslated English content.
Serving them under every locale creates 54 duplicate URLs. Now:
- Middleware 301-redirects /ja/privacy-policy etc. to /privacy-policy
- Sitemap only includes English URLs for legal pages (no locale variants)
- Legal page metadata uses static English-only canonical
* Fix legal page redirect to only match /<locale>/<page> paths
endsWith matched too broadly (e.g. /docs/eula). Now only redirects
when the path after the first segment is an exact legal page match.
* Skip next-intl for legal pages to prevent locale redirect loop
Without this, a Japanese user hitting /privacy-policy could be
redirected by next-intl to /ja/privacy-policy, which our middleware
redirects back to /privacy-policy, creating a loop.
* Rewrite legal pages to /en/ instead of NextResponse.next()
Pages live under app/[locale]/, so skipping next-intl entirely
would break route resolution. Rewrite to /en/privacy-policy etc.
so Next.js can resolve the [locale] segment correctly.
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Fix claude-teams pane anchoring: main-vertical layout + focus
Claude's agent teams sends `split-window -h` for each teammate then
`select-layout main-vertical` to stack them vertically. Three fixes:
1. Implement select-layout main-vertical: track layout state in the
tmux compat store so subsequent horizontal splits of the leader
pane get redirected to vertical splits of the right-side column.
2. Pass focus:false to surface.split from the split-window handler
so internal bonsplit focus stays on the leader pane.
3. Fix tmuxCompatStoreURL to respect $HOME env var (was using
NSString.expandingTildeInPath which ignores $HOME).
Closes https://github.com/manaflow-ai/cmux/issues/2118
* Fix claude-teams split routing: auto-seed main-vertical on first right split
The split-window routing was broken in two ways:
1. After the first teammate split (right), the main-vertical state wasn't
being created, so all subsequent splits also went right instead of
stacking down in the right column.
2. The old code only redirected splits when main-vertical was already active
AND the target was the leader surface. Claude's teams protocol targets
arbitrary panes from list-panes, not necessarily the leader.
Now the first teammate split goes right (creating the column), auto-seeds
the main-vertical state, and all subsequent splits stack downward. The
caller's surface (CMUX_SURFACE_ID) is used as the anchor regardless of
which pane Claude targets.
Also adds caller surface preference in pane target resolution so the
caller's exact surface is used when the target pane matches, preventing
stale selected-surface references after tab switches.
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* test: reproduce Cmd+N snapshot workspace lifetime race
* fix: retain snapshot workspaces through Cmd+N creation
* fix: repair workspace lifetime regression test
* fix: extract workspace config through self to avoid Xcode 16.x ARC crash
The snapshot approach (c1998e34) navigated workspace → panel → surface
through local variables. Xcode 16.4's -O ARC optimizer aggressively
elides retains on these locals through inlined call chains, causing
use-after-free on every Cmd+N in CI-built nightlies.
Fix: extract preferredWorkingDirectory and inheritedTerminalFontPoints
through self (always retained) BEFORE capturing locals. The snapshot
is now purely value-typed with no Workspace references held in locals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Dev builds should use the most permissionless socket mode so coding
agents and external tools can connect without any restrictions.
Changes automation → allowAll in the LSEnvironment plist entries and
the launch-time env vars.
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Fix update attempt refreshing pill without actually updating
The attemptUpdate() subscriber watched for .updateAvailable state to
auto-confirm, but showUpdateFound used setStateAfterMinimumCheckDelay
which delays the transition by up to 2 seconds. During that window,
dismissUpdateInstallation (from a background probe race) could cancel
the pending transition, reverting state to idle without ever confirming.
The subscriber then tore down on the transient idle, silently abandoning
the update.
Fix: move auto-confirm to the Sparkle driver level via an
autoInstallOnNextUpdate flag. When set, showUpdateFound immediately
calls reply(.install) bypassing the delay entirely. The subscriber
is kept as a fallback but no longer tears down on transient idle
while the flag is active.
Closes https://github.com/manaflow-ai/cmux/issues/2166
* Revert "Fix update attempt refreshing pill without actually updating"
This reverts commit 1cd842dd924bf114b096f222851c47d2e36ad4d9.
* Fix update attempt refreshing pill without actually updating
The attemptUpdate() subscriber tore down monitoring whenever it saw
.idle after observing progress. During check startup (retry loop,
background probe race), state can transiently return to .idle before
Sparkle's interactive check begins. The subscriber interpreted this
as "check completed" and stopped monitoring, so the auto-confirm
for .updateAvailable never fired.
Fix: add !state.isIdle to the teardown guard so monitoring only
stops on terminal failures (.notFound, .error), not transient idle.
Closes https://github.com/manaflow-ai/cmux/issues/2166
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* New window inherits size from current window
When creating a new window via Cmd+Shift+N, use the key window's
frame dimensions instead of the hardcoded 460x360 default. The new
window cascades from the existing window's position so it doesn't
stack directly on top.
* Use Ghostty's cascade algorithm for new window positioning
Match upstream Ghostty's window cascade logic: maintain a
lastCascadePoint that tracks where the next window should appear.
First window seeds the point from its own top-left corner, subsequent
windows advance the cascade point via NSWindow.cascadeTopLeft(from:).
On window close, reset the cascade point to the closing window's
position so the next window appears nearby.
New windows still inherit the key window's size so Cmd+Shift+N
creates a window matching the previous one's dimensions.
* Fix frame-to-contentRect conversion and use preferred window resolver
Convert existingFrame to a content rect via
NSWindow.contentRect(forFrameRect:styleMask:) so the new window
matches the source window's actual size instead of growing by
titlebar insets on each Cmd+Shift+N.
Use preferredMainWindowContextForWorkspaceCreation to resolve the
source window, consistent with showOpenFolderPanel and other
callers.
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
The confirm dialog showed a sanitized command (BiDi/zero-width stripped)
but executed the raw string, creating a display/consent mismatch.
Now the command is sanitized once and the same string is used for both
display and execution.
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Handle Cmd+O in handleCustomShortcut to prevent Documents folder open
Cmd+O for "Open Folder" was only handled in SwiftUI menu, which can
fail due to focus bugs when terminal is focused. This caused AppKit's
default NSDocumentController to open the Documents folder instead.
Now Cmd+O is intercepted in handleCustomShortcut like other shortcuts.
Fixes#2010
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix fallback directory loss and deduplicate Open Folder logic
Address review feedback:
1. Pass selected directory URL to fallback window creation so the
user's folder choice is not silently discarded
2. Replace inline NSOpenPanel code in cmuxApp.swift menu action
with a call to AppDelegate.showOpenFolderPanel() to avoid
future divergence between the two code paths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Set NSOpenPanel directoryURL to current terminal working directory
Address review feedback: set panel.directoryURL to the focused
terminal's working directory so Open Folder starts in a contextually
relevant location instead of AppKit's default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use shared main-window resolver and openWorkspaceForExternalDirectory in showOpenFolderPanel
Address review feedback: use preferredMainWindowContextForWorkspaceCreation
for directory seeding (works when auxiliary windows are key) and
openWorkspaceForExternalDirectory for workspace creation (ensures
shouldBringToFront and consistent fallback behavior).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Add set-color/clear-color workspace actions for tab color via CLI
Expose the existing tab color functionality through the workspace-action
CLI command, enabling programmatic tab color setting without the GUI
context menu.
Supports both named colors (Red, Blue, Amber, etc.) and hex values
(#RRGGBB). Named colors resolve against the built-in palette via
case-insensitive matching.
Usage:
cmux workspace-action --action set-color --color blue
cmux workspace-action --action set-color --color "#C0392B"
cmux workspace-action set-color Amber
cmux workspace-action clear-color
* Return explicit null color in clear_color JSON response
Restore "color": null in the clear_color response payload so JSON
consumers can distinguish "color was cleared" from "no color field".
---------
Co-authored-by: Ariel Tobiana <arieltobiana@gmail.com>
The socket API's send-key command only supported enter, escape, tab,
backspace, and ctrl+letter. Arrow keys and shift+tab had to be sent
as raw escape sequences via send_text, but 0x1B is filtered out by
socketTextChunks and converted to a standalone Escape key event,
breaking multi-byte sequences like \x1b[A (ArrowUp).
This commit extends sendNamedKey to handle:
- Arrow keys: up/down/left/right (with arrow_up/arrowup aliases)
- shift+tab / shift-tab / backtab
- home, end
- delete / del / forward_delete
- pageup / page_up
- pagedown / page_down
These map directly to the existing sendKeyEvent infrastructure using
the appropriate kVK_* keycodes and modifier flags.
Co-authored-by: I Luk Kim <yirugi@gmail.com>
* Reduce shell integration prompt latency
Three changes to cut ~10-15ms from every precmd/preexec cycle:
1. Use zsh/net/unix (zsocket) for socket sends when available. Eliminates
fork+exec of ncat/socat/nc for every telemetry send (~3ms per send,
3-4 sends per prompt cycle). Falls back to external tools if the
module is unavailable.
2. Replace _cmux_kill_process_tree (synchronous /bin/ps -ax | awk) with
direct kill in _cmux_stop_pr_poll_loop. The tree-kill enumerated all
system processes on every command (~5-13ms). Orphaned children (gh,
sleep) finish on their own within seconds.
3. Minor savings: guard _cmux_patch_ghostty_semantic_redraw after first
success, make _cmux_clear_pr_for_panel async, cache bash send tool.
* Address review: process-group kill, fix clear_pr race, reorder bash init
1. Use kill -KILL -- -$PID (process-group kill) instead of plain kill.
Background jobs are process-group leaders, so this kills all
descendants (gh, sleep) without /bin/ps overhead.
2. Keep bash _cmux_clear_pr_for_panel synchronous to prevent race
with the next report_pr from the poll loop. Zsh version uses
_cmux_send_bg which is synchronous when zsocket is available.
3. Move _cmux_detect_send_tool after _cmux_fix_path in bash so the
cached tool lookup runs with the final PATH.
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Reject invalid color values at parse time with a clear error message
instead of silently ignoring them. Colors are normalized to #RRGGBB
via WorkspaceTabColorSettings.normalizedHex during decode.
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Pre-launch app for browser UI test on headless CI runners
XCUIApplication.launch() blocks ~60s then fails on headless WarpBuild
runners because foreground activation requires a GUI login session.
Apply the same pre-launch strategy used for the display resolution test:
- CI shell launches the app with env vars before running xcodebuild
- Test detects pre-launched app via manifest, uses activate() instead of
launch() to avoid killing and relaunching the app
- Falls back to clicking the window for focus via accessibility framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert "Pre-launch app for browser UI test on headless CI runners"
This reverts commit a540e2fd99aaa1395b91a8d50caa797cdd7551b8.
* feat: cmux.json for custom commands
* tests: add cmux json tests
* fix: pr review feedback: validation, translations, input handling, and palette improvements
- Fix Danish ("Overfladedef inition") and Norwegian ("rotmapp") translation typos
- Add empty-string check for baseCwd fallback in command palette handlers
- Coalesce \r\n into single Return keypress in sendInput
- Redact command text from timeout log to prevent secret leakage
- Add decode-time validation: reject hybrid/empty commands, ambiguous layout
nodes, wrong split children count, and empty pane surfaces
- Namespace custom command IDs with "cmux.config.command." prefix
- Forward command description to palette subtitle when available
- Update tests for new validation rules and ID prefix
* fix: address PR review feedback — per-window config isolation, blank validation, ancestor walk,
palette sanitization
* fix: fallback to current dir cmux.json watching if no any cmux.json found in full acesor walk
* ci: trigger CI for fork PR
* Add directory trust for cmux.json command confirmation
The confirm dialog now shows the actual command text and has an "Always
trust commands from this folder" checkbox. When checked, future confirm
commands from that directory skip the dialog.
Trust is scoped to the git repo root if the cmux.json is inside a repo,
so trusting once covers all subdirectories. Non-git directories are
trusted by exact path. Global config is always trusted.
Trusted directories are persisted in ~/Library/Application Support/cmux/
trusted-directories.json.
* Add trusted directories section to Settings
Shows all trusted directories with per-directory revoke buttons and a
Clear All option. Placed in a "Custom Commands" section between
Automation and Browser in Settings.
* Replace trusted directories list with editable textarea
One path per line, with a Save button that activates on changes.
Users can add, remove, or edit paths directly.
* Auto-save trusted directories on edit, remove Save button
Matches the behavior of other textarea settings (browser host
whitelist, external URL patterns) which auto-save via @AppStorage.
* Sanitize command text in confirm dialog against BiDi attacks
Strip zero-width and BiDi override characters from the command preview
so the dialog shows exactly what will be executed.
---------
Co-authored-by: austinpower1258 <austinwang115@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Add Codex CLI hooks integration
Adds `cmux codex install-hooks` to install lifecycle hooks into
~/.codex/hooks.json and enable the codex_hooks feature flag. The hooks
call `cmux codex-hook <event>` which gracefully no-ops (exit 0, prints
{}) when not running inside cmux, so they're safe to leave installed
globally.
Supported events: SessionStart (session tracking), UserPromptSubmit
(set Running status), Stop (completion notification + Idle status).
Install merges with existing user hooks and is idempotent. Uninstall
(`cmux codex uninstall-hooks`) removes only cmux-owned hooks,
identified by the `cmux codex-hook` command prefix.
* Show diff and ask for confirmation before modifying user config
install-hooks and uninstall-hooks now preview changes to hooks.json and
config.toml before applying, with a [Y/n] prompt. Pass --yes/-y to
skip confirmation.
Hook commands use `command -v cmux` guard so they silently no-op
(echo '{}') when cmux CLI is not on PATH (e.g. user runs codex in a
non-cmux terminal or after uninstalling cmux).
* Improve diff output with line numbers and context
install-hooks and uninstall-hooks now show unified-diff-style output
with line numbers and surrounding context lines, making it easier to
see exactly what will change in hooks.json and config.toml.
* Check CMUX_SURFACE_ID in shell guard before calling cmux
The hook shell command now checks [ -n "$CMUX_SURFACE_ID" ] first,
so it short-circuits to echo '{}' without ever invoking cmux when
not inside a cmux terminal. Prevents usage text and socket errors
from leaking into Codex hook output.
* Uninstall reverts config.toml; fix [features] section handling
uninstall-hooks now also removes codex_hooks from config.toml and
shows the diff for both files before asking for confirmation.
buildConfigWithCodexHooks uses exact TOML key matching instead of
substring contains, and inserts after the first [features] header
only (not replacingOccurrences which hit all matches).
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
When pressing Cmd+N for a workspace number that doesn't exist,
the event was not consumed and fell through to Ghostty's goto_tab
binding, which could create a new window. Now the event is always
consumed when the digit matches, preventing unintended window creation.
Fixes#1970
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Pre-launch app for browser UI test on headless CI runners
XCUIApplication.launch() blocks ~60s then fails on headless WarpBuild
runners because foreground activation requires a GUI login session.
Apply the same pre-launch strategy used for the display resolution test:
- CI shell launches the app with env vars before running xcodebuild
- Test detects pre-launched app via manifest, uses activate() instead of
launch() to avoid killing and relaunching the app
- Falls back to clicking the window for focus via accessibility framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert "Pre-launch app for browser UI test on headless CI runners"
This reverts commit a540e2fd99aaa1395b91a8d50caa797cdd7551b8.
* Fix browser portal leaking to other tabs on Bonsplit tab switch
When switching between Bonsplit tabs within a workspace, portal-hosted
WKWebViews from deselected browser panels could remain visible above the
newly selected tab. This happened because Bonsplit's keepAllAlive mode
hides non-selected tabs via SwiftUI .opacity(0), but the portal layer
renders at the AppKit window level and is not affected by SwiftUI
opacity changes.
Explicitly hide browser portals for deselected tabs in the pane during
tab selection, ensuring the portal visibility state is always in sync
regardless of SwiftUI re-render timing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: austinpower1258 <austinwang115@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix panel resize stuttering when tiled with browser panels (#1968)
During divider drag, the portal sync system was doing O(N²) work per
frame: each geometry callback synced ALL web views, and multiple
callbacks fired per layout pass (setFrameSize + setFrameOrigin + layout).
Two changes:
1. synchronizeWebViewForAnchor now only syncs the primary web view and
defers the all-sync. Each panel fires its own geometry callback, so
secondary syncs are redundant on the hot path.
2. HostContainerView.setFrameOrigin/setFrameSize use markGeometryDirtyIfNeeded
which defers the callback to layout(), coalescing 2-3 notifications
per frame into one. An async fallback ensures origin-only changes
(without a subsequent layout) are still delivered.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix premature geometryRevision increment in markGeometryDirtyIfNeeded
Address reviewer feedback (Greptile, CodeRabbit): geometryRevision and
lastReportedGeometryState are now only updated when the callback
actually fires, not eagerly. This prevents updateNSView from seeing a
premature revision delta and triggering a redundant synchronizeForAnchor
before the coalesced notification arrives.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>