Compact CLAUDE.md and skills docs (#8990)
* Compact CLAUDE.md and skills docs Cut ~2k lines of duplication without dropping actionable rules. CLAUDE.md (267 -> 103): `reload.sh --tag` was explained four separate times; now once. Dropped the Ghostty submodule and Release sections, which restated the cmux-ghostty and cmux-release skills, and removed the file:// deeplink block, which contradicted the rule that chat output uses http://127.0.0.1:17320/<tag> and never a file:// URL. Pitfalls compressed from paragraphs to one line each, pointing at the owning skill. skills/ (4849 -> 2819 across 20 skills): the dominant waste was SKILL.md files restating their own references/ verbatim. Kept one canonical statement with expansion in references/. Three rules the root file carried had no skill that covered them, so they moved into cmux-architecture rather than being lost: SPM package group folders with check-workspace-package-groups.py, the Package.resolved tracking policy, and "feature flag means a remote PostHog runtime flag" via CmuxFeatureFlags. The cmuxTests pbxproj wiring requirement was promoted from a reference file into cmux-testing, and the shortcut policy moved into cmux-keyboard-shortcuts with its duplicate removed from cmux-localization. Stale references fixed: - `cd cmuxd && zig build` referenced a directory with zero tracked files; the daemon is Go at daemon/remote/cmd/cmuxd-remote. - Changelog page is web/app/[locale]/(landing)/docs/changelog/page.tsx, and configuration is under the same (landing) segment. - Package CmuxSocketControl does not exist; the real one is CmuxControlSocket, cited twice as the exemplar to copy. - Two rg commands in the localization audit were double-escaped and passed `--` as if it were a glob flag, so they matched nothing and silently passed the audit. Left untouched: the auto-generated cmux-settings reference files, which would drift from their generator. * Dedupe release slash commands release.md, release-local.md, and release-nightly.md each restated the same version-bump and changelog procedure (450 -> 192 lines total). release.md is now the canonical command doc holding the shared prep, changelog guidelines, and contributor-credit format; the other two state only their delta (local build-sign-upload.sh path, and no-PR direct-to- main path with the homebrew-cmux submodule pointer commit). Stale and incorrect instructions fixed: - All three pointed at docs-site/content/docs/changelog.mdx. There is no docs-site/ in the repo; the changelog page renders from CHANGELOG.md. - release.md said to hand-edit 'typically 4 occurrences' of MARKETING_VERSION in project.pbxproj. That leaves CURRENT_PROJECT_VERSION stale, which Sparkle requires to be monotonic and which release-pretag-guard.sh rejects. Unified on scripts/bump-version.sh, which bumps both. - Documented build-sign-upload.sh --allow-overwrite, which matters because pushing a v* tag also fires release.yml, so a local upload can race CI for the same assets. * Address review findings on submodule remotes and build links - tagged-builds.md still told contributors to build chat links from the absolute .app path with a file:// URL, contradicting the rule in CLAUDE.md that chat links use http://127.0.0.1:17320/<tag>. - cmux-ghostty said 'origin is upstream and manaflow is the fork' and pushed to a 'manaflow' remote. .gitmodules points every submodule at manaflow-ai/*, and no checkout has a 'manaflow' remote, so those commands would fail. Both the skill and submodule-safety.md now tell you to check git remote -v, and document adding an explicit 'upstream' remote for syncing from ghostty-org. - submodule-safety.md verified ancestry against <remote>/main even when a feature branch was pushed. Now checks the branch actually pushed. - release.md credited @lawrencechen; the account is @lawrencecchen. Skipped, with reasons: the ~/.agents/skills vs ~/.codex/skills split in cmux-customization is the documented convention (normal install vs skills.sh install), matching cmux-diagnostics. Adding per-entry attribution to the un-credited changelog example entry would contradict the policy three lines above it, which exempts core-team work.
This commit is contained in:
@@ -1,33 +1,19 @@
|
||||
# Cleanup Dev Builds
|
||||
|
||||
Reclaim disk space taken by tagged dev artifacts produced by `./scripts/reload.sh --tag <tag>`. Each tagged build is multi-GB of DerivedData plus per-tag sockets and logs.
|
||||
Reclaim disk taken by tagged dev artifacts from `./scripts/reload.sh --tag <tag>`. Each tagged build is multi-GB of DerivedData plus per-tag sockets and logs.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Preview first.**
|
||||
1. **Preview.** `./scripts/cleanup-dev-builds.sh` is dry-run by default and prints what would be deleted, what is skipped, and total reclaimable bytes.
|
||||
|
||||
```bash
|
||||
./scripts/cleanup-dev-builds.sh
|
||||
```
|
||||
2. **Read the preview to the user.** Confirm the active tag and any tag they care about appears under `skipping:`.
|
||||
|
||||
Shows what would be deleted, what is skipped, and total reclaimable bytes. Dry-run by default; nothing is deleted yet.
|
||||
3. **Ask before deleting.** Never run `--apply` without explicit confirmation. Surface tags they may want to protect with `--keep <tag>`.
|
||||
|
||||
2. **Read the preview to the user.** Confirm the active tag and any tag they care about appears under `skipping:` (running, or most recent reload via `/tmp/cmux-last-cli-path`).
|
||||
4. **Apply.** `./scripts/cleanup-dev-builds.sh --apply`. Optional: `--keep <tag>` (repeatable) to protect specific tags, `--older-than <DAYS>` to skip recently touched artifacts.
|
||||
|
||||
3. **Ask the user before deleting.** Do not run `--apply` without explicit user confirmation. Surface any tags they may want to keep so they can add `--keep <tag>`.
|
||||
|
||||
4. **Apply.** Once confirmed:
|
||||
|
||||
```bash
|
||||
./scripts/cleanup-dev-builds.sh --apply
|
||||
```
|
||||
|
||||
Optional: `--keep <tag>` (repeatable) to protect specific tags, `--older-than <DAYS>` to skip anything touched recently.
|
||||
|
||||
5. **Report.** Show the freed-bytes total from the script's final line.
|
||||
5. **Report** the freed-bytes total from the script's final line.
|
||||
|
||||
## Notes
|
||||
|
||||
- Safety rules always on: skip running `cmux DEV <tag>` apps, skip the tag in `/tmp/cmux-last-cli-path` (most recent reload).
|
||||
- Worktrees existing under HQ are NOT a protection. Use `--keep` for explicit protection.
|
||||
- The script never touches `GhosttyKit.xcframework` symlinks, the GhosttyKit cache, or anything outside per-tag artifacts.
|
||||
Always-on safety rules: skip running `cmux DEV <tag>` apps, and skip the tag in `/tmp/cmux-last-cli-path` (the most recent reload). An existing HQ worktree is not protection; use `--keep`. The script never touches `GhosttyKit.xcframework` symlinks, the GhosttyKit cache, or anything outside per-tag artifacts.
|
||||
|
||||
@@ -5,12 +5,6 @@ Pull latest main and update all submodules to their latest remote main. No commi
|
||||
## Steps
|
||||
|
||||
1. `git pull origin main`
|
||||
2. For each submodule (ghostty, homebrew-cmux, vendor/bonsplit):
|
||||
- `cd <submodule>`
|
||||
- `git fetch origin`
|
||||
- Check if behind: `git rev-list HEAD..origin/main --count`
|
||||
- If behind, merge: `git merge origin/main --no-edit`
|
||||
- Do NOT push. We only land submodule changes via PRs.
|
||||
- Go back to repo root
|
||||
2. For each of `ghostty`, `homebrew-cmux`, `vendor/bonsplit`: `git fetch origin`, check `git rev-list HEAD..origin/main --count`, and if behind run `git merge origin/main --no-edit`. Do not push; submodule changes land only via PRs.
|
||||
3. `git submodule update --init --recursive`
|
||||
4. Report: current commit, which submodules were updated and by how many commits
|
||||
4. Report the current commit, plus which submodules moved and by how many commits.
|
||||
|
||||
@@ -1,108 +1,34 @@
|
||||
# Release Local
|
||||
|
||||
Full end-to-end release built locally. Bumps version, updates changelog, tags, then builds/signs/notarizes/uploads via `scripts/build-sign-upload.sh`.
|
||||
Release straight from `main` with no PR, built and published locally.
|
||||
|
||||
## Steps
|
||||
Follow [release.md](release.md) "Shared prep" (version, changelog, contributors, `./scripts/bump-version.sh`) and its changelog and contributor-credit rules. `skills/cmux-release/SKILL.md` covers the bump and tag mechanics.
|
||||
|
||||
### 1. Determine the new version number
|
||||
## Delta: no PR, tag on main, local build
|
||||
|
||||
- Get the current version from `cmux.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`)
|
||||
- Bump the minor version unless the user specifies otherwise (e.g., 0.54.0 → 0.55.0)
|
||||
1. **Commit on main.** Stage `CHANGELOG.md` and `cmux.xcodeproj/project.pbxproj`, commit `Bump version to X.Y.Z`.
|
||||
|
||||
### 2. Gather changes and contributors since the last release
|
||||
2. **Guard, tag, push.**
|
||||
|
||||
- Find the most recent git tag: `git describe --tags --abbrev=0`
|
||||
- Get commits since that tag: `git log --oneline <last-tag>..HEAD --no-merges`
|
||||
- **Filter for end-user visible changes only** — ignore developer tooling, CI, docs, tests
|
||||
- Categorize changes into: Added, Changed, Fixed, Removed
|
||||
- If there are no user-facing changes, ask the user if they still want to release
|
||||
- **Collect contributors:** For each PR referenced in the commits, get the author:
|
||||
```bash
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Also check for linked issue reporters (the person who filed the bug):
|
||||
```bash
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Build a deduplicated list of all contributor `@handle`s for the release
|
||||
```bash
|
||||
./scripts/release-pretag-guard.sh
|
||||
git tag vX.Y.Z
|
||||
git push origin main && git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
### 3. Update the changelog
|
||||
If the guard fails, run `./scripts/bump-version.sh`, commit the build-number bump, and rerun the guard.
|
||||
|
||||
- Add a new section at the top of `CHANGELOG.md` with the new version and today's date
|
||||
- **Only include changes that affect the end-user experience**
|
||||
- Write clear, user-facing descriptions (not raw commit messages)
|
||||
- **Credit contributors inline** (see Contributor Credits below)
|
||||
- Also update `docs-site/content/docs/changelog.mdx` if it exists
|
||||
3. **Build, sign, notarize, upload.**
|
||||
|
||||
### 4. Bump the version
|
||||
```bash
|
||||
./scripts/build-sign-upload.sh vX.Y.Z
|
||||
```
|
||||
|
||||
- Run: `./scripts/bump-version.sh` (bumps minor by default)
|
||||
The script does GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization of app and DMG, appcast generation, GitHub release upload of `cmux-macos.dmg` and `appcast.xml`, homebrew cask update, cleanup, and `say "cmux release complete"` on success. Pass `--allow-overwrite` only to replace existing assets on the same tag during an emergency reroll. If it fails, run `say "cmux release failed"`.
|
||||
|
||||
### 5. Commit, run the pre-tag guard, then tag and push
|
||||
4. **Verify and land the homebrew cask.**
|
||||
|
||||
- Stage: `CHANGELOG.md`, `cmux.xcodeproj/project.pbxproj`
|
||||
- Commit message: `Bump version to X.Y.Z`
|
||||
- Run: `./scripts/release-pretag-guard.sh`
|
||||
- If it fails, run `./scripts/bump-version.sh`, commit the build-number bump, and rerun the guard
|
||||
- Create tag: `git tag vX.Y.Z`
|
||||
- Push: `git push origin main && git push origin vX.Y.Z`
|
||||
|
||||
### 6. Build, sign, notarize, and upload
|
||||
|
||||
```bash
|
||||
./scripts/build-sign-upload.sh vX.Y.Z
|
||||
```
|
||||
|
||||
This script handles: GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization (app + DMG), appcast generation, GitHub release upload, homebrew cask update, and cleanup.
|
||||
|
||||
If the script fails, run `say "cmux release failed"`.
|
||||
|
||||
### 7. Verify homebrew cask
|
||||
|
||||
- Run `bash tests/test_homebrew_sha.sh` to confirm the cask SHA matches the release DMG
|
||||
- Update the homebrew-cmux submodule pointer: `git add homebrew-cmux && git commit -m "Update homebrew-cmux submodule to latest" && git push origin main`
|
||||
|
||||
## Changelog Guidelines
|
||||
|
||||
**Include only end-user visible changes:**
|
||||
- New features users can see or interact with
|
||||
- Bug fixes users would notice (crashes, UI glitches, incorrect behavior)
|
||||
- Performance improvements users would feel
|
||||
- UI/UX changes
|
||||
- Breaking changes or removed features
|
||||
|
||||
**Exclude internal/developer changes:**
|
||||
- Setup scripts, build scripts, reload scripts
|
||||
- CI/workflow changes
|
||||
- Documentation updates (README, CONTRIBUTING, CLAUDE.md)
|
||||
- Test additions or fixes
|
||||
- Internal refactoring with no user-visible effect
|
||||
- Dependency updates (unless they fix a user-facing bug)
|
||||
|
||||
**Writing style:**
|
||||
- Use present tense ("Add feature" not "Added feature")
|
||||
- Group by category: Added, Changed, Fixed, Removed
|
||||
- Be concise but descriptive
|
||||
- Focus on what the user experiences, not how it was implemented
|
||||
|
||||
## Contributor Credits
|
||||
|
||||
Credit the people who made each release happen. This builds community and encourages contributions.
|
||||
|
||||
**Per-entry attribution** — append contributor credit after each changelog bullet:
|
||||
- For code contributions (PR author): `— thanks @user!`
|
||||
- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!`
|
||||
- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline
|
||||
|
||||
**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release:
|
||||
```markdown
|
||||
### Thanks to N contributors!
|
||||
|
||||
- [@user1](https://github.com/user1)
|
||||
- [@user2](https://github.com/user2)
|
||||
```
|
||||
- List all contributors alphabetically by GitHub handle (including core team)
|
||||
- Link each handle to their GitHub profile
|
||||
- Include everyone: PR authors, issue reporters, anyone whose work is in the release
|
||||
|
||||
**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles.
|
||||
```bash
|
||||
bash tests/test_homebrew_sha.sh
|
||||
git add homebrew-cmux && git commit -m "Update homebrew-cmux submodule to latest" && git push origin main
|
||||
```
|
||||
|
||||
@@ -1,107 +1,17 @@
|
||||
# Release Nightly
|
||||
|
||||
End-to-end release via PR flow: bump version, update changelog, create PR, merge, tag, then build locally via `scripts/build-sign-upload.sh`.
|
||||
Release through the PR flow, then build and publish locally instead of waiting on the CI release workflow.
|
||||
|
||||
## Steps
|
||||
Follow [release.md](release.md) "Shared prep" (version, changelog, contributors, `./scripts/bump-version.sh`) and its changelog and contributor-credit rules, then steps 5 through 8 (branch, PR, `gh pr checks --watch`, `gh pr merge --squash --delete-branch`, `./scripts/release-pretag-guard.sh`, tag and push). `skills/cmux-release/SKILL.md` covers the bump and tag mechanics.
|
||||
|
||||
### Phase 1: Version bump, changelog, PR, merge, tag
|
||||
## Delta: build locally instead of from CI
|
||||
|
||||
1. **Determine the new version number**
|
||||
- Get the current version from `cmux.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`)
|
||||
- Bump the minor version unless the user specifies otherwise (e.g., 0.48.0 → 0.49.0)
|
||||
|
||||
2. **Create a release branch**
|
||||
- Create branch: `git checkout -b release/vX.Y.Z`
|
||||
|
||||
3. **Gather changes and contributors since the last release**
|
||||
- Find the most recent git tag: `git describe --tags --abbrev=0`
|
||||
- Get commits since that tag: `git log --oneline <last-tag>..HEAD --no-merges`
|
||||
- **Filter for end-user visible changes only** - ignore developer tooling, CI, docs, tests
|
||||
- Categorize changes into: Added, Changed, Fixed, Removed
|
||||
- **Collect contributors:** For each PR referenced in the commits, get the author:
|
||||
```bash
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Also check for linked issue reporters (the person who filed the bug):
|
||||
```bash
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Build a deduplicated list of all contributor `@handle`s for the release
|
||||
|
||||
4. **Update the changelog**
|
||||
- Add a new section at the top of `CHANGELOG.md` with the new version and today's date
|
||||
- **Only include changes that affect the end-user experience**
|
||||
- Write clear, user-facing descriptions (not raw commit messages)
|
||||
- **Credit contributors inline** (see Contributor Credits below)
|
||||
- Also update `docs-site/content/docs/changelog.mdx` if it exists
|
||||
- If there are no user-facing changes, ask the user if they still want to release
|
||||
|
||||
5. **Bump the version**
|
||||
- Run `./scripts/bump-version.sh` (bumps minor by default)
|
||||
|
||||
6. **Commit and push the release branch**
|
||||
- Stage: `CHANGELOG.md`, `cmux.xcodeproj/project.pbxproj`
|
||||
- Commit message: `Bump version to X.Y.Z`
|
||||
- Push: `git push -u origin release/vX.Y.Z`
|
||||
|
||||
7. **Create PR and wait for CI**
|
||||
- `gh pr create --title "Release vX.Y.Z" --body "...changelog..."`
|
||||
- `gh pr checks --watch`
|
||||
|
||||
8. **Merge PR**
|
||||
- `gh pr merge --squash --delete-branch`
|
||||
- `git checkout main && git pull`
|
||||
|
||||
9. **Create and push the tag**
|
||||
- `git tag vX.Y.Z && git push origin vX.Y.Z`
|
||||
|
||||
### Phase 2: Local build, sign, notarize, upload
|
||||
|
||||
10. **Run the build script**
|
||||
Replace steps 9 through 11 of `/release` with:
|
||||
|
||||
```bash
|
||||
./scripts/build-sign-upload.sh vX.Y.Z
|
||||
```
|
||||
|
||||
This script handles: GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization (app + DMG), appcast generation, GitHub release upload, and cleanup.
|
||||
The script does GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization of app and DMG, appcast generation, GitHub release upload of `cmux-macos.dmg` and `appcast.xml`, homebrew cask update, cleanup, and `say "cmux release complete"` on success. Pass `--allow-overwrite` only to replace existing assets on the same tag during an emergency reroll.
|
||||
|
||||
If the script fails, run `say "cmux release failed"`.
|
||||
|
||||
## Changelog Guidelines
|
||||
|
||||
**Include only end-user visible changes:**
|
||||
- New features users can see or interact with
|
||||
- Bug fixes users would notice (crashes, UI glitches, incorrect behavior)
|
||||
- Performance improvements users would feel
|
||||
- UI/UX changes
|
||||
- Breaking changes or removed features
|
||||
|
||||
**Exclude internal/developer changes:**
|
||||
- Setup scripts, build scripts, reload scripts
|
||||
- CI/workflow changes
|
||||
- Documentation updates (README, CONTRIBUTING, CLAUDE.md)
|
||||
- Test additions or fixes
|
||||
- Internal refactoring with no user-visible effect
|
||||
- Dependency updates (unless they fix a user-facing bug)
|
||||
|
||||
## Contributor Credits
|
||||
|
||||
Credit the people who made each release happen. This builds community and encourages contributions.
|
||||
|
||||
**Per-entry attribution** — append contributor credit after each changelog bullet:
|
||||
- For code contributions (PR author): `— thanks @user!`
|
||||
- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!`
|
||||
- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline
|
||||
|
||||
**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release:
|
||||
```markdown
|
||||
### Thanks to N contributors!
|
||||
|
||||
- [@user1](https://github.com/user1)
|
||||
- [@user2](https://github.com/user2)
|
||||
```
|
||||
- List all contributors alphabetically by GitHub handle (including core team)
|
||||
- Link each handle to their GitHub profile
|
||||
- Include everyone: PR authors, issue reporters, anyone whose work is in the release
|
||||
|
||||
**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles.
|
||||
|
||||
+41
-95
@@ -1,129 +1,75 @@
|
||||
# Release
|
||||
|
||||
Prepare a new release for cmux. This command updates the changelog, bumps the version, creates a PR, monitors CI, and then merges and tags.
|
||||
Ship a stable cmux release built by CI: bump version, update changelog, open a PR, merge, tag, then GitHub Actions builds, signs, and publishes.
|
||||
|
||||
## Steps
|
||||
`skills/cmux-release/SKILL.md` owns the version-bump, pretag-guard, and tag mechanics plus the Apple signing secrets. This file owns the shared changelog and contributor procedure that `/release-nightly` and `/release-local` also use, and the PR-and-CI build path.
|
||||
|
||||
1. **Determine the new version number**
|
||||
- Get the current version from `cmux.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`)
|
||||
- Bump the minor version unless the user specifies otherwise (e.g., 0.12.0 → 0.13.0)
|
||||
## Shared prep (all three release commands)
|
||||
|
||||
2. **Create a release branch**
|
||||
- Create branch: `git checkout -b release/vX.Y.Z`
|
||||
1. **Pick the version.** Read `MARKETING_VERSION` from `cmux.xcodeproj/project.pbxproj`. Bump minor unless the user says otherwise (0.12.0 to 0.13.0).
|
||||
|
||||
3. **Gather changes and contributors since the last release**
|
||||
- Find the most recent git tag: `git describe --tags --abbrev=0`
|
||||
- Get commits since that tag: `git log --oneline <last-tag>..HEAD --no-merges`
|
||||
- **Filter for end-user visible changes only** - ignore developer tooling, CI, docs, tests
|
||||
- Categorize changes into: Added, Changed, Fixed, Removed
|
||||
- **Collect contributors:** For each PR referenced in the commits, get the author:
|
||||
```bash
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Also check for linked issue reporters (the person who filed the bug):
|
||||
```bash
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Build a deduplicated list of all contributor `@handle`s for the release
|
||||
2. **Gather changes and contributors since the last tag.**
|
||||
|
||||
4. **Update the changelog**
|
||||
- Add a new section at the top of `CHANGELOG.md` with the new version and today's date
|
||||
- **Only include changes that affect the end-user experience** - things users will see, feel, or interact with
|
||||
- Write clear, user-facing descriptions (not raw commit messages)
|
||||
- **Credit contributors inline** (see Contributor Credits below)
|
||||
- Also update `docs-site/content/docs/changelog.mdx` with the same content
|
||||
- If there are no user-facing changes, ask the user if they still want to release
|
||||
```bash
|
||||
git describe --tags --abbrev=0
|
||||
git log --oneline <last-tag>..HEAD --no-merges
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
|
||||
5. **Bump the version in Xcode project**
|
||||
- Update all occurrences of `MARKETING_VERSION` in `cmux.xcodeproj/project.pbxproj`
|
||||
- There are typically 4 occurrences (Debug/Release for main app and CLI)
|
||||
Keep only end-user visible changes, categorize into Added, Changed, Fixed, Removed, and build a deduplicated list of contributor `@handle`s from PR authors and linked issue reporters. If nothing is user-facing, ask the user whether to release anyway.
|
||||
|
||||
6. **Commit and push the release branch**
|
||||
- Stage: `CHANGELOG.md`, `docs-site/content/docs/changelog.mdx`, `cmux.xcodeproj/project.pbxproj`
|
||||
- Commit message: `Bump version to X.Y.Z`
|
||||
- Push: `git push -u origin release/vX.Y.Z`
|
||||
3. **Update `CHANGELOG.md`.** Add a section at the top with the new version and today's date, written as user-facing descriptions rather than raw commit messages, with inline contributor credit. The docs changelog page renders from `CHANGELOG.md`, so there is no second changelog file to edit.
|
||||
|
||||
7. **Create a pull request**
|
||||
- Create PR: `gh pr create --title "Release vX.Y.Z" --body "...changelog summary..."`
|
||||
- Include the changelog entries in the PR body
|
||||
4. **Bump the version.** `./scripts/bump-version.sh` (minor by default) updates `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` everywhere in the Xcode project.
|
||||
|
||||
8. **Monitor CI**
|
||||
- Watch the CI workflow: `gh pr checks --watch`
|
||||
- If CI fails, fix the issues and push again
|
||||
- Wait for all checks to pass before proceeding
|
||||
## CI-built release (this command)
|
||||
|
||||
9. **Merge the PR**
|
||||
- Merge: `gh pr merge --squash --delete-branch`
|
||||
- Switch back to main: `git checkout main && git pull`
|
||||
5. **Branch, commit, push.** `git checkout -b release/vX.Y.Z`, stage `CHANGELOG.md` and `cmux.xcodeproj/project.pbxproj`, commit `Bump version to X.Y.Z`, then `git push -u origin release/vX.Y.Z`.
|
||||
|
||||
10. **Run the pre-tag guard, then create and push the tag**
|
||||
- Run: `./scripts/release-pretag-guard.sh`
|
||||
- If it fails, run `./scripts/bump-version.sh`, commit the build-number bump, push/merge that change, and retry the tag
|
||||
- Create tag: `git tag vX.Y.Z`
|
||||
- Push tag: `git push origin vX.Y.Z`
|
||||
6. **PR and CI.** `gh pr create --title "Release vX.Y.Z" --body "...changelog summary..."` with the changelog entries in the body, then `gh pr checks --watch`. Fix failures and push until every check passes.
|
||||
|
||||
11. **Monitor the release workflow**
|
||||
- Watch: `gh run watch --repo manaflow-ai/cmux`
|
||||
- Verify the release appears at: https://github.com/manaflow-ai/cmux/releases
|
||||
- Check that the DMG is attached to the release
|
||||
7. **Merge.** `gh pr merge --squash --delete-branch`, then `git checkout main && git pull`.
|
||||
|
||||
12. **Verify homebrew cask update**
|
||||
- The "Update Homebrew Cask" workflow triggers automatically after the release workflow completes
|
||||
- Watch: `gh run list --workflow=update-homebrew.yml --limit=1` and `gh run watch`
|
||||
- Verify: `cd homebrew-cmux && git pull && grep version Casks/cmux.rb`
|
||||
- Run `bash tests/test_homebrew_sha.sh` to confirm the SHA matches
|
||||
8. **Guard and tag.** `./scripts/release-pretag-guard.sh`, then `git tag vX.Y.Z && git push origin vX.Y.Z`. If the guard fails, run `./scripts/bump-version.sh`, commit the build-number bump, push and merge that change, then retry.
|
||||
|
||||
13. **Notify**
|
||||
- On success: `say "cmux release complete"`
|
||||
- On failure: `say "cmux release failed"`
|
||||
9. **Watch the release workflow.** `gh run watch --repo manaflow-ai/cmux`. Confirm the release at https://github.com/manaflow-ai/cmux/releases exists with `cmux-macos.dmg` attached.
|
||||
|
||||
## Changelog Guidelines
|
||||
10. **Verify the homebrew cask.** `update-homebrew.yml` triggers automatically once the release workflow finishes.
|
||||
|
||||
**Include only end-user visible changes:**
|
||||
- New features users can see or interact with
|
||||
- Bug fixes users would notice (crashes, UI glitches, incorrect behavior)
|
||||
- Performance improvements users would feel
|
||||
- UI/UX changes
|
||||
- Breaking changes or removed features
|
||||
```bash
|
||||
gh run list --workflow=update-homebrew.yml --limit=1
|
||||
gh run watch --repo manaflow-ai/cmux <run-id>
|
||||
cd homebrew-cmux && git pull && grep version Casks/cmux.rb
|
||||
bash tests/test_homebrew_sha.sh
|
||||
```
|
||||
|
||||
**Exclude internal/developer changes:**
|
||||
- Setup scripts, build scripts, reload scripts
|
||||
- CI/workflow changes
|
||||
- Documentation updates (README, CONTRIBUTING, CLAUDE.md)
|
||||
- Test additions or fixes
|
||||
- Internal refactoring with no user-visible effect
|
||||
- Dependency updates (unless they fix a user-facing bug)
|
||||
11. **Notify.** `say "cmux release complete"` on success, `say "cmux release failed"` on failure.
|
||||
|
||||
**Writing style:**
|
||||
- Use present tense ("Add feature" not "Added feature")
|
||||
- Group by category: Added, Changed, Fixed, Removed
|
||||
- Be concise but descriptive
|
||||
- Focus on what the user experiences, not how it was implemented
|
||||
- Link to issues/PRs if relevant
|
||||
## Changelog guidelines
|
||||
|
||||
## Contributor Credits
|
||||
Include what a user can see, feel, or interact with: new features, noticeable bug fixes (crashes, UI glitches, wrong behavior), performance the user would feel, UI/UX changes, breaking changes and removals.
|
||||
|
||||
Exclude internal work: setup/build/reload scripts, CI and workflow changes, docs (README, CONTRIBUTING, CLAUDE.md), tests, refactors with no user-visible effect, and dependency bumps unless they fix a user-facing bug.
|
||||
|
||||
Write in present tense ("Add feature", not "Added feature"), grouped by Added, Changed, Fixed, Removed. Be concise and descriptive, describe what the user experiences rather than how it was implemented, and link the issue or PR when relevant.
|
||||
|
||||
## Contributor credits
|
||||
|
||||
Credit the people who made each release happen. This builds community and encourages contributions.
|
||||
|
||||
**Per-entry attribution** — append contributor credit after each changelog bullet:
|
||||
- For code contributions (PR author): `— thanks @user!`
|
||||
- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!`
|
||||
- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline
|
||||
Per-entry attribution goes after each changelog bullet: `— thanks @user!` for a PR author, `— thanks @reporter for the report!` for an issue reporter who is not the PR author. Core team (`lawrencecchen`, `austinywang`) work is the baseline and gets no per-entry callout.
|
||||
|
||||
Every release ends with a summary section listing all contributors alphabetically by handle, core team included, each linked to their GitHub profile. The published GitHub Release body carries the same section.
|
||||
|
||||
**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release:
|
||||
```markdown
|
||||
### Thanks to N contributors!
|
||||
|
||||
- [@user1](https://github.com/user1)
|
||||
- [@user2](https://github.com/user2)
|
||||
```
|
||||
- List all contributors alphabetically by GitHub handle (including core team)
|
||||
- Link each handle to their GitHub profile
|
||||
- Include everyone: PR authors, issue reporters, anyone whose work is in the release
|
||||
|
||||
**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles.
|
||||
|
||||
## Example Changelog Entry
|
||||
## Example changelog entry
|
||||
|
||||
```markdown
|
||||
## [0.13.0] - 2025-01-30
|
||||
@@ -142,6 +88,6 @@ Credit the people who made each release happen. This builds community and encour
|
||||
|
||||
- [@contributor](https://github.com/contributor)
|
||||
- [@fixer](https://github.com/fixer)
|
||||
- [@lawrencechen](https://github.com/lawrencechen)
|
||||
- [@lawrencecchen](https://github.com/lawrencecchen)
|
||||
- [@reporter](https://github.com/reporter)
|
||||
```
|
||||
|
||||
@@ -6,34 +6,14 @@ Get the current branch ready: update all submodules to their latest remote main,
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Update submodules to latest**
|
||||
- For each submodule (ghostty, homebrew-cmux, vendor/bonsplit):
|
||||
- `cd <submodule>`
|
||||
- `git fetch origin`
|
||||
- Check if behind: `git rev-list HEAD..origin/main --count`
|
||||
- If behind, merge: `git merge origin/main --no-edit`
|
||||
- Do NOT push submodules. We only land submodule changes via PRs.
|
||||
- Go back to repo root
|
||||
1. **Update submodules to latest.** For each of `ghostty`, `homebrew-cmux`, `vendor/bonsplit`: `git fetch origin`, check `git rev-list HEAD..origin/main --count`, and if behind run `git merge origin/main --no-edit`. Do not push submodules; submodule changes land only via PRs.
|
||||
|
||||
2. **Commit submodule updates on main**
|
||||
- `git checkout main && git pull origin main`
|
||||
- Check if any submodules changed: `git diff --name-only` (look for submodule paths)
|
||||
- If changed, stage and commit: `git add ghostty homebrew-cmux vendor/bonsplit && git commit -m "Update submodules: <brief description>"`
|
||||
- **Do not push.** Ask the user if they want to push.
|
||||
2. **Commit submodule updates on main.** `git checkout main && git pull origin main`, check `git diff --name-only` for submodule paths, and if any changed: `git add ghostty homebrew-cmux vendor/bonsplit && git commit -m "Update submodules: <brief description>"`. Do not push. Ask the user whether to push.
|
||||
|
||||
3. **Rebase current branch on main**
|
||||
- `git checkout <original-branch>`
|
||||
- `git rebase main`
|
||||
- If conflicts, resolve them and continue
|
||||
- **Do not push.** Ask the user if they want to force-push the rebased branch.
|
||||
3. **Rebase the branch on main.** `git checkout <original-branch> && git rebase main`, resolving conflicts and continuing. Do not push. Ask the user whether to force-push the rebased branch. Skip this step if already on main.
|
||||
|
||||
4. **Report status**
|
||||
- Show what submodules were updated and by how many commits
|
||||
- Show if rebase was clean or had conflicts
|
||||
- Show current branch and commit
|
||||
4. **Report.** Which submodules moved and by how many commits, whether the rebase was clean or conflicted, and the current branch and commit. If no submodules needed updating and main has no new commits, say "Already up to date".
|
||||
|
||||
## Notes
|
||||
|
||||
- Never commit a submodule pointer in the parent repo unless the submodule commit is reachable from the submodule's remote main (per CLAUDE.md pitfall about orphaned commits)
|
||||
- If no submodules need updating and main has no new commits, just say "Already up to date"
|
||||
- If on main already, skip step 3
|
||||
Never commit a submodule pointer in the parent repo unless that submodule commit is reachable from the submodule's remote main (see the submodule-safety pitfall in CLAUDE.md).
|
||||
|
||||
@@ -1,267 +1,103 @@
|
||||
# cmux agent notes
|
||||
|
||||
## Initial setup
|
||||
## Setup
|
||||
|
||||
Run the setup script to initialize submodules, build GhosttyKit, and install the pbxproj normalization pre-commit hook:
|
||||
`./scripts/setup.sh` initializes submodules, builds GhosttyKit, and installs the pbxproj normalization pre-commit hook.
|
||||
|
||||
## Build and reload
|
||||
|
||||
Always build with a tag. **Never run bare `xcodebuild` or `open` an untagged `cmux DEV.app`**: untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
|
||||
|
||||
```bash
|
||||
./scripts/setup.sh
|
||||
./scripts/reload.sh --tag <branch-slug> # build Debug, kill same-tag app, do not launch
|
||||
./scripts/reload.sh --tag <branch-slug> --launch # also open it
|
||||
```
|
||||
|
||||
## Local dev
|
||||
A tag gives the app its own name, bundle ID, socket, and derived data path, so it runs side-by-side with the user's main app. Report the build to the user as a markdown link to `http://127.0.0.1:17320/<tag>`. Never put a `file://` URL, a raw `.app` path, or `/tmp/cmux-<tag>/...` in chat output.
|
||||
|
||||
After making code changes, always run the reload script with a tag to build the Debug app:
|
||||
Other variants: `reloadp.sh` (Release), `reloads.sh` (Release as isolated "cmux STAGING"), `reload2.sh --tag <tag>` (both).
|
||||
|
||||
Compile-only check, no launch:
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag fix-zsh-autosuggestions
|
||||
xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-<tag> build
|
||||
```
|
||||
|
||||
By default, `reload.sh` builds but does **not** launch the app. The script prints the `.app` path so the user can cmd-click to open it. After a successful build, it always terminates any running app with the same tag (so cmd-clicking launches the freshly-built binary instead of foregrounding the stale instance). Pass `--launch` to open the app automatically after the build:
|
||||
Rebuild GhosttyKit.xcframework with Release optimizations:
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag fix-zsh-autosuggestions --launch
|
||||
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
`reload.sh` prints an `App path:` line with the absolute path to the built `.app`. Use that path to build a cmd-clickable `file://` URL. Steps:
|
||||
Clean up older tags you started this session (quit the app, remove its `/tmp` socket and derived data) before launching a new one.
|
||||
|
||||
1. Grab the path from the `App path:` line in `reload.sh` output.
|
||||
2. Prepend `file://` and URL-encode spaces as `%20`. Do not hardcode any part of the path.
|
||||
3. Format it as a markdown link using the template for your agent type.
|
||||
## Tag-bound debug CLI
|
||||
|
||||
Example. If `reload.sh` output contains:
|
||||
|
||||
```text
|
||||
App path:
|
||||
/Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux DEV my-tag.app
|
||||
```
|
||||
|
||||
**Claude Code** outputs:
|
||||
|
||||
```markdown
|
||||
-------------------------------------------------------
|
||||
[cmux DEV my-tag.app](file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app)
|
||||
-------------------------------------------------------
|
||||
```
|
||||
|
||||
**Codex** outputs:
|
||||
|
||||
```markdown
|
||||
-------------------------------------------------------
|
||||
[my-tag: file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app](file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app)
|
||||
-------------------------------------------------------
|
||||
```
|
||||
|
||||
Never use `/tmp/cmux-<tag>/...` app links in chat output.
|
||||
|
||||
For CLI or socket dogfood against a tagged Debug app, use the tag-bound helper and set `CMUX_TAG`.
|
||||
Do not use `/tmp/cmux-cli` for tagged dogfood, since that symlink points at the most recently reloaded build and can target the user's main app socket.
|
||||
For CLI or socket dogfood against a tagged Debug app, set `CMUX_TAG` and use the helper. Do not use `/tmp/cmux-cli`, which points at the most recently reloaded build and can target the user's main app socket.
|
||||
|
||||
```bash
|
||||
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh list-workspaces
|
||||
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh send --workspace workspace:1 --surface surface:1 "echo ok"
|
||||
```
|
||||
|
||||
The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.sock`, and uses the matching tagged CLI from `~/Library/Developer/Xcode/DerivedData/cmux-<tag>/...`. It also scrubs ambient cmux terminal context (`CMUX_SOCKET`, `CMUX_SOCKET_PASSWORD`, workspace/surface/tab/panel IDs, cmuxd socket, and debug log), then sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and `CMUX_BUNDLED_CLI_PATH` for the selected tag.
|
||||
The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.sock`, and uses the matching tagged CLI from DerivedData. It scrubs ambient cmux terminal context (`CMUX_SOCKET`, `CMUX_SOCKET_PASSWORD`, workspace/surface/tab/panel IDs, cmuxd socket, debug log), then sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and `CMUX_BUNDLED_CLI_PATH` for the tag.
|
||||
|
||||
After making code changes, always use `reload.sh --tag` to build. **Never run bare `xcodebuild` or `open` an untagged `cmux DEV.app`.** Untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
|
||||
## iOS dev auth
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <your-branch-slug>
|
||||
```
|
||||
`ios/scripts/reload.sh` and `scripts/mobile-dev-launch.sh` auto-sign-in from `~/.secrets/cmuxterm-dev.env`. If the phone lands on the login screen or the helper reports missing credentials, do not ask the user to authenticate every build. Tell them to run `scripts/setup-team-dev.sh` once; it verifies their Stack login and writes the file chmod 600. Manual fallback: create it with `CMUX_DOGFOOD_STACK_EMAIL=...` and `CMUX_DOGFOOD_STACK_PASSWORD=...`.
|
||||
|
||||
If you only need to verify the build compiles (no launch), use a tagged derivedDataPath:
|
||||
## Regression test commits
|
||||
|
||||
```bash
|
||||
xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-<your-tag> build
|
||||
```
|
||||
|
||||
When rebuilding GhosttyKit.xcframework, always use Release optimizations:
|
||||
|
||||
```bash
|
||||
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
When rebuilding cmuxd for release/bundling, always use ReleaseFast:
|
||||
|
||||
```bash
|
||||
cd cmuxd && zig build -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
`reload` = build the Debug app (tag required) and terminate any running app with the same tag. Pass `--launch` to also open the freshly-built app:
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <tag>
|
||||
./scripts/reload.sh --tag <tag> --launch
|
||||
```
|
||||
|
||||
`reloadp` = kill and launch the Release app:
|
||||
|
||||
```bash
|
||||
./scripts/reloadp.sh
|
||||
```
|
||||
|
||||
`reloads` = kill and launch the Release app as "cmux STAGING" (isolated from production cmux):
|
||||
|
||||
```bash
|
||||
./scripts/reloads.sh
|
||||
```
|
||||
|
||||
`reload2` = reload both Debug and Release (tag required for Debug reload):
|
||||
|
||||
```bash
|
||||
./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:
|
||||
|
||||
```bash
|
||||
./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).
|
||||
|
||||
For iOS dev auth, `ios/scripts/reload.sh` and `scripts/mobile-dev-launch.sh` auto-sign-in from `~/.secrets/cmuxterm-dev.env`. If the phone lands on the login screen or the helper reports missing dev sign-in credentials, do not ask the user to manually authenticate every build. Tell them to run `scripts/setup-team-dev.sh` once from any cmux checkout; it prompts for and verifies their Stack login, writes `~/.secrets/cmuxterm-dev.env` with chmod 600, and future agents can auto-auth iOS DEBUG reloads. Manual fallback: create that file with `CMUX_DOGFOOD_STACK_EMAIL=...` and `CMUX_DOGFOOD_STACK_PASSWORD=...`.
|
||||
|
||||
## Regression test commit policy
|
||||
|
||||
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
|
||||
|
||||
1. **Commit 1:** Add the failing test only (no fix). CI should go red.
|
||||
2. **Commit 2:** Add the fix. CI should go green.
|
||||
|
||||
This makes it visible in the GitHub PR UI (Commits tab, check statuses) that the test genuinely fails without the fix.
|
||||
Two commits, so CI proves the test catches the bug: commit 1 adds the failing test only (CI red), commit 2 adds the fix (CI green). This is visible in the PR Commits tab.
|
||||
|
||||
## First pass, then dogfood
|
||||
|
||||
A task's first pass ends when the change is implemented, the tagged build succeeded on the pushed HEAD, focused tests ran, and the PR is open (for `web/` PRs, also the live Vercel preview URL given to the user). Then hand off to the user for dogfood. Do not fix CI failures, merge conflicts, or review findings inline in the main conversation after that point.
|
||||
A first pass ends when the change is implemented, the tagged build succeeded on the pushed HEAD, focused tests ran, and the PR is open (for `web/` PRs, also the live Vercel preview URL). Then hand off to the user. Do not fix CI failures, merge conflicts, or review findings inline in the main conversation after that point.
|
||||
|
||||
At handoff, launch one background `$autoreview` subagent with a bounded prompt (PR URL, worktree, base ref, allowed write scope, required verification), never a vague "make it green". That loop owns CI: it runs structured review plus PR feedback, and only when a check actually fails does it spawn a bounded repair subagent with that check's name and log context. Do not launch a separate parallel CI repair agent; two agents mutating one worktree race each other. One writer per worktree: if dogfood feedback needs main-agent edits while the loop runs, stop the loop first or give it its own sibling worktree. In Claude Code spawn the loop with the agent/task tool; in Codex use a background sub-task or bounded background `codex exec`.
|
||||
At handoff, launch one background `$autoreview` subagent with a bounded prompt (PR URL, worktree, base ref, allowed write scope, required verification), never a vague "make it green". That loop owns CI and spawns a bounded repair subagent only when a check actually fails. One writer per worktree: do not run a second CI repair agent against the same worktree, and if dogfood feedback needs main-agent edits while the loop runs, stop the loop first or give it a sibling worktree.
|
||||
|
||||
The loop may commit and push scoped fixes but never merges and never rebuilds the user's tagged build. The main agent inspects every pushed commit, rejects out-of-scope edits, and owns dogfood, approval, and merge. Merging app/runtime/UI changes still requires the user's explicit approval after dogfood; if a pushed fix changes runtime behavior mid-dogfood, rebuild the tag and re-notify, since the earlier verdict covers only the build the user tested.
|
||||
The loop may commit and push scoped fixes but never merges and never rebuilds the user's tagged build. The main agent inspects every pushed commit, rejects out-of-scope edits, and owns dogfood, approval, and merge. Merging app/runtime/UI changes requires the user's explicit approval after dogfood; if a pushed fix changes runtime behavior mid-dogfood, rebuild the tag and re-notify, since the earlier verdict covers only the build the user tested.
|
||||
|
||||
Notify through `cmux notify` so the user can leave and return. At handoff the main agent sends `cmux notify --title "Dogfood ready: <short task>" --subtitle "<branch> · <tag>" --body "Was: <prior bad behavior>. Now: <expected behavior>. <concrete check>. CI + review in background. PR: <pr-url>"`. The loop sends its outcome when done or blocked, e.g. `--title "CI green: <branch>"`, `--title "Review clean: <branch>" --body "fixed <n> findings, pushed"`, or `--title "CI blocked: <branch>" --body "<check>: <one-line cause>, needs your decision"`. Titles carry the outcome and branch; bodies say what happened and the single next action. If there is no cmux socket, skip notify and rely on the chat handoff.
|
||||
|
||||
## Shared behavior policy
|
||||
|
||||
- When a behavior is exposed through multiple entrypoints (keyboard shortcut, command palette, context menu, CLI, settings, debug menu), implement one shared action/model path and verify every entrypoint that should invoke it. Do not patch one surface while leaving the others with duplicated logic.
|
||||
- For optimistic UI or CLI updates, keep one mutation path, record pending state with a request id or previous snapshot, reconcile from the authoritative result, and handle failure with an explicit rollback or error state. Do not let each entrypoint maintain its own optimistic copy.
|
||||
- When a user says tests missed a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete.
|
||||
Notify through `cmux notify` so the user can leave and return. Handoff: `--title "Dogfood ready: <short task>" --subtitle "<branch> · <tag>" --body "Was: <prior bad behavior>. Now: <expected behavior>. <concrete check>. PR: <pr-url>"`. The loop reports its own outcome, e.g. `"CI green: <branch>"` or `"CI blocked: <branch>"` with a one-line cause and the next decision. Titles carry outcome and branch, bodies carry the single next action. Skip notify if there is no cmux socket.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Each of these has full detail in the skill named in parentheses.
|
||||
|
||||
- **Typing-latency-sensitive paths** (`cmux-debugging`): `WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift`, `TabItemView` in `ContentView.swift`, and `TerminalSurface.forceRefresh()` in `GhosttyTerminalView.swift` run on every keystroke. Read the skill before touching them.
|
||||
- **SwiftUI list boundaries** (`cmux-debugging`): no view below a `LazyVStack`/`LazyHStack`/`List`/`ForEach` boundary may hold an observable store reference, and no function called from `body` may write state. Violating either reintroduces the 100% CPU spin loop from https://github.com/manaflow-ai/cmux/issues/2586. Reference pattern: `IndexSectionActions` / `SectionGapActions` / `SessionSearchFn` in `Sources/SessionIndexView.swift`.
|
||||
- **Do not add an app-level display link or manual `ghostty_surface_draw` loop.** Rely on Ghostty wakeups and its renderer, or typing lags.
|
||||
- **Terminal find layering** (`cmux-debugging`): `SurfaceSearchOverlay` mounts from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), never from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
|
||||
- **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.
|
||||
- **Typing-latency-sensitive paths** (read carefully before touching these areas):
|
||||
- `WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift`: called on every event including keyboard. All divider/sidebar/drag routing is gated to pointer events only. Do not add work outside the `isPointerEvent` guard.
|
||||
- `TabItemView` in `ContentView.swift`: uses `Equatable` conformance + `.equatable()` to skip body re-evaluation during typing. Do not add `@EnvironmentObject`, `@ObservedObject` (besides `tab`), or `@Binding` properties without updating the `==` function. Do not remove `.equatable()` from the ForEach call site. Do not read `tabManager` or `notificationStore` in the body; use the precomputed `let` parameters instead.
|
||||
- `TerminalSurface.forceRefresh()` in `GhosttyTerminalView.swift`: called on every keystroke. Do not add allocations, file I/O, or formatting here.
|
||||
- **Terminal find layering contract:** `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
|
||||
- **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`.
|
||||
- **All user-facing strings must be localized.** Use `String(localized: "key.name", defaultValue: "English text")` for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages). Keys go in `Resources/Localizable.xcstrings` with translations for all supported languages (currently English and Japanese). Never use bare string literals in SwiftUI `Text()`, `Button()`, alert titles, etc.
|
||||
- **Localization audit is required for every user-facing change.** Before finishing a task that changes UI, Settings rows, menus, shortcut metadata, schema/config text, docs, command/help text, alerts, or tooltips, enumerate the changed user-facing surfaces and verify each one has entries for every supported locale. `defaultValue`, English fallback text, schema descriptions, or copied English strings do not count as localization. For Swift/AppKit strings, update `Resources/Localizable.xcstrings`; for localized web/docs content, update every supported message catalog (currently `web/messages/en.json` and `web/messages/ja.json`) and any localized data structures that carry inline translations. Parse touched localization files, compare changed message keys across locales, and use `rg` over changed Swift/TS/TSX/docs files for newly introduced bare English. The final handoff must state what localization audit was performed or explicitly say what could not be verified.
|
||||
- **Shortcut policy:** Every new cmux-owned keyboard shortcut must be added to `KeyboardShortcutSettings`, visible/editable in Settings, supported in `~/.config/cmux/cmux.json`, and documented in the keyboard shortcut and configuration docs.
|
||||
- **Snapshot boundary for list subtrees.** In any SwiftUI panel whose `body` contains a `LazyVStack` / `LazyHStack` / `List` / `ForEach` of rows, no view below that boundary may hold a reference to an `ObservableObject` / `@Observable` store (no `@ObservedObject`, `@EnvironmentObject`, `@StateObject`, `@Bindable`, or even a plain `let store: SomeStore` property). Rows and drop-gaps receive immutable value snapshots plus closure action bundles only. Violating this reintroduces the "orthogonal @Published change invalidates every row and thrashes `LazyLayoutViewCache`" class of 100% CPU spin loop that hit the Sessions panel and the workspace sidebar (https://github.com/manaflow-ai/cmux/issues/2586). Reference pattern: `IndexSectionActions` / `SectionGapActions` / `SessionSearchFn` in `Sources/SessionIndexView.swift`.
|
||||
- **No state mutation inside view-body computations.** A function called from `body` (directly or through a helper) must not write `@Published` state, schedule a `Task { @MainActor in store.x = … }`, or `DispatchQueue.main.async` a store write. That creates a re-render feedback loop and pegs the main thread (same root-cause family as the snapshot-boundary rule). State-changing work triggered by "new data appeared" belongs in a `reload()` completion, a `didSet`, or a property-observer — never in the projection that feeds `ForEach`.
|
||||
- **Foundation, SwiftUI, AttributeGraph, and WebKit semantics change silently between macOS major versions.** A function that "obviously" returns the same value on every macOS is not a reliable assumption. Concrete case from https://github.com/manaflow-ai/cmux/issues/4529: `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returns `"/.."` on macOS 14 and 15 but `"/"` on macOS 26 — Apple silently fixed the underlying CFURL normalization. The repo's `macos-26` CI and every maintainer's dev machine were on the fixed-behavior side; every reporter on the issue was on the broken side. Always test on the reporter's macOS before declaring a user-reported repro disproven. AWS M4 Pro builders (`cmux-aws-mac`, `cmux-aws-m4pro`, `aws-m4pro-1..6`) are pre-provisioned on macOS 15.7.4 and the preferred empirical-repro path; see the `regression-hunt` skill in the cmuxterm-hq sibling repo for the full playbook.
|
||||
- **Test files in `cmuxTests/` must be wired into `cmux.xcodeproj/project.pbxproj`.** A `.swift` file added to the worktree without a matching `PBXFileReference` + `PBXSourcesBuildPhase` entry is silently ignored by Xcode and never compiles or runs on CI. Both `xcodebuild test -only-testing:cmuxTests/<TestClass>` and bot reviews pass with "Executed 0 tests" — so the missing wiring is indistinguishable from a clean two-commit red/green regression test until a real user hits the bug. The `workflow-guard-tests` job runs `./scripts/lint-pbxproj-test-wiring.sh` to catch this at PR time; surfaced during the https://github.com/manaflow-ai/cmux/issues/4529 investigation against https://github.com/manaflow-ai/cmux/pull/4536. Add via Xcode (drag the file into the cmuxTests target) or hand-edit the four pbxproj entries; reference any wired sibling like `TabManagerUnitTests.swift` as a template.
|
||||
- **SPM packages live in group folders, and the root workspace mirrors that folder shape exactly.** Every Swift package lives physically under exactly one group directory — `Packages/Shared/<pkg>` (used by both apps), `Packages/iOS/<pkg>` (iOS app only), or `Packages/macOS/<pkg>` (macOS app only) — and `cmux.xcworkspace/contents.xcworkspacedata` has three groups whose container locations are those folders, with every package directory appearing as a FileRef under its folder's group. So opening the workspace shows all packages grouped exactly like the directory tree. The folder is the source of truth: to move a package between groups, `git mv` its directory, then run `python3 scripts/check-workspace-package-groups.py --write` to regenerate the workspace. A new package goes in the group folder matching its consumers (both apps → Shared, iOS only → iOS, macOS only → macOS). Cross-group `.package(path:)` deps use `../../<Group>/<Name>`; never hand-edit the workspace group membership. CI's `python3 scripts/check-workspace-package-groups.py --check` fails on drift.
|
||||
- **Do not ignore cmux-owned `Package.resolved` files.** SwiftPM resolution changes must be visible in PR diffs. Track the root Xcode lockfile and every cmux-owned package-local `Package.resolved` generated by standalone `swift package resolve`, `swift build`, or `swift test`; a package-local lockfile is the source of truth for that package's standalone resolution and is not replaced by `cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`. Vendored third-party directories may preserve their upstream ignore policy, but cmux-owned package `.gitignore` files must not ignore `Package.resolved`. CI's `python3 scripts/check-package-resolved-policy.py` fails if this drifts.
|
||||
- **Submodule safety** (`cmux-ghostty`): push the submodule commit to its remote `main` before committing the pointer in the parent repo. Never commit on a detached HEAD. Verify with `git merge-base --is-ancestor HEAD origin/main`.
|
||||
- **Localize every user-facing string** (`cmux-localization`): `String(localized:)` with keys in `Resources/Localizable.xcstrings`, plus every web message catalog (`web/messages/en.json`, `web/messages/ja.json`). A localization audit is required for any UI, Settings, menu, schema, docs, or help-text change, and the handoff must state what was audited.
|
||||
- **Shortcut policy** (`cmux-keyboard-shortcuts`): every new cmux-owned shortcut goes in `KeyboardShortcutSettings`, is editable in Settings, is supported in `~/.config/cmux/cmux.json`, and is documented.
|
||||
- **Test wiring** (`cmux-testing`): a `.swift` file in `cmuxTests/` without a `PBXFileReference` + `PBXSourcesBuildPhase` entry is silently skipped, and both `xcodebuild test` and bot reviews pass with "Executed 0 tests". `workflow-guard-tests` runs `./scripts/lint-pbxproj-test-wiring.sh` to catch it.
|
||||
- **SPM package groups** (`cmux-architecture`): packages live under `Packages/{Shared,iOS,macOS}/<pkg>` and the workspace mirrors that folder shape. To move one, `git mv` the directory then `python3 scripts/check-workspace-package-groups.py --write`. Never hand-edit workspace group membership.
|
||||
- **Do not gitignore cmux-owned `Package.resolved`.** SwiftPM resolution changes must show in PR diffs; package-local lockfiles are not replaced by the root one. `python3 scripts/check-package-resolved-policy.py` fails on drift.
|
||||
- **"Feature flag" means a remote PostHog runtime flag.** Implement through `CmuxFeatureFlags` with a PostHog key, explicit unavailable fallback, registry metadata, live update behavior, and focused tests. A local override may support dogfood but must not be the production control plane.
|
||||
- **Foundation, SwiftUI, AttributeGraph, and WebKit semantics change between macOS major versions.** `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returns `"/.."` on macOS 14 and 15 but `"/"` on macOS 26 (https://github.com/manaflow-ai/cmux/issues/4529); CI and maintainer machines were all on the fixed side while every reporter was on the broken side. Test on the reporter's macOS before declaring a repro disproven. AWS M4 Pro builders (`aws-m4pro-1..6`) run macOS 15.7.4.
|
||||
|
||||
- **"Feature flag" means a remote PostHog runtime flag.** Unless the user explicitly requests a compile-time flag, local setting, or environment variable, implement feature flags through `CmuxFeatureFlags` with a PostHog key, explicit unavailable fallback, registry metadata, live update behavior, and focused tests. A local override may support dogfood, but it must not be the production control plane.
|
||||
## Shared behavior policy
|
||||
|
||||
## Ghostty submodule workflow
|
||||
When a behavior is exposed through multiple entrypoints (shortcut, command palette, context menu, CLI, settings, debug menu), implement one shared action path and verify every entrypoint. Do not patch one surface and leave the others with duplicated logic.
|
||||
|
||||
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.
|
||||
For optimistic UI or CLI updates, keep one mutation path, record pending state with a request id or previous snapshot, reconcile from the authoritative result, and roll back explicitly on failure. Do not let each entrypoint keep its own optimistic copy.
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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` (the docs changelog page at `web/app/docs/changelog/page.tsx` reads from it)
|
||||
4. Run `./scripts/bump-version.sh` to update both versions
|
||||
5. Commit, run `./scripts/release-pretag-guard.sh`, tag, and push
|
||||
|
||||
Version bumping:
|
||||
|
||||
```bash
|
||||
./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.
|
||||
|
||||
Before creating a release tag, run:
|
||||
|
||||
```bash
|
||||
./scripts/release-pretag-guard.sh
|
||||
```
|
||||
|
||||
If it fails, run `./scripts/bump-version.sh`, commit the build-number bump, then retry tagging.
|
||||
|
||||
Manual release steps (if not using the command):
|
||||
|
||||
```bash
|
||||
./scripts/release-pretag-guard.sh
|
||||
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: update `CHANGELOG.md`; docs changelog is rendered from it.
|
||||
When a user says tests missed a bug, add behavior-level coverage around the exact repro path before claiming the fix is complete.
|
||||
|
||||
## Skills
|
||||
|
||||
Detailed cmux contributor rules live in repo skills under `skills/`; use the task-specific skill before changing that area.
|
||||
Detailed contributor rules live in `skills/`. Use the task-specific skill before changing that area.
|
||||
|
||||
Core skill map:
|
||||
|
||||
- `cmux-dev-workflow`: setup, tagged reloads, Xcode project normalization, sidebar extension tagging, local dev build isolation.
|
||||
- `cmux-architecture`: package boundaries, refactor architecture, file/API discipline, testability, Swift concurrency rules.
|
||||
- `cmux-dev-workflow`: setup, tagged reloads, Xcode project normalization, sidebar extension tagging, build isolation.
|
||||
- `cmux-architecture`: package boundaries, file/API discipline, testability, Swift concurrency.
|
||||
- `cmux-backend`: backend TypeScript, Effect, Cloud VM control plane, provider secrets, Postgres and migrations.
|
||||
- `cmux-billing`: Stripe checkout, entitlements, webhooks, pricing dev stack, live provisioning.
|
||||
- `cmux-debugging`: debug event log, Debug menu, runtime pitfalls, typing-sensitive paths, SwiftUI list boundaries.
|
||||
- `cmux-localization`: user-facing strings, localization files, shortcut text, and localization audit.
|
||||
- `cmux-localization`: user-facing strings, localization files, shortcut text, localization audit.
|
||||
- `cmux-testing`: regression policy, Swift Testing, test quality, test wiring, local vs CI validation.
|
||||
- `cmux-socket-policy`: socket command threading and focus preservation.
|
||||
- `cmux-shared-behavior`: shared action paths for multi-entrypoint behavior and optimistic updates.
|
||||
- `cmux-ghostty`: Ghostty submodule and GhosttyKit workflow.
|
||||
- `cmux-release`: release, version bump, changelog, pretag guard, and release asset workflow.
|
||||
- `cmux-release`: release, version bump, changelog, pretag guard, release assets.
|
||||
|
||||
+13
-35
@@ -1,51 +1,29 @@
|
||||
# cmux Browser contributor notes
|
||||
|
||||
This directory contains the Chromium-based cmux Browser product. It is a
|
||||
source overlay and build harness, not a Chromium checkout. Never vendor
|
||||
Chromium, generated build output, signed applications, update keys, or private
|
||||
builder configuration here.
|
||||
The Chromium-based cmux Browser product: a source overlay and build harness, not a Chromium checkout. Never vendor Chromium, generated build output, signed applications, update keys, or private builder configuration here.
|
||||
|
||||
## Repository boundaries
|
||||
|
||||
- Resolve the cmux TUI backend from `../cmux-tui`.
|
||||
- Resolve Ghostty from the repository's `../ghostty` gitlink.
|
||||
- Keep Chromium overlay paths under `overlay/` identical to their destination
|
||||
paths in a Chromium source tree.
|
||||
- Derive paths from the monorepo and Browser roots. Do not add developer home
|
||||
directories, private hostnames, tailnet addresses, or volume paths.
|
||||
- Pin Chromium with a full commit ID and preserve enough build metadata to
|
||||
reproduce every distributed binary.
|
||||
- Resolve the cmux TUI backend from `../cmux-tui` and Ghostty from the repository's `../ghostty` gitlink.
|
||||
- Keep Chromium overlay paths under `overlay/` identical to their destination paths in a Chromium source tree.
|
||||
- Derive paths from the monorepo and Browser roots. No developer home directories, private hostnames, tailnet addresses, or volume paths.
|
||||
- Pin Chromium with a full commit ID and preserve enough build metadata to reproduce every distributed binary.
|
||||
|
||||
## Licensing and provenance
|
||||
|
||||
Every imported or new source file must have a recorded provenance and license.
|
||||
Do not apply the repository's default license over third-party material.
|
||||
Every imported or new source file has a recorded provenance and license. Do not apply the repository's default license over third-party material.
|
||||
|
||||
- Manaflow rights-controlled files use GPL-3.0-or-later. Commercial terms may
|
||||
be offered separately only for portions whose necessary rights Manaflow
|
||||
controls; authorship alone is not proof of that control.
|
||||
- Manaflow rights-controlled files use GPL-3.0-or-later. Commercial terms may be offered separately only for portions whose necessary rights Manaflow controls; authorship alone is not proof of that control.
|
||||
- Chromium-derived files retain Chromium's BSD-3-Clause notice.
|
||||
- Helium-derived files retain GPL-3.0-only provenance and the exact source
|
||||
revision. Helium-derived code is not available under Manaflow's commercial
|
||||
license.
|
||||
- Other bundled dependencies retain their own terms and must appear in the
|
||||
generated notices and corresponding-source manifest.
|
||||
- Helium-derived files retain GPL-3.0-only provenance and the exact source revision. Helium-derived code is not available under Manaflow's commercial license.
|
||||
- Other bundled dependencies retain their own terms and must appear in the generated notices and corresponding-source manifest.
|
||||
|
||||
When changing a shipped dependency, update its exact revision or digest,
|
||||
source URL, license text, and source-offer record in the same change. A build
|
||||
must fail closed if any shipped file has no license mapping.
|
||||
When changing a shipped dependency, update its exact revision or digest, source URL, license text, and source-offer record in the same change. A build must fail closed if any shipped file has no license mapping.
|
||||
|
||||
Do not add a nested copy of the root `LICENSE` that could be read as licensing
|
||||
third-party or mixed-origin files commercially. Use per-file notices, exact
|
||||
provenance, and a generated release composition manifest.
|
||||
Do not add a nested copy of the root `LICENSE`; it could be read as licensing third-party or mixed-origin files commercially. Use per-file notices, exact provenance, and a generated release composition manifest.
|
||||
|
||||
## Validation
|
||||
|
||||
Run the fast host, patch-fixture, script, and protocol tests before review.
|
||||
Release candidates additionally require a full Chromium build, generated
|
||||
Chromium third-party notices, bundle-license verification, and the macOS
|
||||
XCUITest terminal-render suite against the exact packaged cmux and Ghostty
|
||||
revisions.
|
||||
Run the fast host, patch-fixture, script, and protocol tests before review. Release candidates additionally require a full Chromium build, generated Chromium third-party notices, bundle-license verification, and the macOS XCUITest terminal-render suite against the exact packaged cmux and Ghostty revisions.
|
||||
|
||||
Never run untrusted pull-request code on a self-hosted builder with private
|
||||
network access or signing credentials.
|
||||
Never run untrusted pull-request code on a self-hosted builder with private network access or signing credentials.
|
||||
|
||||
@@ -7,153 +7,124 @@ description: "cmux package architecture, refactor layering, dependency inversion
|
||||
|
||||
## Package architecture
|
||||
|
||||
We are migrating cmux from a single app target into Swift Packages under `Packages/`. Every new package must satisfy three rules:
|
||||
cmux is migrating from a single app target into Swift Packages under `Packages/`. Every new package must be:
|
||||
|
||||
- **Ergonomic.** Public API surface matches what callers naturally want to write. Default to internal access; expose `public` only for types and functions that downstream consumers actually use. Avoid friction such as forcing every call site through a builder or wrapper when a direct API is fine.
|
||||
- **No dependency cycles.** Packages form a strict DAG. A package may only depend on packages strictly lower in the graph. When two packages need to share a type, lift it to a common lower-level package or define a protocol seam in the consumer. Every new dependency edge requires re-checking that the graph stays acyclic.
|
||||
- **Clear but not overly narrow responsibilities.** A package owns one full domain (e.g. _settings_, _appearance_, _workspace_, _terminal_, _browser_, _command palette_), not a slice of one. A package called "appearance math" or "workspace model" is too narrow — it forces every consumer that touches the surrounding domain to also depend on the sibling slices. Prefer a single `CmuxAppearance` that owns settings, theming, colors, glass, and snapshots together, over `CmuxAppearanceMath` + `CmuxAppearanceTheme` + `CmuxAppearanceSettings`. Don't fragment a domain into `CmuxFooFormatting` + `CmuxFooLogic` + `CmuxFooState` — that's folder structure inside a single package, not module structure. A package boundary exists because more than one consumer needs the contents, or a build/test seam needs to exist.
|
||||
- **Ergonomic.** Default to internal access; `public` only what downstream consumers actually use.
|
||||
- **Acyclic.** Packages form a strict DAG. Share a type by lifting it to a lower package or defining a protocol seam in the consumer. Every new dependency edge requires re-checking that the graph stays acyclic.
|
||||
- **Whole-domain.** One package owns a full domain (settings, appearance, workspace, terminal, browser, command palette). `CmuxAppearanceMath` + `CmuxAppearanceTheme` + `CmuxAppearanceSettings` is folder structure inside `CmuxAppearance`, not module structure. A boundary exists because more than one consumer needs the contents, or a build/test seam must exist.
|
||||
|
||||
When in doubt, **extract leaf-first**: pull out the package that has no internal dependencies. Consumers in the app target stay put and only update imports. Each leaf shrinks the app target without requiring downstream packages to exist yet.
|
||||
When in doubt, extract leaf-first: the package with no internal dependencies. Existing packages under `Packages/` predate this policy; do not use them as design references.
|
||||
|
||||
The existing packages under `Packages/` predate this policy and should not be used as design references.
|
||||
Wiring a new package into `cmux.xcodeproj` needs explicit pbxproj entries in **both** the `cmux` and `cmux-unit` targets. See [references/package-boundaries.md](references/package-boundaries.md).
|
||||
|
||||
**Wiring a new local package into the project.** `cmux.xcodeproj` lists package dependencies explicitly (it is not a synchronized-folder project). Adding `Packages/CmuxFoo` means mirroring an existing package's `project.pbxproj` entries — one `XCLocalSwiftPackageReference` (in the project's `packageReferences`), one `XCSwiftPackageProductDependency`, and a `PBXBuildFile` linked in the Frameworks phase of **every** target that imports it. The app-target packages link into **both** `cmux` and `cmux-unit` (so tests can `import` and inject them); copy a recent leaf like `CmuxSocketControl` for the exact shape, then run `scripts/normalize-pbxproj.py` and `scripts/check-pbxproj.sh`. A package the app builds against but `cmux-unit` does not link will compile the app yet fail the test target.
|
||||
**Group folders.** Every package lives physically under exactly one group directory: `Packages/Shared/<pkg>` (both apps), `Packages/iOS/<pkg>` (iOS only), or `Packages/macOS/<pkg>` (macOS only). `cmux.xcworkspace/contents.xcworkspacedata` mirrors that folder shape, with three groups whose container locations are those folders and every package directory as a FileRef under its folder's group. The folder is the source of truth: to move a package, `git mv` the directory then run `python3 scripts/check-workspace-package-groups.py --write`. Cross-group `.package(path:)` deps use `../../<Group>/<Name>`. Never hand-edit workspace group membership. CI runs `python3 scripts/check-workspace-package-groups.py --check` and fails on drift.
|
||||
|
||||
## Refactor architecture: layers, Coordinator/Service/Repository, dependency inversion
|
||||
**Lockfiles.** Do not gitignore cmux-owned `Package.resolved` files; SwiftPM resolution changes must be visible in PR diffs. Track the root Xcode lockfile and every cmux-owned package-local `Package.resolved` produced by standalone `swift package resolve` / `swift build` / `swift test`. A package-local lockfile is the source of truth for that package's standalone resolution and is not replaced by `cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`. Vendored third-party directories may keep their upstream ignore policy. CI runs `python3 scripts/check-package-resolved-policy.py`.
|
||||
|
||||
These higher-level patterns are binding on every new or moved/meaningfully-rewritten file. (The full blueprint, with worked examples and the per-god decomposition, lives in the cmuxterm-hq control repo under `docs/cmux-refactor-audit/blueprint/`; the enforceable core is below.)
|
||||
**Feature flags mean remote PostHog runtime flags.** Unless the user explicitly asks for a compile-time flag, local setting, or environment variable, implement a feature flag through `CmuxFeatureFlags` with a PostHog key, an explicit unavailable fallback, registry metadata, live update behavior, and focused tests. A local override may support dogfood but must not be the production control plane.
|
||||
|
||||
**Layered, downward-only DAG.** Packages form a strict acyclic graph in five layers; dependencies point only downward:
|
||||
## Layers
|
||||
|
||||
1. **Core** (e.g. `CmuxCore`) — pure `Sendable` values, IDs, DTOs, errors, and the protocol seams shared across domains. No AppKit/SwiftUI/I/O. The lift target when two domains need the same type.
|
||||
2. **Services / infrastructure** — `actor`s implementing core protocols against the outside world (process/PTY, filesystem, sockets, web API, notifications, auth). One package per cohesive capability.
|
||||
3. **Domain / state** — `@MainActor @Observable` models + Coordinators, one package per feature domain; owns that domain's mutable state. `CmuxSettings` is the exemplar.
|
||||
4. **UI** — SwiftUI/AppKit views, one UI package per domain package, depending only on its domain package + Core, never on a Service directly. `CmuxSettingsUI` is the exemplar.
|
||||
5. **Executable** (`cmuxApp` / `AppDelegate`) — a thin composition shim, no business logic.
|
||||
Five layers, dependencies point only downward:
|
||||
|
||||
**Classify every extracted entity by intent:**
|
||||
1. **Core** (`CmuxCore`): pure `Sendable` values, IDs, DTOs, errors, shared protocol seams. No AppKit/SwiftUI/I/O. The lift target when two domains need the same type.
|
||||
2. **Services / infrastructure**: `actor`s implementing core protocols against the outside world (process/PTY, filesystem, sockets, web API, notifications, auth). One package per cohesive capability.
|
||||
3. **Domain / state**: `@MainActor @Observable` models plus Coordinators, one package per feature domain, owning that domain's mutable state. Exemplar `CmuxSettings`.
|
||||
4. **UI**: SwiftUI/AppKit views, one UI package per domain package, depending only on its domain package plus Core, never a Service directly. Exemplar `CmuxSettingsUI`.
|
||||
5. **Executable** (`cmuxApp` / `AppDelegate`): thin composition shim, no business logic.
|
||||
|
||||
- **Coordinator** — a `@MainActor @Observable` orchestrator that sequences a user flow and owns navigation/selection/lifecycle state, calling Services and child models. Does no I/O itself.
|
||||
- **Service** — an `actor` (or `@MainActor` only when an AppKit main-thread API forces it) performing one outside-world capability; exposes `async`/`await` + `AsyncStream`, holds only its own resource handles, holds no UI state.
|
||||
- **Repository** — an `actor` mediating one persistence source of truth (file, defaults, web API) behind CRUD-shaped async methods returning value types. Precedents: `JSONConfigStore`, `UserDefaultsSettingsStore`.
|
||||
Classify every extracted entity by intent:
|
||||
|
||||
**Dependency inversion.** Lower packages publish protocols; concrete Services/Repositories conform; higher layers depend on `any Protocol`, never the concrete type. Share a type by lifting it to Core or defining a protocol seam in the consumer — never a stored property reaching across modules. Injection is constructor (`init`) injection only: no global container, no singleton, no `static let shared`. The **executable app target is the single composition root** — the one place concretes are named and the object graph is assembled. SwiftUI `Environment` may carry already-constructed `@Observable` models down a view tree (as `SettingsRuntime` does), but is never the source of truth for service wiring.
|
||||
- **Coordinator**: `@MainActor @Observable` orchestrator that sequences a user flow and owns navigation/selection/lifecycle state, calling Services and child models. Does no I/O itself.
|
||||
- **Service**: `actor` (or `@MainActor` only when an AppKit main-thread API forces it) performing one outside-world capability; exposes `async`/`await` plus `AsyncStream`; holds only its own resource handles and no UI state.
|
||||
- **Repository**: `actor` mediating one persistence source of truth (file, defaults, web API) behind CRUD-shaped async methods returning value types. Precedents: `JSONConfigStore`, `UserDefaultsSettingsStore`.
|
||||
|
||||
**State + SwiftUI wiring.** Domain state lives in `@MainActor @Observable` models (never `ObservableObject`/`@Published`). A god model decomposes into cohesive child `@Observable` sub-models owned by their domain packages and composed by the home object via held references; cross-domain reads go behind read-only protocols. In views use `@State` (owned), `@Bindable` / plain `let` (passed-in), or `@Environment(M.self)` + `.environment(...)` (injected) — never `@StateObject` / `@ObservedObject` / `@EnvironmentObject` / `.environmentObject(_:)`.
|
||||
**Dependency inversion.** Lower packages publish protocols; concrete Services/Repositories conform; higher layers depend on `any Protocol`, never the concrete type, and never a stored property reaching across modules. Constructor (`init`) injection only: no global container, no singleton, no `static let shared`. The executable app target is the single composition root, the one place concretes are named and the object graph is assembled. SwiftUI `Environment` may carry already-constructed `@Observable` models down a view tree (as `SettingsRuntime` does), never service wiring.
|
||||
|
||||
**Executable-target boundary (three hard constraints — invert, never work around):**
|
||||
**State and SwiftUI.** Domain state lives in `@MainActor @Observable` models, never `ObservableObject`/`@Published`. A god model decomposes into cohesive child `@Observable` sub-models owned by their domain packages and composed by held reference; cross-domain reads go behind read-only protocols. In views use `@State` (owned), `@Bindable` or plain `let` (passed in), or `@Environment(M.self)` plus `.environment(...)` (injected). Never `@StateObject` / `@ObservedObject` / `@EnvironmentObject` / `.environmentObject(_:)`.
|
||||
|
||||
1. `@main` `cmuxApp` and `AppDelegate` stay in the executable target as the thin composition shim; that residual is the intended end state, not debt.
|
||||
2. A type is declared in exactly one module and a lower package cannot extend a higher-owned type, so `AppDelegate+*` / `cmuxApp+*` / `Workspace+*` extensions do not move down: extract the behavior into a Coordinator/Service/Repository, have the god object own an instance, and reduce the extension to a one-line forward.
|
||||
3. Stored properties cannot cross module boundaries: decompose god-model state into child `@Observable` sub-models owned by domain packages, composed by held reference, with cross-cutting reads behind read-only protocols.
|
||||
**Executable-target boundary (invert, never work around):**
|
||||
|
||||
1. `@main` `cmuxApp` and `AppDelegate` stay in the executable target as the thin composition shim. That residual is the intended end state, not debt.
|
||||
2. A type is declared in exactly one module and a lower package cannot extend a higher-owned type, so `AppDelegate+*` / `cmuxApp+*` / `Workspace+*` extensions do not move down. Extract the behavior into a Coordinator/Service/Repository, have the god object own an instance, and reduce the extension to a one-line forward.
|
||||
3. Stored properties cannot cross module boundaries. Decompose god-model state into child `@Observable` sub-models owned by domain packages, composed by held reference, with cross-cutting reads behind read-only protocols.
|
||||
|
||||
## File organization
|
||||
|
||||
One major type per file. Each `struct`, `class`, `enum`, `actor`, or `protocol` that is part of a public API (or has any meaningful body) lives in its own file named after the type (`Control.swift`, `LabeledChoice.swift`, `ListControl.swift` — not one shared `SettingControl.swift`). This rule applies to all new code in `Packages/` and to any new files added to the app target.
|
||||
One major type per file, named after the type (`Control.swift`, `LabeledChoice.swift`, `ListControl.swift`, not one shared `SettingControl.swift`). Applies to all new code in `Packages/` and all new app-target files.
|
||||
|
||||
- Small, closely-bound helpers (`private struct`, nested types, single-line extensions used only inside the file) can stay with the parent type. Anything bigger or independently meaningful gets its own file.
|
||||
- Trivial private helpers, nested types, and single-line extensions used only inside the file may stay with the parent type. Anything with a meaningful body gets its own file, including a `private final class` nested in another file's type.
|
||||
- Conformance-adding extensions for a type defined elsewhere go in `TypeName+Conformance.swift` or `TypeName+Feature.swift`, not bundled into the consuming feature file.
|
||||
- Type-erased wrappers (`AnyFoo`) live next to the type they erase (`Foo.swift` and `AnyFoo.swift`), each in its own file.
|
||||
- Existing god files (`ContentView.swift`, `Workspace.swift`, `TabManager.swift`, `cmuxApp.swift`) are the pattern this rule exists to stop. When migrating code out of them, split into one file per type even if it triples the file count. File count is cheap; "find this type" being unanswerable is expensive.
|
||||
- Type-erased wrappers live next to what they erase: `Foo.swift` and `AnyFoo.swift`.
|
||||
- The god files (`ContentView.swift`, `Workspace.swift`, `TabManager.swift`, `cmuxApp.swift`) are what this rule exists to stop. Splitting one file per type is correct even if it triples the file count. File count is cheap; "find this type" being unanswerable is expensive.
|
||||
|
||||
## Documentation
|
||||
|
||||
Every `public` symbol in any new Swift package under `Packages/` is documented with a Swift-DocC triple-slash comment at the time of writing. Treat docs as part of the API surface, not as follow-up work.
|
||||
Every `public` symbol in a new package under `Packages/` gets a Swift-DocC `///` comment at the time it is written. Docs are part of the API surface, not follow-up work.
|
||||
|
||||
- **Format.** Use `///` doc comments above the symbol. First line is a one-sentence summary that fits on a single line and ends with a period. If more context is needed, leave a blank `///` line, then add a discussion paragraph. Use `- Parameter name:` / `- Returns:` / `- Throws:` callouts on `init` and `func` symbols that take parameters or throw. Use Markdown freely (bold, fenced code blocks for examples, backticks for inline code).
|
||||
- **Cross-references.** Refer to other symbols using double-backticks: `` ``CmuxSetting`` ``. Plain backticks are for non-symbol code (`UserDefaults.standard`, `@AppStorage`).
|
||||
- **What to document on each symbol.** Types: what they represent and when to use them. Enums: meaning of each case. Init parameters: especially defaults and the reason for them. Properties: what value they hold and any invariants. Methods: what they do, plus parameters/returns/throws. Generic constraints: which `Value` / `Element` shapes the type accepts and why (e.g., `Sendable & Codable`).
|
||||
- **Examples.** Non-trivial APIs get at least one example in a fenced ` ```swift ` block, ideally a real declaration from this codebase. Keep examples short and idiomatic.
|
||||
- **Internal vs public.** `internal` and `private` symbols get a one-line `///` when the intent is non-obvious; verbosity is not required at that scope. The public boundary is the one that needs full coverage.
|
||||
- **No stale docs.** When you change a symbol's behavior or signature, update its doc comment in the same edit. Docs that describe last week's behavior are worse than no docs.
|
||||
- **Don't comment-narrate the body.** Doc comments describe the contract from the outside. Inline `//` comments inside method bodies are reserved for non-obvious *why*, not *what* (the existing rule from the top-level guidance still applies).
|
||||
- First line is a one-sentence summary that fits on one line and ends with a period. Blank `///` line before any discussion paragraph. Use `- Parameter name:` / `- Returns:` / `- Throws:` on `init` and `func` symbols that take parameters or throw. Markdown is fine.
|
||||
- Reference symbols with double backticks (`` ``CmuxSetting`` ``); plain backticks for non-symbol code (`UserDefaults.standard`).
|
||||
- Document what a type represents and when to use it, the meaning of each enum case, init parameter defaults and their reason, property invariants, method behavior, and which generic `Value`/`Element` shapes are accepted and why.
|
||||
- Non-trivial APIs get at least one short example in a fenced `swift` block, ideally a real declaration from this codebase.
|
||||
- `internal` and `private` symbols get a one-line `///` when the intent is non-obvious. The public boundary is the one that needs full coverage.
|
||||
- Update the doc comment in the same edit that changes behavior or signature. Doc comments describe the contract from outside; inline `//` is reserved for non-obvious *why*.
|
||||
|
||||
This rule applies to all packages under `Packages/`. Code in the main app target is not retroactively required to be documented, but new `public` symbols added to packages must be.
|
||||
Main app target code is not retroactively required to be documented.
|
||||
|
||||
## Package design discipline
|
||||
|
||||
These are the recurring design mistakes that have to be caught at the design step, not at code review:
|
||||
|
||||
- **No shared-singleton accessors.** `static let standard` / `shared` / `default` on a package type that holds runtime state is a singleton-by-another-name. Construct the package type at the app's startup site and inject it. `static let` is fine for *declarations* — identifiers, schema entries, enum cases — but not for behavior.
|
||||
- **No namespace-enums.** `enum Foo { static func bar() }` (a no-case enum used as a namespace) is a fake namespace that fights the rest of the design (no instances, no DI, no test seam). Prefer a value-typed struct passed via constructor when the helper might gain configuration, or a file-scope `private func` for pure helpers internal to one file.
|
||||
- **No parallel hand-maintained registries.** When a list mirrors a set of declared items (e.g. `catalog.all` mirroring the catalog's stored properties), derive the list via `Mirror` reflection or a macro. Two sources of truth drift silently; the IDE doesn't tell you.
|
||||
- **Prefer compile-time invariants to runtime traps.** If the pattern is `guard ... else { assertionFailure(...); return default }` for a "programmer error" case, encode it in the type system (phantom types, separate concrete flavors). Runtime traps become silent fallbacks in release builds.
|
||||
- **No free functions.** Functionality is always scoped to an entity that owns the responsibility: a method on a value type, an extension on the type the operation belongs to, or a member of the Coordinator/Service/Repository that uses it. Top-level `func` declarations (any visibility, including file-scope `private func`) are banned. The only sanctioned exception is a `@convention(c)` trampoline a C API forces on us, marked with a one-line justification.
|
||||
- **Nested types still count for the one-major-type-per-file rule.** A `private final class WatcherAttachment` inside `JSONConfigFileWatcher.swift` is a major type. Move it to its own file the moment it has a meaningful body.
|
||||
- **No shared-singleton accessors.** `static let standard` / `shared` / `default` on a package type holding runtime state is a singleton by another name. Construct at the app startup site and inject. `static let` is fine for declarations (identifiers, schema entries, enum cases), not for behavior.
|
||||
- **No namespace-enums.** `enum Foo { static func bar() }` is a fake namespace with no instances, no DI, and no test seam. Prefer a value-typed struct passed via constructor when the helper might gain configuration.
|
||||
- **No parallel hand-maintained registries.** When a list mirrors declared items (`catalog.all` mirroring stored properties), derive it via `Mirror` reflection or a macro. Two sources of truth drift silently.
|
||||
- **Prefer compile-time invariants to runtime traps.** `guard ... else { assertionFailure(...); return default }` for a "programmer error" case should be encoded in the type system (phantom types, separate concrete flavors). Runtime traps become silent fallbacks in release builds.
|
||||
- **No free functions.** Top-level `func` declarations, any visibility including file-scope `private func`, are banned; scope functionality to the entity that owns the responsibility. The only sanctioned exception is a `@convention(c)` trampoline a C API forces, with a one-line justification.
|
||||
|
||||
## Testability
|
||||
|
||||
Every public type added to `Packages/` must be **testable from a test target** without launching the app target, without booting AppKit, and without depending on the user's filesystem or `UserDefaults.standard`. Production-grade designs surface a test seam at every boundary:
|
||||
Every public type added to `Packages/` must be testable from a test target without launching the app target, booting AppKit, or depending on the user's filesystem or `UserDefaults.standard`.
|
||||
|
||||
- **No global state in package code.** Every public type that needs `UserDefaults`, `FileManager`, an on-disk path, an environment variable, or a clock takes it via initializer parameter. Tests pass a `UserDefaults(suiteName:)` scoped to the test, a temp directory URL, a fixed `Date`, etc.
|
||||
- **No reliance on `.shared` / `.standard`.** A public type that hardcodes `UserDefaults.standard` or `FileManager.default` inside its implementation cannot be tested without polluting the developer's actual settings. Inject these at the seam.
|
||||
- **Test through injected seams, never a static test hook.** A `nonisolated(unsafe) static var fooForTesting` (or any global mutable "override" a test swaps in) is global state by another name: it leaks across tests, forces `nonisolated(unsafe)`, and usually needs a lock. Replace it with a protocol seam injected through `init` (e.g. `init(commandRunner: any CommandRunning = CommandRunner())`); the test passes a conforming fake. When you extract such a type into a package, deleting the static hook (and the lock it required) is part of the extraction, not a follow-up.
|
||||
- **Public APIs return values, not side effects, where possible.** A function that mutates global UserDefaults and returns `Void` is harder to test than one that returns the changed value and lets the caller persist. Prefer pure transformations + thin imperative layers.
|
||||
- **Asynchronous APIs surface their observation as `AsyncStream`.** Tests can iterate `AsyncStream` deterministically and assert the sequence of yielded values. Avoid `NotificationCenter`-only patterns where the test has to spin a runloop.
|
||||
- **Document the test pattern** alongside any non-trivial public surface. The package's `README.md` and any DocC catalog should show how to instantiate the type with test-friendly dependencies.
|
||||
- `UserDefaults`, `FileManager`, on-disk paths, environment variables, and clocks arrive through `init` parameters. Tests pass a `UserDefaults(suiteName:)` scoped to the test, a temp directory URL, a fixed `Date`.
|
||||
- No implementation hardcodes `.shared` / `.standard`.
|
||||
- **No static test hooks.** A `nonisolated(unsafe) static var fooForTesting` (or any global mutable override a test swaps in) leaks across tests and usually needs a lock. Replace it with a protocol seam through `init`, e.g. `init(commandRunner: any CommandRunning = CommandRunner())`. Deleting the static hook and its lock is part of the extraction, not a follow-up.
|
||||
- Prefer returning the changed value and letting the caller persist over mutating global state and returning `Void`.
|
||||
- Surface observation as `AsyncStream` so tests can assert the yielded sequence, rather than `NotificationCenter`-only patterns that need a runloop spin.
|
||||
- Show the test-instantiation pattern in the package `README.md` or DocC catalog.
|
||||
|
||||
If a design is hard to test, it is wrong. Reach for the constructor parameter list, not the test bench.
|
||||
|
||||
## Modern Swift concurrency
|
||||
## Swift 6 concurrency
|
||||
|
||||
All new code in `Packages/` and any new files added to the app target use Swift 6 concurrency primitives: `actor`, `async`/`await`, `AsyncStream`/`AsyncSequence`, `@Observable`, `@MainActor`. Old primitives — locks, manual KVO, `@Published`, completion handlers, `DispatchQueue` used as a serial lock — are not allowed.
|
||||
New code in `Packages/`, new app-target files, and meaningful rewrites use `actor`, `async`/`await`, `AsyncStream`/`AsyncSequence`, `@Observable`, and `@MainActor`.
|
||||
|
||||
If you find yourself reaching for a lock to protect ongoing mutable shared state, the type is almost always the wrong shape — promote it to an `actor`. The exception is the narrow lock carve-out below.
|
||||
**Forbidden without a written justification in the PR description:**
|
||||
|
||||
**Do not introduce a single-method `actor` purely as a mutex.** An `actor Guard { func claim() -> Bool }` whose only job is to guard a flag is a lock with extra ceremony: it forces synchronous callers — a `Process` termination handler, a `DispatchSource` event handler, a `withCheckedContinuation` resume race — through `Task { await guard.claim() }`, which adds suspension points, ordering hops, and reentrancy surface to what is fundamentally a synchronous compare-and-set. That makes the code worse, not safer. A tiny synchronous guard like that belongs in the lock carve-out, not an actor.
|
||||
- **Locks**: `NSLock`, `NSRecursiveLock`, `os_unfair_lock`, `OSAllocatedUnfairLock`, `pthread_mutex_t`, `Synchronization.Mutex`, `DispatchSemaphore` used as a lock. Ongoing mutable shared state belongs in an `actor` with `async` reads and writes.
|
||||
- **KVO via `NSObject` subclassing** to override `observeValue(forKeyPath:...)` or call `addObserver(_:forKeyPath:...)`. Use `NotificationCenter.default.notifications(named:)` or the `NSKeyValueObservation` token API at the seam only.
|
||||
- **`DispatchQueue` as a synchronization primitive** (`queue.sync { ... }` serializing mutable state). Queues are fine for event delivery, not for protecting state.
|
||||
- **Combine for change propagation**: `@Published`, `ObservableObject`, `PassthroughSubject`/`CurrentValueSubject`, `AnyCancellable`.
|
||||
- **Completion-handler public APIs** (`(Result<T, Error>) -> Void`, `(T?, Error?) -> Void`). Use `async throws -> T`; wrap a legacy callback with `withCheckedContinuation`/`withCheckedThrowingContinuation` confined to that one seam.
|
||||
- **`DispatchQueue.main.async`**. Annotate the destination `@MainActor` and `await` it.
|
||||
- **Sleeping as a synchronization substitute**: any sleep used to poll for a condition, let state settle before reading, or race a callback/animation. `DispatchQueue.asyncAfter` is banned outright (not cancellable by structure, not testable).
|
||||
- **A single-method `actor` used as a mutex.** `actor Guard { func claim() -> Bool }` forces synchronous callers (a `Process` termination handler, a `DispatchSource` event handler, a `withCheckedContinuation` resume race) through `Task { await guard.claim() }`, adding suspension points, ordering hops, and reentrancy surface to a fundamentally synchronous compare-and-set. Use the lock carve-out instead.
|
||||
|
||||
When **extracting** existing code that uses a forbidden primitive into a package, reconsider the shape at the seam rather than copying it blindly — usually it wants an `actor`. But a one-shot single-resume guard (a `Process` termination handler vs. a timeout vs. a spawn failure racing to resume one `withCheckedContinuation`) is exactly a case the lock carve-out covers: keep a synchronous primitive, hidden behind the type. Drain `Process` pipes concurrently on detached tasks keyed by the raw fd (an `Int32` is `Sendable`; a `FileHandle` is not).
|
||||
**Required shape**: mutable shared state to an `actor` with `async` accessors and an `AsyncStream` for observers; SwiftUI-render-friendly state to an `@Observable @MainActor` view model subscribing to that stream and projecting snapshots (never read actor state synchronously from view code); cross-process and cross-thread invariants expressed through actor isolation; new public observable surfaces as `AsyncStream`/`AsyncSequence`.
|
||||
|
||||
**Forbidden in new code (no exceptions without a written justification in the PR description):**
|
||||
**Carve-outs**, each with a one-line justification comment on the declaration and hidden behind an `AsyncStream` or `actor` surface so callers never see them:
|
||||
|
||||
- **Locks.** `NSLock`, `NSRecursiveLock`, `os_unfair_lock`, `OSAllocatedUnfairLock`, `pthread_mutex_t`, `Synchronization.Mutex`, `DispatchSemaphore` used as a lock. Use `actor` isolation. Mutable shared state belongs in an actor; reads and writes are `async`. (Narrow carve-out below: a lock is allowed where the `actor`/`async` alternative would genuinely worsen the code, with justification.)
|
||||
- **KVO via `NSObject` subclassing.** Any `class Foo: NSObject` whose purpose is to override `observeValue(forKeyPath:...)` or call `addObserver(_:forKeyPath:...)`. Replace with `NotificationCenter.default.notifications(named:)` `AsyncSequence`, or the `NSKeyValueObservation` token API at the seam only.
|
||||
- **`DispatchQueue` used as a synchronization primitive.** A `DispatchQueue(label:)` accessed via `queue.sync { ... }` to serialize mutable state is a lock with different syntax. Use an `actor`. Queues are fine for *event delivery* (e.g. a `DispatchSource` handler), not for protecting state.
|
||||
- **Combine for change propagation.** No `@Published`, no `ObservableObject`, no `PassthroughSubject`/`CurrentValueSubject`, no `AnyCancellable` for change observation. Use `@Observable` (Observation framework, Swift 5.9+) for SwiftUI state, or `AsyncStream`/`AsyncSequence` for cross-actor change propagation.
|
||||
- **Completion-handler APIs.** Authoring a new public API with a `(Result<T, Error>) -> Void` or `(T?, Error?) -> Void` callback is forbidden. Use `async throws -> T`. When wrapping a legacy callback at the boundary, use `withCheckedContinuation`/`withCheckedThrowingContinuation` and keep it confined to that one seam.
|
||||
- **`DispatchQueue.main.async { ... }`.** Annotate the destination with `@MainActor`. Call sites either `await` the main-isolated function or are themselves `@MainActor`.
|
||||
- **Sleeping as a synchronization substitute.** `Task.sleep` / `Clock.sleep` (or any sleep) used to *poll* for a condition, to let state "settle" before reading it, or to *race* a callback/animation is forbidden — use a real signal (`AsyncStream`, `NSKeyValueObservation`, a completion, a state change). `DispatchQueue.asyncAfter` is banned outright (it is neither cancellable-by-structure nor testable). A *bounded, cancellable, intended* delay or deadline is allowed under the `Clock.sleep` carve-out below.
|
||||
- `DispatchSource.makeFileSystemObjectSource` for file watching, `makeReadSource`/`makeWriteSource` for low-level socket I/O (no async-native replacement).
|
||||
- A bounded, cancellable `Clock.sleep` (preferred) or `Task.sleep` for a genuine delay or deadline that is itself the intended behavior (minimum display duration, auto-dismiss, check timeout). Drive it from an injected `Clock` so tests advance virtual time, store the `Task`, and cancel it on the relevant lifecycle transition. Never to poll, settle, or race.
|
||||
- `DispatchSource.makeTimerSource` (one-shot) only when a genuine deadline must fire outside any async context, in a non-`async` type with no `Task` to host the sleep. Prefer `Clock.sleep` whenever the code is already async or actor-isolated; a raw timer is not cancellation-integrated or testable and has suspend/resume/cancel footguns.
|
||||
- A lock for a short, non-blocking synchronous compare-and-set called from non-async callbacks. Canonical case: several synchronous `Process`/`DispatchSource` callbacks race to resume one `withCheckedContinuation` exactly once, guarded by `OSAllocatedUnfairLock(initialState:)` over a `Bool`. Not for guarding ongoing domain state.
|
||||
- `NSKeyValueObservation` token when wrapping a Foundation/AppKit type that exposes change only via KVO.
|
||||
|
||||
**Required shape:**
|
||||
When extracting existing code that uses a forbidden primitive, reshape it at the seam instead of copying it; usually it wants an `actor`. Drain `Process` pipes concurrently on detached tasks keyed by the raw fd (`Int32` is `Sendable`, `FileHandle` is not).
|
||||
|
||||
- Mutable shared state → `actor`. Reads/writes/reset are `async`. Observers receive `AsyncStream` returned by the actor.
|
||||
- SwiftUI view-render-friendly state → `@Observable @MainActor` view-model that subscribes to the actor's `AsyncStream` and projects snapshots. Don't read actor state synchronously from view code.
|
||||
- Cross-process / cross-thread invariants → expressed via actor isolation, not via locks or queues.
|
||||
- New public observable surfaces → `AsyncStream` or `AsyncSequence`. Not callbacks, not `@Published`, not raw `NotificationCenter` subscription.
|
||||
`@unchecked Sendable` and `nonisolated(unsafe)` require a comment explaining the safety argument, or the diff is rejected. `@unchecked Sendable` on an entire actor or struct is almost always wrong; prefer `nonisolated(unsafe) let` on the single non-Sendable property.
|
||||
|
||||
**Acceptable with a one-line justification comment on the declaration:**
|
||||
|
||||
These low-level primitives have no async-native replacement. They must be hidden behind an `AsyncStream` or `actor` surface; callers never see them.
|
||||
|
||||
- `DispatchSource.makeFileSystemObjectSource` for file watching (no Foundation async equivalent).
|
||||
- `DispatchSource.makeReadSource`/`makeWriteSource` for low-level socket I/O.
|
||||
- **A bounded, cancellable `Clock.sleep` (preferred) or `Task.sleep` for a genuine delay/deadline** that is itself the intended behavior — a minimum display duration, an auto-dismiss, a check timeout. Drive it from an *injected* `Clock` (or a duration) so tests advance virtual time with no real waiting, and wire the sleeping `Task`'s cancellation to the relevant lifecycle so a state transition cancels the pending delay (store the `Task`, cancel it on transition; or use `withTaskCancellationHandler`). For true delays/deadlines only, never to poll, settle, or race — those still require a real signal. One-line justification on the call site.
|
||||
- `DispatchSource.makeTimerSource` (one-shot) **only when a genuine deadline must fire outside any async context** — a non-`async` type with no `Task` to host the sleep. Prefer the `Clock.sleep` carve-out above whenever the code is already on an actor or in async code (it is cancellation-integrated and testable; a raw `DispatchSource` timer is not, and has suspend/resume/cancel footguns). Hide the timer behind the type, cancel it on the non-timeout path, and never use it to poll or fake a sleep.
|
||||
- A **lock for a synchronous compare-and-set called from non-async callbacks**, where promoting to an `actor` would only add `Task`/`await` hops and reentrancy. The canonical case is a one-shot resume guard: several synchronous `Process`/`DispatchSource` callbacks race to resume one `withCheckedContinuation` exactly once. `OSAllocatedUnfairLock(initialState:)` guarding a `Bool` (claimed once, checked synchronously in each callback) is correct, deterministic, and lets the callback resume the continuation inline. This carve-out is for short, non-blocking critical sections over a tiny flag/counter — not for guarding ongoing domain state (that is still an `actor`). Keep it private to the type, with a one-line justification.
|
||||
- `NSKeyValueObservation` token (the closure-based API) when wrapping a Foundation/AppKit type that exposes change only via KVO.
|
||||
|
||||
**`@unchecked Sendable` and `nonisolated(unsafe)`:**
|
||||
|
||||
Both require a comment on the declaration explaining the safety argument. Examples that pass review:
|
||||
|
||||
```swift
|
||||
// Wraps DispatchSourceFileSystemObject; every mutation happens on `queue`.
|
||||
private final class WatcherAttachment: @unchecked Sendable { ... }
|
||||
|
||||
// UserDefaults is Apple-documented thread-safe; OK to read nonisolated.
|
||||
private nonisolated(unsafe) let defaults: UserDefaults
|
||||
```
|
||||
|
||||
Without a justification comment, the diff is rejected. `@unchecked Sendable` on an entire actor or struct is almost always wrong; prefer `nonisolated(unsafe) let` on the single non-Sendable property.
|
||||
|
||||
**Scope and enforcement:**
|
||||
|
||||
- Applies to: every new file in `Packages/`, every new file in the app target, every meaningful rewrite of an existing Swift file.
|
||||
- Existing app target code may continue to use the old primitives until rewritten. Do not retrofit blindly.
|
||||
- Code review checklist (Codex, CodeRabbit, Greptile, and human reviewers): reject diffs that introduce `@Published`/`ObservableObject`/`DispatchQueue.main.async`/`addObserver(_:forKeyPath:...)`/`DispatchQueue.asyncAfter` in new code, or `Task.sleep`/`Clock.sleep` used to poll, settle, or race rather than as a bounded, cancellable, injected-clock delay with justification. Reject a lock (`NSLock`/`OSAllocatedUnfairLock`/etc.) or `@unchecked Sendable`/`nonisolated(unsafe)` unless it falls under a documented carve-out *and* carries a one-line justification — and reject a single-method `actor` that exists only to guard a flag (use the lock carve-out instead).
|
||||
Existing app-target code may keep the old primitives until rewritten. Do not retrofit blindly.
|
||||
|
||||
## Detailed references
|
||||
|
||||
- Read [references/package-boundaries.md](references/package-boundaries.md) for detailed package extraction, dependency graph, composition-root, and pbxproj wiring guidance.
|
||||
- Read [references/concurrency-carveouts.md](references/concurrency-carveouts.md) for detailed examples and review guidance around actors, locks, DispatchSource, sleep, `@unchecked Sendable`, and `nonisolated(unsafe)`.
|
||||
- Read [references/file-api-discipline.md](references/file-api-discipline.md) for one-type-per-file, DocC, public API, and design-smell details.
|
||||
- [references/package-boundaries.md](references/package-boundaries.md): extraction order, dependency graph, composition root, pbxproj wiring.
|
||||
- [references/concurrency-carveouts.md](references/concurrency-carveouts.md): carve-out examples and the reviewer reject-list.
|
||||
- [references/file-api-discipline.md](references/file-api-discipline.md): one-type-per-file, DocC, design smells.
|
||||
|
||||
@@ -1,93 +1,27 @@
|
||||
# Concurrency Carve-outs
|
||||
|
||||
This reference expands the Swift 6 concurrency rules.
|
||||
Examples and review guidance for the Swift 6 concurrency rules in [../SKILL.md](../SKILL.md).
|
||||
|
||||
## Default shape
|
||||
## Actor-owned responsibilities
|
||||
|
||||
Use modern Swift primitives:
|
||||
|
||||
- `actor` for mutable shared state
|
||||
- `async`/`await` for asynchronous APIs
|
||||
- `AsyncStream` or `AsyncSequence` for observation
|
||||
- `@Observable @MainActor` for SwiftUI-facing state
|
||||
- `@MainActor` instead of `DispatchQueue.main.async`
|
||||
|
||||
Do not add new `@Published`, `ObservableObject`, completion-handler APIs, KVO observer overrides, or queue-as-lock patterns in new package code or meaningful rewrites.
|
||||
|
||||
## Actor, not lock
|
||||
|
||||
Ongoing mutable shared state belongs in an actor. If the state has a lifecycle, multiple operations, or can be observed, an actor is almost always the right shape.
|
||||
|
||||
Example actor-owned responsibilities:
|
||||
|
||||
- process registry
|
||||
- file watcher state
|
||||
- socket session table
|
||||
- retry/idempotency state
|
||||
- provider lifecycle state
|
||||
State with a lifecycle, multiple operations, or observers belongs in an `actor`: process registry, file watcher state, socket session table, retry/idempotency state, provider lifecycle state.
|
||||
|
||||
## Single-method actor smell
|
||||
|
||||
Do not introduce an actor whose only job is to guard a boolean flag:
|
||||
|
||||
```swift
|
||||
actor ResumeGuard {
|
||||
func claim() -> Bool { ... }
|
||||
}
|
||||
```
|
||||
|
||||
That pattern is usually a lock with extra suspension and reentrancy surface. In synchronous callback races, the callback often needs an immediate compare-and-set, not a `Task { await ... }` hop.
|
||||
A synchronous callback needs an immediate compare-and-set, not a `Task { await ... }` hop. Use the lock carve-out.
|
||||
|
||||
## Lock carve-out
|
||||
|
||||
A private lock is acceptable for a short, synchronous compare-and-set called from non-async callbacks where an actor would worsen ordering and reentrancy.
|
||||
|
||||
Canonical case:
|
||||
|
||||
- process termination handler
|
||||
- timeout callback
|
||||
- spawn failure callback
|
||||
- all race to resume exactly one `withCheckedContinuation`
|
||||
|
||||
Use a tiny private guard, document the reason on the declaration, and keep the critical section non-blocking.
|
||||
|
||||
This carve-out does not allow locking ongoing domain state.
|
||||
|
||||
## DispatchSource carve-outs
|
||||
|
||||
These low-level primitives have no async-native replacement and are acceptable behind an async or actor surface:
|
||||
|
||||
- `DispatchSource.makeFileSystemObjectSource`
|
||||
- `DispatchSource.makeReadSource`
|
||||
- `DispatchSource.makeWriteSource`
|
||||
|
||||
Hide the source behind the type. Callers should see an `AsyncStream`, `AsyncSequence`, or actor API, not raw DispatchSource lifecycle.
|
||||
|
||||
## Sleep carve-out
|
||||
|
||||
`Clock.sleep` or `Task.sleep` is acceptable only for a genuine bounded delay or deadline that is the intended behavior:
|
||||
|
||||
- minimum display duration
|
||||
- auto-dismiss
|
||||
- check timeout
|
||||
- deadline for a provider operation
|
||||
|
||||
It is not acceptable for polling, settling UI state, or racing an animation/callback.
|
||||
|
||||
Prefer an injected `Clock` or duration so tests can advance virtual time. Store and cancel sleeping tasks on lifecycle transitions.
|
||||
|
||||
## Timer source carve-out
|
||||
|
||||
Use `DispatchSource.makeTimerSource` only when a genuine deadline must fire outside any async context and there is no task to host `Clock.sleep`.
|
||||
|
||||
Prefer `Clock.sleep` whenever the code is already async or actor-isolated.
|
||||
Canonical case: a process termination handler, a timeout callback, and a spawn failure callback all race to resume exactly one `withCheckedContinuation`. Keep the guard private to the type, non-blocking, and documented on the declaration. Never for ongoing domain state.
|
||||
|
||||
## Sendability escape hatches
|
||||
|
||||
`@unchecked Sendable` and `nonisolated(unsafe)` require comments on the declaration explaining why the usage is sound.
|
||||
|
||||
Good examples:
|
||||
|
||||
```swift
|
||||
// Wraps DispatchSourceFileSystemObject; every mutation happens on `queue`.
|
||||
private final class WatcherAttachment: @unchecked Sendable { ... }
|
||||
@@ -96,19 +30,17 @@ private final class WatcherAttachment: @unchecked Sendable { ... }
|
||||
private nonisolated(unsafe) let defaults: UserDefaults
|
||||
```
|
||||
|
||||
Prefer narrowing the escape hatch to one property rather than marking an entire actor or value type unchecked.
|
||||
Narrow the escape hatch to one property rather than marking an entire actor or value type unchecked.
|
||||
|
||||
## Review checklist
|
||||
## Review reject-list
|
||||
|
||||
Reject diffs that introduce any of these in new code without a documented carve-out:
|
||||
Reject diffs introducing any of these in new code without a documented carve-out:
|
||||
|
||||
- `@Published`
|
||||
- `ObservableObject`
|
||||
- `DispatchQueue.main.async`
|
||||
- `DispatchQueue.asyncAfter`
|
||||
- `@Published`, `ObservableObject`
|
||||
- `DispatchQueue.main.async`, `DispatchQueue.asyncAfter`
|
||||
- `addObserver(_:forKeyPath:...)`
|
||||
- queue-as-lock synchronization
|
||||
- lock for ongoing mutable state
|
||||
- `Task.sleep` or `Clock.sleep` used to poll/settle/race
|
||||
- `@unchecked Sendable` without a safety comment
|
||||
- `nonisolated(unsafe)` without a safety comment
|
||||
- a lock guarding ongoing mutable state
|
||||
- an `actor` whose only job is guarding a flag
|
||||
- `Task.sleep` / `Clock.sleep` used to poll, settle, or race
|
||||
- `@unchecked Sendable` or `nonisolated(unsafe)` without a safety comment
|
||||
|
||||
@@ -1,86 +1,36 @@
|
||||
# File and API Discipline
|
||||
|
||||
This reference expands file organization, documentation, and design-smell rules.
|
||||
Expands the file organization, DocC, and design-smell rules in [../SKILL.md](../SKILL.md).
|
||||
|
||||
## One major type per file
|
||||
## What gets its own file
|
||||
|
||||
Each meaningful `struct`, `class`, `enum`, `actor`, or `protocol` lives in its own file named after the type.
|
||||
Public API types, internal types with meaningful bodies, private nested types that grew past a tiny helper, type-erased wrappers, and conformance extensions for externally owned types.
|
||||
|
||||
This applies to:
|
||||
A helper can stay with its parent while it is private and trivial (a nested enum for local branching, a one-line private extension). Move it the moment it has lifecycle, state, a protocol conformance, or enough logic to test independently.
|
||||
|
||||
- public API types
|
||||
- internal types with meaningful bodies
|
||||
- private nested types that have grown beyond a tiny helper
|
||||
- type-erased wrappers
|
||||
- conformance extensions for externally owned types
|
||||
|
||||
File count is cheap. Not knowing where a type lives is expensive.
|
||||
|
||||
## Allowed small helpers
|
||||
|
||||
Small, closely-bound helpers can stay with the parent type when they are private and trivial:
|
||||
|
||||
- a tiny nested enum used only for local branching
|
||||
- a one-line private extension
|
||||
- a local helper that does not have independent behavior
|
||||
|
||||
Move the helper once it has meaningful lifecycle, state, protocol conformance, or enough logic to test independently.
|
||||
|
||||
## Extension files
|
||||
|
||||
Conformance-adding extensions for a type defined elsewhere go in files such as:
|
||||
|
||||
- `TypeName+Conformance.swift`
|
||||
- `TypeName+Feature.swift`
|
||||
|
||||
Do not hide important conformances inside unrelated feature files.
|
||||
|
||||
## DocC for public package APIs
|
||||
|
||||
Every public symbol in new Swift packages under `Packages/` needs a `///` DocC comment at the time it is written.
|
||||
|
||||
Document:
|
||||
|
||||
- what a type represents
|
||||
- when to use it
|
||||
- enum case meaning
|
||||
- property invariants
|
||||
- init parameters and defaults
|
||||
- method parameters, returns, and throws
|
||||
- generic constraints
|
||||
|
||||
Use double-backtick symbol references for symbols:
|
||||
## DocC quick examples
|
||||
|
||||
```swift
|
||||
/// Stores a typed ``CmuxSetting`` value.
|
||||
```
|
||||
|
||||
Use plain backticks for non-symbol code:
|
||||
|
||||
```swift
|
||||
/// Reads from `UserDefaults.standard` only when injected by the caller.
|
||||
```
|
||||
|
||||
Double backticks reference symbols; plain backticks are non-symbol code.
|
||||
|
||||
## Design smells
|
||||
|
||||
Avoid runtime state singletons:
|
||||
|
||||
- `static let shared`
|
||||
- `static let standard`
|
||||
- `static let default`
|
||||
|
||||
Static declarations are fine for identifiers, schema entries, and enum cases. Runtime behavior should be constructed at app startup and injected.
|
||||
|
||||
Avoid namespace enums:
|
||||
|
||||
```swift
|
||||
// Fake namespace: no instances, no DI, no test seam.
|
||||
enum Foo {
|
||||
static func bar() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
If behavior may need configuration or a test seam, use a value type or service. If it is a pure local helper, keep it private near its caller.
|
||||
Runtime-state singletons (`static let shared` / `standard` / `default`) are constructed at app startup and injected instead. `static let` stays legal for identifiers, schema entries, and enum cases.
|
||||
|
||||
Avoid parallel hand-maintained registries. If a list mirrors declared items, derive it via reflection or a macro where practical.
|
||||
A `guard` plus `assertionFailure` plus a fallback usually means the type model is too weak. Encode the invariant in the type system.
|
||||
|
||||
Prefer compile-time invariants to runtime traps. A `guard` plus `assertionFailure` plus fallback often means the type model is too weak.
|
||||
A hand-maintained list that mirrors declared items drifts silently. Derive it via reflection or a macro where practical.
|
||||
|
||||
@@ -1,87 +1,34 @@
|
||||
# Package Boundaries
|
||||
|
||||
This reference expands cmux package extraction and refactor architecture rules.
|
||||
Expands the package extraction and layering rules in [../SKILL.md](../SKILL.md).
|
||||
|
||||
## Why package boundaries exist
|
||||
## Domain names vs slice names
|
||||
|
||||
A package boundary should exist because more than one consumer needs the domain, because a build/test seam is useful, or because the package isolates a cohesive external capability. It should not exist just to make a file list look smaller.
|
||||
Good (a domain): `CmuxSettings`, `CmuxSettingsUI`, `CmuxAppearance`, `CmuxWorkspaces`, `CmuxBrowser`, `CmuxControlSocket`.
|
||||
|
||||
Good package names describe a domain:
|
||||
|
||||
- `CmuxSettings`
|
||||
- `CmuxSettingsUI`
|
||||
- `CmuxAppearance`
|
||||
- `CmuxWorkspace`
|
||||
- `CmuxBrowser`
|
||||
- `CmuxSocketControl`
|
||||
|
||||
Weak package names describe a slice:
|
||||
|
||||
- `CmuxAppearanceMath`
|
||||
- `CmuxWorkspaceModel`
|
||||
- `CmuxFooFormatting`
|
||||
- `CmuxFooLogic`
|
||||
- `CmuxFooState`
|
||||
|
||||
Slices force callers to depend on several sibling packages any time they touch the real domain.
|
||||
|
||||
## Dependency graph
|
||||
|
||||
Packages form a strict downward-only DAG:
|
||||
|
||||
1. Core: pure `Sendable` values, IDs, DTOs, errors, and protocol seams. No AppKit, SwiftUI, or I/O.
|
||||
2. Services/infrastructure: actors implementing core protocols against external systems.
|
||||
3. Domain/state: `@MainActor @Observable` models and Coordinators.
|
||||
4. UI: SwiftUI/AppKit views that depend on domain packages and Core, not services directly.
|
||||
5. Executable: `cmuxApp` and `AppDelegate` as the composition root.
|
||||
|
||||
If two domains need a shared type, lift the type to a lower package or define a protocol seam. Do not make sibling packages reach sideways.
|
||||
Weak (a slice): `CmuxAppearanceMath`, `CmuxWorkspaceModel`, `CmuxFooFormatting`, `CmuxFooLogic`, `CmuxFooState`. Slices force callers to depend on several sibling packages any time they touch the real domain.
|
||||
|
||||
## Extract leaf-first
|
||||
|
||||
When uncertain, extract the package that has no internal dependencies first. This keeps the migration incremental and avoids needing several downstream packages to exist before one package can compile.
|
||||
|
||||
Leaf-first extraction also makes review easier:
|
||||
|
||||
- fewer dependency edges
|
||||
- fewer project-file entries
|
||||
- simpler tests
|
||||
- clearer rollback path
|
||||
Extracting the package with no internal dependencies first keeps the migration incremental: fewer dependency edges, fewer project-file entries, simpler tests, a clearer rollback path. It also avoids needing several downstream packages to exist before one package compiles.
|
||||
|
||||
## Composition root
|
||||
|
||||
The executable app target is the single composition root. Concrete services and repositories are named there and injected into coordinators/models.
|
||||
|
||||
Do not introduce:
|
||||
|
||||
- global containers
|
||||
- runtime state singletons
|
||||
- `static let shared`
|
||||
- service lookups from package internals
|
||||
|
||||
SwiftUI `Environment` may carry already-constructed observable models down a view tree. It should not become the source of truth for service wiring.
|
||||
The executable app target names concrete services and repositories and injects them. No global containers, no runtime-state singletons, no `static let shared`, no service lookups from package internals. SwiftUI `Environment` carries already-constructed observable models down a view tree, never service wiring.
|
||||
|
||||
## Executable target boundary
|
||||
|
||||
`@main` `cmuxApp` and `AppDelegate` stay in the executable target. Do not move extensions of executable-owned types down into lower packages. A lower package cannot extend a higher-owned type without creating the wrong dependency direction.
|
||||
|
||||
Instead:
|
||||
|
||||
1. Extract behavior into a Coordinator, Service, or Repository in the appropriate package.
|
||||
2. Inject it into the god object or app composition root.
|
||||
3. Reduce the original extension to a one-line forward if it must remain.
|
||||
A lower package cannot extend a higher-owned type without inverting the dependency direction, so `AppDelegate+*` / `cmuxApp+*` extensions do not move down. Extract the behavior into a Coordinator/Service/Repository, inject it into the god object or composition root, and reduce the original extension to a one-line forward.
|
||||
|
||||
## pbxproj wiring
|
||||
|
||||
`cmux.xcodeproj` lists package dependencies explicitly. Adding `Packages/CmuxFoo` means mirroring existing package entries:
|
||||
`cmux.xcodeproj` lists package dependencies explicitly. Adding `Packages/CmuxFoo` means mirroring an existing package's entries:
|
||||
|
||||
- one `XCLocalSwiftPackageReference`
|
||||
- one `XCLocalSwiftPackageReference` in the project's `packageReferences`
|
||||
- one `XCSwiftPackageProductDependency`
|
||||
- one `PBXBuildFile` linked in the Frameworks phase of every target that imports it
|
||||
|
||||
App-target packages link into both `cmux` and `cmux-unit`, so tests can import and inject them. A package linked by the app but not `cmux-unit` can make the app build pass while the test target fails.
|
||||
|
||||
After editing the project file, run:
|
||||
App-target packages link into **both** `cmux` and `cmux-unit` so tests can import and inject them. A package linked by the app but not `cmux-unit` compiles the app and fails the test target. Copy a recent leaf package for the exact shape, then run:
|
||||
|
||||
```bash
|
||||
scripts/normalize-pbxproj.py
|
||||
|
||||
@@ -5,31 +5,20 @@ description: "Backend TypeScript and Cloud VM development rules for cmux. Use wh
|
||||
|
||||
# cmux Backend
|
||||
|
||||
Use this skill for backend TypeScript, Cloud VM, provider, database, auth, rate-limit, retry, timeout, or telemetry work.
|
||||
|
||||
## Core rules
|
||||
|
||||
- Default backend TypeScript to Effect under `web/app/api/**`, `web/services/**`, and backend scripts that touch providers, databases, auth, rate limits, retries, timeouts, or telemetry.
|
||||
- Keep Next route handlers thin: parse the request, run one Effect program at the boundary, map typed errors to HTTP responses, and treat unexpected defects separately.
|
||||
- Use plain TypeScript only for trivial data shapes, constants, config files, frontend React code, or small glue where Effect would add ceremony without improving failure handling.
|
||||
- Cloud VM backend logic must stay in Vercel route handlers and Effect services backed by Postgres.
|
||||
- Do not reintroduce Rivet or a raw actor protocol for Cloud VM unless a later architecture doc explicitly changes the control plane.
|
||||
- Production and staging Cloud VM Postgres use the Vercel Marketplace AWS Aurora PostgreSQL OIDC/RDS IAM path.
|
||||
- Runtime env names are `CMUX_DB_DRIVER=aws-rds-iam`, `AWS_ROLE_ARN`, `AWS_REGION`, `PGHOST`, `PGPORT`, `PGUSER`, and `PGDATABASE`.
|
||||
- Run production/staging migrations with `bun db:migrate:aws-rds-iam`; never run Drizzle migrations from Vercel build or route startup.
|
||||
- Local development keeps using the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
|
||||
- Cloud VM create pricing gates should use Stack Auth team payment items when enabled.
|
||||
- Postgres remains the source of truth for VM lifecycle, active VM limits, idempotency, and usage events.
|
||||
- Default backend TypeScript to Effect under `web/app/api/**`, `web/services/**`, and backend scripts touching providers, databases, auth, rate limits, retries, timeouts, or telemetry.
|
||||
- Keep Next route handlers thin: parse the request, run one Effect program at the boundary, map typed errors to HTTP responses, treat unexpected defects separately.
|
||||
- Plain TypeScript is for trivial data shapes, constants, config files, frontend React, and small glue where Effect would add ceremony without improving failure handling.
|
||||
- Cloud VM backend logic stays in Vercel route handlers and Effect services backed by Postgres. Do not reintroduce Rivet or a raw actor protocol unless a later architecture doc explicitly changes the control plane.
|
||||
- Postgres is the source of truth for VM lifecycle, active VM limits, idempotency, and usage events.
|
||||
- Production and staging Cloud VM Postgres use the Vercel Marketplace AWS Aurora PostgreSQL OIDC/RDS IAM path, with runtime env `CMUX_DB_DRIVER=aws-rds-iam`, `AWS_ROLE_ARN`, `AWS_REGION`, `PGHOST`, `PGPORT`, `PGUSER`, `PGDATABASE`.
|
||||
- Run production/staging migrations with `bun db:migrate:aws-rds-iam`; never from Vercel build or route startup. Local dev keeps the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
|
||||
- Cloud VM create pricing gates use Stack Auth team payment items when enabled.
|
||||
|
||||
## Secrets
|
||||
|
||||
Cloud VM build, test, and local dev scripts use provider secrets from `~/.secrets/cmux.env`.
|
||||
|
||||
- `E2B_API_KEY`
|
||||
- `FREESTYLE_API_KEY`
|
||||
- R2 upload vars used by `web/scripts/build-cloud-vm-images.ts` when creating Freestyle snapshots
|
||||
|
||||
Load them with:
|
||||
Cloud VM build, test, and local dev scripts read provider secrets from `~/.secrets/cmux.env`: `E2B_API_KEY`, `FREESTYLE_API_KEY`, and the R2 upload vars `web/scripts/build-cloud-vm-images.ts` needs when creating Freestyle snapshots.
|
||||
|
||||
```bash
|
||||
set -a
|
||||
@@ -37,9 +26,9 @@ source ~/.secrets/cmux.env
|
||||
set +a
|
||||
```
|
||||
|
||||
`~/.secrets/cmuxterm-dev.env` is for local Stack/web env and does not contain the provider build keys. `bun dev` sources `~/.secrets/cmux.env` first when present, then `~/.secrets/cmuxterm-dev.env` so cmuxterm-specific Stack settings override broader cmux secrets. The web dev loader still accepts the legacy `~/.secret/cmuxterm.env` and `~/.secrets/cmuxterm.env` paths while machines migrate.
|
||||
`~/.secrets/cmuxterm-dev.env` holds local Stack/web env and not the provider build keys. `bun dev` sources `~/.secrets/cmux.env` first when present, then `~/.secrets/cmuxterm-dev.env`, so cmuxterm-specific Stack settings override broader cmux secrets. The web dev loader still accepts the legacy `~/.secret/cmuxterm.env` and `~/.secrets/cmuxterm.env` paths while machines migrate.
|
||||
|
||||
## Detailed references
|
||||
|
||||
- Read [references/effect-boundaries.md](references/effect-boundaries.md) when shaping route handlers, services, typed errors, retries, or dependency injection.
|
||||
- Read [references/cloud-vm-control-plane.md](references/cloud-vm-control-plane.md) when touching VM lifecycle, migrations, Postgres, provider idempotency, or pricing gates.
|
||||
- [references/effect-boundaries.md](references/effect-boundaries.md): route handlers, services, typed errors, retries, dependency injection.
|
||||
- [references/cloud-vm-control-plane.md](references/cloud-vm-control-plane.md): VM lifecycle, migrations, Postgres, provider idempotency, pricing gates.
|
||||
|
||||
@@ -1,60 +1,23 @@
|
||||
# Cloud VM Control Plane
|
||||
|
||||
This reference expands the Cloud VM rules for lifecycle, persistence, migrations, and provider coordination.
|
||||
Expands the Cloud VM rules in [../SKILL.md](../SKILL.md).
|
||||
|
||||
## Source of truth
|
||||
|
||||
Postgres is the source of truth for:
|
||||
Postgres owns VM lifecycle state, active VM limits, idempotency records, usage events, provider identifiers, and team/account ownership. Provider state is observed and reconciled, not treated as canonical. When provider state and database state disagree, make the reconciliation explicit in code.
|
||||
|
||||
- VM lifecycle state
|
||||
- active VM limits
|
||||
- idempotency records
|
||||
- usage events
|
||||
- provider identifiers
|
||||
- team/account ownership
|
||||
|
||||
Provider state is observed and reconciled, not treated as the canonical application state. If provider state and database state disagree, write code that makes the reconciliation explicit.
|
||||
|
||||
## Vercel and Effect boundary
|
||||
|
||||
Cloud VM backend logic lives in Vercel route handlers and Effect services. Route handlers should not become a raw actor protocol or long-running in-memory control plane. The durable state belongs in Postgres, and request-time workflows should be idempotent.
|
||||
|
||||
Do not reintroduce Rivet or a raw actor protocol unless a later architecture document explicitly changes this control plane.
|
||||
Cloud VM backend logic lives in Vercel route handlers and Effect services. Request-time workflows must be idempotent; durable state belongs in Postgres. Do not reintroduce Rivet or a raw actor protocol unless a later architecture document explicitly changes this control plane.
|
||||
|
||||
## Migrations
|
||||
|
||||
Production and staging migrations use:
|
||||
|
||||
```bash
|
||||
bun db:migrate:aws-rds-iam
|
||||
```
|
||||
|
||||
Never run Drizzle migrations from Vercel build or route startup. Build/startup migrations make deploy behavior non-deterministic and couple app availability to schema mutation.
|
||||
|
||||
Local development keeps using the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
|
||||
Production and staging: `bun db:migrate:aws-rds-iam`. Never run Drizzle migrations from Vercel build or route startup; that makes deploy behavior non-deterministic and couples app availability to schema mutation. Local development keeps the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
|
||||
|
||||
## AWS RDS IAM runtime
|
||||
|
||||
Production and staging Cloud VM Postgres should use the Vercel Marketplace AWS Aurora PostgreSQL OIDC/RDS IAM path with these runtime env names:
|
||||
|
||||
- `CMUX_DB_DRIVER=aws-rds-iam`
|
||||
- `AWS_ROLE_ARN`
|
||||
- `AWS_REGION`
|
||||
- `PGHOST`
|
||||
- `PGPORT`
|
||||
- `PGUSER`
|
||||
- `PGDATABASE`
|
||||
|
||||
Avoid inventing parallel env names for the same settings. Every new name creates another migration and deploy surface.
|
||||
Production and staging use the Vercel Marketplace AWS Aurora PostgreSQL OIDC/RDS IAM path with `CMUX_DB_DRIVER=aws-rds-iam`, `AWS_ROLE_ARN`, `AWS_REGION`, `PGHOST`, `PGPORT`, `PGUSER`, `PGDATABASE`. Do not invent parallel env names for the same settings; each new name is another migration and deploy surface.
|
||||
|
||||
## Pricing and active limits
|
||||
|
||||
Cloud VM create pricing gates should use Stack Auth team payment items when enabled. Active limits and usage events should be persisted, not inferred from transient process memory.
|
||||
Create pricing gates use Stack Auth team payment items when enabled. Active limits and usage events are persisted, not inferred from process memory.
|
||||
|
||||
When changing create/start flows, verify:
|
||||
|
||||
- idempotency prevents duplicate provider creates
|
||||
- team ownership is checked before provider allocation
|
||||
- active VM limits are enforced before expensive provider work
|
||||
- usage events are written exactly once for the lifecycle moment they represent
|
||||
- failed provider calls leave a recoverable database state
|
||||
When changing create/start flows, verify that idempotency prevents duplicate provider creates, team ownership is checked before provider allocation, active VM limits are enforced before expensive provider work, usage events are written exactly once per lifecycle moment, and a failed provider call leaves a recoverable database state.
|
||||
|
||||
@@ -1,72 +1,27 @@
|
||||
# Effect Boundaries
|
||||
|
||||
This reference expands the backend TypeScript rules for route handlers, services, and scripts.
|
||||
Expands the backend TypeScript rules in [../SKILL.md](../SKILL.md) for route handlers, services, and scripts.
|
||||
|
||||
## Route handler shape
|
||||
|
||||
Route handlers should be shallow adapters. They should parse request input, construct or select the required Effect program, run it once at the boundary, and translate domain errors to HTTP responses. Keep workflow sequencing, retries, provider calls, and database updates outside the handler body.
|
||||
A handler is a shallow adapter: parse request input, select the Effect program, run it once at the boundary, translate domain errors to HTTP. Workflow sequencing, retries, provider calls, and database updates live outside the handler body. A handler that interleaves parsing, database writes, provider calls, and response construction makes retries and idempotency impossible to audit.
|
||||
|
||||
A good handler answers these questions quickly:
|
||||
|
||||
- What input does the route accept?
|
||||
- Which Effect program performs the workflow?
|
||||
- Which typed errors map to expected HTTP statuses?
|
||||
- Which failures are unexpected defects?
|
||||
|
||||
Avoid route handlers that interleave parsing, database writes, provider calls, and response construction. That shape makes retries and idempotency hard to audit.
|
||||
Reading one should immediately answer: what input the route accepts, which Effect program performs the workflow, which typed errors map to expected statuses, and which failures are unexpected defects.
|
||||
|
||||
## Service shape
|
||||
|
||||
Use Effect services when a workflow crosses an external boundary or has meaningful failure semantics:
|
||||
Use an Effect service when a workflow crosses an external boundary or has meaningful failure semantics: provider APIs, database reads/writes, auth and team lookup, payment or quota checks, retry and timeout policy, telemetry and usage recording, idempotency claims.
|
||||
|
||||
- provider APIs
|
||||
- database reads or writes
|
||||
- auth and team lookup
|
||||
- payment or quota checks
|
||||
- retries and timeout policy
|
||||
- telemetry and usage recording
|
||||
- idempotency claims
|
||||
|
||||
Model expected failures as typed domain errors. Prefer names that describe the business failure, not the transport layer. For example, `VmLimitExceeded`, `ProviderCapacityUnavailable`, or `IdempotencyConflict` is more useful to callers than a raw `FetchError`.
|
||||
Model expected failures as typed domain errors named for the business failure. `VmLimitExceeded`, `ProviderCapacityUnavailable`, and `IdempotencyConflict` tell a caller more than a raw `FetchError`.
|
||||
|
||||
## Dependency shape
|
||||
|
||||
Make service dependencies explicit. Do not hide important runtime dependencies behind globals when an Effect service can receive them as layer requirements.
|
||||
|
||||
Good dependencies are concrete capabilities:
|
||||
|
||||
- database client
|
||||
- provider client
|
||||
- auth/team service
|
||||
- clock or timeout policy
|
||||
- telemetry sink
|
||||
- idempotency repository
|
||||
|
||||
Bad dependencies are broad ambient containers or untyped option bags that force every workflow to rediscover what it actually needs.
|
||||
Service dependencies are concrete capabilities declared as layer requirements: database client, provider client, auth/team service, clock or timeout policy, telemetry sink, idempotency repository. Not globals, broad ambient containers, or untyped option bags.
|
||||
|
||||
## Plain TypeScript carve-out
|
||||
|
||||
Plain TypeScript is fine for data-only code:
|
||||
|
||||
- constants
|
||||
- schema declarations
|
||||
- config objects
|
||||
- frontend components
|
||||
- pure formatting helpers
|
||||
- tiny route glue with no external effects
|
||||
|
||||
The point is not to use Effect everywhere. The point is to use it where explicit failure, dependency, retry, and cancellation semantics reduce real ambiguity.
|
||||
Constants, schema declarations, config objects, frontend components, pure formatting helpers, and tiny route glue with no external effects stay plain TypeScript. Effect earns its place where explicit failure, dependency, retry, and cancellation semantics reduce real ambiguity.
|
||||
|
||||
## Error mapping
|
||||
|
||||
Expected domain errors should become clear HTTP responses. Unexpected defects should not be disguised as expected user errors.
|
||||
|
||||
When adding a new route, check that:
|
||||
|
||||
- invalid input maps to 400 or the existing validation status
|
||||
- auth and entitlement failures map to the existing auth/payment statuses
|
||||
- active-limit or quota failures are explicit
|
||||
- provider unavailability is distinguishable from a defect
|
||||
- idempotency conflicts return a deterministic response
|
||||
|
||||
If a caller needs to retry, the response should make that practical.
|
||||
When adding a route, check that invalid input maps to 400 or the existing validation status, auth and entitlement failures map to the existing auth/payment statuses, active-limit and quota failures are explicit, provider unavailability is distinguishable from a defect, and idempotency conflicts return a deterministic response. Never disguise an unexpected defect as an expected user error.
|
||||
|
||||
@@ -5,55 +5,42 @@ description: "Stripe checkout, pricing, subscription, Pro plan, webhook, and ent
|
||||
|
||||
# cmux Billing
|
||||
|
||||
Use this skill before changing billing, pricing, Stripe, Pro entitlement, checkout, webhook, or subscription code.
|
||||
Read before changing billing, pricing, Stripe, Pro entitlement, checkout, webhook, or subscription code.
|
||||
|
||||
## Architecture Map
|
||||
## Architecture map
|
||||
|
||||
- `/api/billing/checkout` creates Stripe Checkout Sessions for Pro when `STRIPE_SECRET_KEY` is set. It sets `client_reference_id` to the Stack user id, auto-creates an anonymous Stack user for signed-out buyers, and falls back to the legacy Stack purchase path when Stripe is unset or `plan=team`.
|
||||
- `/api/billing/checkout` creates Stripe Checkout Sessions for Pro when `STRIPE_SECRET_KEY` is set. It sets `client_reference_id` to the Stack user id, auto-creates an anonymous Stack user for signed-out buyers, and falls back to the legacy Stack purchase path when Stripe is unset or `plan=team`. The "already active" short-circuit lives here.
|
||||
- `/api/billing/portal` resolves the current Stack user, looks up their `stripe_customers` row, and creates a Stripe customer portal session returning to `/pricing`.
|
||||
- `/api/billing/subscription` cancels or resumes the current user's active Stripe Pro subscription, and `/dashboard/billing` renders localized in-dashboard plan state and self-serve billing actions.
|
||||
- `web/services/billing/purchase.ts` is the shared idempotent recorder used by both `/api/billing/complete` and `/api/stripe/webhook`. It attaches email to the purchaser, records `billing_email_claims` on conflict, and never cross-grants based on an unverified email.
|
||||
- `cmuxPlan` in Stack `clientReadOnlyMetadata` is the only entitlement VM code reads. `cmuxVmPlan` manual override wins.
|
||||
- `resolveProPlanStatus` ORs legacy Stack products with active `stripe_subscriptions` DB rows.
|
||||
- `/api/billing/subscription` cancels or resumes the active Stripe Pro subscription; `/dashboard/billing` renders localized in-dashboard plan state and self-serve actions.
|
||||
- `web/services/billing/purchase.ts` is the shared idempotent recorder used by `/api/billing/complete` and `/api/stripe/webhook`. It attaches email to the purchaser, records `billing_email_claims` on conflict, and never cross-grants based on an unverified email.
|
||||
- `cmuxPlan` in Stack `clientReadOnlyMetadata` is the only entitlement VM code reads; a `cmuxVmPlan` manual override wins. `resolveProPlanStatus` ORs legacy Stack products with active `stripe_subscriptions` rows.
|
||||
- `/api/stripe/webhook` is signature-verified, insert-first idempotent through `stripe_webhook_events`, safe for foreign events in the shared Stripe account, and gates cmux handling on `metadata.app === "cmux"`. Return 2xx only after durable writes; return 500 to make Stripe retry.
|
||||
|
||||
## Dev Workflow
|
||||
## Dev workflow
|
||||
|
||||
- Use `web/scripts/stripe/dev-stack.sh`.
|
||||
- The tagged app bakes `CMUX_PORT` into `Info.plist`; run the dev server on the tag's printed port. Do not hardcode a port.
|
||||
- Per-branch Docker Postgres ports can collide with other agents' containers. Use `--db-port` and never stop containers you did not create.
|
||||
- `/app-pricing` requires `cmux_app=1`.
|
||||
- `cmux_scheme` threads the native deeplink return scheme. `cmux-dev-*` schemes are honored only for localhost requests.
|
||||
- The tagged app bakes `CMUX_PORT` into `Info.plist`; run the dev server on the tag's printed port, never a hardcoded one.
|
||||
- Per-branch Docker Postgres ports collide with other agents' containers. Use `--db-port` and never stop containers you did not create.
|
||||
- `/app-pricing` requires `cmux_app=1`. `cmux_scheme` threads the native deeplink return scheme; `cmux-dev-*` schemes are honored only for localhost requests.
|
||||
- Repeat dogfood: use a private window for a fresh anonymous buyer, and `web/scripts/stripe/dev-reset.sh <email>` to un-Pro a signed-in dev account before retesting checkout.
|
||||
|
||||
## Repeat Dogfood
|
||||
## Test-mode resources
|
||||
|
||||
- Use a private window for a fresh anonymous buyer.
|
||||
- Use `web/scripts/stripe/dev-reset.sh <email>` to un-Pro a signed-in dev account before retesting checkout.
|
||||
- The "already active" short-circuit lives in `/api/billing/checkout`.
|
||||
Product `prod_UpIQRE6cj0nFjs`. Lookup keys `cmux-pro-monthly` ($30/mo) and `cmux-pro-yearly` ($240/yr). Staging webhook endpoint `we_1Tq1SZGhInAdn3JbWJReKNEN` forwards to `cmux-staging.vercel.app`; its secrets are already in the `cmux-staging` Vercel project.
|
||||
|
||||
## Test-Mode Resources
|
||||
## Feature flags
|
||||
|
||||
- Product: `prod_UpIQRE6cj0nFjs`.
|
||||
- Lookup keys: `cmux-pro-monthly` ($30/mo) and `cmux-pro-yearly` ($240/yr).
|
||||
- Staging webhook endpoint: `we_1Tq1SZGhInAdn3JbWJReKNEN` forwarding to `cmux-staging.vercel.app`; secrets are already in the `cmux-staging` Vercel project.
|
||||
`pro-upgrade-ui-enabled-release` (PostHog id `741838`) gates all Pro UI and stays OFF in release until launch; DEBUG builds default it on. Public Pro and Team pricing CTAs always route through `/api/billing/checkout`, never the download confirmation page. `cmux __internal_flags`, once merged, inspects and overrides flags locally.
|
||||
|
||||
## Feature Flags
|
||||
## Prod runbook
|
||||
|
||||
- `pro-upgrade-ui-enabled-release` (PostHog id `741838`) gates all Pro UI and stays OFF in release until launch. DEBUG builds default the UI on.
|
||||
- Public Pro and Team pricing CTAs always route through `/api/billing/checkout`; do not fall back to the download confirmation page.
|
||||
- `cmux __internal_flags`, once merged, inspects and overrides flags locally.
|
||||
Run `web/scripts/stripe/provision-live.sh` with an operator key, add the two Vercel envs, deploy, validate live with a 100-percent-off promotion code purchase, then cancel.
|
||||
|
||||
## Prod Runbook
|
||||
DB migrations: `bun run cloud-vm:preflight`, `bun run cloud-vm:migrate -- staging`, staging deploy, then `bun run cloud-vm:migrate -- production`. Never run migrations from builds. See the Cloud VM ops flow.
|
||||
|
||||
- Run `web/scripts/stripe/provision-live.sh` with an operator key, add the two Vercel envs, deploy, validate live with a 100 percent-off promotion code purchase, then cancel.
|
||||
- DB migrations go through `bun run cloud-vm:preflight`, `bun run cloud-vm:migrate -- staging`, staging deploy, then `bun run cloud-vm:migrate -- production`. See the Cloud VM ops flow. Never run migrations from builds.
|
||||
## Gotchas
|
||||
|
||||
## Testing Gotchas
|
||||
|
||||
- `bun mock.module` is process-global, so every module mock must carry every real export other suite files import. Missing exports may surface only in CI's test order as `Export named X not found`.
|
||||
- `bun mock.module` is process-global, so every module mock must carry every real export other suite files import. A missing export can surface only in CI's test order as `Export named X not found`.
|
||||
- Tests must not depend on `DATABASE_URL` being set.
|
||||
- drizzle-1.0-beta wraps pg errors in `DrizzleQueryError`; read `error.cause` for the pg `code` and `constraint`.
|
||||
|
||||
## Route Placement Gotcha
|
||||
|
||||
Pages outside `app/[locale]` need a `proxy.ts` bypass, like `/app-pricing` and `/billing`, or `next-intl` rewrites them into the locale tree and they 404 through missing root layout tags. Those subtrees also need their own layout with `html` and `body`.
|
||||
- Pages outside `app/[locale]` need a `proxy.ts` bypass (like `/app-pricing` and `/billing`), or `next-intl` rewrites them into the locale tree and they 404 through missing root layout tags. Those subtrees also need their own layout with `html` and `body`.
|
||||
|
||||
+36
-106
@@ -5,21 +5,12 @@ description: End-user browser automation with cmux. Use when you need to open si
|
||||
|
||||
# Browser Automation with cmux
|
||||
|
||||
Use this skill for browser tasks inside cmux webviews.
|
||||
## Core workflow
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. Open or target a browser surface.
|
||||
2. Verify navigation with `get url` before waiting or snapshotting.
|
||||
3. Snapshot (`--interactive`) to get fresh element refs.
|
||||
4. Act with refs (`click`, `fill`, `type`, `select`, `press`).
|
||||
5. Wait for state changes.
|
||||
6. Re-snapshot after DOM/navigation changes.
|
||||
Open or target a browser surface, verify navigation with `get url`, snapshot for fresh element refs, act on refs, wait for the state change, re-snapshot.
|
||||
|
||||
```bash
|
||||
cmux --json browser open https://example.com
|
||||
# use returned surface ref, for example: surface:7
|
||||
|
||||
cmux --json browser open https://example.com # returns a surface ref, e.g. surface:7
|
||||
cmux browser surface:7 get url
|
||||
cmux browser surface:7 wait --load-state complete --timeout-ms 15000
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
@@ -28,24 +19,20 @@ cmux --json browser surface:7 click e2 --snapshot-after
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
```
|
||||
|
||||
## Surface Targeting
|
||||
If `get url` is empty or `about:blank`, navigate first instead of waiting on load state. Re-snapshot after navigation, modal open/close, or major DOM changes; refs go stale.
|
||||
|
||||
## Surface targeting
|
||||
|
||||
`browser open` targets the workspace of the terminal running the command (`CMUX_WORKSPACE_ID`), even when another workspace is focused. Override with `--workspace` / `--window`:
|
||||
|
||||
```bash
|
||||
# identify current context
|
||||
cmux identify --json
|
||||
|
||||
# open routed to a specific topology target
|
||||
cmux browser open https://example.com --workspace workspace:2 --window window:1 --json
|
||||
```
|
||||
|
||||
Notes:
|
||||
- CLI output defaults to short refs (`surface:N`, `pane:N`, `workspace:N`, `window:N`).
|
||||
- UUIDs are still accepted on input; only request UUID output when needed (`--id-format uuids|both`).
|
||||
- Keep using one `surface:N` per task unless you intentionally switch.
|
||||
Output defaults to short refs (`surface:N`, `pane:N`, `workspace:N`, `window:N`); UUIDs are accepted on input, and `--id-format uuids|both` requests them on output. Keep one `surface:N` per task.
|
||||
|
||||
## Wait Support
|
||||
|
||||
cmux supports wait patterns similar to agent-browser:
|
||||
## Wait
|
||||
|
||||
```bash
|
||||
cmux browser <surface> wait --selector "#ready" --timeout-ms 10000
|
||||
@@ -55,93 +42,19 @@ cmux browser <surface> wait --load-state complete --timeout-ms 15000
|
||||
cmux browser <surface> wait --function "document.readyState === 'complete'" --timeout-ms 10000
|
||||
```
|
||||
|
||||
## Common Flows
|
||||
## Viewport sizing (WKWebView)
|
||||
|
||||
### Form Submit
|
||||
`cmux browser <surface> viewport <width> <height>` sets an exact logical viewport from 1 to 4096 CSS pixels. The page is aspect-fitted inside its existing pane, so pane layout and focus stay unchanged, and screenshots use the requested logical dimensions. `viewport reset` returns to native pane sizing.
|
||||
|
||||
```bash
|
||||
cmux --json browser open https://example.com/signup
|
||||
cmux browser surface:7 get url
|
||||
cmux browser surface:7 wait --load-state complete --timeout-ms 15000
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
cmux browser surface:7 fill e1 "Jane Doe"
|
||||
cmux browser surface:7 fill e2 "[email protected]"
|
||||
cmux --json browser surface:7 click e3 --snapshot-after
|
||||
cmux browser surface:7 wait --url-contains "/welcome" --timeout-ms 15000
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
```
|
||||
|
||||
### Clear an Input
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 fill e11 "" --snapshot-after --json
|
||||
cmux browser surface:7 get value e11 --json
|
||||
```
|
||||
|
||||
### Stable Agent Loop (Recommended)
|
||||
|
||||
```bash
|
||||
# navigate -> verify -> wait -> snapshot -> action -> snapshot
|
||||
cmux browser surface:7 get url
|
||||
cmux browser surface:7 wait --load-state complete --timeout-ms 15000
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
cmux --json browser surface:7 click e5 --snapshot-after
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
```
|
||||
|
||||
If `get url` is empty or `about:blank`, navigate first instead of waiting on load state.
|
||||
|
||||
## Deep-Dive References
|
||||
|
||||
| Reference | When to Use |
|
||||
|-----------|-------------|
|
||||
| [references/commands.md](references/commands.md) | Full browser command mapping and quick syntax |
|
||||
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle and stale-ref troubleshooting |
|
||||
| [references/authentication.md](references/authentication.md) | Login/OAuth/2FA patterns and state save/load |
|
||||
| [references/authentication.md#saving-authentication-state](references/authentication.md#saving-authentication-state) | Save authenticated state right after login |
|
||||
| [references/session-management.md](references/session-management.md) | Multi-surface isolation and state persistence patterns |
|
||||
| [references/video-recording.md](references/video-recording.md) | Current recording status and practical alternatives |
|
||||
| [references/proxy-support.md](references/proxy-support.md) | Proxy behavior in WKWebView and workarounds |
|
||||
|
||||
## Ready-to-Use Templates
|
||||
|
||||
| Template | Description |
|
||||
|----------|-------------|
|
||||
| [templates/form-automation.sh](templates/form-automation.sh) | Snapshot/ref form fill loop |
|
||||
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, save/load state |
|
||||
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Navigate + capture snapshots/screenshots |
|
||||
|
||||
## Viewport Sizing (WKWebView)
|
||||
|
||||
Use `cmux browser <surface> viewport <width> <height>` to set an exact logical
|
||||
viewport from 1...4096 CSS pixels. The page is aspect-fitted inside its existing
|
||||
pane, so pane layout and focus stay unchanged; screenshots use the requested
|
||||
logical dimensions. Run `cmux browser <surface> viewport reset` to follow native
|
||||
pane sizing again. Close or detach the browser inspector first because its
|
||||
inspector-managed split layout cannot be combined with viewport emulation.
|
||||
Large viewport and page-zoom combinations are bounded; the viewport command
|
||||
returns structured `maximum_page_zoom` details without changing the current
|
||||
viewport when the combination exceeds the WKWebView render limits.
|
||||
Opening or redocking an attached browser inspector resets emulation to native
|
||||
sizing because WebKit owns the attached split geometry.
|
||||
Close or detach the browser inspector first: its inspector-managed split layout cannot be combined with viewport emulation, and opening or redocking an attached inspector resets emulation to native sizing. Large viewport and page-zoom combinations are bounded; the command returns structured `maximum_page_zoom` details and leaves the viewport unchanged when the combination exceeds WKWebView render limits.
|
||||
|
||||
## Limits (WKWebView)
|
||||
|
||||
These commands currently return `not_supported` because they rely on Chrome/CDP-only APIs not exposed by WKWebView:
|
||||
- offline emulation
|
||||
- trace/screencast recording
|
||||
- network route interception/mocking
|
||||
- low-level raw input injection
|
||||
Offline emulation, trace/screencast recording, network route interception/mocking, and low-level raw input injection return `not_supported`; they depend on Chrome/CDP-only APIs. Use `click`, `fill`, `press`, `scroll`, `wait`, `snapshot` instead.
|
||||
|
||||
Use supported high-level commands (`click`, `fill`, `press`, `scroll`, `wait`, `snapshot`) instead.
|
||||
## Troubleshooting `js_error`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `js_error` on `snapshot --interactive` or `eval`
|
||||
|
||||
Some complex pages can reject or break the JavaScript used for rich snapshots and ad-hoc evaluation.
|
||||
|
||||
Recovery steps:
|
||||
Some complex pages reject the JavaScript behind `snapshot --interactive` and `eval`. Recover by checking whether the page actually navigated, then falling back to raw text or HTML:
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 get url
|
||||
@@ -149,6 +62,23 @@ cmux browser surface:7 get text body
|
||||
cmux browser surface:7 get html body
|
||||
```
|
||||
|
||||
- Use `get url` first so you know whether the page actually navigated.
|
||||
- Fall back to `get text body` or `get html body` when `snapshot --interactive` or `eval` returns `js_error`.
|
||||
- If the page is still failing, navigate to a simpler intermediate page, then retry the task from there.
|
||||
If it still fails, navigate to a simpler intermediate page and retry from there.
|
||||
|
||||
## Deep-dive references
|
||||
|
||||
| Reference | When to Use |
|
||||
|-----------|-------------|
|
||||
| [references/commands.md](references/commands.md) | Full command mapping, `agent-browser` equivalents, viewport error codes |
|
||||
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle and stale-ref troubleshooting |
|
||||
| [references/authentication.md](references/authentication.md) | Login/OAuth/2FA patterns and state save/load |
|
||||
| [references/session-management.md](references/session-management.md) | Multi-surface isolation and state persistence |
|
||||
| [references/video-recording.md](references/video-recording.md) | Recording status and practical alternatives |
|
||||
| [references/proxy-support.md](references/proxy-support.md) | Proxy behavior in WKWebView and workarounds |
|
||||
|
||||
## Ready-to-use templates
|
||||
|
||||
| Template | Description |
|
||||
|----------|-------------|
|
||||
| [templates/form-automation.sh](templates/form-automation.sh) | Snapshot/ref form fill loop |
|
||||
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, save/load state |
|
||||
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Navigate and capture snapshots/screenshots |
|
||||
|
||||
@@ -1,38 +1,20 @@
|
||||
# Authentication Patterns
|
||||
|
||||
Login flows, session persistence, OAuth, and 2FA patterns for cmux browser surfaces.
|
||||
Login flows, session persistence, OAuth, and 2FA for cmux browser surfaces. Related: [session-management.md](session-management.md), [../SKILL.md](../SKILL.md).
|
||||
|
||||
**Related**: [session-management.md](session-management.md), [SKILL.md](../SKILL.md)
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Login Flow](#basic-login-flow)
|
||||
- [Saving Authentication State](#saving-authentication-state)
|
||||
- [Restoring Authentication](#restoring-authentication)
|
||||
- [OAuth / SSO Flows](#oauth--sso-flows)
|
||||
- [Two-Factor Authentication](#two-factor-authentication)
|
||||
- [Cookie-Based Auth](#cookie-based-auth)
|
||||
- [Token Refresh Handling](#token-refresh-handling)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
|
||||
## Basic Login Flow
|
||||
## Basic login
|
||||
|
||||
```bash
|
||||
cmux browser open https://app.example.com/login --json
|
||||
cmux browser surface:7 wait --load-state complete --timeout-ms 15000
|
||||
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
# [ref=e1] email, [ref=e2] password, [ref=e3] submit
|
||||
|
||||
cmux browser surface:7 snapshot --interactive # e1 email, e2 password, e3 submit
|
||||
cmux browser surface:7 fill e1 "[email protected]"
|
||||
cmux browser surface:7 fill e2 "$APP_PASSWORD"
|
||||
cmux browser surface:7 click e3 --snapshot-after --json
|
||||
cmux browser surface:7 wait --url-contains "/dashboard" --timeout-ms 20000
|
||||
```
|
||||
|
||||
## Saving Authentication State
|
||||
|
||||
After logging in, save state for reuse:
|
||||
## Saving authentication state
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 state save ./auth-state.json
|
||||
@@ -40,7 +22,7 @@ cmux browser surface:7 state save ./auth-state.json
|
||||
|
||||
State includes cookies, localStorage, sessionStorage, and open tab metadata for that surface.
|
||||
|
||||
## Restoring Authentication
|
||||
## Restoring authentication
|
||||
|
||||
```bash
|
||||
cmux browser open https://app.example.com --json
|
||||
@@ -49,58 +31,44 @@ cmux browser surface:8 goto https://app.example.com/dashboard
|
||||
cmux browser surface:8 snapshot --interactive
|
||||
```
|
||||
|
||||
## OAuth / SSO Flows
|
||||
## OAuth / SSO
|
||||
|
||||
Same shape as basic login, waiting on the provider host and then the return host, with generous timeouts:
|
||||
|
||||
```bash
|
||||
cmux browser open https://app.example.com/auth/google --json
|
||||
cmux browser surface:7 wait --url-contains "accounts.google.com" --timeout-ms 30000
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
|
||||
cmux browser surface:7 fill e1 "[email protected]"
|
||||
cmux browser surface:7 click e2 --snapshot-after --json
|
||||
|
||||
# fill and click the provider's fields
|
||||
cmux browser surface:7 wait --url-contains "app.example.com" --timeout-ms 45000
|
||||
cmux browser surface:7 state save ./oauth-state.json
|
||||
```
|
||||
|
||||
## Two-Factor Authentication
|
||||
## Two-factor
|
||||
|
||||
```bash
|
||||
cmux browser open https://app.example.com/login --json
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
cmux browser surface:7 fill e1 "[email protected]"
|
||||
cmux browser surface:7 fill e2 "$APP_PASSWORD"
|
||||
cmux browser surface:7 click e3
|
||||
Drive the password step, let the user complete 2FA in the webview, then wait with a long timeout (`--url-contains "/dashboard" --timeout-ms 120000`) and save state.
|
||||
|
||||
# complete 2FA manually in the webview, then:
|
||||
cmux browser surface:7 wait --url-contains "/dashboard" --timeout-ms 120000
|
||||
cmux browser surface:7 state save ./2fa-state.json
|
||||
```
|
||||
|
||||
## Cookie-Based Auth
|
||||
## Cookie-based auth
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 cookies set session_token "abc123xyz"
|
||||
cmux browser surface:7 goto https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Token Refresh Handling
|
||||
## Token refresh
|
||||
|
||||
Load saved state, navigate, and re-login only when the URL bounced to `/login`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
STATE_FILE="./auth-state.json"
|
||||
SURFACE="surface:7"
|
||||
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
cmux browser "$SURFACE" state load "$STATE_FILE"
|
||||
fi
|
||||
|
||||
[ -f "$STATE_FILE" ] && cmux browser "$SURFACE" state load "$STATE_FILE"
|
||||
cmux browser "$SURFACE" goto https://app.example.com/dashboard
|
||||
URL=$(cmux browser "$SURFACE" get url)
|
||||
|
||||
if printf '%s' "$URL" | grep -q '/login'; then
|
||||
if cmux browser "$SURFACE" get url | grep -q '/login'; then
|
||||
cmux browser "$SURFACE" snapshot --interactive
|
||||
cmux browser "$SURFACE" fill e1 "$APP_USERNAME"
|
||||
cmux browser "$SURFACE" fill e2 "$APP_PASSWORD"
|
||||
@@ -110,11 +78,9 @@ if printf '%s' "$URL" | grep -q '/login'; then
|
||||
fi
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
## Security
|
||||
|
||||
1. Never commit state files (they include auth tokens).
|
||||
2. Use environment variables for credentials.
|
||||
3. Clear state/cookies after sensitive tasks:
|
||||
Never commit state files; they contain auth tokens. Take credentials from environment variables. Clear state after sensitive tasks:
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 cookies clear
|
||||
|
||||
@@ -1,35 +1,20 @@
|
||||
# Command Reference (cmux Browser)
|
||||
|
||||
This maps common `agent-browser` usage to `cmux browser` usage.
|
||||
## agent-browser equivalents
|
||||
|
||||
## Direct Equivalents
|
||||
`agent-browser <verb>` maps to `cmux browser <surface> <verb>` for `goto`/`navigate`, `click`, `fill`, `type`, `select`, `get text`, `get url`, `get title`. `agent-browser snapshot -i` is `cmux browser <surface> snapshot --interactive`. `agent-browser open <url>` is `cmux browser open <url>` (no surface, since it creates one).
|
||||
|
||||
- `agent-browser open <url>` -> `cmux browser open <url>`
|
||||
- `agent-browser goto|navigate <url>` -> `cmux browser <surface> goto|navigate <url>`
|
||||
- `agent-browser snapshot -i` -> `cmux browser <surface> snapshot --interactive`
|
||||
- `agent-browser click <ref>` -> `cmux browser <surface> click <ref>`
|
||||
- `agent-browser fill <ref> <text>` -> `cmux browser <surface> fill <ref> <text>`
|
||||
- `agent-browser type <ref> <text>` -> `cmux browser <surface> type <ref> <text>`
|
||||
- `agent-browser select <ref> <value>` -> `cmux browser <surface> select <ref> <value>`
|
||||
- `agent-browser get text <ref>` -> `cmux browser <surface> get text <ref-or-selector>`
|
||||
- `agent-browser get url` -> `cmux browser <surface> get url`
|
||||
- `agent-browser get title` -> `cmux browser <surface> get title`
|
||||
|
||||
## Core Command Groups
|
||||
|
||||
### Navigation
|
||||
## Navigation
|
||||
|
||||
```bash
|
||||
cmux browser open <url> # opens in caller's workspace (uses CMUX_WORKSPACE_ID)
|
||||
cmux browser open <url> --workspace <id|ref> # opens in a specific workspace
|
||||
cmux browser open <url> # caller's workspace, via CMUX_WORKSPACE_ID
|
||||
cmux browser open <url> --workspace <id|ref>
|
||||
cmux browser <surface> goto <url>
|
||||
cmux browser <surface> back|forward|reload
|
||||
cmux browser <surface> get url|title
|
||||
```
|
||||
|
||||
> **Workspace context:** `browser open` targets the workspace of the terminal where the command is run (via `CMUX_WORKSPACE_ID`), even if a different workspace is currently focused. Use `--workspace` to override.
|
||||
|
||||
### Snapshot and Inspection
|
||||
## Snapshot and inspection
|
||||
|
||||
```bash
|
||||
cmux browser <surface> snapshot --interactive
|
||||
@@ -44,7 +29,7 @@ cmux browser <surface> get styles "#submit" --property color
|
||||
cmux browser <surface> eval '<js>'
|
||||
```
|
||||
|
||||
### Interaction
|
||||
## Interaction
|
||||
|
||||
```bash
|
||||
cmux browser <surface> click|dblclick|hover|focus <selector-or-ref>
|
||||
@@ -56,21 +41,9 @@ cmux browser <surface> check|uncheck <selector-or-ref>
|
||||
cmux browser <surface> scroll [--selector <css>] [--dx <n>] [--dy <n>]
|
||||
```
|
||||
|
||||
### Design Mode
|
||||
Keyboard names follow Playwright/W3C conventions (`Enter`, `Tab`, `Escape`, `ArrowLeft`, `Space`). `Space`, `Spacebar`, and `space` all emit DOM key `" "` with code `"Space"`; use `--key ' '` to pass the raw DOM key.
|
||||
|
||||
```bash
|
||||
cmux browser design-mode enable --surface <surface>
|
||||
cmux browser design-mode status --surface <surface> --json
|
||||
cmux browser design-mode disable --surface <surface>
|
||||
```
|
||||
|
||||
Design mode lets a user select page elements and copy their DOM, style, URL, and screenshot context for pasting into an agent. CLI enable/disable never moves application focus or copies context automatically.
|
||||
|
||||
Keyboard names follow Playwright/W3C conventions, including `Enter`, `Tab`,
|
||||
`Escape`, `ArrowLeft`, and `Space`. `Space`, `Spacebar`, and `space` all emit
|
||||
DOM key `" "` with code `"Space"`; use `--key ' '` to pass the raw DOM key.
|
||||
|
||||
### Wait
|
||||
## Wait
|
||||
|
||||
```bash
|
||||
cmux browser <surface> wait --selector "#ready" --timeout-ms 10000
|
||||
@@ -80,18 +53,21 @@ cmux browser <surface> wait --load-state complete --timeout-ms 15000
|
||||
cmux browser <surface> wait --function "document.readyState === 'complete'" --timeout-ms 10000
|
||||
```
|
||||
|
||||
### Session/State
|
||||
## Design mode
|
||||
|
||||
```bash
|
||||
cmux browser design-mode enable|status|disable --surface <surface> [--json]
|
||||
```
|
||||
|
||||
Design mode lets a user select page elements and copy their DOM, style, URL, and screenshot context for pasting into an agent. CLI enable/disable never moves application focus or copies context automatically.
|
||||
|
||||
## Session, state, diagnostics
|
||||
|
||||
```bash
|
||||
cmux browser <surface> cookies get|set|clear ...
|
||||
cmux browser <surface> storage local|session get|set|clear ...
|
||||
cmux browser <surface> tab list|new|switch|close ...
|
||||
cmux browser <surface> state save|load <path>
|
||||
```
|
||||
|
||||
### Diagnostics
|
||||
|
||||
```bash
|
||||
cmux browser <surface> console list|clear
|
||||
cmux browser <surface> errors list|clear
|
||||
cmux browser <surface> highlight <selector>
|
||||
@@ -99,16 +75,11 @@ cmux browser <surface> screenshot
|
||||
cmux browser <surface> download wait --timeout-ms 10000
|
||||
```
|
||||
|
||||
## Agent Reliability Tips
|
||||
## Agent reliability
|
||||
|
||||
- Use `--snapshot-after` on mutating actions to return a fresh post-action snapshot.
|
||||
- Re-snapshot after navigation, modal open/close, or major DOM changes.
|
||||
- Prefer short handles in outputs by default (`surface:N`, `pane:N`, `workspace:N`, `window:N`).
|
||||
- Use `--id-format both` only when a UUID must be logged/exported.
|
||||
Use `--snapshot-after` on mutating actions to get a fresh post-action snapshot. Re-snapshot after navigation, modal open/close, or major DOM changes. Prefer short handles in output; use `--id-format both` only when a UUID must be logged or exported.
|
||||
|
||||
## WKWebView Viewport Emulation
|
||||
|
||||
Set an exact logical viewport after opening a browser surface:
|
||||
## Viewport emulation
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 viewport 1280 720
|
||||
@@ -116,30 +87,12 @@ cmux browser surface:7 screenshot --out /tmp/desktop.png
|
||||
cmux browser surface:7 viewport reset
|
||||
```
|
||||
|
||||
The requested dimensions are limited to 1...4096 CSS pixels. cmux changes
|
||||
`window.innerWidth` and `window.innerHeight`, then aspect-fits the page inside
|
||||
the existing pane. It does not resize the pane, move other surfaces, or change
|
||||
focus. Visible-viewport screenshots use the emulated dimensions. The JSON result
|
||||
includes the logical and displayed dimensions, scale, presentation mode, and
|
||||
whether the pane was resized. Screenshot PNG dimensions are exact CSS pixels on
|
||||
both Retina and non-Retina displays. Combined viewport/page-zoom geometry is
|
||||
bounded; unsupported combinations leave the viewport unchanged and return
|
||||
`invalid_params` with `reason: viewport_zoom_render_geometry_too_large` and
|
||||
`maximum_page_zoom`. Close or detach an attached browser inspector before setting
|
||||
or resetting the viewport; the command otherwise returns `invalid_state` with
|
||||
`reason: attached_browser_inspector`. Opening or redocking an attached inspector
|
||||
while emulation is active resets the viewport to native sizing.
|
||||
Dimensions are limited to 1..4096 CSS pixels. cmux changes `window.innerWidth`/`window.innerHeight` and aspect-fits the page inside the existing pane; it does not resize the pane, move other surfaces, or change focus. The JSON result includes logical and displayed dimensions, scale, presentation mode, and whether the pane was resized. Screenshot PNG dimensions are exact CSS pixels on Retina and non-Retina displays.
|
||||
|
||||
## Known WKWebView Gaps (`not_supported`)
|
||||
Error cases: an unsupported viewport/page-zoom combination leaves the viewport unchanged and returns `invalid_params` with `reason: viewport_zoom_render_geometry_too_large` plus `maximum_page_zoom`. An attached browser inspector returns `invalid_state` with `reason: attached_browser_inspector`; close or detach it first. Opening or redocking an attached inspector while emulation is active resets the viewport to native sizing.
|
||||
|
||||
- `browser.geolocation.set`
|
||||
- `browser.offline.set`
|
||||
- `browser.trace.start|stop`
|
||||
- `browser.network.route|unroute|requests`
|
||||
- `browser.screencast.start|stop`
|
||||
- `browser.input_mouse|input_keyboard|input_touch`
|
||||
## Known WKWebView gaps (`not_supported`)
|
||||
|
||||
See also:
|
||||
- [snapshot-refs.md](snapshot-refs.md)
|
||||
- [authentication.md](authentication.md)
|
||||
- [session-management.md](session-management.md)
|
||||
`browser.geolocation.set`, `browser.offline.set`, `browser.trace.start|stop`, `browser.network.route|unroute|requests`, `browser.screencast.start|stop`, `browser.input_mouse|input_keyboard|input_touch`.
|
||||
|
||||
See also [snapshot-refs.md](snapshot-refs.md), [authentication.md](authentication.md), [session-management.md](session-management.md).
|
||||
|
||||
@@ -1,37 +1,12 @@
|
||||
# Proxy Support
|
||||
|
||||
How proxy behavior works for cmux browser automation.
|
||||
cmux browser uses WKWebView networking, so proxy behavior follows macOS/system networking and the app process environment. Related: [commands.md](commands.md), [../SKILL.md](../SKILL.md).
|
||||
|
||||
**Related**: [commands.md](commands.md), [SKILL.md](../SKILL.md)
|
||||
There is no `cmux browser proxy ...` command for per-surface routing: WKWebView has no CDP-style per-context proxy controls. Configure a system or network-level proxy for the environment cmux runs in, or route traffic through an upstream gateway you control.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Current Behavior](#current-behavior)
|
||||
- [What Is Not Exposed via CLI](#what-is-not-exposed-via-cli)
|
||||
- [Workarounds](#workarounds)
|
||||
- [Verification](#verification)
|
||||
|
||||
## Current Behavior
|
||||
|
||||
cmux browser uses WKWebView networking. Proxy behavior follows macOS/system networking and app process environment.
|
||||
|
||||
## What Is Not Exposed via CLI
|
||||
|
||||
There is currently no first-class `cmux browser proxy ...` command for per-surface proxy routing.
|
||||
|
||||
Why: WKWebView does not provide CDP-style per-context proxy controls equivalent to Chrome automation stacks.
|
||||
|
||||
## Workarounds
|
||||
|
||||
1. Configure system/network-level proxy for the environment where cmux runs.
|
||||
2. Route traffic through an upstream gateway you control.
|
||||
3. Validate behavior with explicit IP checks.
|
||||
|
||||
## Verification
|
||||
Verify egress:
|
||||
|
||||
```bash
|
||||
cmux browser open https://httpbin.org/ip --json
|
||||
cmux browser surface:7 get text body
|
||||
```
|
||||
|
||||
Compare returned IP against expected proxy egress.
|
||||
|
||||
@@ -1,94 +1,35 @@
|
||||
# Session Management
|
||||
|
||||
cmux uses isolated browser contexts per surface. Treat each browser surface as its own session.
|
||||
cmux gives each browser surface its own context. Every surface is an independent session with its own cookies, localStorage/sessionStorage, tab list and active tab, and navigation history. Related: [authentication.md](authentication.md), [../SKILL.md](../SKILL.md).
|
||||
|
||||
**Related**: [authentication.md](authentication.md), [SKILL.md](../SKILL.md)
|
||||
## Parallel sessions
|
||||
|
||||
## Contents
|
||||
|
||||
- [Surface-Based Sessions](#surface-based-sessions)
|
||||
- [Isolation Properties](#isolation-properties)
|
||||
- [State Persistence](#state-persistence)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Cleanup](#cleanup)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Surface-Based Sessions
|
||||
Each `cmux browser open` returns a new surface ref; drive them independently.
|
||||
|
||||
```bash
|
||||
# session A
|
||||
cmux browser open https://app.example.com/login --json
|
||||
# -> surface:7
|
||||
|
||||
# session B
|
||||
cmux browser open https://example.com --json
|
||||
# -> surface:8
|
||||
|
||||
cmux browser surface:7 get url
|
||||
cmux browser surface:8 get url
|
||||
```
|
||||
|
||||
## Isolation Properties
|
||||
|
||||
Each surface has independent:
|
||||
- cookies
|
||||
- localStorage/sessionStorage
|
||||
- tab list and active tab
|
||||
- navigation history
|
||||
|
||||
## State Persistence
|
||||
|
||||
### Save State
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 state save /tmp/auth-state.json
|
||||
```
|
||||
|
||||
### Load State
|
||||
|
||||
```bash
|
||||
cmux browser surface:8 state load /tmp/auth-state.json
|
||||
cmux browser surface:8 goto https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Reuse Auth Across New Surface
|
||||
|
||||
```bash
|
||||
cmux browser open https://app.example.com/login --json
|
||||
# login on surface:7 ...
|
||||
cmux browser surface:7 state save /tmp/auth.json
|
||||
|
||||
cmux browser open https://app.example.com --json
|
||||
# assume surface:8
|
||||
cmux browser surface:8 state load /tmp/auth.json
|
||||
cmux browser surface:8 goto https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### Parallel Multi-Site Tasks
|
||||
|
||||
```bash
|
||||
cmux browser open https://site-a.example --json
|
||||
cmux browser open https://site-b.example --json
|
||||
cmux browser open https://site-c.example --json
|
||||
cmux browser open https://site-a.example --json # -> surface:11
|
||||
cmux browser open https://site-b.example --json # -> surface:12
|
||||
|
||||
cmux browser surface:11 get text body > /tmp/a.txt
|
||||
cmux browser surface:12 get text body > /tmp/b.txt
|
||||
cmux browser surface:13 get text body > /tmp/c.txt
|
||||
```
|
||||
|
||||
## Reusing auth across surfaces
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 state save /tmp/auth.json # after logging in on surface:7
|
||||
cmux browser open https://app.example.com --json # -> surface:8
|
||||
cmux browser surface:8 state load /tmp/auth.json
|
||||
cmux browser surface:8 goto https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
cmux close-surface --surface surface:7
|
||||
cmux close-surface --surface surface:8
|
||||
rm -f /tmp/auth-state.json
|
||||
rm -f /tmp/auth.json
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## Best practices
|
||||
|
||||
1. Name/log surfaces in your script output so actions stay attributable.
|
||||
2. Keep one task per surface to avoid ref churn.
|
||||
3. Save state after successful auth milestones.
|
||||
4. Re-snapshot after switching tabs/pages inside a surface.
|
||||
Log surface refs in script output so actions stay attributable, keep one task per surface to avoid ref churn, save state after successful auth milestones, and re-snapshot after switching tabs or pages inside a surface.
|
||||
|
||||
@@ -1,88 +1,23 @@
|
||||
# Snapshot and Refs
|
||||
|
||||
Element refs from snapshots make browser automation compact and reliable.
|
||||
|
||||
**Related**: [commands.md](commands.md), [SKILL.md](../SKILL.md)
|
||||
|
||||
## Contents
|
||||
|
||||
- [How Refs Work](#how-refs-work)
|
||||
- [The Snapshot Command](#the-snapshot-command)
|
||||
- [Using Refs](#using-refs)
|
||||
- [Ref Lifecycle](#ref-lifecycle)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## How Refs Work
|
||||
|
||||
Classic flow:
|
||||
|
||||
```text
|
||||
full DOM/HTML -> selector guessing -> action
|
||||
```
|
||||
|
||||
cmux flow:
|
||||
|
||||
```text
|
||||
snapshot -> refs (e1/e2/...) -> direct action
|
||||
```
|
||||
|
||||
## The Snapshot Command
|
||||
Instead of dumping the DOM and guessing selectors, snapshot the page and act on the returned refs (`e1`, `e2`, ...). Related: [commands.md](commands.md), [../SKILL.md](../SKILL.md).
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 snapshot
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
cmux browser surface:7 snapshot --interactive --compact --max-depth 3
|
||||
```
|
||||
|
||||
## Using Refs
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 click e6
|
||||
cmux browser surface:7 fill e10 "[email protected]"
|
||||
cmux browser surface:7 fill e11 "password123"
|
||||
cmux browser surface:7 click e12
|
||||
```
|
||||
|
||||
## Ref Lifecycle
|
||||
## Ref lifecycle
|
||||
|
||||
Refs are invalidated when page structure changes.
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
# e1 is "Next"
|
||||
|
||||
cmux browser surface:7 click e1
|
||||
|
||||
# page changed, take a fresh snapshot
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Snapshot before interacting.
|
||||
2. Re-snapshot after navigation/modal/open-close flows.
|
||||
3. Use `--snapshot-after` on mutating actions.
|
||||
4. Scope snapshots with `--selector` for very large pages.
|
||||
Refs are invalidated when page structure changes. Snapshot before interacting, re-snapshot after navigation and modal open/close, and use `--snapshot-after` on mutating actions so the fresh snapshot comes back with the result.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### not_found / stale ref
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
```
|
||||
|
||||
### Element missing due visibility/timing
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 wait --selector "#target" --timeout-ms 10000
|
||||
cmux browser surface:7 scroll --dy 400
|
||||
cmux browser surface:7 snapshot --interactive
|
||||
```
|
||||
|
||||
### Too many elements
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 snapshot --selector "form#checkout" --interactive
|
||||
```
|
||||
- **`not_found` / stale ref**: take a fresh `snapshot --interactive`.
|
||||
- **Element missing on visibility or timing**: `wait --selector "#target" --timeout-ms 10000`, or `scroll --dy 400`, then re-snapshot.
|
||||
- **Too many elements**: scope the snapshot, e.g. `snapshot --selector "form#checkout" --interactive`.
|
||||
|
||||
@@ -1,52 +1,15 @@
|
||||
# Video Recording
|
||||
|
||||
Status and alternatives for capturing browser automation evidence in cmux.
|
||||
`cmux browser` exposes no built-in video recording command: automation runs on WKWebView, and the agent-browser recording pipeline is Chrome/CDP-specific. Related: [commands.md](commands.md), [../SKILL.md](../SKILL.md).
|
||||
|
||||
**Related**: [commands.md](commands.md), [SKILL.md](../SKILL.md)
|
||||
|
||||
## Contents
|
||||
|
||||
- [Current Status](#current-status)
|
||||
- [Recommended Alternatives](#recommended-alternatives)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Current Status
|
||||
|
||||
`cmux browser` currently does not expose a built-in video recording command.
|
||||
|
||||
Why: cmux browser automation runs on WKWebView, and the agent-browser style recording pipeline is Chrome/CDP-specific.
|
||||
|
||||
## Recommended Alternatives
|
||||
|
||||
### 1. Step Screenshots
|
||||
Capture evidence for flaky-automation debugging, CI logs, and release-to-release flow diffs with step screenshots and a snapshot timeline instead:
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 screenshot > /tmp/step1.b64
|
||||
cmux browser surface:7 click e3 --snapshot-after --json
|
||||
cmux browser surface:7 screenshot > /tmp/step2.b64
|
||||
```
|
||||
|
||||
### 2. Snapshot Timeline
|
||||
|
||||
```bash
|
||||
cmux browser surface:7 snapshot --interactive > /tmp/snap-1.txt
|
||||
cmux browser surface:7 click e3 --snapshot-after --json > /tmp/action-1.json
|
||||
cmux --json browser surface:7 click e3 --snapshot-after > /tmp/action-1.json
|
||||
cmux browser surface:7 screenshot > /tmp/step2.b64
|
||||
cmux browser surface:7 snapshot --interactive > /tmp/snap-2.txt
|
||||
```
|
||||
|
||||
### 3. macOS Window Capture (external)
|
||||
|
||||
Use an external screen recorder if full-motion capture is required.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Debug flaky browser automation.
|
||||
- Produce artifacts for CI logs.
|
||||
- Document flow changes between releases.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Capture snapshot before and after each mutating action.
|
||||
2. Add `--snapshot-after` on clicks/fills/types that change state.
|
||||
3. Keep artifacts grouped by timestamp/run id.
|
||||
Capture before and after each mutating action, add `--snapshot-after` on state-changing clicks/fills/types, and group artifacts by timestamp or run id. Use an external screen recorder when full-motion capture is genuinely required.
|
||||
|
||||
@@ -5,13 +5,11 @@ description: "Build a custom cmux sidebar from a plain-language request. Use whe
|
||||
|
||||
# cmux Custom Sidebar
|
||||
|
||||
cmux renders custom sidebars from a small SwiftUI-style file at runtime: no Xcode, no build step, no signing. The file hot-reloads on save, binds to live cmux state (workspaces, tabs, git, PRs, clock), and can run real cmux commands on tap.
|
||||
cmux renders custom sidebars from a small SwiftUI-style file at runtime: no Xcode, no build step, no signing. The file hot-reloads on save, binds to live cmux state (workspaces, tabs, git, PRs, clock), and runs real cmux commands on tap.
|
||||
|
||||
The person asking is usually describing a result ("a sidebar that shows my workspaces and lets me jump between them"), not an implementation. Turn that into a clean, native-looking sidebar and make the engineering decisions for them. Do not ask them about SwiftUI, files, or syntax.
|
||||
The person asking is describing a result ("a sidebar that shows my workspaces and lets me jump between them"), not an implementation. Make the engineering decisions for them; do not ask them about SwiftUI, files, or syntax.
|
||||
|
||||
## Full reference
|
||||
|
||||
This skill is the workflow summary. The complete authoring contract (every supported view, modifier, language feature, and data field) is one command away; read it before writing a non-trivial sidebar:
|
||||
This skill is the workflow summary. Read the complete authoring contract (every supported view, modifier, language feature, and data field) before writing a non-trivial sidebar:
|
||||
|
||||
```bash
|
||||
cmux docs sidebars
|
||||
@@ -20,28 +18,24 @@ curl -fsSL https://raw.githubusercontent.com/manaflow-ai/cmux/main/docs/custom-s
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Enable the beta** (once). Custom sidebars are behind Settings → Beta features → Custom sidebars (`customSidebars.beta.enabled`). If a written sidebar does not appear in the picker, this flag is the first thing to check.
|
||||
2. **Write a named file.** The name becomes the menu label; use short kebab-case:
|
||||
```
|
||||
~/.config/cmux/sidebars/<name>.swift
|
||||
```
|
||||
The file is a single SwiftUI-style view expression (no `struct`, no `var body`, no imports). A `.json` variant exists for static layouts; prefer `.swift` for anything dynamic.
|
||||
3. **Validate and select it:**
|
||||
1. **Enable the beta** (once): Settings > Beta features > Custom sidebars (`customSidebars.beta.enabled`). If a written sidebar does not appear in the picker, check this first.
|
||||
2. **Write a named file** at `~/.config/cmux/sidebars/<name>.swift`. The name becomes the menu label; use short kebab-case. The file is a single SwiftUI-style view expression (no `struct`, no `var body`, no imports). A `.json` variant exists for static layouts; prefer `.swift` for anything dynamic.
|
||||
3. **Validate and select:**
|
||||
```bash
|
||||
cmux sidebar validate <name> # parse/interpret check with real data shapes
|
||||
cmux sidebar select <name> # switch the sidebar to it
|
||||
cmux sidebar select <name>
|
||||
```
|
||||
The user can also pick it manually: right-click the sidebar toggle button.
|
||||
4. **Iterate.** Saving the file hot-reloads the sidebar in place (`cmux sidebar reload` forces it). Look at the result, fix what looks off, and verify rows show real data and taps do the right thing before declaring it done.
|
||||
The user can also right-click the sidebar toggle button to pick it.
|
||||
4. **Iterate.** Saving hot-reloads in place (`cmux sidebar reload` forces it). Verify rows show real data and taps do the right thing before declaring it done.
|
||||
|
||||
## Authoring rules
|
||||
|
||||
- **Default to live data.** Bind to the `workspaces` context instead of hard-coding text so the sidebar stays correct on its own.
|
||||
- **Make it interactive by default.** Rows that represent something openable should run the matching `cmux(...)` action on tap. A list that just displays text is rarely what they wanted.
|
||||
- **Prefer `Reorderable` for workspace-like lists.** It gives persisted drag-and-drop reordering for free.
|
||||
- **Keep it native and uncluttered:** a title, a divider, then the content.
|
||||
- **Cap long lists** (`.prefix(20)`, filter/sort before rendering). The sidebar re-evaluates about once a second; do not render hundreds of rows.
|
||||
- **Stay inside the supported subset.** Unsupported syntax is skipped gracefully (never crashes), but choose the closest supported approach rather than shipping a half-blank sidebar.
|
||||
- Bind to the `workspaces` context instead of hard-coding text, so the sidebar stays correct on its own.
|
||||
- Rows that represent something openable run the matching `cmux(...)` action on tap. A list that only displays text is rarely what they wanted.
|
||||
- Use `Reorderable` for workspace-like lists; it gives persisted drag-and-drop reordering for free.
|
||||
- Keep it native and uncluttered: a title, a divider, then the content.
|
||||
- Cap long lists (`.prefix(20)`, filter/sort before rendering). The sidebar re-evaluates about once a second.
|
||||
- Stay inside the supported subset. Unsupported syntax is skipped gracefully rather than crashing, but choose the closest supported approach instead of shipping a half-blank sidebar.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -67,7 +61,7 @@ cmux sidebar validate mine && cmux sidebar select mine
|
||||
|
||||
## Live data context (read-only, refreshes ~1s)
|
||||
|
||||
- `workspaces`: array with `id`, `title`, `selected`, `pinned`, `index`, `directory`, `ports` + `portCount`, `unread`, `tabs` + `tabCount`; plus, when present: `description`, `color`, `branch` + `dirty`, `pr` / `prs` (`{number, label, url, status, stale, branch}`), `progress` (`{value, label}`), `latestMessage`, `latestPrompt`, `latestAt`, `remote` (`{target, state, connected}`).
|
||||
- `workspaces`: `id`, `title`, `selected`, `pinned`, `index`, `directory`, `ports` + `portCount`, `unread`, `tabs` + `tabCount`; when present also `description`, `color`, `branch` + `dirty`, `pr` / `prs` (`{number, label, url, status, stale, branch}`), `progress` (`{value, label}`), `latestMessage`, `latestPrompt`, `latestAt`, `remote` (`{target, state, connected}`).
|
||||
- `workspaces[i].tabs`: `id`, `title`, `focused`, `pinned`; plus `directory`, `branch` + `dirty`, `ports` when available.
|
||||
- `clock`: `{time, hour, minute, second, weekday, epoch}`.
|
||||
- Scalars: `workspaceCount`, `selectedTitle`, `selectedId`, `unreadTotal`.
|
||||
@@ -76,17 +70,17 @@ Optional fields are omitted when absent; guard with `if let b = w.branch { ... }
|
||||
|
||||
## Actions
|
||||
|
||||
A button or `.onTapGesture` body calls `cmux("<method>", param: value)`, dispatched through the same surface as the `cmux` CLI. Common methods: `workspace.select` (`workspace_id`), `surface.focus` (`surface_id`), `workspace.reorder` (`workspace_id` + `index`). `openURL("https://...")` opens links. Discover the full command surface with `cmux docs api`.
|
||||
A button or `.onTapGesture` body calls `cmux("<method>", param: value)`, dispatched through the same surface as the CLI. Common methods: `workspace.select` (`workspace_id`), `surface.focus` (`surface_id`), `workspace.reorder` (`workspace_id` + `index`). `openURL("https://...")` opens links. Full command surface: `cmux docs api`.
|
||||
|
||||
## Supported subset at a glance
|
||||
## Supported subset
|
||||
|
||||
Containers: stacks (incl. lazy), `Group`, `List`, `Section`, grids, `ViewThatFits`, `ScrollView`, `HSplitView` (two resizable columns). Content: `Text`, `Label`, `Image(systemName:)`, `Button` (title and label form), `Menu`, `ProgressView`, `Gauge`, `Spacer`, `Divider`, shapes, gradients via `.background`. Modifiers: full typography set, colors as hex strings or tokens, `.padding`/`.frame`/layout, `.background`/`.overlay`/`.mask`/`.contextMenu` with arbitrary nested views, shadows/borders/opacity/effects, `.onTapGesture`, `.help`, `.disabled`. Language: `let`, user `func` helpers, `for`/`ForEach`, `if/else`, ternary, string interpolation, arithmetic, array methods (`filter`/`map`/`sorted`/`prefix`/...), string and number formatting.
|
||||
Containers: stacks (including lazy), `Group`, `List`, `Section`, grids, `ViewThatFits`, `ScrollView`, `HSplitView` (two resizable columns). Content: `Text`, `Label`, `Image(systemName:)`, `Button` (title and label form), `Menu`, `ProgressView`, `Gauge`, `Spacer`, `Divider`, shapes, gradients via `.background`. Modifiers: full typography set, colors as hex strings or tokens, `.padding`/`.frame`/layout, `.background`/`.overlay`/`.mask`/`.contextMenu` with arbitrary nested views, shadows/borders/opacity/effects, `.onTapGesture`, `.help`, `.disabled`. Language: `let`, user `func` helpers, `for`/`ForEach`, `if/else`, ternary, string interpolation, arithmetic, array methods (`filter`/`map`/`sorted`/`prefix`), string and number formatting.
|
||||
|
||||
Not yet supported (write the natural Swift anyway; it degrades gracefully): `@State` and input controls (`TextField`, `Toggle`, `Slider`, `Picker`), custom `struct`/`View` definitions, navigation (`sheet`/`popover`), `AsyncImage`. Two-way editing does not work yet; taps that run `cmux(...)` do.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Sidebar missing from the right-click picker: the beta flag is off, or the file is not directly under `~/.config/cmux/sidebars/`.
|
||||
- Blank or partial render: run `cmux sidebar validate <name>`; errors show inline in the sidebar with the failing location. A broken save keeps the last working render on screen, so re-save after fixing.
|
||||
- Missing from the right-click picker: the beta flag is off, or the file is not directly under `~/.config/cmux/sidebars/`.
|
||||
- Blank or partial render: run `cmux sidebar validate <name>`. Errors show inline in the sidebar with the failing location; a broken save keeps the last working render on screen, so re-save after fixing.
|
||||
- Rows not tappable: wrap the row in `Button(action: { cmux(...) }) { ... }` or add `.onTapGesture { cmux(...) }`.
|
||||
- Reorder not persisting: use `Reorderable(data, move: "workspace.reorder")`, not `List`/`.onMove`/`.draggable`.
|
||||
|
||||
@@ -5,50 +5,36 @@ description: "Customize cmux for an end user. Use when changing cmux.json action
|
||||
|
||||
# cmux Customization
|
||||
|
||||
Use this skill for user-facing cmux customization. Keep the user's config intact, prefer schema-backed edits, and validate before reporting completion.
|
||||
Keep the user's config intact, prefer schema-backed edits, and validate before reporting completion.
|
||||
|
||||
## What Can Be Customized
|
||||
## Choose the right surface
|
||||
|
||||
- Custom actions: define reusable `actions` in `cmux.json`. Actions can appear in Cmd+Shift+P, surface tab bars, shortcuts, and the plus-button right-click menu.
|
||||
- New workspace button: set `ui.newWorkspace.action` to replace the normal plus-button click, and `ui.newWorkspace.contextMenu` to control right-click actions. `ui.newWorkspace.rightClick` is accepted as an alias, but new examples should use `contextMenu`.
|
||||
- Surface tab bar buttons: set `ui.surfaceTabBar.buttons` to replace the default tab bar buttons. Include built-in IDs such as `cmux.newTerminal`, `cmux.newBrowser`, `cmux.splitRight`, and `cmux.splitDown` only when they should stay visible.
|
||||
- Workflows and layouts: use `commands` with workspace definitions to open a worktree, multiple checkouts, local services, browser previews, or SSH sessions in a deliberate split layout.
|
||||
- Dock controls: create `.cmux/dock.json` or `~/.config/cmux/dock.json` for right-sidebar terminal controls such as logs, test watchers, git TUIs, dev servers, queues, or `cmux feed tui --opentui`.
|
||||
- Sidebar and app behavior: use `cmux-settings` for supported settings such as appearance, sidebar display, notification behavior, browser routing, automation, shortcuts, and new-workspace placement.
|
||||
- Workspace metadata: use the cmux CLI or `cmux-workspace` for workspace names, descriptions, colors, read state, and sidebar metadata updates.
|
||||
- Feed and notifications: use `cmux hooks setup` for Feed event sources, notification settings for delivery behavior, and notification hooks in `cmux.json` for filtering or post-processing banners.
|
||||
- Team presets and examples: use project-local `.cmux/cmux.json` and `.cmux/dock.json` to share worktree, SSH, review, dev, CI, and docs workspace patterns with a repo.
|
||||
- Import, export, and reset: back up the current config, apply the smallest diff, validate it, and keep a rollback path for user-owned customizations.
|
||||
- Terminal behavior: use Ghostty config for fonts, themes, cursor style, copy-on-select, shell integration, terminal keybindings, and terminal rendering.
|
||||
| Want to change | Edit |
|
||||
|---|---|
|
||||
| App preferences (appearance, sidebar, notifications, browser routing, automation, shortcuts, new-workspace placement) | `~/.config/cmux/cmux.json` via the `cmux-settings` helper |
|
||||
| Custom actions, workspace layouts/commands, tab bar buttons, plus-button behavior, Command Palette entries, notification hooks | `~/.config/cmux/cmux.json` globally or `.cmux/cmux.json` in the project |
|
||||
| Dock controls (right-sidebar terminals: logs, test watchers, git TUIs, dev servers, queues, `cmux feed tui --opentui`) | `.cmux/dock.json` or `~/.config/cmux/dock.json`; `cmux docs dock` when available |
|
||||
| Terminal rendering and terminal keybindings (fonts, themes, cursor style, copy-on-select, shell integration) | Ghostty config, usually `~/.config/ghostty/config` |
|
||||
| Workspace names, descriptions, colors, read state, sidebar metadata | cmux CLI, see [../cmux-workspace/SKILL.md](../cmux-workspace/SKILL.md) |
|
||||
| Feed event sources | `cmux hooks setup` |
|
||||
|
||||
## Choose the Right Surface
|
||||
Project-local `.cmux/cmux.json` and `.cmux/dock.json` let worktree, SSH, review, dev, CI, and docs patterns travel with the repo; project actions and commands override global entries with the same ID or name. Global app preferences do not belong there.
|
||||
|
||||
- cmux app preferences: use `cmux-settings` for global `~/.config/cmux/cmux.json` settings such as appearance, sidebar, notifications, browser behavior, automation, and shortcuts.
|
||||
- Custom actions, workspace layouts, tab bar buttons, plus-button behavior, and Command Palette entries: edit `~/.config/cmux/cmux.json` globally or `.cmux/cmux.json` in the project. Project-local actions and commands override global entries with the same ID or name.
|
||||
- Dock controls: edit `.cmux/dock.json` in the project or `~/.config/cmux/dock.json` globally. Run `cmux docs dock` when available.
|
||||
- Terminal rendering and terminal keybindings: use Ghostty config, usually `~/.config/ghostty/config`. This includes fonts, cursor style, copy-on-select, shell integration, themes, and terminal keybindings.
|
||||
- Project-specific behavior: prefer `.cmux/cmux.json` in the project so actions, commands, UI action wiring, and notification hooks travel with the repo. Do not put global app preferences there.
|
||||
If a request can be handled by Ghostty config, say so and use Ghostty config instead of inventing cmux UI settings.
|
||||
|
||||
If a request can be handled by Ghostty config, say that and use Ghostty config instead of inventing cmux UI settings.
|
||||
|
||||
## Examples Library
|
||||
|
||||
For reusable patterns such as worktree agents, full-stack dev layouts, SSH
|
||||
devboxes, PR review workspaces, docs workspaces, quick agent tab buttons, and
|
||||
CI watches, read `references/examples.md`. Load it when the user asks for examples, presets,
|
||||
templates, starter configs, or a known workflow shape.
|
||||
Key surfaces in `cmux.json`: `actions` (reusable, can appear in Cmd+Shift+P, surface tab bars, shortcuts, and the plus-button right-click menu), `ui.newWorkspace.action` (replaces the plus-button click) and `ui.newWorkspace.contextMenu` (right-click menu; `ui.newWorkspace.rightClick` is an accepted alias but new examples use `contextMenu`), `ui.surfaceTabBar.buttons` (replaces default tab bar buttons; include built-ins like `cmux.newTerminal`, `cmux.newBrowser`, `cmux.splitRight`, `cmux.splitDown` only when they should stay visible), and `commands` (workspace definitions with split layouts).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect existing config before editing.
|
||||
1. Inspect existing config.
|
||||
|
||||
```bash
|
||||
test -f ~/.config/cmux/cmux.json && sed -n '1,220p' ~/.config/cmux/cmux.json
|
||||
test -f .cmux/cmux.json && sed -n '1,220p' .cmux/cmux.json
|
||||
```
|
||||
|
||||
2. Pick global or project-local scope. Ask only when the choice changes behavior meaningfully. Default to project-local for repo-specific commands and global for app preferences.
|
||||
3. Before editing, back up the target file when it already exists:
|
||||
2. Pick global or project-local scope. Default to project-local for repo-specific commands, global for app preferences. Ask only when the choice changes behavior meaningfully.
|
||||
3. Back up the target file when it already exists (applicable path only, no backup for a missing file).
|
||||
|
||||
```bash
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
@@ -56,8 +42,7 @@ templates, starter configs, or a known workflow shape.
|
||||
test -f .cmux/cmux.json && cp -p .cmux/cmux.json .cmux/cmux.json."$stamp".bak
|
||||
```
|
||||
|
||||
Use the applicable path only. Do not create a backup for a missing file.
|
||||
4. For app settings and cmux-owned shortcuts, use the settings helper from the installed skill or checkout:
|
||||
4. For app settings and cmux-owned shortcuts, use the settings helper (`~/.codex/skills/...` if the user installed with `skills.sh`).
|
||||
|
||||
```bash
|
||||
~/.agents/skills/cmux-settings/scripts/cmux-settings list-supported
|
||||
@@ -65,19 +50,13 @@ templates, starter configs, or a known workflow shape.
|
||||
~/.agents/skills/cmux-settings/scripts/cmux-settings validate
|
||||
```
|
||||
|
||||
If the user installed with `skills.sh`, use `~/.codex/skills/cmux-settings/scripts/cmux-settings` instead.
|
||||
5. For actions, UI wiring, workspace layouts, notification hooks, and Dock controls, edit JSONC or JSON carefully. Preserve unrelated sections such as `vault`, `rightSidebar`, `commands`, `actions`, `ui`, and `notifications`.
|
||||
6. Reload config after successful edits:
|
||||
5. For actions, UI wiring, workspace layouts, notification hooks, and Dock controls, edit the JSONC by hand and preserve unrelated sections (`vault`, `rightSidebar`, `commands`, `actions`, `ui`, `notifications`).
|
||||
6. `cmux reload-config`.
|
||||
7. Verify the configured entrypoint exists: read back the shortcut binding, or confirm the action ID and where it should appear.
|
||||
|
||||
```bash
|
||||
cmux reload-config
|
||||
```
|
||||
## Example: Command Palette action
|
||||
|
||||
7. Verify the configured entrypoint exists. For shortcuts, read back the binding. For custom actions, confirm the action ID and where it should appear.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
Add a Command Palette action that opens Codex in a new tab. It will appear in Cmd+Shift+P unless `palette` is false:
|
||||
Appears in Cmd+Shift+P unless `palette` is false.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -94,141 +73,20 @@ Add a Command Palette action that opens Codex in a new tab. It will appear in Cm
|
||||
}
|
||||
```
|
||||
|
||||
Replace the plus-button click and define the plus-button right-click menu.
|
||||
This is the pattern for "bring your own worktree, multiple checkouts, or SSH
|
||||
setup". The `workspaceCommand` action ID is `worktree-agents`, and its
|
||||
`commandName` must match a command named `Worktree Agents` in the same config:
|
||||
|
||||
```json
|
||||
{
|
||||
"actions": {
|
||||
"worktree-agents": {
|
||||
"type": "workspaceCommand",
|
||||
"title": "Worktree Agents",
|
||||
"commandName": "Worktree Agents",
|
||||
"icon": { "type": "symbol", "name": "folder.badge.plus" }
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"newWorkspace": {
|
||||
"action": "worktree-agents",
|
||||
"contextMenu": [
|
||||
{ "action": "worktree-agents", "title": "Worktree Agents" },
|
||||
{ "type": "separator" },
|
||||
{ "action": "cmux.newTerminal", "title": "New Terminal" },
|
||||
{ "action": "cmux.newBrowser", "title": "New Browser" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"name": "Worktree Agents",
|
||||
"description": "Create a worktree and open agents inside it",
|
||||
"workspace": {
|
||||
"name": "Worktree Agents",
|
||||
"cwd": "../worktrees/my-feature",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "Codex", "command": "codex" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "SSH", "command": "ssh devbox" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Add a project workspace layout:
|
||||
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "dev",
|
||||
"workspace": {
|
||||
"name": "Dev",
|
||||
"cwd": ".",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"children": [
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "command": "bun dev" }] } },
|
||||
{ "pane": { "surfaces": [{ "type": "browser", "url": "http://localhost:3000" }] } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Replace surface tab bar buttons:
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": {
|
||||
"surfaceTabBar": {
|
||||
"buttons": [
|
||||
"cmux.newTerminal",
|
||||
"cmux.newBrowser",
|
||||
{
|
||||
"action": "codex-new-tab",
|
||||
"title": "Codex",
|
||||
"icon": { "type": "symbol", "name": "terminal" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add project Dock controls:
|
||||
|
||||
```json
|
||||
{
|
||||
"controls": [
|
||||
{
|
||||
"id": "git",
|
||||
"title": "Git",
|
||||
"command": "lazygit",
|
||||
"cwd": ".",
|
||||
"height": 300
|
||||
},
|
||||
{
|
||||
"id": "feed",
|
||||
"title": "Feed",
|
||||
"command": "cmux feed tui --opentui",
|
||||
"height": 260
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
For worktree agents, full-stack dev layouts, SSH devboxes, PR review workspaces, docs workspaces, tab bar buttons, and CI watch Dock controls, read [references/examples.md](references/examples.md). Load it when the user asks for examples, presets, templates, starter configs, or a known workflow shape.
|
||||
|
||||
## Validation
|
||||
|
||||
- App settings: run `cmux-settings validate`.
|
||||
- JSONC shape: keep valid JSONC and avoid duplicate keys.
|
||||
- Dock JSON: parse `.cmux/dock.json` or `~/.config/cmux/dock.json` with a JSON parser before reporting completion.
|
||||
- Runtime reload: run `cmux reload-config` when the CLI is available.
|
||||
- User-facing action: confirm the action title, shortcut, plus-button behavior, context-menu entry, or tab bar placement the user asked for.
|
||||
- App settings: `cmux-settings validate`.
|
||||
- Keep valid JSONC, no duplicate keys.
|
||||
- Parse `.cmux/dock.json` or `~/.config/cmux/dock.json` with a JSON parser before reporting completion.
|
||||
- `cmux reload-config` when the CLI is available.
|
||||
- Confirm the exact user-facing result: action title, shortcut, plus-button behavior, context-menu entry, or tab bar placement.
|
||||
|
||||
## Rules
|
||||
|
||||
- Do not overwrite whole top-level config sections unless you own the full section.
|
||||
- Do not store secrets directly in actions, commands, or prompts. Use environment variables or the user's secret manager.
|
||||
- Do not use app/runtime sleeps or timing workarounds in generated commands.
|
||||
- Do not overwrite a whole top-level config section unless you own the full section.
|
||||
- Do not store secrets in actions, commands, or prompts. Use environment variables or the user's secret manager.
|
||||
- Do not use sleeps or timing workarounds in generated commands.
|
||||
- Do not add a cmux setting for behavior Ghostty already owns.
|
||||
- Keep labels short enough for menus, buttons, and the Command Palette.
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
# cmux Customization Examples
|
||||
|
||||
Use these examples as starting points. Merge only the relevant top-level keys
|
||||
into the target config file named above each code block. Unlabeled JSON examples
|
||||
target `cmux.json`. Preserve unrelated sections, then run `cmux reload-config`
|
||||
when available.
|
||||
|
||||
Prefer project-local `.cmux/cmux.json` for team workflows and global
|
||||
`~/.config/cmux/cmux.json` for personal app preferences.
|
||||
Merge only the relevant top-level keys into the file named above each block. Unlabeled JSON targets `cmux.json`. Preserve unrelated sections, then run `cmux reload-config`. Prefer project-local `.cmux/cmux.json` for team workflows and global `~/.config/cmux/cmux.json` for personal preferences.
|
||||
|
||||
## Worktree Agents
|
||||
|
||||
Use this when the user wants the plus button to open a worktree or checkout
|
||||
layout, and right-click to offer alternate starters.
|
||||
Plus button opens a worktree layout; right-click offers alternate starters. `workspaceCommand.commandName` must match a `commands[].name`.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -34,87 +27,41 @@ layout, and right-click to offer alternate starters.
|
||||
]
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"name": "Worktree Agents",
|
||||
"workspace": {
|
||||
"name": "Worktree Agents",
|
||||
"cwd": "../worktrees/my-feature",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "Codex", "command": "codex" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "Claude", "command": "claude" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"commands": [{
|
||||
"name": "Worktree Agents",
|
||||
"description": "Create a worktree and open agents inside it",
|
||||
"workspace": {
|
||||
"name": "Worktree Agents", "cwd": "../worktrees/my-feature",
|
||||
"layout": { "direction": "horizontal", "children": [
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "Codex", "command": "codex" }] } },
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "Claude", "command": "claude" }] } }
|
||||
]}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Full-Stack Dev
|
||||
|
||||
Use this when a repo needs terminals, browser preview, and persistent Dock
|
||||
controls for repeated local development.
|
||||
Terminals plus browser preview plus persistent Dock controls.
|
||||
|
||||
`.cmux/cmux.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "Full-Stack Dev",
|
||||
"workspace": {
|
||||
"name": "Dev",
|
||||
"cwd": ".",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"split": 0.55,
|
||||
"children": [
|
||||
{
|
||||
"direction": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "Web", "command": "bun dev" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "Tests", "command": "bun test --watch" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "browser", "name": "Preview", "url": "http://localhost:3000" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"commands": [{
|
||||
"name": "Full-Stack Dev",
|
||||
"workspace": {
|
||||
"name": "Dev", "cwd": ".",
|
||||
"layout": { "direction": "horizontal", "split": 0.55, "children": [
|
||||
{ "direction": "vertical", "children": [
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "Web", "command": "bun dev" }] } },
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "Tests", "command": "bun test --watch" }] } }
|
||||
]},
|
||||
{ "pane": { "surfaces": [{ "type": "browser", "name": "Preview", "url": "http://localhost:3000" }] } }
|
||||
]}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -131,162 +78,76 @@ controls for repeated local development.
|
||||
|
||||
## SSH Devbox
|
||||
|
||||
Use this when the user's normal environment is remote, or when local cmux should
|
||||
open a known SSH session beside project notes or a browser preview.
|
||||
Remote shell beside a local preview.
|
||||
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "SSH Devbox",
|
||||
"keywords": ["ssh", "remote", "devbox"],
|
||||
"workspace": {
|
||||
"name": "Devbox",
|
||||
"cwd": ".",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "SSH", "command": "ssh devbox" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "browser", "name": "Preview", "url": "http://localhost:3000" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"commands": [{
|
||||
"name": "SSH Devbox",
|
||||
"keywords": ["ssh", "remote", "devbox"],
|
||||
"workspace": {
|
||||
"name": "Devbox", "cwd": ".",
|
||||
"layout": { "direction": "horizontal", "children": [
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "SSH", "command": "ssh devbox" }] } },
|
||||
{ "pane": { "surfaces": [{ "type": "browser", "name": "Preview", "url": "http://localhost:3000" }] } }
|
||||
]}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Review PR
|
||||
|
||||
Use this when a project needs one command to review a pull request with a
|
||||
terminal, browser, and notes panel. Adjust the URL and command for the user's
|
||||
GitHub workflow.
|
||||
One command for terminal plus browser PR review. Adjust the URL and command for the user's GitHub workflow.
|
||||
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "Review PR",
|
||||
"keywords": ["review", "pull request", "pr"],
|
||||
"workspace": {
|
||||
"name": "PR Review",
|
||||
"cwd": ".",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "GitHub", "command": "gh pr status" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "browser", "name": "Pull Request", "url": "https://github.com/owner/repo/pulls" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"commands": [{
|
||||
"name": "Review PR",
|
||||
"keywords": ["review", "pull request", "pr"],
|
||||
"workspace": {
|
||||
"name": "PR Review", "cwd": ".",
|
||||
"layout": { "direction": "horizontal", "children": [
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "GitHub", "command": "gh pr status" }] } },
|
||||
{ "pane": { "surfaces": [{ "type": "browser", "name": "Pull Request", "url": "https://github.com/owner/repo/pulls" }] } }
|
||||
]}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Docs Workspace
|
||||
|
||||
Use this when a repo needs one command for docs authoring with a dev server,
|
||||
browser preview, and markdown viewer. Adjust the command, URL, and markdown path
|
||||
for the docs stack.
|
||||
Docs server, markdown viewer, and browser preview. Adjust command, URL, and markdown path for the docs stack.
|
||||
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "Docs Workspace",
|
||||
"keywords": ["docs", "documentation", "preview"],
|
||||
"workspace": {
|
||||
"name": "Docs",
|
||||
"cwd": ".",
|
||||
"layout": {
|
||||
"direction": "horizontal",
|
||||
"split": 0.45,
|
||||
"children": [
|
||||
{
|
||||
"direction": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "terminal", "name": "Docs Server", "command": "bun run docs:dev" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{
|
||||
"type": "terminal",
|
||||
"name": "Markdown",
|
||||
"command": "cmux markdown open docs/README.md --direction right --focus false; exec ${SHELL:-/bin/zsh} -l"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pane": {
|
||||
"surfaces": [
|
||||
{ "type": "browser", "name": "Docs Preview", "url": "http://localhost:3000/docs" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"commands": [{
|
||||
"name": "Docs Workspace",
|
||||
"keywords": ["docs", "documentation", "preview"],
|
||||
"workspace": {
|
||||
"name": "Docs", "cwd": ".",
|
||||
"layout": { "direction": "horizontal", "split": 0.45, "children": [
|
||||
{ "direction": "vertical", "children": [
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "Docs Server", "command": "bun run docs:dev" }] } },
|
||||
{ "pane": { "surfaces": [{ "type": "terminal", "name": "Markdown", "command": "cmux markdown open docs/README.md --direction right --focus false; exec ${SHELL:-/bin/zsh} -l" }] } }
|
||||
]},
|
||||
{ "pane": { "surfaces": [{ "type": "browser", "name": "Docs Preview", "url": "http://localhost:3000/docs" }] } }
|
||||
]}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Agent Buttons
|
||||
|
||||
Use this when the user wants tab bar buttons for common agents while keeping
|
||||
the default new terminal and browser buttons.
|
||||
Agent buttons alongside the default terminal and browser buttons.
|
||||
|
||||
```json
|
||||
{
|
||||
"actions": {
|
||||
"codex-new-tab": {
|
||||
"type": "agent",
|
||||
"agent": "codex",
|
||||
"title": "Codex",
|
||||
"target": "newTabInCurrentPane",
|
||||
"palette": true
|
||||
},
|
||||
"claude-new-tab": {
|
||||
"type": "agent",
|
||||
"agent": "claude",
|
||||
"title": "Claude",
|
||||
"target": "newTabInCurrentPane",
|
||||
"palette": true
|
||||
}
|
||||
"codex-new-tab": { "type": "agent", "agent": "codex", "title": "Codex", "target": "newTabInCurrentPane", "palette": true },
|
||||
"claude-new-tab": { "type": "agent", "agent": "claude", "title": "Claude", "target": "newTabInCurrentPane", "palette": true }
|
||||
},
|
||||
"ui": {
|
||||
"surfaceTabBar": {
|
||||
@@ -303,36 +164,24 @@ the default new terminal and browser buttons.
|
||||
|
||||
## CI Watch
|
||||
|
||||
Use this when the user wants a repeatable place for GitHub Actions, CircleCI,
|
||||
or release-monitoring commands. Prefer Dock controls for long-running monitors.
|
||||
Long-running monitors belong in Dock controls, not workspace panes.
|
||||
|
||||
`.cmux/dock.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"controls": [
|
||||
{
|
||||
"id": "gh-runs",
|
||||
"title": "GitHub Runs",
|
||||
"command": "gh run list --limit 10 && exec ${SHELL:-/bin/zsh} -l",
|
||||
"cwd": ".",
|
||||
"height": 260
|
||||
},
|
||||
{
|
||||
"id": "feed",
|
||||
"title": "Feed",
|
||||
"command": "cmux feed tui --opentui",
|
||||
"height": 260
|
||||
}
|
||||
{ "id": "gh-runs", "title": "GitHub Runs", "command": "gh run list --limit 10 && exec ${SHELL:-/bin/zsh} -l", "cwd": ".", "height": 260 },
|
||||
{ "id": "feed", "title": "Feed", "command": "cmux feed tui --opentui", "height": 260 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
## Validation checklist
|
||||
|
||||
- Parse any changed JSON or JSONC before reporting success.
|
||||
- Keep `cwd` inside the `workspace` object for workspace commands.
|
||||
- Confirm `workspaceCommand.commandName` matches a `commands[].name`.
|
||||
- Use `ui.newWorkspace.contextMenu` in new examples, not the alias.
|
||||
- Use `ui.newWorkspace.contextMenu`, not the `rightClick` alias.
|
||||
- Keep built-in tab bar buttons only when the user wants them visible.
|
||||
- Keep secrets out of config. Use shell profiles, env vars, or a secret store.
|
||||
|
||||
@@ -7,48 +7,38 @@ description: "Debug logging, Debug menu, runtime pitfalls, typing-latency-sensit
|
||||
|
||||
## Debug event log
|
||||
|
||||
When adding debug event instrumentation, put events (keys, mouse, focus, splits, tabs) in the unified DEBUG build log. This is not a blanket requirement to add logs to every new code path. Most temporary probes should be added only during the dogfood debug loop and removed before merge.
|
||||
Put debug event instrumentation (keys, mouse, focus, splits, tabs) in the unified DEBUG build log. This is not a requirement to log every new code path; most probes belong to a dogfood debug loop and are removed before merge.
|
||||
|
||||
```bash
|
||||
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`
|
||||
- `reload.sh` writes the selected dev CLI path to `/tmp/cmux-last-cli-path`
|
||||
- `reload.sh` updates `/tmp/cmux-cli` and `$HOME/.local/bin/cmux-dev` to that CLI
|
||||
- Implementation: `Packages/macOS/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift`
|
||||
- App shim: `Sources/App/DebugLogging.swift`
|
||||
- Free function `cmuxDebugLog("message")` logs with timestamp and appends to file in real time from cmux code
|
||||
- The package implementation and app shim are `#if DEBUG`; all call sites must be wrapped in `#if DEBUG` / `#endif`
|
||||
- 500-entry ring buffer; `CMUXDebugLog.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`
|
||||
- Untagged Debug app logs to `/tmp/cmux-debug.log`; tagged (`./scripts/reload.sh --tag <tag>`) to `/tmp/cmux-debug-<tag>.log`.
|
||||
- `reload.sh` writes the current log path to `/tmp/cmux-last-debug-log-path` and the selected dev CLI path to `/tmp/cmux-last-cli-path`, and points `/tmp/cmux-cli` and `$HOME/.local/bin/cmux-dev` at that CLI.
|
||||
- Implementation: `Packages/macOS/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift`. App shim: `Sources/App/DebugLogging.swift`. Both are `#if DEBUG`, so every call site must be wrapped in `#if DEBUG` / `#endif`.
|
||||
- `cmuxDebugLog("message")` timestamps and appends in real time. A 500-entry ring buffer backs it; `CMUXDebugLog.DebugEventLog.shared.dump()` writes the full buffer to file.
|
||||
- Key events are logged in `AppDelegate.swift` (monitor, `performKeyEquivalent`); mouse/UI events inline in views (`ContentView`, `BrowserPanelView`).
|
||||
- Stable event prefixes: `focus.panel`, `focus.bonsplit`, `focus.firstResponder`, `focus.moveFocus`, `tab.select`, `tab.close`, `tab.dragStart`, `tab.drop`, `pane.focus`, `pane.drop`, `divider.dragStart`.
|
||||
|
||||
## Debug menu
|
||||
|
||||
The app has a **Debug** menu in the macOS menu bar only in DEBUG builds. Use it for visual iteration.
|
||||
DEBUG builds get a **Debug** menu in the macOS menu bar. When the user says "debug menu" or "debug window" they mean this, not `defaults write`.
|
||||
|
||||
- **Debug > Debug Windows** contains panels for tuning layout, colors, and behavior. Entries are alphabetical with no dividers.
|
||||
- To add a debug toggle or visual option: create an `NSWindowController` subclass with a `shared` singleton, add it to the "Debug Windows" menu in `Sources/cmuxApp.swift`, and add a SwiftUI view with `@AppStorage` bindings for live changes.
|
||||
- When the user says "debug menu" or "debug window", they mean this menu, not `defaults write`.
|
||||
**Debug > Debug Windows** holds panels for tuning layout, colors, and behavior, listed alphabetically with no dividers. To add one: create an `NSWindowController` subclass with a `shared` singleton, register it in the "Debug Windows" menu in `Sources/cmuxApp.swift`, and back it with a SwiftUI view using `@AppStorage` bindings for live changes.
|
||||
|
||||
## Runtime pitfalls
|
||||
|
||||
- Custom UTTypes for drag-and-drop must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations`.
|
||||
- Custom drag-and-drop UTTypes must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations`.
|
||||
- Do not add an app-level display link or manual `ghostty_surface_draw` loop; rely on Ghostty wakeups/renderer to avoid typing lag.
|
||||
- `WindowTerminalHostView.hitTest()` is typing-latency-sensitive. All divider/sidebar/drag routing is gated to pointer events only. Do not add work outside the `isPointerEvent` guard.
|
||||
- `TabItemView` uses `Equatable` conformance plus `.equatable()` to skip body re-evaluation during typing. Do not add environment/store/binding reads without updating equality and the call site.
|
||||
- `TerminalSurface.forceRefresh()` is called on every keystroke. Do not add allocations, file I/O, or formatting there.
|
||||
- `WindowTerminalHostView.hitTest()` in `Sources/TerminalWindowPortal.swift` runs on every event including keyboard. Add no work outside the `isPointerEvent` guard.
|
||||
- `TabItemView` in `Sources/ContentView.swift` uses `Equatable` plus `.equatable()` to skip body re-evaluation during typing. Do not add environment/store/binding reads without updating `==` and keeping `.equatable()` at the call site.
|
||||
- `TerminalSurface.forceRefresh()` in `Sources/GhosttyTerminalView.swift` runs on every keystroke. No allocations, file I/O, or formatting.
|
||||
- `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift`, not from SwiftUI panel containers.
|
||||
- List subtrees with `LazyVStack`, `LazyHStack`, `List`, or `ForEach` must pass immutable row snapshots plus closures below the boundary. Do not pass observable stores into row views.
|
||||
- Views below a `LazyVStack` / `LazyHStack` / `List` / `ForEach` boundary receive immutable snapshots plus closures, never an observable store.
|
||||
- Functions called from SwiftUI `body` must not mutate state or schedule store writes.
|
||||
- Foundation, SwiftUI, AttributeGraph, and WebKit semantics can change between macOS major versions. Test on the reporter's macOS before declaring a user repro disproven.
|
||||
- Foundation, SwiftUI, AttributeGraph, and WebKit semantics change between macOS majors. Test on the reporter's macOS before declaring a user repro disproven.
|
||||
|
||||
## Detailed references
|
||||
|
||||
- Read [references/debug-event-log.md](references/debug-event-log.md) when adding or interpreting debug log probes.
|
||||
- Read [references/runtime-pitfalls.md](references/runtime-pitfalls.md) before touching terminal rendering, hit testing, tab rows, list virtualization, search overlay layering, or OS-version-sensitive code.
|
||||
- [references/debug-event-log.md](references/debug-event-log.md): when to add probes and how to name them.
|
||||
- [references/runtime-pitfalls.md](references/runtime-pitfalls.md): read before touching terminal rendering, hit testing, tab rows, list virtualization, search overlay layering, or OS-version-sensitive code.
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
# Debug Event Log
|
||||
|
||||
The debug event log is the preferred shared destination for temporary and durable DEBUG-only probes.
|
||||
Paths, prefixes, and the tail command are in [../SKILL.md](../SKILL.md). This covers when to add a probe.
|
||||
|
||||
## Destination
|
||||
|
||||
Tagged builds write tag-specific logs:
|
||||
|
||||
- untagged Debug app: `/tmp/cmux-debug.log`
|
||||
- tagged Debug app: `/tmp/cmux-debug-<tag>.log`
|
||||
|
||||
`reload.sh` writes the current path to `/tmp/cmux-last-debug-log-path`, so the most robust tail command is:
|
||||
|
||||
```bash
|
||||
tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug.log)"
|
||||
```
|
||||
|
||||
Use this instead of guessing whether the current run is tagged.
|
||||
|
||||
## Shape
|
||||
|
||||
The package implementation lives in `Packages/macOS/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift`, and the app shim lives in `Sources/App/DebugLogging.swift`.
|
||||
|
||||
Call sites use:
|
||||
## Call site shape
|
||||
|
||||
```swift
|
||||
#if DEBUG
|
||||
@@ -29,38 +10,18 @@ cmuxDebugLog("focus.panel ...")
|
||||
#endif
|
||||
```
|
||||
|
||||
Every call site must be guarded by `#if DEBUG` / `#endif`. The implementation and shim are DEBUG-only, so unguarded call sites break non-Debug builds.
|
||||
The package implementation and app shim are DEBUG-only, so an unguarded call site breaks non-Debug builds.
|
||||
|
||||
## When to add probes
|
||||
|
||||
Add probes during a dogfood debug loop when they help answer a concrete question:
|
||||
Add a probe during a dogfood debug loop when it answers a concrete question: which event path fired, which panel or pane had focus, which split/tab/drop transition occurred, whether a stale view or responder received an event, whether a path fires on every keypress.
|
||||
|
||||
- Which event path fired?
|
||||
- Which panel or pane had focus?
|
||||
- Which split/tab/drop transition occurred?
|
||||
- Did a stale view or responder receive an event?
|
||||
- Did a path fire on every keypress?
|
||||
|
||||
Do not add broad instrumentation just because a file is nearby. Remove temporary probes before merge unless they are low-volume and clearly useful for future debugging.
|
||||
Do not instrument a file just because it is nearby. Remove temporary probes before merge unless they are low-volume and clearly useful later.
|
||||
|
||||
## Naming
|
||||
|
||||
Prefer stable event prefixes:
|
||||
Use a stable event prefix (see the list in the skill) and put dynamic details after it, so `rg`, `tail`, and log filtering stay practical.
|
||||
|
||||
- `focus.panel`
|
||||
- `focus.bonsplit`
|
||||
- `focus.firstResponder`
|
||||
- `focus.moveFocus`
|
||||
- `tab.select`
|
||||
- `tab.close`
|
||||
- `tab.dragStart`
|
||||
- `tab.drop`
|
||||
- `pane.focus`
|
||||
- `pane.drop`
|
||||
- `divider.dragStart`
|
||||
## Recovering missed events
|
||||
|
||||
Put dynamic details after the prefix. This makes `rg`, `tail`, and log filtering practical.
|
||||
|
||||
## Ring buffer
|
||||
|
||||
The debug logger has a 500-entry ring buffer. `CMUXDebugLog.DebugEventLog.shared.dump()` writes the full buffer to file. Use this when the interesting event occurred before you started tailing.
|
||||
The 500-entry ring buffer holds history from before you started tailing. `CMUXDebugLog.DebugEventLog.shared.dump()` flushes it to file.
|
||||
|
||||
@@ -1,84 +1,37 @@
|
||||
# Runtime Pitfalls
|
||||
|
||||
This reference expands the high-risk cmux runtime rules.
|
||||
Why the rules in [../SKILL.md](../SKILL.md) exist, and what to check when one bites.
|
||||
|
||||
## Drag-and-drop UTTypes
|
||||
|
||||
Custom UTTypes must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations`. Examples include:
|
||||
|
||||
- `com.splittabbar.tabtransfer`
|
||||
- `com.cmux.sidebar-tab-reorder`
|
||||
|
||||
If drag/drop works only inside a narrow local test but fails across process or extension boundaries, check Info.plist before rewriting the drag model.
|
||||
Custom UTTypes are declared in `Resources/Info.plist` under `UTExportedTypeDeclarations`, for example `com.splittabbar.tabtransfer` and `com.cmux.sidebar-tab-reorder`. If drag/drop works in a narrow local test but fails across a process or extension boundary, check Info.plist before rewriting the drag model.
|
||||
|
||||
## Terminal rendering and typing latency
|
||||
|
||||
Do not add an app-level display link or manual `ghostty_surface_draw` loop. cmux relies on Ghostty wakeups and renderer scheduling. A second draw loop can make typing lag worse and hide the real invalidation source.
|
||||
A second draw loop (an app-level display link or manual `ghostty_surface_draw`) can make typing lag worse and hide the real invalidation source. cmux relies on Ghostty wakeups and renderer scheduling.
|
||||
|
||||
`TerminalSurface.forceRefresh()` in `Sources/GhosttyTerminalView.swift` is called on every keystroke. Do not add:
|
||||
|
||||
- allocation-heavy formatting
|
||||
- file I/O
|
||||
- logging to disk
|
||||
- string interpolation in hot loops
|
||||
- layout work
|
||||
|
||||
If you need to observe this path, use the smallest possible DEBUG-only probe and remove it before merge unless it is intentionally durable.
|
||||
|
||||
## Hit testing
|
||||
|
||||
`WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift` is called on every event, including keyboard events. Divider/sidebar/drag routing is intentionally gated to pointer events.
|
||||
|
||||
Do not add work outside the `isPointerEvent` guard. Even "small" checks compound on typing paths.
|
||||
`TerminalSurface.forceRefresh()` runs on every keystroke: no allocation-heavy formatting, file I/O, disk logging, hot-loop string interpolation, or layout work. `WindowTerminalHostView.hitTest()` runs on every event including keyboard, so divider/sidebar/drag routing stays inside the `isPointerEvent` guard. Even "small" checks compound on typing paths.
|
||||
|
||||
## Tab rows
|
||||
|
||||
`TabItemView` in `ContentView.swift` uses `Equatable` conformance plus `.equatable()` to skip body re-evaluation during typing.
|
||||
|
||||
Before adding any of these to the view:
|
||||
|
||||
- `@EnvironmentObject`
|
||||
- `@ObservedObject`
|
||||
- `@Binding`
|
||||
- a plain store read in `body`
|
||||
- a new parameter derived from mutable global state
|
||||
|
||||
Update the `==` function and verify the `ForEach` call site still uses `.equatable()`. Prefer passing precomputed immutable values.
|
||||
Before adding `@EnvironmentObject`, `@ObservedObject`, `@Binding`, a store read in `body`, or a parameter derived from mutable global state to `TabItemView`, update the `==` function and confirm the `ForEach` call site still uses `.equatable()`. Prefer passing precomputed immutable values.
|
||||
|
||||
## Terminal find layering
|
||||
|
||||
`SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift`, the AppKit portal layer. Do not mount it from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`.
|
||||
|
||||
Portal-hosted terminal views can sit above SwiftUI during split/workspace churn. Mounting the search UI at the wrong layer creates intermittently hidden or detached search controls.
|
||||
Portal-hosted terminal views can sit above SwiftUI during split/workspace churn, so mounting `SurfaceSearchOverlay` from a SwiftUI panel container such as `Sources/Panels/TerminalPanelView.swift` produces intermittently hidden or detached search controls. It belongs in `GhosttySurfaceScrollView` (the AppKit portal layer) in `Sources/GhosttyTerminalView.swift`.
|
||||
|
||||
## Snapshot boundary for list subtrees
|
||||
|
||||
In any SwiftUI panel whose `body` contains a `LazyVStack`, `LazyHStack`, `List`, or `ForEach` of rows, no view below that boundary may hold a reference to an `ObservableObject` or `@Observable` store. That includes:
|
||||
Below a `LazyVStack` / `LazyHStack` / `List` / `ForEach` boundary, no view may hold an `ObservableObject` or `@Observable` store reference: not `@ObservedObject`, `@EnvironmentObject`, `@StateObject`, `@Bindable`, nor a plain `let store: SomeStore`. Rows and drop gaps take immutable value snapshots plus closure action bundles.
|
||||
|
||||
- `@ObservedObject`
|
||||
- `@EnvironmentObject`
|
||||
- `@StateObject`
|
||||
- `@Bindable`
|
||||
- a plain `let store: SomeStore`
|
||||
|
||||
Rows and drop gaps receive immutable value snapshots plus closure action bundles only.
|
||||
|
||||
This avoids the class of bugs where an orthogonal published change invalidates every row and thrashes `LazyLayoutViewCache`, causing a main-thread spin loop. Reference patterns include `IndexSectionActions`, `SectionGapActions`, and `SessionSearchFn` in `Sources/SessionIndexView.swift`.
|
||||
Violating this reintroduces the class of bug where an orthogonal published change invalidates every row and thrashes `LazyLayoutViewCache` into a 100% CPU main-thread spin loop, which hit the Sessions panel and the workspace sidebar (https://github.com/manaflow-ai/cmux/issues/2586). Reference patterns: `IndexSectionActions`, `SectionGapActions`, `SessionSearchFn` in `Sources/SessionIndexView.swift`.
|
||||
|
||||
## No body-time mutation
|
||||
|
||||
A function called from SwiftUI `body`, directly or through a helper, must not:
|
||||
|
||||
- write observable state
|
||||
- schedule `Task { @MainActor in store.x = ... }`
|
||||
- call `DispatchQueue.main.async` to write store state
|
||||
|
||||
State-changing work triggered by "new data appeared" belongs in a reload completion, a `didSet`, or a property observer. It does not belong in the projection that feeds `ForEach`.
|
||||
A function called from `body`, directly or through a helper, must not write observable state, schedule `Task { @MainActor in store.x = ... }`, or `DispatchQueue.main.async` a store write. That is a re-render feedback loop, the same root-cause family as the snapshot-boundary rule. State-changing work triggered by "new data appeared" belongs in a `reload()` completion, a `didSet`, or a property observer, never in the projection feeding `ForEach`.
|
||||
|
||||
## OS-version repros
|
||||
|
||||
Foundation, SwiftUI, AttributeGraph, and WebKit behavior can change silently between macOS versions. A function that seems deterministic on macOS 26 may behave differently on macOS 14 or 15.
|
||||
Foundation, SwiftUI, AttributeGraph, and WebKit behavior changes silently between macOS majors. From https://github.com/manaflow-ai/cmux/issues/4529: `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returns `"/.."` on macOS 14 and 15 but `"/"` on macOS 26, because Apple fixed CFURL normalization. The repo's `macos-26` CI and every maintainer's machine were on the fixed side; every reporter was on the broken side.
|
||||
|
||||
Concrete example: `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returned `"/.."` on macOS 14 and 15 but `"/"` on macOS 26.
|
||||
|
||||
When a user reports a repro on an older macOS, test on that macOS before declaring the repro disproven. AWS M4 Pro builders such as `cmux-aws-mac`, `cmux-aws-m4pro`, and `aws-m4pro-1..6` are pre-provisioned on macOS 15.7.4 and are the preferred empirical repro path.
|
||||
Test on the reporter's macOS before declaring a repro disproven. AWS M4 Pro builders (`cmux-aws-mac`, `cmux-aws-m4pro`, `aws-m4pro-1..6`) are pre-provisioned on macOS 15.7.4 and are the preferred empirical repro path.
|
||||
|
||||
@@ -5,68 +5,46 @@ description: "Contributor workflow rules for cmux setup, Xcode project normaliza
|
||||
|
||||
# cmux Dev Workflow
|
||||
|
||||
## Initial setup
|
||||
|
||||
`./scripts/setup.sh` initializes submodules, builds GhosttyKit, and installs the pbxproj normalization pre-commit hook.
|
||||
|
||||
## Tagged local dev
|
||||
|
||||
After making code changes, always run the reload script with a tag to build the Debug app:
|
||||
Build the Debug app after every code change:
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <short-tag>
|
||||
```
|
||||
|
||||
By default, `reload.sh` builds but does not launch the app. Pass `--launch` only when you need to open it automatically.
|
||||
It builds without launching; pass `--launch` only when you need the app open. Never run bare `xcodebuild` or open an untagged `cmux DEV.app`: untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
|
||||
|
||||
Never run bare `xcodebuild` or open an untagged `cmux DEV.app`. Untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
|
||||
|
||||
For CLI or socket dogfood against a tagged Debug app, use the tag-bound helper and set `CMUX_TAG`:
|
||||
For CLI or socket dogfood against a tagged Debug app:
|
||||
|
||||
```bash
|
||||
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh list-workspaces
|
||||
```
|
||||
|
||||
Do not use `/tmp/cmux-cli` for tagged dogfood. That symlink points at the most recently reloaded build.
|
||||
|
||||
When rebuilding cmuxd for release/bundling, always use ReleaseFast:
|
||||
|
||||
```bash
|
||||
cd cmuxd && zig build -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
## Initial setup
|
||||
|
||||
Run the setup script to initialize submodules, build GhosttyKit, and install the pbxproj normalization pre-commit hook:
|
||||
|
||||
```bash
|
||||
./scripts/setup.sh
|
||||
```
|
||||
Do not use `/tmp/cmux-cli` for tagged dogfood; that symlink points at the most recently reloaded build. See [references/tagged-builds.md](references/tagged-builds.md).
|
||||
|
||||
## Xcode toolchain
|
||||
|
||||
The team is pinned to Xcode 26.x. `.xcode-version` records the major; `cmux.xcodeproj/project.pbxproj` carries `objectVersion = 60`, which is what Xcode 26 writes by default. (objectVersion 77 is reserved for projects that adopt synchronized folder groups, which cmux does not use yet. Bumping to a different value requires a deliberate team decision.)
|
||||
The team is pinned to Xcode 26.x. `.xcode-version` is the single source of truth for the major; `cmux.xcodeproj/project.pbxproj` carries `objectVersion = 60`, what Xcode 26 writes by default. (`objectVersion = 77` is reserved for synchronized folder groups, which cmux does not use.)
|
||||
|
||||
`scripts/setup.sh` installs a tracked pre-commit hook (`scripts/git-hooks/pre-commit`) that runs `scripts/normalize-pbxproj.py` on any staged `cmux.xcodeproj/project.pbxproj`, sorting the high-churn sections so Xcode's nondeterministic reordering never reaches a commit. The hook is idempotent. CI runs `scripts/check-pbxproj.sh` to enforce both the `objectVersion` pin and normalization, so anyone who skips the hook (or never ran setup) gets a clear failure on their PR.
|
||||
|
||||
`.xcode-version` is the single source of truth. To bump the pin: edit `.xcode-version`, open `cmux.xcodeproj` in the new Xcode (which rewrites `objectVersion` automatically when it touches the file), and add a case for the new Xcode major in `scripts/check-pbxproj.sh` mapping it to the `objectVersion` that major writes.
|
||||
`scripts/setup.sh` installs the tracked `scripts/git-hooks/pre-commit`, which runs `scripts/normalize-pbxproj.py` on any staged `project.pbxproj` so Xcode's nondeterministic reordering never reaches a commit. The hook is idempotent. CI runs `scripts/check-pbxproj.sh` to enforce both the `objectVersion` pin and normalization, so skipping the hook gives a clear PR failure. Bumping the pin is a deliberate team decision: see [references/xcode-project-normalization.md](references/xcode-project-normalization.md).
|
||||
|
||||
## Sidebar extension point (dev tagging)
|
||||
|
||||
Each tagged dev build gets its own ExtensionKit sidebar extension point so concurrent dev builds don't collide. Three build settings drive this:
|
||||
Each tagged dev build gets its own ExtensionKit sidebar extension point so concurrent dev builds do not collide. Three build settings drive it:
|
||||
|
||||
- `CMUX_SIDEBAR_EXTENSION_POINT_ID` (default `com.cmuxterm.app.cmux.sidebar`): the extension point identifier baked into Info.plist at build time.
|
||||
- `CMUX_BUNDLE_ID_SUFFIX` (default empty): inserted into the app and appex bundle ids so a tagged extension gets a distinct identity that pkd records separately.
|
||||
- `CMUX_DISPLAY_NAME_SUFFIX` (default empty): appended to the appex `CFBundleDisplayName`. The OS groups sidebar extensions by display name for the enable/disable + availability counts the host reads (`AppExtensionIdentity` exposes only `bundleIdentifier`, `localizedName`, `extensionPointIdentifier`, `id` — cmux already keys its own identity off the stable `bundleIdentifier`, but the OS-level grouping is by name). Two same-named appexes installed side by side (a base build and a tagged build) are treated as one logical extension, so toggling one perturbs the other; a per-tag display name keeps them distinct.
|
||||
- `CMUX_DISPLAY_NAME_SUFFIX` (default empty): appended to the appex `CFBundleDisplayName`. The OS groups sidebar extensions by display name for the enable/disable and availability counts the host reads, so two same-named appexes installed side by side are treated as one logical extension and toggling one perturbs the other.
|
||||
|
||||
The host resolves its point id at runtime from the Info.plist key `CMUXSidebarExtensionPointIdentifier` via `CmuxSidebarExtensionPoint.identifier(in:)`. `./scripts/reload.sh --tag <tag>` scopes the host point to `com.cmuxterm.app.debug.<tag>.cmux.sidebar`. `./scripts/reload-extension.sh --tag <tag> [--host-bundle-id <id>] [--example sample|tabs|both]` builds a matching tag-scoped sample extension, passing `CMUX_SIDEBAR_EXTENSION_POINT_ID=<host-bundle-id>.cmux.sidebar`, `CMUX_BUNDLE_ID_SUFFIX=.<tag>`, and `CMUX_DISPLAY_NAME_SUFFIX=" <tag>"`. It installs exactly what xcodebuild produced (xcodebuild ad-hoc signs with entitlements intact) — it does NOT re-sign, because a bare `codesign --force --sign -` strips the appex entitlements and the extension then drops its host XPC connection. pkd ingests the tagged copy because its bundle id is distinct. Verify with `pluginkit -m -p <host-bundle-id>.cmux.sidebar`.
|
||||
The host resolves its point id at runtime from the Info.plist key `CMUXSidebarExtensionPointIdentifier` via `CmuxSidebarExtensionPoint.identifier(in:)`. `./scripts/reload.sh --tag <tag>` scopes the host point to `com.cmuxterm.app.debug.<tag>.cmux.sidebar`. Build a matching tag-scoped sample extension with:
|
||||
|
||||
To author a NEW sample extension that is tag-ready:
|
||||
```bash
|
||||
./scripts/reload-extension.sh --tag <tag> [--host-bundle-id <id>] [--example sample|tabs|both]
|
||||
```
|
||||
|
||||
- appex Info.plist: `EXAppExtensionAttributes:EXExtensionPointIdentifier = $(CMUX_SIDEBAR_EXTENSION_POINT_ID)`.
|
||||
- add `CMUX_SIDEBAR_EXTENSION_POINT_ID` (default `com.cmuxterm.app.cmux.sidebar`), `CMUX_BUNDLE_ID_SUFFIX` (default empty), and `CMUX_DISPLAY_NAME_SUFFIX` (default empty) build settings to the app and appex targets in all build configs.
|
||||
- `PRODUCT_BUNDLE_IDENTIFIER` = `<appBase>$(CMUX_BUNDLE_ID_SUFFIX)` for the app target and `<appBase>$(CMUX_BUNDLE_ID_SUFFIX).<leaf>` for the appex (suffix before the appex leaf so the appex id stays prefixed by the app id).
|
||||
- appex `INFOPLIST_KEY_CFBundleDisplayName` (or the `CFBundleDisplayName` Info.plist value) = `<Name>$(CMUX_DISPLAY_NAME_SUFFIX)`.
|
||||
- it must be ad-hoc signed by xcodebuild (Info.plist bound, entitlements intact) for pkd to ingest the tagged copy; do not re-sign post-build.
|
||||
|
||||
## Detailed references
|
||||
|
||||
- Read [references/tagged-builds.md](references/tagged-builds.md) for detailed tagged reload, app link, socket, and cleanup behavior.
|
||||
- Read [references/xcode-project-normalization.md](references/xcode-project-normalization.md) before touching `.xcode-version` or `cmux.xcodeproj/project.pbxproj`.
|
||||
- Read [references/sidebar-extension-tagging.md](references/sidebar-extension-tagging.md) when changing ExtensionKit sidebar extension identifiers, tagged sample extensions, or `pluginkit` verification.
|
||||
See [references/sidebar-extension-tagging.md](references/sidebar-extension-tagging.md) for the settings it passes, the no-re-signing rule, and the checklist for authoring a new tag-ready sample extension.
|
||||
|
||||
@@ -1,60 +1,27 @@
|
||||
# Sidebar Extension Tagging
|
||||
|
||||
Tagged dev builds need distinct ExtensionKit sidebar extension points so concurrent dev builds do not collide.
|
||||
The three build settings and why display name matters are in [../SKILL.md](../SKILL.md).
|
||||
|
||||
## Build settings
|
||||
## What reload-extension.sh passes
|
||||
|
||||
Three build settings drive the tagging model:
|
||||
|
||||
- `CMUX_SIDEBAR_EXTENSION_POINT_ID`
|
||||
- `CMUX_BUNDLE_ID_SUFFIX`
|
||||
- `CMUX_DISPLAY_NAME_SUFFIX`
|
||||
|
||||
The default extension point is:
|
||||
|
||||
```text
|
||||
com.cmuxterm.app.cmux.sidebar
|
||||
```
|
||||
|
||||
Tagged host builds scope it to:
|
||||
|
||||
```text
|
||||
com.cmuxterm.app.debug.<tag>.cmux.sidebar
|
||||
```
|
||||
|
||||
## Why display name matters
|
||||
|
||||
`AppExtensionIdentity` exposes stable fields such as bundle identifier, localized name, extension point identifier, and id. cmux keys its identity off the stable bundle identifier, but OS-level enable/disable and availability grouping uses display name.
|
||||
|
||||
Two same-named appexes installed side by side can be treated as one logical extension. Per-tag display names keep tagged sample extensions distinct.
|
||||
|
||||
## Tagged sample extensions
|
||||
|
||||
`./scripts/reload-extension.sh --tag <tag> [--host-bundle-id <id>] [--example sample|tabs|both]` builds a matching tag-scoped sample extension.
|
||||
|
||||
It passes:
|
||||
`./scripts/reload-extension.sh --tag <tag> [--host-bundle-id <id>] [--example sample|tabs|both]` builds a tag-scoped sample extension with:
|
||||
|
||||
- `CMUX_SIDEBAR_EXTENSION_POINT_ID=<host-bundle-id>.cmux.sidebar`
|
||||
- `CMUX_BUNDLE_ID_SUFFIX=.<tag>`
|
||||
- `CMUX_DISPLAY_NAME_SUFFIX=" <tag>"`
|
||||
|
||||
It installs exactly what xcodebuild produced. It does not re-sign. A bare `codesign --force --sign -` strips appex entitlements and the extension drops its host XPC connection.
|
||||
|
||||
## New sample extension checklist
|
||||
|
||||
For a new tag-ready sample extension:
|
||||
|
||||
- appex Info.plist has `EXAppExtensionAttributes:EXExtensionPointIdentifier = $(CMUX_SIDEBAR_EXTENSION_POINT_ID)`
|
||||
- app and appex targets define `CMUX_SIDEBAR_EXTENSION_POINT_ID`
|
||||
- app and appex targets define `CMUX_BUNDLE_ID_SUFFIX`
|
||||
- app and appex targets define `CMUX_DISPLAY_NAME_SUFFIX`
|
||||
- app `PRODUCT_BUNDLE_IDENTIFIER` uses `<appBase>$(CMUX_BUNDLE_ID_SUFFIX)`
|
||||
- appex `PRODUCT_BUNDLE_IDENTIFIER` uses `<appBase>$(CMUX_BUNDLE_ID_SUFFIX).<leaf>`
|
||||
- appex display name appends `$(CMUX_DISPLAY_NAME_SUFFIX)`
|
||||
- xcodebuild ad-hoc signs the appex with entitlements intact
|
||||
It installs exactly what xcodebuild produced and does **not** re-sign. A bare `codesign --force --sign -` strips the appex entitlements and the extension then drops its host XPC connection. pkd ingests the tagged copy because its bundle id is distinct.
|
||||
|
||||
Verify with:
|
||||
|
||||
```bash
|
||||
pluginkit -m -p <host-bundle-id>.cmux.sidebar
|
||||
```
|
||||
|
||||
## New tag-ready sample extension checklist
|
||||
|
||||
- appex Info.plist: `EXAppExtensionAttributes:EXExtensionPointIdentifier = $(CMUX_SIDEBAR_EXTENSION_POINT_ID)`.
|
||||
- app and appex targets define `CMUX_SIDEBAR_EXTENSION_POINT_ID` (default `com.cmuxterm.app.cmux.sidebar`), `CMUX_BUNDLE_ID_SUFFIX` (default empty), and `CMUX_DISPLAY_NAME_SUFFIX` (default empty) in all build configs.
|
||||
- app `PRODUCT_BUNDLE_IDENTIFIER` = `<appBase>$(CMUX_BUNDLE_ID_SUFFIX)`; appex = `<appBase>$(CMUX_BUNDLE_ID_SUFFIX).<leaf>`, so the suffix lands before the appex leaf and the appex id stays prefixed by the app id.
|
||||
- appex `INFOPLIST_KEY_CFBundleDisplayName` (or the `CFBundleDisplayName` value) = `<Name>$(CMUX_DISPLAY_NAME_SUFFIX)`.
|
||||
- xcodebuild ad-hoc signs the appex with Info.plist bound and entitlements intact; do not re-sign post-build.
|
||||
|
||||
@@ -1,61 +1,29 @@
|
||||
# Tagged Builds
|
||||
|
||||
Tagged builds isolate app name, bundle ID, socket, and DerivedData path so multiple agents and the user's normal app do not collide.
|
||||
|
||||
## Reload
|
||||
|
||||
Use:
|
||||
Tagged builds isolate app name, bundle ID, debug socket, and DerivedData path so multiple agents and the user's normal app do not collide.
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <tag>
|
||||
./scripts/reload.sh --tag <tag> # build only (default)
|
||||
./scripts/reload.sh --tag <tag> --launch # build, then open
|
||||
```
|
||||
|
||||
`reload.sh` builds but does not launch by default. It terminates any running app with the same tag after a successful build, so opening the printed app path launches the fresh binary.
|
||||
|
||||
Use:
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <tag> --launch
|
||||
```
|
||||
|
||||
only when the task requires launching.
|
||||
After a successful build `reload.sh` terminates any running app with the same tag, so opening the printed app path launches the fresh binary.
|
||||
|
||||
## App path links
|
||||
|
||||
`reload.sh` prints:
|
||||
|
||||
```text
|
||||
App path:
|
||||
/absolute/path/to/cmux DEV <tag>.app
|
||||
```
|
||||
|
||||
Build chat links from that exact path. Prepend `file://` and URL-encode spaces as `%20`. Do not hardcode DerivedData paths and never use `/tmp/cmux-<tag>/...` app links in chat output.
|
||||
`reload.sh` prints an `App path:` line with the absolute path to the built `.app`. Use it to confirm the tag built, but link the build in chat as `http://127.0.0.1:17320/<tag>` through the local Tag Opener. Never put a `file://` URL, a raw `.app` or DerivedData path, or a `/tmp/cmux-<tag>/...` link in chat output.
|
||||
|
||||
## Tagged CLI and socket
|
||||
|
||||
For CLI or socket dogfood against a tagged Debug app, use:
|
||||
|
||||
```bash
|
||||
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh list-workspaces
|
||||
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh send --workspace workspace:1 --surface surface:1 "echo ok"
|
||||
```
|
||||
|
||||
Do not use `/tmp/cmux-cli` for tagged dogfood. That symlink points at the most recently reloaded build and can target the user's main app socket.
|
||||
The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.sock`, uses the matching tagged CLI from DerivedData, scrubs ambient cmux terminal context (`CMUX_SOCKET`, `CMUX_SOCKET_PASSWORD`, workspace/surface/tab/panel IDs, cmuxd socket, debug log), then sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and `CMUX_BUNDLED_CLI_PATH` for that tag.
|
||||
|
||||
The helper:
|
||||
|
||||
- refuses to run without `CMUX_TAG`
|
||||
- targets `/tmp/cmux-debug-<tag>.sock`
|
||||
- uses the matching tagged CLI from DerivedData
|
||||
- scrubs ambient cmux terminal context
|
||||
- sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and `CMUX_BUNDLED_CLI_PATH`
|
||||
`/tmp/cmux-cli` points at the most recently reloaded build and can target the user's main app socket, so it is never safe for tagged dogfood.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Before launching a new tagged run, clean up older tags started in the same session:
|
||||
|
||||
- quit old tagged app
|
||||
- remove its `/tmp` socket if stale
|
||||
- remove derived data only when you are sure no active task needs it
|
||||
|
||||
Do not open an untagged `cmux DEV.app` from DerivedData. It shares the default debug socket and bundle ID with other agents.
|
||||
Before launching a new tagged run, quit older tagged apps you started this session and remove their stale `/tmp` sockets. Remove derived data only when no active task needs it.
|
||||
|
||||
@@ -1,46 +1,12 @@
|
||||
# Xcode Project Normalization
|
||||
|
||||
cmux is pinned to Xcode 26.x. `.xcode-version` records the major version. `cmux.xcodeproj/project.pbxproj` carries `objectVersion = 60`, which is what Xcode 26 writes by default.
|
||||
The pin (`.xcode-version`, `objectVersion = 60`), the pre-commit hook, and the CI guard are described in [../SKILL.md](../SKILL.md).
|
||||
|
||||
`objectVersion = 77` is reserved for projects that adopt synchronized folder groups. cmux does not use synchronized folder groups yet.
|
||||
|
||||
## Pre-commit hook
|
||||
|
||||
`scripts/setup.sh` installs:
|
||||
|
||||
```text
|
||||
scripts/git-hooks/pre-commit
|
||||
```
|
||||
|
||||
The hook runs:
|
||||
|
||||
```bash
|
||||
scripts/normalize-pbxproj.py
|
||||
```
|
||||
|
||||
on staged `cmux.xcodeproj/project.pbxproj` changes. This sorts high-churn sections so Xcode's nondeterministic reordering does not reach commits.
|
||||
|
||||
## CI guard
|
||||
|
||||
CI runs:
|
||||
|
||||
```bash
|
||||
scripts/check-pbxproj.sh
|
||||
```
|
||||
|
||||
It enforces both:
|
||||
|
||||
- the `.xcode-version` / `objectVersion` pin
|
||||
- pbxproj normalization
|
||||
|
||||
## Bumping Xcode
|
||||
|
||||
To bump the pin:
|
||||
## Bumping the Xcode pin
|
||||
|
||||
1. Edit `.xcode-version`.
|
||||
2. Open `cmux.xcodeproj` in the new Xcode so it rewrites `objectVersion`.
|
||||
3. Add a case in `scripts/check-pbxproj.sh` mapping the new Xcode major to the objectVersion that Xcode writes.
|
||||
4. Normalize the project file.
|
||||
5. Treat the bump as a deliberate team decision.
|
||||
3. Add a case in `scripts/check-pbxproj.sh` mapping the new Xcode major to the `objectVersion` that major writes.
|
||||
4. Run `scripts/normalize-pbxproj.py`.
|
||||
|
||||
Do not change `objectVersion` opportunistically as part of unrelated project edits.
|
||||
|
||||
@@ -5,90 +5,40 @@ description: "Run end-user cmux diagnostics. Use when cmux hooks, notifications,
|
||||
|
||||
# cmux Diagnostics
|
||||
|
||||
Use this skill to collect and interpret support-safe cmux diagnostics for end users. Default to read-only checks. Do not dump hook config files, session stores, prompt logs, tokens, or environment secrets.
|
||||
Collect and interpret support-safe cmux diagnostics for end users. Default to read-only checks. Never dump hook config files, session stores, prompt logs, tokens, or environment secrets.
|
||||
|
||||
## Quick Report
|
||||
## Quick report
|
||||
|
||||
Run the bundled read-only diagnostic script first:
|
||||
Run the bundled read-only script first, from whichever install path exists:
|
||||
|
||||
```bash
|
||||
# From a cmux checkout
|
||||
skills/cmux-diagnostics/scripts/cmux-diagnostics
|
||||
|
||||
# From an installed skill
|
||||
~/.agents/skills/cmux-diagnostics/scripts/cmux-diagnostics
|
||||
|
||||
# From a Codex-only skills.sh install
|
||||
~/.codex/skills/cmux-diagnostics/scripts/cmux-diagnostics
|
||||
skills/cmux-diagnostics/scripts/cmux-diagnostics # cmux checkout
|
||||
~/.agents/skills/cmux-diagnostics/scripts/cmux-diagnostics # installed skill
|
||||
~/.codex/skills/cmux-diagnostics/scripts/cmux-diagnostics # Codex-only skills.sh install
|
||||
```
|
||||
|
||||
Use `--include-context` only when workspace names, cwd paths, and current cmux identifiers are relevant to the user-reported issue:
|
||||
Add `--include-context` only when workspace names, cwd paths, and current cmux identifiers are relevant to the reported issue.
|
||||
|
||||
```bash
|
||||
skills/cmux-diagnostics/scripts/cmux-diagnostics --include-context
|
||||
```
|
||||
## What to check
|
||||
|
||||
## What to Check
|
||||
|
||||
1. CLI and socket health:
|
||||
|
||||
```bash
|
||||
command -v cmux
|
||||
cmux ping
|
||||
cmux capabilities --json
|
||||
```
|
||||
|
||||
If socket commands fail, check whether the agent is running inside a cmux terminal and whether socket automation is enabled.
|
||||
|
||||
2. Settings health:
|
||||
|
||||
```bash
|
||||
~/.agents/skills/cmux-settings/scripts/cmux-settings validate
|
||||
~/.agents/skills/cmux-settings/scripts/cmux-settings get terminal.autoResumeAgentSessions
|
||||
```
|
||||
|
||||
If the user installed with `skills.sh`, use `~/.codex/skills/cmux-settings/scripts/cmux-settings` instead.
|
||||
If `terminal.autoResumeAgentSessions` is false, cmux restores panes but will not automatically resume saved agent sessions.
|
||||
|
||||
3. Hook installation:
|
||||
|
||||
```bash
|
||||
cmux hooks setup --agent codex
|
||||
cmux hooks setup --agent opencode
|
||||
cmux hooks setup
|
||||
```
|
||||
|
||||
Only run install or uninstall commands after the user agrees. `cmux hooks setup` installs supported agents found on PATH and skips missing agents.
|
||||
|
||||
4. Session restore evidence:
|
||||
|
||||
```bash
|
||||
ls -lh ~/.cmuxterm/*-hook-sessions.json 2>/dev/null
|
||||
```
|
||||
|
||||
Missing session stores usually means the agent has not run inside cmux since hooks were installed, hooks are disabled, or the agent integration does not support resume capture.
|
||||
|
||||
5. Notification path:
|
||||
|
||||
```bash
|
||||
cmux notify "cmux diagnostic test"
|
||||
```
|
||||
|
||||
Use this only when the user is ready for a visible test notification.
|
||||
1. **CLI and socket health**: `command -v cmux`, `cmux ping`, `cmux capabilities --json`. If socket commands fail, check whether the agent is running inside a cmux terminal and whether socket automation is enabled.
|
||||
2. **Settings health**: `cmux-settings validate` and `cmux-settings get terminal.autoResumeAgentSessions` (from `~/.agents/skills/cmux-settings/scripts/`, or `~/.codex/skills/...` for a `skills.sh` install). When `terminal.autoResumeAgentSessions` is false, cmux restores panes but does not resume saved agent sessions.
|
||||
3. **Hook installation**: `cmux hooks setup --agent codex`, `--agent opencode`, or bare `cmux hooks setup` (installs supported agents found on PATH, skips missing ones). Run install or uninstall commands only after the user agrees.
|
||||
4. **Session restore evidence**: `ls -lh ~/.cmuxterm/*-hook-sessions.json 2>/dev/null`. Missing stores usually mean the agent has not run inside cmux since hooks were installed, hooks are disabled, or the integration does not support resume capture.
|
||||
5. **Notification path**: `cmux notify "cmux diagnostic test"`, only when the user is ready for a visible test notification.
|
||||
|
||||
## Interpretation
|
||||
|
||||
- `cmux` not found: the CLI is not installed or not on PATH for this shell.
|
||||
- `cmux ping` fails: app is not reachable through the current socket path, the app is closed, or automation access is disabled.
|
||||
- No `CMUX_WORKSPACE_ID` or `CMUX_SURFACE_ID`: the command is probably running outside a cmux terminal. Some hooks intentionally no-op outside cmux.
|
||||
- Hook config exists but no session store: run one supported agent inside cmux after installing hooks, then re-check.
|
||||
- Session store exists but restore does not launch agents: check `terminal.autoResumeAgentSessions` and whether the saved executable still exists on PATH.
|
||||
- Settings validation fails: fix the config first. Invalid config can make later symptoms misleading.
|
||||
- `cmux ping` fails: the app is closed, unreachable through the current socket path, or automation access is disabled.
|
||||
- No `CMUX_WORKSPACE_ID` or `CMUX_SURFACE_ID`: the command is running outside a cmux terminal. Some hooks intentionally no-op there.
|
||||
- Hook config but no session store: run one supported agent inside cmux after installing hooks, then re-check.
|
||||
- Session store but no agents on restore: check `terminal.autoResumeAgentSessions` and whether the saved executable still exists on PATH.
|
||||
- Settings validation fails: fix the config first. Invalid config makes later symptoms misleading.
|
||||
|
||||
## Rules
|
||||
|
||||
- Stay read-only until the user asks to fix something.
|
||||
- Never print raw hook files, session JSON, prompt logs, shell history, tokens, or API keys.
|
||||
- Summarize file presence, size, modified time, and marker presence instead of contents.
|
||||
- Prefer narrow fixes such as `cmux hooks setup --agent codex` over reinstalling every integration.
|
||||
- Never print raw hook files, session JSON, prompt logs, shell history, tokens, or API keys. Summarize file presence, size, modified time, and marker presence instead.
|
||||
- Prefer a narrow fix such as `cmux hooks setup --agent codex` over reinstalling every integration.
|
||||
- After a fix, rerun the diagnostic script and report the changed lines.
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Ghostty submodule and GhosttyKit workflow rules for cmux. Use when
|
||||
|
||||
## GhosttyKit builds
|
||||
|
||||
When rebuilding GhosttyKit.xcframework, always use Release optimizations:
|
||||
Always rebuild the xcframework with Release optimizations:
|
||||
|
||||
```bash
|
||||
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
@@ -15,28 +15,31 @@ cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -
|
||||
|
||||
## 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.
|
||||
Ghostty changes are committed in the `ghostty` submodule and pushed to the `manaflow-ai/ghostty` fork. Keep `docs/ghostty-fork.md` current with fork changes and conflict notes.
|
||||
|
||||
Always run `git remote -v` first and push to whichever remote is `manaflow-ai/ghostty`. `.gitmodules` sets the submodule URL to that fork, so in a normal checkout it is `origin`; older setups tracked upstream as `origin` and added the fork as `manaflow`. Substitute the right name below.
|
||||
|
||||
```bash
|
||||
cd ghostty
|
||||
git remote -v # origin = upstream, manaflow = fork
|
||||
git remote -v # find the manaflow-ai/ghostty remote (usually origin)
|
||||
git checkout -b <branch>
|
||||
git add <files>
|
||||
git commit -m "..."
|
||||
git push manaflow <branch>
|
||||
git push origin <branch>
|
||||
```
|
||||
|
||||
To keep the fork up to date with upstream:
|
||||
To pull in changes from upstream `ghostty-org/ghostty`, add it as an explicit remote first, since no checkout has it by default:
|
||||
|
||||
```bash
|
||||
cd ghostty
|
||||
git fetch origin
|
||||
git remote add upstream https://github.com/ghostty-org/ghostty.git # once
|
||||
git fetch upstream
|
||||
git checkout main
|
||||
git merge origin/main
|
||||
git push manaflow main
|
||||
git merge upstream/main
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Then update the parent repo with the new submodule SHA:
|
||||
Then record the new SHA in the parent repo:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
@@ -46,14 +49,14 @@ git commit -m "Update ghostty submodule"
|
||||
|
||||
## Submodule safety
|
||||
|
||||
When modifying a submodule, 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 can be orphaned and lost.
|
||||
For any submodule (ghostty, `vendor/bonsplit`, `homebrew-cmux`), push the submodule commit to its remote branch **before** committing the updated pointer in the parent repo. Never commit on a detached HEAD or a temporary branch: the parent then points at a SHA unreachable from any remote branch, and a future checkout or CI job fails to fetch it.
|
||||
|
||||
Verify with:
|
||||
Verify the commit is reachable from the branch the pointer should track, using the remote you just pushed to:
|
||||
|
||||
```bash
|
||||
cd <submodule> && git merge-base --is-ancestor HEAD origin/main
|
||||
cd ghostty && git fetch origin main && git merge-base --is-ancestor HEAD origin/main
|
||||
```
|
||||
|
||||
## Detailed reference
|
||||
|
||||
- Read [references/submodule-safety.md](references/submodule-safety.md) before committing submodule pointer updates or resolving Ghostty fork conflicts.
|
||||
- [references/submodule-safety.md](references/submodule-safety.md): the ordered safe sequence and fork documentation expectations.
|
||||
|
||||
@@ -1,51 +1,18 @@
|
||||
# Submodule Safety
|
||||
|
||||
Submodule commits can be easy to lose. The parent repository records only a commit SHA, not the branch that made the SHA reachable.
|
||||
The parent repository records only a commit SHA, not the branch that makes the SHA reachable, so submodule commits are easy to lose.
|
||||
|
||||
## Safe sequence
|
||||
|
||||
1. Enter the submodule.
|
||||
2. Create or select the intended branch.
|
||||
2. Create or select the intended branch (never a detached HEAD).
|
||||
3. Commit the submodule changes.
|
||||
4. Push the submodule commit to the correct remote.
|
||||
5. Verify the pushed branch contains the commit.
|
||||
6. Return to the parent repository.
|
||||
7. Commit the updated submodule pointer.
|
||||
4. Push to the remote that hosts the fork. `.gitmodules` points every submodule at `manaflow-ai/*`, so that is normally `origin`; run `git remote -v` to confirm before pushing.
|
||||
5. Verify the pushed branch contains the commit, checking the branch you actually pushed rather than always `main`: `git merge-base --is-ancestor HEAD <remote>/<branch>`.
|
||||
6. Return to the parent repository and commit the updated pointer.
|
||||
|
||||
For Ghostty:
|
||||
|
||||
```bash
|
||||
cd ghostty
|
||||
git remote -v
|
||||
git checkout -b <branch>
|
||||
git add <files>
|
||||
git commit -m "..."
|
||||
git push manaflow <branch>
|
||||
```
|
||||
|
||||
If the parent pointer is supposed to track fork `main`, make sure the commit is an ancestor of that remote branch:
|
||||
|
||||
```bash
|
||||
git fetch manaflow main
|
||||
git merge-base --is-ancestor HEAD manaflow/main
|
||||
```
|
||||
|
||||
The top-level CLAUDE note uses `origin/main` as the generic verification form for submodules. In the Ghostty submodule, check the actual remote names first because `origin` may be upstream and `manaflow` may be the fork.
|
||||
|
||||
## Detached HEAD hazard
|
||||
|
||||
Do not commit submodule changes on a detached HEAD and then update the parent pointer. That creates a parent commit pointing at a SHA that may not be reachable from any remote branch. A future checkout or CI job can fail to fetch it.
|
||||
Skipping step 4 or 5 produces a parent commit pointing at an orphaned SHA that a future checkout or CI job cannot fetch.
|
||||
|
||||
## Fork documentation
|
||||
|
||||
Keep `docs/ghostty-fork.md` updated when fork changes or conflict notes matter for future upstream merges. The point is to preserve why the fork diverged, not just that it diverged.
|
||||
|
||||
## GhosttyKit optimization
|
||||
|
||||
Rebuild GhosttyKit.xcframework with ReleaseFast:
|
||||
|
||||
```bash
|
||||
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
Debug or default optimization builds can hide performance characteristics and should not be used for the checked-in framework refresh path.
|
||||
Keep `docs/ghostty-fork.md` updated when fork changes or conflict notes matter for a future upstream merge. Record why the fork diverged, not just that it did.
|
||||
|
||||
@@ -5,59 +5,51 @@ description: "Guide and apply cmux keyboard shortcut customization. Use when the
|
||||
|
||||
# cmux-keyboard-shortcuts
|
||||
|
||||
Use this skill to turn a user's workflow preferences into cmux shortcut bindings in `~/.config/cmux/cmux.json`. It should guide the user, propose compact templates, apply selected changes, and confirm the config parses with recognized keys.
|
||||
Turn a user's workflow preferences into cmux shortcut bindings in `~/.config/cmux/cmux.json`: guide, propose compact templates, apply the selected changes, and confirm the config parses with recognized keys.
|
||||
|
||||
## Contributor rule: adding a new shortcut
|
||||
|
||||
Every new cmux-owned keyboard shortcut must be added to `Sources/KeyboardShortcutSettings.swift`, visible and editable in Settings > Keyboard Shortcuts, supported as `shortcuts.bindings.<actionId>` in `~/.config/cmux/cmux.json`, and documented in `web/app/[locale]/(landing)/docs/keyboard-shortcuts/page.tsx` and the configuration docs. All four, not a subset.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Work from a cmux checkout or worktree root when possible.
|
||||
- Use `skills/cmux-settings/scripts/cmux-settings` for every read/write. It reads JSONC, writes atomically, and validates JSON plus recognized settings keys.
|
||||
- For action IDs, read `skills/cmux-settings/references/shortcut-actions.md`.
|
||||
- For current defaults, read `web/data/cmux-shortcuts.ts` or `Sources/KeyboardShortcutSettings.swift`.
|
||||
- Action IDs: `skills/cmux-settings/references/shortcut-actions.md`. Current defaults: `web/data/cmux-shortcuts.ts` or `Sources/KeyboardShortcutSettings.swift`.
|
||||
|
||||
```bash
|
||||
find_cmux_settings() {
|
||||
local root
|
||||
root="$(git -C "$(pwd)" rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
if [[ -z "${CMUX_SETTINGS:-}" ]]; then
|
||||
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
for candidate in \
|
||||
"$root/skills/cmux-settings/scripts/cmux-settings" \
|
||||
"${CODEX_HOME:-$HOME/.codex}/skills/cmux-settings/scripts/cmux-settings" \
|
||||
"$HOME/.agents/skills/cmux-settings/scripts/cmux-settings"; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
[[ -x "$candidate" ]] && CMUX_SETTINGS="$candidate" && break
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -z "${CMUX_SETTINGS:-}" ]]; then
|
||||
CMUX_SETTINGS="$(find_cmux_settings)" || {
|
||||
[[ -n "${CMUX_SETTINGS:-}" ]] || {
|
||||
echo "cmux-settings helper not found; run from a cmux checkout or install cmux-settings" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
```
|
||||
|
||||
## Shortcut Model
|
||||
## Shortcut model
|
||||
|
||||
- Setting path: `shortcuts.bindings.<actionId>`.
|
||||
- Single stroke: `"cmd+b"`.
|
||||
- Chord: `["ctrl+b","c"]`. The first stroke needs a modifier unless the key is Space. The second stroke can be bare.
|
||||
- Unbind: prefer `null` for explicit unbinds. `""`, `"none"`, `"clear"`, `"unbound"`, and `"disabled"` are accepted aliases, but `null` is the clearest JSON value and matches the templates below.
|
||||
- `selectSurfaceByNumber` and `selectWorkspaceByNumber` must use a digit from 1 to 9. `cmd+1` means the full `cmd+1` through `cmd+9` family.
|
||||
- `showHideAllWindows` and `globalSearch` are system-wide shortcuts. They cannot be chords, require modifiers, and may be rejected by macOS if reserved.
|
||||
- `showHideAllWindows` also requires Settings > Global Hotkey > Enable System-Wide Hotkey. The binding can validate in `cmux.json` while the feature is disabled, so warn the user to enable that setting before reporting the shortcut as usable.
|
||||
- `unset` deletes a `cmux.json` override. It does not clear shortcut changes saved through the Settings UI/UserDefaults. If the user asks for true built-in defaults, tell them to use Settings > Keyboard Shortcuts > Reset Default Shortcuts after clearing file-managed overrides, then verify in the app. For `showHideAllWindows`, use Settings > Global Hotkey to restore the shortcut to `ctrl+opt+cmd+.` because Keyboard Shortcuts > Reset Default Shortcuts intentionally skips the global hotkey.
|
||||
- Saving `cmux.json` live reloads. Do not tell the user to restart cmux.
|
||||
- Setting path: `shortcuts.bindings.<actionId>`. Single stroke `"cmd+b"`; chord `["ctrl+b","c"]` where the first stroke needs a modifier unless the key is Space and the second stroke can be bare.
|
||||
- Unbind with `null`. `""`, `"none"`, `"clear"`, `"unbound"`, and `"disabled"` are accepted aliases.
|
||||
- `selectSurfaceByNumber` and `selectWorkspaceByNumber` take a digit 1-9; `cmd+1` means the whole `cmd+1`..`cmd+9` family.
|
||||
- `showHideAllWindows` and `globalSearch` are system-wide: no chords, modifiers required, and macOS may reject reserved combinations. `showHideAllWindows` also needs Settings > Global Hotkey > Enable System-Wide Hotkey; the binding validates in `cmux.json` while the feature is off, so warn the user before calling it usable.
|
||||
- `unset` deletes a `cmux.json` override; it does not clear shortcut changes saved through the Settings UI/UserDefaults. For true built-in defaults, clear file overrides then use Settings > Keyboard Shortcuts > Reset Default Shortcuts and verify in the app. That reset intentionally skips the global hotkey, so restore `showHideAllWindows` to `ctrl+opt+cmd+.` from Settings > Global Hotkey.
|
||||
- Saving `cmux.json` live reloads. Never tell the user to restart cmux.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Classify the request:
|
||||
- One-off rebind or unbind: map the phrase to an action ID, apply it, validate, and report the previous and new binding.
|
||||
- Audit-only request: inspect current bindings, validate, and summarize overrides/unbound shortcuts without writing.
|
||||
- Reset request: clarify whether the user means file-managed overrides or true built-in defaults. For file-managed resets, use `unset` for named actions. For true built-in defaults, remove file overrides and direct the user to Settings > Keyboard Shortcuts > Reset Default Shortcuts; do not report built-in defaults restored from `cmux-settings` alone. If `showHideAllWindows` is included, also direct them to Settings > Global Hotkey to restore `ctrl+opt+cmd+.` and the enable toggle.
|
||||
- Broad customization request: propose 3 to 5 templates from "Preset Templates" and ask the user to choose.
|
||||
- Named style such as tmux, Vim, iTerm, browser, or agent triage: select the closest template, show the changed actions and likely collisions, and ask before a bulk apply unless the user explicitly said to apply it.
|
||||
- **One-off rebind/unbind:** map the phrase to an action ID, apply, validate, report previous and new binding.
|
||||
- **Audit:** inspect bindings, validate, summarize overrides and unbound shortcuts without writing.
|
||||
- **Reset:** clarify file-managed overrides vs true built-in defaults (see the `unset` rule above).
|
||||
- **Broad customization:** propose 3-5 templates below and ask the user to choose.
|
||||
- **Named style** (tmux, Vim, iTerm, browser, agent triage): pick the closest template, show the changed actions and likely collisions, and ask before a bulk apply unless the user already named that template.
|
||||
2. Inspect existing config:
|
||||
|
||||
```bash
|
||||
@@ -66,36 +58,30 @@ fi
|
||||
"$CMUX_SETTINGS" validate
|
||||
```
|
||||
|
||||
3. Before applying a template, snapshot prior values for every action you will change. A path that is absent must revert with `unset`; a path with an existing custom value must revert with `set <same-json-value>`.
|
||||
3. Snapshot prior values for every action you will change. A path that was absent reverts with `unset`; a path with an existing custom value reverts with `set <same-json-value>`.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" get shortcuts.bindings.focusLeft 2>/dev/null || printf '<absent>\n'
|
||||
```
|
||||
|
||||
4. Apply only the chosen action paths:
|
||||
4. Apply only the chosen action paths, then `"$CMUX_SETTINGS" validate`.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.newSurface '["ctrl+b","c"]'
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.focusLeft cmd+opt+h
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.sendFeedback null
|
||||
"$CMUX_SETTINGS" validate
|
||||
```
|
||||
|
||||
5. Verify readback for changed actions:
|
||||
5. Read back each changed action: `"$CMUX_SETTINGS" get shortcuts.bindings.newSurface`.
|
||||
6. Finish with the template name, changed actions, and exact revert commands from the snapshot.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" get shortcuts.bindings.newSurface
|
||||
```
|
||||
## Preset templates
|
||||
|
||||
6. Finish with the template name, changed actions, and exact revert commands from the snapshot. Use `unset` only for actions that were absent before the template; use `set` to restore previous custom bindings.
|
||||
|
||||
## Preset Templates
|
||||
|
||||
Use these as proposal templates. Apply them action by action, not by overwriting the whole `shortcuts.bindings` object.
|
||||
Apply action by action, never by overwriting the whole `shortcuts.bindings` object.
|
||||
|
||||
### Tmux Prefix
|
||||
|
||||
For users who want one terminal-style shortcut namespace and accept that `ctrl+b` starts a cmux chord instead of going directly to the shell.
|
||||
One terminal-style namespace; `ctrl+b` starts a cmux chord instead of reaching the shell.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.newSurface '["ctrl+b","c"]'
|
||||
@@ -116,24 +102,18 @@ For users who want one terminal-style shortcut namespace and accept that `ctrl+b
|
||||
|
||||
### macOS Terminal/iTerm Restore
|
||||
|
||||
For users who want surface, split, and tab behavior to feel like common macOS terminals again. These actions already match cmux built-in defaults when no Settings UI override exists, so unset file overrides instead of writing default values.
|
||||
These actions already match cmux built-in defaults, so clear file overrides rather than writing default values.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.newSurface
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.closeTab
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.nextSurface
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.prevSurface
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.selectSurfaceByNumber
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.splitRight
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.splitDown
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.toggleSplitZoom
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.toggleTerminalCopyMode
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.renameTab
|
||||
for a in newSurface closeTab nextSurface prevSurface selectSurfaceByNumber \
|
||||
splitRight splitDown toggleSplitZoom toggleTerminalCopyMode renameTab; do
|
||||
"$CMUX_SETTINGS" unset "shortcuts.bindings.$a"
|
||||
done
|
||||
```
|
||||
|
||||
### Vim Pane Navigation
|
||||
|
||||
For users who want fast pane movement without a prefix and do not want to depend on arrow keys.
|
||||
Prefix-free pane movement with no arrow keys.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.focusLeft cmd+opt+h
|
||||
@@ -148,7 +128,7 @@ For users who want fast pane movement without a prefix and do not want to depend
|
||||
|
||||
### Agent Triage
|
||||
|
||||
For users who live in notifications and want unread handling on one key family. This keeps toggle unread on `cmd+opt+u` so it can be combined with Vim Pane Navigation without colliding with `cmd+opt+j`.
|
||||
Unread handling on one key family. `toggleUnread` stays on `cmd+opt+u` so this composes with Vim Pane Navigation without colliding with `cmd+opt+j`.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.showNotifications cmd+u
|
||||
@@ -161,7 +141,7 @@ For users who live in notifications and want unread handling on one key family.
|
||||
|
||||
### Workspace And Surface Lanes
|
||||
|
||||
For users who want workspaces and surfaces on distinct number and bracket lanes.
|
||||
Workspaces and surfaces on distinct number and bracket lanes.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.selectWorkspaceByNumber cmd+1
|
||||
@@ -174,47 +154,32 @@ For users who want workspaces and surfaces on distinct number and bracket lanes.
|
||||
|
||||
### Browser Defaults Restore
|
||||
|
||||
For users who changed too much and want embedded-browser behavior to match common macOS browser shortcuts again. Use `unset` to clear file overrides so future cmux defaults still apply when no Settings UI override exists.
|
||||
Return embedded-browser behavior to common macOS browser shortcuts. `unset` keeps future cmux defaults applying.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.openBrowser
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.focusBrowserAddressBar
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.browserBack
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.browserForward
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.browserReload
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.browserZoomIn
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.browserZoomOut
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.browserZoomReset
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.toggleBrowserDeveloperTools
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.showBrowserJavaScriptConsole
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.find
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.findNext
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.findPrevious
|
||||
for a in openBrowser focusBrowserAddressBar browserBack browserForward browserReload \
|
||||
browserZoomIn browserZoomOut browserZoomReset toggleBrowserDeveloperTools \
|
||||
showBrowserJavaScriptConsole find findNext findPrevious; do
|
||||
"$CMUX_SETTINGS" unset "shortcuts.bindings.$a"
|
||||
done
|
||||
```
|
||||
|
||||
### Terminal-First Cleanup
|
||||
|
||||
For users who want fewer app-level shortcuts. Prefer unbinding only the actions they name, but this is a reasonable starting proposal.
|
||||
Fewer app-level shortcuts. Prefer unbinding only the actions the user names; this is a starting proposal.
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.renameTab null
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.renameWorkspace null
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.editWorkspaceDescription null
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.triggerFlash null
|
||||
"$CMUX_SETTINGS" set shortcuts.bindings.sendFeedback null
|
||||
for a in renameTab renameWorkspace editWorkspaceDescription triggerFlash sendFeedback; do
|
||||
"$CMUX_SETTINGS" set "shortcuts.bindings.$a" null
|
||||
done
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Do not edit `~/.config/cmux/settings.json` unless the user explicitly asks. It is legacy fallback config.
|
||||
- Do not overwrite all of `shortcuts.bindings` unless the user explicitly wants a full replacement.
|
||||
- Do not edit `~/.config/cmux/settings.json` unless the user explicitly asks; it is legacy fallback config.
|
||||
- Do not overwrite all of `shortcuts.bindings` unless the user wants a full replacement.
|
||||
- Do not invent action IDs. Validate against the schema or `shortcut-actions.md`.
|
||||
- Do not apply a broad template without showing the changed actions first unless the user explicitly said to apply that named template.
|
||||
- Do not promise conflict detection from `cmux-settings validate`; it validates JSON and supported keys, not shortcut syntax, macOS reservation, or every focus-context conflict.
|
||||
- Before assigning `cmd+[` or `cmd+]` to application-scoped actions, warn that they collide with common browser Back/Forward behavior unless the browser actions are also changed or unbound.
|
||||
- Prefer `unset` to clear file-managed overrides for individual actions. Do not call this a built-in default reset unless Settings UI/UserDefaults values have also been reset:
|
||||
|
||||
```bash
|
||||
"$CMUX_SETTINGS" unset shortcuts.bindings.focusLeft
|
||||
"$CMUX_SETTINGS" validate
|
||||
```
|
||||
- Do not apply a broad template without showing the changed actions first, unless the user named that template.
|
||||
- Do not promise conflict detection from `cmux-settings validate`. It validates JSON and supported keys, not shortcut syntax, macOS reservation, or focus-context conflicts.
|
||||
- Before assigning `cmd+[` or `cmd+]` to application-scoped actions, warn that they collide with browser Back/Forward unless the browser actions are also changed or unbound.
|
||||
- `unset` clears file-managed overrides for one action. Do not call that a built-in default reset unless Settings UI/UserDefaults values were also reset.
|
||||
|
||||
@@ -9,13 +9,11 @@ Use this skill for any user-facing string change.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- All user-facing strings must be localized.
|
||||
- Use `String(localized: "key.name", defaultValue: "English text")` for Swift/AppKit/SwiftUI strings.
|
||||
- Keys go in `Resources/Localizable.xcstrings` with translations for all supported languages, currently English and Japanese.
|
||||
- Never use bare string literals in SwiftUI `Text()`, `Button()`, alert titles, tooltips, menus, or dialogs.
|
||||
- Localization audit is required for every user-facing change.
|
||||
- `defaultValue`, English fallback text, schema descriptions, or copied English strings do not count as localization.
|
||||
- For localized web/docs content, update every supported message catalog, currently `web/messages/en.json` and `web/messages/ja.json`, plus any localized data structures carrying inline translations.
|
||||
- Every user-facing string is localized. Never a bare string literal in SwiftUI `Text()`, `Button()`, alert titles, tooltips, menus, or dialogs.
|
||||
- Swift/AppKit/SwiftUI: `String(localized: "key.name", defaultValue: "English text")`, with keys in `Resources/Localizable.xcstrings` translated for every supported language (currently English and Japanese).
|
||||
- `defaultValue`, English fallback text, schema descriptions, and copied English strings do not count as localization.
|
||||
- Localized web/docs content updates every supported message catalog (currently `web/messages/en.json` and `web/messages/ja.json`) plus any localized data structures carrying inline translations.
|
||||
- A localization audit is required for every user-facing change.
|
||||
|
||||
## Audit checklist
|
||||
|
||||
@@ -23,15 +21,12 @@ Before finishing a task that changes UI, Settings rows, menus, shortcut metadata
|
||||
|
||||
1. Enumerate the changed user-facing surfaces.
|
||||
2. Verify each surface has entries for every supported locale.
|
||||
3. Parse touched localization files.
|
||||
4. Compare changed message keys across locales.
|
||||
5. Use `rg` over changed Swift/TS/TSX/docs files for newly introduced bare English.
|
||||
6. State the localization audit in the final handoff, or explicitly say what could not be verified.
|
||||
|
||||
## Related shortcut rule
|
||||
|
||||
Every new cmux-owned keyboard shortcut must be added to `KeyboardShortcutSettings`, visible/editable in Settings, supported in `~/.config/cmux/cmux.json`, and documented in the keyboard shortcut and configuration docs.
|
||||
3. Parse the touched localization files and compare changed message keys across locales.
|
||||
4. Run `rg` over changed Swift/TS/TSX/docs files for newly introduced bare English.
|
||||
5. State in the final handoff what audit was performed, or explicitly say what could not be verified.
|
||||
|
||||
## Detailed reference
|
||||
|
||||
- Read [references/audit-workflow.md](references/audit-workflow.md) for a deeper audit process, common false positives, and examples of surfaces that count as user-facing.
|
||||
- [references/audit-workflow.md](references/audit-workflow.md): what counts as user-facing, search patterns, and handoff wording.
|
||||
|
||||
New keyboard shortcuts also need docs and Settings entries; see [../cmux-keyboard-shortcuts/SKILL.md](../cmux-keyboard-shortcuts/SKILL.md).
|
||||
|
||||
@@ -1,68 +1,30 @@
|
||||
# Localization Audit Workflow
|
||||
|
||||
This reference expands the localization rules for cmux.
|
||||
Expands the rules in [../SKILL.md](../SKILL.md).
|
||||
|
||||
## What counts as user-facing
|
||||
|
||||
Treat text as user-facing if it can appear in:
|
||||
SwiftUI views, AppKit menus and dialogs, alerts and confirmation sheets, tooltips and accessibility labels, Settings rows and descriptions, command palette entries, keyboard shortcut metadata, CLI help and command output, JSON schema descriptions shown in docs or editors, docs pages, web UI, and generated configuration examples shown to users.
|
||||
|
||||
- SwiftUI views
|
||||
- AppKit menus and dialogs
|
||||
- alerts and confirmation sheets
|
||||
- tooltips and accessibility labels
|
||||
- Settings rows and descriptions
|
||||
- command palette entries
|
||||
- keyboard shortcut metadata
|
||||
- CLI help or command output
|
||||
- JSON schema descriptions shown in docs or editors
|
||||
- docs pages
|
||||
- web UI
|
||||
- generated configuration examples shown to users
|
||||
Debug-menu and debug-window labels are contributor-facing but still deserve localization when they are visible in the app.
|
||||
|
||||
Internal debug-only labels may still deserve localization if they are visible in the Debug menu or a debug window used by contributors.
|
||||
## Keys across locales
|
||||
|
||||
## Swift and AppKit
|
||||
|
||||
Use:
|
||||
|
||||
```swift
|
||||
String(localized: "key.name", defaultValue: "English text")
|
||||
```
|
||||
|
||||
Update `Resources/Localizable.xcstrings` for all supported languages. Currently that means English and Japanese.
|
||||
|
||||
Do not rely on `defaultValue` as the English localization. It is a fallback and development convenience, not a completed localization entry.
|
||||
|
||||
## Web and docs
|
||||
|
||||
For localized web/docs content, update:
|
||||
|
||||
- `web/messages/en.json`
|
||||
- `web/messages/ja.json`
|
||||
- any localized data structures with inline translations
|
||||
|
||||
Keep keys aligned across locales. A key added only to English is incomplete even if the UI falls back at runtime.
|
||||
A key added only to `web/messages/en.json` is incomplete even though the UI falls back at runtime. Same for a Swift key with a `defaultValue` but no `Resources/Localizable.xcstrings` entry per locale: `defaultValue` is a development convenience, not the English localization.
|
||||
|
||||
## Bare English search
|
||||
|
||||
After changing Swift, TS, TSX, or docs files, search the changed files for newly introduced user-facing English. Useful patterns include:
|
||||
Search the changed files, not the whole tree:
|
||||
|
||||
```bash
|
||||
git diff --name-only -- '*.swift' '*.ts' '*.tsx' '*.md'
|
||||
rg 'Text\\("[A-Z][^"]+"' -- '*.swift'
|
||||
rg 'Button\\("[A-Z][^"]+"' -- '*.swift'
|
||||
rg 'tooltip|alert|title|description|label' -- '*.swift' '*.ts' '*.tsx'
|
||||
rg -g '*.swift' 'Text\("[A-Z][^"]+"'
|
||||
rg -g '*.swift' 'Button\("[A-Z][^"]+"'
|
||||
rg -g '*.swift' -g '*.ts' -g '*.tsx' 'tooltip|alert|title|description|label'
|
||||
```
|
||||
|
||||
These searches are not proof by themselves. They are prompts to inspect likely user-facing strings.
|
||||
These are prompts to inspect likely user-facing strings, not proof on their own.
|
||||
|
||||
## Final handoff
|
||||
|
||||
Every UI/text-affecting final handoff should state:
|
||||
|
||||
- which surfaces changed
|
||||
- which localization files were updated
|
||||
- which audit commands or manual checks were run
|
||||
- anything that could not be verified
|
||||
|
||||
If no user-facing strings changed, say that clearly.
|
||||
State which surfaces changed, which localization files were updated, which audit commands or manual checks ran, and anything that could not be verified. If no user-facing strings changed, say so.
|
||||
|
||||
@@ -5,74 +5,21 @@ description: Open markdown files in a formatted viewer panel with live reload. U
|
||||
|
||||
# Markdown Viewer with cmux
|
||||
|
||||
Use this skill to display markdown files in a dedicated panel with rich formatting and live file watching.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. Write your plan or notes to a `.md` file.
|
||||
2. Open it in a markdown panel.
|
||||
3. The panel auto-updates when the file changes on disk.
|
||||
Write a `.md` file, open it in a panel, and the panel re-renders whenever the file changes on disk. Use it for agent plans and task lists alongside the terminal, documentation and changelogs while working, and notes another process updates progressively.
|
||||
|
||||
```bash
|
||||
# Open a markdown file as a split panel next to the current terminal
|
||||
cmux markdown open plan.md
|
||||
|
||||
# Absolute path
|
||||
cmux markdown open plan.md # split next to the current terminal
|
||||
cmux markdown open /path/to/PLAN.md
|
||||
|
||||
# Target a specific workspace
|
||||
cmux markdown open design.md --workspace workspace:2
|
||||
cmux markdown open design.md --workspace workspace:2 # also --surface, --window
|
||||
```
|
||||
|
||||
## When to Use
|
||||
Relative paths resolve against the caller's cwd and `~` expands; the resolved absolute path comes back in the output.
|
||||
|
||||
- Displaying an agent plan or task list alongside the terminal
|
||||
- Showing documentation, changelogs, or READMEs while working
|
||||
- Reviewing notes that update in real-time (e.g., a plan file being written by another process)
|
||||
## Agent usage
|
||||
|
||||
## Live File Watching
|
||||
Write the full plan file first, then open it, so the panel never shows a partially written file. After that, overwrite or append freely: each write triggers a re-render, and atomic replacement (editor saves, `sed -i`, VS Code) is handled.
|
||||
|
||||
The panel automatically re-renders when the file changes on disk. This works with:
|
||||
|
||||
- Direct writes (`echo "..." >> plan.md`)
|
||||
- Editor saves (vim, nano, VS Code)
|
||||
- Atomic file replacement (write to temp, rename over original)
|
||||
- Agent-generated plan files that are updated progressively
|
||||
|
||||
If the file is deleted, the panel shows a "file unavailable" state. During atomic replace, the panel attempts automatic reconnection within its short retry window. If the file returns later, close and reopen the panel.
|
||||
|
||||
## Agent Integration
|
||||
|
||||
### Opening a plan file
|
||||
|
||||
Write your plan to a file, then open it:
|
||||
|
||||
```bash
|
||||
cat > plan.md << 'EOF'
|
||||
# Task Plan
|
||||
|
||||
## Steps
|
||||
1. Analyze the codebase
|
||||
2. Implement the feature
|
||||
3. Write tests
|
||||
4. Verify the build
|
||||
EOF
|
||||
|
||||
cmux markdown open plan.md
|
||||
```
|
||||
|
||||
### Updating a plan in real-time
|
||||
|
||||
The panel live-reloads, so simply overwrite the file as work progresses:
|
||||
|
||||
```bash
|
||||
# The markdown panel updates automatically when the file changes
|
||||
echo "## Step 1: Complete" >> plan.md
|
||||
```
|
||||
|
||||
### Recommended AGENTS.md instruction
|
||||
|
||||
Add this to your project's `AGENTS.md` to instruct coding agents to use the markdown viewer:
|
||||
To instruct coding agents in a project, add to its `AGENTS.md`:
|
||||
|
||||
```markdown
|
||||
## Plan Display
|
||||
@@ -84,42 +31,13 @@ When creating a plan or task list, write it to a `.md` file and open it in cmux:
|
||||
The panel renders markdown with rich formatting and auto-updates when the file changes.
|
||||
```
|
||||
|
||||
## Routing
|
||||
## Rendering
|
||||
|
||||
```bash
|
||||
# Open in the caller's workspace (default -- uses CMUX_WORKSPACE_ID)
|
||||
cmux markdown open plan.md
|
||||
Headings h1-h6 (dividers on h1/h2), fenced code blocks in monospace, inline code with a highlighted background, tables with alternating row colors, nested ordered and unordered lists, blockquotes with a left border, bold/italic/strikethrough, clickable links, horizontal rules, and inline images. Light and dark mode both supported.
|
||||
|
||||
# Open in a specific workspace
|
||||
cmux markdown open plan.md --workspace workspace:2
|
||||
|
||||
# Open splitting from a specific surface
|
||||
cmux markdown open plan.md --surface surface:5
|
||||
|
||||
# Open in a specific window
|
||||
cmux markdown open plan.md --window window:1
|
||||
```
|
||||
|
||||
## Deep-Dive References
|
||||
## Deep-dive references
|
||||
|
||||
| Reference | When to Use |
|
||||
|-----------|-------------|
|
||||
| [references/commands.md](references/commands.md) | Full command syntax and options |
|
||||
| [references/live-reload.md](references/live-reload.md) | File watching behavior, atomic writes, edge cases |
|
||||
|
||||
## Rendering Support
|
||||
|
||||
The markdown panel renders:
|
||||
|
||||
- Headings (h1-h6) with dividers on h1/h2
|
||||
- Fenced code blocks with monospaced font
|
||||
- Inline code with highlighted background
|
||||
- Tables with alternating row colors
|
||||
- Ordered and unordered lists (nested)
|
||||
- Blockquotes with left border
|
||||
- Bold, italic, strikethrough
|
||||
- Links (clickable)
|
||||
- Horizontal rules
|
||||
- Images (inline)
|
||||
|
||||
Supports both light and dark mode.
|
||||
| [references/commands.md](references/commands.md) | Full command syntax, options, output shape, panel behavior |
|
||||
| [references/live-reload.md](references/live-reload.md) | File watching, atomic writes, unavailable-file state, performance |
|
||||
|
||||
@@ -1,69 +1,29 @@
|
||||
# Command Reference (cmux Markdown)
|
||||
|
||||
## Opening a Markdown Panel
|
||||
|
||||
```bash
|
||||
cmux markdown open <path>
|
||||
cmux markdown <path> # shorthand (implicit "open")
|
||||
cmux markdown <path> # shorthand, "open" is implicit
|
||||
cmux markdown --help
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--workspace <id\|ref\|index>` | Target workspace | `$CMUX_WORKSPACE_ID` |
|
||||
| `--surface <id\|ref\|index>` | Source surface to split from | Focused surface |
|
||||
| `--window <id\|ref>` | Target window | Current window |
|
||||
|
||||
### Output
|
||||
## Output
|
||||
|
||||
```
|
||||
OK surface=surface:8 pane=pane:3 path=/absolute/path/to/file.md
|
||||
```
|
||||
|
||||
With `--json`:
|
||||
`--json` returns `window_id`, `workspace_id`, `pane_id`, `surface_id`, and `path`.
|
||||
|
||||
```json
|
||||
{
|
||||
"window_id": "...",
|
||||
"workspace_id": "...",
|
||||
"pane_id": "...",
|
||||
"surface_id": "...",
|
||||
"path": "/absolute/path/to/file.md"
|
||||
}
|
||||
```
|
||||
## Panel behavior
|
||||
|
||||
## Path Resolution
|
||||
The panel opens as a horizontal split to the right of the source surface. The tab shows the filename and a document icon; the file path appears as a breadcrumb at the top. Content is read-only with text selection enabled.
|
||||
|
||||
- Relative paths are resolved against the caller's current working directory.
|
||||
- `~` is expanded to the home directory.
|
||||
- The resolved absolute path is returned in the output.
|
||||
Markdown panels are saved and restored across sessions and re-read the file from disk on restore. A panel is not recreated if the file no longer exists at restore time.
|
||||
|
||||
```bash
|
||||
# These are equivalent when run from /Users/me/project
|
||||
cmux markdown open plan.md
|
||||
cmux markdown open ./plan.md
|
||||
cmux markdown open /Users/me/project/plan.md
|
||||
```
|
||||
|
||||
## Panel Behavior
|
||||
|
||||
- The panel opens as a **horizontal split** to the right of the source surface.
|
||||
- The tab title shows the filename (e.g., `plan.md`).
|
||||
- The tab icon is a document icon.
|
||||
- Content is **read-only** with text selection enabled.
|
||||
- The file path is displayed as a breadcrumb at the top of the panel.
|
||||
|
||||
## Session Persistence
|
||||
|
||||
Markdown panels are saved and restored across sessions. On restore, the panel re-reads the file from disk. If the file no longer exists at restore time, the panel is not recreated.
|
||||
|
||||
## Help
|
||||
|
||||
```bash
|
||||
cmux markdown --help
|
||||
cmux markdown -h
|
||||
```
|
||||
|
||||
See also:
|
||||
- [live-reload.md](live-reload.md)
|
||||
See also [live-reload.md](live-reload.md).
|
||||
|
||||
@@ -1,53 +1,19 @@
|
||||
# Live Reload Behavior
|
||||
|
||||
The markdown panel watches the file on disk and automatically re-renders when it changes. This enables real-time plan tracking as agents or editors update the file.
|
||||
The panel watches the file with a kernel-level watcher (`DispatchSource` with `O_EVTONLY`) for write, extend, delete, and rename events, and re-renders on change.
|
||||
|
||||
## How It Works
|
||||
## Supported write patterns
|
||||
|
||||
The panel uses a kernel-level file system watcher (`DispatchSource` with `O_EVTONLY`) that monitors the file for:
|
||||
Direct writes (`echo >>`), editor saves, atomic replace (write temp then rename), `sed -i`, VS Code/IDE saves, and progressive agent writes all work. Most of these are atomic replace under the hood.
|
||||
|
||||
- **Write events** -- content was modified in place
|
||||
- **Extend events** -- content was appended
|
||||
- **Delete events** -- file was removed (atomic replace step 1)
|
||||
- **Rename events** -- file was moved or renamed
|
||||
## Atomic file replacement
|
||||
|
||||
## Supported Write Patterns
|
||||
An atomic replace shows up as a delete event followed by a new file at the same path. The panel detects it, re-reads immediately (in case the rename already landed), waits 500 ms and checks again if the file is missing, then reconnects the watcher to the new inode.
|
||||
|
||||
| Pattern | Supported | Notes |
|
||||
|---------|-----------|-------|
|
||||
| Direct write (`echo >>`) | Yes | Triggers write/extend event |
|
||||
| Editor save (vim, nano) | Yes | Most editors use atomic write (see below) |
|
||||
| Atomic replace (write tmp + rename) | Yes | Handled via delete/rename recovery |
|
||||
| `sed -i` | Yes | Uses atomic replace internally |
|
||||
| VS Code / IDE save | Yes | Uses atomic replace |
|
||||
| Agent progressive writes | Yes | Each write triggers a re-render |
|
||||
## File unavailable state
|
||||
|
||||
## Atomic File Replacement
|
||||
|
||||
Many editors and tools write files atomically: write to a temporary file, then rename it over the original. This shows up as a **delete** event followed by a new file appearing at the same path.
|
||||
|
||||
The panel handles this by:
|
||||
|
||||
1. Detecting the delete/rename event
|
||||
2. Attempting to re-read the file immediately (in case the rename already happened)
|
||||
3. If the file is missing, wait 500 ms and check again (the new file may not yet be in place)
|
||||
4. Reconnecting the file watcher to the new inode
|
||||
|
||||
## File Unavailable State
|
||||
|
||||
If the file is deleted and does not reappear within the retry window, the panel shows a "file unavailable" state with the original path. The panel does not close automatically -- the user must close it manually.
|
||||
|
||||
If the file later reappears at the same path (e.g., the user recreates it), the panel does NOT automatically reconnect. Close and reopen the panel to pick up the new file.
|
||||
If the file is deleted and does not reappear within the retry window, the panel shows a "file unavailable" state with the original path and stays open until the user closes it. It does not reconnect if the file later reappears; close and reopen the panel.
|
||||
|
||||
## Performance
|
||||
|
||||
- Re-reads are dispatched to the main thread and run synchronously.
|
||||
- Large files (100KB+) may cause brief UI hitches during re-render. For extremely large markdown files, consider splitting into smaller documents.
|
||||
- The file watcher runs on a low-priority background queue and has negligible CPU impact.
|
||||
|
||||
## Tips for Agents
|
||||
|
||||
- **Write the full plan file first, then open it.** This avoids the panel showing a partially written file.
|
||||
- **Append-style updates work well.** Adding sections to the end of a file triggers a smooth re-render.
|
||||
- **Overwriting the entire file is fine.** The atomic replace handling ensures no data is lost.
|
||||
- **Don't delete and recreate rapidly.** If writing a new version, prefer overwriting in place or using atomic replacement.
|
||||
Re-reads are dispatched to the main thread and run synchronously, so files over ~100KB can cause brief UI hitches during re-render; split very large documents. The watcher itself runs on a low-priority background queue with negligible CPU impact.
|
||||
|
||||
@@ -5,34 +5,22 @@ description: "cmux release workflow, version bumping, changelog updates, pretag
|
||||
|
||||
# cmux Release
|
||||
|
||||
Use the `/release` command to prepare a new release. This will:
|
||||
Prefer the `/release` command. It determines the new version (minor by default), gathers commits since the last tag, updates `CHANGELOG.md`, runs `./scripts/bump-version.sh`, commits, runs `./scripts/release-pretag-guard.sh`, then tags and pushes.
|
||||
|
||||
1. Determine the new version (bumps minor by default)
|
||||
2. Gather commits since the last tag and update the changelog
|
||||
3. Update `CHANGELOG.md` (the docs changelog page at `web/app/docs/changelog/page.tsx` reads from it)
|
||||
4. Run `./scripts/bump-version.sh` to update both versions
|
||||
5. Commit, run `./scripts/release-pretag-guard.sh`, tag, and push
|
||||
The docs changelog page at `web/app/[locale]/(landing)/docs/changelog/page.tsx` renders from `CHANGELOG.md`, so there is no separate docs changelog source to update.
|
||||
|
||||
## Version bumping
|
||||
|
||||
```bash
|
||||
./scripts/bump-version.sh
|
||||
./scripts/bump-version.sh patch
|
||||
./scripts/bump-version.sh major
|
||||
./scripts/bump-version.sh 1.0.0
|
||||
./scripts/bump-version.sh # minor (0.15.0 -> 0.16.0)
|
||||
./scripts/bump-version.sh patch # 0.15.0 -> 0.15.1
|
||||
./scripts/bump-version.sh major # 0.15.0 -> 1.0.0
|
||||
./scripts/bump-version.sh 1.0.0 # explicit version
|
||||
```
|
||||
|
||||
This updates both `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`. The build number is auto-incremented and is required for Sparkle auto-update to work.
|
||||
This updates `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`. The build number auto-increments and must increase for Sparkle auto-update to work. Bump the minor version unless explicitly asked otherwise.
|
||||
|
||||
Before creating a release tag, run:
|
||||
|
||||
```bash
|
||||
./scripts/release-pretag-guard.sh
|
||||
```
|
||||
|
||||
If it fails, run `./scripts/bump-version.sh`, commit the build-number bump, then retry tagging.
|
||||
|
||||
Manual release steps if not using the command:
|
||||
## Tagging
|
||||
|
||||
```bash
|
||||
./scripts/release-pretag-guard.sh
|
||||
@@ -41,14 +29,13 @@ git push origin vX.Y.Z
|
||||
gh run watch --repo manaflow-ai/cmux
|
||||
```
|
||||
|
||||
## Notes
|
||||
If the pretag guard fails, run `./scripts/bump-version.sh`, commit the build-number bump, then retry.
|
||||
|
||||
- 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`.
|
||||
- Bump the minor version for updates unless explicitly asked otherwise.
|
||||
- Update `CHANGELOG.md`; docs changelog is rendered from it.
|
||||
## Release artifacts and secrets
|
||||
|
||||
- The release asset is `cmux-macos.dmg`, attached to the tag. The README download button points to `releases/latest/download/cmux-macos.dmg`.
|
||||
- Signing and notarization require the GitHub secrets `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`.
|
||||
|
||||
## Detailed reference
|
||||
|
||||
- Read [references/release-checklist.md](references/release-checklist.md) for a more detailed release checklist and common failure handling.
|
||||
- [references/release-checklist.md](references/release-checklist.md): changelog tone, failure triage, and asset-rename fallout.
|
||||
|
||||
@@ -1,77 +1,21 @@
|
||||
# Release Checklist
|
||||
|
||||
This reference expands the cmux release workflow.
|
||||
|
||||
## Default path
|
||||
|
||||
Prefer the `/release` command. It should handle:
|
||||
|
||||
- choosing the version
|
||||
- gathering commits since the last tag
|
||||
- updating `CHANGELOG.md`
|
||||
- running `./scripts/bump-version.sh`
|
||||
- committing release metadata
|
||||
- running `./scripts/release-pretag-guard.sh`
|
||||
- tagging and pushing
|
||||
The command sequence lives in [../SKILL.md](../SKILL.md). This covers judgment calls and failure triage.
|
||||
|
||||
## Version policy
|
||||
|
||||
Use a minor bump by default. Use patch or major only when explicitly requested or clearly justified by the release scope.
|
||||
|
||||
The version bump script updates both:
|
||||
|
||||
- `MARKETING_VERSION`
|
||||
- `CURRENT_PROJECT_VERSION`
|
||||
|
||||
The build number must increase for Sparkle auto-update. If `release-pretag-guard.sh` fails because the build number is not monotonic, run the bump script, commit the build-number bump, and retry the guard.
|
||||
Minor bump by default. Patch or major only when explicitly requested or clearly justified by the release scope.
|
||||
|
||||
## Changelog
|
||||
|
||||
Update `CHANGELOG.md`. The docs changelog page at `web/app/docs/changelog/page.tsx` renders from it, so do not update a separate docs changelog source.
|
||||
Keep `CHANGELOG.md` user-facing: user-visible fixes, behavior changes, and compatibility notes rank above internal refactors.
|
||||
|
||||
Keep the changelog user-facing. Mention user-visible fixes, behavior changes, and compatibility notes more prominently than internal refactors.
|
||||
## Failure triage
|
||||
|
||||
## Tagging
|
||||
- `release-pretag-guard.sh` fails on a non-monotonic build number: run `./scripts/bump-version.sh`, commit the bump, retry.
|
||||
- Release automation fails **before** signing: inspect workflow configuration and version metadata.
|
||||
- Release automation fails **during** signing or notarization: inspect secret availability and Apple account status.
|
||||
|
||||
Run before tagging:
|
||||
## Asset rename
|
||||
|
||||
```bash
|
||||
./scripts/release-pretag-guard.sh
|
||||
```
|
||||
|
||||
Manual tag flow:
|
||||
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
gh run watch --repo manaflow-ai/cmux
|
||||
```
|
||||
|
||||
## Release asset
|
||||
|
||||
The expected release asset is:
|
||||
|
||||
```text
|
||||
cmux-macos.dmg
|
||||
```
|
||||
|
||||
The README download button points to:
|
||||
|
||||
```text
|
||||
releases/latest/download/cmux-macos.dmg
|
||||
```
|
||||
|
||||
If the asset name changes, update every surface that assumes this path.
|
||||
|
||||
## Required secrets
|
||||
|
||||
Release signing/notarization depends on:
|
||||
|
||||
- `APPLE_CERTIFICATE_BASE64`
|
||||
- `APPLE_CERTIFICATE_PASSWORD`
|
||||
- `APPLE_SIGNING_IDENTITY`
|
||||
- `APPLE_ID`
|
||||
- `APPLE_APP_SPECIFIC_PASSWORD`
|
||||
- `APPLE_TEAM_ID`
|
||||
|
||||
If release automation fails before signing, inspect workflow configuration and version metadata first. If it fails during signing/notarization, inspect the secret availability and Apple account status.
|
||||
If `cmux-macos.dmg` is ever renamed, update every surface that assumes the `releases/latest/download/cmux-macos.dmg` path (README, website, updater feed, Homebrew formula).
|
||||
|
||||
@@ -5,25 +5,20 @@ description: "View and edit cmux settings in ~/.config/cmux/cmux.json. Use when
|
||||
|
||||
# cmux-settings
|
||||
|
||||
cmux reads user settings from `~/.config/cmux/cmux.json` (JSONC). The app installs a file watcher; saving the file applies changes immediately, no restart needed. Legacy `~/.config/cmux/settings.json` is read only as a fallback for keys not present in `cmux.json`.
|
||||
cmux reads user settings from `~/.config/cmux/cmux.json` (JSONC). A file watcher applies changes on save, no restart. Legacy `~/.config/cmux/settings.json` is read only as a fallback for keys absent from `cmux.json`.
|
||||
|
||||
Schema: `https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json`. The authoritative path list lives in `Sources/CmuxSettingsJSONPathSupport.swift` in the cmux checkout, and the installed skill includes a generated copy in `references/all-keys.md`. Top-level sections are `app`, `terminal`, `notifications`, `sidebar`, `sidebarAppearance`, `workspaceColors`, `automation`, `browser`, and `shortcuts`. Non-settings sections (`actions`, `ui`, `commands`, `vault`, `rightSidebar`) coexist in the same file.
|
||||
Schema: `https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json`. The authoritative path list is `Sources/CmuxSettingsJSONPathSupport.swift`; the installed skill carries a generated copy in `references/all-keys.md`. Settings sections are `app`, `terminal`, `notifications`, `sidebar`, `sidebarAppearance`, `workspaceColors`, `automation`, `browser`, `shortcuts`. Non-settings sections (`actions`, `ui`, `commands`, `vault`, `rightSidebar`) share the same file.
|
||||
|
||||
## Helper script
|
||||
|
||||
Use the bundled helper for every read/write. It strips JSONC comments, writes atomically, and validates keys against the schema.
|
||||
|
||||
```bash
|
||||
# From a cmux checkout
|
||||
skills/cmux-settings/scripts/cmux-settings <subcommand>
|
||||
|
||||
# From an installed Codex skill
|
||||
~/.codex/skills/cmux-settings/scripts/cmux-settings <subcommand>
|
||||
skills/cmux-settings/scripts/cmux-settings <subcommand> # from a cmux checkout
|
||||
~/.codex/skills/cmux-settings/scripts/cmux-settings <subcommand> # installed Codex skill
|
||||
```
|
||||
|
||||
For brevity in the rest of this doc, assume the script is on `$PATH` as `cmux-settings`. To make it so for a session from a checkout: `export PATH="$PWD/skills/cmux-settings/scripts:$PATH"`.
|
||||
|
||||
Subcommands:
|
||||
The rest of this doc assumes it is on `$PATH` as `cmux-settings`; from a checkout, `export PATH="$PWD/skills/cmux-settings/scripts:$PATH"`.
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
@@ -31,53 +26,48 @@ Subcommands:
|
||||
| `cmux-settings dump` | Print the raw file (preserves comments). |
|
||||
| `cmux-settings dump --no-comments` | Print the parsed JSON. |
|
||||
| `cmux-settings get <a.b.c>` | Print value at dotted JSON path. |
|
||||
| `cmux-settings set <a.b.c> <value>` | Set value. `<value>` is parsed as JSON (`true`, `42`, `"text"`, `[…]`, `{…}`); plain strings without quotes are stored as strings. |
|
||||
| `cmux-settings set <a.b.c> <value>` | Set value. `<value>` is parsed as JSON (`true`, `42`, `"text"`, `[…]`, `{…}`); unquoted plain words are stored as strings. |
|
||||
| `cmux-settings unset <a.b.c>` | Delete key, reverting to the in-app default. |
|
||||
| `cmux-settings list-supported` | List every settings JSON path the app recognizes. |
|
||||
| `cmux-settings validate` | Parse the file and flag any unknown settings keys. |
|
||||
| `cmux-settings validate` | Parse the file and flag unknown settings keys. |
|
||||
| `cmux-settings open` | Open `cmux.json` in `$EDITOR`, VS Code, Cursor, or TextEdit. |
|
||||
|
||||
`--file <path>` overrides the target file (useful for `--file ~/.config/cmux/settings.json` when the user keeps things in the legacy file).
|
||||
`--file <path>` overrides the target file, useful for `--file ~/.config/cmux/settings.json`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the change. If the user named a setting in plain English (e.g. "make the sidebar tint match the terminal background"), look it up first.
|
||||
1. Look up the key when the user named a setting in plain English:
|
||||
```bash
|
||||
cmux-settings list-supported | rg -i 'sidebar.*terminal|terminal.*sidebar'
|
||||
```
|
||||
2. Set the value. JSON literals (`true`, `false`, numbers, arrays, objects) must be valid JSON. Plain words are stored as strings.
|
||||
2. Set it. JSON literals must be valid JSON.
|
||||
```bash
|
||||
cmux-settings set sidebarAppearance.matchTerminalBackground true
|
||||
cmux-settings set app.appearance dark
|
||||
cmux-settings set shortcuts.bindings.toggleSidebar cmd+b
|
||||
cmux-settings set shortcuts.bindings.newTab '["ctrl+b","c"]'
|
||||
cmux-settings set browser.hostsToOpenInEmbeddedBrowser '["localhost","*.internal.example"]'
|
||||
```
|
||||
3. Verify by reading back and validating.
|
||||
```bash
|
||||
cmux-settings get sidebarAppearance.matchTerminalBackground
|
||||
cmux-settings validate
|
||||
```
|
||||
4. Tell the user it auto-reloaded. No app restart. If they want to revert, run `cmux-settings unset <key>`.
|
||||
3. Read back and `cmux-settings validate`.
|
||||
4. Tell the user it auto-reloaded, and that `cmux-settings unset <key>` reverts it.
|
||||
|
||||
## Quick reference
|
||||
|
||||
- Appearance: `app.appearance` = `"system" | "light" | "dark"`, `app.appIcon`, `app.menuBarOnly`, `app.minimalMode`.
|
||||
- Sidebar tint: `sidebarAppearance.matchTerminalBackground`, `sidebarAppearance.tintColor`, `sidebarAppearance.tintOpacity` (0..1).
|
||||
- Sidebar details: `sidebar.hideAllDetails`, `sidebar.showBranchDirectory`, `sidebar.showPullRequests`, `sidebar.showPorts`, `sidebar.showLog`.
|
||||
- Notifications: `notifications.dockBadge`, `notifications.sound` (enum incl. `"none"`, `"custom_file"`), `notifications.customSoundFilePath`, `notifications.hooks` (array).
|
||||
- Browser: `browser.defaultSearchEngine`, `browser.theme`, `browser.openTerminalLinksInCmuxBrowser`, `browser.hostsToOpenInEmbeddedBrowser`.
|
||||
- Automation: `automation.socketControlMode` (`off | cmuxOnly | automation | password | allowAll`), `automation.portBase`, `automation.portRange`.
|
||||
- Shortcuts: `shortcuts.bindings.<actionId>` = `"cmd+b"`, `["ctrl+b","c"]`, `null`, or `""` to unbind. See `references/shortcut-actions.md`.
|
||||
- Appearance: `app.appearance` (`"system" | "light" | "dark"`), `app.appIcon`, `app.menuBarOnly`, `app.minimalMode`.
|
||||
- Sidebar tint: `sidebarAppearance.matchTerminalBackground`, `.tintColor`, `.tintOpacity` (0..1).
|
||||
- Sidebar details: `sidebar.hideAllDetails`, `.showBranchDirectory`, `.showPullRequests`, `.showPorts`, `.showLog`.
|
||||
- Notifications: `notifications.dockBadge`, `.sound` (enum including `"none"`, `"custom_file"`), `.customSoundFilePath`, `.hooks` (array).
|
||||
- Browser: `browser.defaultSearchEngine`, `.theme`, `.openTerminalLinksInCmuxBrowser`, `.hostsToOpenInEmbeddedBrowser`.
|
||||
- Automation: `automation.socketControlMode` (`off | cmuxOnly | automation | password | allowAll`), `.portBase`, `.portRange`.
|
||||
- Shortcuts: `shortcuts.bindings.<actionId>` = `"cmd+b"`, `["ctrl+b","c"]`, `null`, or `""` to unbind. Action ids in [references/shortcut-actions.md](references/shortcut-actions.md).
|
||||
|
||||
For the full list of settings, defaults, and descriptions, run `cmux-settings list-supported` or read [references/all-keys.md](references/all-keys.md).
|
||||
Full list of settings, defaults, and descriptions: `cmux-settings list-supported` or [references/all-keys.md](references/all-keys.md).
|
||||
|
||||
## Rules
|
||||
|
||||
- Only edit `cmux.json`. Never edit `settings.json` unless the user explicitly asks; it is legacy and only read when the key is absent from `cmux.json`.
|
||||
- Never tell the user to restart cmux to apply a change. The file watcher reloads on save.
|
||||
- Always validate after a bulk edit: `cmux-settings validate`. Unknown keys mean the user pasted a key the app does not consume.
|
||||
- Do not blindly overwrite top-level sections (`actions`, `ui`, `commands`, `vault`, `rightSidebar`). They live in the same file and contain non-settings config the user has hand-tuned.
|
||||
- Shortcut action ids must match the schema enum. Look them up in [references/shortcut-actions.md](references/shortcut-actions.md) before binding.
|
||||
- Color values must be `#RRGGBB`. Opacities are `0..1`.
|
||||
- For settings the user expressed in app-level language (e.g. "Settings > Notifications > Dock badge"), translate to the matching JSON path first; the docs page at `web/app/[locale]/docs/configuration/page.tsx` mirrors the schema 1:1.
|
||||
- Only edit `cmux.json`. Never `settings.json` unless the user explicitly asks; it is legacy and read only when a key is absent from `cmux.json`.
|
||||
- Never tell the user to restart cmux. The file watcher reloads on save.
|
||||
- Always `cmux-settings validate` after a bulk edit. Unknown keys mean the user pasted a key the app does not consume.
|
||||
- Do not blindly overwrite `actions`, `ui`, `commands`, `vault`, or `rightSidebar`; they share the file and hold hand-tuned non-settings config.
|
||||
- Shortcut action ids must match the schema enum. Look them up before binding.
|
||||
- Colors are `#RRGGBB`; opacities are `0..1`.
|
||||
- Translate app-level phrasing ("Settings > Notifications > Dock badge") to the JSON path first; `web/app/[locale]/(landing)/docs/configuration/page.tsx` mirrors the schema 1:1.
|
||||
|
||||
@@ -5,33 +5,13 @@ description: "Shared behavior and mutation-path rules for cmux. Use when a behav
|
||||
|
||||
# cmux Shared Behavior
|
||||
|
||||
Use one shared action/model path when behavior is exposed through multiple entrypoints.
|
||||
|
||||
## Shared entrypoints
|
||||
|
||||
When a behavior is exposed through multiple surfaces, implement one shared action/model path and verify every entrypoint that should invoke it.
|
||||
|
||||
Common entrypoints include:
|
||||
|
||||
- keyboard shortcut
|
||||
- command palette
|
||||
- context menu
|
||||
- CLI/socket command
|
||||
- settings UI
|
||||
- debug menu
|
||||
|
||||
Do not patch one surface while leaving the others with duplicated logic.
|
||||
When a behavior is exposed through multiple surfaces (keyboard shortcut, command palette, context menu, CLI/socket command, settings UI, debug menu), implement one shared action/model path and verify every entrypoint that should invoke it. Do not patch one surface and leave the others with duplicated logic.
|
||||
|
||||
## Optimistic updates
|
||||
|
||||
For optimistic UI or CLI updates:
|
||||
|
||||
- keep one mutation path
|
||||
- record pending state with a request id or previous snapshot
|
||||
- reconcile from the authoritative result
|
||||
- handle failure with an explicit rollback or error state
|
||||
|
||||
Do not let each entrypoint maintain its own optimistic copy.
|
||||
Keep one mutation path. Record pending state with a request id or a previous snapshot, reconcile from the authoritative result, and handle failure with an explicit rollback or error state. Never let each entrypoint maintain its own optimistic copy.
|
||||
|
||||
## Missed-bug coverage
|
||||
|
||||
|
||||
@@ -1,60 +1,17 @@
|
||||
# Socket Threading and Focus
|
||||
|
||||
Socket commands are a control plane. They often run because an agent, script, or background tool is reporting state, not because a user asked the app to become active.
|
||||
Socket commands are a control plane. They usually run because an agent, script, or background tool is reporting state, not because a user asked the app to become active. The rules are in [../SKILL.md](../SKILL.md); this is the reasoning.
|
||||
|
||||
## Telemetry hot paths
|
||||
## Why telemetry stays off-main
|
||||
|
||||
High-frequency telemetry commands include:
|
||||
`DispatchQueue.main.sync` blocks the socket handling path behind UI work and can deadlock when the command path is already main-adjacent. High-frequency commands (`report_*`, `ports_kick`, status, progress, log metadata) parse and validate off-main, dedupe and coalesce before crossing to UI state, then schedule only the smallest required mutation.
|
||||
|
||||
- `report_*`
|
||||
- `ports_kick`
|
||||
- status updates
|
||||
- progress updates
|
||||
- log metadata updates
|
||||
## When main actor is justified
|
||||
|
||||
These should avoid synchronous main-thread work. Parse and validate arguments off-main, dedupe/coalesce before crossing to UI state, and schedule only the smallest required mutation.
|
||||
|
||||
`DispatchQueue.main.sync` is especially risky because it can block the socket handling path behind UI work and can deadlock if the command path is already main-adjacent.
|
||||
|
||||
## Commands allowed on main actor
|
||||
|
||||
Commands that directly manipulate AppKit or Ghostty UI state may need main actor execution:
|
||||
|
||||
- focus
|
||||
- select
|
||||
- open/close UI surfaces
|
||||
- send key/input
|
||||
- list/current queries requiring an exact synchronous UI snapshot
|
||||
|
||||
The command should document why main-thread execution is necessary. Do not cargo-cult main actor isolation onto telemetry commands.
|
||||
Commands that directly manipulate AppKit or Ghostty UI state may need it: focus, select, open/close UI surfaces, send key/input, and list/current queries that require an exact synchronous UI snapshot. Document why in the command. Do not cargo-cult main actor isolation onto telemetry commands.
|
||||
|
||||
## Focus preservation
|
||||
|
||||
Most socket commands should not change the user's macOS focus. A background agent may be running in one workspace while the user is actively using another app or cmux workspace.
|
||||
A background agent may be working in one workspace while the user is in another app or workspace. A non-focus command applies model and data changes without activating the app, raising a window, selecting another workspace, or focusing a pane or surface.
|
||||
|
||||
Non-focus commands should apply model/data changes without:
|
||||
|
||||
- activating the app
|
||||
- raising a window
|
||||
- selecting another workspace
|
||||
- focusing a pane
|
||||
- focusing a surface
|
||||
|
||||
If a command needs focus behavior, name and document it as focus-intent.
|
||||
|
||||
## Explicit focus-intent commands
|
||||
|
||||
Only explicit focus-intent commands may mutate in-app focus/selection. Examples:
|
||||
|
||||
- `window.focus`
|
||||
- `workspace.select`
|
||||
- `workspace.next`
|
||||
- `workspace.previous`
|
||||
- `workspace.last`
|
||||
- `surface.focus`
|
||||
- `pane.focus`
|
||||
- `pane.last`
|
||||
- browser focus commands
|
||||
- v1 focus equivalents
|
||||
|
||||
When adding a new command, decide whether it is focus-intent as part of the API contract, not as an implementation accident.
|
||||
Decide whether a new command is focus-intent as part of its API contract, not as an implementation accident.
|
||||
|
||||
@@ -1,108 +1,52 @@
|
||||
---
|
||||
name: cmux-testing
|
||||
description: "cmux testing rules for Swift Testing, test target compilation, and package/refactor validation. Use when adding or changing tests, touching package/refactor code, or deciding whether reload.sh is enough validation."
|
||||
description: "cmux testing rules for Swift Testing, test target compilation, test wiring, and package/refactor validation. Use when adding or changing tests, touching package/refactor code, or deciding whether reload.sh is enough validation."
|
||||
---
|
||||
|
||||
# cmux Testing
|
||||
|
||||
## Regression test commit policy
|
||||
|
||||
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
|
||||
A regression test for a bug fix ships as two commits so CI proves the test catches the bug:
|
||||
|
||||
1. **Commit 1:** Add the failing test only (no fix). CI should go red.
|
||||
2. **Commit 2:** Add the fix. CI should go green.
|
||||
1. The failing test only, no fix. CI goes red.
|
||||
2. The fix. CI goes green.
|
||||
|
||||
This makes it visible in the GitHub PR UI that the test genuinely fails without the fix.
|
||||
The GitHub PR Commits tab then shows the test genuinely fails without the fix.
|
||||
|
||||
## Test wiring
|
||||
|
||||
Test files in `cmuxTests/` must be wired into `cmux.xcodeproj/project.pbxproj` with a matching `PBXFileReference` and `PBXSourcesBuildPhase` entry. A `.swift` file added without them is silently ignored by Xcode: `xcodebuild test -only-testing:cmuxTests/<TestClass>` and bot reviews both pass with "Executed 0 tests", so the missing wiring is indistinguishable from a clean red/green regression test until a real user hits the bug. Surfaced during https://github.com/manaflow-ai/cmux/issues/4529 against https://github.com/manaflow-ai/cmux/pull/4536.
|
||||
|
||||
The `workflow-guard-tests` CI job runs `./scripts/lint-pbxproj-test-wiring.sh`. Add the file through Xcode (drag into the cmuxTests target) or hand-edit the pbxproj entries using a wired sibling such as `cmuxTests/TabManagerUnitTests.swift` as the template.
|
||||
|
||||
## Test quality policy
|
||||
|
||||
- Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns.
|
||||
- Do not add tests that read checked-in metadata or project files such as `Resources/Info.plist`, `project.pbxproj`, `.xcconfig`, or source files only to assert that a key, string, plist entry, or snippet exists.
|
||||
- Tests must verify observable runtime behavior through executable paths (unit/integration/e2e/CLI), not implementation shape.
|
||||
- For metadata changes, prefer verifying the built app bundle or the runtime behavior that depends on that metadata, not the checked-in source file.
|
||||
- If a behavior cannot be exercised end-to-end yet, add a small runtime seam or harness first, then test through that seam.
|
||||
- If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and state that explicitly.
|
||||
- No tests that only verify source text, method signatures, AST fragments, or grep-style patterns.
|
||||
- No tests that read checked-in metadata or project files (`Resources/Info.plist`, `project.pbxproj`, `.xcconfig`, source files) just to assert a key, string, plist entry, or snippet exists.
|
||||
- Tests verify observable runtime behavior through executable paths (unit, integration, e2e, CLI), not implementation shape.
|
||||
- For metadata changes, verify the built app bundle or the runtime behavior that depends on the metadata.
|
||||
- If a behavior cannot be exercised end to end yet, add a small runtime seam or harness first, then test through it.
|
||||
- If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and say so.
|
||||
|
||||
## Test framework
|
||||
|
||||
Swift Testing is the current Apple-supported primitive for tests on this codebase (shipped with Swift 6 / Xcode 16, supported on the macOS versions we target). Use it for everything that is not a UI test.
|
||||
Swift Testing (Swift 6 / Xcode 16) is the default for every unit and integration test: `import Testing`, `@Test`, `@Suite`, `#expect(...)`, `try #require(...)`. Do not write new `import XCTest` tests except UI tests.
|
||||
|
||||
- **Default to Swift Testing for all unit and integration tests.** `import Testing`, annotate tests with `@Test`, group with `@Suite`, assert with `#expect(...)` and `try #require(...)`. Do not write new tests with `import XCTest` unless they are UI tests.
|
||||
- **UI tests stay on XCTest / XCUITest.** Swift Testing does not support UI testing (no `XCUIApplication` integration). Files under `cmuxUITests/` continue to use `XCTestCase` + `XCUIApplication`. Do not migrate them and do not try to bridge Swift Testing into UI tests.
|
||||
- **New test targets start on Swift Testing.** Every new Swift package's `Tests/<Name>Tests/` directory (e.g. `Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/`) should ship with Swift Testing from the first commit. Xcode 16 auto-detects the framework based on the `import Testing` statement; no extra `Package.swift` configuration is required.
|
||||
- **Migration guide when touching an existing XCTest test.** Convert in place: `XCTestCase` subclass becomes a `@Suite struct` (or `final class` if you need a reference type); each `func testFoo()` becomes `@Test func foo()`; `XCTAssertEqual(a, b)` becomes `#expect(a == b)`; `XCTAssertTrue(cond)` becomes `#expect(cond)`; `XCTUnwrap(x)` becomes `try #require(x)`; `XCTFail("msg")` becomes `Issue.record("msg")`. `setUp()` becomes `init()` on the suite; `tearDown()` becomes `deinit`. Async setup is `async init()`. Do not bulk-rewrite untouched tests; migrate incrementally as a side effect of editing the file.
|
||||
- **Parameterized tests** use `@Test(arguments: [...])`. Prefer this over duplicate test methods.
|
||||
- **Parallelization and shared state.** Swift Testing runs tests in parallel by default, including across suites. If a suite genuinely needs ordering or guards shared mutable state, annotate it with `.serialized` instead of adding locks or sleeps.
|
||||
- **Tags** with `@Test(.tags(.something))` (or on a `@Suite`) let CI and local runs filter selectively.
|
||||
- **UI tests stay on XCTest/XCUITest.** Swift Testing has no `XCUIApplication` integration. Files under `cmuxUITests/` keep `XCTestCase`; do not migrate or bridge them.
|
||||
- **New test targets start on Swift Testing.** Every new package's `Tests/<Name>Tests/` ships with it from the first commit; Xcode 16 auto-detects the framework from `import Testing` with no `Package.swift` configuration.
|
||||
- **Parameterized tests** use `@Test(arguments: [...])` instead of duplicate methods.
|
||||
- **Parallelization.** Swift Testing runs tests in parallel by default, including across suites. A suite that needs ordering or guards shared mutable state gets `.serialized`, not locks or sleeps.
|
||||
- **Tags** via `@Test(.tags(.something))` let CI and local runs filter selectively.
|
||||
- Migrate an existing XCTest file in place only when an edit already crosses it. Mapping in [references/swift-testing-migration.md](references/swift-testing-migration.md).
|
||||
|
||||
## Test target validation
|
||||
|
||||
`reload.sh` does not compile the test target. It builds only the `cmux` scheme, so a green `reload.sh` says nothing about whether `cmuxTests`/`cmuxUITests` still compile. A symbol that is moved or renamed can keep the `cmux` app building while breaking the test target (real case: a `write(to:atomically:)` typo and a removed `TabManager.CommandResult` only surfaced in the `tests` job). Before pushing package/refactor changes, build the `cmux-unit` scheme (with `-derivedDataPath /tmp/cmux-<tag>` and, for `cmuxApp`/`AppDelegate` churn, the GlobalISel workaround flag) or let the `tests` CI job gate it — never treat `reload.sh` alone as proof the tests build.
|
||||
|
||||
## Remote-tmux live layout fuzz
|
||||
|
||||
The remote-tmux mirror has a live fuzz: the real app mirroring a real tmux
|
||||
server, driven with random layouts and churn, judged at settle by two
|
||||
oracles — sizing (claims, plans, and rendered grids agree, settle within
|
||||
budget) and content (each pane's `read-screen`, unwrapped, matches
|
||||
`tmux capture-pane -J`). Seeds are deterministic: the same seed replays the
|
||||
same op sequence, so "seed 3, iteration 1" in a commit message is a
|
||||
complete repro recipe.
|
||||
|
||||
Everything runs against a local fixture, on any machine, with no real
|
||||
network and no MFA.
|
||||
|
||||
Use the dedicated fuzz alias `cmux-fuzzhost`, and stand it up first:
|
||||
|
||||
```
|
||||
scripts/remote-tmux-fuzz-host.sh cmux-fuzzhost # loopback-only sshd, isolated tmux
|
||||
CMUX_TAG=<tag> scripts/remote-tmux-fuzz-marathon.sh cmux-fuzzhost [seeds] [iters]
|
||||
```
|
||||
|
||||
The host script generates a loopback sshd whose logins land in an isolated
|
||||
`TMUX_TMPDIR` the harness owns, so it can create and kill that tmux lab
|
||||
freely. Use `cmux-fuzzhost` — **not** `cmux-srvA`/`cmux-srvB`. Those are the
|
||||
render-harness/interactive loopback aliases: their `/tmp/cmux-srv*` holds a
|
||||
live interactive tmux the fuzz harness refuses to clobber, and their tmux
|
||||
dir isn't where the app's `ssh-tmux` connects, so the mirror comes up empty.
|
||||
|
||||
`scripts/remote-tmux-live-fuzz.sh cmux-fuzzhost <seed> <iters>` replays one
|
||||
seed against a running tagged app — the way to reproduce a specific
|
||||
commit's failure. Seeds are deterministic, so "seed 3, iteration 1" is a
|
||||
complete repro.
|
||||
|
||||
Run it on a quiet machine and treat load as part of the result: settle
|
||||
budgets are latency assertions, and a loaded box manufactures failures that
|
||||
read like code bugs.
|
||||
|
||||
**Run it once and let it finish.** Launch in the background (or a plain
|
||||
terminal) and wait — never inside a tmux session (the per-seed reset runs
|
||||
`tmux kill-server`, which inside tmux hits your default server), and don't
|
||||
kill the wrapper mid-run: that orphans the driver, which then blocks the
|
||||
next run. Both scripts allow only one driver at a time.
|
||||
|
||||
Setup failures and their fixes (the message tells you which):
|
||||
|
||||
- `no workspace mirroring session 'fuzz'` — wrong host. The fuzz session's
|
||||
tmux dir isn't where `ssh-tmux <alias>` connects, so the app mirrored the
|
||||
default shell instead. Use `cmux-fuzzhost`.
|
||||
- `refusing to kill an unowned lab` — a stale lab tmux from an aborted run
|
||||
or a manual `ssh cmux-fuzzhost` probe. Kill it scoped to that dir:
|
||||
`TMUX_TMPDIR=<host's fuzz tmux dir> tmux kill-server` (never a bare
|
||||
`kill-server`).
|
||||
- `another fuzz driver (pid N) is running` — a prior or orphaned driver
|
||||
still holds the lock. `pkill -9 -f remote-tmux-fuzz-marathon.sh;
|
||||
pkill -9 -f remote-tmux-live-fuzz.sh`, then remove the
|
||||
`cmux-fuzz-marathon.lock` directory under the temp root.
|
||||
- ssh to the alias shows `REMOTE HOST IDENTIFICATION HAS CHANGED` or
|
||||
`no such identity` — the host script was re-run and regenerated the
|
||||
sshd host key / relocated the client key. Clear the stale host key with
|
||||
`ssh-keygen -R "[127.0.0.1]:<port>"`, and make sure the alias's
|
||||
`IdentityFile` points at the key the script actually wrote.
|
||||
`reload.sh` builds only the `cmux` scheme, so a green reload says nothing about whether `cmuxTests`/`cmuxUITests` still compile. A moved or renamed symbol can keep the app building while breaking the test target (real case: a `write(to:atomically:)` typo and a removed `TabManager.CommandResult` surfaced only in the `tests` job). Before pushing package/refactor changes, build the `cmux-unit` scheme with `-derivedDataPath /tmp/cmux-<tag>` (plus the GlobalISel workaround flag for `cmuxApp`/`AppDelegate` churn), or let the `tests` CI job gate it.
|
||||
|
||||
## Detailed references
|
||||
|
||||
- Read [references/swift-testing-migration.md](references/swift-testing-migration.md) when converting XCTest unit tests to Swift Testing or adding new package tests.
|
||||
- Read [references/regression-and-quality.md](references/regression-and-quality.md) when adding a regression test, deciding whether a test is behavioral enough, or checking Xcode project test wiring.
|
||||
- Read [references/local-vs-ci-validation.md](references/local-vs-ci-validation.md) when choosing between `reload.sh`, `cmux-unit`, GitHub Actions, E2E/UI tests, and Python socket tests.
|
||||
- Read [references/remote-tmux-sizing-e2e.md](references/remote-tmux-sizing-e2e.md) when working on remote-tmux mirror sizing, the sizing UI-test suite, its ssh shim, or the `remote.tmux.pane_grids` / `remote.tmux.test_exec` debug verbs.
|
||||
- [references/swift-testing-migration.md](references/swift-testing-migration.md): XCTest to Swift Testing conversion mapping.
|
||||
- [references/regression-and-quality.md](references/regression-and-quality.md): deciding whether a test is behavioral enough.
|
||||
- [references/local-vs-ci-validation.md](references/local-vs-ci-validation.md): choosing between `reload.sh`, `cmux-unit`, GitHub Actions, E2E/UI tests, and Python socket tests.
|
||||
- [references/remote-tmux-sizing-e2e.md](references/remote-tmux-sizing-e2e.md): the remote-tmux mirror sizing UI suite, its ssh shim, the `remote.tmux.pane_grids` / `remote.tmux.test_exec` debug verbs, and the live layout fuzz harness.
|
||||
|
||||
@@ -2,45 +2,23 @@
|
||||
|
||||
## `reload.sh`
|
||||
|
||||
`reload.sh` builds the Debug app for a tag. It does not compile the test target.
|
||||
|
||||
A successful reload proves the app target built. It does not prove:
|
||||
|
||||
- `cmuxTests` compile
|
||||
- `cmuxUITests` compile
|
||||
- package test targets compile
|
||||
- test-only imports still resolve
|
||||
|
||||
For package/refactor work, treat reload as insufficient by itself.
|
||||
Proves the app target built. Proves nothing about `cmuxTests`, `cmuxUITests`, package test targets, or test-only imports. For package/refactor work, treat it as insufficient on its own.
|
||||
|
||||
## Unit test target
|
||||
|
||||
`xcodebuild -scheme cmux-unit` is safe because it does not launch the app. Prefer CI when practical, but use `cmux-unit` when package/refactor changes can break tests while the app target still builds.
|
||||
|
||||
Use a tagged derived data path:
|
||||
`cmux-unit` is safe locally because it does not launch the app. Use it when package/refactor changes can break tests while the app target still builds; prefer CI when practical.
|
||||
|
||||
```bash
|
||||
xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-<tag> build
|
||||
xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-destination 'platform=macOS' -derivedDataPath /tmp/cmux-<tag> build
|
||||
```
|
||||
|
||||
For `cmuxApp` or `AppDelegate` churn, include the repo's known GlobalISel workaround flag if required by current project instructions.
|
||||
For `cmuxApp` or `AppDelegate` churn, add the repo's GlobalISel workaround flag if current project instructions require it.
|
||||
|
||||
## E2E and UI tests
|
||||
|
||||
E2E and UI tests run via GitHub Actions or on the VM. Trigger E2E/UI through:
|
||||
|
||||
```bash
|
||||
gh workflow run test-e2e.yml
|
||||
```
|
||||
|
||||
Do not launch an untagged app locally to satisfy socket/UI tests.
|
||||
Run through GitHub Actions or the VM: `gh workflow run test-e2e.yml`. Never launch an untagged app locally to satisfy socket or UI tests.
|
||||
|
||||
## Python socket tests
|
||||
|
||||
Python socket tests under `tests_v2/` connect to a running cmux instance socket. If they must be run locally, use a tagged build socket:
|
||||
|
||||
```bash
|
||||
CMUX_SOCKET_PATH=/tmp/cmux-debug-<tag>.sock
|
||||
```
|
||||
|
||||
Never launch or target an untagged `cmux DEV.app` for these tests. It can conflict with the user's running debug instance.
|
||||
`tests_v2/` connects to a running cmux instance socket. Locally, point it at a tagged build with `CMUX_SOCKET_PATH=/tmp/cmux-debug-<tag>.sock`. Never target an untagged `cmux DEV.app`; it conflicts with the user's running debug instance.
|
||||
|
||||
@@ -1,50 +1,13 @@
|
||||
# Regression and Test Quality
|
||||
|
||||
## Regression commit policy
|
||||
The two-commit policy, wiring requirement, and quality rules are in [../SKILL.md](../SKILL.md). This covers the judgment call.
|
||||
|
||||
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
|
||||
## Is the test behavioral?
|
||||
|
||||
1. Add the failing test only.
|
||||
2. Add the fix.
|
||||
Behavioral tests exercise unit, integration, E2E, CLI, or artifact-level paths of a built product. A test that reads source text, method signatures, AST fragments, grep patterns, or checked-in plist/project/config snippets asserts implementation shape, not behavior, and passes while the user-visible bug remains.
|
||||
|
||||
This makes it visible in GitHub that the test fails without the fix and passes with it.
|
||||
|
||||
## Behavioral tests
|
||||
|
||||
Tests should verify observable runtime behavior through executable paths:
|
||||
|
||||
- unit
|
||||
- integration
|
||||
- E2E
|
||||
- CLI
|
||||
- artifact-level behavior of a built product
|
||||
|
||||
Avoid tests that only verify:
|
||||
|
||||
- source code text
|
||||
- method signatures
|
||||
- AST fragments
|
||||
- grep-style patterns
|
||||
- checked-in plist/project/config snippets
|
||||
|
||||
For metadata changes, prefer testing the built app bundle or the runtime behavior that depends on the metadata. If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and say so.
|
||||
|
||||
## Test wiring
|
||||
|
||||
Test files in `cmuxTests/` must be wired into `cmux.xcodeproj/project.pbxproj`.
|
||||
|
||||
A `.swift` file added to `cmuxTests/` without matching project entries can be silently ignored by Xcode. Both targeted `xcodebuild test -only-testing:cmuxTests/<TestClass>` and bot reviews can pass with "Executed 0 tests".
|
||||
|
||||
The `workflow-guard-tests` job runs:
|
||||
|
||||
```bash
|
||||
./scripts/lint-pbxproj-test-wiring.sh
|
||||
```
|
||||
|
||||
When hand-editing wiring, use a wired sibling like `TabManagerUnitTests.swift` as the template.
|
||||
For a metadata change, test the built app bundle or the runtime behavior that depends on that metadata. If neither is practical, skip the test and state that in the handoff instead of adding a shape assertion.
|
||||
|
||||
## When tests missed a bug
|
||||
|
||||
When the user says tests missed a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete.
|
||||
|
||||
Do not add a broad implementation-shape test that would have passed while the user-visible bug remained.
|
||||
When the user says tests missed a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete. Do not add a broad implementation-shape test that would have passed while the bug was live.
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
# Remote-tmux sizing e2e suite
|
||||
# Remote-tmux testing
|
||||
|
||||
`cmuxUITests/RemoteTmuxSizingUITests` verifies the full remote-tmux mirror
|
||||
sizing flow end to end: a lab tmux server holding a zoo of layout shapes, a
|
||||
real attach through the app's ssh transport, and — after every window drag
|
||||
and tab click — the assertion that every pane renders the size tmux assigned
|
||||
it. The oracle is the `remote.tmux.pane_grids` debug socket verb (grid cells
|
||||
straight from the app), never screenshots.
|
||||
Two harnesses cover the remote-tmux mirror: the `RemoteTmuxSizingUITests` XCUITest suite and the live layout fuzz.
|
||||
|
||||
## Running it
|
||||
## Sizing UI suite
|
||||
|
||||
This suite runs LOCALLY. It is hermetic — no network, no ssh config, no
|
||||
pre-existing tmux server, every path unique per run — so it is NOT CI-only;
|
||||
run it here, on every change, and read red/green directly. (Do not confuse it
|
||||
with the BrowserFixture socket suites, which do fail locally by design.)
|
||||
`cmuxUITests/RemoteTmuxSizingUITests` verifies the full sizing flow: a lab tmux server holding a zoo of layout shapes, a real attach through the app's ssh transport, and, after every window drag and tab click, the assertion that every pane renders the size tmux assigned it. The oracle is the `remote.tmux.pane_grids` debug socket verb (grid cells straight from the app), never screenshots.
|
||||
|
||||
### Running it
|
||||
|
||||
Hermetic (no network, no ssh config, no pre-existing tmux server, unique paths per run), so run it locally on every change and read red/green directly. Do not confuse it with the BrowserFixture socket suites, which fail locally by design.
|
||||
|
||||
```bash
|
||||
xcodebuild test -project cmux.xcodeproj -scheme cmux -configuration Debug \
|
||||
@@ -20,17 +16,9 @@ xcodebuild test -project cmux.xcodeproj -scheme cmux -configuration Debug \
|
||||
-only-testing:cmuxUITests/RemoteTmuxSizingUITests
|
||||
```
|
||||
|
||||
Scope to one scenario while iterating with
|
||||
`-only-testing:cmuxUITests/RemoteTmuxSizingUITests/<testName>`.
|
||||
Scope to one scenario while iterating with `-only-testing:cmuxUITests/RemoteTmuxSizingUITests/<testName>`. Requires a local `tmux` at `/opt/homebrew/bin/tmux`, `/usr/local/bin/tmux`, or `/usr/bin/tmux` (the exact paths the suite and the `test_exec` allowlist probe); it skips when none exists.
|
||||
|
||||
Requires a local `tmux` at one of `/opt/homebrew/bin/tmux`,
|
||||
`/usr/local/bin/tmux`, or `/usr/bin/tmux` (the exact paths the suite — and
|
||||
the `test_exec` allowlist — probe; the suite skips when none exists).
|
||||
|
||||
Running it as a sandboxed agent: `xcodebuild` cannot run under the Bash-tool
|
||||
sandbox (its SwiftPM resolver's `sandbox-exec` dies with `Operation not
|
||||
permitted`). Run it OUTSIDE the sandbox — through the ssh hairpin, exactly
|
||||
like the build:
|
||||
As a sandboxed agent, `xcodebuild` cannot run under the Bash-tool sandbox (its SwiftPM resolver's `sandbox-exec` dies with `Operation not permitted`). Run it outside the sandbox through the ssh hairpin, exactly like the build:
|
||||
|
||||
```bash
|
||||
ssh cmux-srvA "zsh -lc 'cd <repo> && CMUX_SKIP_ZIG_BUILD=1 xcodebuild test \
|
||||
@@ -39,59 +27,43 @@ ssh cmux-srvA "zsh -lc 'cd <repo> && CMUX_SKIP_ZIG_BUILD=1 xcodebuild test \
|
||||
-only-testing:cmuxUITests/RemoteTmuxSizingUITests/<testName>; echo EXIT=\$?'"
|
||||
```
|
||||
|
||||
`CMUX_SKIP_ZIG_BUILD=1` skips the Ghostty CLI-helper script phase, which
|
||||
otherwise fails the run on its strict zig-version check (same flag the
|
||||
`reload.sh` builds use). Reuse the warm `-derivedDataPath` and never
|
||||
`xcodebuild clean` (a wiped Build/ forces SwiftPM re-resolution, which needs
|
||||
the same sandbox-exec and fails).
|
||||
`CMUX_SKIP_ZIG_BUILD=1` skips the Ghostty CLI-helper script phase, which otherwise fails the run on its strict zig-version check (the same flag `reload.sh` uses). Reuse the warm `-derivedDataPath` and never `xcodebuild clean`: a wiped `Build/` forces SwiftPM re-resolution, which needs the same `sandbox-exec` and fails.
|
||||
|
||||
## Architecture (why it is shaped this way)
|
||||
### Architecture
|
||||
|
||||
The XCUITest RUNNER is sandboxed — it cannot create files in `/tmp` or spawn
|
||||
a tmux server there — while the app under test is not. The two processes
|
||||
have disjoint filesystem reach, so the app owns everything:
|
||||
The XCUITest runner is sandboxed and the app under test is not, so the two processes have disjoint filesystem reach and the app owns everything:
|
||||
|
||||
- **Lab tmux server.** The runner never spawns tmux. It drives every
|
||||
`new-session` / `split-window` / `resize-pane` through
|
||||
`remote.tmux.test_exec`, a DEBUG-only socket verb that runs a tmux argv
|
||||
inside the app with the lab `TMUX_TMPDIR`.
|
||||
- **ssh shim.** `scripts/remote-tmux-e2e-ssh-shim.sh` replaces `ssh` via
|
||||
`CMUX_REMOTE_TMUX_SSH_FOR_TESTING`: it strips ssh's option framing and
|
||||
runs the "remote" command locally, replicating the three ssh behaviors the
|
||||
transport depends on — the remote shell re-splits the quoted command, a
|
||||
pty exists only under `-t`/`-tt` (`tmux -CC` needs one; one-shot probes
|
||||
must NOT get one, because the app classifies probe failures by stderr
|
||||
text), and `-O check/exit` ControlMaster ops succeed.
|
||||
- **Attach path.** `remote.tmux.window` (the `cmux ssh-tmux` entry point)
|
||||
mirrors the lab host in a dedicated, activated window — activation mounts
|
||||
the mirror views, whose geometry feeds the client-size pushes.
|
||||
- **Lab tmux server.** The runner never spawns tmux. Every `new-session` / `split-window` / `resize-pane` goes through `remote.tmux.test_exec`, a DEBUG-only socket verb running a tmux argv inside the app with the lab `TMUX_TMPDIR`.
|
||||
- **ssh shim.** `scripts/remote-tmux-e2e-ssh-shim.sh` replaces `ssh` via `CMUX_REMOTE_TMUX_SSH_FOR_TESTING`. It strips ssh's option framing and runs the "remote" command locally, replicating the three behaviors the transport depends on: the remote shell re-splits the quoted command, a pty exists only under `-t`/`-tt` (`tmux -CC` needs one, one-shot probes must not get one because the app classifies probe failures by stderr text), and `-O check/exit` ControlMaster ops succeed.
|
||||
- **Attach path.** `remote.tmux.window` (the `cmux ssh-tmux` entry point) mirrors the lab host in a dedicated, activated window; activation mounts the mirror views whose geometry feeds the client-size pushes.
|
||||
|
||||
## The oracle contract
|
||||
### Oracle contract
|
||||
|
||||
Per settle check, EVERY mirrored window must hold `base == pushed` (hidden
|
||||
tabs keep their claimed size and re-render when selected), and the SELECTED
|
||||
window must additionally be present with panes that satisfy the render
|
||||
contract — exact on the immediate parent split's axis, rendered ≥ assigned
|
||||
on the fill axis (a smaller render loses content; a larger one is background
|
||||
beyond the PTY). Stability (window size steady across samples) and coherence
|
||||
(top-row pane widths + separators sum to the window width, via `test_exec`
|
||||
tmux queries) are asserted first.
|
||||
Per settle check, every mirrored window must hold `base == pushed` (hidden tabs keep their claimed size and re-render when selected), and the selected window must additionally have panes satisfying the render contract: exact on the immediate parent split's axis, rendered >= assigned on the fill axis (a smaller render loses content, a larger one is background beyond the PTY). Stability (window size steady across samples) and coherence (top-row pane widths plus separators sum to the window width, via `test_exec` tmux queries) are asserted first.
|
||||
|
||||
## Debugging a red run
|
||||
### Debugging a red run
|
||||
|
||||
- **Shim suspicion:** `bash scripts/remote-tmux-e2e-ssh-shim-check.sh`
|
||||
exercises the shim through every ssh invocation shape the transport makes
|
||||
(master ops, one-shot probes with stderr classification, the `-tt`
|
||||
control stream with a live stdin dialogue, SIGTERM cleanup) in seconds.
|
||||
- **Sizing suspicion:** the failure messages carry the full `pane_grids`
|
||||
introspection — per-window `base`/`pushed`/`current_f`,
|
||||
`visible_for_sizing`, `container_pt`, and per-pane assigned vs rendered
|
||||
with the raw calibration sample. `pushed != current_f` on a visible window
|
||||
means a push trigger was missed; `base != pushed` means tmux never applied
|
||||
(or a co-attached client constrained) the request.
|
||||
- **Fast live iteration:** the same scenario can be replicated against a
|
||||
running tagged build outside the runner sandbox — mirror a loopback host,
|
||||
resize the window with `osascript`/System Events, switch tabs with the
|
||||
`surface.focus` socket verb, and poll `remote.tmux.pane_grids` between
|
||||
steps. Iterations take seconds instead of a build cycle; codify anything
|
||||
it finds back into the suite.
|
||||
- **Shim suspicion:** `bash scripts/remote-tmux-e2e-ssh-shim-check.sh` exercises every ssh invocation shape the transport makes (master ops, one-shot probes with stderr classification, the `-tt` control stream with a live stdin dialogue, SIGTERM cleanup) in seconds.
|
||||
- **Sizing suspicion:** failure messages carry the full `pane_grids` introspection: per-window `base`/`pushed`/`current_f`, `visible_for_sizing`, `container_pt`, and per-pane assigned vs rendered with the raw calibration sample. `pushed != current_f` on a visible window means a push trigger was missed; `base != pushed` means tmux never applied the request (or a co-attached client constrained it).
|
||||
- **Fast live iteration:** replicate the scenario against a running tagged build outside the runner sandbox. Mirror a loopback host, resize the window through GUI automation, switch tabs with the `surface.focus` socket verb, and poll `remote.tmux.pane_grids` between steps. Iterations take seconds instead of a build cycle; codify anything it finds back into the suite.
|
||||
|
||||
## Live layout fuzz
|
||||
|
||||
The real app mirrors a real tmux server, driven with random layouts and churn and judged at settle by two oracles: sizing (claims, plans, and rendered grids agree and settle within budget) and content (each pane's `read-screen`, unwrapped, matches `tmux capture-pane -J`). Everything runs against a local fixture with no network and no MFA. Seeds are deterministic, so "seed 3, iteration 1" in a commit message is a complete repro recipe.
|
||||
|
||||
```bash
|
||||
scripts/remote-tmux-fuzz-host.sh cmux-fuzzhost # loopback-only sshd, isolated tmux
|
||||
CMUX_TAG=<tag> scripts/remote-tmux-fuzz-marathon.sh cmux-fuzzhost [seeds] [iters]
|
||||
scripts/remote-tmux-live-fuzz.sh cmux-fuzzhost <seed> <iters> # replay one seed
|
||||
```
|
||||
|
||||
Use the `cmux-fuzzhost` alias, never `cmux-srvA`/`cmux-srvB`. Those are the render-harness/interactive loopback aliases: their `/tmp/cmux-srv*` holds a live interactive tmux the fuzz harness refuses to clobber, and their tmux dir is not where the app's `ssh-tmux` connects, so the mirror comes up empty. The host script generates a loopback sshd whose logins land in an isolated `TMUX_TMPDIR` the harness owns and can freely create and kill.
|
||||
|
||||
Run on a quiet machine and treat load as part of the result: settle budgets are latency assertions, and a loaded box manufactures failures that read like code bugs. Launch in the background or a plain terminal and let it finish. Never run it inside a tmux session (the per-seed reset runs `tmux kill-server`, which inside tmux hits your default server), and do not kill the wrapper mid-run: that orphans the driver, which blocks the next run. Both scripts allow one driver at a time.
|
||||
|
||||
Setup failures and their fixes:
|
||||
|
||||
- `no workspace mirroring session 'fuzz'`: wrong host. Use `cmux-fuzzhost`.
|
||||
- `refusing to kill an unowned lab`: a stale lab tmux from an aborted run or a manual `ssh cmux-fuzzhost` probe. Kill it scoped to that dir with `TMUX_TMPDIR=<host's fuzz tmux dir> tmux kill-server`, never a bare `kill-server`.
|
||||
- `another fuzz driver (pid N) is running`: `pkill -9 -f remote-tmux-fuzz-marathon.sh; pkill -9 -f remote-tmux-live-fuzz.sh`, then remove the `cmux-fuzz-marathon.lock` directory under the temp root.
|
||||
- ssh shows `REMOTE HOST IDENTIFICATION HAS CHANGED` or `no such identity`: the host script regenerated the sshd host key or relocated the client key. Run `ssh-keygen -R "[127.0.0.1]:<port>"` and confirm the alias's `IdentityFile` points at the key the script wrote.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# Swift Testing Migration
|
||||
|
||||
Use Swift Testing for unit and integration tests. XCTest remains for UI tests.
|
||||
Swift Testing for unit and integration tests; XCTest stays for UI tests under `cmuxUITests/` (Swift Testing has no `XCUIApplication` support).
|
||||
|
||||
## New tests
|
||||
|
||||
New unit and integration tests should:
|
||||
|
||||
```swift
|
||||
import Testing
|
||||
|
||||
@@ -20,28 +18,23 @@ struct ExampleTests {
|
||||
|
||||
Use `try #require(...)` when a value must be unwrapped before continuing.
|
||||
|
||||
## XCTest conversion
|
||||
## XCTest conversion mapping
|
||||
|
||||
When touching an existing XCTest unit test, convert in place if the edit naturally crosses that code.
|
||||
Convert in place only when an edit naturally crosses the file; do not bulk-rewrite untouched tests.
|
||||
|
||||
Mapping:
|
||||
|
||||
- `XCTestCase` subclass -> `@Suite struct` or `@Suite final class`
|
||||
- `func testFoo()` -> `@Test func foo()`
|
||||
- `XCTAssertEqual(a, b)` -> `#expect(a == b)`
|
||||
- `XCTAssertTrue(condition)` -> `#expect(condition)`
|
||||
- `XCTUnwrap(value)` -> `try #require(value)`
|
||||
- `XCTFail("message")` -> `Issue.record("message")`
|
||||
- `setUp()` -> `init()`
|
||||
- `tearDown()` -> `deinit`
|
||||
- async setup -> `async init()`
|
||||
|
||||
Do not bulk-rewrite untouched tests just to migrate them.
|
||||
| XCTest | Swift Testing |
|
||||
|---|---|
|
||||
| `XCTestCase` subclass | `@Suite struct` (or `@Suite final class` for a reference type) |
|
||||
| `func testFoo()` | `@Test func foo()` |
|
||||
| `XCTAssertEqual(a, b)` | `#expect(a == b)` |
|
||||
| `XCTAssertTrue(cond)` | `#expect(cond)` |
|
||||
| `XCTUnwrap(x)` | `try #require(x)` |
|
||||
| `XCTFail("msg")` | `Issue.record("msg")` |
|
||||
| `setUp()` | `init()` (async setup: `async init()`) |
|
||||
| `tearDown()` | `deinit` |
|
||||
|
||||
## Parameterized tests
|
||||
|
||||
Prefer:
|
||||
|
||||
```swift
|
||||
@Test(arguments: [
|
||||
("input-a", "output-a"),
|
||||
@@ -52,19 +45,6 @@ func formats(input: String, expected: String) {
|
||||
}
|
||||
```
|
||||
|
||||
This is clearer than duplicating test methods with copy/paste assertions.
|
||||
|
||||
## Parallel execution
|
||||
|
||||
Swift Testing runs tests in parallel by default, including across suites. If a suite genuinely needs ordering or guards shared mutable state, use `.serialized`:
|
||||
|
||||
```swift
|
||||
@Suite(.serialized)
|
||||
struct FileBackedTests { ... }
|
||||
```
|
||||
|
||||
Prefer isolated temp directories and injected dependencies over serialization when practical.
|
||||
|
||||
## UI tests
|
||||
|
||||
Files under `cmuxUITests/` stay on XCTest/XCUITest. Swift Testing does not support `XCUIApplication` UI testing.
|
||||
Tests run in parallel by default, including across suites. Prefer isolated temp directories and injected dependencies; use `@Suite(.serialized)` only when a suite genuinely needs ordering or guards shared mutable state.
|
||||
|
||||
+44
-153
@@ -5,221 +5,112 @@ description: "Work inside the current cmux workspace and terminal. Use for cmux
|
||||
|
||||
# cmux Workspace
|
||||
|
||||
Use this skill when a task should be scoped to the cmux workspace that invoked the agent. A workspace is the sidebar tab-like unit in cmux. It contains split panes, and each pane contains one or more surfaces. A surface is the terminal or browser session the user interacts with.
|
||||
Scope work to the cmux workspace that invoked the agent.
|
||||
|
||||
## Default Rule
|
||||
- **Window**: a macOS cmux window.
|
||||
- **Workspace**: a sidebar entry. The UI calls it a tab; CLI/socket APIs call it a workspace.
|
||||
- **Pane**: a split region inside a workspace.
|
||||
- **Surface**: a tab inside a pane, terminal or browser.
|
||||
- **Panel**: internal content type inside a surface. Prefer CLI surface commands over panel internals.
|
||||
|
||||
Scope actions to the current caller workspace unless the user explicitly asks for another workspace, another window, or global state.
|
||||
## Default rule
|
||||
|
||||
Do not assume the visually focused cmux workspace is the right target. An agent can be running in one workspace while the user is looking at another. Prefer the caller environment first:
|
||||
Scope actions to the current caller workspace unless the user explicitly asks for another workspace, another window, or global state. Do not assume the visually focused workspace is the right target: an agent can run in one workspace while the user looks at another.
|
||||
|
||||
```bash
|
||||
printf 'workspace=%s\nsurface=%s\nsocket=%s\n' \
|
||||
"${CMUX_WORKSPACE_ID:-}" \
|
||||
"${CMUX_SURFACE_ID:-}" \
|
||||
"${CMUX_SOCKET_PATH:-}"
|
||||
"${CMUX_WORKSPACE_ID:-}" "${CMUX_SURFACE_ID:-}" "${CMUX_SOCKET_PATH:-}"
|
||||
cmux identify --json
|
||||
```
|
||||
|
||||
Use `CMUX_WORKSPACE_ID` as the default workspace anchor and `CMUX_SURFACE_ID` as the default caller terminal/surface anchor. If those are missing, use `cmux identify --json` and be explicit that you are using the currently focused cmux context.
|
||||
`CMUX_WORKSPACE_ID` is the default workspace anchor and `CMUX_SURFACE_ID` the default caller terminal anchor. If they are missing, fall back to `cmux identify --json` and say explicitly that you are using the currently focused context.
|
||||
|
||||
## Non-Disruptive Automation
|
||||
## Non-disruptive automation
|
||||
|
||||
The user may be visually focused on a different workspace, window, or app while an agent works in the caller workspace. Treat layout and focus as separate concerns. Never call focus-changing verbs speculatively.
|
||||
Treat layout and focus as separate concerns. `select-workspace`, `focus-pane`, `focus-panel`, and focus-changing `tab-action` verbs are user-affecting actions, like clicks. Never call them speculatively, even inside the caller's own workspace, since the user may be looking elsewhere.
|
||||
|
||||
Never call these without an explicit user ask:
|
||||
|
||||
- `select-workspace` switches the visible sidebar tab.
|
||||
- `focus-pane` / `focus-panel` yanks pane or surface focus.
|
||||
- `tab-action` with focus-changing actions.
|
||||
|
||||
These are user-affecting actions, like clicks. The rule applies even inside the caller's own workspace, since the user may be looking elsewhere.
|
||||
|
||||
Build layout additively, in one shot. Prefer commands that create a new pane already populated with the right surface:
|
||||
Build layout additively in one shot, using commands that create a pane already populated with the right surface:
|
||||
|
||||
```bash
|
||||
# pane and content in one call, no follow-up needed
|
||||
cmux new-pane --workspace "${CMUX_WORKSPACE_ID}" --type browser --direction right --url "http://127.0.0.1:8765"
|
||||
cmux new-pane --workspace "${CMUX_WORKSPACE_ID}" --type terminal --direction down
|
||||
```
|
||||
|
||||
Avoid create-then-move-then-focus chains. If a layout command rejects a valid `surface:` or `pane:` ref, do not work around it by focusing. Report the bug to the user and stop.
|
||||
Avoid create-then-move-then-focus chains. Pass `--focus false` wherever the verb supports it (`move-surface --focus false` preserves the user's attention; more commands may grow the flag, see https://github.com/manaflow-ai/cmux/issues/1418 and https://github.com/manaflow-ai/cmux/issues/2820). If a layout command rejects a valid `surface:` or `pane:` ref, report the bug and stop rather than working around it by focusing.
|
||||
|
||||
Pass `--focus false` whenever the verb supports it. `move-surface --focus false` preserves the user's current attention. Other commands may grow the same flag over time (https://github.com/manaflow-ai/cmux/issues/1418, https://github.com/manaflow-ai/cmux/issues/2820).
|
||||
## Right-side helper pane
|
||||
|
||||
## Right-Side Helper Pane
|
||||
For auxiliary output (preview apps, TUIs, logs, one-off shells, browser checks), reuse one helper pane to the right of the caller terminal. Inspect first with `cmux identify --json`, `cmux list-panes`, and `cmux list-pane-surfaces`, then:
|
||||
|
||||
When opening auxiliary output for the current task (preview apps, TUIs, logs, one-off shells, browser checks), keep the workspace organized by reusing a helper pane to the right of the caller terminal.
|
||||
|
||||
First inspect the caller context and panes:
|
||||
|
||||
```bash
|
||||
cmux identify --json
|
||||
cmux list-panes --workspace "${CMUX_WORKSPACE_ID:-}" --json
|
||||
cmux list-pane-surfaces --workspace "${CMUX_WORKSPACE_ID:-}" --json
|
||||
```
|
||||
|
||||
Use this policy:
|
||||
|
||||
- If the caller workspace already has a non-caller helper pane, add a new surface to that pane instead of creating another pane:
|
||||
- Helper pane exists: add a surface to it.
|
||||
```bash
|
||||
cmux new-surface --workspace "${CMUX_WORKSPACE_ID:-}" --pane pane:<helper> --type terminal --focus false
|
||||
```
|
||||
- If there is no helper pane, create exactly one right-side pane:
|
||||
- No helper pane: create exactly one.
|
||||
```bash
|
||||
cmux new-pane --workspace "${CMUX_WORKSPACE_ID:-}" --type terminal --direction right --focus false
|
||||
```
|
||||
- If there are multiple obvious stale helper panes from this same automation and the user asked to tidy or reuse, keep one right helper pane and clean up the duplicates. Do not close panes you cannot confidently identify as stale helper output.
|
||||
- Send commands to the new or reused helper surface by explicit surface ref. Do not focus it unless the user asks.
|
||||
- Multiple obvious stale helper panes from this same automation, and the user asked to tidy: keep one and clean up duplicates. Never close a pane you cannot confidently identify as stale helper output.
|
||||
|
||||
This means repeated "open it" requests should normally create tabs inside the existing right helper pane, not more splits.
|
||||
Send commands to the new or reused surface by explicit surface ref. Repeated "open it" requests create tabs inside the existing right helper pane, not more splits.
|
||||
|
||||
## Hierarchy
|
||||
## Caller terminal
|
||||
|
||||
- Window: a macOS cmux window.
|
||||
- Workspace: a sidebar entry. The UI may call it a tab, but CLI/socket APIs call it a workspace.
|
||||
- Pane: a split region inside a workspace.
|
||||
- Surface: a tab inside a pane. Surfaces can be terminals or browser panels.
|
||||
- Panel: internal content type inside a surface. Prefer CLI surface commands instead of panel internals.
|
||||
|
||||
## Inspect Current Context
|
||||
The surface that invoked the agent is the safest anchor for relative operations.
|
||||
|
||||
```bash
|
||||
cmux identify --json
|
||||
cmux current-workspace --json
|
||||
cmux list-workspaces --json
|
||||
cmux list-panes --workspace "${CMUX_WORKSPACE_ID:-}" --json
|
||||
cmux list-pane-surfaces --workspace "${CMUX_WORKSPACE_ID:-}" --json
|
||||
cmux list-panels --workspace "${CMUX_WORKSPACE_ID:-}" --json
|
||||
```
|
||||
|
||||
Use `--id-format both` when logs or handoffs need stable UUIDs plus human refs:
|
||||
|
||||
```bash
|
||||
cmux --json --id-format both identify
|
||||
```
|
||||
|
||||
## Workspace-Scoped Actions
|
||||
|
||||
Prefer explicit workspace flags even when env vars are set. It makes automation auditable and avoids affecting a focused workspace in another window.
|
||||
|
||||
```bash
|
||||
# create a new workspace when the user asks for a new task area
|
||||
cmux new-workspace --name "debug auth" --cwd "$PWD"
|
||||
|
||||
# rename / close (only when explicitly requested)
|
||||
cmux rename-workspace --workspace "${CMUX_WORKSPACE_ID:-}" -- "build fix"
|
||||
cmux close-workspace --workspace workspace:4
|
||||
cmux close-surface --workspace "${CMUX_WORKSPACE_ID:-}" --surface surface:3
|
||||
|
||||
# additive layout (safe, no focus side effects beyond the command's own defaults)
|
||||
cmux new-pane --workspace "${CMUX_WORKSPACE_ID:-}" --type terminal --direction right
|
||||
cmux new-surface --workspace "${CMUX_WORKSPACE_ID:-}" --type terminal
|
||||
|
||||
# focus-changing (USER-AFFECTING, only on explicit ask, see Non-Disruptive Automation above)
|
||||
cmux select-workspace --workspace workspace:2
|
||||
cmux focus-pane --workspace "${CMUX_WORKSPACE_ID:-}" --pane pane:2
|
||||
cmux focus-panel --workspace "${CMUX_WORKSPACE_ID:-}" --panel surface:3
|
||||
```
|
||||
|
||||
## Caller Terminal
|
||||
|
||||
The current terminal is the surface that invoked the agent. Treat it as the safest anchor for relative operations.
|
||||
|
||||
```bash
|
||||
# send to the focused terminal in the caller workspace
|
||||
cmux send "npm test\n"
|
||||
|
||||
# send to the exact caller surface
|
||||
cmux send --surface "${CMUX_SURFACE_ID:-}" "git status\n"
|
||||
cmux send "npm test\n" # focused terminal in caller workspace
|
||||
cmux send --surface "${CMUX_SURFACE_ID:-}" "git status\n" # exact caller surface
|
||||
cmux send-key --surface "${CMUX_SURFACE_ID:-}" enter
|
||||
```
|
||||
|
||||
Do not send keystrokes, close surfaces, or change focus in other workspaces unless the user asked for that target.
|
||||
Do not send keystrokes, close surfaces, or change focus in another workspace unless the user named that target.
|
||||
|
||||
## Moving Surfaces
|
||||
|
||||
Reorder a surface within its pane:
|
||||
|
||||
```bash
|
||||
cmux move-surface --surface "${CMUX_SURFACE_ID}" --before surface:3
|
||||
cmux move-surface --surface "${CMUX_SURFACE_ID}" --after surface:3
|
||||
cmux move-surface --surface "${CMUX_SURFACE_ID}" --index 0
|
||||
```
|
||||
|
||||
Move a surface to another existing pane. Pass `--focus false` to keep the user's current attention put:
|
||||
## Moving surfaces
|
||||
|
||||
```bash
|
||||
cmux move-surface --surface "${CMUX_SURFACE_ID}" --before surface:3 # also --after, --index
|
||||
cmux move-surface --surface surface:240 --pane pane:172 --focus false
|
||||
```
|
||||
|
||||
Split a surface off into a new pane:
|
||||
|
||||
```bash
|
||||
cmux drag-surface-to-split --surface surface:240 down
|
||||
```
|
||||
|
||||
Known papercut: `drag-surface-to-split` currently routes through V1 and resolves the workspace via UI focus, so it can fail with `ERROR: Surface not found` when the caller's workspace is not the visually focused one. Tracked at https://github.com/manaflow-ai/cmux/issues/1901, related to https://github.com/manaflow-ai/cmux/issues/3189. Until that lands, prefer building the layout additively (see Non-Disruptive Automation above) over create-then-split.
|
||||
Known papercut: `drag-surface-to-split` routes through V1 and resolves the workspace via UI focus, so it fails with `ERROR: Surface not found` when the caller's workspace is not the visually focused one (https://github.com/manaflow-ai/cmux/issues/1901, related https://github.com/manaflow-ai/cmux/issues/3189). Until that lands, build layout additively. Never call `focus-pane` or `focus-panel` to recover from a failed move; report the failure and stop.
|
||||
|
||||
Do not call `focus-pane` or `focus-panel` to recover from a failed move. Report the failure and stop.
|
||||
## Sidebar state
|
||||
|
||||
## Sidebar State
|
||||
|
||||
Status, progress, and logs should usually be attached to the current workspace so the sidebar reflects this task.
|
||||
Attach status, progress, and logs to the current workspace so the sidebar reflects this task.
|
||||
|
||||
```bash
|
||||
cmux set-status build "running" --workspace "${CMUX_WORKSPACE_ID:-}" --color "#ff9500"
|
||||
cmux set-progress 0.4 --label "Building" --workspace "${CMUX_WORKSPACE_ID:-}"
|
||||
cmux log --workspace "${CMUX_WORKSPACE_ID:-}" --level info -- "Started build"
|
||||
cmux sidebar-state --workspace "${CMUX_WORKSPACE_ID:-}" --json
|
||||
cmux clear-status build --workspace "${CMUX_WORKSPACE_ID:-}"
|
||||
cmux clear-progress --workspace "${CMUX_WORKSPACE_ID:-}"
|
||||
```
|
||||
|
||||
## Contributor Reloads
|
||||
## Contributor reloads
|
||||
|
||||
For cmux app/runtime changes in a cmux source checkout, use tagged reloads from the active worktree. A tagged reload creates an isolated app name, bundle ID, debug socket, and DerivedData path.
|
||||
For cmux app/runtime changes in a cmux source checkout, use a tagged reload from the active worktree. It creates an isolated app name, bundle ID, debug socket, and DerivedData path. Never build or launch untagged `cmux DEV`.
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <short-tag>
|
||||
```
|
||||
|
||||
Never build or launch untagged `cmux DEV`. If tests or tools need a socket, use the tag-specific socket:
|
||||
|
||||
```bash
|
||||
CMUX_SOCKET_PATH=/tmp/cmux-debug-<short-tag>.sock cmux identify --json
|
||||
```
|
||||
|
||||
## Socket and Access
|
||||
## Socket access
|
||||
|
||||
Use the socket path provided by cmux before falling back to defaults:
|
||||
|
||||
```bash
|
||||
SOCK="${CMUX_SOCKET_PATH:-/tmp/cmux.sock}"
|
||||
```
|
||||
|
||||
Socket access can be off, restricted to cmux-spawned processes, or allow all local processes. If a command cannot connect, inspect capabilities before changing settings:
|
||||
|
||||
```bash
|
||||
cmux capabilities --json
|
||||
cmux ping
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [references/commands.md](references/commands.md) enumerates workspace, pane, surface, notification, and utility commands.
|
||||
- [../cmux-browser/SKILL.md](../cmux-browser/SKILL.md) covers browser surfaces with the same current-workspace rule.
|
||||
Use the socket path cmux provided before any default: `SOCK="${CMUX_SOCKET_PATH:-/tmp/cmux.sock}"`. Socket access can be off, restricted to cmux-spawned processes, or open to all local processes. If a command cannot connect, inspect `cmux capabilities --json` and `cmux ping` before changing settings.
|
||||
|
||||
## Rules
|
||||
|
||||
- Work in the current caller workspace by default.
|
||||
- Use `CMUX_WORKSPACE_ID`, `CMUX_SURFACE_ID`, and `CMUX_SOCKET_PATH` before focused-window fallbacks.
|
||||
- Prefer explicit `--workspace` and `--surface` flags for mutating actions.
|
||||
- Never call `focus-pane`, `focus-panel`, `select-workspace`, or focus-changing `tab-action` verbs unless the user explicitly asked. The user may be visually on a different workspace, window, or app.
|
||||
- Work in the caller workspace by default; prefer explicit `--workspace` and `--surface` flags for mutating actions even when env vars are set, so automation is auditable.
|
||||
- Never call `focus-pane`, `focus-panel`, `select-workspace`, or focus-changing `tab-action` verbs unless the user explicitly asked.
|
||||
- Pass `--focus false` on `move-surface` and any creation verb that supports it.
|
||||
- For auxiliary output, reuse the right-side helper pane; create one only if it does not exist.
|
||||
- Build layout additively with `new-pane --type ... --url ...` rather than create-then-move-then-focus chains.
|
||||
- If a CLI command rejects a valid surface or pane ref, report it to the user. Do not work around by focusing.
|
||||
- Build layout additively with `new-pane --type ... --url ...`, not create-then-move-then-focus.
|
||||
- If a CLI command rejects a valid surface or pane ref, report it. Do not work around by focusing.
|
||||
- Do not close, focus, move, or send input to another workspace unless the user names that target.
|
||||
- Use short refs for chat and command examples. Use UUIDs only for logs, persistence, or debugging.
|
||||
- For app/runtime changes in a cmux source checkout, reload with `./scripts/reload.sh --tag <tag>` from the worktree before dogfood handoff.
|
||||
- Use short refs in chat and examples; UUIDs only for logs, persistence, or debugging.
|
||||
|
||||
## References
|
||||
|
||||
- [references/commands.md](references/commands.md): full workspace, pane, surface, notification, and utility command list.
|
||||
- [../cmux-browser/SKILL.md](../cmux-browser/SKILL.md): browser surfaces under the same current-workspace rule.
|
||||
|
||||
@@ -6,6 +6,7 @@ Use these commands from a cmux terminal. Most commands infer the caller workspac
|
||||
|
||||
```bash
|
||||
cmux identify --json
|
||||
cmux --json --id-format both identify # stable UUIDs plus human refs, for logs and handoffs
|
||||
cmux current-workspace --json
|
||||
cmux capabilities --json
|
||||
cmux ping
|
||||
@@ -101,10 +102,3 @@ cmux settings cmux-json
|
||||
cmux settings shortcuts
|
||||
cmux reload-config
|
||||
```
|
||||
|
||||
## Tagged Reloads
|
||||
|
||||
```bash
|
||||
./scripts/reload.sh --tag <short-tag>
|
||||
CMUX_SOCKET_PATH=/tmp/cmux-debug-<short-tag>.sock cmux identify --json
|
||||
```
|
||||
|
||||
+16
-48
@@ -5,72 +5,40 @@ description: End-user control of cmux topology and routing (windows, workspaces,
|
||||
|
||||
# cmux Core Control
|
||||
|
||||
Use this skill to control non-browser cmux topology and routing.
|
||||
Non-browser cmux topology and routing.
|
||||
|
||||
## Core Concepts
|
||||
- **Window**: top-level macOS cmux window.
|
||||
- **Workspace**: tab-like group within a window.
|
||||
- **Pane**: split container in a workspace.
|
||||
- **Surface**: a tab within a pane (terminal or browser panel).
|
||||
|
||||
- Window: top-level macOS cmux window.
|
||||
- Workspace: tab-like group within a window.
|
||||
- Pane: split container in a workspace.
|
||||
- Surface: a tab within a pane (terminal or browser panel).
|
||||
|
||||
## Fast Start
|
||||
## Fast start
|
||||
|
||||
```bash
|
||||
# identify current caller context
|
||||
cmux identify --json
|
||||
|
||||
# list topology
|
||||
cmux list-windows
|
||||
cmux list-workspaces
|
||||
cmux list-panes
|
||||
cmux identify --json # current caller context
|
||||
cmux list-windows / list-workspaces / list-panes
|
||||
cmux list-pane-surfaces --pane pane:1
|
||||
|
||||
# create/focus/move
|
||||
cmux new-workspace
|
||||
cmux new-split right --panel pane:1
|
||||
cmux move-surface --surface surface:7 --pane pane:2 --focus true
|
||||
cmux split-off --surface surface:7 right
|
||||
cmux reorder-surface --surface surface:7 --before surface:3
|
||||
|
||||
# attention cue
|
||||
cmux trigger-flash --surface surface:7
|
||||
cmux trigger-flash --surface surface:7 # attention cue
|
||||
```
|
||||
|
||||
## Settings and Docs
|
||||
## Handle model
|
||||
|
||||
Use `cmux docs settings` before changing cmux-owned settings. It prints the docs URL, schema URL, raw GitHub resources, cmux.json paths, and reload command.
|
||||
Output defaults to short refs (`window:N`, `workspace:N`, `pane:N`, `surface:N`). UUIDs are accepted as input; request UUID output only when needed with `--id-format uuids|both`.
|
||||
|
||||
```bash
|
||||
cmux docs settings
|
||||
cmux settings path
|
||||
```
|
||||
## Settings
|
||||
|
||||
cmux-owned settings live in `~/.config/cmux/cmux.json`. Legacy `~/.config/cmux/settings.json` and `~/Library/Application Support/com.cmuxterm.app/settings.json` files are read only as fallback for missing keys. Before editing, copy any existing `cmux.json` file to a timestamped `.bak` next to it so the user can revert. Edit the user file, then reload:
|
||||
cmux-owned settings live in `~/.config/cmux/cmux.json`. `cmux docs settings` prints the docs URL, schema URL, raw GitHub resources, cmux.json paths, and reload command. `cmux settings`, `cmux settings cmux-json`, and `cmux settings shortcuts` open the UI.
|
||||
|
||||
```bash
|
||||
cmux reload-config
|
||||
```
|
||||
`cmux reload-config` reloads both `cmux.json` and `~/.config/ghostty/config`, refreshing terminals in place with no app restart.
|
||||
|
||||
`cmux reload-config` reloads BOTH `cmux.json` and Ghostty config (`~/.config/ghostty/config`) and refreshes terminals in place. No app restart needed.
|
||||
Terminal rendering (font, cursor style, theme, scrollback, `background-opacity`, `background-blur`) belongs in Ghostty config, not cmux settings. Everything else (app behavior, sidebar, notifications, browser behavior, automation, workspace colors, cmux-owned shortcuts) is cmux settings. Before editing, copy any existing `cmux.json` to a timestamped `.bak` next to it. Legacy `~/.config/cmux/settings.json` and `~/Library/Application Support/com.cmuxterm.app/settings.json` are read only as fallback for missing keys.
|
||||
|
||||
Use cmux settings for app behavior, sidebar, notifications, browser behavior, automation, workspace colors, and cmux-owned shortcuts. Terminal rendering settings such as font, cursor style, theme, scrollback, background transparency (`background-opacity`), and blur (`background-blur`) belong in Ghostty config at `~/.config/ghostty/config`.
|
||||
|
||||
Open the UI when useful:
|
||||
|
||||
```bash
|
||||
cmux settings
|
||||
cmux settings cmux-json
|
||||
cmux settings shortcuts
|
||||
```
|
||||
|
||||
## Handle Model
|
||||
|
||||
- Default output uses short refs: `window:N`, `workspace:N`, `pane:N`, `surface:N`.
|
||||
- UUIDs are still accepted as inputs.
|
||||
- Request UUID output only when needed: `--id-format uuids|both`.
|
||||
|
||||
## Deep-Dive References
|
||||
## Deep-dive references
|
||||
|
||||
| Reference | When to Use |
|
||||
|-----------|-------------|
|
||||
|
||||
@@ -1,35 +1,12 @@
|
||||
# Handles and Identify
|
||||
|
||||
Use `identify` and short handles for deterministic automation targeting.
|
||||
|
||||
## Handle Inputs
|
||||
|
||||
Most v2-backed commands accept:
|
||||
- UUID
|
||||
- short ref (`window:N`, `workspace:N`, `pane:N`, `surface:N`)
|
||||
- index (where legacy/index-based commands still allow it)
|
||||
|
||||
## Self Identify
|
||||
Most v2-backed commands accept a UUID, a short ref (`window:N`, `workspace:N`, `pane:N`, `surface:N`), or an index where legacy index-based commands still allow it.
|
||||
|
||||
```bash
|
||||
cmux identify --json
|
||||
```
|
||||
|
||||
Returns current focused topology plus optional caller resolution.
|
||||
|
||||
## Caller Override
|
||||
|
||||
```bash
|
||||
cmux identify --workspace workspace:2
|
||||
cmux identify --json # focused topology + caller resolution
|
||||
cmux identify --workspace workspace:2 # route relative actions from a known anchor
|
||||
cmux identify --workspace workspace:2 --surface surface:8
|
||||
```
|
||||
|
||||
Useful for agents that need to route relative actions from a known caller anchor.
|
||||
|
||||
## Output Shaping
|
||||
|
||||
```bash
|
||||
cmux --json identify # refs-first output
|
||||
cmux --json --id-format both identify
|
||||
cmux --json --id-format both identify # refs plus UUIDs
|
||||
cmux --json --id-format uuids identify
|
||||
```
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
# Panes and Surfaces
|
||||
|
||||
Split layout, surface creation, focus, move, and reorder.
|
||||
|
||||
## Inspect
|
||||
|
||||
```bash
|
||||
# inspect
|
||||
cmux list-panes
|
||||
cmux list-pane-surfaces --pane pane:1
|
||||
```
|
||||
|
||||
## Create Splits/Surfaces
|
||||
|
||||
```bash
|
||||
# create
|
||||
cmux new-split right --panel pane:1
|
||||
cmux new-surface --type terminal --pane pane:1
|
||||
cmux new-surface --type browser --pane pane:1 --url https://example.com
|
||||
```
|
||||
|
||||
## Focus and Close
|
||||
|
||||
```bash
|
||||
# focus and close
|
||||
cmux focus-pane --pane pane:2
|
||||
cmux focus-panel --panel surface:7
|
||||
cmux close-surface --surface surface:7
|
||||
```
|
||||
|
||||
## Move/Reorder Surfaces
|
||||
|
||||
```bash
|
||||
# move and reorder
|
||||
cmux move-surface --surface surface:7 --pane pane:2 --focus true
|
||||
cmux move-surface --surface surface:7 --workspace workspace:2 --window window:1 --after surface:4
|
||||
cmux split-off --surface surface:7 right
|
||||
cmux reorder-surface --surface surface:7 --before surface:3
|
||||
```
|
||||
|
||||
Surface identity is stable across move/reorder/split-off operations. Layout commands are focus-neutral by default; pass `--focus true` only when you want the moved or created surface selected.
|
||||
Surface identity is stable across move, reorder, and split-off. Layout commands are focus-neutral by default; pass `--focus true` only when the moved or created surface should be selected.
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
# Trigger Flash and Surface Health
|
||||
|
||||
Operational checks useful in automation loops.
|
||||
|
||||
## Trigger Flash
|
||||
|
||||
Flash a surface or workspace to provide visual confirmation in UI:
|
||||
Flash a surface or workspace for visual confirmation in the UI:
|
||||
|
||||
```bash
|
||||
cmux trigger-flash --surface surface:7
|
||||
cmux trigger-flash --workspace workspace:2
|
||||
```
|
||||
|
||||
## Surface Health
|
||||
|
||||
Use health output to detect hidden/detached/non-windowed surfaces:
|
||||
Detect hidden, detached, or non-windowed surfaces before routing focused input when UI state may be stale:
|
||||
|
||||
```bash
|
||||
cmux surface-health
|
||||
cmux surface-health --workspace workspace:2
|
||||
```
|
||||
|
||||
Use this before routing focused input if UI state may be stale.
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
# Windows and Workspaces
|
||||
|
||||
Window/workspace lifecycle and ordering operations.
|
||||
|
||||
## Inspect
|
||||
|
||||
```bash
|
||||
# inspect
|
||||
cmux list-windows
|
||||
cmux current-window
|
||||
cmux list-workspaces
|
||||
cmux current-workspace
|
||||
```
|
||||
|
||||
## Create/Focus/Close
|
||||
|
||||
```bash
|
||||
# lifecycle
|
||||
cmux new-window
|
||||
cmux focus-window --window window:2
|
||||
cmux close-window --window window:2
|
||||
|
||||
cmux new-workspace
|
||||
cmux select-workspace --workspace workspace:4
|
||||
cmux close-workspace --workspace workspace:4
|
||||
```
|
||||
|
||||
## Reorder and Move
|
||||
|
||||
```bash
|
||||
# reorder and move
|
||||
cmux reorder-workspace --workspace workspace:4 --before workspace:2
|
||||
cmux move-workspace-to-window --workspace workspace:4 --window window:1
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user