fix(tracker): wire kanban merged-state lookup to GitHubContext - #229
Conversation
The kanban read PR status via wt.pr.is_merged, but WorktreeInfo.pr was declared and never assigned anywhere — PR data lives on GitHubContext.pullRequests, keyed by worktree path. Two prior fixes (PR #221, PR #224) shipped reading the same broken field and were dead on arrival. Switch the lookup to pullRequests[wt.path], extract a small isItemPRMerged helper with regression tests, and delete the dead pr field plus the two unused getters that depended on it. Also scope the kanban's PR auto-refresh to its own visible worktrees via setVisibleWorktrees, so it doesn't keep polling whatever set the worktree list left in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #229: fix(tracker): wire kanban merged-state lookup to GitHubContextOverviewThis PR correctly diagnoses and fixes a silent wiring bug: Strengths
Issues1. Missing integration test — acceptance criterion #5 not met The requirements list criterion #5 as:
The new tests in
2. Comment style
// PRs are keyed by worktree path in GitHubContext, never on WorktreeInfo.
// Exported + unit-tested because the obvious-looking `wt.pr.is_merged` spelling
// silently returns undefined and has bitten two prior fixes.The why is genuinely non-obvious and worth documenting, but this is three lines. Could be trimmed to one, e.g.: // Reads from GitHubContext (keyed by path), not wt.pr — which is never assigned in prod.The detailed history lives in the tracker notes and commit messages where it belongs. 3. Manual verification step still unchecked The test plan notes manual verification against Minor Observations
SummaryThe core fix and dead-code removal are solid. The main ask before merging: add at least one E2E or rendered test that goes through the full data path (memoryStore → GitHubContext → kanban render → |
- Add terminal test that drives the full memoryStore → kanban render path, asserting the merged glyph and "Merged" label appear on a card whose worktree has a MERGED PR seeded in memoryStore.prStatus. This is the integration coverage requirements criterion #5 called for and the gap that hid the bug twice. - Trim the multi-line comment on isItemPRMerged to a single line per AGENTS.md style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review: fix(tracker): wire kanban merged-state lookup to GitHubContextOverall: Approve with minor notes. This is a well-reasoned, well-scoped fix that correctly diagnoses a structural mismatch and resolves it without over-engineering. What the PR DoesThe kanban was computing Code QualityStrengths:
Minor notes:
Test Coverage
This is exactly the coverage that was missing before. Good addition. PerformanceNo regressions. The SecurityNo concerns. SummaryThe diagnosis is correct, the fix is minimal and precisely targeted, the dead field is gone, and the data path is now pinned end-to-end. The one thing worth double-checking before merge is that |
* tracker: seed item files for pr-changes-chips * add code-state chips (diff/changes/PR) to tracker board cards Surface git/PR signals on each kanban card so users can see real code state without bouncing to mainview. Three chips render on a second row below the existing agent/shell/run row, in fixed order: - diff (blue): +adds/-dels against base, excluding tracker/** so agent-driven status.json/notes/requirements churn doesn't dominate - changes (cyan): commits ahead/behind base - PR (green/red/yellow/gray): #NNN + check badge, color by check state Each chip is independent and renders only when meaningful; the whole row is omitted when none would render so clean worktrees stay quiet. Backing data: GitStatus gains base_added_lines_excl_tracker / base_deleted_lines_excl_tracker, populated by one extra `git diff --shortstat <baseRev> HEAD -- ':!tracker'` call piggybacking on the existing slow-cache refresh. Mainview unchanged. Per-card row budget holds because secondary maxLines now drops by chipRowCount instead of a binary "running chips? -1". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * suppress chip backgrounds on inactive tracker cards Bright colored pills on dimmed (inactive) cards read as too loud — they fight the rest of the card's visual de-emphasis. Switch both the running-status and code-state chip rows to plain-text mode (chip color as fg, no background) when item.inactive is true. The same semantic colors (cyan/green/magenta/blue/etc.) still convey the signal, just as text instead of as filled pills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: source PR data for the PR chip from GitHubContext The original implementation read PR state via worktree.pr, but as PR #229 (merged in the upstream rebase) called out, that field is never assigned in production — PR data lives on GitHubContext.pullRequests, keyed by worktree path. So the PR chip never rendered. computeCodeStateChips now takes the PR as a separate argument; the TrackerBoardScreen call site looks it up from the GitHub context (which is already wired for isItemPRMerged on the same row). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * show PR chip on merged items too (gray, #NNN⟫) The original suppression for merged PRs leaned on the gray "Merged" secondary text to convey the state. But seeing the PR number alongside is useful when scanning the board for which merged item belongs to which upstream PR. Drop the !is_merged guard; render merged PRs as a gray chip with formatPRStatus's '⟫' badge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * quiet diff/changes chips: plain text + gray when nothing pending Three filled pills next to the agent/shell/run row read as a badge dump. Switch the diff and changes chips to plain colored text (no background pill) so only the PR chip keeps a filled pill. CodeStateChip carries a new plain:boolean so the renderer doesn't have to know which chip is which. Also: diff and changes only get their semantic color (blue/cyan) when there's actionable pending work (!git.is_pushed — covers both uncommitted changes and unpushed commits). When everything is committed and pushed the chips still render but fade to gray, so the eye isn't drawn to a clean state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * gray out all chips on merged tracker cards A merged card is "done, archived" — sessions that happen to still be attached or stray git state on it should not compete with active work for attention. When prMerged, both chip rows (agent/shell/run and diff/changes/PR) drop their colors and render in gray plain-text. The check is in the renderer; the chip data still carries its semantic color, so the precedence (merged > inactive > active) stays local to one render block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * keep gray bg pill on merged-card chips Plain gray text looks like a layout glitch. Merged cards still want the filled pill shape so the row reads as a cohesive chip strip — just colorless. Switch the merged branch from {color:undefined, fg:'gray'} to {color:'gray', fg:'white'} on both rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * preserve chip shape on merged cards (plain stays plain, filled stays filled) Diff and changes chips never have a colored background, so a sudden gray-bg pill on a merged card looked out of place next to the same chips on active cards. Branch the merged-render logic on chip.plain: plain chips render as plain gray text; filled chips (agent/shell/run, PR) render as gray-bg pills with white fg. The chip's silhouette is unchanged across the active → merged transition, only its color drops out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * restrict code-state chips to selected card; render above running chips Diff/changes/PR are detail data — useful for the card you're focused on, not for every card on the board. Showing them everywhere crowded the columns and pushed the running-chip row down where it competed with slugs for attention. Skip the row entirely on non-selected cards. Also flip the chip-row order: code-state above running. The selected card now reads slug → secondary → code-state row → running row, so the more-specific signals sit closer to the slug and the running chips read as a footer rather than fighting code-state for visual weight. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * remove code-state chips entirely After iterating on visibility (selected-only, ordering, color rules), the diff/changes/PR chip row didn't earn its place next to the existing agent/shell/run row on the tracker board. Drop it and revert the supporting plumbing: - delete src/screens/codeStateChips.ts and its tests - drop GitStatus.base_added_lines_excl_tracker / base_deleted_lines_excl_tracker - drop the second `git diff --shortstat … -- ':!tracker'` in GitService and the corresponding GitSlowMetrics fields - restore the WorktreeCore equality check - restore the statusChipMapping test fixture What's kept (useful side-effects of this work): the inactive and prMerged styling tweaks on the pre-existing running-chip row, which gray out / plain-text-out the agent/shell/run pills appropriately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * restore PR chip on tracker board cards Followup to dropping the code-state chip row: the PR chip stays — it's the most actionable signal (number + check state at a glance, plus merged status). The diff and changes chips are still gone. The PR chip now renders inline on the same row as the agent/shell/run chips, appended after them with a single-space separator. Same render branch handles inactive (plain text) and merged (gray-bg pill) so all chips on the row stay consistent in styling. Per-card row budget is unchanged from the running-chips-only baseline because there's still just one chip row. PR data is sourced from GitHubContext.pullRequests (per PR #229); new computePRChip helper returns null when the fetch is unresolved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pr-cache): invalidate cached entry once branch acquires a remote PRStatusCacheService.isValid() guarded the remote-hash check with `if (entry.remoteCommitHash && ...)`, which short-circuited whenever the cached value was empty. That happened when the entry was first cached before the branch had a remote (typical: 'no_pr' cached on a freshly-created worktree). Once the user pushed and opened a PR, the cache stayed pinned to no_pr for the full 7-day TTL because nothing re-checked the remote. Repro on this repo: hide-binary-diff-content's PR #230 was already merged, but the kanban PR chip stayed hidden because the cache still said 'no_pr' from before the branch was first pushed. Fix: if entry.remoteCommitHash is empty, call getRemoteCommitHash now — if a remote exists today, invalidate so the next setVisibleWorktrees refresh re-fetches. Regression test uses jest.spyOn to flip the remote hash between set() and isValid() calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * prefix tracker PR chip label with 'PR' instead of '#' The chip stands alone at the end of the running-chips row; 'PR42✓' reads as a self-contained token, where '#42✓' looked like a stray hashtag without any column header to anchor it. Mainview's PR column keeps its own '#NNN' format since it's framed by a column header ('PR') already. The chip's label format is now local to prChip.ts (computePRChip inlines the badge logic instead of borrowing formatPRStatus from mainview's utils). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * add space after 'PR' in tracker chip label Reads as 'PR 42✓' instead of 'PR42✓' — easier to scan as a number when the chip is rendered as a filled pill. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * simplify pass: dedupe chip styling, share PR badge, fix cache TTL the cheap way Code review caught the previous PRStatusCacheService.isValid() change as a hot-path regression: the per-isValid getRemoteCommitHash() shells out to git twice (sync) and runs over every visible-worktree path on every setVisibleWorktrees call. Revert that and instead lower PR_TTL_NO_PR_MS from 7 days → 15 minutes, putting it in the same band as the rest of the TTL table. Same outcome (stale no_pr clears within 15m of the user pushing+opening a PR), no per-check shell-out. Also from review: - Extract prBadge(pr) into MainView/utils.ts; computePRChip and formatPRStatus both call it. State-glyph mapping can't drift. - Drop redundant pr.isLoading check in computePRChip — pr.exists already excludes the loading status. - Extract renderCardChip(chip, prMerged, inactive) helper in TrackerBoardScreen so the two map blocks (running + PR) collapse into one map over a concatenated cardChips array. - Trim narrative / historical comments throughout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
wt.pr.is_merged, butWorktreeInfo.prwas declared and never assigned in production. PR data actually lives onGitHubContext.pullRequests, keyed by worktree path. Switching the lookup unblocks both fixes.isItemPRMerged(worktree, pullRequests)helper with unit tests, including one that proves the helper does not consult a straywt.pr— pinning the lookup path so this can't silently regress a third time.pr?: PRStatusfield onWorktreeInfoplus the two getters that depended on it (needs_attention,action_priority) — both never called anywhere. Test fixtures stop mirroringworktree.prand seedmemoryStore.prStatus.set(path, pr)directly where assertions need PR data (matching the production data path).setVisibleWorktreeseffect on the kanban so its PR auto-refresh hits this project's currently-visible worktrees, not whatever set the worktree list left in place.Test plan
npm run typecheckpassesnpm test— 750/750 passnpm run test:terminal— 281/281 pass (1 pre-existing skip)devteammain; merged-PR items in the cleanup column (terminal-ui-state-detection,requirements-in-worktree) render◆ Mergedin gray instead of their pre-merge state.🤖 Generated with Claude Code