Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1025 +/- ##
==========================================
+ Coverage 81.98% 84.13% +2.14%
==========================================
Files 28 32 +4
Lines 2548 3120 +572
Branches 485 629 +144
==========================================
+ Hits 2089 2625 +536
- Misses 328 340 +12
- Partials 131 155 +24 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
d64904e to
db303db
Compare
Code reviewFound 4 issues:
tmuxp/src/tmuxp/workspace/builder.py Lines 674 to 680 in db303db
Lines 82 to 88 in db303db Lines 81 to 88 in db303db Lines 81 to 88 in db303db Lines 82 to 88 in db303db
Lines 326 to 334 in db303db
tmuxp/src/tmuxp/workspace/importers.py Lines 93 to 107 in db303db 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
60cd8fc to
92ba6f4
Compare
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Review scope (5 parallel agents):
Prior review rounds: 3 rounds of 3-model (Claude/Gemini/GPT) loom reviews found 16 issues, all fixed in subsequent commits on this branch. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code reviewFound 1 issue:
Lines 334 to 337 in aa69986 Lines 403 to 406 in aa69986 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code reviewFound 2 issues:
Lines 792 to 819 in 395b1e1
tmuxp/src/tmuxp/workspace/loader.py Lines 43 to 48 in 395b1e1 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
c8dae37 to
3246cb1
Compare
067bc1f to
ac94a3d
Compare
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
Code reviewFound 1 issue:
tmuxp/src/tmuxp/workspace/builder.py Lines 144 to 151 in 3d150bc Call site passing the un-normalized attribute: tmuxp/src/tmuxp/workspace/builder.py Lines 792 to 796 in 3d150bc 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
…eamocil Side-by-side comparison covering architecture, config keys, CLI commands, hooks, and config file discovery across all three tools.
- Remove duplicate 'Attach on create' row in comparison table, keep
corrected version with '(default: true)' near socket_path
- Annotate pre_tab as (deprecated) in comparison table
- Annotate startup_window as accepting name or index
- Fix pre_tab description: deprecated predecessor, not alias (it was
renamed in tmuxinator, not aliased)
- Clarify startup_window renders as "#{name}:#{value}"
- tmuxinator min tmux is 1.8 (recommended), not 1.5; tmux 2.5 is explicitly unsupported - teamocil has no documented min tmux version - tmuxinator detach is via `attach: false` config or `--no-attach` CLI flag, not `-d` (which doesn't exist in tmuxinator)
- Fix "1.5+" to "1.8+" in architecture description (was already fixed in overview table but missed in prose) - Clarify YAML anchors: tmuxinator enables via YAML.safe_load aliases param, not a config key - Clarify tmuxinator edit is alias of new command
Support both space-separated (-L mysocket) and attached (-Lmysocket) forms for -f, -L, and -S flags when parsing tmuxinator cli_args. Previously only the space-separated form was recognized; attached forms were silently dropped.
…cters render_template() now rejects values containing colons, braces, brackets, or newlines before substitution. These characters can corrupt YAML document structure when injected as raw text before parsing.
…broader Add note explaining that tmuxp's --no-shell-command-before strips at all levels (session/window/pane), which is intentionally broader than tmuxinator's --no-pre-window that only targets the window chain.
Wrap the client-detached hook command in a #{session_attached} guard
so it only fires when the last client detaches. This prevents
premature cleanup in multi-client scenarios (pair programming, SSH
drops) where other clients are still attached.
The dict was re-created inside import_tmuxinator() on every call. Move to module-level constant per Python convention for immutable lookup tables.
Add require_pane_resolution parameter to get_session(). When True, raises SessionNotFound instead of falling back to server.sessions[0] when TMUX_PANE is unset/stale. tmuxp stop now uses strict mode to prevent killing an unrelated session. Non-destructive commands (shell, freeze) retain the fallback behavior.
…end_keys
Replace send_keys("export ...") and send_keys(window_shell) with tmux
primitives in --here mode:
- Environment: session.set_environment() + respawn-pane -e (inherited
by new panes, no POSIX shell assumption)
- Shell replacement: respawn-pane -k (kills current process, starts
fresh shell — no typing into foreground programs)
- Directory: respawn-pane -c (tmux primitive, no send_keys cd)
This eliminates all send_keys usage for infrastructure setup in --here
mode, matching teamocil's approach of using tmux primitives over
send_keys. Fixes the fish/nu shell incompatibility and the "types
into vim" failure mode.
Closes #1031
Before calling respawn-pane -k, check pgrep -P <pane_pid> for child processes. If the shell has running children (background jobs, foreground programs), log a WARNING so users know their processes will be terminated. Gracefully handles missing pgrep.
…recovery New builder tests (NamedTuple + test_id pattern): - HereRespawnFixture: parametrized over 4 scenarios (dir-only, env-only, dir-and-env, nothing-to-provision) verifying PID changes on respawn, directory provisioning, and session environment - test_here_mode_respawn_multiple_env_vars: 3 env vars via set_environment - test_here_mode_respawn_warns_on_running_processes: background sleep job triggers pgrep WARNING before respawn-pane -k - test_here_mode_no_warning_when_pane_idle: idle pane produces no warning New load CLI tests (NamedTuple + test_id pattern): - HereErrorRecoveryFixture: parametrized over 2 scenarios verifying --here mode skips (k)ill option (choices=[a,d], default=d) while normal mode retains it (choices=[k,a,d], default=k)
why: on_project_start had been triggered before dispatch, so it also ran for paths that reused an existing session. That made --here rebuilds and interactive append flows execute a hook documented as new-session-only. what: - Move on_project_start execution into the attached and detached new-session load paths - Keep --here rebuilds inside tmux and append flows from invoking the hook - Preserve the outside-tmux --here fallback behavior, which still creates a new session - Add dispatch tests for attached, detached, append, and here routing - Add an on_project_exit guard assertion and fix the loader doctest ellipsis - Update the related load, comparison, and configuration docs to match current behavior
why: Keep the changelog aligned with the lifecycle behavior shipped on this branch so readers do not infer broader hook semantics than the code implements. what: - Document on_project_exit as running when the last client detaches - Match the guarded client-detached hook behavior in WorkspaceBuilder
…ures why: --here and --append reuse an existing tmux session. Startup failures must abort without destroying that live session, and duplicate target names must stop before any plugin hooks or before_script side effects run. what: - track whether build created the session before cleaning it up on failure - move the --here duplicate-session check ahead of startup hooks and script execution - add builder coverage for reused-session failures and pre-hook rename conflicts
why: --here is a current-window workflow. Accepting multiple workspace files silently changes behavior for earlier entries and leaves behind unexpected sessions instead of failing fast. what: - reject --here when more than one workspace file is provided - clarify the parser help text for the single-workspace contract - add CLI coverage that exits before any workspace is loaded - document the single-workspace restriction in the load guide
…port why: tmuxinator numeric startup_window and startup_pane values are tmux indices, not Python list offsets. Importing them as list positions changes which window or pane receives focus and breaks compatibility with existing configs, especially when base-index or pane-base-index are nonzero. what: - resolve numeric startup targets against tmux base-index and pane-base-index - read live tmux index settings in the tmuxinator import CLI path - add importer and CLI coverage for base-index aware conversion and fallback
…nd env intact why: append and --here reuse a live tmux session rather than creating a tmuxp owned session. Writing lifecycle hooks or stop metadata onto that reused session can overwrite unrelated teardown behavior, and copying first-pane environment into session state makes later windows inherit variables they were never meant to see. what: - limit on_project_exit, on_project_stop, and start_directory session metadata to sessions created by the current build - keep --here first-pane provisioning local to respawn-pane instead of the session environment - add reused-session and non-leaking here-mode tests around hooks and env
why: The unreleased entry removed the KEEP THIS PLACEHOLDER insertion anchor and skipped the lead paragraph and fixed subheadings that every published release uses, while the importer section leaked internal key-mapping mechanics into user-facing notes. what: - Restore the placeholder block; entries land below the END marker - Open with a release lead paragraph; group deliverables under What's new - Collapse importer key-mapping bullets into one prose deliverable - Cross-reference cli-stop, top-level, cli-import, and comparison docs - Drop tmux mechanism asides (select-pane -T, client-detached hook)
why: Dated "as of" claims rot immediately (AGENTS.md brittle-references rule); the page read as three months stale despite ongoing updates. what: - Remove the Last updated line; the Version table row remains the durable anchor for which tmuxinator/teamocil releases were compared
…rror recovery test why: AGENTS.md testing guidelines require monkeypatch over unittest.mock; the MagicMock builder also hid which attributes _dispatch_build actually touches, weakening the test's contract. what: - Stub the failing load paths with typed raising functions matching the real loader signatures, registered via monkeypatch.setattr - Stand in for the builder with a minimal DummyBuilder exposing only session, mirroring the dispatch-loaders test's existing pattern - Promote the exc import to module level alongside cli
why: AGENTS.md requires working doctests; both entry points were substantially rewritten for parity (new keyword params, v1.x support) yet only their helpers gained examples, and the new base_index / pane_base_index parameters were undocumented. what: - Add Examples covering session_name, window, and pane conversion for both importers, plus the pre -> on_project_start hook mapping - Document base_index and pane_base_index in the Parameters section
…ost-build fan-out why: synchronize: after desugars to options_after synchronize-panes=on, and tmux mirrors send-keys input across panes while that option is on. Applying options_after first made every shell_command_after entry (and clear) run once per pane per send — four executions in a two-pane window instead of two. what: - Reorder config_after_window: shell_command_after, then clear, then options_after - Add test asserting each pane runs the after-command exactly once while synchronize-panes still ends up enabled
why: --here is normally invoked from the pane being reused, so respawn-pane -k killed the foreground tmuxp process mid-build: pane commands were never sent and remaining windows never built. The existing tests drove the builder from outside the pane, so the self-kill path was unreachable in CI. what: - Add _running_inside_pane() predicate (TMUX/TMUX_PANE plus socket comparison when the server socket path is known) - Self-pane fallback: environment via session set-environment (inherited by the panes the build creates), directory via quoted cd send-keys, window_shell skipped with a warning; other panes keep the respawn-pane path - Scrub ambient TMUX_PANE in builder tests so the developer's own pane id cannot collide with fresh test-server pane ids - Document both provisioning paths in the --here note
…omparison why: libtmux Server.socket_path may hold a pathlib.Path, and str != Path is always True, so _running_inside_pane misclassified a genuine self-pane as not-self for programmatic callers — falling through to respawn-pane -k and killing the tmuxp process mid-build, the exact failure the guard exists to prevent. what: - Compare the TMUX env socket against str(socket_path); widen the parameter to accept pathlib.Path and add a Path doctest case - Document that socket_path=None skips the cross-server check and errs toward the kill-free provisioning path - Rescope the respawn rationale comment to the respawn branch; the block header no longer claims "no typing into foreground programs" above the self-pane branch that sends a cd
why: find_workspace_file resolves any existing direct path, so `tmuxp delete -y README.md` unlinked an arbitrary file. The command's contract is deleting workspace configs; destruction happened before any validation. what: - Require the resolved path's extension to be .yaml/.yml/.json before os.remove; refuse with a warning and exit code 1 otherwise - Add parametrized refusal tests (markdown, plain text) asserting the file survives and the exit code - Add a refusal doctest and a docs note on the constraint
why: The 120s subprocess timeout silently killed long-running hooks (database setup, docker compose teardown) and continued anyway — on_project_stop could leave cleanup half-done while the session was still killed afterward. The hook docs promise the commands run at lifecycle boundaries with no mention of a limit, and tmuxinator (the parity target) imposes none. what: - Drop the timeout and the TimeoutExpired handler; OSError handling and the non-zero-exit warning stay - Log a structured INFO record when hooks start, so long waits are attributable - Add a test pinning the no-time-limit contract and a docs note that hooks block until completion (output captured; Ctrl-C interrupts)
…sts handling why: The session-exists guard skipped its prompt-and-reattach path for here=True unconditionally, but the outside-tmux fallback to a normal attach happened later in _dispatch_build — so a running session reached Server.new_session() and crashed with libtmux's TmuxSessionExists, which the TmuxpException recovery prompt does not catch. what: - Normalize here=False (with the relocated warning) at the top of load_workspace when no TMUX client is present, so the existing-session prompt, on_project_restart, and attach flow behave like a normal load - Drop the now-unreachable outside-tmux fallback from _dispatch_build and document that its here path requires a tmux client - Replace the dispatch-level fallback fixture with a load_workspace-level test: existing session + --here outside tmux now prompts to attach instead of crashing
…ively why: The new deletion guard compared extensions case-sensitively while finders, search, and ls all lowercase before matching, so an explicitly-typed .YAML path was refused on case-insensitive filesystems even though tmuxp load accepts it. what: - Lowercase the splitext result before checking VALID_WORKSPACE_DIR_FILE_EXTENSIONS, matching the discovery code's established pattern - Add parametrized tests deleting uppercase .YAML/.JSON paths
why: The hook start record tagged a user shell string with tmux_cmd, whose schema contract is "tmux command line" and which downstream consumers may filter on; the message also used progressive tense against the past-tense-for-events standard. what: - Log "hook commands started" with a dedicated tmux_hook_cmd key - Register tmux_hook_cmd in the AGENTS.md core key table - Test the INFO record's key, value, message, and the absence of tmux_cmd on the hook path
why: The help text claimed resolution parity with tmuxp load without mentioning that delete refuses non-workspace files, implying identical accept/reject behavior between the two commands. what: - State in the --help intro that only .yaml/.yml/.json files are deleted, matching the docs page note
Summary
Bring tmuxp to feature parity with tmuxinator and teamocil. This adds the missing CLI commands, config keys, lifecycle hooks, config templating, and importer improvements that users migrating from those tools expect — plus a full feature comparison page and documentation for every new command, flag, and config key.
New CLI commands
tmuxp stop— kill a session with cleanup$ tmuxp stop mysessionRuns the
on_project_stoplifecycle hook before killing the session, giving projects a chance to tear down background services, save state, etc.tmuxp new— create a workspace config$ tmuxp new myprojectCreates a new workspace config from a minimal template and opens it in
$EDITOR.tmuxp copy— copy a workspace config$ tmuxp copy myproject myproject-backupCopies an existing workspace config to a new name. Source is resolved using the same logic as
tmuxp load.tmuxp delete— delete workspace configs$ tmuxp delete old-projectDeletes workspace config files. Prompts for confirmation unless
-yis passed.Lifecycle hooks
Workspace configs now support four hooks, matching tmuxinator's hook system:
on_project_starton_project_restarton_project_exitclient-detachedhook)on_project_stoptmuxp stopkills the sessionConfig templating
Workspace configs now support
{{ variable }}placeholders with values passed via--set. Givenmytemplate.yaml:$ tmuxp load --set project=myapp mytemplate.yamlNew config keys
Pane titles
synchronizeshorthandDesugars
synchronize: before→options: {synchronize-panes: on}andsynchronize: after→options_after: {synchronize-panes: on}.trueis equivalent tobefore.shell_command_afterandclearNew
tmuxp loadflags--here--no-shell-command-beforeshell_command_beforeentries--debug--set KEY=VALUEImporter improvements
tmuxinator
pre→on_project_start,pre_window→shell_command_beforecli_args(-f,-S,-L) parsed into tmuxp equivalentssynchronizewindow key convertedstartup_window/startup_pane→focus: trueon the targettitleon the panestr(fixes numeric/emoji YAML keys)teamocil
windowsat top level,commandskey in panes)focus: trueon windows and panes convertedoptionspassed throughDesign decisions
{{ var }}substitution, not Jinja2 — noconditionals or loops. Keeps configs declarative and avoids a template-engine
dependency;
--setvalues are validated against YAML-unsafe characters.--hereprovisions the reused pane withrespawn-pane -krather thantyping
cd/exportinto the running shell — no POSIX-shell assumption, nokeystrokes landing in foreground programs. tmuxp warns first if the pane has
running child processes, since
-kkills them.on_project_exitrides tmux'sclient-detachedhook, so it fires on anyclient detach — not only
tmuxp-initiated ones.Documentation
docs/comparison.md): Side-by-side of tmuxp vs tmuxinator vs teamocil — architecture, config keys, CLI flags, hooksdocs/configuration/top-level.md): New keys, lifecycle hooks, synchronize, pane titlesdocs/configuration/examples.md): Working examples for each new featureRelated issues
--hereprovisions viaset_environment+respawn-pane)on_project_first_starthook), here mode level 2: eliminate send_keys entirely — split-window/swap-pane/kill-pane #1032 (here mode withoutsend_keys),heremode:send_keysfor env vars and shell replacement diverges from teamocil and is unsafe #1030 (parent)Test plan
uv run py.testpassestmuxp loadwith lifecycle hooks,--here,--debug,--settmuxp stop,tmuxp new,tmuxp copy,tmuxp delete