Skip to content

feat(launch): add herdr session launcher - #16

Merged
hamidi-dev merged 1 commit into
hamidi-dev:developfrom
SimonEisenhauer:develop
Aug 5, 2026
Merged

feat(launch): add herdr session launcher#16
hamidi-dev merged 1 commit into
hamidi-dev:developfrom
SimonEisenhauer:develop

Conversation

@SimonEisenhauer

@SimonEisenhauer SimonEisenhauer commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add Herdr tab and split targets to the session launcher
  • select the innermost active multiplexer while preserving hook and tmux behavior
  • reopen pulled sessions over SSH through Herdr and bind new tabs to the active workspace
  • stabilize Doctor tests when CI has no terminal environment

Validation

  • hooks/pre-push
  • 1042/1042 tests passed

Summary by CodeRabbit

  • New Features
    • Added Herdr support for launching tabs and right/lower splits from the L menu.
    • Automatically selects the available launcher, including tmux, Herdr, or custom launchers.
    • Uses the innermost multiplexer in nested environments.
  • Bug Fixes
    • Improved handling of unavailable targets, invalid responses, and timeouts.
    • Remote sessions continue launching on their originating machine over SSH.
  • Documentation
    • Clarified Herdr targets, popup limitations, SSH requirements, and URL-based pull behavior.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

OpenTab now supports Herdr tab and split launches through a unified backend API. Backend selection handles nested multiplexers and launcher hooks. The TUI exposes backend-specific targets, and documentation and tests cover local, remote, and error paths.

Changes

Herdr launch integration

Layer / File(s) Summary
Backend selection and Herdr CLI execution
src/opentab/util.py, src/opentab/__init__.py, tests/test_util.py, tests/test_doctor.py, tests/__init__.py
Added Herdr detection, backend precedence, tab and split command construction, JSON pane-ID parsing, command execution, error handling, and compatibility support for tmux_launch.
TUI backend-aware launch menu
src/opentab/tui/app.py, src/opentab/tui/renderer.py, tests/test_tui_app.py
The launch menu snapshots the selected backend, exposes backend-specific targets, dispatches through launch_command, and covers nested, remote, unavailable, and error cases.
Launch behavior documentation
README.md, docs/keys.md, docs/architecture.md, docs/privacy.md
Documented Herdr targets, CLI launch steps, nested multiplexer selection, remote handling, and excluded socket operations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant OpenTabTUI
  participant launch_command
  participant HerdrCLI
  User->>OpenTabTUI: select a tab or split target
  OpenTabTUI->>launch_command: dispatch launch with selected backend
  launch_command->>HerdrCLI: create tab or split
  HerdrCLI-->>launch_command: return JSON pane ID
  launch_command->>HerdrCLI: run command in pane
  HerdrCLI-->>OpenTabTUI: return launch result
Loading

Suggested reviewers: hamidi-dev

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a Herdr session launcher.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@SimonEisenhauer
SimonEisenhauer marked this pull request as ready for review August 5, 2026 06:38

@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: 3

🤖 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 `@README.md`:
- Line 232: Update the README table entry for the L key to qualify the SSH
reopening behavior: sessions pulled from another machine should reopen over SSH
only when an SSH target exists in remotes.json, while URL-based pulls should be
described as providing only the copied command. Preserve the existing launcher
examples and key behavior.

In `@tests/test_tui_app.py`:
- Around line 2081-2086: Pin backend detection in the affected UI tests by
stubbing ot.util.launch_backend: return "tmux" for the tmux phase and None for
the copy-only phase at tests/test_tui_app.py lines 2081-2086, and return "tmux"
at lines 2158-2163 and 2352-2357. Save and restore the original launch_backend
in each test’s finally block, while leaving
test_launch_menu_uses_the_innermost_nested_multiplexer environment-based.

In `@tests/test_util.py`:
- Around line 476-496: Make the no-hook assertions in
test_launch_backend_prefers_hook_and_selects_innermost_multiplexer independent
of the real config directory by temporarily patching ot.util.launcher_hook to
return None before the TMUX and HERDR_ENV checks. Restore the original
launcher_hook in the test’s existing finally block, then run the
OPENTAB_LAUNCHER hook-precedence assertion against the real function.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a595e4c0-48b2-4364-ad12-aefff6a95f72

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb3dce and d31eae7.

📒 Files selected for processing (12)
  • README.md
  • docs/architecture.md
  • docs/keys.md
  • docs/privacy.md
  • src/opentab/__init__.py
  • src/opentab/tui/app.py
  • src/opentab/tui/renderer.py
  • src/opentab/util.py
  • tests/__init__.py
  • tests/test_doctor.py
  • tests/test_tui_app.py
  • tests/test_util.py

Comment thread README.md Outdated
Comment thread tests/test_tui_app.py Outdated
Comment thread tests/test_util.py
@hamidi-dev

Copy link
Copy Markdown
Owner

Looks good — I checked it against herdr 0.7.5's source and the flags, JSON paths, create-then-run sequence and nesting detection all hold up.

One thing to fix first. herdr pane split --current resolves via $HERDR_PANE_ID (src/cli/pane.rs:517); when that's unset herdr silently falls through to the UI-focused pane (src/app/api/panes.rs:40), and popup panes strip the variable on purpose (src/app/popup.rs:154). So L → s from a popup can type claude --resume … into someone else's pane. Pass --pane "$HERDR_PANE_ID" so it fails visibly, or hide the split rows when it's unset.

Two doc fixes: herdr pane run is pane.send_input (src/cli/pane.rs:930), and the socket API isn't deprecated. And let palette_writes_ignored() call your new in_herdr() — its comment says there should only ever be one copy of that check.

Also the popup wording, in launch_targets() and docs/keys.md: herdr does have popups, they're just only openable from inside herdr — a [[keys.command]] type = "popup" binding, or a plugin pane entrypoint. Nothing an external caller can drive, so leaving p off the menu is right; "no native popup" isn't.

Rest is minor: stale launch_menu_backend, raw JSON in error toasts, an unreachable ValueError path, and the docstring blank lines from a newer ruff than CI's 0.1.15.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 4

🧹 Nitpick comments (1)
src/opentab/util.py (1)

786-792: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep one validation site for Herdr launch kinds.

herdr_launch rejects unsupported kinds before it calls _herdr_cli_launch, so the ValueError raises in herdr_create_argv are unreachable on this path. The except ValueError block in launch_command (Line 889) is also unreachable, because herdr_launch returns a message instead of raising. Keep the raises in herdr_create_argv for direct callers, and drop the duplicated guard plus the dead handler.

♻️ Proposed refactor
 def herdr_launch(kind: str, directory: str, command: str) -> str | None:
     """Create a Herdr tab or split through its CLI, then run the command in it."""
-    if kind not in ("window", "hsplit", "vsplit"):
-        if kind == "popup":
-            return "herdr does not support popups"
-        return f"unknown Herdr launch kind: {kind}"
-    return _herdr_cli_launch(kind, directory, command)
+    try:
+        return _herdr_cli_launch(kind, directory, command)
+    except ValueError as exc:
+        return str(exc)

Then simplify launch_command:

     if backend == "herdr":
-        try:
-            return herdr_launch(kind, directory, command)
-        except ValueError as exc:
-            return str(exc)
+        return herdr_launch(kind, directory, command)

The existing tests keep passing: herdr_launch("popup", ...) still returns "herdr does not support popups".

🤖 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 `@src/opentab/util.py` around lines 786 - 792, Remove the unsupported-kind
validation from herdr_launch so it delegates all kinds to _herdr_cli_launch,
preserving herdr_create_argv as the single validation site while keeping direct
herdr_launch("popup", ...) behavior unchanged. In launch_command, remove the
now-dead ValueError exception handler around the Herdr launch path.
🤖 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 `@docs/architecture.md`:
- Around line 146-149: Update the OpenTab description to state that it uses the
CLI and never opens a Herdr socket, while retaining the unchanged visible and
copyable command behavior. Remove pane.send_input from the excluded operations,
leaving only layout.apply and pane.current.

In `@src/opentab/util.py`:
- Around line 866-868: Update the nonzero-return handling in the launch-result
helper to strip proc.stderr before selecting the fallback, so whitespace-only
stderr produces the "{label} failed" message instead of an empty string.
Preserve returning None for successful processes and ensure _do_launch receives
a non-empty failure result for every failed launch.

In `@tests/test_tui_app.py`:
- Around line 2296-2304: Update the test setup and cleanup around the tmux/Herdr
assertions to isolate launch_backend from launcher hooks: save the existing
OPENTAB_LAUNCHER value, clear it during the test, and restore or remove it in
the finally block. Also stub ot.util.launcher_hook if needed so real launcher
configuration cannot affect the backend assertions.

In `@tests/test_util.py`:
- Line 400: Update the assertion in the test covering captured calls so it
checks that no invocation contains the argv sequence representing the “pane
current” subcommand, rather than searching for the bare string “current”.
Preserve validation across every call and assert against the actual split argv
elements.

---

Nitpick comments:
In `@src/opentab/util.py`:
- Around line 786-792: Remove the unsupported-kind validation from herdr_launch
so it delegates all kinds to _herdr_cli_launch, preserving herdr_create_argv as
the single validation site while keeping direct herdr_launch("popup", ...)
behavior unchanged. In launch_command, remove the now-dead ValueError exception
handler around the Herdr launch path.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 05088bbc-82b0-4426-b31c-f28453b80a25

📥 Commits

Reviewing files that changed from the base of the PR and between 4a36959 and 226cae6.

📒 Files selected for processing (12)
  • README.md
  • docs/architecture.md
  • docs/keys.md
  • docs/privacy.md
  • src/opentab/__init__.py
  • src/opentab/tui/app.py
  • src/opentab/tui/renderer.py
  • src/opentab/util.py
  • tests/__init__.py
  • tests/test_doctor.py
  • tests/test_tui_app.py
  • tests/test_util.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/init.py
  • tests/test_doctor.py
  • docs/keys.md
  • README.md
  • docs/privacy.md
  • src/opentab/tui/renderer.py
  • src/opentab/init.py
  • src/opentab/tui/app.py

Comment thread docs/architecture.md Outdated
Comment thread src/opentab/util.py
Comment thread tests/test_tui_app.py Outdated
Comment thread tests/test_util.py Outdated
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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.

🧹 Nitpick comments (2)
src/opentab/util.py (2)

759-768: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a type annotation for proc.

_herdr_failure uses getattr on an untyped parameter. Annotate it as subprocess.CompletedProcess[str] and read proc.stderr and proc.returncode directly, since both call sites pass a completed process.

🤖 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 `@src/opentab/util.py` around lines 759 - 768, Update _herdr_failure to
annotate proc as subprocess.CompletedProcess[str], then access proc.stderr and
proc.returncode directly instead of using getattr. Preserve the existing
diagnostic formatting and fallback exit-status behavior.

722-753: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: collapse the two split branches.

hsplit and vsplit differ only in the --direction value. One branch with a direction map removes the duplicated pane lookup and argv literal.

♻️ Proposed refactor
-    if kind == "hsplit":
-        pane = herdr_pane_id()
-        if pane is None:
-            raise ValueError("HERDR_PANE_ID is required for Herdr splits")
-        return [
-            herdr,
-            "pane",
-            "split",
-            "--pane",
-            pane,
-            "--direction",
-            "right",
-            "--cwd",
-            directory,
-            "--focus",
-        ]
-    if kind == "vsplit":
-        pane = herdr_pane_id()
-        if pane is None:
-            raise ValueError("HERDR_PANE_ID is required for Herdr splits")
-        return [
-            herdr,
-            "pane",
-            "split",
-            "--pane",
-            pane,
-            "--direction",
-            "down",
-            "--cwd",
-            directory,
-            "--focus",
-        ]
+    direction = {"hsplit": "right", "vsplit": "down"}.get(kind)
+    if direction is not None:
+        pane = herdr_pane_id()
+        if pane is None:
+            raise ValueError("HERDR_PANE_ID is required for Herdr splits")
+        return [herdr, "pane", "split", "--pane", pane,
+                "--direction", direction, "--cwd", directory, "--focus"]
🤖 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 `@src/opentab/util.py` around lines 722 - 753, Collapse the duplicated hsplit
and vsplit branches in the split-command builder by mapping each kind to its
direction and sharing the Herdr pane lookup, validation, and argument
construction. Preserve “right” for hsplit and “down” for vsplit, including the
existing missing HERDR_PANE_ID error.
🤖 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.

Nitpick comments:
In `@src/opentab/util.py`:
- Around line 759-768: Update _herdr_failure to annotate proc as
subprocess.CompletedProcess[str], then access proc.stderr and proc.returncode
directly instead of using getattr. Preserve the existing diagnostic formatting
and fallback exit-status behavior.
- Around line 722-753: Collapse the duplicated hsplit and vsplit branches in the
split-command builder by mapping each kind to its direction and sharing the
Herdr pane lookup, validation, and argument construction. Preserve “right” for
hsplit and “down” for vsplit, including the existing missing HERDR_PANE_ID
error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 405eada6-40c6-4283-8a2e-6c842f30984c

📥 Commits

Reviewing files that changed from the base of the PR and between 4a36959 and 9941835.

📒 Files selected for processing (12)
  • README.md
  • docs/architecture.md
  • docs/keys.md
  • docs/privacy.md
  • src/opentab/__init__.py
  • src/opentab/tui/app.py
  • src/opentab/tui/renderer.py
  • src/opentab/util.py
  • tests/__init__.py
  • tests/test_doctor.py
  • tests/test_tui_app.py
  • tests/test_util.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/init.py
  • README.md
  • docs/privacy.md
  • src/opentab/init.py
  • docs/keys.md
  • tests/test_doctor.py
  • src/opentab/tui/renderer.py
  • src/opentab/tui/app.py

@hamidi-dev
hamidi-dev merged commit dc48c4c into hamidi-dev:develop Aug 5, 2026
5 checks passed
@hamidi-dev

Copy link
Copy Markdown
Owner

thank you for this feature!

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