Repository navigation
Conversation
Rex (https://www.superlogical.com/rex) is detected via $REX_BLOCK, after tmux and Zellij so a multiplexer started inside Rex still wins. Rex sessions map to sessions, windows (tabs) to windows, and blocks to panes. The backend drives Rex only through the rex CLI. Pane listing runs one Lua script through `rex do`, which returns every pane on the server with its process, working directory and title in a single call. Refs raine#341
Add RexEnvironment so the suite can run with --backend=rex. Each test gets its own session on the running Rex server and removes it after, like the WezTerm workspaces. The 'test:' target follows tmux and sends to the session's active pane.
Behaviour: - Pass `--` to `rex send` so text starting with "-" isn't read as a flag - Capture with `--trim` so previews don't end in blank rows - Scope `block.close` to the pane's session; it failed outside it - Make switching the Rex app's session best effort. Without Remote Control, print the session to open instead of failing after the work is done - List sessions that have no panes, so session_exists and kill_session see them, and prefer the current session when labels repeat - Check Rex last in detection, so kitty started from a Rex pane wins, and ignore an empty REX_BLOCK - Return an error from resolve_instance_id when REX_SERVER is missing, like WezTerm and kitty - Implement zoom_pane, rename_window, rename_window_at_pane and rename_session Structure: - Name detection signals in a struct instead of five bools - Derive the accepted WORKMUX_BACKEND names from BackendType::ALL - Share REX_BLOCK detection with the status hook - Pull repeated lookups and JSON into helpers, in rex.rs and conftest.py Docs: - Add a Rex guide page and mention Rex in the README, the guide index and CONTRIBUTING
- Add Multiplexer::supports_session_mode, true for tmux and Rex, and use it instead of name checks in create and open - Start Rex panes as `$SHELL -l` without `shell: "none"`, which routed terminals through login(1) and reset HOME and PATH - Skip empty text in Rex send - Run each Rex test on a private Rex server, like tmux's private socket - Make session test helpers backend neutral: list_sessions, list_windows_in_session, session_pane_path, new_session - Document session mode on Rex
The deferred cleanup worker waits for the source session to close by its stable ID, but the Rex backend never reported one, so the worker stopped and left the worktree and branch behind. - Report the current session ID from REX_SESSION, and close sessions by ID. - Let session_exists take a session ID as well as a label. - Run the snapshot's `rex do` without REX_SESSION and REX_BLOCK. The worker inherits them from the session it closed, and `rex do` fails when they name a session that is gone and more than one other session exists. - Add a Cmd::env_remove option for that. - Test removal from inside a session on tmux and Rex, with a second session open.
|
This one includes all of #344, so merging it alone covers both. I split them because I thought two smaller PRs would be easier to read. #344 is the backend itself, and this PR adds session mode, the full test suite on Rex and the remove fix on top. If you merge #344 first, this diff shrinks to the last two commits. If you'd rather do it in one go, merge this and close #344. |
Detection checks Rex last, after tmux, WezTerm, Zellij and kitty, since every terminal started from a Rex pane inherits REX_BLOCK. Detection signals move into a BackendSignals struct, and BackendType::ALL with expected_names() lists the backends in one place. The mod.rs and types.rs seams are taken from raine#345 so the fork stays rebaseable on it; supports_session_mode is left to WR-3. Label lookups (kill_session, kill_window and the rest) now act on one match, the caller's own session or window when it matches, never every window that shares the label. Rex key names were checked live against cat -v: delete, pageup and pagedown are what Rex documents and accepts, and dc is rejected. An empty send_text makes no call. Agent scans skip floating layers, which hold the dashboard and sidebar. A refused app switch says to turn on Remote Control in Rex Settings. An unsupported server now stops add with the version message instead of reporting that Rex is not running. Approach follows raine#344 by agawish Co-authored-by: Amr Gawish <amr@gawi.sh> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused or ambiguous `session.select` failed `add` and `open` after the worktree and window were already made. The switch is now a courtesy: workmux asks one app client with `-C`, and on refusal prints the Remote Control fix once and carries on. Only the app showing the caller's pane is moved, as tmux moves only the clients on the caller's session. Rex.app attaches to every session in its sidebar, so `session_ids` alone would still move an app showing another session; the app must also own the caller's block. Call-site tests now cover the app switch, one-match kills, the parent session lookup and the pane scans over onboarding and floating blocks. The version check is keyed on the canonical socket, `workmux list` names an unsupported server instead of listing nothing, the unread `WindowRecord.block_id` is gone, and the Rex test fixture stops its server when startup fails. rex.mdx describes detection, nesting, window ownership and Remote Control as they now work. B2 scan filter and S4 test helpers follow raine#344/raine#345 by agawish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unify the Rex.app switch on WR-2's show_on_app: one selector
(app_showing_caller, deciding through shows_caller), one select path
(rex -C <app> do session.select). WR-3's select_on_app, app_clients_for
and client.perform select are deleted; session close and the deferred
close now move only the app showing the caller.
show_on_app returns AppSwitch { Shown, Refused, NoApp } and prints
nothing. CLI paths print the Remote Control fix once; the new
jump_to_pane trait method fails on a refusal, so the dashboard shows the
fix in its status line and stays open instead of closing after a jump
that never happened.
select_pane no longer switches the app, as tmux select-pane moves no
client, so a window-mode add asks the app once. The registry no longer
lists sessions outside its lock: create records without listing, and
closing prunes gone sessions under the lock instead of forgetting the
closing session before its deferred destroy. The session-mode error
text matches raine#345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I've been running workmux on Rex for a while too, and ran into a few Rex behaviors that #345 may hit. Sharing in case it saves you some time. All of these are on Rex 0.0.1793377904 (API revision 8), and I'm reporting them to the Rex team.
If #345 lands, I'd be glad to send small follow-ups for things I've needed: honoring |
|
Thanks, this is really useful! Hadn't come across I went through the quirks against #345:
The floating layer one doesn't affect workmux, but it explains something I'd noticed with popups in my own Rex config. And yes please to the follow-ups once #345 lands: |
|
One more Rex issue I hit with #345, separate from the list above.
Repro: The worktree records its parent session ( Fix: override Patch against patchdiff --git a/src/multiplexer/rex.rs b/src/multiplexer/rex.rs
index aeb4af01..e04675cc 100644
--- a/src/multiplexer/rex.rs
+++ b/src/multiplexer/rex.rs
@@ -237,6 +237,30 @@ impl RexSnapshot {
.next()
.ok_or_else(|| anyhow!("Window '{}' not found", full_name))
}
+
+ /// The window a target names: by label within its parent session when it
+ /// has one, otherwise as `window_panes` resolves the label.
+ fn target_window(&self, target: &WindowTarget) -> Option<&RexPane> {
+ match target.parent_session() {
+ Some(session) => self
+ .panes
+ .iter()
+ .find(|p| !p.floating && p.window == target.full_name && p.session == session),
+ None => self.window_panes(&target.full_name).into_iter().next(),
+ }
+ }
+
+ fn require_target_window(&self, target: &WindowTarget) -> Result<&RexPane> {
+ self.target_window(target)
+ .ok_or_else(|| match target.parent_session() {
+ Some(session) => anyhow!(
+ "Window '{}' not found in session '{}'",
+ target.full_name,
+ session
+ ),
+ None => anyhow!("Window '{}' not found", target.full_name),
+ })
+ }
}
/// A connected client as reported by `rex client ls --json`.
@@ -729,6 +753,55 @@ impl Multiplexer for RexBackend {
util::run_detached_sh_c(script)
}
+ // Window targets carry the parent session a window-mode worktree was
+ // opened in. Without these, the defaults look only in the session workmux
+ // runs from, so `close` from another session reports no open window.
+
+ fn window_target_exists(&self, target: &WindowTarget) -> Result<bool> {
+ Ok(self.snapshot()?.target_window(target).is_some())
+ }
+
+ fn kill_window_target(&self, target: &WindowTarget) -> Result<()> {
+ let snapshot = self.snapshot()?;
+ if let Some(pane) = snapshot.target_window(target) {
+ self.call_api("session.close_window", pane.window_ref())
+ .context("Failed to close Rex window")?;
+ }
+ Ok(())
+ }
+
+ fn schedule_window_target_close(&self, target: &WindowTarget, delay: Duration) -> Result<()> {
+ let snapshot = self.snapshot()?;
+ let Some(pane) = snapshot.target_window(target) else {
+ return Ok(());
+ };
+ let script = format!(
+ "sleep {}; {}",
+ delay.as_secs_f64(),
+ Self::close_window_shell_cmd(pane)
+ );
+ util::run_detached_sh_c(&script)
+ }
+
+ fn shell_kill_window_target_cmd(&self, target: &WindowTarget) -> Result<String> {
+ let snapshot = self.snapshot()?;
+ Ok(Self::close_window_shell_cmd(
+ snapshot.require_target_window(target)?,
+ ))
+ }
+
+ fn select_window_target(&self, target: &WindowTarget) -> Result<()> {
+ let snapshot = self.snapshot()?;
+ let pane = snapshot.require_target_window(target)?;
+ if !Self::is_current_session(&pane.session_id) {
+ let label = snapshot.session_label(&pane.session_id).unwrap_or_default();
+ self.show_session(&pane.session_id, label, Some(&pane.window_id));
+ }
+ self.call_api("session.focus_window", pane.window_ref())
+ .context("Failed to focus Rex window")?;
+ Ok(())
+ }
+
fn shell_select_window_cmd(&self, full_name: &str) -> Result<String> {
let snapshot = self.snapshot()?;
let pane = snapshot.require_window(full_name)?;
diff --git a/tests/test_workmux_close.py b/tests/test_workmux_close.py
index 6c606a71..147d21ab 100644
--- a/tests/test_workmux_close.py
+++ b/tests/test_workmux_close.py
@@ -182,15 +182,24 @@ def test_close_does_not_adopt_same_name_window_from_another_directory(
assert impostor_id in window_ids
-@pytest.mark.tmux_only
def test_close_window_in_parent_session(
- mux_server: TmuxEnvironment, workmux_exe_path: Path, mux_repo_path: Path
+ mux_server: MuxEnvironment, workmux_exe_path: Path, mux_repo_path: Path
):
+ """Close finds a window in its parent session from another session."""
env = mux_server
+ if env.backend_name not in ("tmux", "rex"):
+ pytest.skip(f"{env.backend_name} has no parent sessions")
branch_name = "feature/parent-session-close"
session_name = "prs-close"
window_name = get_window_name(branch_name)
+ def windows_in_parent() -> list[str]:
+ # The parent session may be gone once its last window closes
+ try:
+ return env.list_windows_in_session(session_name)
+ except Exception:
+ return []
+
write_workmux_config(mux_repo_path)
run_workmux_command(
env,
@@ -199,21 +208,12 @@ def test_close_window_in_parent_session(
f"add {branch_name} --parent-session {session_name} --background",
)
- result = env.tmux(
- ["list-windows", "-t", f"{session_name}:", "-F", "#{window_name}"]
- )
- assert window_name in [w for w in result.stdout.strip().split("\n") if w]
+ assert window_name in windows_in_parent()
+ # Runs from the test session, not the parent session
run_workmux_close(env, workmux_exe_path, mux_repo_path, branch_name)
- assert poll_until(
- lambda: window_name
- not in env.tmux(
- ["list-windows", "-t", f"{session_name}:", "-F", "#{window_name}"],
- check=False,
- ).stdout.splitlines(),
- timeout=5.0,
- )
+ assert poll_until(lambda: window_name not in windows_in_parent(), timeout=5.0)
worktree_path = get_worktree_path(mux_repo_path, branch_name)
assert worktree_path.exists(), "Worktree should still exist after close"
|
`workmux close` failed for a window-mode worktree when run from a different Rex session than the one its window is in. RexBackend didn't override the WindowTarget methods, so the trait defaults only looked in the caller's session. Override window_target_exists, kill_window_target, schedule_window_target_close, shell_kill_window_target_cmd and select_window_target to find the window by label within the target's parent session, falling back to window_panes when there is none. test_close_window_in_parent_session now runs on Rex too. Co-authored-by: logan-broit <29020058+logan-broit@users.noreply.github.com>
|
Thanks for the patch, nice catch! It's in now as |
723a378 to
48bdc1d
Compare
|
Just commenting as a rando to say I've been running a workmux built from this PR for the past few days with Rex and it is working well for me. |
|
@ficusk I've been running it for the past 2 weeks and so, and it has been a treat, I know it is a massive piece of code, so happy to split it however the author likes, but in the same time I know he probably gets a lot of AI driven stuff, so I know he should be wary of anything indeed |
Stacked on #344. Until that one merges, this diff also shows its commits. The new work is the last two commits.
Summary
In #344 I left the Rex suite at 572 passed and 65 failed, mostly because session mode was tmux-only. This PR adds session mode on Rex and gets the full suite passing there.
I use session mode with Rex every day, so each worktree gets its own Rex session with its
windows:layout.What changed
Multiplexer::supports_session_mode()replaces thetmuxname checks increate.rsandopen.rs. It's true for tmux and Rex.$SHELL -l, the same as tmux. Without a command, a Rex terminal goes through login(1), which resetsHOMEandPATH, so panes lost the environment workmux runs in.send_textskips empty text, because Rex rejects it.workmux removefrom inside its own session now finishes the cleanup on Rex. The deferred worker waits for the session to close by its ID, and Rex never reported one. Rex now does, andsession_existstakes an ID too.REX_SESSIONandREX_BLOCKfrom the session it just closed.rex dofails on those when more than one other session is open, so the pane snapshot runs without them. I addedCmd::env_removefor that.Tests
RexEnvironmentnow starts a private Rex server for each test, on its own socket, state dir and empty config, like the tmux tests do. The Rex app doesn't need to run, and your own sessions are never touched.list_sessions,new_session,session_pane_pathandsend_keys_to_session. The session, naming, basic and headless tests use them in place ofenv.tmux(...).test_remove_from_inside_session. It keeps a second session open, which is what caught therex doproblem.Testing
cargo testpasses, 2165 tests.mainfor me: two dashboard base branch tests, two OpenCode v2 setup tests andtest_setup_token_reaches_claude_panes_on_the_host.test_add_prompt_file_injects_into_gemini. It's flaky under-n 8and passes on its own.workmux removefrom inside a session-mode worktree by hand on a real Rex server with several sessions open. The session, worktree and branch are all gone afterwards.Docs
The session mode guide, README,
addandopenreferences, the Rex guide and CONTRIBUTING now say session mode works on tmux and Rex.Switching the app to another session still needs Remote Control in Rex. Without it, workmux prints the session to open, same as in #344.