perf(worktrees): preserve Git scans when saving sidebar order (#27343)

* perf(worktrees): preserve Git scans when saving sort order

Credit @mmarabel for identifying the repeated scans and the host-only approach in #27271.

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>

* perf(worktrees): skip Git scan invalidation for order saves on desktop hosts too

The desktop window notifier ran the worktree-change invalidators unconditionally,
so a desktop (or paired-host) Orca still cleared Git scan caches on every sidebar
order save. Carry the invalidateGitScans flag through the notifier; every other
caller keeps the default.

---------

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-10 21:24:34 -07:00
committed by GitHub
co-authored by mmarabel
parent 74856c83b3
commit e4f599df05
5 changed files with 111 additions and 9 deletions
@@ -179,7 +179,7 @@ export class OrcaRuntimeWithCreateManagedRemoteWorktree extends OrcaRuntimeWithC
orderedIds,
store: this.store,
invalidateResolved: () => this.invalidateResolvedWorktreeCache(),
notifyChanged: (repoId) => this.notifyWorktreesChanged(repoId)
notifyChanged: (repoId) => this.notifyWorktreesChanged(repoId, { invalidateGitScans: false })
})
}
+13 -5
View File
@@ -267,11 +267,19 @@ export class OrcaRuntimeWithGetStatus extends OrcaRuntimeWithGetRuntimeId {
return { tabId, worktreeId: pty.worktreeId }
}
protected notifyWorktreesChanged(repoId: string): void {
// Why here: the listing re-runs a scan this generation overtook, and a headless host has no
// window notifier to bump it, so the runtime's own change event bumps before it is sent.
runWorktreeChangeInvalidators(repoId)
this.notifier?.worktreesChanged(repoId)
protected notifyWorktreesChanged(
repoId: string,
options?: { invalidateGitScans?: boolean }
): void {
// Why: headless hosts have no window notifier, so invalidate here unless the change is metadata-only.
if (options?.invalidateGitScans !== false) {
runWorktreeChangeInvalidators(repoId)
}
if (options) {
this.notifier?.worktreesChanged(repoId, undefined, options)
} else {
this.notifier?.worktreesChanged(repoId)
}
this.emitClientEvent({ type: 'worktreesChanged', repoId })
}
@@ -31,7 +31,12 @@ export type RuntimeNotifier = {
selector?: { kind: 'self' } | { kind: 'ssh'; targetId: string } | { kind: 'orphan' }
reason?: 'definition' | 'run' | 'usage'
}): void
worktreesChanged(repoId: string, renamed?: { oldWorktreeId: string; newWorktreeId: string }): void
worktreesChanged(
repoId: string,
renamed?: { oldWorktreeId: string; newWorktreeId: string },
// Why: false for metadata-only changes (sort order) that leave Git scan results valid.
options?: { invalidateGitScans?: boolean }
): void
worktreeBaseStatus?(event: WorktreeBaseStatusEvent): void
worktreeRemoteBranchConflict?(event: WorktreeRemoteBranchConflictEvent): void
reposChanged(): void
@@ -0,0 +1,87 @@
import { describe, expect, it, vi } from 'vitest'
import { join } from 'node:path'
import type { BrowserWindow } from 'electron'
import './orca-runtime-test-lifecycle.spec'
import { OrcaRuntimeService, listWorktrees } from './orca-runtime-test-mocks.spec'
import type { WorktreeMeta } from './orca-runtime-test-mocks.spec'
import {
TEST_REPO_ID,
TEST_REPO_PATH,
makeWorktreeInfo,
makeWorktreeMeta,
store
} from './orca-runtime-test-fixtures.spec'
import { registerRuntimeWindowLifecycle } from '../window/runtime-window-lifecycle'
vi.mock('electron', () => ({ ipcMain: { on: vi.fn(), removeListener: vi.fn() } }))
const firstPath = join(TEST_REPO_PATH, 'first')
const secondPath = join(TEST_REPO_PATH, 'second')
const firstId = `${TEST_REPO_ID}::${firstPath}`
const secondId = `${TEST_REPO_ID}::${secondPath}`
async function createScannedRuntime(): Promise<InstanceType<typeof OrcaRuntimeService>> {
const metaById: Record<string, WorktreeMeta> = {
[firstId]: makeWorktreeMeta({ sortOrder: 200 }),
[secondId]: makeWorktreeMeta({ sortOrder: 100 })
}
const runtime = new OrcaRuntimeService({
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (id: string) => metaById[id],
setWorktreeMeta: (id: string, meta: Partial<WorktreeMeta>) => {
metaById[id] = { ...metaById[id], ...meta }
return metaById[id]
}
})
vi.mocked(listWorktrees).mockClear()
vi.mocked(listWorktrees).mockResolvedValue([
makeWorktreeInfo(firstPath),
makeWorktreeInfo(secondPath)
])
await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)
return runtime
}
async function expectOrderSaveSkipsGitScan(
runtime: InstanceType<typeof OrcaRuntimeService>
): Promise<void> {
const events: { type: string; repoId?: string }[] = []
const unsubscribe = runtime.onClientEvent((event) => events.push(event))
expect(runtime.persistManagedWorktreeSortOrder([secondId, firstId])).toEqual({ updated: 2 })
unsubscribe()
const listed = await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)
expect(events).toEqual([{ type: 'worktreesChanged', repoId: TEST_REPO_ID }])
expect(listWorktrees).toHaveBeenCalledTimes(1)
const sortOrderOf = (id: string) =>
listed.worktrees.find((worktree) => worktree.id === id)?.sortOrder ?? Number.NaN
expect(sortOrderOf(secondId)).toBeGreaterThan(sortOrderOf(firstId))
runtime.publishWorktreeRemovalChange(TEST_REPO_ID)
await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)
expect(listWorktrees).toHaveBeenCalledTimes(2)
}
describe('runtime sort-order saves', () => {
it('publishes the changed order without rescanning Git on a headless host', async () => {
await expectOrderSaveSkipsGitScan(await createScannedRuntime())
})
it('publishes the changed order without rescanning Git through the desktop window notifier', async () => {
const runtime = await createScannedRuntime()
const send = vi.fn()
const mainWindow = {
id: 1,
isDestroyed: () => false,
on: vi.fn(),
webContents: { isDestroyed: () => false, send, on: vi.fn() }
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: registration only reads id, isDestroyed, on and webContents.
registerRuntimeWindowLifecycle(mainWindow as unknown as BrowserWindow, runtime)
await expectOrderSaveSkipsGitScan(runtime)
expect(send).toHaveBeenCalledWith('worktrees:changed', { repoId: TEST_REPO_ID })
})
})
+4 -2
View File
@@ -37,9 +37,11 @@ export function registerRuntimeWindowLifecycle(
})
const send = rendererNotifications.send
runtime.setNotifier({
worktreesChanged: (repoId, renamed) => {
worktreesChanged: (repoId, renamed, options) => {
// Why: clear scan caches before the renderer handles this event, so it can't read stale TTL entries after a mutation.
runWorktreeChangeInvalidators(repoId)
if (options?.invalidateGitScans !== false) {
runWorktreeChangeInvalidators(repoId)
}
send('worktrees:changed', renamed ? { repoId, renamed } : { repoId })
},
worktreeBaseStatus: (event) => send('worktree:baseStatus', event),