cmux/CLAUDE.md

7.7 KiB

cmux agent notes

Initial setup

Run the setup script to initialize submodules and build GhosttyKit:

./scripts/setup.sh

Local dev

After making code changes, always run the reload script with a tag to launch the Debug app:

./scripts/reload.sh --tag fix-zsh-autosuggestions

After making code changes, always run the build:

xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' build

When rebuilding GhosttyKit.xcframework, always use Release optimizations:

cd ghostty && zig build -Demit-xcframework=true -Doptimize=ReleaseFast

When rebuilding cmuxd for release/bundling, always use ReleaseFast:

cd cmuxd && zig build -Doptimize=ReleaseFast

reload = kill and launch the Debug app only (tag required):

./scripts/reload.sh --tag <tag>

reloadp = kill and launch the Release app:

./scripts/reloadp.sh

reloads = kill and launch the Release app as "cmux STAGING" (isolated from production cmux):

./scripts/reloads.sh

reload2 = reload both Debug and Release (tag required for Debug reload):

./scripts/reload2.sh --tag <tag>

For parallel/isolated builds (e.g., testing a feature alongside the main app), use --tag with a short descriptive name:

./scripts/reload.sh --tag fix-blur-effect

This creates an isolated app with its own name, bundle ID, socket, and derived data path so it runs side-by-side with the main app. Important: use a non-/tmp derived data path if you need xcframework resolution (the script handles this automatically).

Before launching a new tagged run, clean up any older tags you started in this session (quit old tagged app + remove its /tmp socket/derived data).

Debug event log

All debug events (keys, mouse, focus, splits, tabs) go to a unified log in DEBUG builds:

tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug.log)"
  • Untagged Debug app: /tmp/cmux-debug.log

  • Tagged Debug app (./scripts/reload.sh --tag <tag>): /tmp/cmux-debug-<tag>.log

  • reload.sh writes the current path to /tmp/cmux-last-debug-log-path

  • Implementation: vendor/bonsplit/Sources/Bonsplit/Public/DebugEventLog.swift

  • Free function dlog("message") — logs with timestamp and appends to file in real time

  • Entire file is #if DEBUG; all call sites must be wrapped in #if DEBUG / #endif

  • 500-entry ring buffer; DebugEventLog.shared.dump() writes full buffer to file

  • Key events logged in AppDelegate.swift (monitor, performKeyEquivalent)

  • Mouse/UI events logged inline in views (ContentView, BrowserPanelView, etc.)

  • Focus events: focus.panel, focus.bonsplit, focus.firstResponder, focus.moveFocus

  • Bonsplit events: tab.select, tab.close, tab.dragStart, tab.drop, pane.focus, pane.drop, divider.dragStart

Pitfalls

  • Custom UTTypes for drag-and-drop must be declared in Resources/Info.plist under UTExportedTypeDeclarations (e.g. com.splittabbar.tabtransfer, com.cmux.sidebar-tab-reorder).
  • Do not add an app-level display link or manual ghostty_surface_draw loop; rely on Ghostty wakeups/renderer to avoid typing lag.
  • Submodule safety: When modifying a submodule (ghostty, vendor/bonsplit, etc.), always push the submodule commit to its remote main branch BEFORE committing the updated pointer in the parent repo. Never commit on a detached HEAD or temporary branch — the commit will be orphaned and lost. Verify with: cd <submodule> && git merge-base --is-ancestor HEAD origin/main.

Socket command threading policy

  • Do not use DispatchQueue.main.sync for high-frequency socket telemetry commands (report_*, ports_kick, status/progress/log metadata updates).
  • For telemetry hot paths:
    • Parse and validate arguments off-main.
    • Dedupe/coalesce off-main first.
    • Schedule minimal UI/model mutation with DispatchQueue.main.async only when needed.
  • Commands that directly manipulate AppKit/Ghostty UI state (focus/select/open/close/send key/input, list/current queries requiring exact synchronous snapshot) are allowed to run on main actor.
  • If adding a new socket command, default to off-main handling; require an explicit reason in code comments when main-thread execution is necessary.

Socket focus policy

  • Socket/CLI commands must not steal macOS app focus (no app activation/window raising side effects).
  • Only explicit focus-intent commands may mutate in-app focus/selection (window.focus, workspace.select/next/previous/last, surface.focus, pane.focus/last, browser focus commands, and v1 focus equivalents).
  • All non-focus commands should preserve current user focus context while still applying data/model changes.

E2E mac UI tests

Run UI tests on the UTM macOS VM (never on the host machine). Always run e2e UI tests via ssh cmux-vm:

ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:cmuxUITests/UpdatePillUITests test'

Basic tests

Run basic automated tests on the UTM macOS VM (never on the host machine):

ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" build && pkill -x "cmux DEV" || true && APP=$(find /Users/cmux/Library/Developer/Xcode/DerivedData -path "*/Build/Products/Debug/cmux DEV.app" -print -quit) && open "$APP" --env CMUX_SOCKET_MODE=allowAll && for i in {1..20}; do [ -S /tmp/cmux-debug.sock ] && break; sleep 0.5; done && python3 tests/test_update_timing.py && python3 tests/test_signals_auto.py && python3 tests/test_ctrl_socket.py && python3 tests/test_notifications.py'

Ghostty submodule workflow

Ghostty changes must be committed in the ghostty submodule and pushed to the manaflow-ai/ghostty fork. Keep docs/ghostty-fork.md up to date with any fork changes and conflict notes.

cd ghostty
git remote -v  # origin = upstream, manaflow = fork
git checkout -b <branch>
git add <files>
git commit -m "..."
git push manaflow <branch>

To keep the fork up to date with upstream:

cd ghostty
git fetch origin
git checkout main
git merge origin/main
git push manaflow main

Then update the parent repo with the new submodule SHA:

cd ..
git add ghostty
git commit -m "Update ghostty submodule"

Release

Use the /release command to prepare a new release. This will:

  1. Determine the new version (bumps minor by default)
  2. Gather commits since the last tag and update the changelog
  3. Update CHANGELOG.md and docs-site/content/docs/changelog.mdx
  4. Run ./scripts/bump-version.sh to update both versions
  5. Commit, tag, and push

Version bumping:

./scripts/bump-version.sh          # bump minor (0.15.0 → 0.16.0)
./scripts/bump-version.sh patch    # bump patch (0.15.0 → 0.15.1)
./scripts/bump-version.sh major    # bump major (0.15.0 → 1.0.0)
./scripts/bump-version.sh 1.0.0    # set specific version

This updates both MARKETING_VERSION and CURRENT_PROJECT_VERSION (build number). The build number is auto-incremented and is required for Sparkle auto-update to work.

Manual release steps (if not using the command):

git tag vX.Y.Z
git push origin vX.Y.Z
gh run watch --repo manaflow-ai/cmux

Notes:

  • Requires GitHub secrets: APPLE_CERTIFICATE_BASE64, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID.
  • The release asset is cmux-macos.dmg attached to the tag.
  • README download button points to releases/latest/download/cmux-macos.dmg.
  • Versioning: bump the minor version for updates unless explicitly asked otherwise.
  • Changelog: always update both CHANGELOG.md and the docs-site version.