mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(worktree): collapse path-equal rows on the detected list too
`worktree.detectedList` served the same cached scan without the collapse, so remote/web clients kept seeing the duplicate row `worktree.list` no longer emits. The terminal sweep opts out: a spelling the collapse drops still names a live directory, and killing its PTYs is irreversible. Also makes `pruneLineageForMissingRepoWorktrees`'s `platform` required and threads `resolveRepoWorktreePathPlatform(repo)` through all four call sites, so an SSH repo is never evaluated under the desktop's macOS firmlink rules.
This commit is contained in:
@@ -4,6 +4,7 @@ import { readFile, stat } from 'node:fs/promises'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Store } from '../persistence'
|
||||
import { pruneLineageForMissingRepoWorktrees } from '../worktree-lineage-pruning'
|
||||
import { resolveRepoWorktreePathPlatform } from '../runtime/path-equal-worktree-row-collapse'
|
||||
import { isFolderRepo } from '../../shared/repo-kind'
|
||||
import { readBranchRenameFailureOutputForDisplay } from '../agent-hooks/branch-rename-failure-output'
|
||||
import { parseWorkspaceKey } from '../../shared/workspace-scope'
|
||||
@@ -1303,7 +1304,12 @@ async function listDetectedWorktreesForCapturedRepo(
|
||||
}
|
||||
if (freshScan) {
|
||||
rememberLocalWorktreeRoots(store, repo, gitWorktrees)
|
||||
pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees)
|
||||
pruneLineageForMissingRepoWorktrees(
|
||||
store,
|
||||
repo,
|
||||
gitWorktrees,
|
||||
resolveRepoWorktreePathPlatform(repo)
|
||||
)
|
||||
}
|
||||
loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`)
|
||||
return {
|
||||
@@ -1921,7 +1927,12 @@ export function registerWorktreeHandlers(
|
||||
}
|
||||
if (freshScan) {
|
||||
rememberLocalWorktreeRoots(store, repo, gitWorktrees)
|
||||
pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees)
|
||||
pruneLineageForMissingRepoWorktrees(
|
||||
store,
|
||||
repo,
|
||||
gitWorktrees,
|
||||
resolveRepoWorktreePathPlatform(repo)
|
||||
)
|
||||
}
|
||||
loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`)
|
||||
return buildDetectedGitWorktrees(store, repo, gitWorktrees)
|
||||
@@ -1993,7 +2004,12 @@ export function registerWorktreeHandlers(
|
||||
}
|
||||
if (freshScan) {
|
||||
rememberLocalWorktreeRoots(store, repo, gitWorktrees)
|
||||
pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees)
|
||||
pruneLineageForMissingRepoWorktrees(
|
||||
store,
|
||||
repo,
|
||||
gitWorktrees,
|
||||
resolveRepoWorktreePathPlatform(repo)
|
||||
)
|
||||
}
|
||||
loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`)
|
||||
return buildDetectedGitWorktrees(store, repo, gitWorktrees)
|
||||
|
||||
@@ -41217,6 +41217,68 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(removeWorktreeLineage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('collapses path-equal rows in the detected worktree list', async () => {
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
{
|
||||
path: TEST_WORKTREE_PATH,
|
||||
head: 'abc',
|
||||
branch: '',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
{
|
||||
path: '/tmp/./worktree-a',
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
const runtime = new OrcaRuntimeService(store as never)
|
||||
|
||||
const result = await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)
|
||||
|
||||
expect(result.worktrees.map((worktree) => worktree.id)).toEqual([TEST_WORKTREE_ID])
|
||||
expect(result.worktrees[0]?.branch).toBe('feature/foo')
|
||||
})
|
||||
|
||||
it('keeps terminals for a path-equal spelling the detected-list collapse drops', async () => {
|
||||
const dotSpellingId = `${TEST_REPO_ID}::/tmp/./worktree-a`
|
||||
const localProvider = {
|
||||
listProcesses: vi.fn(async () => [
|
||||
{ id: `${dotSpellingId}@@session`, cwd: '/tmp/./worktree-a', title: 'shell' }
|
||||
]),
|
||||
shutdown: vi.fn(async () => {})
|
||||
}
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
{
|
||||
path: TEST_WORKTREE_PATH,
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
{
|
||||
path: '/tmp/./worktree-a',
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
const runtime = new OrcaRuntimeService(store as never, undefined, {
|
||||
getLocalProvider: () => localProvider as never
|
||||
})
|
||||
|
||||
const result = await runtime.teardownMissingManagedWorktreeTerminals(`id:${TEST_REPO_ID}`, [
|
||||
TEST_WORKTREE_ID,
|
||||
dotSpellingId
|
||||
])
|
||||
|
||||
expect(result).toEqual({ stoppedWorktreeIds: [] })
|
||||
expect(localProvider.shutdown).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('revalidates missing worktrees before stopping their local provider sessions', async () => {
|
||||
const deletedId = `${TEST_REPO_ID}::/tmp/deleted`
|
||||
const survivingId = `${TEST_REPO_ID}::/tmp/surviving`
|
||||
|
||||
@@ -1051,6 +1051,10 @@ import {
|
||||
getDefaultVoiceSettings
|
||||
} from '../../shared/constants'
|
||||
import { pruneLineageForMissingRepoWorktrees } from '../worktree-lineage-pruning'
|
||||
import {
|
||||
collapsePathEqualWorktreeRows,
|
||||
resolveRepoWorktreePathPlatform
|
||||
} from './path-equal-worktree-row-collapse'
|
||||
import {
|
||||
createWorktreeCopiedPaths,
|
||||
createWorktreeLinkedPaths,
|
||||
@@ -23320,7 +23324,10 @@ export class OrcaRuntimeService {
|
||||
|
||||
private async listDetectedWorktreesForResolvedRepo(
|
||||
repo: Repo,
|
||||
sourceDefaultsSupported = true
|
||||
sourceDefaultsSupported = true,
|
||||
// Why opt-out: the terminal sweep judges "missing" against every spelling git reported, because
|
||||
// a spelling the collapse drops still names a live directory and killing its PTYs is final.
|
||||
collapsePathEqualRows = true
|
||||
): Promise<DetectedWorktreeListResult> {
|
||||
const store = this.requireStore()
|
||||
const settings = store.getSettings()
|
||||
@@ -23361,8 +23368,11 @@ export class OrcaRuntimeService {
|
||||
} catch {
|
||||
scan = { ok: false, worktrees: [] }
|
||||
}
|
||||
const pathPlatform = resolveRepoWorktreePathPlatform(repo)
|
||||
if (scan.ok) {
|
||||
pruneLineageForMissingRepoWorktrees(store, repo, scan.worktrees)
|
||||
// Why raw rows: a spelling the collapse drops still names a live directory, so pruning
|
||||
// against the collapsed set would read it as a deleted worktree.
|
||||
pruneLineageForMissingRepoWorktrees(store, repo, scan.worktrees, pathPlatform)
|
||||
}
|
||||
const worktreeVisibilitySourceMatcher = createWorktreeVisibilitySourceMatcher(
|
||||
[repo.path, ...scan.worktrees.map((worktree) => worktree.path)],
|
||||
@@ -23372,7 +23382,14 @@ export class OrcaRuntimeService {
|
||||
const expectedHostId = getRepoExecutionHostId(repo)
|
||||
const repoOwnerCount = store.getRepos().filter((candidate) => candidate.id === repo.id).length
|
||||
const metaById = store.getAllWorktreeMeta()
|
||||
const detected = scan.worktrees.map((gitWorktree) => {
|
||||
const rows = collapsePathEqualRows
|
||||
? collapsePathEqualWorktreeRows(scan.worktrees, {
|
||||
repoPath: repo.path,
|
||||
hasStoredMeta: (worktreePath) => metaById[`${repo.id}::${worktreePath}`] !== undefined,
|
||||
platform: pathPlatform
|
||||
})
|
||||
: scan.worktrees
|
||||
const detected = rows.map((gitWorktree) => {
|
||||
const worktreeId = `${repo.id}::${gitWorktree.path}`
|
||||
const meta = getRepoOwnedWorktreeMeta(repo, worktreeId, metaById, repoOwnerCount)
|
||||
const worktree = {
|
||||
@@ -23412,7 +23429,7 @@ export class OrcaRuntimeService {
|
||||
// Why: rescanning by `id:` would re-resolve the already-resolved repo, and a
|
||||
// duplicate id across hosts makes that second lookup throw selector_ambiguous
|
||||
// even though the caller's selector was unique — losing the sweep entirely.
|
||||
const detected = await this.listDetectedWorktreesForResolvedRepo(repo)
|
||||
const detected = await this.listDetectedWorktreesForResolvedRepo(repo, true, false)
|
||||
if (!detected.authoritative) {
|
||||
return { stoppedWorktreeIds: [] }
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('pruneLineageForMissingRepoWorktrees', () => {
|
||||
}
|
||||
const store = createStore(worktreeLineageById, workspaceLineageByChildKey, metaById)
|
||||
|
||||
pruneLineageForMissingRepoWorktrees(store as never, repo, [])
|
||||
pruneLineageForMissingRepoWorktrees(store as never, repo, [], 'linux')
|
||||
|
||||
expect(store.removeWorktreeLineage).not.toHaveBeenCalled()
|
||||
expect(store.removeWorkspaceLineage).not.toHaveBeenCalled()
|
||||
@@ -128,22 +128,27 @@ describe('pruneLineageForMissingRepoWorktrees', () => {
|
||||
}
|
||||
const store = createStore(worktreeLineageById, workspaceLineageByChildKey, metaById)
|
||||
|
||||
pruneLineageForMissingRepoWorktrees(store as never, repo, [
|
||||
{
|
||||
path: '/repo/live-parent',
|
||||
head: 'a',
|
||||
branch: 'main',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
{
|
||||
path: '/repo/live-child',
|
||||
head: 'b',
|
||||
branch: 'child',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
pruneLineageForMissingRepoWorktrees(
|
||||
store as never,
|
||||
repo,
|
||||
[
|
||||
{
|
||||
path: '/repo/live-parent',
|
||||
head: 'a',
|
||||
branch: 'main',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
{
|
||||
path: '/repo/live-child',
|
||||
head: 'b',
|
||||
branch: 'child',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
],
|
||||
'linux'
|
||||
)
|
||||
|
||||
expect(store.removeWorktreeLineage).toHaveBeenCalledWith(missingChildId)
|
||||
expect(store.removeWorktreeLineage).not.toHaveBeenCalledWith(liveChildId)
|
||||
|
||||
@@ -40,7 +40,9 @@ export function pruneLineageForMissingRepoWorktrees(
|
||||
store: Store,
|
||||
repo: Repo,
|
||||
gitWorktrees: GitWorktreeInfo[],
|
||||
platform: NodeJS.Platform = process.platform
|
||||
// Why required: rows can come from an SSH or WSL host, and silently defaulting to the desktop's
|
||||
// rules would apply macOS `/private/tmp` remapping to a remote host's paths.
|
||||
platform: NodeJS.Platform
|
||||
): void {
|
||||
if (
|
||||
typeof store.getAllWorktreeLineage !== 'function' ||
|
||||
|
||||
Reference in New Issue
Block a user