Add support for using jj and jj worspaces#71
Conversation
* docs: add zellij screenshot to docs page * docs: update zellij screenshot
Retina (2x) screenshots look blurry on non-retina displays because browser downscaling produces poor results. This adds a Vite plugin that uses sharp to generate proper half-resolution 1x WebP variants at build time and injects srcset density descriptors into <img> tags automatically. The plugin has two parts: - A transform hook that adds srcset="/_1x/foo.webp 1x, /foo.webp 2x" to all <img> tags with .webp src in markdown files - A buildStart hook that generates half-size images into public/_1x/ using sharp's Lanczos resampling Images with existing srcset or data-no-retina attributes are skipped. No markdown files need editing -- the transform is fully automatic.
close.rs was deriving mode and constructing MuxHandle from raw CLI input instead of resolving through git::find_worktree first. When a user ran 'workmux close feature/foo' but the worktree handle was 'api-foo', it would look up the wrong mode (workmux.worktree.feature/foo.mode instead of workmux.worktree.api-foo.mode) and construct an incorrect target name (wm-feature/foo instead of wm-api-foo). Now find_worktree is called during handle resolution. The actual handle is extracted from the worktree path basename before mode lookup and MuxHandle construction. This matches the pattern already used in workflow::open. Adds integration test that creates a worktree with --name (custom handle) and closes it using the branch name to verify resolution works correctly.
When users click on any content image in the docs, the full-size version now displays in a modal overlay with a dimmed background. Click outside the image or press Escape to dismiss. Implementation uses vanilla JS with document-level event delegation, which handles VitePress SPA route changes automatically without rebinding listeners. Images are filtered to only zoom .vp-doc content images, excluding SVGs and linked images. Retina srcset attributes are preserved in the zoomed view. Includes smooth open/close CSS animations (fade + scale) and body scroll lock while the overlay is open.
- Click on zoomed image now closes the overlay (was blocked before, creating confusing UX with zoom-out cursor) - SVG exclusion uses regex to handle query params/hashes in URLs - Add popstate listener to close overlay on browser back/forward - Guard against double-close race condition during animation - Use window sentinel instead of module var for HMR safety - Don't copy srcset to modal so full-size image always displays (on DPR=1 screens, srcset 1x candidate would be half-size)
wait-timeout was listed in Cargo.toml but never imported or used anywhere in the codebase. Removing dead dependency.
strip-ansi-escapes was used in a single call site (capture.rs). The console crate, already a dependency, provides strip_ansi_codes which does the same thing. Removes a redundant dependency.
fs_extra was used in a single file (setup.rs) for copying files and directories into new worktrees. Replaced with std::fs::copy for files and a small copy_dir_recursive helper for directories. The helper properly handles: - Overwriting existing files/symlinks at the destination - Preserving symlinks (avoids infinite recursion on symlink loops) - Skipping special files like sockets/FIFOs (avoids blocking)
Add Apple Container (`container` binary) as a third container runtime alongside Docker and Podman. This enables sandboxing on macOS using Apple's native container/VM technology (macOS 26+, Apple Silicon). Key changes: - Add `AppleContainer` variant to `SandboxRuntime` with serde name `apple-container` (alias `apple`) - Add capability methods on the enum (`binary_name`, `needs_add_host`, `needs_userns_keep_id`, `needs_deny_mode_caps`, `pull_args`, `supports_file_mounts`) to replace match blocks throughout - Auto-detection prefers `container` > `docker` > `podman` on macOS (gated behind `cfg!(target_os = "macos")` to avoid false positives) - Skip freshness checking for Apple Container (no Docker-buildx equivalent available) - Store runtime in StateStore marker files for cleanup correctness, so containers are stopped with the correct binary even if config changes between start and stop - `stop_containers_for_handle` groups by stored runtime, no longer takes a config parameter - `list_containers` returns `Vec<(String, SandboxRuntime)>` with tolerant parsing (empty marker files default to Docker for backwards compatibility) - `sandbox shell --exec` reads stored runtime from state instead of current config - Apple Container only supports directory mounts (virtiofs), so Claude config uses `~/.claude-sandbox-config/claude.json` with a directory mount and in-container symlink, while Docker/Podman keep the existing `~/.claude-sandbox.json` file mount - `rpc_host` defaults to `192.168.64.1` for Apple Container - Apple Container uses `image pull` subcommand (vs `pull` for others) - Skip `--cap-add=NET_ADMIN` and `--security-opt` in deny mode since Apple Container VMs already have full capabilities as root
Add `workmux update` that downloads and installs the latest release binary from GitHub Releases. Uses curl/tar subprocesses to avoid adding HTTP or archive dependencies. The update flow: - Fetches latest version from GitHub API - Downloads the platform-appropriate tar.gz and .sha256 checksum - Verifies SHA-256 integrity before installing - Replaces the binary atomically with rollback on failure - Detects Homebrew installs and directs to `brew upgrade` instead Spinner shows progress through each phase and cleanly reports success or failure.
On CLI startup, workmux now reads a local cache file to check if a newer version is available and shows a one-line stderr notice at most once per 24 hours. A detached background process (`_check-update`) refreshes the cache by querying the GitHub API, keeping the main command non-blocking. Design choices: - Detached subprocess avoids blocking the main command - Cache at ~/.cache/workmux/update_check.json prevents repeated API calls - Numeric version comparison (not string equality) handles 0.1.10 > 0.1.9 - Suppressed in non-interactive contexts (piped stdout/stderr) - Background curl has 5s connect / 10s max timeout - Spawn failure doesn't suppress future checks (no silent blackout) Disable via config (`auto_update_check: false`) or environment (`WORKMUX_NO_UPDATE_CHECK=1`).
Only check for updates during `workmux add` to avoid unexpected output on other commands. This is the most common entry point and keeps the notification surface minimal.
jq is a fundamental CLI tool that hooks and scripts commonly depend on. Including it in the base image ensures all agent containers (claude, codex, gemini, opencode) have it available without needing per-image installs.
Under parallel pytest load (12 workers), tmux pane startup and script execution can exceed the previous 2-second timeouts. Bump wait_for_file default from 2.0s to 5.0s and run_workmux_command timeout from 5.0s to 10.0s. Remove explicit timeout=2.0 overrides in test_agents.py so they inherit the new default.
Allow TUI agents (Claude Code, opencode) inside sandboxed containers/VMs to paste images via Ctrl+V by proxying clipboard reads to the host. Built-in wl-paste and xclip shims translate Linux clipboard tool CLIs into `workmux clipboard-read` calls. A ClipboardRead RPC request triggers the host to read the clipboard (osascript on macOS, wl-paste/xclip on Linux), write the PNG to the shared worktree filesystem (.workmux/tmp/), and return the absolute path. The guest reads the file, writes raw bytes to stdout, and deletes it. No binary data travels over JSON-lines RPC. Key details: - Only image/png supported for v1 - Clipboard shims use separate ClipboardRead RPC, not host-exec Exec RPC - Shims are regular script files (not symlinks to _shim dispatcher) - 20 MB size limit, stale file pruning >1 hour - xclip write mode rejected with error message
Apple Container's `container build` doesn't resolve "." correctly when the process working directory is set via current_dir. Pass the absolute context path as a positional argument instead, which works with both Apple Container and Docker/Podman.
SOPS stores age secret keys and PGP material in ~/.config/sops/. A compromised guest build file could exfiltrate these via an allowlisted host command. Add the directory to DENY_READ_DIRS so both Seatbelt (macOS) and bwrap (Linux) block access.
The retina images plugin's buildStart hook fires async but Vite's dev server doesn't block module resolution on it. When index.md gets processed, the transform hook injects /_1x/ image references before generateRetina1x has finished creating the files, causing import resolution errors on first run in a new worktree. Store the generation promise and await it in the transform hook so markdown processing is blocked until images exist.
The dashboard rendered tmux style codes like #[fg=#a6e3a1] as literal text because ratatui uses its own styling system, not tmux's. Meanwhile the tmux status bar handled them correctly since tmux interprets its own format codes natively. Add a tmux style parser (parse_tmux_styles) that converts #[...] blocks into ratatui styled spans. Change get_status_display() to return Vec<(String, Style)> instead of (String, Color), matching the pattern already used by git and PR column formatting. Supported tmux attributes: fg=, bg=, default/none. Color formats: hex (#RRGGBB), named colors (red, blue, etc.), indexed (colour0-colour255). Malformed or unknown directives are handled gracefully. Closes raine#66
The previous architecture defined _workmux (clap-generated) and _workmux_dynamic (wrapper), relying on `compdef _workmux_dynamic workmux` inside the autoloaded function to swap the completion handler. This reassignment didn't reliably take effect for subsequent completions when loaded via zsh's fpath autoloading mechanism. Restructure so the dynamic wrapper IS the _workmux function: - Post-process clap's zsh output via prepare_zsh_base() to rename _workmux -> _workmux_base and strip clap's autoload/eval block - The dynamic wrapper is now _workmux itself, delegating flag completion to _workmux_base and positional args to the dynamic helpers (_workmux_handles, _workmux_git_branches) - _comps[workmux] stays _workmux throughout -- no compdef swap needed Works correctly with both `eval "$(workmux completions zsh)"` and file-based fpath autoloading.
Commands like open, remove, merge, add only accept worktree handles or branch names, never file paths. Previously _workmux_base was called alongside the dynamic completers, which dragged in _default (file completion) via clap's generated _arguments positional specs. Stop calling _workmux_base for positional args on these commands. Flag completion still delegates to _workmux_base when the current word starts with -.
\`"${(@f)$(cmd)}"\` on empty command output produces a single-element
array containing an empty string. compadd then offers that empty string
as a completion candidate, resulting in phantom whitespace.
Filter empty strings from the array with \`${handles:#}\` and skip
compadd entirely when there are no candidates.
git worktree prune silently skips locked worktrees. If git worktree add was interrupted, it leaves a "locked" file (containing "initializing") in $GIT_COMMON_DIR/worktrees/<name>/ that never gets cleaned up. This caused wm rm to fail with "cannot delete branch used by worktree" because prune didn't remove the metadata. Before pruning, the cleanup now resolves the worktree admin directory (by reading the .git file's gitdir pointer, with a fallback to the conventional path) and removes any lock file. This applies to both the synchronous and deferred cleanup paths. Relative gitdir paths are normalized to absolute, and parse failures are logged.
Allow users to configure a custom command for LLM-based branch name generation instead of the hardcoded `llm` CLI. The command string is parsed with shlex into program + fixed arguments, and the composed prompt (system prompt + user input) is appended as the final argument at execution time. For example, `command: "claude -p"` becomes `claude -p "<prompt>"`. When `command` is set, the `model` option is ignored since it is specific to the `llm` CLI. When `command` is absent or empty, behavior is unchanged and `llm` is used as before. Implementation splits the monolithic generate_branch_name into a dispatcher (run_generator_command) that routes to either run_custom_command or run_llm_command, with shared prompt composition and branch name sanitization.
When no auto_name.command is configured, automatically use the
configured agent's CLI for branch name generation with a fast/cheap
model. The resolution order is:
1. auto_name.command (explicit config)
2. Agent profile default (claude, gemini, codex, opencode)
3. llm CLI fallback
Agent profile defaults:
- claude: claude --model haiku -p
- gemini: gemini -m gemini-2.5-flash-lite -p
- codex: codex exec --config model_reasoning_effort="low" -m gpt-5.1-codex-mini
- opencode: opencode run
Additional improvements from review feedback:
- auto_name.command is now global-only for security (prevents RCE
via malicious .workmux.yaml)
- Deep merge for auto_name config (project can override model,
system_prompt, background but not command)
- Custom commands now pipe prompt via stdin instead of argv
(avoids E2BIG and process listing exposure)
- Spinner shows which tool is being used ("Generating branch name
with claude")
- Error messages fall back to stdout when stderr is empty
- Whitespace-only command no longer bypasses agent inference
Introduce src/vcs/ module with: - Vcs trait encapsulating all VCS operations needed by workmux - GitVcs implementation delegating to existing git:: functions - JjVcs stub implementation with todo errors for future phases - detect_vcs() factory function that walks up from CWD to find .jj or .git - VcsStatus type alias for GitStatus This is Phase 1 of jj support: purely additive, no existing behavior changes. The existing git:: module remains intact and all callers still use it directly.
Replace all direct git::* calls with Vcs trait calls throughout the codebase. WorkflowContext now holds vcs: Arc<dyn Vcs> and exposes shared_dir (renamed from git_common_dir). All workflow/ modules use context.vcs.* methods. All command/ modules detect VCS via vcs::detect_vcs() or vcs::try_detect_vcs(). cli.rs and config.rs use VCS-agnostic repo discovery. Key changes: - DeferredCleanup stores pre-computed vcs_cleanup_commands instead of git_common_dir, keeping it VCS-agnostic - workflow::list() and agent_resolve functions take &dyn Vcs param - GitStatus type alias renamed to VcsStatus - All worktree terminology replaced with workspace in VCS calls - git::parse_remote_branch_spec kept as git-specific string parsing
Implement the core jj operations needed for workmux add/list/open: Repo detection: - get_repo_root/get_repo_root_for via .jj/ directory walking - get_shared_dir returns repo root (where .jj/ lives) - has_commits always true (jj has root commit) Workspace lifecycle: - create_workspace: jj workspace add + jj bookmark create - list_workspaces: parse jj workspace list + metadata lookup - find_workspace: match by handle, bookmark, or workspace name - prune_workspaces: forget workspaces whose paths no longer exist Metadata storage: - set/get_workspace_meta via jj config set/get --repo - get_all_workspace_modes by parsing .jj/repo/config.toml - remove_workspace_meta by editing config.toml directly Branch/bookmark operations: - get_default_branch: probe main/master bookmarks - branch_exists via jj bookmark list with template - get_current_branch via jj log -r @ -T bookmarks - list_checkout_branches, delete_branch, get_merge_base - get_unmerged_branches via revset queries Status operations: - has_uncommitted_changes via jj diff --stat - has_staged/unstaged both map to has_uncommitted (no staging area) - has_untracked_files always false (jj auto-tracks) - get_status with conflict detection via conflicts() revset Also implements: - Base branch tracking via jj config (workmux.base.<branch>) - Deferred cleanup commands (workspace forget, bookmark delete) - Unit tests for workspace list and diff stat parsing
Implement the merge/cleanup operations needed for workmux merge/remove: Merge operations: - commit_with_editor: jj commit (prompts for description) - merge_in_workspace: jj new @ <branch> (creates merge commit) - rebase_onto_base: jj rebase -s @ -d <base> - merge_squash: jj squash --from <branch> --into @ - switch_branch: jj edit <bookmark> - reset_hard: jj restore (restores working copy to parent) - abort_merge: jj undo (undoes last operation) - stash_push/pop remain no-ops (jj auto-commits working copy)
Implement remote operations for jj repos with git backend: Remote operations: - list_remotes: jj git remote list (parse name/url pairs) - remote_exists, add_remote: jj git remote add - set_remote_url: remove + re-add (jj has no set-url) - get_remote_url: parse jj git remote list output - fetch_remote: jj git fetch --remote <name> - fetch_prune: jj git fetch (auto-prunes) - ensure_fork_remote: construct fork URL from origin, reuses git-url-parse for URL manipulation - get_repo_owner: parse owner from origin remote URL Helper functions: - parse_owner_from_url: extract owner from HTTPS/SSH git URLs - construct_fork_url: build fork URL preserving host and protocol
Add comprehensive unit tests for jj VCS implementation: Parser tests: - Workspace list parsing (single, multiple, empty, descriptions, hyphenated names) - Diff stat totals parsing (full, insertions-only, deletions-only, empty, single file, no changes) URL parsing tests: - HTTPS, SSH, HTTP formats - GitHub Enterprise domains - Invalid URLs, local paths Cleanup command tests: - Full cleanup (workspace forget + bookmark delete + config unsets) - Keep-branch mode (no bookmark delete) - Special characters in paths (shell quoting) Metadata tests: - TOML config parsing for get_all_workspace_modes - Verifies section header matching and mode extraction Behavioral tests: - VCS name returns "jj" - has_commits always returns true (jj has root commit) - stash_push/pop are no-ops (jj auto-commits) - has_untracked_files always false (jj auto-tracks) 26 jj-specific tests total.
Add jj workspace mentions alongside git worktree references across all docs, skills, and README. workmux now supports both Git and jj as VCS backends via the Vcs trait, so the documentation should reflect this. Key changes: - Add jj as alternative VCS in taglines, descriptions, and requirements - Rename "Git worktree caveats" page to "Worktree caveats" with git-specific sections marked - Add VCS detection step (Step 0) to merge and rebase skills - Add jj command equivalents to merge strategies and remove --gone flag - Broaden VCS-specific wording (e.g., "gitignored" → "ignored")
git worktree prune silently skips locked worktrees. If git worktree add was interrupted, it leaves a "locked" file in $GIT_COMMON_DIR/worktrees/<name>/ that prevents prune from cleaning up the metadata, causing "cannot delete branch used by worktree" errors during cleanup. Add resolve_worktree_admin_dir() to GitVcs which reads the worktree's .git file to find the admin directory (with fallback to the conventional path), then: - remove_workspace_lock(): resolves admin dir and removes the lock file before pruning in the synchronous cleanup path - build_cleanup_commands(): includes an rm -f <locked> command before the prune command in deferred cleanup scripts Both methods are no-ops in JjVcs since jj has no equivalent lock mechanism.
|
Thanks for the PR.
Maybe, eventually as an additional VCS backend alternative to git, with a single new documentation page covering it. It's far too niche to be woven into every existing documentation page. This PR touches 40+ files and adds jj mentions, parenthetical alternatives, and terminology changes throughout the entire docs, README, and skill files. That level of interleaving dilutes the clarity of the existing documentation for the 99% of users who use git. If support were to land, I'd prefer it to be contained to a single dedicated docs page (e.g., "Using workmux with jj") covering setup, differences, and caveats, with at most a brief mention and link from the main docs. The core documentation should remain git-focused. Same way as tmux is the primary multiplexer backend. |
a657335 to
a475cb1
Compare
This PR adds support for jj workspaces. I attempted to start a discussion in #59 - making this draft PR as a next step in seeing if there's any interest to support this.