Fix sidebar branch refresh during agent-driven git checkout (#671)

This commit is contained in:
Austin Wang 2026-03-04 19:13:19 -08:00 committed by GitHub
parent 80baae355a
commit 39a0da2b7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 10 deletions

View file

@ -46,6 +46,7 @@ typeset -g _CMUX_GIT_FORCE=0
typeset -g _CMUX_GIT_HEAD_LAST_PWD=""
typeset -g _CMUX_GIT_HEAD_PATH=""
typeset -g _CMUX_GIT_HEAD_MTIME=0
typeset -g _CMUX_GIT_HEAD_WATCH_PID=""
typeset -g _CMUX_HAVE_ZSTAT=0
typeset -g _CMUX_PR_LAST_PWD=""
typeset -g _CMUX_PR_LAST_RUN=0
@ -148,6 +149,65 @@ _cmux_ports_kick() {
} >/dev/null 2>&1 &!
}
_cmux_report_git_branch_for_path() {
local repo_path="$1"
[[ -n "$repo_path" ]] || return 0
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local branch dirty_opt="" first
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
if [[ -n "$branch" ]]; then
first="$(git -C "$repo_path" status --porcelain -uno 2>/dev/null | head -1)"
[[ -n "$first" ]] && dirty_opt="--status=dirty"
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi
}
_cmux_stop_git_head_watch() {
if [[ -n "$_CMUX_GIT_HEAD_WATCH_PID" ]]; then
kill "$_CMUX_GIT_HEAD_WATCH_PID" >/dev/null 2>&1 || true
_CMUX_GIT_HEAD_WATCH_PID=""
fi
}
_cmux_start_git_head_watch() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local watch_pwd="$PWD"
local watch_head_path
watch_head_path="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
[[ -n "$watch_head_path" ]] || return 0
local watch_head_mtime
watch_head_mtime="$(_cmux_git_head_mtime "$watch_head_path" 2>/dev/null || echo 0)"
_CMUX_GIT_HEAD_LAST_PWD="$watch_pwd"
_CMUX_GIT_HEAD_PATH="$watch_head_path"
_CMUX_GIT_HEAD_MTIME="$watch_head_mtime"
_cmux_stop_git_head_watch
{
local last_mtime="$watch_head_mtime"
while true; do
sleep 1
local mtime
mtime="$(_cmux_git_head_mtime "$watch_head_path" 2>/dev/null || echo 0)"
if [[ -n "$mtime" && "$mtime" != 0 && "$mtime" != "$last_mtime" ]]; then
last_mtime="$mtime"
_cmux_report_git_branch_for_path "$watch_pwd"
fi
done
} >/dev/null 2>&1 &!
_CMUX_GIT_HEAD_WATCH_PID=$!
}
_cmux_preexec() {
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
@ -169,9 +229,12 @@ _cmux_preexec() {
# Register TTY + kick batched port scan for foreground commands (servers).
_cmux_report_tty_once
_cmux_ports_kick
_cmux_start_git_head_watch
}
_cmux_precmd() {
_cmux_stop_git_head_watch
# Skip if socket doesn't exist yet
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
@ -227,6 +290,8 @@ _cmux_precmd() {
fi
# Git branch/dirty: update immediately on directory change, otherwise every ~3s.
# While a foreground command is running, _cmux_start_git_head_watch probes HEAD
# once per second so agent-initiated git checkouts still surface quickly.
local should_git=0
# Git branch can change without a `git ...`-prefixed command (aliases like `gco`,
@ -280,16 +345,7 @@ _cmux_precmd() {
_CMUX_GIT_LAST_PWD="$pwd"
_CMUX_GIT_LAST_RUN=$now
{
local branch dirty_opt=""
branch=$(git branch --show-current 2>/dev/null)
if [[ -n "$branch" ]]; then
local first
first=$(git status --porcelain -uno 2>/dev/null | head -1)
[[ -n "$first" ]] && dirty_opt="--status=dirty"
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
else
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi
_cmux_report_git_branch_for_path "$pwd"
} >/dev/null 2>&1 &!
_CMUX_GIT_JOB_PID=$!
_CMUX_GIT_JOB_STARTED_AT=$now
@ -387,7 +443,12 @@ _cmux_fix_path() {
add-zsh-hook -d precmd _cmux_fix_path
}
_cmux_zshexit() {
_cmux_stop_git_head_watch
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec _cmux_preexec
add-zsh-hook precmd _cmux_precmd
add-zsh-hook precmd _cmux_fix_path
add-zsh-hook zshexit _cmux_zshexit

View file

@ -72,6 +72,7 @@ def _wait_for_git_branch(
expected: str,
timeout: float = 12.0,
interval: float = 0.15,
allow_force_fallback: bool = True,
) -> dict[str, str]:
def pred():
state = _parse_sidebar_state(client.sidebar_state())
@ -82,6 +83,8 @@ def _wait_for_git_branch(
try:
return _wait_for(pred, timeout=timeout, interval=interval, label=f"git_branch={expected!r}")
except AssertionError as original_error:
if not allow_force_fallback:
raise original_error
# VM shells can occasionally skip a prompt hook; force a one-shot report so
# the remainder of the flow can still validate transition behavior.
try:
@ -180,6 +183,18 @@ def main() -> int:
_send_cd_and_wait(client, repo)
_wait_for_git_branch(client, "main")
# Branch changes during a long-running foreground command should still
# propagate before the prompt returns (agent-style workflows).
client.send("bash -lc 'git checkout -b feature/agent-live >/dev/null 2>&1; sleep 6'\n")
_wait_for_git_branch(
client,
"feature/agent-live",
timeout=3.5,
interval=0.1,
allow_force_fallback=False,
)
time.sleep(6.3)
# Branch change should update.
# Cover alias/non-`git ...` command paths too (regression: branch could
# stick for ~3s when switching via alias/tools like `gh pr checkout`).