perf(renderer): keep worktree lineage map identity on no-op refresh

* perf(renderer): keep worktree lineage map identity on no-op refresh

A host-scoped lineage refresh always spread IPC clones into new maps, so
WorktreeList Object.is subscriptions missed every time even when capture,
taskId, and coordinatorHandle were unchanged. Reuse structurally equal
records, return the previous maps when nothing moved, and skip set() when
both maps are still current so a no-op refresh is actually a no-op.

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

* type(store): mark worktree lineage maps readonly

Widen reuseEqualRecordMap and the renderer lineage store fields to
Readonly<Record<...>> so a reused no-op refresh map cannot be mutated
in place.

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

* type(store): build lineage overlays on mutable records

mergeExactHostLineage was writing into AppState map types after they
became Readonly. Use local mutable Records, then reuseEqualRecordMap.

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

* fix(renderer): keep direct-ssh host hydration under the max-lines cap

The lineage overlay rewrite pushed the file to 305 counted lines against the 300 cap, failing oxlint. Fold the duplicated per-map filter/overlay into one scoped helper so both maps share it.

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

* perf(renderer): drop dead reuseEqualRecordMap key loop

A key missing from next always shows up as either a key-count mismatch or a failed previous lookup in the first pass, so the second pass over the previous keys could never flip identical. Cover the identity contract with focused unit tests, including the same-count key swap that the removed loop appeared to guard.

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

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-13 00:18:47 -07:00
committed by GitHub
co-authored by Orca
parent 6afba56501
commit ac68eead64
7 changed files with 446 additions and 59 deletions
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { HostLineageSnapshot } from '../../../shared/host-lineage-contract'
import type { HostRepoCatalogSnapshot } from '../../../shared/host-repo-catalog-contract'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import type { Repo } from '../../../shared/types'
import type { Repo, WorktreeLineage, WorkspaceLineage } from '../../../shared/types'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../shared/workspace-scope'
import type { AppState } from '../store/types'
import { createDirectSshHostHydration } from './direct-ssh-host-hydration'
@@ -40,6 +40,34 @@ function state(overrides: Record<string, unknown> = {}): AppState {
} as unknown as AppState
}
function productionLineage(worktreeId: string, parentWorktreeId: string): WorktreeLineage {
return {
worktreeId,
worktreeInstanceId: `${worktreeId}-instance`,
parentWorktreeId,
parentWorktreeInstanceId: `${parentWorktreeId}-instance`,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-1',
coordinatorHandle: 'coord-1',
createdAt: 1
}
}
function productionWorkspaceLineage(childId: string, parentId: string): WorkspaceLineage {
return {
childWorkspaceKey: worktreeWorkspaceKey(childId),
childInstanceId: `${childId}-instance`,
parentWorkspaceKey: worktreeWorkspaceKey(parentId),
parentInstanceId: `${parentId}-instance`,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-1',
coordinatorHandle: 'coord-1',
createdAt: 1
}
}
function hostSnapshot(
owner: DirectSshAuthority,
repos: ReturnType<typeof repo>[]
@@ -341,6 +369,79 @@ describe('createDirectSshHostHydration', () => {
})
})
it('keeps lineage map identity for a cloned no-op host snapshot', async () => {
const owner = authority()
const hostLineage = productionLineage('a::/work', 'a::/parent')
const foreignLineage = productionLineage('b::/work', 'b::/parent')
const hostWorkspace = productionWorkspaceLineage('a::/work', 'a::/parent')
const foreignWorkspace = productionWorkspaceLineage('b::/work', 'b::/parent')
const store = createStore<AppState>(() =>
state({
repos: [repo('a', 'target-a'), repo('b', 'target-b')],
worktreesByRepo: {
a: [{ id: 'a::/work', repoId: 'a', hostId: 'ssh:target-a' }],
b: [{ id: 'b::/work', repoId: 'b', hostId: 'ssh:target-b' }]
},
worktreeLineageById: {
'a::/work': hostLineage,
'b::/work': foreignLineage
},
workspaceLineageByChildKey: {
[worktreeWorkspaceKey('a::/work')]: hostWorkspace,
[worktreeWorkspaceKey('b::/work')]: foreignWorkspace
}
})
)
const snapshot: HostLineageSnapshot = {
authoritative: true,
authority: {
kind: 'direct-ssh',
executionHostId: 'ssh:target-a',
...owner
},
worktreeLineageById: {
'a::/work': hostLineage
},
workspaceLineageByChildKey: {
[worktreeWorkspaceKey('a::/work')]: hostWorkspace
}
}
let publications = 0
store.subscribe(() => {
publications += 1
})
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(),
listLineage: vi.fn(async () => structuredClone(snapshot)),
isCurrentAuthority: () => true
})
const beforeLineage = store.getState().worktreeLineageById
const beforeWorkspace = store.getState().workspaceLineageByChildKey
await expect(
hydration.readHostScopedLineage({
...owner,
catalogRevision: 0,
repoRefs: [{ repoId: 'a', executionHostId: 'ssh:target-a' }],
authorityRequirement: 'required',
reason: 'reconnect'
})
).resolves.toBe('complete')
expect(store.getState().worktreeLineageById).toBe(beforeLineage)
expect(store.getState().workspaceLineageByChildKey).toBe(beforeWorkspace)
expect(store.getState().worktreeLineageById['a::/work']).toBe(hostLineage)
expect(store.getState().worktreeLineageById['b::/work']).toBe(foreignLineage)
expect(store.getState().workspaceLineageByChildKey[worktreeWorkspaceKey('a::/work')]).toBe(
hostWorkspace
)
expect(store.getState().workspaceLineageByChildKey[worktreeWorkspaceKey('b::/work')]).toBe(
foreignWorkspace
)
expect(publications).toBe(0)
})
it('rejects lineage captured before a newer same-authority catalog revision', async () => {
const owner = authority()
const store = createStore<AppState>(() =>
@@ -5,6 +5,7 @@ import type { HostRepoCatalogSnapshot } from '../../../shared/host-repo-catalog-
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import { isWorkspaceKey } from '../../../shared/workspace-scope'
import type { AppState } from '../store/types'
import { reuseEqualRecordMap } from '../store/slices/repo-identity-reconcile'
import type {
DirectSshLineageOutcome,
DirectSshPreparationInput,
@@ -71,6 +72,20 @@ function mergeExactHostCatalog(state: AppState, snapshot: HostRepoCatalogSnapsho
}
}
// Why: the host owns every in-scope key, so drop the stale in-scope rows, overlay the snapshot's,
// and leave out-of-scope hosts alone. Reusing equal rows keeps a cloned no-op read from republishing.
function overlayHostScopedLineage<T>(
previous: Readonly<Record<string, T>>,
incoming: Readonly<Record<string, T>>,
isHostScoped: (key: string) => boolean
): Readonly<Record<string, T>> {
const next: Record<string, T> = Object.fromEntries([
...Object.entries(previous).filter(([key]) => !isHostScoped(key)),
...Object.entries(incoming).filter(([key]) => isHostScoped(key))
])
return reuseEqualRecordMap(previous, next)
}
function mergeExactHostLineage(
state: AppState,
snapshot: Extract<HostLineageSnapshot, { authoritative: true }>,
@@ -78,38 +93,23 @@ function mergeExactHostLineage(
catalogRevision: number
): AppState {
const scope = directSshHostHydrationScope(state, authority, catalogRevision)
const worktreeLineageById = Object.fromEntries(
Object.entries(state.worktreeLineageById).filter(
([worktreeId]) => !scope.gitWorktreeIds.has(worktreeId)
)
const worktreeLineageById = overlayHostScopedLineage(
state.worktreeLineageById,
snapshot.worktreeLineageById,
(worktreeId) => scope.gitWorktreeIds.has(worktreeId)
)
const workspaceLineageByChildKey = Object.fromEntries(
Object.entries(state.workspaceLineageByChildKey).filter(
([childKey]) => !isWorkspaceKey(childKey) || !scope.lineageWorkspaceKeys.has(childKey)
)
const workspaceLineageByChildKey = overlayHostScopedLineage(
state.workspaceLineageByChildKey,
snapshot.workspaceLineageByChildKey,
(childKey) => isWorkspaceKey(childKey) && scope.lineageWorkspaceKeys.has(childKey)
)
const incomingWorktreeLineage = Object.fromEntries(
Object.entries(snapshot.worktreeLineageById).filter(([worktreeId]) =>
scope.gitWorktreeIds.has(worktreeId)
)
)
const incomingWorkspaceLineage = Object.fromEntries(
Object.entries(snapshot.workspaceLineageByChildKey).filter(
([workspaceKey]) =>
isWorkspaceKey(workspaceKey) && scope.lineageWorkspaceKeys.has(workspaceKey)
)
)
return {
...state,
worktreeLineageById: {
...worktreeLineageById,
...incomingWorktreeLineage
},
workspaceLineageByChildKey: {
...workspaceLineageByChildKey,
...incomingWorkspaceLineage
}
if (
worktreeLineageById === state.worktreeLineageById &&
workspaceLineageByChildKey === state.workspaceLineageByChildKey
) {
return state
}
return { ...state, worktreeLineageById, workspaceLineageByChildKey }
}
export function createDirectSshHostHydration(
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import fs from 'node:fs'
import type { Repo } from '../../../../shared/types'
import { reconcileFetchedRepos } from './repo-identity-reconcile'
import { reconcileFetchedRepos, reuseEqualRecordMap } from './repo-identity-reconcile'
function makeRepo(id: string, overrides: Partial<Repo> = {}): Repo {
return { id, path: `/${id}`, displayName: id, badgeColor: '#000', addedAt: 1, ...overrides }
@@ -123,3 +123,54 @@ describe('reconcileFetchedRepos', () => {
expect(source).not.toMatch(/previous\.find|next\.find|findIndex/)
})
})
describe('reuseEqualRecordMap', () => {
const row = (value: string): { value: string; nested: { tags: string[] } } => ({
value,
nested: { tags: [value, 'shared'] }
})
it('returns the previous map when every key holds an equal value', () => {
const previous = { a: row('a'), b: row('b') }
expect(reuseEqualRecordMap(previous, { a: row('a'), b: row('b') })).toBe(previous)
})
it('reuses the previous entry for structurally equal nested values', () => {
const previous = { a: row('a'), b: row('b') }
const result = reuseEqualRecordMap(previous, { a: row('a'), b: row('changed') })
expect(result).not.toBe(previous)
expect(result.a).toBe(previous.a)
expect(result.b).toEqual(row('changed'))
})
it('returns a new map when a value changed', () => {
const previous = { a: row('a') }
const result = reuseEqualRecordMap(previous, { a: row('changed') })
expect(result).not.toBe(previous)
expect(result.a).toEqual(row('changed'))
})
it('returns a new map when a key is added', () => {
const previous = { a: row('a') }
const result = reuseEqualRecordMap(previous, { a: row('a'), b: row('b') })
expect(result).not.toBe(previous)
expect(result.a).toBe(previous.a)
expect(Object.keys(result)).toEqual(['a', 'b'])
})
it('returns a new map when a key is removed', () => {
const previous = { a: row('a'), b: row('b') }
const result = reuseEqualRecordMap(previous, { a: row('a') })
expect(result).not.toBe(previous)
expect(Object.keys(result)).toEqual(['a'])
})
it('returns a new map when a key is swapped for a same-count replacement', () => {
// Why: equal key counts alone must not read as unchanged — the replacement key misses the
// previous lookup, which is why no second pass over the previous keys is needed.
const previous = { a: row('a'), b: row('b') }
const result = reuseEqualRecordMap(previous, { a: row('a'), c: row('b') })
expect(result).not.toBe(previous)
expect(Object.keys(result)).toEqual(['a', 'c'])
})
})
@@ -79,3 +79,28 @@ export function reconcileFetchedRepos(
): readonly Repo[] {
return reconcileCatalogRows(previous, next, getRepoHostIdentity)
}
/**
* Reuses equal record values from `previous` — and the whole map when nothing
* changed — so a cloned no-op refresh leaves Object.is subscribers untouched.
*/
export function reuseEqualRecordMap<T>(
previous: Readonly<Record<string, T>>,
next: Readonly<Record<string, T>>
): Readonly<Record<string, T>> {
const nextKeys = Object.keys(next)
// Why: a matching key count plus every `next` key resolving to an equal `previous` entry below
// means the key sets match, so a removed key always lands as either a count or a lookup miss.
let identical = nextKeys.length === Object.keys(previous).length
const reconciled: Record<string, T> = {}
for (const key of nextKeys) {
const existing = Object.hasOwn(previous, key) ? previous[key] : undefined
if (existing !== undefined && areValuesEqual(existing, next[key])) {
reconciled[key] = existing
continue
}
identical = false
reconciled[key] = next[key]
}
return identical ? previous : reconciled
}
@@ -89,8 +89,8 @@ export type ActiveWorktreeStateTransition = (state: AppState) => {
export type WorktreeSlice = {
worktreesByRepo: Record<string, Worktree[]>
detectedWorktreesByRepo: Record<string, DetectedWorktreeListResult>
worktreeLineageById: Record<string, WorktreeLineage>
workspaceLineageByChildKey: Record<WorkspaceKey, WorkspaceLineage>
worktreeLineageById: Readonly<Record<string, WorktreeLineage>>
workspaceLineageByChildKey: Readonly<Record<WorkspaceKey, WorkspaceLineage>>
activeWorktreeId: string | null
activeWorkspaceKey: WorkspaceKey | null
activeWorkspaceExecutionHostId: ExecutionHostId | null
@@ -3187,6 +3187,71 @@ describe('fetchWorktrees', () => {
expect(store.getState().sortEpoch).toBe(7)
})
it('keeps remote lineage map identity when a cloned payload is unchanged', async () => {
const store = createTestStore()
const worktree = makeWorktree({
id: 'repo1::/remote/wt1',
repoId: 'repo1',
path: '/remote/wt1',
branch: 'refs/heads/remote',
hostId: 'runtime:env-1'
})
const lineage = makeLineage({
worktreeId: worktree.id,
parentWorktreeId: 'repo1::/remote/parent',
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-42',
coordinatorHandle: 'coord-1'
})
const workspaceLineage = makeWorkspaceLineage({
childWorkspaceKey: worktreeWorkspaceKey(worktree.id),
parentWorkspaceKey: worktreeWorkspaceKey(lineage.parentWorktreeId),
childInstanceId: lineage.worktreeInstanceId,
parentInstanceId: lineage.parentWorktreeInstanceId,
origin: lineage.origin,
capture: lineage.capture,
taskId: lineage.taskId,
coordinatorHandle: lineage.coordinatorHandle,
createdAt: lineage.createdAt
})
const detected = makeDetectedResult('repo1', [worktree])
store.setState({
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
worktreesByRepo: { repo1: [worktree] },
detectedWorktreesByRepo: { repo1: detected },
worktreeLineageById: { [lineage.worktreeId]: lineage },
workspaceLineageByChildKey: {
[workspaceLineage.childWorkspaceKey]: workspaceLineage
},
sortEpoch: 7
} as Partial<AppState>)
runtimeEnvironmentCall.mockImplementation(({ method }: RuntimeEnvironmentCallRequest) => {
const result =
method === 'worktree.lineageList'
? structuredClone({
lineage: { [lineage.worktreeId]: lineage },
workspaceLineage: { [workspaceLineage.childWorkspaceKey]: workspaceLineage }
})
: structuredClone(detected)
return Promise.resolve({
id: 'rpc-1',
ok: true,
result,
_meta: { runtimeId: 'runtime-remote' }
})
})
const before = store.getState()
await store.getState().fetchWorktrees('repo1')
expect(store.getState().worktreeLineageById).toBe(before.worktreeLineageById)
expect(store.getState().workspaceLineageByChildKey).toBe(before.workspaceLineageByChildKey)
expect(store.getState().worktreeLineageById[lineage.worktreeId]).toBe(lineage)
expect(store.getState().workspaceLineageByChildKey[workspaceLineage.childWorkspaceKey]).toBe(
workspaceLineage
)
})
it('defers remote lineage when a caller owns the final host refresh', async () => {
const store = createTestStore()
const worktree = makeWorktree({
@@ -3319,6 +3384,135 @@ describe('worktree lineage state', () => {
})
})
it('keeps lineage map and entry identity across a cloned no-op refresh', async () => {
const lineage = makeLineage({
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-42',
coordinatorHandle: 'coord-1'
})
const workspaceLineage = makeWorkspaceLineage({
childWorkspaceKey: worktreeWorkspaceKey(lineage.worktreeId),
parentWorkspaceKey: worktreeWorkspaceKey(lineage.parentWorktreeId),
childInstanceId: lineage.worktreeInstanceId,
parentInstanceId: lineage.parentWorktreeInstanceId,
origin: lineage.origin,
capture: lineage.capture,
taskId: lineage.taskId,
coordinatorHandle: lineage.coordinatorHandle,
createdAt: lineage.createdAt
})
const store = createLocalLineageTestStore(lineage)
const payload = {
lineage: { [lineage.worktreeId]: lineage },
workspaceLineage: { [workspaceLineage.childWorkspaceKey]: workspaceLineage }
}
mockApi.worktrees.listLineage.mockImplementation(async () => structuredClone(payload))
await store.getState().fetchWorktreeLineage()
const afterFirst = store.getState()
const lineageById = afterFirst.worktreeLineageById
const workspaceByKey = afterFirst.workspaceLineageByChildKey
const lineageEntry = lineageById[lineage.worktreeId]
const workspaceEntry = workspaceByKey[workspaceLineage.childWorkspaceKey]
expect(lineageEntry).toEqual(lineage)
expect(lineageEntry).not.toBe(lineage)
const subscriber = vi.fn()
const unsubscribe = store.subscribe(subscriber)
await store.getState().fetchWorktreeLineage()
unsubscribe()
expect(store.getState().worktreeLineageById).toBe(lineageById)
expect(store.getState().workspaceLineageByChildKey).toBe(workspaceByKey)
expect(store.getState().worktreeLineageById[lineage.worktreeId]).toBe(lineageEntry)
expect(store.getState().workspaceLineageByChildKey[workspaceLineage.childWorkspaceKey]).toBe(
workspaceEntry
)
expect(subscriber).not.toHaveBeenCalled()
expect(mockApi.worktrees.listLineage).toHaveBeenCalledTimes(2)
})
it('preserves other-host lineage row identity across a cloned same-host refresh', async () => {
const store = createTestStore()
const localWorktree = makeWorktree({
id: 'repo1::/local/child',
repoId: 'repo1',
hostId: LOCAL_EXECUTION_HOST_ID
})
const sshWorktree = makeWorktree({
id: 'repo2::/ssh/child',
repoId: 'repo2',
hostId: 'ssh:ssh-1'
})
const localLineage = makeLineage({
worktreeId: localWorktree.id,
parentWorktreeId: 'repo1::/local/parent',
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-local',
coordinatorHandle: 'coord-local'
})
const sshLineage = makeLineage({
worktreeId: sshWorktree.id,
parentWorktreeId: 'repo2::/ssh/parent',
capture: { source: 'cwd-context', confidence: 'inferred' },
taskId: 'task-ssh',
coordinatorHandle: 'coord-ssh'
})
const localWorkspace = makeWorkspaceLineage({
childWorkspaceKey: worktreeWorkspaceKey(localWorktree.id),
parentWorkspaceKey: worktreeWorkspaceKey(localLineage.parentWorktreeId),
childInstanceId: localLineage.worktreeInstanceId,
parentInstanceId: localLineage.parentWorktreeInstanceId,
origin: localLineage.origin,
capture: localLineage.capture,
taskId: localLineage.taskId,
coordinatorHandle: localLineage.coordinatorHandle
})
const sshWorkspace = makeWorkspaceLineage({
childWorkspaceKey: worktreeWorkspaceKey(sshWorktree.id),
parentWorkspaceKey: worktreeWorkspaceKey(sshLineage.parentWorktreeId),
childInstanceId: sshLineage.worktreeInstanceId,
parentInstanceId: sshLineage.parentWorktreeInstanceId,
origin: sshLineage.origin,
capture: sshLineage.capture,
taskId: sshLineage.taskId,
coordinatorHandle: sshLineage.coordinatorHandle
})
store.setState({
worktreesByRepo: {
repo1: [localWorktree],
repo2: [sshWorktree]
},
worktreeLineageById: {
[localWorktree.id]: localLineage,
[sshWorktree.id]: sshLineage
},
workspaceLineageByChildKey: {
[localWorkspace.childWorkspaceKey]: localWorkspace,
[sshWorkspace.childWorkspaceKey]: sshWorkspace
}
} as Partial<AppState>)
const payload = {
lineage: { [localWorktree.id]: localLineage },
workspaceLineage: { [localWorkspace.childWorkspaceKey]: localWorkspace }
}
mockApi.worktrees.listLineage.mockImplementation(async () => structuredClone(payload))
const before = store.getState()
await store.getState().fetchWorktreeLineage()
expect(store.getState().worktreeLineageById).toBe(before.worktreeLineageById)
expect(store.getState().workspaceLineageByChildKey).toBe(before.workspaceLineageByChildKey)
expect(store.getState().worktreeLineageById[localWorktree.id]).toBe(localLineage)
expect(store.getState().worktreeLineageById[sshWorktree.id]).toBe(sshLineage)
expect(store.getState().workspaceLineageByChildKey[localWorkspace.childWorkspaceKey]).toBe(
localWorkspace
)
expect(store.getState().workspaceLineageByChildKey[sshWorkspace.childWorkspaceKey]).toBe(
sshWorkspace
)
})
it('clears workspace lineage on successful old-shape lineage refresh', async () => {
const store = createTestStore()
const lineage = makeLineage()
+42 -26
View File
@@ -133,6 +133,7 @@ import type {
import type { DirectSshAuthority } from '../../../../shared/ssh-types'
import { findIndexedWorktreeOwnerForHost } from '@/lib/worktree-runtime-owner-index'
import { catalogRowsEqual, reuseEqualCatalogRows } from './worktree-catalog-reconciliation'
import { reuseEqualRecordMap } from './repo-identity-reconcile'
export type { WorktreeSlice, WorktreeDeleteState } from './worktree-helpers'
// Why: old runtime servers only have `worktree.list`; preserve the large-list UI hydration parity used before `worktree.detectedList` existed.
@@ -1390,8 +1391,8 @@ async function listWorktreeLineageForRuntime(
settings: AppState['settings'],
options: BackgroundRuntimeRefreshOptions = {}
): Promise<{
worktreeLineageById: Record<string, WorktreeLineage>
workspaceLineageByChildKey: Record<string, WorkspaceLineage>
worktreeLineageById: Readonly<Record<string, WorktreeLineage>>
workspaceLineageByChildKey: Readonly<Record<string, WorkspaceLineage>>
}> {
const target = getActiveRuntimeTarget(settings)
type LineageListResponse = {
@@ -1570,21 +1571,38 @@ function applyWorktreeLineageUpdate(
})
}
function applyHostLineageRefresh(
set: Parameters<StateCreator<AppState>>[0],
hostId: ExecutionHostId,
lineage: {
worktreeLineageById: Readonly<Record<string, WorktreeLineage>>
workspaceLineageByChildKey: Readonly<Record<string, WorkspaceLineage>>
}
): void {
set((s) => {
const worktreeLineageById = mergeLineageForHost(s, hostId, lineage.worktreeLineageById)
const workspaceLineageByChildKey = mergeWorkspaceLineageForHost(
s,
hostId,
lineage.workspaceLineageByChildKey
)
if (
worktreeLineageById === s.worktreeLineageById &&
workspaceLineageByChildKey === s.workspaceLineageByChildKey
) {
return s
}
return { worktreeLineageById, workspaceLineageByChildKey }
})
}
async function refreshWorktreeLineageForSettings(
settings: AppState['settings'],
set: Parameters<StateCreator<AppState>>[0],
options: BackgroundRuntimeRefreshOptions = {}
): Promise<void> {
const lineage = await listWorktreeLineageForRuntime(settings, options)
const hostId = getSettingsFocusedExecutionHostId(settings)
set((s) => ({
worktreeLineageById: mergeLineageForHost(s, hostId, lineage.worktreeLineageById),
workspaceLineageByChildKey: mergeWorkspaceLineageForHost(
s,
hostId,
lineage.workspaceLineageByChildKey
)
}))
applyHostLineageRefresh(set, getSettingsFocusedExecutionHostId(settings), lineage)
}
async function refreshRemoteWorktreeLineageBestEffort(
@@ -1598,15 +1616,7 @@ async function refreshRemoteWorktreeLineageBestEffort(
const lineage = await listWorktreeLineageForRuntime(settings, {
reuseRecentCompatibilityFailure: true
})
const hostId = getSettingsFocusedExecutionHostId(settings)
set((s) => ({
worktreeLineageById: mergeLineageForHost(s, hostId, lineage.worktreeLineageById),
workspaceLineageByChildKey: mergeWorkspaceLineageForHost(
s,
hostId,
lineage.workspaceLineageByChildKey
)
}))
applyHostLineageRefresh(set, getSettingsFocusedExecutionHostId(settings), lineage)
} catch (err) {
// Why: lineage is supplemental, so a remote timeout here must not discard a successful worktree refresh.
console.error('Failed to fetch worktree lineage:', err)
@@ -1638,15 +1648,18 @@ function mergeLineageForHost(
'repos' | 'settings' | 'worktreesByRepo' | 'detectedWorktreesByRepo' | 'worktreeLineageById'
>,
hostId: ExecutionHostId,
lineage: Record<string, WorktreeLineage>
): Record<string, WorktreeLineage> {
lineage: Readonly<Record<string, WorktreeLineage>>
): Readonly<Record<string, WorktreeLineage>> {
const next: Record<string, WorktreeLineage> = {}
for (const [worktreeId, existing] of Object.entries(state.worktreeLineageById)) {
if (getWorktreeHostId(state, worktreeId) !== hostId) {
next[worktreeId] = existing
}
}
return { ...next, ...lineage }
for (const [worktreeId, incoming] of Object.entries(lineage)) {
next[worktreeId] = incoming
}
return reuseEqualRecordMap(state.worktreeLineageById, next)
}
function mergeWorkspaceLineageForHost(
@@ -1659,8 +1672,8 @@ function mergeWorkspaceLineageForHost(
| 'workspaceLineageByChildKey'
>,
hostId: ExecutionHostId,
lineage: Record<string, WorkspaceLineage>
): Record<string, WorkspaceLineage> {
lineage: Readonly<Record<string, WorkspaceLineage>>
): Readonly<Record<string, WorkspaceLineage>> {
const next: Record<string, WorkspaceLineage> = {}
for (const [childKey, existing] of Object.entries(state.workspaceLineageByChildKey)) {
const childScope = parseWorkspaceKey(existing.childWorkspaceKey)
@@ -1671,7 +1684,10 @@ function mergeWorkspaceLineageForHost(
next[childKey] = existing
}
}
return { ...next, ...lineage }
for (const [childKey, incoming] of Object.entries(lineage)) {
next[childKey] = incoming
}
return reuseEqualRecordMap(state.workspaceLineageByChildKey, next)
}
async function persistWorktreeMeta(