mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
`orca worktree list` returned zero of 24 SSH worktrees at the default limit (#18104). Rows are resolved repo by repo, so every SSH repo's rows land contiguously at the end of the fleet order — the 24 remote rows sat at indices 496-520 of 521 and a plain `slice(0, 200)` never reached them. The omission was not fully silent: text output printed `truncated: showing 200 of 521` and JSON carried `totalCount` / `truncated`. What was missing is that the omission was *categorically every remote host* — no host column, no `hostScope`, nothing to distinguish "200 of 521" from "one host is entirely absent". Per docs/reference/ssh-execution-boundary.md, a listing that does not name its scope reads as absolute. Adopt the mechanism `terminal list` already has rather than inventing a second one: - `RuntimeTerminalListHostScope` becomes an alias of a shared `RuntimeListingHostScope`, now also carried (optional, so old hosts are unaffected) on `worktree.list` and `worktree.ps` results. - `src/shared/host-balanced-listing-page.ts` round-robins the row cap across hosts and returns the survivors in the caller's original relative order, so the page stays a subsequence of the unbounded listing and nothing downstream re-sorts. An uncapped listing is returned unchanged. - `worktree list` / `worktree ps` text output gains a `host=` column and the same trailing `scope:` line `terminal list` prints. Third defect, same mechanism: `hostScope.omittedHostIds` is built from the runtime's own bookkeeping, so it names `runtime:` ids for servers that are no longer paired — 6 of 9 in the recorded QA run hard-error when queried. Since `hostScope` is *the* documented way to complete a partial listing, that makes the mechanism unreliable for its intended use. Annotate rather than filter. Dropping an id would shrink what the listing admits it did not cover, and the boundary doc requires a listing to name its gaps — the gap is real whether or not this machine can name the host that owns it. `src/cli/omitted-host-scope-selectors.ts` resolves each omitted id against this machine's pairing store and the runtime's SSH-target registry and attaches the exact flag that reaches it, or `null` marked "not selectable from this machine". This is a client-side annotation: nothing new goes over the wire, it answers "can I select it" and never "is it up", and the SSH round trip is only paid when an `ssh:` host was actually omitted. No `--host` filter was added; the host column plus scope line covers the reported need without a new selector axis.
325 lines
13 KiB
TypeScript
325 lines
13 KiB
TypeScript
import type { DetectedWorktreeListResult, Worktree } from '../../shared/worktree/types'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import type { RuntimeWorktreeListResult } from '../../shared/runtime-types'
|
|
import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
|
|
import { buildWorktreeListingPage } from './worktree-listing-host-scope'
|
|
import { readWorktreeMetaForHost } from '../persistence/host-qualified-worktree-meta'
|
|
import { getRepoOwnedWorktreeMeta } from '../worktree-metadata-ownership'
|
|
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
|
|
import { isFolderRepo } from '../../shared/repo-kind'
|
|
import {
|
|
applyMetadataFallbackVisibility,
|
|
buildKnownOrcaWorkspaceLayouts,
|
|
isLegacyRepoForExternalWorktreeVisibility,
|
|
toDetectedWorktree
|
|
} from '../../shared/worktree/ownership'
|
|
import { projectResolvedWorktreeLineage } from '../../shared/resolved-worktree-lineage'
|
|
import {
|
|
createWorktreeVisibilitySourceMatcher,
|
|
resolveCustomWorktreeVisibilitySources,
|
|
type WorktreeVisibilitySourceMatcher
|
|
} from '../../shared/worktree/visibility-sources'
|
|
import { mergeWorktree } from '../ipc/worktree-logic'
|
|
import { pruneLineageForMissingRepoWorktrees } from '../worktree-lineage-pruning'
|
|
import { pruneMetadataMissingFromAuthoritativeLocalScan } from '../ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning'
|
|
import type { NativeLocalWorktreeMetadataScanExpectation } from '../persistence/tracking-repos/missing-local-worktree-metadata-pruning'
|
|
import { getLocalWorktreeScanGeneration } from '../local-worktree-scan-generation'
|
|
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
|
|
import type { Store } from '../persistence'
|
|
import type { RuntimeStore } from './runtime-store-contract'
|
|
import type { RuntimeWorktreeScanResult } from './repo-worktree-resolution-scan'
|
|
import { listRuntimeFolderWorkspaces } from './runtime-worktree-filesystem'
|
|
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
|
|
import { resolveConfiguredWorktreeBasePaths } from '../../shared/worktree/configured-worktree-base-path'
|
|
import { getRetiredNameRegistryForRepo } from '../worktree-name-retirement'
|
|
|
|
type Dependencies = {
|
|
getStore(): RuntimeStore | null
|
|
listResolved(): Promise<ResolvedWorktree[]>
|
|
resolveRepo(selector: string): Promise<Repo>
|
|
selectRepos(selector: string): Repo[]
|
|
scanRepo(repo: Repo): Promise<RuntimeWorktreeScanResult>
|
|
/** Hosts this runtime has repos or workspaces on, so a host with no rows is still named. */
|
|
listKnownHostIds(): Iterable<ExecutionHostId>
|
|
}
|
|
|
|
/**
|
|
* The destructive scan expectation for one repo, or undefined when this repo must not carry one.
|
|
*
|
|
* WSL-routed repos are excluded for the same reason the desktop listing excludes them: the listing
|
|
* runs in the distro and reports Linux paths while metadata can hold UNC ones, and v1 cannot prove
|
|
* those aliases equivalent. A runtime that needs repair throws rather than resolving routing, which
|
|
* is likewise no basis for deleting rows.
|
|
*/
|
|
function captureLocalMetadataPruneExpectation(
|
|
store: RuntimeStore,
|
|
repo: Repo
|
|
): NativeLocalWorktreeMetadataScanExpectation | undefined {
|
|
if (typeof store.captureNativeLocalWorktreeMetadataScanExpectation !== 'function') {
|
|
return undefined
|
|
}
|
|
try {
|
|
if (getLocalProjectWorktreeGitOptions(store as unknown as Store, repo).wslDistro) {
|
|
return undefined
|
|
}
|
|
} catch {
|
|
return undefined
|
|
}
|
|
return store.captureNativeLocalWorktreeMetadataScanExpectation(repo)
|
|
}
|
|
|
|
export class RuntimeManagedWorktreeQueries {
|
|
constructor(private readonly deps: Dependencies) {}
|
|
|
|
async list(
|
|
repoSelector: string | undefined,
|
|
limit: number,
|
|
sourceDefaultsSupported = true
|
|
): Promise<RuntimeWorktreeListResult> {
|
|
if (!Number.isInteger(limit) || limit <= 0) {
|
|
throw new Error('invalid_limit')
|
|
}
|
|
const resolved = await this.deps.listResolved()
|
|
const repoId = repoSelector ? (await this.deps.resolveRepo(repoSelector)).id : null
|
|
const pathsByRepo = new Map<string, string[]>()
|
|
for (const worktree of resolved) {
|
|
const paths = pathsByRepo.get(worktree.repoId) ?? []
|
|
paths.push(worktree.path)
|
|
pathsByRepo.set(worktree.repoId, paths)
|
|
}
|
|
const visibilityDefaults = this.visibilityDefaults(sourceDefaultsSupported)
|
|
const matchers = new Map(
|
|
(this.deps.getStore()?.getRepos() ?? []).map((repo) => [
|
|
repo.id,
|
|
createWorktreeVisibilitySourceMatcher(
|
|
[repo.path, ...(pathsByRepo.get(repo.id) ?? [])],
|
|
resolveCustomWorktreeVisibilitySources(repo, visibilityDefaults),
|
|
resolveConfiguredWorktreeBasePaths(repo)
|
|
)
|
|
])
|
|
)
|
|
const worktrees = resolved.filter(
|
|
(worktree) =>
|
|
(!repoId || worktree.repoId === repoId) &&
|
|
this.isVisible(worktree, matchers.get(worktree.repoId), sourceDefaultsSupported)
|
|
)
|
|
// Why: a `--repo` listing was scoped by the caller, so naming every configured host as
|
|
// omitted would report a gap the caller deliberately excluded.
|
|
return buildWorktreeListingPage(worktrees, limit, repoId ? [] : this.deps.listKnownHostIds())
|
|
}
|
|
|
|
resolveRepoForConnection(selector: string, connectionId?: string | null): Promise<Repo> {
|
|
if (connectionId === undefined) {
|
|
return this.deps.resolveRepo(selector)
|
|
}
|
|
const wanted = connectionId?.trim() || null
|
|
const matches = this.deps
|
|
.selectRepos(selector)
|
|
.filter((repo) => (repo.connectionId?.trim() || null) === wanted)
|
|
if (matches.length !== 1) {
|
|
throw new Error(matches.length > 1 ? 'selector_ambiguous' : 'repo_not_found')
|
|
}
|
|
return Promise.resolve(matches[0])
|
|
}
|
|
|
|
async listDetected(
|
|
repo: Repo,
|
|
sourceDefaultsSupported = true
|
|
): Promise<DetectedWorktreeListResult> {
|
|
const store = this.deps.getStore()
|
|
if (!store) {
|
|
throw new Error('runtime_unavailable')
|
|
}
|
|
const settings = store.getSettings()
|
|
const visibilityDefaults = this.visibilityDefaults(sourceDefaultsSupported)
|
|
const visibilitySettings = { ...settings, worktreeVisibilityDefaults: visibilityDefaults }
|
|
if (isFolderRepo(repo)) {
|
|
const worktrees = listRuntimeFolderWorkspaces(store, repo)
|
|
const metaById = store.getAllWorktreeMeta()
|
|
const repoOwnerCount = store.getRepos().filter((candidate) => candidate.id === repo.id).length
|
|
const matcher = createWorktreeVisibilitySourceMatcher(
|
|
[repo.path, ...worktrees.map((worktree) => worktree.path)],
|
|
resolveCustomWorktreeVisibilitySources(repo, visibilityDefaults),
|
|
resolveConfiguredWorktreeBasePaths(repo)
|
|
)
|
|
const detected = worktrees.map((worktree) =>
|
|
this.toDetected(
|
|
repo,
|
|
worktree,
|
|
matcher,
|
|
sourceDefaultsSupported,
|
|
visibilitySettings,
|
|
getRepoOwnedWorktreeMeta(repo, worktree.id, metaById, repoOwnerCount) ?? null
|
|
)
|
|
)
|
|
return {
|
|
repoId: repo.id,
|
|
authoritative: true,
|
|
source: 'git',
|
|
worktrees: projectResolvedWorktreeLineage(detected, store.getAllWorktreeLineage?.() ?? {})
|
|
}
|
|
}
|
|
// Why capture before the scan: listing can mutate metadata synchronously before its first
|
|
// await, and the prune revalidates against the rows as they stood when the scan was issued.
|
|
const metadataScanGeneration = getLocalWorktreeScanGeneration(repo.id)
|
|
const metadataPruneExpectation = captureLocalMetadataPruneExpectation(store, repo)
|
|
let scan: RuntimeWorktreeScanResult
|
|
try {
|
|
scan = await this.deps.scanRepo(repo)
|
|
} catch {
|
|
scan = { ok: false, worktrees: [] }
|
|
}
|
|
if (scan.ok) {
|
|
// Why the runtime sweeps too: the desktop listing that used to own this runs off `ipcMain`,
|
|
// so a headless host -- which has no renderer -- never pruned its own repos' rows (#17776).
|
|
if (metadataPruneExpectation) {
|
|
await pruneMetadataMissingFromAuthoritativeLocalScan({
|
|
store: store as unknown as Store,
|
|
repo,
|
|
gitWorktrees: scan.worktrees,
|
|
scan: metadataPruneExpectation,
|
|
scanGeneration: metadataScanGeneration
|
|
})
|
|
}
|
|
pruneLineageForMissingRepoWorktrees(store as unknown as Store, repo, scan.worktrees)
|
|
}
|
|
const matcher = createWorktreeVisibilitySourceMatcher(
|
|
[repo.path, ...scan.worktrees.map((worktree) => worktree.path)],
|
|
resolveCustomWorktreeVisibilitySources(repo, visibilityDefaults),
|
|
resolveConfiguredWorktreeBasePaths(repo)
|
|
)
|
|
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 id = `${repo.id}::${gitWorktree.path}`
|
|
const meta =
|
|
readWorktreeMetaForHost(store as unknown as Store, id, expectedHostId) ??
|
|
getRepoOwnedWorktreeMeta(repo, id, metaById, repoOwnerCount)
|
|
const worktree = {
|
|
...mergeWorktree(repo.id, gitWorktree, meta, repo.displayName),
|
|
hostId: repoOwnerCount === 1 ? (meta?.hostId ?? expectedHostId) : expectedHostId
|
|
}
|
|
const result = this.toDetected(
|
|
repo,
|
|
worktree,
|
|
matcher,
|
|
sourceDefaultsSupported,
|
|
visibilitySettings,
|
|
meta ?? null
|
|
)
|
|
return scan.ok ? result : applyMetadataFallbackVisibility(result)
|
|
})
|
|
return {
|
|
repoId: repo.id,
|
|
authoritative: scan.ok,
|
|
source: scan.ok ? 'git' : 'metadata-fallback',
|
|
worktrees: projectResolvedWorktreeLineage(detected, store.getAllWorktreeLineage?.() ?? {})
|
|
}
|
|
}
|
|
|
|
isVisible(
|
|
worktree: Worktree,
|
|
matcher?: WorktreeVisibilitySourceMatcher,
|
|
sourceDefaultsSupported = true,
|
|
providedSettings?: ReturnType<RuntimeStore['getSettings']>
|
|
): boolean {
|
|
const repo = this.deps.getStore()?.getRepo(worktree.repoId)
|
|
return repo
|
|
? this.toDetected(repo, worktree, matcher, sourceDefaultsSupported, providedSettings).visible
|
|
: true
|
|
}
|
|
|
|
buildVisibilityMatchers(
|
|
worktrees: readonly Worktree[],
|
|
sourceDefaultsSupported = true,
|
|
providedSettings?: ReturnType<RuntimeStore['getSettings']>
|
|
): Map<string, WorktreeVisibilitySourceMatcher> {
|
|
const checkoutPathsByRepoId = new Map<string, string[]>()
|
|
for (const worktree of worktrees) {
|
|
const checkoutPaths = checkoutPathsByRepoId.get(worktree.repoId) ?? []
|
|
checkoutPaths.push(worktree.path)
|
|
checkoutPathsByRepoId.set(worktree.repoId, checkoutPaths)
|
|
}
|
|
const visibilityDefaults = this.visibilityDefaults(sourceDefaultsSupported, providedSettings)
|
|
return new Map(
|
|
(this.deps.getStore()?.getRepos() ?? [])
|
|
.filter((repo) => checkoutPathsByRepoId.has(repo.id))
|
|
.map((repo) => [
|
|
repo.id,
|
|
createWorktreeVisibilitySourceMatcher(
|
|
[repo.path, ...(checkoutPathsByRepoId.get(repo.id) ?? [])],
|
|
resolveCustomWorktreeVisibilitySources(repo, visibilityDefaults),
|
|
resolveConfiguredWorktreeBasePaths(repo)
|
|
)
|
|
])
|
|
)
|
|
}
|
|
|
|
private toDetected(
|
|
repo: Repo,
|
|
worktree: Worktree,
|
|
matcher?: WorktreeVisibilitySourceMatcher,
|
|
sourceDefaultsSupported = true,
|
|
providedSettings?: ReturnType<RuntimeStore['getSettings']>,
|
|
providedMeta?: WorktreeMeta | null
|
|
) {
|
|
const store = this.deps.getStore()
|
|
const settings = providedSettings ?? store?.getSettings()
|
|
if (!settings) {
|
|
return {
|
|
...worktree,
|
|
ownership: 'unknown-legacy' as const,
|
|
selectedCheckout: false,
|
|
visible: true
|
|
}
|
|
}
|
|
const visibilityDefaults = this.visibilityDefaults(sourceDefaultsSupported, settings)
|
|
return toDetectedWorktree({
|
|
repo,
|
|
worktree,
|
|
meta:
|
|
providedMeta === undefined
|
|
? store?.getWorktreeMeta(worktree.id)
|
|
: (providedMeta ?? undefined),
|
|
settings: { ...settings, worktreeVisibilityDefaults: visibilityDefaults },
|
|
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo),
|
|
isLegacyRepoForVisibility: isLegacyRepoForExternalWorktreeVisibility(repo),
|
|
worktreeVisibilitySourceMatcher: matcher
|
|
})
|
|
}
|
|
|
|
async listRetiredNames(repoSelector: string): Promise<{
|
|
retiredNamesByRepo: Record<string, readonly string[]>
|
|
retiredNameTiersByRepo: Record<string, number>
|
|
}> {
|
|
const store = this.deps.getStore()
|
|
if (!store?.getRetiredWorktreeNameRegistry || !store.mergeRetiredWorktreeNames) {
|
|
return { retiredNamesByRepo: {}, retiredNameTiersByRepo: {} }
|
|
}
|
|
const repo = await this.deps.resolveRepo(repoSelector)
|
|
const settings = store.getSettings()
|
|
const registry = await getRetiredNameRegistryForRepo(
|
|
store as never,
|
|
repo,
|
|
store.getRepos(),
|
|
settings
|
|
)
|
|
return {
|
|
retiredNamesByRepo: { [repo.id]: registry.names },
|
|
retiredNameTiersByRepo: { [repo.id]: registry.exhaustedTiers }
|
|
}
|
|
}
|
|
|
|
private visibilityDefaults(
|
|
sourceDefaultsSupported: boolean,
|
|
providedSettings?: ReturnType<RuntimeStore['getSettings']>
|
|
) {
|
|
const defaults =
|
|
providedSettings !== undefined
|
|
? providedSettings.worktreeVisibilityDefaults
|
|
: this.deps.getStore()?.getSettings().worktreeVisibilityDefaults
|
|
return sourceDefaultsSupported || !defaults ? defaults : { external: defaults.external }
|
|
}
|
|
}
|