Skip to content

fix(swift-ios): show cached source control status - #7330

Merged
t3dotgg merged 6 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:contrib/issue107-cached-vcs-upstream
Sep 6, 2026
Merged

fix(swift-ios): show cached source control status#7330
t3dotgg merged 6 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:contrib/issue107-cached-vcs-upstream

Conversation

@saphid

@saphid saphid commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Source Control shows cached branch and changed-file data as soon as it opens, then fills in remote and pull-request data. Manual reload still asks the server for fresh status. Remote data stays marked as unknown until it arrives.

This version is rebased onto the current SwiftUI branch. It preserves failed Git output and the exact Retry operation. If a Git action succeeds but its status refresh fails, Retry repeats only the refresh.

Validation: 53 focused native tests passed in FeatureToolRecoveryTests and FeatureToolStateTests at 4992d1dac5ac24d0a0aff5deb81dd6b876443f4c, based on 614a7b672a86ee638f284298fe26e8b0194e0eaa. A second source review found no defects.

Modernized and fixed with GPT-6 in Codex.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 92811ea0-563d-4f6a-accb-50194112ba01

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 17, 2026
Comment thread apps/swift-ios/Features/SourceControl/FeatureSourceControlView.swift Outdated
Comment thread apps/swift-ios/Features/SourceControl/FeatureSourceControlView.swift Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This changes the existing iOS Source Control path to use cached-first asynchronous VCS streaming, timeout/recovery handling, remote-state action gating, and load/action race coordination. The behavior spans several production layers and introduces substantial concurrency and state-management complexity beyond a narrow fix.

You can add or adjust custom eligibility rules. Learn more.

@saphid

saphid commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both — the Bugbot and Macroscope findings were the same real defect, and they were right. Fixed in fe78298b2 + edae435c6.

"Stream overwrites post-action status" / Macroscope's medium — correct, and the diagnosis of why was the useful part: the accumulator retains the local half from before the action, so a late remoteUpdated folds stale local state into a status that lands after the action's own result.

The suggested fix (bump loadGeneration in perform()) doesn't work as-is: load(force:)'s defer is guarded by that same token, so bumping it from perform() orphans the guard and leaves isLoading stuck true forever — reintroducing the stuck-indicator bug this series exists to fix.

So the token is split into the two roles it was overloading:

  • loadGeneration — owns the loading indicator, bumped only by a load, so the indicator is always cleared by the load that set it.
  • statusGeneration — invalidates status/error writes, bumped by a load and by an action.

"Stream error masks action failure" — same fix: the streamed load's catch is guarded by statusGeneration, so a late stream error (including the 30s bounded-wait expiry) can no longer overwrite the action's message.

edae435c6 then closes the mirror image, which neither bot flagged but follows from the same asymmetry: pull-to-refresh isn't gated on a running action, so a refresh started after an action could still land last and wipe its failure message. The action now guards its own writes with the same token, so whichever side started later wins rather than whichever finishes later. That also made the first-status-only error latch provably dead, so it's gone.

Focused suite green on the new head: 26 tests, 26 passed, 0 failed.

Comment thread apps/swift-ios/Features/SourceControl/FeatureSourceControlView.swift Outdated
@saphid

saphid commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

6f75428e4 — good catch on "Failed action drops pending remote", that one was real too.

An action deliberately supersedes an open stream (that's what stops a stale local half from reverting the action's result), but a failed action writes no status of its own, so on a cold-cache entry the screen kept the pending status the stream had got to — reporting the remote as unavailable and withholding the PR actions until a manual reload.

A failed action now re-runs the cached-status stream, without clearing the message explaining why it failed.

Focused suite green: 26 tests, 26 passed, 0 failed.

@t3dotgg
t3dotgg force-pushed the t3code/rebuild-mobile-app-swift branch from 1f39d72 to fce7740 Compare August 18, 2026 21:35
@saphid
saphid force-pushed the contrib/issue107-cached-vcs-upstream branch from 6f75428 to 2bb15db Compare August 18, 2026 22:23
Comment thread apps/swift-ios/Features/SourceControl/FeatureSourceControlView.swift Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9236039. Configure here.

Comment thread apps/swift-ios/Features/SourceControl/FeatureSourceControlView.swift Outdated
saphid and others added 6 commits September 6, 2026 15:08
Follow-up to the cached-status change, from an independent review of it.

Showing the cached status first is right, but the streaming path had
replaced the forced refresh outright and made the screen depend on an
event the server does not guarantee to send.

- Explicit refresh affordances (toolbar reload, pull-to-refresh) go back
  through `vcs.refreshStatus`. `streamStatus` is cache-first over a cache
  with no TTL, so a reload would otherwise only replay what the server
  already had, and working-tree changes made outside the app would never
  appear on the one screen meant to show them. Entering the screen still
  streams the cached status first.
- Bound the stream. `updateCachedRemoteStatus` publishes only when the
  remote fingerprint changes, and a failed remote refresh backs off
  silently, so the remote half may never arrive; subscriptions carry no
  deadline. Without a bound the screen could sit loading forever.
- Stop gating actions on `isLoading`. Combined with the above, a stalled
  stream left a fully populated screen where every action and the reload
  button were disabled, pull-to-refresh was a silent no-op, and nothing
  explained why.
- Surface mid-stream failures. Once a status renders, the unavailable
  state is unreachable, so an error after the first status was stored and
  never shown.
- Distinguish a pending remote from zero. Ahead/behind and the pull
  request now read as "not yet known" instead of "0 ahead, 0 behind, no
  PR", which had offered Create Pull Request for a branch that may
  already have one.
- Carry the last known remote across local-only updates and retain a
  remote that arrives before the first local, mirroring
  `applyGitStatusStreamEvent`; latch completion so a later local-only
  event cannot reopen a finished stream.
- Adopt the surrounding streaming conventions in NativeFeatureClient:
  weak self plus the environment-generation guard used by sibling
  subscriptions.
Second follow-up, from an independent review of the previous commit.

- A superseded stream is no longer reported as a protocol violation.
  Breaking out of the event loop on cancellation or an environment switch
  fell through to the end-of-stream validation, so a normal client
  replacement surfaced "the stream ended before completion" to the user.
  Sibling subscriptions in this file all finish plainly in that case.
- Track whether the remote half has resolved, separately from its value.
  `remoteUpdated` carries an optional payload and the server does publish
  null for a workspace without a repository, which previously read as
  "still pending": the status stayed remote-unknown on an already-closed
  stream, and a leading null made the client wait out the whole bound for
  a half the server had said was absent.
- A snapshot now replaces the remote half rather than merging into it,
  matching `applyGitStatusStreamEvent`. On resubscribe after a reconnect
  the server prepends a snapshot carrying whatever its cache holds, so
  merging kept a stale pull request and stale ahead/behind counts and
  reported them as known.
- Say when the bounded wait gave up. Expiry finished the stream silently,
  leaving "Checking remote…" on screen forever with the pull-request
  actions withheld and nothing to act on. It now reports that the remote
  status is unavailable and points at pull-to-refresh. The bound goes to
  30s: the first refresh on a cold cache is a fetch plus a pull-request
  lookup, and 10s expired routinely on a slow network.
- Only the first streamed status clears the error message. The screen is
  interactive for the rest of the stream, so clearing on every element
  wiped the failure message of an action run meanwhile.
- Give the toolbar reload a spinner. It is no longer disabled while
  loading and the full-screen indicator only covers the empty state, so
  on a populated screen it looked like nothing happened.
- Share the file and pull-request mapping between the two status mappers
  instead of duplicating them.
The mixed-sequence and snapshot-replacement tests asserted the latch
without ever driving the one input that can break it, so both still
passed with the latch removed. Drive a cache-empty snapshot after the
remote half has resolved, which is the resubscribe-after-reconnect case,
and assert completion holds across it.

Also reword the expiry message, which recommended pull-to-refresh in a
state that has no pull-to-refresh, and soften the accumulator's doc
comment: it is modelled on `applyGitStatusStreamEvent` rather than
mirroring it, since the pending state has no counterpart there.
@t3dotgg
t3dotgg force-pushed the contrib/issue107-cached-vcs-upstream branch from ede5862 to 4992d1d Compare September 6, 2026 22:11
@t3dotgg
t3dotgg merged commit 90ecbf6 into pingdotgg:t3code/rebuild-mobile-app-swift Sep 6, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants