Skip to content

fix(tracker): wire kanban merged-state lookup to GitHubContext - #229

Merged
agent-era-ai merged 2 commits into
mainfrom
merged-status-not-showing
Apr 26, 2026
Merged

fix(tracker): wire kanban merged-state lookup to GitHubContext#229
agent-era-ai merged 2 commits into
mainfrom
merged-status-not-showing

Conversation

@agent-era-ai

Copy link
Copy Markdown
Collaborator

Summary

  • The kanban's merged-state branch (PR Show subdued Merged label on completed tracker cards #224) and the earlier "stays green after merge" guard (PR merged item stays green #221) both read wt.pr.is_merged, but WorktreeInfo.pr was declared and never assigned in production. PR data actually lives on GitHubContext.pullRequests, keyed by worktree path. Switching the lookup unblocks both fixes.
  • Extracts a small isItemPRMerged(worktree, pullRequests) helper with unit tests, including one that proves the helper does not consult a stray wt.pr — pinning the lookup path so this can't silently regress a third time.
  • Deletes the dead pr?: PRStatus field on WorktreeInfo plus the two getters that depended on it (needs_attention, action_priority) — both never called anywhere. Test fixtures stop mirroring worktree.pr and seed memoryStore.prStatus.set(path, pr) directly where assertions need PR data (matching the production data path).
  • Adds a setVisibleWorktrees effect 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 typecheck passes
  • npm test — 750/750 pass
  • npm run test:terminal — 281/281 pass (1 pre-existing skip)
  • Manual: open the tracker board on devteam main; merged-PR items in the cleanup column (terminal-ui-state-detection, requirements-in-worktree) render ◆ Merged in gray instead of their pre-merge state.

🤖 Generated with Claude Code

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>
@claude

claude Bot commented Apr 26, 2026

Copy link
Copy Markdown

Code Review — PR #229: fix(tracker): wire kanban merged-state lookup to GitHubContext

Overview

This PR correctly diagnoses and fixes a silent wiring bug: TrackerBoardScreen read wt?.pr?.is_merged, but WorktreeInfo.pr was declared and never assigned in production — PR data lives in GitHubContext.pullRequests keyed by worktree path. The fix is surgical and includes a dead-field removal and scoped PR-refresh wiring. Overall this is well-executed; a few things worth calling out.


Strengths

  • Root cause correctly identified and fixed. Switching to pullRequests[wt.path]?.is_merged via useGitHubContext() is the right lookup path, consistent with how WorktreeListScreen already does it.
  • isItemPRMerged extraction is a good call. The inline fix is one line, but the third repeat of the same silent failure justifies naming it and pinning it with tests — including the regression-prevention test that proves the helper ignores a stray wt.pr.
  • Dead-code removal is clean. Deleting the pr?, needs_attention, and action_priority fields eliminates the trap for future contributors. The test infrastructure updates are consistent throughout.
  • setVisibleWorktrees scoping is correct. Deriving kanbanWorktreePaths from sessionMap via useMemo (and sorting for stable key comparison) means the effect only re-fires when the set of visible worktrees actually changes.

Issues

1. Missing integration test — acceptance criterion #5 not met

The requirements list criterion #5 as:

A new test covers the data path: when a fake worktree's PR (in memoryStore.prStatus) has state MERGED, the rendered kanban frame shows the merged indicator. This is the gap that hid the bug.

The new tests in tests/unit/TrackerBoardScreen.test.ts only cover isItemPRMerged in isolation. There is no test that:

  1. Seeds memoryStore.prStatus.set(path, new PRStatus({state: 'MERGED'})),
  2. Renders the kanban screen, and
  3. Asserts ◆ Merged appears in the output frame.

getTrackerCardDisplayState tests pass prMerged: true directly — exactly the "gap that hid the bug" the notes describe. Without this integration test, the same class of wiring bug (e.g., forgetting to pass pullRequests into the render branch, or a future refactor breaking the hook call) could ship undetected again.

2. Comment style

AGENTS.md says "one short line max" for code comments. The block on isItemPRMerged:

// 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 devteam/main as unchecked. Not blocking if that's by design for this environment, but worth confirming before merge.


Minor Observations

  • The type change in utils.ts (WorktreeInfo['pr']PRStatus | undefined | null) is correct and cleaner — no concern.
  • Removing the memoryStore.worktrees.set(worktree.path, worktree) call in setupWorktreeWithPR is safe because setupTestWorktree already registers the worktree in the store. Correct, just worth a quick read to confirm for reviewers unfamiliar with the test infrastructure.

Summary

The 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 → ◆ Merged visible), which is what the requirements called out as the gap that allowed this class of bug to ship twice.

- 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>
@claude

claude Bot commented Apr 26, 2026

Copy link
Copy Markdown

Code Review: fix(tracker): wire kanban merged-state lookup to GitHubContext

Overall: 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 Does

The kanban was computing prMerged from wt?.pr?.is_merged, but WorktreeInfo.pr was never assigned in production — PR data lives in GitHubContext.pullRequests keyed by worktree path. This meant the merged label (PR #224) and the "stays green after merge" guard (PR #221) were both silently dead since they shipped. This PR wires the correct data source, removes the dead field that was a repeating trap, scopes kanban PR refreshes to the kanban's own visible worktrees, and pins the data path with unit + terminal E2E tests.


Code Quality

Strengths:

  • isItemPRMerged extraction is the right call. The lookup is one line, but the fact that two prior PRs fell into the same trap justifies extracting, naming, and independently testing it. The test that explicitly proves the helper ignores a stray wt.pr (even if someone reintroduces the field) is a good regression pin.
  • Dead code removal is clean and complete. Deleting pr?: PRStatus, needs_attention, and action_priority from WorktreeInfo eliminates the trap entirely — a future contributor can no longer mistake wt.pr for the canonical source. The two removed getters were provably uncalled.
  • Test fixture alignment. Stripping worktree.pr = pr from helpers and having them seed memoryStore.prStatus directly mirrors the real data path through FakeGitHubService → GitHubContext.pullRequests. This is the right fix for the fixtures.
  • The terminal E2E test covers the gap. A test through memoryStore.prStatus → FakeGitHubService → GitHubContext.pullRequests → kanban render → assert label is exactly the missing coverage that hid the bug both times.
  • kanbanWorktreePaths memo is correct. Array.from(sessionMap.values(), w => w.path).sort() properly derives the kanban-scoped worktree set, and the useEffect fires only when the set of paths changes.

Minor notes:

  • Comment on isItemPRMerged (line ~90). The comment // Reads from GitHubContext (keyed by path), not wt.pr — which is never assigned in prod. documents past failure more than present behavior. With wt.pr now deleted, "not wt.pr" is vacuously true — future readers won't know there was a wt.pr to avoid. Consider dropping this or condensing to just document why the function exists as a separate helper rather than an inline lookup (i.e., the regression-pinning rationale).

  • (wtWithStrayPr as any).pr = new PRStatus(...) in the test. The any cast is acceptable here since it's deliberately simulating a future regression, but it would silently fail to test anything if someone later adds a readonly pr property. The test comment explains the intent well; just worth being aware of the brittleness.

  • setVisibleWorktrees effect deps. [kanbanWorktreePaths, setVisibleWorktrees] — if setVisibleWorktrees is a new function reference on every render, this effect fires on every render regardless of the path set change. Assuming setVisibleWorktrees is stable (defined outside the provider's render path or memoized), this is fine. Worth verifying in GitHubContext.tsx if it hasn't already been confirmed.


Test Coverage

Test added What it covers
5 × isItemPRMerged unit tests Happy path, open PR, no entry, null worktree, stray-field regression
Terminal E2E tracker-board-merged-label.test.mjs Full path: memoryStore.prStatus → FakeGitHubService → GitHubContext → kanban render → ◆ Merged label
Updated archived/data-flow/unarchive fixtures Use correct store path, not worktree.pr

This is exactly the coverage that was missing before. Good addition.


Performance

No regressions. The kanbanWorktreePaths memo is cheap (one pass over sessionMap values), and setVisibleWorktrees scoping is strictly better than the prior state where the kanban didn't register any visible worktrees at all.


Security

No concerns.


Summary

The 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 setVisibleWorktrees from useGitHubContext() is a stable function reference so the effect doesn't fire on every render. Otherwise this is ready to merge.

@agent-era-ai
agent-era-ai merged commit 647aea9 into main Apr 26, 2026
2 checks passed
@agent-era-ai
agent-era-ai deleted the merged-status-not-showing branch April 26, 2026 17:45
agent-era-ai added a commit that referenced this pull request Apr 27, 2026
* 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>
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.

1 participant