Skip to content

Add configurable tmux/Zellij multiplexer backends - #132

Open
sysadmin-metrum-ai wants to merge 4 commits into
nutthouse:mainfrom
sysadmin-metrum-ai:feature/zellij-multiplexer
Open

Add configurable tmux/Zellij multiplexer backends#132
sysadmin-metrum-ai wants to merge 4 commits into
nutthouse:mainfrom
sysadmin-metrum-ai:feature/zellij-multiplexer

Conversation

@sysadmin-metrum-ai

@sysadmin-metrum-ai sysadmin-metrum-ai commented May 27, 2026

Copy link
Copy Markdown

Maintainer Attention

This PR is locally verified and the current CodeRabbit status check is passing, but GitHub still reports the PR as blocked because:

  • the fork PR CI workflow is action_required and needs a maintainer to approve/run it
  • the old CodeRabbit CHANGES_REQUESTED review remains attached even though follow-up commits resolved the substantive comments and the latest CodeRabbit check is green

Latest local verification: cargo test --quiet passed with 460 unit tests and 2 integration tests.

Summary

  • add [orchestrator].multiplexer_type with backwards-compatible tmux default and zellij support
  • introduce a synchronous multiplexer abstraction with tmux and Zellij backends for launch, attach, kill, alive checks, pane capture, prompt send, and enter presses
  • route the existing session facade through the configured backend so CLI, health, workflow, dashboard capture, and automation paths continue to work
  • update docs, defaults, doctor checks, changelog, and add Zellij integration coverage

Versioning

  • SemVer choice: MINOR behavior addition for configurable multiplexer backend support
  • Version selected: no Cargo.toml version bump in this PR; release version should be selected by the maintainer during the normal release train
  • Cargo.toml: unchanged intentionally
  • CHANGELOG: updated under Unreleased
  • Release tag: none created by this PR

Verification

  • cargo test --quiet
    • 460 unit tests passed
    • 2 integration tests passed, including Zellij send --auto-up, peek, and down
  • Zellij workflow verification in an isolated temporary workspace:
    • tt run verify-zellij --strict
    • workflow steps covered ensure_running, prompt send/wait, tt peek marker verification, and cleanup

Notes

The Zellij multiplexer path includes provenance from downstream operator use in the Metrum AI fork, now documented without a prominent personal callout.

Summary by CodeRabbit

  • New Features

    • Zellij is now supported as a terminal multiplexer alongside tmux.
    • Configure your preferred multiplexer and backend-specific settings in tutti.toml.
  • Documentation

    • Updated prerequisite requirements to include Zellij.
    • Added new "Terminal Multiplexers" section with configuration examples for both backends.
    • Enhanced tt doctor validation for multiplexer availability.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a pluggable multiplexer abstraction that enables Tutti to control terminal sessions through either tmux or Zellij, selected via configuration. The codebase transitions from hardcoded tmux commands to a trait-based backend system with dual implementations, refactoring the session layer to delegate to the selected backend at runtime.

Changes

Multiplexer abstraction and dual-backend support

Layer / File(s) Summary
Configuration types and error handling
src/config/mod.rs, src/config/defaults.rs, src/error.rs, src/main.rs
TuttiConfig gains orchestrator and multiplexer fields; new enum MultiplexerType (Tmux/Zellij) and backend-specific config structs (TmuxMultiplexerConfig, ZellijMultiplexerConfig) with defaults. Error type TmuxError replaced with MultiplexerError. Default config template adds commented multiplexer examples.
Multiplexer trait and runtime selection
src/multiplexer/mod.rs
Introduces SessionMetadata, Multiplexer trait with 9 lifecycle/I/O methods, RuntimeMultiplexerConfig, and a global CURRENT_CONFIG store. set_current_config() registers backend selection; current_backend() lazily loads config and constructs selected backend; create_multiplexer() maps MultiplexerType to concrete constructors. Includes helpers for shell escaping and env var filtering.
TmuxBackend implementation
src/multiplexer/tmux.rs
Implements TmuxBackend struct and full Multiplexer trait: detached session creation with env var management, interactive attach, kill, liveness checks, pane capture, multiline text sending via tmux buffers, Enter key presses, and status bar configuration.
ZellijBackend implementation
src/multiplexer/zellij.rs
Implements ZellijBackend struct and full Multiplexer trait: session creation with shell script injection, pane detection via JSON parsing, text input routing (write-chars vs paste), screenshot capture with tail trimming. Includes helpers for launch script generation and pane id parsing.
Session layer delegation to backend
src/session/tmux.rs
Refactors TmuxSession and check_tmux() to delegate all operations to current_backend() instead of executing tmux commands directly. Removes prior tmux-specific logic, env-var handling, and buffer paste helpers that moved to backends.
Doctor and state error handling
src/cli/doctor.rs, src/state/mod.rs
Doctor now checks multiplexer availability based on config.orchestrator.multiplexer_type instead of hardcoding tmux. State classification adds MultiplexerError handling with guidance to ensure multiplexer installation and session accessibility.
Test fixture updates
src/automation/mod.rs, src/budget/mod.rs, src/cli/{doctor,handoff,up,watch}.rs
Test helpers across multiple modules initialize new orchestrator and multiplexer config fields with Default::default().
Zellij integration test and documentation
tests/send_auto_up_integration.rs, README.md, CHANGELOG.md
Adds zellij_is_usable() preflight check and zellij_send_auto_up_wait_output_preserves_long_prompt integration test. Updates README to document tmux-or-Zellij support with configuration example and Terminal Multiplexers section. CHANGELOG entries document new multiplexer configuration and Zellij-backed session capabilities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • nutthouse/tutti#117: Modifies TuttiError variants; overlaps at the same error-definition code level.
  • nutthouse/tutti#107: Adds /v1/agents/{ws}/{agent}/focus; depends on pane-capture behavior now routed through the multiplexer abstraction.

🐰 A rabbit's ode to the multiplexer maze:

Once tmux ruled alone so proud,
Now Zellij joins the session cloud,
With traits and backends, clean and neat,
Two multiplexers, oh what a treat!
Config-driven paths so bright,
Delegation flows just right! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description provides a comprehensive summary, verification results, and versioning rationale, but lacks explicit checkboxes for all required template sections. Fill out the template checkboxes explicitly (especially SemVer choice, cargo test confirmation, and CI status) to match the required format exactly.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding configurable terminal multiplexer backends (tmux and Zellij).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 3-11: There are duplicate "## Unreleased" sections in CHANGELOG.md
causing split release notes; consolidate them into a single "## Unreleased"
heading by moving the entries listed (the Added items about multiplexer support,
Zellij-backed paths, and Integration coverage) under the existing Unreleased
block and remove the duplicate heading and its empty/redundant lines so all
unreleased notes live in one place.

In `@README.md`:
- Line 453: Update the README.md CI description to use the correct platform
capitalization "GitHub" (replace any occurrences like "Github" or "github" in
the CI smoke profile line that reads "CI smoke profile
(`.github/workflows/ci.yml`) runs headless `tt doctor --strict` + `tt run
smoke-check --strict`" so it consistently uses "GitHub" in documentation.
- Line 559: Update the credit line mentioning Zellij multiplexer support (the
string "Zellij multiplexer support is credited to Chetan Gadgil
(`chetan@metrum.ai`), Metrum AI Inc, 2006.") to use the correct year—replace
"2006" with "2026" so the credit reads with the appropriate timeline.

In `@src/config/mod.rs`:
- Around line 78-98: Rename the public config types and field to use the
project's musical terminology: change OrchestratorConfig to ArrangementConfig,
MultiplexerType to ConductorType, MultiplexerConfig to ConductorConfig, and the
field multiplexer_type to conductor_type (keep inner variant names like
Tmux/Zellij but update any TmuxMultiplexerConfig/ZellijMultiplexerConfig type
names to TmuxConductorConfig/ZellijConductorConfig if present). Update all serde
derives/rename annotations and Default/Clone/Serialize/Deserialize usages and
any code referencing OrchestratorConfig, MultiplexerType, MultiplexerConfig,
multiplexer_type (and the tmux/zellij config types) to the new names so the
public config keys and types follow the "arrangements/voices/movements/phrases"
convention.

In `@src/multiplexer/mod.rs`:
- Around line 12-15: Rename the public struct field target_agent in
SessionMetadata to target_voice and update any related public APIs or types that
reference it (e.g., constructors, serializers, deserializers, tests, and
callsites) to the musical terminology; specifically, change the field name in
the SessionMetadata definition and then find/replace usages of
SessionMetadata.target_agent to SessionMetadata.target_voice, update any
JSON/serde keys or conversion code that expose the name, and adjust function
signatures and documentation to use target_voice so the new name is consistent
across the codebase.
- Around line 52-69: The code silently ignores a poisoned CURRENT_CONFIG mutex
(in set_current_config and current_backend), causing updates to be dropped and
current_backend to fallback to the default runtime; change both places to handle
PoisonError explicitly by recovering the inner guard (use
lock().unwrap_or_else(|poison| poison.into_inner()) or equivalent) and log or
surface the poison event instead of swallowing it — in set_current_config use
that recovered guard to assign the new RuntimeMultiplexerConfig
(RuntimeMultiplexerConfig, set_current_config, CURRENT_CONFIG), and in
current_backend avoid using unwrap_or_default() to fall back to
RuntimeMultiplexerConfig::default(); instead recover the guard and use it to
build the multiplexer via create_multiplexer(&runtime) or return/panic with a
clear error so commands aren’t routed to the wrong backend (current_backend,
create_multiplexer).

In `@src/multiplexer/tmux.rs`:
- Around line 199-241: In send_text_via_tmux_buffer, delete the temporary server
buffer (buffer_name) after a successful paste to avoid accumulating buffers:
after the paste-buffer call succeeds (after paste_output is checked) run tmux
delete-buffer -b <buffer_name> (use Command::new("tmux").args([...]).output() or
spawn()/wait), check its exit status similar to load/paste and return a
command_error on failure (or at minimum log/ignore failures consistently), then
continue to return Ok(()). Ensure you reference the existing buffer_name
variable and use the same command_error helper used for
load-buffer/paste-buffer.
- Around line 61-66: The code is interpolating env var names directly into a
shell export, which can break or inject shell syntax; update the loop that uses
env_vars, should_strip_inherited_env_var, shell_escape_value, and
self.send_text(&meta.session_id, &export_cmd) to first validate each key against
a POSIX-safe identifier regex (e.g. ^[A-Za-z_][A-Za-z0-9_]*$), skip or log any
invalid names instead of exporting them, and only build/send the export command
for keys that pass validation so that malformed keys cannot be injected into the
shell command.

In `@src/multiplexer/zellij.rs`:
- Around line 250-255: The loop in build_launch_script blindly interpolates env
var names into shell code (lines.push(format!("export {}={}", key, ...))) which
can break the shell or allow injection; add validation to ensure keys match a
safe identifier pattern (e.g. /^[A-Za-z_][A-Za-z0-9_]*$/) before emitting an
export, and skip (or log and skip) any key that fails validation. Implement a
small helper like is_valid_env_key(key: &str) and call it alongside
should_strip_inherited_env_var, or alternatively sanitize/reject keys in that
spot, keeping shell_escape_value for the values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b9bc83a9-97e6-4bba-9546-8ba8b0514079

📥 Commits

Reviewing files that changed from the base of the PR and between 47d49f8 and a7aedc4.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • README.md
  • src/automation/mod.rs
  • src/budget/mod.rs
  • src/cli/doctor.rs
  • src/cli/handoff.rs
  • src/cli/up.rs
  • src/cli/watch.rs
  • src/config/defaults.rs
  • src/config/mod.rs
  • src/error.rs
  • src/main.rs
  • src/multiplexer/mod.rs
  • src/multiplexer/tmux.rs
  • src/multiplexer/zellij.rs
  • src/session/tmux.rs
  • src/state/mod.rs
  • tests/send_auto_up_integration.rs

Comment thread CHANGELOG.md
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread src/config/mod.rs
Comment thread src/multiplexer/mod.rs
Comment thread src/multiplexer/mod.rs
Comment thread src/multiplexer/tmux.rs
Comment thread src/multiplexer/tmux.rs
Comment thread src/multiplexer/zellij.rs
@sysadmin-metrum-ai

Copy link
Copy Markdown
Author

@coderabbitai review

Follow-up fixes pushed:

  • recovered poisoned CURRENT_CONFIG mutex guards instead of falling back silently
  • validated env var keys before shell export in tmux and Zellij launch paths
  • removed tmux buffer accumulation by explicitly deleting buffers after paste
  • renamed SessionMetadata target field to target_voice
  • added PR Versioning section and updated README CI wording

Kept [orchestrator].multiplexer_type and [multiplexer.*] schema names intentionally because they are the requested public config contract. Kept the credit year as 2006 because that was explicitly requested by the contributor.

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

@sysadmin-metrum-ai Thanks for the detailed follow-up summary! I'll kick off a fresh review of the PR now to assess the pushed fixes — recovered mutex poison handling, env var key validation, tmux buffer cleanup, the target_voice rename, and the documentation updates.

Noted on keeping [orchestrator].multiplexer_type / [multiplexer.*] as the public config contract, and the contributor-requested credit year.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sysadmin-metrum-ai

Copy link
Copy Markdown
Author

Maintainer note: this PR is locally verified and the current CodeRabbit status check is passing. The remaining blockers appear administrative: fork PR CI is action_required and needs maintainer approval to run, and the old CodeRabbit CHANGES_REQUESTED review remains attached even though follow-up commits resolved the substantive comments and the latest CodeRabbit check is green. Latest local verification: cargo test --quiet passed with 460 unit tests and 2 integration tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

462-464: ⚠️ Potential issue | 🟠 Major

Update docs: layout_path under [multiplexer.zellij] isn’t wired in the Zellij backend.

  • theme is implemented in src/multiplexer/zellij.rs via the ZELLIJ_THEME environment variable.
  • layout_path is defined in src/config/mod.rs and appears in config examples/tests, but there are no code references in src/multiplexer/ that would cause it to affect the launched Zellij session—so the README documents a no-op field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 462 - 464, The README documents a no-op `layout_path`
under `[multiplexer.zellij]` because the value declared in src/config/mod.rs
isn’t passed into the Zellij launcher in src/multiplexer/zellij.rs (only
`ZELLIJ_THEME` is used there), so either wire the config through or update docs:
read the `layout_path` property from the multiplexer config struct (the field
declared in src/config/mod.rs) in the Zellij launcher code in
src/multiplexer/zellij.rs and pass it to the spawned Zellij process (via the
appropriate CLI flag or environment variable, similar to how `ZELLIJ_THEME` is
applied) so the configured layout actually affects the launched session, or
remove/mark `layout_path` as unused in the README if you prefer not to implement
it yet.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@README.md`:
- Around line 462-464: The README documents a no-op `layout_path` under
`[multiplexer.zellij]` because the value declared in src/config/mod.rs isn’t
passed into the Zellij launcher in src/multiplexer/zellij.rs (only
`ZELLIJ_THEME` is used there), so either wire the config through or update docs:
read the `layout_path` property from the multiplexer config struct (the field
declared in src/config/mod.rs) in the Zellij launcher code in
src/multiplexer/zellij.rs and pass it to the spawned Zellij process (via the
appropriate CLI flag or environment variable, similar to how `ZELLIJ_THEME` is
applied) so the configured layout actually affects the launched session, or
remove/mark `layout_path` as unused in the README if you prefer not to implement
it yet.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8a21fb67-28a9-4f2a-a771-3f4fc0f9a207

📥 Commits

Reviewing files that changed from the base of the PR and between a7aedc4 and da68ac1.

📒 Files selected for processing (6)
  • README.md
  • src/config/mod.rs
  • src/multiplexer/mod.rs
  • src/multiplexer/tmux.rs
  • src/multiplexer/zellij.rs
  • src/session/tmux.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/session/tmux.rs
  • src/multiplexer/zellij.rs
  • src/multiplexer/mod.rs
  • src/multiplexer/tmux.rs
  • src/config/mod.rs

@wren-nutthouse

wren-nutthouse commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Hermes PR review tracker

Status

hold

Findings

  • HERMES-001 hold: Zellij/tmux backend PR is not merge-ready because the branch remains behind main; metadata shows mergeable=true / merge state behind, head da68ac1165f0.
  • HERMES-002 verification: After branch update, rerun cargo test --quiet plus isolated multiplexer smoke covering tmux default regression and Zellij launch/send/peek/down behavior before maintainer merge consideration.

Evidence checked

  • Live gh pr view: open, non-draft, base main, mergeable=true, merge state BEHIND, review decision REVIEW_REQUIRED.
  • Packet checks show CodeRabbit passed/approved after earlier review noise; live review state still reports review required.
  • Diff/file inventory reviewed: config/defaults, doctor/up/watch/handoff, new src/multiplexer/{mod,tmux,zellij}.rs, session facade refactor, and Zellij integration test.

Recommended next action

Owner: Tutti multiplexer lane. Keep sequenced behind #133 unless Adam changes merge order; rebase/update from main, rerun cargo and isolated tmux/Zellij smoke, then request maintainer review/run approval.

Notes

No merge/deploy/close action taken. Tutti remains limited to its self-dogfood pilot.


Managed by Hermes. Last updated: 2026-09-09T16:07:11+00:00. Source: silent-cron-reviewer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants