mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
* fix(workspaces): add collision-safe worktree identity * fix(workspaces): read worktree metadata per host and repair ambiguous identities The canonical identity store landed write-only: getWorktreeMetaForHost had no production callers while setWorktreeMetaForHost kept the legacy projection only for the first known owner, so a second host's edits persisted and were never read back. Wire the listing paths through host-qualified reads. An ambiguous alias was also unrecoverable — reads returned undefined and writes threw forever, and the throw escaped the detected-worktree loop, emptying the whole repo's sidebar. Fail open onto the most recently active instance instead. - collapse ambiguous aliases deterministically and persist the repair - reclaim identity rows in the metadata GC so they cannot outlive their locator or resurrect onto a worktree recreated at the same path - drop every host's rows when a locator is removed outright, not just the owner's - honour an explicit instanceId so the stale-lineage rotation guard still works - scope a rename to the moving host; other hosts keep their own locator - prefer the project host setup matching the repo's own execution host, so a repoId registered on two hosts no longer stamps the wrong one durably - reject an unencoded `|` in a host id, the invariant the alias delimiter needs - drop the never-populated hostGeneration from the canonical key * fix(workspaces): close remaining identity review gaps * fix(workspaces): close remaining review gaps * fix(workspaces): address review and CI regressions * test(workspaces): update host-qualified metadata expectations * fix(workspaces): preserve ambiguous identity records * fix(workspaces): snapshot metadata during listing * test(workspaces): mirror listing metadata snapshot in windows fixture * fix(workspaces): preserve identity routing for metadata writes * fix(workspaces): scope stale metadata cleanup by host * fix(workspaces): rekey identities on SSH readoption * fix(workspaces): fail closed for ambiguous board ids * perf(workspaces): snapshot metadata across catalog listing * fix(workspaces): retain neighboring manual order updates * test(workspaces): cover ambiguous board id index * fix(persistence): harden host-qualified worktree metadata * refactor(shared): split project host setup lookup * refactor(workspaces): simplify host-qualified metadata
194 lines
7.4 KiB
TypeScript
194 lines
7.4 KiB
TypeScript
import type {
|
|
ForgetRemovedWorktreesForExecutionHostArgs,
|
|
ForgetRemovedWorktreesForExecutionHostResult,
|
|
HostQualifiedDetectedWorktreeResult,
|
|
HostQualifiedKnownWorktreeResult,
|
|
LegacyDetectedWorktreeRequest,
|
|
ListDetectedWorktreesArgs,
|
|
ListKnownWorktreesForExecutionHostArgs,
|
|
ProviderRequestId
|
|
} from '../../shared/detected-worktree-provider-contract'
|
|
import type { ExecutionHostId } from '../../shared/execution-host'
|
|
import type { RetiredNameRegistry } from '../../shared/worktree/retired-name-registry'
|
|
import type {
|
|
FolderWorkspacePathStatus,
|
|
FolderWorkspacePathStatusRequest
|
|
} from '../../shared/folder-workspace-path-status'
|
|
import type {
|
|
HostLineageSnapshot,
|
|
ListDesktopLineageForHostArgs
|
|
} from '../../shared/host-lineage-contract'
|
|
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
|
|
import type {
|
|
WorktreeBaseStatusEvent,
|
|
WorktreeRemoteBranchConflictEvent
|
|
} from '../../shared/worktree/base-ref-drift-types'
|
|
import type {
|
|
AdoptProvisionedRootArgs,
|
|
CreateWorktreeArgs,
|
|
CreateWorktreeResult,
|
|
ForceDeleteWorktreeBranchResult,
|
|
RemoveWorktreeResult,
|
|
SparsePreset
|
|
} from '../../shared/worktree/create-types'
|
|
import type { WorkspaceLineage, WorktreeLineage } from '../../shared/worktree/lineage-types'
|
|
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
|
|
import type {
|
|
DetectedWorktreeListResult,
|
|
GitHubPrStartPoint,
|
|
GitPushTarget,
|
|
Worktree,
|
|
WorktreeHeadIdentity
|
|
} from '../../shared/worktree/types'
|
|
|
|
export type WorktreeApi = {
|
|
list: (args: { repoId: string }) => Promise<Worktree[]>
|
|
/** Generated names already spent in this repo, including deleted workspaces. Name suggestions
|
|
* exclude these so a recreated workspace never lands on a prior occupant's path. Compacted: a
|
|
* fully spent tier is reported as the watermark rather than as its 552 names. */
|
|
listRetiredNames: (args: { repoId: string }) => Promise<RetiredNameRegistry>
|
|
listDetected: {
|
|
(
|
|
args: ListDetectedWorktreesArgs
|
|
): Promise<HostQualifiedDetectedWorktreeResult | DetectedWorktreeListResult>
|
|
(args: LegacyDetectedWorktreeRequest): Promise<DetectedWorktreeListResult>
|
|
}
|
|
listKnownForExecutionHost?: (
|
|
args: ListKnownWorktreesForExecutionHostArgs
|
|
) => Promise<HostQualifiedKnownWorktreeResult>
|
|
/** Retires the persisted metadata an authoritative scan proved gone, so it stops feeding the read above. */
|
|
forgetRemovedForExecutionHost?: (
|
|
args: ForgetRemovedWorktreesForExecutionHostArgs
|
|
) => Promise<ForgetRemovedWorktreesForExecutionHostResult>
|
|
cancelListDetected?: (args: { providerRequestId: ProviderRequestId }) => Promise<void>
|
|
listAll: () => Promise<Worktree[]>
|
|
create: (args: CreateWorktreeArgs) => Promise<CreateWorktreeResult>
|
|
adoptProvisionedRoot: (args: AdoptProvisionedRootArgs) => Promise<CreateWorktreeResult>
|
|
/** Two-phase progress for a background `create`, correlated by `creationId`. The remote/runtime
|
|
* create path emits nothing, so the surface falls back to an indeterminate spinner. */
|
|
onCreateProgress: (
|
|
callback: (data: { creationId?: string; phase: 'fetching' | 'creating' }) => void
|
|
) => () => void
|
|
prefetchCreateBase: (args: { repoId: string; baseBranch?: string }) => Promise<void>
|
|
resolvePrBase: (args: {
|
|
repoId: string
|
|
prNumber: number
|
|
headRefName?: string
|
|
baseRefName?: string
|
|
isCrossRepository?: boolean
|
|
}) => Promise<GitHubPrStartPoint | { error: string }>
|
|
/** GitLab parallel of resolvePrBase. For same-project MRs returns
|
|
* `<remote>/<source_branch>`; for fork MRs fetches
|
|
* refs/merge-requests/<iid>/head and returns the SHA. */
|
|
resolveMrBase: (args: {
|
|
repoId: string
|
|
mrIid: number
|
|
sourceBranch?: string
|
|
targetBranch?: string
|
|
isCrossRepository?: boolean
|
|
}) => Promise<
|
|
{ baseBranch: string; compareBaseRef?: string; pushTarget?: GitPushTarget } | { error: string }
|
|
>
|
|
remove: (args: {
|
|
worktreeId: string
|
|
hostId?: ExecutionHostId
|
|
force?: boolean
|
|
// Why (#11960): distinct from `force`, which the plain Delete confirmation
|
|
// already sets to skip the dirty-file prompt. Only an explicit Force Delete
|
|
// may waive the proof that every PTY stopped.
|
|
allowUnverifiedPtyStop?: boolean
|
|
skipArchive?: boolean
|
|
snapshotPruneBatchId?: string
|
|
}) => Promise<RemoveWorktreeResult>
|
|
// Forget a workspace from Orca only (no remote Git/FS work) — for workspaces pinned to a removed/disconnected SSH host.
|
|
forgetLocal: (args: {
|
|
worktreeId: string
|
|
hostId?: ExecutionHostId
|
|
snapshotPruneBatchId?: string
|
|
}) => Promise<RemoveWorktreeResult>
|
|
forceDeletePreservedBranch: (args: {
|
|
worktreeId: string
|
|
branchName: string
|
|
expectedHead: string
|
|
hostId?: ExecutionHostId
|
|
}) => Promise<ForceDeleteWorktreeBranchResult>
|
|
updateMeta: (args: {
|
|
worktreeId: string
|
|
executionHostId?: ExecutionHostId
|
|
updates: Partial<WorktreeMeta>
|
|
}) => Promise<Worktree>
|
|
listLineage: () => Promise<{
|
|
lineage: Record<string, WorktreeLineage>
|
|
workspaceLineage?: Record<string, WorkspaceLineage>
|
|
}>
|
|
listLineageForHost?: (args: ListDesktopLineageForHostArgs) => Promise<HostLineageSnapshot>
|
|
updateLineage: (args: {
|
|
worktreeId: string
|
|
parentWorktreeId?: string
|
|
noParent?: boolean
|
|
}) => Promise<WorktreeLineage | null>
|
|
persistSortOrder: (args: { orderedIds: string[] }) => Promise<void>
|
|
/** Full CLI output of the last branch auto-rename generation failure, held
|
|
* in main memory only — null after a restart or once the failure clears. */
|
|
getBranchRenameFailureOutput: (args: { worktreeId: string }) => Promise<string | null>
|
|
onChanged: (callback: (data: { repoId: string }) => void) => () => void
|
|
onGitStatusMetadataChanged: (callback: (data: { repoId: string }) => void) => () => void
|
|
onHeadIdentitiesChanged: (
|
|
callback: (data: { repoId: string; identities: WorktreeHeadIdentity[] }) => void
|
|
) => () => void
|
|
onBaseStatus: (callback: (data: WorktreeBaseStatusEvent) => void) => () => void
|
|
onRemoteBranchConflict: (
|
|
callback: (data: WorktreeRemoteBranchConflictEvent) => void
|
|
) => () => void
|
|
}
|
|
|
|
export type FolderWorkspacesApi = {
|
|
list: () => Promise<FolderWorkspace[]>
|
|
getPathStatus: (args: FolderWorkspacePathStatusRequest) => Promise<FolderWorkspacePathStatus>
|
|
create: (args: {
|
|
projectGroupId: string
|
|
name?: string
|
|
folderPath?: string | null
|
|
connectionId?: string | null
|
|
linkedTask?: FolderWorkspace['linkedTask']
|
|
createdWithAgent?: FolderWorkspace['createdWithAgent']
|
|
pendingFirstAgentMessageRename?: boolean
|
|
}) => Promise<FolderWorkspace>
|
|
update: (args: {
|
|
folderWorkspaceId: string
|
|
updates: Partial<
|
|
Pick<
|
|
FolderWorkspace,
|
|
| 'name'
|
|
| 'folderPath'
|
|
| 'linkedTask'
|
|
| 'comment'
|
|
| 'isArchived'
|
|
| 'isUnread'
|
|
| 'isPinned'
|
|
| 'sortOrder'
|
|
| 'manualOrder'
|
|
| 'workspaceStatus'
|
|
| 'createdWithAgent'
|
|
| 'pendingFirstAgentMessageRename'
|
|
| 'firstAgentMessageRenameError'
|
|
| 'lastActivityAt'
|
|
| 'diffComments'
|
|
>
|
|
>
|
|
}) => Promise<FolderWorkspace | null>
|
|
delete: (args: { folderWorkspaceId: string }) => Promise<boolean>
|
|
}
|
|
|
|
export type SparsePresetsApi = {
|
|
list: (args: { repoId: string }) => Promise<SparsePreset[]>
|
|
save: (args: {
|
|
repoId: string
|
|
id?: string
|
|
name: string
|
|
directories: string[]
|
|
}) => Promise<SparsePreset>
|
|
remove: (args: { repoId: string; presetId: string }) => Promise<void>
|
|
onChanged: (callback: (data: { repoId: string }) => void) => () => void
|
|
}
|