Repository navigation
fix(sidebar): render the PR icon, and let a stale PR badge come back down - #182
Conversation
`gh pr view --json state` answers OPEN / MERGED / CLOSED, and `report_pr` carries that string through untouched. `SidebarMetadata.prStatus` is a plain string while `WorkspaceInfo.prStatus` is the lowercase union, so the casing difference has nothing to catch it on the way into the store, and PrStatusIcon's switch — written against the union — runs off the end for every real report. React 19 renders the implicit `undefined` as nothing, so the row degrades quietly to a bare "#450 MERGED" with no glyph, which reads more like a stuck badge than a merged one. The fold onto the union lives next to the icon that needs it, as a small pure function, so the render boundary is safe whatever a caller passes. The switch also gains an explicit `null` default, so an unknown state can't go back to drawing a silent blank. The badge text keeps its uppercase look via `text-transform`, so nothing about the row's appearance changes except the icon appearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR metadata is workspace-scoped while every PowerShell pane in the workspace runs its own poller, and they all write the same row. Two things follow from that, both about which pane speaks for the row. The first is that a `clear_pr` from one pane can take down a PR another pane just reported — two panes, two repos, one workspace. The reporting surface is now recorded alongside the PR (`prSurfaceId`), and a `clear_pr` is honoured only from that surface. Tracking it shell-side instead would not be enough on its own: a poller job cannot know another pane has since overwritten the row. The second is the case the report opens with — closing the reporting pane leaves the badge up. That one cannot be answered from the shell at all, since a killed PTY runs no exit handler, so the badge is dropped where the surface itself goes away: the same close chokepoints that already reap the PTY (closeSurface, closePane, closeOtherSurfaces, closeSurfacesToRight). A surface merely moved between panes keeps its badge; only a real close drops it, which is pinned either way. Once a clear is owner-gated, any path that destroys the owning surface without clearing strands the badge for good — no other pane's clear is accepted either — so the two remaining paths matter more than they did before. An agent spawn with `--replace-tab` swaps out a pane's sole terminal and kills it directly rather than going through closeSurface, so it now runs the same owner-gated clear, exposed as a store action rather than duplicated. And `createWorkspace` carries `prSurfaceId` alongside the PR fields already copied there, so a workspace can never be created owning a badge it cannot clear. A shell that dies inside a still-open tab (the user types `exit`, or the process falls over) reaches none of those transitions — nothing calls closeSurface, the tab is still there — so the badge is dropped from the `pty:exit` handler instead, which is already where a stuck "Running" badge and a leftover progress indicator are healed for the same reason. That signal arrives whether the shell left gracefully or was killed, so it needs no help from the shell side. The decision itself moves to a pure `applyPrCommand()`, next to the handler it came from, so both paths are testable without the component tree — and the normalization the icon already does is applied there too, at the point the value enters the store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ise one
`clear_pr` is handled in App.tsx but has no sender, unlike `clear_git_branch`,
whose clearing half is wired up in both integrations. The tick reports only on
the success path, so a `gh pr view` that finds nothing sends nothing and the
last reported PR stays on the row for the life of the workspace.
The job's location is the other half of it. `Start-Job` hands the child
runspace the location captured at call time and keeps it there. Starting the
poller on the shell's first idle is what keeps several hundred ms of runspace
startup off the first prompt and is worth keeping — but it does mean the
captured location is the one the pane opened in, so a pane that cd's into
another repo or worktree keeps being answered for the first one.
So the tick now decides through one pure function, `Get-WmuxPrMessage`, and the
prompt publishes the pane's live location for the job to read back each tick:
in a repo, gh resolves a PR → report_pr
in a repo, gh finds nothing → clear_pr, but only if this pane reported
not in a repo → clear_pr, but only if this pane reported
Retracting only its own claim is what keeps a pane from speaking for panes it
knows nothing about: one workspace with a pane on a PR branch and a pane in `~`
would otherwise take turns reporting and clearing every 45 seconds. It also
means the pipe goes quiet once a badge is down, rather than carrying a
`clear_pr` forever. The renderer honours a clear only from the surface that
owns the row, so a retraction can never reach past this pane's own claim.
Walking out of a repo retracts for the same reason the branch does: the prompt
drops the branch off the row on the very next command, and a poller that stayed
quiet there would leave the PR sitting beside a row with no repo behind it.
Three tick-state edges come with it:
- The "did I claim this" flag must only advance on a send that actually
landed. Advancing it on the decision left a `clear_pr` that failed to go out
(pipe not up yet, wmux busy, connect timeout) believing there was nothing
left to retract — the badge this change exists to unstick, stuck again one
step later. `Invoke-WmuxPrTick` leaves the flag where it was so the next
tick recomputes and retries.
- An unreadable cwd hand-off used to fall back to whatever location the
runspace last held, then report that repo's PR as current — the frozen
location again, in miniature. `Resolve-WmuxPaneCwd` answers null for every
case where the pane's location cannot be trusted (file missing, empty,
unreadable, or naming somewhere that is no longer a directory), and the tick
then says nothing rather than answering for a pane that may have moved on.
- The hand-off file outlives panes that are killed rather than closed: a kill
runs no exit handler, so nothing removes theirs and the directory grows one
small file per pane. Age separates the two, and a live pane keeps its own
file young from both ends — the prompt rewrites it on every command, and its
poller touches it on every tick, so a pane parked at an idle prompt for days
still cannot be swept out from under itself. The sweep runs on the same
first idle as the poller, so it stays off the first prompt too.
The cwd hand-off is a file rather than the pipe deliberately, and the reason is
recorded next to it: the pipe runs one direction, shell to wmux, and the
consumer here is another shell process. It lives in the temp directory
wmux-bash-integration.sh already uses for its own hand-off, and is removed on
exit. Both copies of the script move together, as the mirror check added in
1.1.1 requires.
Verified end to end on PowerShell 7 and Windows PowerShell 5.1; the decision
functions are exercised in a real host, including through an actual job's
initialization script, rather than asserted against the source text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Merged, and shipping in v1.6.0. Thank you — this is one of the best-argued reports this repo has had. The casing bug is the kind I'd never have found by reading the code. The two calls you left meBoth in favour of what you shipped — no changes on merge. A merged PR keeps its badge. You're right that A pane that walks out of a repo retracts its own badge. Same reasoning as yours — the prompt drops the branch off the row on the very next command, so a PR sitting beside a repo-less row reads as stuck. That it only ever retracts its own claim is what makes it safe to do at all. On the unauthenticated- What I expect to reuse
Also appreciated the parse gate on the integration script. A script that fails to parse costs the pane its whole prompt, git branch and shell state, and nothing else in the suite would have noticed. Verified on merge
One tiny follow-up commit of my own on top, unrelated to your work: #183 merged alongside this and moved |
The PR badge on a workspace row can be raised but never lowered, and its icon
doesn't reach the screen. Three commits, each standing on its own.
1. The icon never renders
gh pr view --json stateanswers in GitHub's casing —OPEN/MERGED/CLOSED— andreport_prcarries that string through untouched.SidebarMetadata.prStatusis a plainstringwhileWorkspaceInfo.prStatusisthe lowercase union, so the casing difference has nothing to catch it on the way
into the store.
PrStatusIconswitches on the lowercase union, so every real report falls pastall three arms and the component returns
undefined. React 19 renders that asnothing rather than throwing, so the row degrades quietly to a bare
#450 MERGEDwith a missing glyph — which reads more like a stuck badge than amerged one.
The fold onto the union lives next to the icon as a small pure function, so the
render boundary is safe whatever a caller passes, and the switch gains an
explicit
nulldefault. Badge text keeps its uppercase appearance viatext-transform, so nothing about the row changes except the icon appearing.2. A badge belongs to the pane that reported it
PR metadata is workspace-scoped while every PowerShell pane in the workspace
runs its own poller, and they all write the same row. Two things follow.
A
clear_prfrom one pane could take down a PR another pane had just reported —two panes, two repos, one workspace. The reporting surface is now recorded
alongside the PR (
prSurfaceId) and aclear_pris honoured only from thatsurface. Tracking it shell-side wouldn't be enough on its own: a poller job
can't know another pane has since overwritten the row.
And closing the reporting pane left the badge up — the case the report opens
with. That can't be answered from the shell, since a killed PTY runs no exit
handler, so the badge is dropped where the surface itself goes away: the same
close chokepoints that already reap the PTY. A surface merely moved between
panes keeps its badge; only a real close drops it.
A shell that dies inside a still-open tab reaches none of those transitions, so
that one is answered from the
pty:exithandler inuseTerminal.ts— alreadywhere a stuck "Running" badge and a leftover progress indicator are healed for
the same reason, and a signal that arrives whether the shell left gracefully or
was killed.
3. The poller can lower a badge, and follows the pane
clear_pris handled inApp.tsxbut has no sender, unlikeclear_git_branch,whose clearing half is wired up in both integrations. The tick reports only on
the success path, so a
gh pr viewthat finds nothing sends nothing.The job's location is the other half.
Start-Jobhands the child runspace thelocation captured at call time and keeps it there. Starting the poller on the
shell's first idle is what keeps several hundred ms of runspace startup off the
first prompt and is worth keeping — but it does mean a pane that cd's into
another repo or worktree keeps being answered for the first one.
The tick now decides through one pure function and the prompt publishes the
pane's live location for the job to read back each tick:
report_prclear_prclear_prRetracting only its own claim is what keeps a pane from speaking for panes it
knows nothing about — one workspace with a pane on a PR branch and a pane in
~would otherwise take turns reporting and clearing every 45s — and it means the
pipe goes quiet once a badge is down. The renderer honours a clear only from the
surface that owns the row, so a retraction can never reach past its own claim.
Three edges came with it: the "did I claim this" flag now advances only on a
send that actually landed (a
clear_prthat failed to go out used to leave thepane believing it had nothing left to retract); an unreadable cwd hand-off is
treated as no information rather than "stay put", which would have gone on
polling a stale repo; and the hand-off files of panes that were killed rather
than closed are swept by age, since a kill runs no exit handler. A live pane
keeps its own file young from both ends — the prompt rewrites it on every
command and the poller touches it on every tick — so a pane parked at an idle
prompt for days can't be swept out from under itself.
Calls that felt like yours to make, not mine
Should a merged PR display at all? Left exactly as it is.
gh pr viewresolves a PR from its branch after it's merged, so a pane sitting on a merged
branch legitimately keeps reporting
MERGED— refreshed, not stale. Now thatthe icon renders, that may well be the useful behaviour; if you'd rather a
merged PR cleared itself, that's a one-line change and I'm glad to make it.
When a pane walks out of a repo. It retracts its own badge, on the reasoning
that the prompt drops the branch off the row on the very next command and a PR
sitting beside a row with no repo behind it reads as stuck. Happy to make that
row of the table silent instead if you'd read it the other way.
Why the cwd hand-off is a file and not the pipe
Worth stating outright, since the pipe is the obvious candidate. It runs one
direction — shell to wmux — and the consumer here is another shell process.
The prompt already sends this exact value across as
report_pwd; routing thehand-off through the pipe would mean adding
currentCwdto the surface listing,a V2 method and a CLI verb to read it back, and a
nodespawn on every 45stick, so that the shell could ask wmux for something the shell itself had just
told it.
The file lives in
<temp>\wmux— the directorywmux-bash-integration.shalready uses for its own hand-off — is removed on shell exit, and panes that
were killed rather than closed are swept by age on a later shell's first idle,
so the directory can't grow one file per pane forever.
Known edge
If
ghis unauthenticated while the pane is in a repo,gh pr viewfails thesame way "no PR on this branch" does, so a pane that had reported retracts once.
It re-reports as soon as auth is restored. Separating the two means parsing gh's
stderr text, which doesn't hold across gh versions — the single spurious clear
seemed the better trade, but say the word if you'd rather gate on
gh auth status.Verification
npx vitest run— 1225 passed / 100 files (master @ v1.5.1: 1159 / 95).npm run lint— unchanged; the errors in touched files are pre-existing onmaster (confirmed by stashing).
decision functions run inside an actual
Start-Job -InitializationScript—a job runspace inherits none of the session's functions, and a silent failure
there would look exactly like the bug.
$($function:…)reference while this was being written, and nothing else inthe suite would notice a script that fails to parse — the cost of one is the
pane's whole prompt, git branch and shell state going quiet.
requires.
Notes