mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(runtime): re-list a repo's worktrees when repo.update changes visibility (#32783)
* fix(runtime): re-list a repo's worktrees when repo.update changes visibility `orca repo set --external-worktree-visibility show` (and any other repo.update client) only emitted repos:changed, so the host renderer kept stale `visible: false` worktree rows and the newly shown worktree vanished from the sidebar. The settings controller now also emits worktrees:changed for the repo when a visibility input changes, without invalidating cached Git scans. Fixes #32764 Co-authored-by: Younghwan Chae <chyohw97@gmail.com> * fix(runtime): declare notifyWorktreesChanged on RuntimeConstructionServices The repository settings controller is built in OrcaRuntimeWithPreservedBranchCleanup, but notifyWorktreesChanged is implemented further down the chain in OrcaRuntimeWithGetStatus, so the base class needs the abstract member to typecheck. --------- Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
@@ -183,7 +183,8 @@ describe('repo settings on another host under a WSL move', () => {
|
||||
forgetTerminalTopology: vi.fn(),
|
||||
invalidateResolvedWorktrees: vi.fn(),
|
||||
invalidateWorktreeScan: vi.fn(),
|
||||
notifyReposChanged: vi.fn()
|
||||
notifyReposChanged: vi.fn(),
|
||||
notifyWorktreesChanged: vi.fn()
|
||||
})
|
||||
|
||||
await expect(controller.setBaseRef('repo-1', 'origin/dev')).resolves.toBe(onHost)
|
||||
|
||||
@@ -138,7 +138,8 @@ describe('RuntimeRepositorySettingsController under a WSL move', () => {
|
||||
forgetTerminalTopology: vi.fn(),
|
||||
invalidateResolvedWorktrees: vi.fn(),
|
||||
invalidateWorktreeScan: vi.fn(),
|
||||
notifyReposChanged
|
||||
notifyReposChanged,
|
||||
notifyWorktreesChanged: vi.fn()
|
||||
})
|
||||
|
||||
await expect(controller.update('repo-1', { displayName: 'renamed' })).rejects.toBeInstanceOf(
|
||||
|
||||
@@ -50,6 +50,7 @@ export abstract class RuntimeConstructionServices {
|
||||
abstract listRepos(): Repo[]
|
||||
abstract invalidateResolvedWorktreeCache(): void
|
||||
abstract invalidateWorktreeScanCacheForRepo(repoId: string): void
|
||||
abstract notifyWorktreesChanged(repoId: string, options?: { invalidateGitScans?: boolean }): void
|
||||
protected abstract notifyReposChanged(): void
|
||||
protected abstract isPtyKnownExited(ptyId: string): boolean
|
||||
|
||||
|
||||
@@ -333,13 +333,11 @@ export abstract class OrcaRuntimeWithPreservedBranchCleanup extends OrcaRuntimeW
|
||||
)
|
||||
|
||||
protected readonly repositorySettings = new RuntimeRepositorySettingsController({
|
||||
getStore: () => this.store,
|
||||
...this.repositoryCatalogDeps,
|
||||
resolveRepo: (selector) => this.resolveRepoSelector(selector),
|
||||
forgetTerminalTopology: (repoId) => this.terminalTopologyRevisionByRepoId.delete(repoId),
|
||||
stopProjectTerminals: (repo, options) => this.stopTerminalsForProject(repo, options),
|
||||
invalidateResolvedWorktrees: () => this.invalidateResolvedWorktreeCache(),
|
||||
invalidateWorktreeScan: (repoId) => this.invalidateWorktreeScanCacheForRepo(repoId),
|
||||
notifyReposChanged: () => this.notifyReposChanged()
|
||||
notifyWorktreesChanged: (repoId, options) => this.notifyWorktreesChanged(repoId, options)
|
||||
})
|
||||
|
||||
protected readonly repositorySparsePresets = new RuntimeRepositorySparsePresets({
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import './orca-runtime-test-lifecycle.spec'
|
||||
import { OrcaRuntimeService, listWorktrees } from './orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
TEST_REPO_ID,
|
||||
TEST_REPO_PATH,
|
||||
makeWorktreeInfo,
|
||||
store
|
||||
} from './orca-runtime-test-fixtures.spec'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import type { RuntimeClientEvent } from '../../shared/runtime-client-events'
|
||||
|
||||
const externalPath = '/tmp/elsewhere/external'
|
||||
|
||||
async function createScannedRuntime() {
|
||||
let repo: Repo = { ...store.getRepos()[0], externalWorktreeVisibility: 'hide' }
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...store,
|
||||
getRepo: (id: string) => (id === repo.id ? repo : undefined),
|
||||
getRepos: () => [repo],
|
||||
updateRepo: (_target: Repo, updates: Partial<Repo>) => {
|
||||
repo = { ...repo, ...updates }
|
||||
return repo
|
||||
}
|
||||
})
|
||||
vi.mocked(listWorktrees).mockClear()
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
makeWorktreeInfo(TEST_REPO_PATH),
|
||||
makeWorktreeInfo(externalPath)
|
||||
])
|
||||
const events: RuntimeClientEvent[] = []
|
||||
runtime.onClientEvent((event) => events.push(event))
|
||||
const isExternalVisible = async () =>
|
||||
(await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)).worktrees.find(
|
||||
(worktree) => worktree.path === externalPath
|
||||
)?.visible
|
||||
expect(await isExternalVisible()).toBe(false)
|
||||
return { runtime, events, isExternalVisible }
|
||||
}
|
||||
|
||||
describe('repo.update worktree refresh', () => {
|
||||
it('re-lists a newly shown external worktree without rescanning Git', async () => {
|
||||
const { runtime, events, isExternalVisible } = await createScannedRuntime()
|
||||
|
||||
await runtime.updateRepo(`id:${TEST_REPO_ID}`, { externalWorktreeVisibility: 'show' })
|
||||
|
||||
expect(events).toContainEqual({ type: 'worktreesChanged', repoId: TEST_REPO_ID })
|
||||
expect(await isExternalVisible()).toBe(true)
|
||||
expect(listWorktrees).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('skips the worktree refresh for catalog-only edits', async () => {
|
||||
const { runtime, events } = await createScannedRuntime()
|
||||
|
||||
await runtime.updateRepo(`id:${TEST_REPO_ID}`, { displayName: 'renamed' })
|
||||
|
||||
expect(events.some((event) => event.type === 'worktreesChanged')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,7 @@ type RuntimeRepositorySettingsDependencies = {
|
||||
invalidateResolvedWorktrees: () => void
|
||||
invalidateWorktreeScan: (repoId: string) => void
|
||||
notifyReposChanged: () => void
|
||||
notifyWorktreesChanged: (repoId: string, options: { invalidateGitScans: false }) => void
|
||||
}
|
||||
|
||||
type RepositoryUpdates = Partial<
|
||||
@@ -44,6 +45,8 @@ type RepositoryUpdates = Partial<
|
||||
| 'externalWorktreeInboxBaselinePaths'
|
||||
| 'importedExternalWorktreePaths'
|
||||
| 'agentWorktreeVisibility'
|
||||
| 'customWorktreeVisibilitySources'
|
||||
| 'worktreeVisibilitySourcePreferences'
|
||||
| 'projectGroupId'
|
||||
| 'projectGroupOrder'
|
||||
>
|
||||
@@ -54,6 +57,16 @@ type RepositoryUpdates = Partial<
|
||||
ghAccount?: GhAccountBinding | null
|
||||
}
|
||||
|
||||
// Main computes each row's `visible` from these over cached raw Git scans, so re-list without rescanning.
|
||||
const WORKTREE_VISIBILITY_INPUTS = [
|
||||
'externalWorktreeVisibility',
|
||||
'importedExternalWorktreePaths',
|
||||
'agentWorktreeVisibility',
|
||||
'worktreeBasePath',
|
||||
'customWorktreeVisibilitySources',
|
||||
'worktreeVisibilitySourcePreferences'
|
||||
] as const satisfies readonly (keyof RepositoryUpdates)[]
|
||||
|
||||
function omitUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(([, entry]) => entry !== undefined)
|
||||
@@ -120,6 +133,9 @@ export class RuntimeRepositorySettingsController {
|
||||
this.deps.invalidateWorktreeScan(repo.id)
|
||||
}
|
||||
this.deps.notifyReposChanged()
|
||||
if (WORKTREE_VISIBILITY_INPUTS.some((key) => key in updates)) {
|
||||
this.deps.notifyWorktreesChanged(repo.id, { invalidateGitScans: false })
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user