mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user