Files
orca/mobile/src/worktree/host-worktree-refresh.ts
T
NeilandOrca 96e31e7bf8 Remove a paired computer's deleted projects from every connected device (#12215)
* fix(repos): remove a paired computer's deleted projects from every connected device

A project deleted on a paired Orca host stayed in every connected client's
sidebar and could not be removed there.

Two independent defects:

1. Host-local repo IPC mutations only sent `repos:changed` to the host's own
   renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was
   fed only by mutations arriving over runtime RPC, and clients refetch a remote
   catalog only on a `reposChanged` event -- there is no polling on desktop -- so
   the deleted rows persisted indefinitely. The shared `notifyReposChanged`
   helper now also calls the new
   `OrcaRuntimeService.notifyReposChangedForRemoteClients()`
   (src/main/runtime/orca-runtime.ts:5175), mirroring the existing
   `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo,
   project-group and folder-workspace IPC mutation, so renames, colors, reorders
   and adds propagate too.

2. Deleting the ghost row on the client routed `repo.rm` to the owner, which
   answered `repo_not_found`. `removeProject` wrapped its whole body in one
   try/catch, so the rejection aborted the local purge before the `set()`
   (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared
   to do nothing. Only `repo_not_found` is now tolerated; any other failure still
   keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the
   three single-project user-initiated entry points. Bulk and background callers
   keep today's silence plus their own aggregate reporting.

Closes #11994

Co-authored-by: Orca <help@stably.ai>

* fix(repos): revert inert RepositoryPane removeProject arg

The settings pane's only render site drops the argument; the toast is
already delivered by removeSettingsProjectFromAllHosts.

Co-authored-by: Orca <help@stably.ai>

* fix(repos): scope duplicate-repo-id deletes to the owning execution host

Cover the cross-host collisions #11994's broadcast now fans out to every paired
device. Same-name projects on different hosts were already isolated (per-host
UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests.

Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and
`deleteProjectHostSetup` both resolved one row and then deleted by bare id,
taking the sibling host's registration with it.

Co-authored-by: Orca <help@stably.ai>

* test(mobile): align the poll-interval rationale with the new reposChanged emission

Co-authored-by: Orca <help@stably.ai>

* fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host

The sibling-host fallback could only ever pick a row on a host the caller
did not name; with no exact match the setup is stale and the existing path
already drops just the setup.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 01:26:48 -07:00

91 lines
3.0 KiB
TypeScript

import { AppState } from 'react-native'
import type { RuntimeClientEventStreamMessage } from '../../../src/shared/runtime-client-events'
import type { RpcClient } from '../transport/rpc-client'
const WORKTREE_REFRESH_MS = 3000
type WorktreeRefreshOptions = { allowDuringModal?: boolean }
type RepoRefreshOptions = { force?: boolean; queueIfInFlight?: boolean }
type HostWorktreeRefreshArgs = {
client: RpcClient
fetchWorktrees: (options?: WorktreeRefreshOptions) => Promise<void>
fetchRepoMetadata: (options?: RepoRefreshOptions) => Promise<void>
}
export function startHostWorktreeRefresh({
client,
fetchWorktrees,
fetchRepoMetadata
}: HostWorktreeRefreshArgs): () => void {
let stale = false
let eventStreamReady = false
const refreshOnForeground = (): void => {
if (AppState.currentState !== 'active') {
return
}
void fetchWorktrees({ allowDuringModal: true })
void fetchRepoMetadata({ queueIfInFlight: true })
}
const appStateSubscription = AppState.addEventListener('change', (state) => {
if (state === 'active') {
refreshOnForeground()
}
})
const interval = setInterval(() => {
if (AppState.currentState !== 'active') {
return
}
void fetchWorktrees()
// Why: desktop Settings repo edits (icon/color/name, repo removal) now emit `reposChanged`
// (#11994), but a host on an older build does not; keep this periodic repo.list as the
// convergence safety-net. fetchRepoMetadata self-throttles to REPO_METADATA_REFRESH_MS
// (60s), so this is ~1 request/min while foregrounded — the AppState gate is what removes
// the waste (both stop while backgrounded).
void fetchRepoMetadata()
}, WORKTREE_REFRESH_MS)
const unsubscribe = client.subscribe(
'runtime.clientEvents.subscribe',
null,
(payload: unknown) => {
if (stale || !payload || typeof payload !== 'object') {
return
}
const event = payload as RuntimeClientEventStreamMessage | { type: 'error' }
if (event.type === 'ready') {
const replayedAfterReconnect = eventStreamReady
eventStreamReady = true
if (replayedAfterReconnect) {
// Why: client events are not queued while disconnected, so re-read both snapshots after replay.
void fetchWorktrees()
void fetchRepoMetadata({ force: true, queueIfInFlight: true })
}
return
}
if (event.type === 'end' || event.type === 'error') {
eventStreamReady = false
return
}
if (event.type === 'reposChanged') {
// Why: folder workspace mutations publish reposChanged, and worktree.ps owns this catalog.
void fetchWorktrees()
void fetchRepoMetadata({ force: true, queueIfInFlight: true })
} else if (event.type === 'worktreesChanged') {
void fetchWorktrees()
}
}
)
void fetchWorktrees()
void fetchRepoMetadata({ force: true, queueIfInFlight: true })
return () => {
stale = true
clearInterval(interval)
appStateSubscription.remove()
unsubscribe()
}
}