Skip to content

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

Open
saphid wants to merge 10 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:contrib/issue107-cached-vcs-upstream
Open

fix(swift-ios): show cached source control status#7330
saphid wants to merge 10 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

Observed problem and reproduction

Open SwiftUI Source Control on a cold cache or slow network. Before this change, the screen showed a spinner until a full VCS refresh finished, including remote fetch-class work and pull-request lookup, even when cached local branch and changed-file status already existed.

Cause

The native Source Control screen used the forcing vcs.refreshStatus path for initial presentation and represented the remote half as already known. It had no screen-specific accumulator for the server's local-first vcs.subscribeStatus stream or bounded pending-remote presentation.

Change and ownership boundary

Initial presentation now streams cached VCS status, renders local branch and changed files immediately, and fills ahead/behind and pull-request state when remote data resolves. Explicit toolbar and pull-to-refresh actions still force vcs.refreshStatus.

Pending remote state is distinct from zero, so create-PR actions remain withheld until remote state is known. After the bounded presentation wait reports remote unavailability, the screen follows the target branch's shared monitor and accepts a later remote result. Action failures use their own alert, background load failures stay inline, actions supersede stale loading ownership, and pull-to-refresh cannot race a running action.

Conflict resolution merged target commit 22b22f1463b83963d851bd0574a36a20f224a28d. It preserves the target's shared sourceControlStatusEvents monitor and its thread/workspace consumers, then re-expresses this PR's Source Control screen behavior on top. Both monitor paths now use one event accumulator, including branch-change invalidation and remote-before-local ordering. Only the saphid-owned head branch was modified.

Non-goals

This PR does not change the server VCS cache, polling/backoff, wire events, git commands, pull-request semantics, or other clients. It does not keep the bounded presentation stream open indefinitely. Web, desktop, and React Native mobile remain unchanged.

Affected areas

  • Client: SwiftUI mobile Source Control view and native feature client.
  • Platform: iOS.
  • Contracts: existing VCS stream and refresh calls are reused.
  • Providers: none.
  • States: cached local before remote, remote before local, null remote, same-branch and changed-branch updates, overwrite refresh, timeout/late recovery, action success/failure, stale-stream rejection, and failed-action recovery.

Validation

Current head: 9236039409eb3dadfe6004d3b38bc11d0e20ff11.

  • Focused T3CodeTests/FeatureToolStateTests on iPhone 17 Pro with isolated DerivedData: 28 passed, 0 failed, 0 skipped; real xcodebuild exit 0.
  • The previously failing remote-before-local regression passes on this exact head.
  • All six changed Swift files pass swiftc -frontend -parse.
  • git diff --check passes.
  • The committed net diff SHA-256 against target is 5db6b01a8bb0b5fa174ebf3ddf60e61fea8aa29cc002f1869697da50173a8696, matching the reviewed and tested frozen diff.
  • A direct claude-opus-5 high review found actionable race and presentation issues; those were adjudicated and fixed. The required fresh follow-up attempt produced no output by the five-minute threshold and was terminated, so no fresh-review verdict is claimed.

Risks, untested paths, and known gaps

  • Remote, relay, and tunnel runtime paths were not exercised.
  • Slow/failing remote behavior and alert presentation were not visually exercised on a simulator; focused reducer and action-state tests cover the underlying status transitions.
  • Known gaps: current-head full-size light/dark before-and-after captures and a video showing cached local status followed by delayed remote completion or failure must be refreshed in proof lane Virtualize branch picker list and forward ComboboxList ref #150 before human review is re-requested.

Evidence

No current-head screenshots or video are embedded. The prior head's proof is historical only and is not claimed for this conflict-resolved head. The required visual proof refresh is tracked in saphid/t3code-personal#150.

Owning issue and stack

Owning issue: saphid/t3code-personal#107. This PR targets the open SwiftUI parent branch and depends on #5178 landing. It is not stacked on #7345. Maintainer edits are enabled. Human review is not re-requested until current-head proof is complete.

Implementation and conflict resolution: GPT-5.6 Sol high in the Codex harness. Independent review attempt: Claude Opus 5 high through the direct Claude CLI.


Note

Medium Risk
iOS-only but touches async VCS streaming, timeouts, and race-prone UI updates that gate git/PR actions; server contracts are unchanged.

Overview
iOS Source Control no longer blocks on a full refreshStatus for the first paint. Initial load uses a new sourceControlStatuses stream over vcs.subscribeStatus, so branch and changed files appear from cache while ahead/behind and PR data fill in later. Toolbar reload and pull-to-refresh still force sourceControlStatus so refresh bypasses the cache.

FeatureSourceControlStatus gains isRemoteKnown: pending remote is not treated as zero, and create-PR actions stay hidden until remote state is known. NativeSourceControlStatusAccumulator centralizes folding VCS events for both the bounded presentation stream and the existing shared monitor (branch changes, snapshots, remote-before-local).

The presentation stream is capped at 30 seconds; if remote never lands, the client surfaces remoteStatusUnavailable and the UI can keep listening via sourceControlStatusEvents for a late remote result. FeatureSourceControlView splits load vs reload, uses generation guards against stale streams/actions, separates action alerts from inline load errors, and recovers status after failed actions.

Focused tests cover the accumulator, stream completion rules, and action progress ordering.

Reviewed by Cursor Bugbot for commit ede5862. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Show cached source control status with bounded streaming API in Swift iOS app

  • Adds FeatureClient.sourceControlStatuses(threadID:) returning an AsyncThrowingStream that yields cached-local-first status, then remote status as it resolves
  • Introduces NativeSourceControlStatusAccumulator to fold VCS status events into FeatureSourceControlStatus values, tracking local/remote halves and resetting remote on branch changes
  • Adds 30s timeout (sourceControlStatusStreamTimeoutSeconds) that finishes the stream with NativeFeatureClientError.remoteStatusUnavailable if remote data never arrives
  • Updates FeatureSourceControlView to stream status, show inline pending/unavailable remote states, use a dedicated action error alert, and trigger uncached refresh on pull-to-refresh
  • Withholds pull-request creation actions when FeatureSourceControlStatus.isRemoteKnown is false
  • Risk: callers relying on the single-shot sourceControlStatus default implementation in FeatureClient get a one-element stream; conformers not overriding sourceControlStatuses(threadID:) will not benefit from cached-local-first delivery

Macroscope summarized ede5862.

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

@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 added 7 commits August 19, 2026 08:03
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.
An action runs while a cached-status stream may still be open: the screen
stays interactive throughout, and the stream's accumulator still holds
the local half from before the action. A late event folds that stale half
into a status that overwrites the action's result, so a successful commit
could show the old dirty working tree until a manual reload; a late stream
error, including the bounded-wait expiry, could likewise mask the action's
own failure message.

Split the two roles the load token was serving. `loadGeneration` still
owns the loading indicator and is bumped only by a load, so the indicator
is always cleared by the load that set it. `statusGeneration` invalidates
status and error writes and is bumped by an action as well, so an action
supersedes an open stream without stranding the indicator.
Pull-to-refresh is not gated on a running action, so a refresh started
after an action could still land last and wipe that action's failure
message — the mirror image of the race the previous commit closed. Guard
the action's own writes with the same token so whichever side started
later wins, rather than whichever happens to finish later.

Drop the first-status-only error latch from the streamed load: any writer
that could have set an error since the stream started has already bumped
the token and invalidated it, so the latch defends against nothing now.
An action supersedes any stream that is still open, which is what keeps a
stale local half from reverting its result. But when the action itself
fails it writes no status, so on a cold-cache entry the screen was left
holding the pending status the stream had produced so far — reporting the
remote as unavailable and withholding the pull-request actions until the
user reloaded by hand.

Re-run the cached-status stream after a failed action, without clearing
the message that says why it failed.
@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.

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.

1 participant