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.shwrites 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.plistunderUTExportedTypeDeclarations(e.g.com.splittabbar.tabtransfer,com.cmux.sidebar-tab-reorder). - Do not add an app-level display link or manual
ghostty_surface_drawloop; 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
mainbranch 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.syncfor 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.asynconly 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:
- Determine the new version (bumps minor by default)
- Gather commits since the last tag and update the changelog
- Update
CHANGELOG.mdanddocs-site/content/docs/changelog.mdx - Run
./scripts/bump-version.shto update both versions - 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.dmgattached 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.mdand the docs-site version.