Poll sidebar PR status while shells are idle

This commit is contained in:
austinpower1258 2026-03-09 18:06:30 -07:00
parent 06cb63cc8d
commit 844d85fe80
2 changed files with 261 additions and 126 deletions

View file

@ -44,11 +44,14 @@ _CMUX_GIT_JOB_STARTED_AT="${_CMUX_GIT_JOB_STARTED_AT:-0}"
_CMUX_GIT_HEAD_LAST_PWD="${_CMUX_GIT_HEAD_LAST_PWD:-}"
_CMUX_GIT_HEAD_PATH="${_CMUX_GIT_HEAD_PATH:-}"
_CMUX_GIT_HEAD_SIGNATURE="${_CMUX_GIT_HEAD_SIGNATURE:-}"
_CMUX_PR_LAST_PWD="${_CMUX_PR_LAST_PWD:-}"
_CMUX_PR_LAST_RUN="${_CMUX_PR_LAST_RUN:-0}"
_CMUX_PR_JOB_PID="${_CMUX_PR_JOB_PID:-}"
_CMUX_PR_JOB_STARTED_AT="${_CMUX_PR_JOB_STARTED_AT:-0}"
_CMUX_PR_POLL_PID="${_CMUX_PR_POLL_PID:-}"
_CMUX_PR_POLL_PWD="${_CMUX_PR_POLL_PWD:-}"
_CMUX_PR_POLL_INTERVAL="${_CMUX_PR_POLL_INTERVAL:-45}"
_CMUX_PR_FORCE="${_CMUX_PR_FORCE:-0}"
_CMUX_ASYNC_JOB_TIMEOUT="${_CMUX_ASYNC_JOB_TIMEOUT:-20}"
_CMUX_PREEXEC_READY="${_CMUX_PREEXEC_READY:-0}"
_CMUX_IN_PROMPT_COMMAND="${_CMUX_IN_PROMPT_COMMAND:-0}"
_CMUX_DEBUG_TRAP_INSTALLED="${_CMUX_DEBUG_TRAP_INSTALLED:-0}"
_CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}"
_CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}"
@ -115,10 +118,136 @@ _cmux_ports_kick() {
} >/dev/null 2>&1 & disown
}
_cmux_clear_pr_for_panel() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}
_cmux_report_pr_for_path() {
local repo_path="$1"
[[ -n "$repo_path" ]] || {
_cmux_clear_pr_for_panel
return 0
}
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local branch pr_tsv gh_status number state url status_opt=""
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
_cmux_clear_pr_for_panel
return 0
fi
pr_tsv="$(
builtin cd "$repo_path" 2>/dev/null \
&& gh pr list \
--head "$branch" \
--state all \
--json number,state,url,updatedAt \
--jq 'if length == 0 then "" else (sort_by(.updatedAt) | last | [.number, .state, .url] | @tsv) end' \
2>/dev/null
)"
gh_status=$?
if (( gh_status != 0 )); then
# Preserve the last-known PR badge when gh fails transiently, then retry
# on the next background poll instead of clearing visible state.
return 1
fi
if [[ -z "$pr_tsv" ]]; then
_cmux_clear_pr_for_panel
return 0
fi
IFS=$'\t' read -r number state url <<< "$pr_tsv"
if [[ -z "$number" || -z "$url" ]]; then
return 1
fi
case "$state" in
MERGED) status_opt="--state=merged" ;;
OPEN) status_opt="--state=open" ;;
CLOSED) status_opt="--state=closed" ;;
*) return 1 ;;
esac
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}
_cmux_stop_pr_poll_loop() {
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
kill "$_CMUX_PR_POLL_PID" >/dev/null 2>&1 || true
_CMUX_PR_POLL_PID=""
fi
}
_cmux_start_pr_poll_loop() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local watch_pwd="${1:-$PWD}"
local force_restart="${2:-0}"
local watch_shell_pid="$$"
local interval="${_CMUX_PR_POLL_INTERVAL:-45}"
if [[ "$force_restart" != "1" && "$watch_pwd" == "$_CMUX_PR_POLL_PWD" && -n "$_CMUX_PR_POLL_PID" ]] \
&& kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
return 0
fi
_cmux_stop_pr_poll_loop
_CMUX_PR_POLL_PWD="$watch_pwd"
{
while :; do
kill -0 "$watch_shell_pid" 2>/dev/null || break
_cmux_report_pr_for_path "$watch_pwd" || true
sleep "$interval"
done
} >/dev/null 2>&1 &
_CMUX_PR_POLL_PID=$!
disown "$_CMUX_PR_POLL_PID" 2>/dev/null || disown
}
_cmux_bash_cleanup() {
_cmux_stop_pr_poll_loop
}
_cmux_preexec_trap() {
(( _CMUX_IN_PROMPT_COMMAND )) && return 0
(( _CMUX_PREEXEC_READY )) || return 0
_CMUX_PREEXEC_READY=0
local cmd="${BASH_COMMAND## }"
case "$cmd" in
git\ *|git|gh\ *|gh|lazygit|lazygit\ *|tig|tig\ *|gitui|gitui\ *|stg\ *|jj\ *)
_CMUX_PR_FORCE=1
;;
esac
}
_cmux_install_debug_trap() {
(( _CMUX_DEBUG_TRAP_INSTALLED )) && return 0
local existing
existing="$(trap -p DEBUG 2>/dev/null || true)"
if [[ -n "$existing" ]]; then
return 0
fi
trap '_cmux_preexec_trap' DEBUG
_CMUX_DEBUG_TRAP_INSTALLED=1
}
_cmux_prompt_command() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_CMUX_IN_PROMPT_COMMAND=1
local now=$SECONDS
local pwd="$PWD"
@ -135,16 +264,6 @@ _cmux_prompt_command() {
fi
fi
if [[ -n "$_CMUX_PR_JOB_PID" ]]; then
if ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
elif (( _CMUX_PR_JOB_STARTED_AT > 0 )) && (( now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
fi
fi
# Resolve TTY name once.
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
@ -178,8 +297,8 @@ _cmux_prompt_command() {
if [[ -n "$head_signature" && "$head_signature" != "$_CMUX_GIT_HEAD_SIGNATURE" ]]; then
_CMUX_GIT_HEAD_SIGNATURE="$head_signature"
git_head_changed=1
# Also invalidate the PR probe so it refreshes with the new branch.
_CMUX_PR_LAST_RUN=0
# Also invalidate the PR poller so it refreshes with the new branch.
_CMUX_PR_FORCE=1
fi
fi
@ -215,49 +334,20 @@ _cmux_prompt_command() {
_CMUX_GIT_JOB_STARTED_AT=$now
fi
# Pull request metadata (number/state/url):
# refresh on cwd change, HEAD change, and periodically to avoid stale status.
if [[ -n "$_CMUX_PR_JOB_PID" ]] && kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" || "$git_head_changed" == "1" ]]; then
kill "$_CMUX_PR_JOB_PID" >/dev/null 2>&1 || true
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
fi
# Pull request metadata is remote state. Keep polling while the shell sits
# at a prompt so newly created or merged PRs appear without another command.
local should_restart_pr_poll=0
if [[ "$pwd" != "$_CMUX_PR_POLL_PWD" || "$git_head_changed" == "1" ]]; then
should_restart_pr_poll=1
elif (( _CMUX_PR_FORCE )); then
should_restart_pr_poll=1
elif [[ -z "$_CMUX_PR_POLL_PID" ]] || ! kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
should_restart_pr_poll=1
fi
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" || "$git_head_changed" == "1" ]] || (( now - _CMUX_PR_LAST_RUN >= 60 )); then
if [[ -z "$_CMUX_PR_JOB_PID" ]] || ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
_CMUX_PR_LAST_PWD="$pwd"
_CMUX_PR_LAST_RUN=$now
{
local branch pr_tsv number state url status_opt=""
branch=$(git branch --show-current 2>/dev/null)
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
pr_tsv="$(gh pr view --json number,state,url --jq '[.number, .state, .url] | @tsv' 2>/dev/null || true)"
if [[ -z "$pr_tsv" ]]; then
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
IFS=$'\t' read -r number state url <<< "$pr_tsv"
if [[ -z "$number" || -z "$url" ]]; then
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
case "$state" in
MERGED) status_opt="--state=merged" ;;
OPEN) status_opt="--state=open" ;;
CLOSED) status_opt="--state=closed" ;;
*) status_opt="" ;;
esac
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi
fi
fi
} >/dev/null 2>&1 &
_CMUX_PR_JOB_PID=$!
disown
_CMUX_PR_JOB_STARTED_AT=$now
fi
if (( should_restart_pr_poll )); then
_CMUX_PR_FORCE=0
_cmux_start_pr_poll_loop "$pwd" 1
fi
# Ports: lightweight kick to the app's batched scanner every ~10s.
@ -265,6 +355,8 @@ _cmux_prompt_command() {
_cmux_ports_kick
fi
_CMUX_IN_PROMPT_COMMAND=0
_CMUX_PREEXEC_READY=1
}
_cmux_install_prompt_command() {
@ -317,3 +409,4 @@ _cmux_fix_path
unset -f _cmux_fix_path
_cmux_install_prompt_command
_cmux_install_debug_trap

View file

@ -47,10 +47,9 @@ typeset -g _CMUX_GIT_HEAD_LAST_PWD=""
typeset -g _CMUX_GIT_HEAD_PATH=""
typeset -g _CMUX_GIT_HEAD_SIGNATURE=""
typeset -g _CMUX_GIT_HEAD_WATCH_PID=""
typeset -g _CMUX_PR_LAST_PWD=""
typeset -g _CMUX_PR_LAST_RUN=0
typeset -g _CMUX_PR_JOB_PID=""
typeset -g _CMUX_PR_JOB_STARTED_AT=0
typeset -g _CMUX_PR_POLL_PID=""
typeset -g _CMUX_PR_POLL_PWD=""
typeset -g _CMUX_PR_POLL_INTERVAL=45
typeset -g _CMUX_PR_FORCE=0
typeset -g _CMUX_ASYNC_JOB_TIMEOUT=20
@ -141,6 +140,101 @@ _cmux_report_git_branch_for_path() {
fi
}
_cmux_clear_pr_for_panel() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}
_cmux_report_pr_for_path() {
local repo_path="$1"
[[ -n "$repo_path" ]] || {
_cmux_clear_pr_for_panel
return 0
}
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local branch pr_tsv number state url status_opt="" gh_status
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
_cmux_clear_pr_for_panel
return 0
fi
pr_tsv="$(
builtin cd "$repo_path" 2>/dev/null \
&& gh pr list \
--head "$branch" \
--state all \
--json number,state,url,updatedAt \
--jq 'if length == 0 then "" else (sort_by(.updatedAt) | last | [.number, .state, .url] | @tsv) end' \
2>/dev/null
)"
gh_status=$?
if (( gh_status != 0 )); then
# Keep the last-known PR badge on transient gh failures (auth hiccups,
# API lag after creation, or rate limiting) and retry on the next poll.
return 1
fi
if [[ -z "$pr_tsv" ]]; then
_cmux_clear_pr_for_panel
return 0
fi
local IFS=$'\t'
read -r number state url <<< "$pr_tsv"
if [[ -z "$number" ]] || [[ -z "$url" ]]; then
return 1
fi
case "$state" in
MERGED) status_opt="--state=merged" ;;
OPEN) status_opt="--state=open" ;;
CLOSED) status_opt="--state=closed" ;;
*) return 1 ;;
esac
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}
_cmux_stop_pr_poll_loop() {
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
kill "$_CMUX_PR_POLL_PID" >/dev/null 2>&1 || true
_CMUX_PR_POLL_PID=""
fi
}
_cmux_start_pr_poll_loop() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local watch_pwd="${1:-$PWD}"
local force_restart="${2:-0}"
local watch_shell_pid="$$"
local interval="${_CMUX_PR_POLL_INTERVAL:-45}"
if [[ "$force_restart" != "1" && "$watch_pwd" == "$_CMUX_PR_POLL_PWD" && -n "$_CMUX_PR_POLL_PID" ]] \
&& kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
return 0
fi
_cmux_stop_pr_poll_loop
_CMUX_PR_POLL_PWD="$watch_pwd"
{
while true; do
kill -0 "$watch_shell_pid" >/dev/null 2>&1 || break
_cmux_report_pr_for_path "$watch_pwd" || true
sleep "$interval"
done
} >/dev/null 2>&1 &!
_CMUX_PR_POLL_PID=$!
}
_cmux_stop_git_head_watch() {
if [[ -n "$_CMUX_GIT_HEAD_WATCH_PID" ]]; then
kill "$_CMUX_GIT_HEAD_WATCH_PID" >/dev/null 2>&1 || true
@ -241,17 +335,6 @@ _cmux_precmd() {
fi
fi
if [[ -n "$_CMUX_PR_JOB_PID" ]]; then
if ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
elif (( _CMUX_PR_JOB_STARTED_AT > 0 )) && (( now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
_CMUX_PR_FORCE=1
fi
fi
# CWD: keep the app in sync with the actual shell directory.
# This is also the simplest way to test sidebar directory behavior end-to-end.
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
@ -326,63 +409,21 @@ _cmux_precmd() {
fi
fi
# Pull request metadata (number/state/url):
# - refresh on cwd change, explicit git/gh commands, and occasionally for status drift
# - keep this independent from the git probe cadence to avoid hitting GitHub too often
local should_pr=0
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]]; then
should_pr=1
# Pull request metadata is remote state. Keep a lightweight background poll
# alive while the shell is idle so gh-created PRs and merge status changes
# appear even without another prompt.
local should_restart_pr_poll=0
if [[ "$pwd" != "$_CMUX_PR_POLL_PWD" ]]; then
should_restart_pr_poll=1
elif (( _CMUX_PR_FORCE )); then
should_pr=1
elif (( now - _CMUX_PR_LAST_RUN >= 60 )); then
should_pr=1
should_restart_pr_poll=1
elif [[ -z "$_CMUX_PR_POLL_PID" ]] || ! kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
should_restart_pr_poll=1
fi
if (( should_pr )); then
local can_launch_pr=1
if [[ -n "$_CMUX_PR_JOB_PID" ]] && kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]] || (( _CMUX_PR_FORCE )); then
kill "$_CMUX_PR_JOB_PID" >/dev/null 2>&1 || true
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
else
can_launch_pr=0
fi
fi
if (( can_launch_pr )); then
_CMUX_PR_FORCE=0
_CMUX_PR_LAST_PWD="$pwd"
_CMUX_PR_LAST_RUN=$now
{
local branch pr_tsv number state url status_opt=""
branch=$(git branch --show-current 2>/dev/null)
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
pr_tsv="$(gh pr view --json number,state,url --jq '[.number, .state, .url] | @tsv' 2>/dev/null || true)"
if [[ -z "$pr_tsv" ]]; then
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
local IFS=$'\t'
read -r number state url <<< "$pr_tsv"
if [[ -z "$number" ]] || [[ -z "$url" ]]; then
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
case "$state" in
MERGED) status_opt="--state=merged" ;;
OPEN) status_opt="--state=open" ;;
CLOSED) status_opt="--state=closed" ;;
*) status_opt="" ;;
esac
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi
fi
fi
} >/dev/null 2>&1 &!
_CMUX_PR_JOB_PID=$!
_CMUX_PR_JOB_STARTED_AT=$now
fi
if (( should_restart_pr_poll )); then
_CMUX_PR_FORCE=0
_cmux_start_pr_poll_loop "$pwd" 1
fi
# Ports: lightweight kick to the app's batched scanner.
@ -419,6 +460,7 @@ _cmux_fix_path() {
_cmux_zshexit() {
_cmux_stop_git_head_watch
_cmux_stop_pr_poll_loop
}
autoload -Uz add-zsh-hook