Skip to content

Fix sidebar PR badge polling for idle shells - #1139

Merged
austinywang merged 4 commits into
mainfrom
issue-1138-sidebar-pr-badge-detection
Mar 12, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-1138-sidebar-pr-badge-detection

Conversation

@austinywang

@austinywang austinywang commented Mar 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression harness for sidebar PR polling after transient gh failures
  • keep shell integration polling GitHub PR state while the shell is idle
  • stop clearing PR badges on transient gh failures so eventual retries can recover

Closes #1138


Summary by cubic

Fixes sidebar PR badges so they keep updating while the shell is idle, don’t disappear on transient gh errors, and clear promptly on branch changes. Closes #1138.

  • Bug Fixes
    • Added a lightweight background PR poller in cmux-bash-integration.bash and cmux-zsh-integration.zsh that runs while idle (default: 45s); stops on command start and restarts on prompt/HEAD/cwd changes; clean teardown on shell exit.
    • Preserve the last-known PR badge on transient gh failures; only clear when gh reports no PR or when the cwd/HEAD context changes to avoid stale badges.
    • Timeout protection: run probes with _CMUX_ASYNC_JOB_TIMEOUT; kill wedged gh pr view and its child processes, then retry on the next poll.
    • Expanded tests (tests/test_issue_1138_sidebar_pr_polling.py) to cover idle polling (including bash prompt-helper commands), transient recovery, clearing on branch switch when the new probe fails, and timeout recovery without leaked gh processes.

Written for commit 84edefa. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Continuous background PR polling with per-path reporting, timeout-aware probes, and prompt-aware lifecycle that restarts on directory/HEAD changes or forced refreshes.
    • Debug/trap support to coordinate polling during interactive commands and cleanup.
  • Bug Fixes

    • Robust handling of transient GitHub failures—preserves last-known badge and clears only on definite no-PR cases.
    • Ensures polling stops cleanly on shell exit.
  • Tests

    • New integration test validating polling behavior, transient-failure handling, and recovery.

@vercel

vercel Bot commented Mar 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 10, 2026 7:41am

@coderabbitai

coderabbitai Bot commented Mar 10, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces per-prompt PR checks with a restartable background PR-polling subsystem for bash and zsh: a poll loop probes GitHub (gh) for PR state, reports per-path PR metadata, handles transient failures, restarts on cwd/HEAD/force changes, and provides trap-based cleanup and prompt coordination.

Changes

Cohort / File(s) Summary
Bash PR Polling Subsystem
Resources/shell-integration/cmux-bash-integration.bash
Adds polling-based PR lifecycle: _CMUX_PR_POLL_* vars, start/stop poll loop, async probe with timeout, per-path report_pr/clear_pr helpers, preexec/debug traps, prompt coordination flags, process-management helpers, and teardown; removes legacy per-PR job state.
Zsh PR Polling Subsystem
Resources/shell-integration/cmux-zsh-integration.zsh
Introduces equivalent poll loop and vars (replacing _CMUX_PR_LAST_* job vars), probe timeout and restart logic, per-path reporting, integrates start/stop into _cmux_precmd and _cmux_zshexit, and treats transient gh failures as non-fatal.
PR Polling Regression Test
tests/test_issue_1138_sidebar_pr_polling.py
New integration test that mocks git/gh and a UNIX socket server to validate background polling, transient-gh-failure behavior (no premature clear), multiple gh probes while idle, and recovery reporting for both bash and zsh.

Sequence Diagram

sequenceDiagram
    participant Shell as Shell Prompt
    participant Monitor as CWD/HEAD Watcher
    participant Poller as PR Poll Loop
    participant GH as GitHub CLI (gh)
    participant Sidebar as Sidebar UI

    Shell->>Monitor: detect cwd/HEAD change or force
    Monitor->>Poller: start / restart poll loop
    activate Poller
    loop every poll interval
        Poller->>GH: probe PR for repo/path
        alt gh returns PR metadata
            GH-->>Poller: PR data
            Poller->>Sidebar: report_pr (per-path)
        else gh transient failure
            GH-->>Poller: error
            Poller->>Sidebar: preserve last-known PR (no clear_pr)
        end
    end
    deactivate Poller
    Shell->>Poller: exit / cleanup
    Poller-->>Shell: stopped
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped the branch and sniffed the log,

I polled the sky when gh went fog,
Patience kept the badge alive,
I nudged the loop till results arrived,
Hooray — the sidebar wiggled with joy!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title 'Fix sidebar PR badge polling for idle shells' clearly and concisely describes the main change: fixing PR badge polling while shells are idle.
Description check ✅ Passed The PR description covers summary (what/why), includes linked issue reference (#1138), and mentions test additions, but lacks explicit testing methodology and demo video sections from the template.
Linked Issues check ✅ Passed The PR addresses all core requirements from #1138: adds background PR polling for idle shells, preserves badges on transient gh failures, and includes regression tests for the polling mechanism and transient failure recovery.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to addressing #1138: shell integration polling enhancements, supporting infrastructure functions, and regression tests for the new polling system.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1138-sidebar-pr-badge-detection

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/shell-integration/cmux-bash-integration.bash`:
- Around line 155-158: When gh fails (gh_status != 0) we currently return 1 and
preserve the previous PR badge unconditionally; modify _cmux_prompt_command so
that before returning on a transient gh error it checks whether the git context
has changed (compare current PWD/.git/HEAD or the current repo/branch to the
stored context variables, e.g. _cmux_last_git_dir/_cmux_last_head or
_cmux_last_branch) and if the context has changed clear/invalidate the PR
display state variables (e.g. unset or reset
_cmux_pr_badge/_cmux_pr_number/_cmux_pr_state) so the old branch’s badge is not
shown on the new branch, then return 1.

In `@Resources/shell-integration/cmux-zsh-integration.zsh`:
- Around line 177-180: When gh_status != 0, the code preserves the last-known PR
badge across all failures; change this so you first check whether the git
context (repo and branch) has changed since the last successful poll and, if it
has, clear the stored PR state before returning. In practice, update the
gh_status failure branch inside the _cmux_precmd polling logic: compare current
repo/branch (from git rev-parse --show-toplevel and git rev-parse --abbrev-ref
HEAD or the project’s existing context helpers) to the stored last-known context
variables (e.g., LAST_PR_REPO, LAST_PR_BRANCH or whatever state variables hold
the badge), and if they differ unset/clear the last-known PR badge state before
returning 1 so badges aren’t carried into a new git context. Ensure the
comparison and clearing happen immediately before the existing "return 1" in the
gh_status != 0 block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a6cb056-7504-40e1-a238-7165d7daa30b

📥 Commits

Reviewing files that changed from the base of the PR and between b824147 and 844d85f.

📒 Files selected for processing (3)
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • tests/test_issue_1138_sidebar_pr_polling.py

Comment thread Resources/shell-integration/cmux-bash-integration.bash
Comment thread Resources/shell-integration/cmux-zsh-integration.zsh

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Resources/shell-integration/cmux-bash-integration.bash">

<violation number="1" location="Resources/shell-integration/cmux-bash-integration.bash:216">
P1: `_cmux_bash_cleanup` is defined but never registered as an EXIT trap, so it never runs. The background PR poll loop will keep running for up to 45 seconds after the shell exits before `kill -0` detects the dead parent. Register the trap where the poll loop is set up (e.g., alongside `_cmux_install_debug_trap`).</violation>

<violation number="2" location="Resources/shell-integration/cmux-bash-integration.bash:227">
P2: Missing bare `jj` and `stg` patterns in the preexec case — running `jj` or `stg` with no arguments won't trigger a PR poll refresh, unlike every other listed tool.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

disown "$_CMUX_PR_POLL_PID" 2>/dev/null || disown
}

_cmux_bash_cleanup() {

@cubic-dev-ai cubic-dev-ai Bot Mar 10, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: _cmux_bash_cleanup is defined but never registered as an EXIT trap, so it never runs. The background PR poll loop will keep running for up to 45 seconds after the shell exits before kill -0 detects the dead parent. Register the trap where the poll loop is set up (e.g., alongside _cmux_install_debug_trap).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Resources/shell-integration/cmux-bash-integration.bash, line 216:

<comment>`_cmux_bash_cleanup` is defined but never registered as an EXIT trap, so it never runs. The background PR poll loop will keep running for up to 45 seconds after the shell exits before `kill -0` detects the dead parent. Register the trap where the poll loop is set up (e.g., alongside `_cmux_install_debug_trap`).</comment>

<file context>
@@ -115,10 +118,136 @@ _cmux_ports_kick() {
+    disown "$_CMUX_PR_POLL_PID" 2>/dev/null || disown
+}
+
+_cmux_bash_cleanup() {
+    _cmux_stop_pr_poll_loop
+}
</file context>
Fix with Cubic

Comment thread Resources/shell-integration/cmux-bash-integration.bash Outdated
@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the sidebar PR badge polling in both the bash and zsh shell integrations, replacing a one-shot async background job (fired each prompt) with a persistent background poll loop that keeps running while the shell is idle. It also stops clearing the PR badge on transient gh failures (rate limits, auth hiccups) so the last-known state is preserved until a successful retry, and adds a regression test that validates both behaviours end-to-end.

Key changes:

  • Persistent poll loop (_cmux_start_pr_poll_loop / _cmux_stop_pr_poll_loop): a long-lived background process polls gh pr list --head <branch> --state all on a configurable interval (default 45 s), replacing the old per-prompt one-shot job.
  • Transient failure handling: _cmux_report_pr_for_path now returns non-zero (badge preserved) instead of sending clear_pr when gh exits with a non-zero status.
  • Bash preexec shim: a DEBUG trap (_cmux_preexec_trap) detects git/gh commands typed at the prompt and sets _CMUX_PR_FORCE=1, triggering an immediate poll loop restart — matching the zsh preexec hook behaviour.
  • Regression test (test_issue_1138_sidebar_pr_polling.py): exercises both shells, stubs git/gh, simulates a first-call failure, and asserts that polling continues and eventually reports the correct PR payload.
  • One actionable issue: _cmux_bash_cleanup is defined to stop the poll loop on exit but is never registered as a bash EXIT trap — the zsh side already handles this correctly via add-zsh-hook zshexit. The test masks this gap by calling the function explicitly, but real users won't get that call.

Confidence Score: 4/5

  • Safe to merge after addressing the missing bash EXIT trap for poll loop cleanup.
  • The core logic is sound and well-tested. The only issue found is the missing trap '_cmux_bash_cleanup' EXIT in the bash integration, which causes the background poll process to linger for up to the full poll interval (45 s) after a bash shell exits. This is a resource concern rather than a correctness bug, since the loop eventually self-terminates via its kill -0 liveness check. The zsh path is correctly wired up and no other logic issues were found.
  • Resources/shell-integration/cmux-bash-integration.bash — missing EXIT trap for _cmux_bash_cleanup

Important Files Changed

Filename Overview
Resources/shell-integration/cmux-bash-integration.bash Replaces one-shot async PR probe with a persistent background poll loop; adds DEBUG-trap-based preexec shim for force-refresh on git/gh commands. The poll loop is well-structured, but _cmux_bash_cleanup is never registered as an EXIT trap, so the orphaned background process can linger for up to the full poll interval (45 s) after the shell exits.
Resources/shell-integration/cmux-zsh-integration.zsh Mirrors the bash refactor; correctly registers _cmux_stop_pr_poll_loop in _cmux_zshexit via add-zsh-hook zshexit. No issues found.
tests/test_issue_1138_sidebar_pr_polling.py New regression harness that validates polling survives a transient gh failure and recovers on the next interval. Test overrides _cmux_send to write to a log file and mocks git/gh stubs. Assertions are clear, but the test calls _cmux_bash_cleanup explicitly — masking the missing EXIT-trap gap in real bash usage.

Sequence Diagram

sequenceDiagram
    participant Shell as Shell (bash/zsh)
    participant PC as PROMPT_COMMAND/precmd
    participant PollBg as PR Poll Background
    participant GH as gh CLI
    participant App as cmux App (socket)

    Shell->>PC: prompt displayed
    PC->>PC: evaluate should_restart_pr_poll
    PC->>PollBg: _cmux_start_pr_poll_loop(pwd, force=1)
    activate PollBg
    Note over PollBg: runs every _CMUX_PR_POLL_INTERVAL (45s)
    PollBg->>PollBg: kill -0 parent_pid (alive check)
    PollBg->>GH: gh pr list --head branch --state all --jq ...
    alt gh succeeds, PR found
        GH-->>PollBg: TSV (number, state, url)
        PollBg->>App: report_pr number url --state=open/merged/closed
    else gh succeeds, no PR
        GH-->>PollBg: empty string
        PollBg->>App: clear_pr
    else gh fails transiently
        GH-->>PollBg: exit 1
        PollBg->>PollBg: return 1, badge preserved, retry next interval
    end
    PollBg->>PollBg: sleep interval, then repeat

    Shell->>PC: user types git or gh command
    Note over PC: bash DEBUG trap / zsh preexec sets _CMUX_PR_FORCE=1
    PC->>PollBg: stop old loop, start fresh loop immediately
    activate PollBg

    Shell-->>PollBg: shell exits, kill -0 fails, poll loop breaks
    deactivate PollBg
Loading

Last reviewed commit: 844d85f

Comment on lines +216 to +218
_cmux_bash_cleanup() {
_cmux_stop_pr_poll_loop
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Poll loop leaks on bash shell exit — no EXIT trap wired up

_cmux_bash_cleanup is defined but never registered as an EXIT handler. The zsh side correctly registers _cmux_zshexit via add-zsh-hook zshexit _cmux_zshexit, so the poll loop is killed immediately on clean exit. In bash, nothing calls _cmux_bash_cleanup automatically — the orphaned poll process keeps sleeping for up to _CMUX_PR_POLL_INTERVAL seconds (45 s by default) after the shell exits, since disown prevents SIGHUP delivery and the kill -0 "$watch_shell_pid" liveness check only runs at the top of each loop iteration (after the sleep).

The test exercises this by calling _cmux_bash_cleanup explicitly, but real users closing a terminal tab get no such call.

A one-liner addition at the end of the installation block would fix this:

# at the end of the file, after _cmux_install_debug_trap
trap '_cmux_bash_cleanup' EXIT

Or, if you want to preserve any existing EXIT trap, chain it:

_cmux_install_exit_trap() {
    local existing
    existing="$(trap -p EXIT 2>/dev/null || true)"
    if [[ -n "$existing" ]]; then
        eval "${existing%\'EXIT\'} ; _cmux_bash_cleanup' EXIT"
    else
        trap '_cmux_bash_cleanup' EXIT
    fi
}
_cmux_install_exit_trap

@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

🧹 Nitpick comments (1)
tests/test_issue_1138_sidebar_pr_polling.py (1)

166-175: This matrix still misses the slow old-probe race.

branch_switch_clear waits a full second before flipping HEAD, so the old-branch gh pr view has already finished. That means it still won’t catch the case where the previous probe is hanging during the branch switch and later re-reports feature/old after clear_pr. Adding that scenario would protect the exact restart bug these shell changes are most exposed to.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_issue_1138_sidebar_pr_polling.py` around lines 166 - 175, Create
an additional test matrix case (or modify the existing branch_switch_clear
variant) to simulate the slow old-probe race by flipping HEAD immediately while
the previous probe may still be running: in the tuple labeled
"branch_switch_clear" (and where _CMUX_PR_POLL_INTERVAL is set and
_cmux_prompt_entry is invoked), remove or shorten the initial sleep (the 'sleep
1' before writing to "$CMUX_TEST_HEAD_FILE") so the HEAD update occurs
immediately after the first _cmux_prompt_entry (i.e., call printf 'ref:
refs/heads/feature/new\n' > "$CMUX_TEST_HEAD_FILE" right away), which will cause
the old gh pr view probe to still be in-flight and reproduce the re-reporting
race. Ensure the rest of the sequence (_cmux_prompt_entry, sleeps, and
_cmux_cleanup) remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/shell-integration/cmux-bash-integration.bash`:
- Around line 201-230: The timeout handler in _cmux_run_pr_probe_with_timeout
currently kills only the wrapper subshell (probe_pid) leaving the inner gh pr
view orphaned; modify the subshell invocation in
_cmux_report_pr_for_path/_cmux_run_pr_probe_with_timeout to create a new process
group (e.g., run the subshell with setsid or use bash -c 'set -m; ...' or use
"trap" and "exec" to make the gh process the group leader) and on timeout kill
the entire process group (kill -TERM -$pgid then kill -KILL -$pgid if needed),
or alternatively have the subshell write the child PID of gh pr view to a known
file/variable and have the timeout handler read and kill that PID as well; apply
the same fix to _cmux_stop_pr_poll_loop so it also terminates any active probe
child processes instead of only the wrapper.

In `@Resources/shell-integration/cmux-zsh-integration.zsh`:
- Around line 223-252: The probe wrapper spawns _cmux_report_pr_for_path in a
subshell so kill targets only the wrapper shell and not the underlying `gh`
child; modify _cmux_run_pr_probe_with_timeout (and similarly
_cmux_stop_pr_poll_loop) to start the probe as a direct child or group such that
you can track and kill the actual `gh` process: invoke _cmux_report_pr_for_path
without an extra subshell (so $! is the real child), or capture the child's pid
from within the probe (export or write the child PID to a temp file), or use
process group handling (setsid/set -m and kill -TERM -$pgid) and forward signals
to that pid/group; ensure wait uses the real child PID, and that timeouts send
TERM (and if needed SIGKILL) to the actual probe process id/group to prevent
orphaned `gh` processes.

In `@tests/test_issue_1138_sidebar_pr_polling.py`:
- Around line 224-262: Check for the shell executable before spawning
subprocesses in _run_case: use shutil.which(shell) (or Path(shell).exists() for
absolute paths) and if it returns None, return a clean skip tuple (e.g., (0,
f"skipped: {shell} not found")) instead of calling subprocess.run; apply the
same guard to the other invocation of subprocess.run in this file (the block
around the second call) so missing shells produce a skipped result rather than
raising FileNotFoundError.

---

Nitpick comments:
In `@tests/test_issue_1138_sidebar_pr_polling.py`:
- Around line 166-175: Create an additional test matrix case (or modify the
existing branch_switch_clear variant) to simulate the slow old-probe race by
flipping HEAD immediately while the previous probe may still be running: in the
tuple labeled "branch_switch_clear" (and where _CMUX_PR_POLL_INTERVAL is set and
_cmux_prompt_entry is invoked), remove or shorten the initial sleep (the 'sleep
1' before writing to "$CMUX_TEST_HEAD_FILE") so the HEAD update occurs
immediately after the first _cmux_prompt_entry (i.e., call printf 'ref:
refs/heads/feature/new\n' > "$CMUX_TEST_HEAD_FILE" right away), which will cause
the old gh pr view probe to still be in-flight and reproduce the re-reporting
race. Ensure the rest of the sequence (_cmux_prompt_entry, sleeps, and
_cmux_cleanup) remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecf93983-a58e-43af-a53e-421d04ba03f1

📥 Commits

Reviewing files that changed from the base of the PR and between 844d85f and 0ad9775.

📒 Files selected for processing (3)
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • tests/test_issue_1138_sidebar_pr_polling.py

Comment thread Resources/shell-integration/cmux-bash-integration.bash
Comment thread Resources/shell-integration/cmux-zsh-integration.zsh
Comment on lines +224 to +262
def _run_case(base: Path, *, shell: str, shell_args: list[str], script: Path, scenario: str) -> tuple[int, str]:
bindir = base / "bin"
repo = base / "repo"
repo_git = repo / ".git"
socket_path = base / "cmux.sock"
send_log = base / f"{shell}-{scenario}-send.log"
gh_count_file = base / f"{shell}-{scenario}-gh-count.txt"
gh_args_log = base / f"{shell}-{scenario}-gh-args.log"
head_file = repo_git / "HEAD"

bindir.mkdir(parents=True, exist_ok=True)
repo_git.mkdir(parents=True, exist_ok=True)
initial_branch = "feature/old" if scenario == "branch_switch_clear" else "feature/issue-1138"
head_file.write_text(f"ref: refs/heads/{initial_branch}\n", encoding="utf-8")
_write_executable(bindir / "git", _git_stub())
_write_executable(bindir / "gh", _gh_stub())

env = dict(os.environ)
env["PATH"] = f"{bindir}:{env.get('PATH', '')}"
env["CMUX_SOCKET_PATH"] = str(socket_path)
env["CMUX_TAB_ID"] = "00000000-0000-0000-0000-000000000001"
env["CMUX_PANEL_ID"] = "00000000-0000-0000-0000-000000000002"
env["CMUX_TEST_SCRIPT"] = str(script)
env["CMUX_TEST_REPO"] = str(repo)
env["CMUX_TEST_SEND_LOG"] = str(send_log)
env["CMUX_TEST_GH_COUNT_FILE"] = str(gh_count_file)
env["CMUX_TEST_GH_ARGS_LOG"] = str(gh_args_log)
env["CMUX_TEST_SCENARIO"] = scenario
env["CMUX_TEST_HEAD_FILE"] = str(head_file)
env["CMUX_TEST_HANG_SECONDS"] = "4"

with BoundUnixSocket(socket_path):
result = subprocess.run(
[shell, *shell_args, _shell_command(shell, scenario)],
env=env,
capture_output=True,
text=True,
timeout=12,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skip absent shells before spawning subprocesses.

Right now only the integration script path is guarded. If zsh is missing on a runner image, subprocess.run() raises FileNotFoundError inside _run_case() and aborts the whole harness instead of reporting a clean skip for that matrix entry.

🧪 Proposed fix
-def _run_case(base: Path, *, shell: str, shell_args: list[str], script: Path, scenario: str) -> tuple[int, str]:
+def _run_case(
+    base: Path,
+    *,
+    shell: str,
+    shell_path: str,
+    shell_args: list[str],
+    script: Path,
+    scenario: str,
+) -> tuple[int, str]:
     bindir = base / "bin"
@@
         result = subprocess.run(
-            [shell, *shell_args, _shell_command(shell, scenario)],
+            [shell_path, *shell_args, _shell_command(shell, scenario)],
             env=env,
             capture_output=True,
             text=True,
             timeout=12,
         )
@@
         failures: list[str] = []
         for shell, shell_args, script in cases:
             if not script.exists():
                 print(f"SKIP: missing integration script at {script}")
                 continue
+            shell_path = shutil.which(shell)
+            if shell_path is None:
+                print(f"SKIP: missing shell binary {shell}")
+                continue
             for scenario in scenarios:
                 rc, detail = _run_case(
                     base / f"{shell}-{scenario}",
                     shell=shell,
+                    shell_path=shell_path,
                     shell_args=shell_args,
                     script=script,
                     scenario=scenario,
                 )

Also applies to: 329-340

🧰 Tools
🪛 Ruff (0.15.5)

[error] 256-256: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_issue_1138_sidebar_pr_polling.py` around lines 224 - 262, Check
for the shell executable before spawning subprocesses in _run_case: use
shutil.which(shell) (or Path(shell).exists() for absolute paths) and if it
returns None, return a clean skip tuple (e.g., (0, f"skipped: {shell} not
found")) instead of calling subprocess.run; apply the same guard to the other
invocation of subprocess.run in this file (the block around the second call) so
missing shells produce a skipped result rather than raising FileNotFoundError.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/test_issue_1138_sidebar_pr_polling.py">

<violation number="1" location="tests/test_issue_1138_sidebar_pr_polling.py:341">
P2: Make PID capture mandatory in `timeout_recovery`; currently the leak check is skipped when the PID file is missing, which can let this regression test pass without validating teardown.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +341 to +344
if gh_pid_file.exists():
gh_pid = int(gh_pid_file.read_text(encoding="utf-8").strip() or "0")
if gh_pid > 0 and _pid_exists(gh_pid):
return (1, f"{shell}/{scenario}: timed-out gh probe still running as pid {gh_pid}")

@cubic-dev-ai cubic-dev-ai Bot Mar 10, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Make PID capture mandatory in timeout_recovery; currently the leak check is skipped when the PID file is missing, which can let this regression test pass without validating teardown.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test_issue_1138_sidebar_pr_polling.py, line 341:

<comment>Make PID capture mandatory in `timeout_recovery`; currently the leak check is skipped when the PID file is missing, which can let this regression test pass without validating teardown.</comment>

<file context>
@@ -303,6 +338,10 @@ def _run_case(base: Path, *, shell: str, shell_args: list[str], script: Path, sc
             return (1, f"{shell}/{scenario}: expected timed-out probe to be retried, saw {gh_count}")
         if _report_line(1138) not in send_lines:
             return (1, f"{shell}/{scenario}: missing report_pr after timeout recovery\n" + "\n".join(send_lines))
+        if gh_pid_file.exists():
+            gh_pid = int(gh_pid_file.read_text(encoding="utf-8").strip() or "0")
+            if gh_pid > 0 and _pid_exists(gh_pid):
</file context>
Suggested change
if gh_pid_file.exists():
gh_pid = int(gh_pid_file.read_text(encoding="utf-8").strip() or "0")
if gh_pid > 0 and _pid_exists(gh_pid):
return (1, f"{shell}/{scenario}: timed-out gh probe still running as pid {gh_pid}")
if not gh_pid_file.exists():
return (1, f"{shell}/{scenario}: missing timed-out probe pid capture")
gh_pid = int(gh_pid_file.read_text(encoding="utf-8").strip() or "0")
if gh_pid > 0 and _pid_exists(gh_pid):
return (1, f"{shell}/{scenario}: timed-out gh probe still running as pid {gh_pid}")
Fix with Cubic

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

♻️ Duplicate comments (1)
tests/test_issue_1138_sidebar_pr_polling.py (1)

250-290: ⚠️ Potential issue | 🟡 Minor

Skip missing shell binaries before spawning the matrix entry.

If bash or zsh is missing on a runner, Line 284 raises FileNotFoundError and aborts the whole harness instead of producing a clean skip for that case.

🧪 Proposed fix
-def _run_case(base: Path, *, shell: str, shell_args: list[str], script: Path, scenario: str) -> tuple[int, str]:
+def _run_case(
+    base: Path,
+    *,
+    shell: str,
+    shell_path: str,
+    shell_args: list[str],
+    script: Path,
+    scenario: str,
+) -> tuple[int, str]:
@@
         result = subprocess.run(
-            [shell, *shell_args, _shell_command(shell, scenario)],
+            [shell_path, *shell_args, _shell_command(shell, scenario)],
             env=env,
             capture_output=True,
             text=True,
             timeout=12,
         )
@@
         for shell, shell_args, script in cases:
             if not script.exists():
                 print(f"SKIP: missing integration script at {script}")
                 continue
+            shell_path = shutil.which(shell)
+            if shell_path is None:
+                print(f"SKIP: missing shell binary {shell}")
+                continue
             for scenario in scenarios:
                 rc, detail = _run_case(
                     base / f"{shell}-{scenario}",
                     shell=shell,
+                    shell_path=shell_path,
                     shell_args=shell_args,
                     script=script,
                     scenario=scenario,
                 )

Also applies to: 369-380

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_issue_1138_sidebar_pr_polling.py` around lines 250 - 290, The test
harness currently calls subprocess.run in _run_case (around where
BoundUnixSocket is used) and will raise FileNotFoundError if the requested shell
(bash/zsh) is not present; wrap the subprocess.run call in a try/except
FileNotFoundError and convert that into a clean skip by returning a skip tuple
(e.g., an exit code and message) instead of letting the exception propagate.
Update both occurrences (the subprocess.run in _run_case and the similar block
at the later location around lines 369-380) to catch FileNotFoundError, include
the missing shell name in the returned message, and ensure the function still
returns the expected tuple[int, str] for skipped cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/shell-integration/cmux-bash-integration.bash`:
- Around line 155-168: The temp stderr file (err_file) created for gh pr view
can be left behind if the function is killed early (e.g., by
_cmux_run_pr_probe_with_timeout); add a trap immediately after err_file is set
that removes the file on EXIT (e.g., trap '/bin/rm -f -- "$err_file" >/dev/null
2>&1 || true' EXIT) to guarantee cleanup, and ensure you clear or reset that
trap (remove the EXIT handler) before the normal cleanup and return so you don't
try to remove a non-existent or moved file later; reference err_file, the
gh_output/gh_status/gh_error handling block and the enclosing function where
_cmux_run_pr_probe_with_timeout is invoked.

In `@Resources/shell-integration/cmux-zsh-integration.zsh`:
- Around line 179-192: The temp stderr file created in _cmux_report_pr_for_path
(err_file from the mktemp call) can be leaked if the process is killed; add a
trap right after creating err_file to remove it on EXIT, TERM, INT, and HUP
(e.g., trap 'rm -f -- "$err_file" >/dev/null 2>&1 || true' EXIT TERM INT HUP),
and restore or clear the trap after the existing cleanup code runs so it only
affects this function; ensure the trap is installed immediately after err_file
creation and that the existing /bin/rm -f cleanup stays in place.

---

Duplicate comments:
In `@tests/test_issue_1138_sidebar_pr_polling.py`:
- Around line 250-290: The test harness currently calls subprocess.run in
_run_case (around where BoundUnixSocket is used) and will raise
FileNotFoundError if the requested shell (bash/zsh) is not present; wrap the
subprocess.run call in a try/except FileNotFoundError and convert that into a
clean skip by returning a skip tuple (e.g., an exit code and message) instead of
letting the exception propagate. Update both occurrences (the subprocess.run in
_run_case and the similar block at the later location around lines 369-380) to
catch FileNotFoundError, include the missing shell name in the returned message,
and ensure the function still returns the expected tuple[int, str] for skipped
cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86b5f367-80aa-4208-9ae5-705393bb7cf4

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad9775 and 84edefa.

📒 Files selected for processing (3)
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • tests/test_issue_1138_sidebar_pr_polling.py

Comment on lines +155 to +168
err_file="$(/usr/bin/mktemp "${TMPDIR:-/tmp}/cmux-gh-pr-view.XXXXXX" 2>/dev/null || true)"
[[ -n "$err_file" ]] || return 1
gh_output="$(
builtin cd "$repo_path" 2>/dev/null \
&& gh pr view \
--json number,state,url \
--jq '[.number, .state, .url] | @tsv' \
2>"$err_file"
)"
gh_status=$?
if [[ -f "$err_file" ]]; then
gh_error="$("/bin/cat" -- "$err_file" 2>/dev/null || true)"
/bin/rm -f -- "$err_file" >/dev/null 2>&1 || true
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n Resources/shell-integration/cmux-bash-integration.bash | head -200

Repository: manaflow-ai/cmux

Length of output: 8436


🏁 Script executed:

rg "_cmux_report_pr_for_path" Resources/shell-integration/cmux-bash-integration.bash -A 3 -B 3

Repository: manaflow-ai/cmux

Length of output: 346


🏁 Script executed:

rg "_cmux_report_pr_for_path" Resources/shell-integration/cmux-bash-integration.bash -B 15 -A 15

Repository: manaflow-ai/cmux

Length of output: 2134


🏁 Script executed:

sed -n '200,280p' Resources/shell-integration/cmux-bash-integration.bash

Repository: manaflow-ai/cmux

Length of output: 2373


Add trap cleanup to ensure temporary file is removed on early termination.

The function creates a temporary stderr file at line 155, but if the process is killed by the timeout in _cmux_run_pr_probe_with_timeout() before reaching the cleanup at lines 165-167, the file is never removed. Add trap '/bin/rm -f -- "$err_file" >/dev/null 2>&1 || true' EXIT after the variable declarations to ensure cleanup runs even if the function exits early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/shell-integration/cmux-bash-integration.bash` around lines 155 -
168, The temp stderr file (err_file) created for gh pr view can be left behind
if the function is killed early (e.g., by _cmux_run_pr_probe_with_timeout); add
a trap immediately after err_file is set that removes the file on EXIT (e.g.,
trap '/bin/rm -f -- "$err_file" >/dev/null 2>&1 || true' EXIT) to guarantee
cleanup, and ensure you clear or reset that trap (remove the EXIT handler)
before the normal cleanup and return so you don't try to remove a non-existent
or moved file later; reference err_file, the gh_output/gh_status/gh_error
handling block and the enclosing function where _cmux_run_pr_probe_with_timeout
is invoked.

Comment on lines +179 to +192
err_file="$(/usr/bin/mktemp "${TMPDIR:-/tmp}/cmux-gh-pr-view.XXXXXX" 2>/dev/null || true)"
[[ -n "$err_file" ]] || return 1
gh_output="$(
builtin cd "$repo_path" 2>/dev/null \
&& gh pr view \
--json number,state,url \
--jq '[.number, .state, .url] | @tsv' \
2>"$err_file"
)"
gh_status=$?
if [[ -f "$err_file" ]]; then
gh_error="$("/bin/cat" -- "$err_file" 2>/dev/null || true)"
/bin/rm -f -- "$err_file" >/dev/null 2>&1 || true
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "cmux-zsh-integration.zsh" -type f

Repository: manaflow-ai/cmux

Length of output: 114


🏁 Script executed:

wc -l ./Resources/shell-integration/cmux-zsh-integration.zsh

Repository: manaflow-ai/cmux

Length of output: 118


🏁 Script executed:

cat -n ./Resources/shell-integration/cmux-zsh-integration.zsh | head -250

Repository: manaflow-ai/cmux

Length of output: 10042


🏁 Script executed:

cat -n ./Resources/shell-integration/cmux-zsh-integration.zsh | sed -n '244,300p'

Repository: manaflow-ai/cmux

Length of output: 2129


🏁 Script executed:

cat -n ./Resources/shell-integration/cmux-zsh-integration.zsh | sed -n '158,221p'

Repository: manaflow-ai/cmux

Length of output: 2572


🏁 Script executed:

grep -n "trap\|always" ./Resources/shell-integration/cmux-zsh-integration.zsh

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Verify zsh signal behavior - when a background process is killed, 
# does execution continue or stop?
cat > /tmp/test_signal.sh << 'EOF'
#!/bin/zsh
test_func() {
    local tmpfile="/tmp/test_$$_$RANDOM"
    touch "$tmpfile"
    echo "Created: $tmpfile"
    sleep 10  # Simulate long-running command
    echo "Cleanup line 1"
    echo "Cleanup line 2"
}

# Run in background
(test_func) &
bg_pid=$!
sleep 0.5
kill -TERM $bg_pid 2>/dev/null
wait $bg_pid 2>/dev/null
echo "Parent continues after kill"
EOF
chmod +x /tmp/test_signal.sh
/tmp/test_signal.sh
ls -la /tmp/test_*_* 2>/dev/null | head -5

Repository: manaflow-ai/cmux

Length of output: 140


🏁 Script executed:

# Check the exact flow: background subshell in _cmux_run_pr_probe_with_timeout
sed -n '244,273p' ./Resources/shell-integration/cmux-zsh-integration.zsh

Repository: manaflow-ai/cmux

Length of output: 912


Add cleanup guard for temporary stderr file to prevent leaks on timeout kill.

When _cmux_run_pr_probe_with_timeout() kills the probe with TERM/KILL signals, the _cmux_report_pr_for_path() process terminates immediately. If the signal arrives during the gh pr view command (lines 183-187) or before the cleanup code (lines 189-191) executes, the temporary stderr file created at line 179 remains in $TMPDIR. Repeated timeout retries accumulate these cmux-gh-pr-view.* files. Wrap the temp file operations in a trap or always block to ensure cleanup runs on process termination.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/shell-integration/cmux-zsh-integration.zsh` around lines 179 - 192,
The temp stderr file created in _cmux_report_pr_for_path (err_file from the
mktemp call) can be leaked if the process is killed; add a trap right after
creating err_file to remove it on EXIT, TERM, INT, and HUP (e.g., trap 'rm -f --
"$err_file" >/dev/null 2>&1 || true' EXIT TERM INT HUP), and restore or clear
the trap after the existing cleanup code runs so it only affects this function;
ensure the trap is installed immediately after err_file creation and that the
existing /bin/rm -f cleanup stays in place.

@austinywang
austinywang merged commit 05ad899 into main Mar 12, 2026
17 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…ar-pr-badge-detection

Fix sidebar PR badge polling for idle shells

This branch was successfully deployed

1 active deployment
Preview — 84edefa9 Deployed Mar 10, 2026 by vercel[bot]
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.

Sidebar PR/issue badges often missing when agents (Claude/Codex) create them

1 participant