Skip to content

Fix PR state flicker: make confirmed-no-PR clearing effective and order-independent - #538

Merged
onevcat merged 6 commits into
mainfrom
fix/pr-state-refresh-explicit-clear
Jul 5, 2026
Merged

Fix PR state flicker: make confirmed-no-PR clearing effective and order-independent#538
onevcat merged 6 commits into
mainfrom
fix/pr-state-refresh-explicit-clear

Conversation

@onevcat

@onevcat onevcat commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Supersedes #533. Includes the original tri-state work by @Alex-ai-future (commits kept as-is) plus two fixes and the missing semantic test coverage.

Background

#533 introduced Set-based tri-state semantics (confirmedNoPrBranches) to distinguish "queried, confirmed no PR" from "status unknown (fetch failed)", so partial refresh failures no longer clear PR badges and cause flicker. The design is sound, but the implementation had two gaps.

Fixes

1. Confirmed-no-PR clearing was a no-op (dead code).

pullRequestsByWorktreeID stored the explicit clear via a nil literal:

prsByWorktreeID[worktreeID] = nil  // [Worktree.ID: GithubPullRequest?]

On a dictionary with optional values, assigning a nil literal through the subscript removes the key instead of storing .some(nil). Since the downstream repositoryPullRequestsLoaded handler only iterates keys present in the dictionary, the clear never happened: stale badges (PR deleted, or worktree switched to a branch without a PR) persisted forever, and the entire tri-state mechanism had zero runtime effect. Fixed with updateValue(nil, forKey:).

2. Cross-host suppression of confirmed clears was arrival-order dependent.

With multiple GitHub hosts, a .failed batch arriving before the final .refreshed outcome left the accumulated confirmed set intact, so a healthy host could clear a PR that lives on the failed host — the exact flicker this PR set out to fix, surviving as a coin flip. Only the failed-batch-arrives-last ordering discarded the set. Now failed batches are tracked per repository (prRefreshFailedBatchRepositoryIDs) and confirmed clears are suppressed whenever any batch failed, regardless of order.

Test coverage

All #533 test call sites passed confirmedNoPrBranches: [], so the non-empty path — the feature's whole point — was untested, which is why the no-op was invisible. Added:

  • Reducer: confirmed-no-PR clears a stale badge (fails against the nil-literal version — verified red before the fix); unknown status preserves the badge; both failed/refreshed arrival orderings preserve the badge; a later host batch's real PR overrides an earlier confirmation.
  • Coordinator: confirmedNoPrBranches is populated only when all candidate repos succeed; a partial candidate failure (fallback failing too) leaves branches unconfirmed.

Verification

  • make check clean
  • BatchedPullRequestRefreshReducerTests + PullRequestRefreshCoordinatorTests + RepositoriesFeatureTests: 256 tests passing
  • Red-check: the new clear test fails when the fix is reverted to the nil literal
  • make build-app succeeds

Alex-ai-future and others added 6 commits July 3, 2026 10:27
The sidebar PR badge flickers (disappears briefly then reappears) on every
refresh cycle because the PR data dictionary only contains branches with a
matching PR. Branches without a PR are absent from the dictionary, but the
downstream mapping treats 'key absent' the same as 'no PR', clearing stale
PR state for worktrees that weren't actually queried.

Change the entire PR refresh data pipeline from [String: GithubPullRequest]
to [String: GithubPullRequest?] so the three cases are distinguished:
- key present with PR → update
- key present with nil → confirmed no PR, clear old value
- key absent → not queried this cycle, preserve old value

Also add race protection so a nil result from one host does not overwrite
a PR already merged from another host.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
Update test assertions to handle [String: GithubPullRequest?] instead of
[String: GithubPullRequest]:
- Use (dict[key] ?? nil)?.property to unwrap double optional
- Use (dict[key] ?? nil) == nil to check for nil value
- Remove .repositoryPullRequestsLoaded receives when remoteInfos is empty
  (new behavior preserves existing PR state instead of clearing)

Signed-off-by: Alex <alex.tech.lab@outlook.com>
Revert the [String: GithubPullRequest?] approach (Swift dict[key]=nil deletes
the key, not stores .some(nil)) and implement tri-state using a separate
Set<String> for "confirmed no PR" branches.

Three-way distinction:
- prsByBranch contains branches with a PR -> update
- confirmedNoPrBranches contains branches all repos confirmed as no PR -> clear
- neither -> unknown (partial failure) -> preserve existing state

Key changes:
- Outcome.refreshed gains confirmedNoPrBranches: Set<String>
- emitOutcomes computes it only when ALL candidate repos succeeded
- pullRequestsByWorktreeID only clears worktrees in confirmedNoPrBranches
- mergePullRequestRefreshResults accumulates Set without overwriting existing PRs
- Keep no-remote path fix (remoteInfos.isEmpty no longer clears PRs)
- Rename refreshClearsStalePullRequestsWhenGithubRemotesDisappear to
  refreshPreservesPullRequestsWhenGithubRemotesUnavailable

Signed-off-by: Alex <alex.tech.lab@outlook.com>
Signed-off-by: Alex <alex.tech.lab@outlook.com>
Signed-off-by: Alex <alex.tech.lab@outlook.com>
- pullRequestsByWorktreeID assigned a nil literal through the
  optional-value dictionary subscript, which removes the key instead of
  storing an explicit nil. The confirmed-no-PR clear never reached the
  reducer, so stale PR badges were never cleared and the tri-state
  mechanism was a no-op. Use updateValue(nil, forKey:) instead.
- A failed host batch arriving before the final refreshed outcome left
  the accumulated confirmed-no-PR set intact, so a healthy host could
  clear a PR that lives on the failed host. Track failed batches per
  repository and suppress confirmed clears when any batch failed,
  regardless of outcome arrival order.
- Add reducer tests for explicit clear, unknown-status preserve, both
  failed/refreshed arrival orderings, and a later host batch overriding
  an earlier confirmation; add coordinator tests covering
  confirmedNoPrBranches computation (all-candidates-succeeded vs
  partial candidate failure).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes PR badge “flicker” and stale-state issues in the Repositories feature by making the tri-state “confirmed no PR” semantics actually take effect, and by ensuring cross-host failures suppress clears regardless of batch arrival order.

Changes:

  • Make “confirmed no PR” clears effective by emitting explicit nil values for worktrees (updateValue(nil, forKey:)) and threading confirmedNoPrBranches through the refresh pipeline.
  • Make confirmed clears order-independent across multiple GitHub hosts by tracking per-repository failed batches and suppressing clears whenever any host batch failed.
  • Add missing semantic test coverage for non-empty confirmedNoPrBranches and the failure/refreshed ordering cases.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
supacode/Features/Repositories/Reducer/RepositoriesFeature+GithubIntegration.swift Implements effective explicit clears and order-independent suppression when any host batch fails.
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift Adds per-repository tracking for confirmed-no-PR branches and failed batches.
supacode/Features/Repositories/BusinessLogic/PullRequestRefreshCoordinator.swift Computes confirmedNoPrBranches only when all candidate repos succeed, emitting tri-state outcomes.
supacodeTests/BatchedPullRequestRefreshReducerTests.swift Adds targeted reducer tests covering clears, unknown-state preservation, ordering, and override behavior.
supacodeTests/PullRequestRefreshCoordinatorTests.swift Adds coordinator tests ensuring confirmed-no-PR is only produced on full success and suppressed on partial failure.
supacodeTests/RepositoriesFeatureTests.swift Updates expectations for the “no GitHub remotes” path (no longer emits a clearing payload).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@onevcat
onevcat merged commit 4650367 into main Jul 5, 2026
2 checks passed
@onevcat
onevcat deleted the fix/pr-state-refresh-explicit-clear branch July 5, 2026 00:40
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.

3 participants