Display SSH worktrees immediately using persisted metadata

Users can now see known worktrees for SSH hosts without waiting for the
provider connection to establish. Worktrees are fetched from local metadata
and displayed as non-authoritative, then merged without replacing richer
live data once the provider becomes available.
This commit is contained in:
Jinjing
2026-08-04 22:59:54 -07:00
parent 9507cbce0c
commit f6a4abffdc
8 changed files with 419 additions and 1 deletions
+78
View File
@@ -2589,6 +2589,84 @@ describe('registerWorktreeHandlers', () => {
})
})
it('lists every persisted SSH worktree without accessing the live provider', async () => {
const sshHostId = toSshExecutionHostId('target-a')
const sshRepo = {
id: 'repo-1',
path: '/remote/repo',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'target-a'
}
const metaById = {
'repo-1::/remote/repo': makeWorktreeMeta({
displayName: 'main',
hostId: sshHostId
}),
'repo-1::/remote/queued': makeWorktreeMeta({
displayName: 'queued',
hostId: sshHostId
}),
'repo-1::/remote/other-host': makeWorktreeMeta({
displayName: 'other host',
hostId: toSshExecutionHostId('target-b')
})
}
store.getRepos.mockReturnValue([sshRepo])
store.getProjectHostSetups.mockReturnValue([])
store.getAllWorktreeMeta.mockReturnValue(metaById)
store.getWorktreeMeta.mockImplementation((worktreeId: string) => metaById[worktreeId])
const result = await handlers['worktrees:listKnownForExecutionHost'](null, {
repoId: sshRepo.id,
executionHostId: sshHostId
})
expect(result).toMatchObject({
status: 'complete',
repoId: sshRepo.id,
executionHostId: sshHostId,
result: {
repoId: sshRepo.id,
authoritative: false,
source: 'metadata-fallback',
worktrees: [
expect.objectContaining({ path: '/remote/repo', isMainWorktree: true }),
expect.objectContaining({ path: '/remote/queued', isMainWorktree: false })
]
}
})
expect(getSshGitProviderMock).not.toHaveBeenCalled()
expect(listWorktreesMock).not.toHaveBeenCalled()
})
it('rejects ambiguous metadata-only SSH owners', async () => {
const sshHostId = toSshExecutionHostId('target-a')
const sshRepo = {
id: 'repo-1',
path: '/remote/repo-a',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'target-a'
}
store.getRepos.mockReturnValue([sshRepo, { ...sshRepo, path: '/remote/repo-b' }])
const result = await handlers['worktrees:listKnownForExecutionHost'](null, {
repoId: sshRepo.id,
executionHostId: sshHostId
})
expect(result).toEqual({
status: 'rejected',
repoId: sshRepo.id,
executionHostId: sshHostId
})
expect(store.getAllWorktreeMeta).not.toHaveBeenCalled()
expect(getSshGitProviderMock).not.toHaveBeenCalled()
})
it('fails closed for duplicate exact owners and ambiguous legacy repo IDs', async () => {
const sshRepo = {
id: 'shared-repo',
+41
View File
@@ -49,7 +49,9 @@ import {
import {
PROVIDER_REQUEST_ID_MAX_UTF8_BYTES,
type DirectSshDetectedWorktreeRequest,
type HostQualifiedKnownWorktreeResult,
type HostQualifiedDetectedWorktreeResult,
type ListKnownWorktreesForExecutionHostArgs,
type ListDetectedWorktreesArgs,
type ProviderRequestId
} from '../../shared/detected-worktree-provider-contract'
@@ -1826,6 +1828,7 @@ export function registerWorktreeHandlers(
ipcMain.removeHandler('worktrees:listAll')
ipcMain.removeHandler('worktrees:list')
ipcMain.removeHandler('worktrees:listDetected')
ipcMain.removeHandler('worktrees:listKnownForExecutionHost')
ipcMain.removeHandler('worktrees:cancelListDetected')
ipcMain.removeHandler('worktrees:create')
ipcMain.removeHandler('worktrees:prefetchCreateBase')
@@ -1970,6 +1973,44 @@ export function registerWorktreeHandlers(
}
})
ipcMain.handle(
'worktrees:listKnownForExecutionHost',
(_event, args: ListKnownWorktreesForExecutionHostArgs): HostQualifiedKnownWorktreeResult => {
const rejected = (): HostQualifiedKnownWorktreeResult => ({
status: 'rejected',
repoId: args.repoId,
executionHostId: args.executionHostId
})
const parsedHost = parseExecutionHostId(args.executionHostId)
if (parsedHost?.kind !== 'ssh') {
return rejected()
}
const candidates = store.getRepos().filter((candidate) => candidate.id === args.repoId)
if (
candidates.some((candidate) => resolveRepoOwnershipEvidence(candidate).status !== 'owned')
) {
return rejected()
}
const repo = findExactRepoOwner(store, args.repoId, args.executionHostId)
if (!repo || repo.connectionId !== parsedHost.targetId) {
return rejected()
}
const metaIndex = createSshWorktreeMetaIndex(Object.entries(store.getAllWorktreeMeta()))
const worktrees = listDisconnectedSshWorktrees(store, repo, metaIndex)
return {
status: 'complete',
repoId: repo.id,
executionHostId: args.executionHostId,
result: {
repoId: repo.id,
authoritative: false,
source: 'metadata-fallback',
worktrees: buildDisconnectedDetectedWorktrees(store, repo, worktrees)
}
}
}
)
ipcMain.handle(
'worktrees:listDetected',
async (
+5
View File
@@ -30,8 +30,10 @@ import type { ReadClipboardTextOptions } from '../shared/clipboard-text'
import type { AppIdentity } from '../shared/app-identity'
import type { ReleaseChannel } from '../shared/release-channel'
import type {
HostQualifiedKnownWorktreeResult,
HostQualifiedDetectedWorktreeResult,
LegacyDetectedWorktreeRequest,
ListKnownWorktreesForExecutionHostArgs,
ListDetectedWorktreesArgs,
ProviderRequestId
} from '../shared/detected-worktree-provider-contract'
@@ -1396,6 +1398,9 @@ export type PreloadApi = {
): Promise<HostQualifiedDetectedWorktreeResult | DetectedWorktreeListResult>
(args: LegacyDetectedWorktreeRequest): Promise<DetectedWorktreeListResult>
}
listKnownForExecutionHost?: (
args: ListKnownWorktreesForExecutionHostArgs
) => Promise<HostQualifiedKnownWorktreeResult>
cancelListDetected?: (args: { providerRequestId: ProviderRequestId }) => Promise<void>
listAll: () => Promise<Worktree[]>
create: (args: CreateWorktreeArgs) => Promise<CreateWorktreeResult>
+3
View File
@@ -764,6 +764,9 @@ const api = {
listDetected: (args) => ipcRenderer.invoke('worktrees:listDetected', args),
listKnownForExecutionHost: (args) =>
ipcRenderer.invoke('worktrees:listKnownForExecutionHost', args),
cancelListDetected: (args) => ipcRenderer.invoke('worktrees:cancelListDetected', args),
listAll: () => ipcRenderer.invoke('worktrees:listAll'),
@@ -68,6 +68,16 @@ describe('native preload SSH authority forwarding', () => {
>()
})
it('forwards host-qualified metadata-only worktree reads', async () => {
await import('./index')
const api = exposeInMainWorld.mock.calls.find(([name]) => name === 'api')?.[1] as PreloadApi
const args = { repoId: 'repo-1', executionHostId: 'ssh:ssh-1' as const }
await api.worktrees.listKnownForExecutionHost?.(args)
expect(invoke).toHaveBeenCalledWith('worktrees:listKnownForExecutionHost', args)
})
it('forwards full-pair get and push states without cloning away authority', async () => {
const state: SshConnectionState = {
targetId: 'ssh-1',
@@ -21,6 +21,8 @@ import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rp
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type {
HostQualifiedDetectedWorktreeResult,
HostQualifiedKnownWorktreeResult,
ListKnownWorktreesForExecutionHostArgs,
ListDetectedWorktreesArgs
} from '../../../../shared/detected-worktree-provider-contract'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../../shared/ssh-types'
@@ -105,12 +107,17 @@ const listDetectedMock = vi.fn<
return qualifyDetectedResult(args, result)
})
const listKnownForExecutionHostMock = vi.fn<
(args: ListKnownWorktreesForExecutionHostArgs) => Promise<HostQualifiedKnownWorktreeResult>
>(async (args) => ({ status: 'rejected', ...args }))
const mockApi = {
worktrees: {
create: vi.fn(),
prefetchCreateBase: vi.fn().mockResolvedValue(undefined),
list: worktreeListMock,
listDetected: listDetectedMock,
listKnownForExecutionHost: listKnownForExecutionHostMock,
cancelListDetected: vi.fn().mockResolvedValue(undefined),
listLineage: vi.fn().mockResolvedValue({}),
remove: vi.fn().mockResolvedValue(undefined),
@@ -1350,6 +1357,107 @@ describe('fetchWorktrees', () => {
expect(store.getState().detectedWorktreesByRepo).toBe(detectedWorktreesByRepo)
})
it('shows persisted secondary worktrees while SSH is connecting', async () => {
const store = createTestStore()
const sshRepo = {
id: 'repo-ssh',
path: '/home/orca/repo',
displayName: 'SSH Repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-1'
}
const queued = makeWorktree({
id: 'repo-ssh::/home/orca/queued',
repoId: 'repo-ssh',
path: '/home/orca/queued',
displayName: 'queued'
})
const detected = makeDetectedResult('repo-ssh', [queued], {
authoritative: false,
source: 'metadata-fallback'
})
listKnownForExecutionHostMock.mockResolvedValueOnce({
status: 'complete',
repoId: sshRepo.id,
executionHostId: 'ssh:ssh-1',
result: detected
})
store.setState({
repos: [sshRepo],
sshConnectionStates: new Map([
[
'ssh-1',
{
targetId: 'ssh-1',
status: 'connecting',
error: null,
reconnectAttempt: 0,
providerEpoch: null
}
]
])
} as Partial<AppState>)
await expect(store.getState().fetchWorktrees(sshRepo.id)).resolves.toBe(false)
expect(store.getState().worktreesByRepo[sshRepo.id]).toEqual([
{ ...queued, hostId: 'ssh:ssh-1' }
])
expect(listKnownForExecutionHostMock).toHaveBeenCalledWith({
repoId: sshRepo.id,
executionHostId: 'ssh:ssh-1'
})
expect(mockApi.worktrees.listDetected).not.toHaveBeenCalled()
})
it('adds metadata rows without replacing richer cached SSH worktrees', async () => {
const store = createTestStore()
const sshRepo = {
id: 'repo-ssh',
path: '/home/orca/repo',
displayName: 'SSH Repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-1'
}
const existing = makeWorktree({
id: 'repo-ssh::/home/orca/existing',
repoId: 'repo-ssh',
path: '/home/orca/existing',
hostId: 'ssh:ssh-1',
head: 'live-head',
branch: 'refs/heads/live-branch'
})
const metadataExisting = { ...existing, head: '', branch: '' }
const queued = makeWorktree({
id: 'repo-ssh::/home/orca/queued',
repoId: 'repo-ssh',
path: '/home/orca/queued'
})
listKnownForExecutionHostMock.mockResolvedValueOnce({
status: 'complete',
repoId: sshRepo.id,
executionHostId: 'ssh:ssh-1',
result: makeDetectedResult('repo-ssh', [metadataExisting, queued], {
authoritative: false,
source: 'metadata-fallback'
})
})
store.setState({
repos: [sshRepo],
sshConnectionStates: new Map(),
worktreesByRepo: { [sshRepo.id]: [existing] }
} as Partial<AppState>)
await store.getState().fetchWorktrees(sshRepo.id)
expect(store.getState().worktreesByRepo[sshRepo.id]).toEqual([
existing,
{ ...queued, hostId: 'ssh:ssh-1' }
])
})
it('keeps worktree maps byte-identical for stale and malformed direct results', async () => {
const store = createTestStore()
const existing = makeWorktree({
@@ -7536,6 +7644,63 @@ describe('fetchAllWorktrees hydration-time purge (design §4.4)', () => {
addedAt: 0
}
it.each([false, true])(
'hydrates connecting SSH worktrees with hydration purge completed=%s',
async (hasHydratedWorktreePurge) => {
const store = createTestStore()
const sshRepo = {
id: 'repo-ssh',
path: '/home/orca/repo',
displayName: 'SSH Repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-1'
}
const queued = makeWorktree({
id: 'repo-ssh::/home/orca/queued',
repoId: 'repo-ssh',
path: '/home/orca/queued',
displayName: 'queued'
})
listKnownForExecutionHostMock.mockResolvedValueOnce({
status: 'complete',
repoId: sshRepo.id,
executionHostId: 'ssh:ssh-1',
result: makeDetectedResult(sshRepo.id, [queued], {
authoritative: false,
source: 'metadata-fallback'
})
})
store.setState({
repos: [sshRepo],
hasHydratedWorktreePurge,
sshConnectionStates: new Map([
[
'ssh-1',
{
targetId: 'ssh-1',
status: 'connecting',
error: null,
reconnectAttempt: 0,
providerEpoch: null
}
]
])
} as Partial<AppState>)
await store.getState().fetchAllWorktrees()
expect(store.getState().worktreesByRepo[sshRepo.id]).toEqual([
{ ...queued, hostId: 'ssh:ssh-1' }
])
expect(listKnownForExecutionHostMock).toHaveBeenCalledWith({
repoId: sshRepo.id,
executionHostId: 'ssh:ssh-1'
})
expect(mockApi.worktrees.listDetected).not.toHaveBeenCalled()
}
)
it('preserves resolved inline legacy lineage when side-map hydration is absent', async () => {
const store = createTestStore()
const parent = makeWorktree({
+99 -1
View File
@@ -121,6 +121,7 @@ import {
import { getTerminalActivationSpawnSuppression } from './terminal-activation-spawn-suppression'
import type {
HostQualifiedDetectedWorktreeResult,
HostQualifiedKnownWorktreeResult,
ListDetectedWorktreesArgs,
ProviderRequestId,
SshExecutionHostId
@@ -2980,6 +2981,100 @@ function mergeFetchedWorktrees(
return admitted
}
function appendMissingWorktreesForHost<
T extends { id: string; hostId?: ExecutionHostId; runtimeOwnerEnvironmentId?: string }
>(
current: readonly T[] | undefined,
incoming: readonly T[],
hostId: ExecutionHostId,
options: WorktreeHostMatchOptions
): T[] {
const existing = current ?? []
const existingHostIds = new Set(
existing
.filter((worktree) => worktreeMatchesHost(worktree, hostId, options))
.map(({ id }) => id)
)
return [...existing, ...incoming.filter((worktree) => !existingHostIds.has(worktree.id))]
}
function isAdmittedKnownSshWorktreeResult(
result: HostQualifiedKnownWorktreeResult,
repoId: string,
executionHostId: SshExecutionHostId
): result is Extract<HostQualifiedKnownWorktreeResult, { status: 'complete' }> {
return (
result.status === 'complete' &&
result.repoId === repoId &&
result.executionHostId === executionHostId &&
isDetectedWorktreeListResult(result.result) &&
result.result.repoId === repoId &&
result.result.authoritative === false
)
}
async function fetchKnownSshWorktreesForRepo(
set: Parameters<StateCreator<AppState, [], [], WorktreeSlice>>[0],
repoId: string,
executionHostId: SshExecutionHostId
): Promise<DetectedWorktreeListResult | null> {
const listKnown = window.api.worktrees.listKnownForExecutionHost
if (typeof listKnown !== 'function') {
return null
}
const result = await listKnown({ repoId, executionHostId })
if (!isAdmittedKnownSshWorktreeResult(result, repoId, executionHostId)) {
return null
}
let admitted = false
set((state) => {
if (!repoHasExactlyOneExecutionHostOwner(state, repoId, executionHostId, false)) {
return state
}
admitted = true
const setup = getProjectHostSetupForRepoHost(state, repoId, executionHostId)
const matchOptions = worktreeHostMatchOptions(state, repoId, executionHostId)
const incomingDetected = result.result.worktrees.map((worktree) =>
withRepoHostOwnership(worktree, executionHostId, setup)
)
const detected = {
...result.result,
worktrees: appendMissingWorktreesForHost(
state.detectedWorktreesByRepo[repoId]?.worktrees,
incomingDetected,
executionHostId,
matchOptions
)
}
const worktrees = appendMissingWorktreesForHost(
state.worktreesByRepo[repoId],
toVisibleWorktrees(result.result, executionHostId, setup),
executionHostId,
matchOptions
)
const worktreesChanged = !areWorktreesEqual(state.worktreesByRepo[repoId], worktrees)
const detectedChanged = !areDetectedWorktreeResultsEqual(
state.detectedWorktreesByRepo[repoId],
detected
)
if (!worktreesChanged && !detectedChanged) {
return state
}
return {
...(worktreesChanged
? {
worktreesByRepo: { ...state.worktreesByRepo, [repoId]: worktrees },
sortEpoch: state.sortEpoch + 1
}
: {}),
...(detectedChanged
? { detectedWorktreesByRepo: { ...state.detectedWorktreesByRepo, [repoId]: detected } }
: {})
}
})
return admitted ? result.result : null
}
export type DirectSshDetectedWorktreeRefresh = {
waiterLeaseId: DetectedWorktreeRefreshLease['waiterLeaseId']
providerRequestId: ProviderRequestId
@@ -3110,7 +3205,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
? (getCurrentDirectSshAuthority(ownerState, hostId) ?? undefined)
: undefined
if (parsedHost?.kind === 'ssh' && !directSshAuthority) {
return null
return fetchKnownSshWorktreesForRepo(set, repoId, parsedHost.id)
}
const refresh = await listDetectedWorktreesForRepoCoalesced(
settingsForRepoOwner(ownerState, repoId, hostId),
@@ -3208,6 +3303,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
? (directCallerAuthority ?? getCurrentDirectSshAuthority(ownerState, hostId) ?? undefined)
: undefined
if (parsedHost?.kind === 'ssh' && !directSshAuthority) {
await fetchKnownSshWorktreesForRepo(set, repoId, parsedHost.id)
return false
}
const refresh = await listDetectedWorktreesForRepoCoalesced(settings, repoId, {
@@ -3272,6 +3368,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
? (getCurrentDirectSshAuthority(requestStartedState, hostId) ?? undefined)
: undefined
if (parsedHost?.kind === 'ssh' && !directSshAuthority) {
await fetchKnownSshWorktreesForRepo(set, r.id, parsedHost.id)
return
}
const refresh = await listDetectedWorktreesForRepoCoalesced(settings, r.id, {
@@ -3323,6 +3420,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
? (getCurrentDirectSshAuthority(requestStartedState, hostId) ?? undefined)
: undefined
if (parsedHost?.kind === 'ssh' && !directSshAuthority) {
await fetchKnownSshWorktreesForRepo(set, r.id, parsedHost.id)
return { repoId: r.id, ok: false as const }
}
const refresh = await listDetectedWorktreesForRepoCoalesced(
@@ -23,6 +23,24 @@ export type ListDetectedWorktreesArgs =
| LocalDetectedWorktreeRequest
| DirectSshDetectedWorktreeRequest
export type ListKnownWorktreesForExecutionHostArgs = {
repoId: string
executionHostId: SshExecutionHostId
}
export type HostQualifiedKnownWorktreeResult =
| {
status: 'complete'
repoId: string
executionHostId: SshExecutionHostId
result: DetectedWorktreeListResult
}
| {
status: 'rejected'
repoId: string
executionHostId: SshExecutionHostId
}
export type AuthoritativeDetectedWorktreeHost =
| {
kind: 'local'