Files
orca/src/main/runtime/rpc/methods/worktree.ts
T
1f8dce85d4 refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code (#26895)
* refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code

- One admission prologue and one unary invocation shared by both RPC dispatchers
- Delete rpc schema re-export shims; import from the shared params catalog
- Delete the unregistered duplicate markdown tab method module
- Collapse multiplex installer stage types; add isMultiplexStreamAttached
- One source-identity guard in the ACK recovery snapshot
- Single stream teardown in the renderer multiplexer
- De-duplicate RPC method declarations; derive dispatch options from RpcContext
- Share viewport-frame decoding and one head-trim loop for pending queues
- Single sendTerminal call in sendTerminalStreamInput

* test: point cross-version and scope checks at the moved browser tab-create schema; run transport parity test under Node

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:50:31 -07:00

317 lines
13 KiB
TypeScript

import {
finishAutomationWorkspaceProvenanceRequest,
releaseAutomationWorkspaceProvenanceRequest,
resolveAutomationWorkspaceProvenance
} from '../../../automations/workspace-provenance'
import { getLocalWorktreeCatalogVersion } from '../../../local-worktree-scan-generation'
import { getExplicitWorktreeIdSelector } from '../../runtime-worktree-selection'
import { splitWorktreeId } from '../../../../shared/worktree/id'
import { buildCliWorkspaceProvenance } from '../../../../shared/cli-workspace-provenance'
import { displayNameUpdatePinsLabel } from '../../../../shared/worktree/display-name-provenance'
import { createWorktreeWithStartupAgent } from '../../../agent-launch/startup-agent-worktree-create'
import { defineMethod } from '../core'
import { buildManagedWorktreeCreateArgs } from './worktree-create-args'
import { resolvePairedCallerHostId } from './paired-caller-host-id'
import { resolveRuntimeNavigationTarget } from '../../../../shared/runtime-navigation'
import { resolveRpcWorkspaceCreatorProvenance } from '../workspace-creator-context'
import {
WorktreeCreate,
WorktreePrefetchCreateBase
} from '../../../../shared/rpc-contract/worktree-create-params'
import {
WorktreeActivate,
WorktreeForceDeleteBranch,
WorktreeRemove,
WorktreeResolveMrBase,
WorktreeResolvePrBase,
WorktreeSelector,
WorktreeSet,
WorktreeSortOrder,
WorktreeTeardownMissingTerminalsParams
} from '../../../../shared/rpc-contract/worktree-params'
import { WORKTREE_CATALOG_METHODS } from './worktree-catalog-methods'
import { readsWorktreeRemovalMarker } from '../worktree-removal-marker-projection'
export const WORKTREE_METHODS = [
...WORKTREE_CATALOG_METHODS,
defineMethod({
name: 'worktree.teardownMissingTerminals',
permission: 'workspace',
params: WorktreeTeardownMissingTerminalsParams,
handler: async (params, { runtime }) =>
runtime.teardownMissingManagedWorktreeTerminals(
params.repo,
params.worktreeIds,
params.connectionId
)
}),
defineMethod({
name: 'worktree.lineageList',
permission: 'workspace',
params: null,
handler: async (_params, { runtime }) => ({
lineage: await runtime.listWorktreeLineage(),
workspaceLineage: await runtime.listWorkspaceLineage()
})
}),
defineMethod({
name: 'worktree.show',
permission: 'workspace',
params: WorktreeSelector,
handler: async (params, { runtime }) => ({
worktree: await runtime.showManagedWorktree(params.worktree)
})
}),
defineMethod({
name: 'worktree.sleep',
permission: 'workspace',
params: WorktreeSelector,
handler: async (params, { runtime }) => runtime.sleepManagedWorktree(params.worktree)
}),
defineMethod({
name: 'worktree.activate',
permission: 'workspace',
params: WorktreeActivate,
handler: async (params, { runtime, clientKind }) =>
// Why: clientKind ('mobile'|'runtime') scopes the host-renderer slept-agent
// wake to phones so web/desktop activation behavior is unchanged.
runtime.activateManagedWorktree(params.worktree, {
notifyClients: params.notifyClients !== false,
clientKind,
navigation: resolveRuntimeNavigationTarget({
navigation: params.navigation,
notifyClients: params.notifyClients,
clientKind,
defaultTarget: 'host'
})
})
}),
defineMethod({
name: 'worktree.create',
permission: 'workspace',
params: WorktreeCreate,
handler: async (params, context) =>
// Why: a mobile create interrupted by a connection migration is retried with
// the same clientMutationId; dedupe so the host returns the in-flight/created
// worktree instead of spawning a duplicate. No key (desktop/CLI) runs plainly.
context.runtime.dedupeWorktreeCreate(params.repo, params.clientMutationId, async () => {
const { runtime } = context
const repo = await runtime.showRepo(params.repo)
const automationProvenance = resolveAutomationWorkspaceProvenance({
authority: runtime,
repoSelector: params.repo,
repo,
request: params.automationProvenanceRequest
})
// Why: provenance tokens are reserved before creation so retries can recover,
// but failed create attempts must release the reservation for a safe retry.
try {
const result = await createWorktreeWithStartupAgent(
runtime,
buildManagedWorktreeCreateArgs(
params,
{
automationProvenance,
cliProvenance: buildCliWorkspaceProvenance(params.cliProvenanceRequest, {
startupAgent: params.startupAgent ?? params.createdWithAgent,
createdAt: Date.now()
}),
creatorProvenance: resolveRpcWorkspaceCreatorProvenance(context)
},
context.clientKind ? { clientKind: context.clientKind } : {}
)
)
finishAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
// Why stamped here: the create's change notification has bumped the generation, so this
// names the catalog that contains the new worktree.
const stamped = { ...result, catalogVersion: getLocalWorktreeCatalogVersion(repo.id) }
// Why: agent callers need a stable dispatch target without traversing
// terminal-list layout duplicates after creating the worktree.
return params.startupAgent && result.startupTerminal?.handle
? { ...stamped, agentTerminalHandle: result.startupTerminal.handle }
: stamped
} catch (error) {
releaseAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
throw error
}
})
}),
defineMethod({
name: 'worktree.prefetchCreateBase',
permission: 'workspace',
params: WorktreePrefetchCreateBase,
handler: async (params, { runtime }) => {
await runtime.prefetchManagedWorktreeCreateBase({
repoSelector: params.repo,
baseBranch: params.baseBranch
})
return null
}
}),
defineMethod({
name: 'worktree.set',
permission: 'workspace',
params: WorktreeSet,
handler: async (params, { runtime }) => ({
worktree: await runtime.updateManagedWorktreeMeta(params.worktree, {
displayName: params.displayName,
...(params.displayName !== undefined
? { displayNameIsPinned: displayNameUpdatePinsLabel(params.displayName) }
: {}),
linkedIssue: params.linkedIssue,
linkedPR: params.linkedPR,
suppressedGitHubPR: params.suppressedGitHubPR,
linkedLinearIssue: params.linkedLinearIssue,
linkedLinearIssueWorkspaceId: params.linkedLinearIssueWorkspaceId,
linkedLinearIssueOrganizationUrlKey: params.linkedLinearIssueOrganizationUrlKey,
linkedGitLabMR: params.linkedGitLabMR,
linkedGitLabIssue: params.linkedGitLabIssue,
linkedBitbucketPR: params.linkedBitbucketPR,
linkedAzureDevOpsPR: params.linkedAzureDevOpsPR,
linkedGiteaPR: params.linkedGiteaPR,
linkedWorkItem: params.linkedWorkItem,
linkedItems: params.linkedItems,
...(params.linkedItemsBase !== undefined
? { linkedItemsBase: params.linkedItemsBase }
: {}),
...(params.linkedItemsSelectionChanged !== undefined
? { linkedItemsSelectionChanged: params.linkedItemsSelectionChanged }
: {}),
linkedTaskSourceContext: params.linkedTaskSourceContext,
comment: params.comment,
isArchived: params.isArchived,
isUnread: params.isUnread,
isPinned: params.isPinned,
sortOrder: params.sortOrder,
manualOrder: params.manualOrder,
lastActivityAt: params.lastActivityAt,
createdAt: params.createdAt,
sparseDirectories: params.sparseDirectories,
sparseBaseRef: params.sparseBaseRef,
sparsePresetId: params.sparsePresetId,
baseRef: params.baseRef,
workspaceStatus: params.workspaceStatus,
pushTarget: params.pushTarget,
diffComments: params.diffComments,
mobileDiffReview: params.mobileDiffReview,
lineage:
params.parentWorktree || params.noParent === true
? {
parentWorktree: params.parentWorktree,
noParent: params.noParent === true
}
: undefined
})
})
}),
defineMethod({
name: 'worktree.persistSortOrder',
permission: 'workspace',
params: WorktreeSortOrder,
handler: async (params, { runtime }) =>
runtime.persistManagedWorktreeSortOrder(params.orderedIds)
}),
defineMethod({
name: 'worktree.resolvePrBase',
permission: 'workspace',
params: WorktreeResolvePrBase,
handler: async (params, { runtime }) =>
runtime.resolveManagedPrBase({
repoSelector: params.repo,
prNumber: params.prNumber,
headRefName: params.headRefName,
baseRefName: params.baseRefName,
isCrossRepository: params.isCrossRepository
})
}),
defineMethod({
name: 'worktree.resolveMrBase',
permission: 'workspace',
params: WorktreeResolveMrBase,
handler: async (params, { runtime }) =>
runtime.resolveManagedMrBase({
repoSelector: params.repo,
mrIid: params.mrIid,
sourceBranch: params.sourceBranch,
targetBranch: params.targetBranch,
isCrossRepository: params.isCrossRepository
})
}),
defineMethod({
name: 'worktree.rm',
permission: 'workspace',
params: WorktreeRemove,
handler: async (params, context) => {
const { runtime } = context
// Translate a paired client's runtime-local host spelling before host-qualified reads.
let resolvedHostId = resolvePairedCallerHostId(
() => runtime.listRepos(),
params.worktree,
params.hostId
)
// Older mobile clients omit hostId, so resolve through the ambiguity gate
// before pinning removal. An ambiguous selector still fails closed: two
// hosts own the id and an unqualified client cannot say which it meant.
if (!resolvedHostId) {
try {
resolvedHostId = (await runtime.showManagedWorktree(params.worktree)).hostId
if (!resolvedHostId) {
throw new Error('worktree.rm could not resolve the workspace host')
}
} catch (error) {
// 'selector_not_found' is not a failure to attribute — Git simply no
// longer lists the path. A delete legitimately arrives in that state and
// `removeManagedWorktree` handles it, so a stale workspace stays
// deletable by a client that sends no host. Anything else propagates.
if (!(error instanceof Error) || error.message !== 'selector_not_found') {
throw error
}
}
}
// Why parsed, not resolved: an `id:` selector (what clients send) names its repo, and a second
// resolution costs a scan and throws for an id two hosts share. Other selectors stay unstamped.
const explicitWorktreeId = getExplicitWorktreeIdSelector(params.worktree)
const repoId = explicitWorktreeId ? splitWorktreeId(explicitWorktreeId)?.repoId : undefined
const result = await runtime.removeManagedWorktree(params.worktree, {
force: params.force === true,
runHooks: params.runHooks === true,
allowUnverifiedPtyStop: params.allowUnverifiedPtyStop === true,
allowFailedArchiveHook: params.allowFailedArchiveHook === true,
...(resolvedHostId ? { hostId: resolvedHostId } : {}),
// Why: only a client that shows the `removing` marker can wait out Git's delete; older
// clients get the acceptance and never see the row again.
...(readsWorktreeRemovalMarker(context) ? { waitForBackgroundRemoval: true } : {})
})
return {
removed: true,
...result,
...(repoId ? { catalogVersion: getLocalWorktreeCatalogVersion(repoId) } : {})
}
}
}),
defineMethod({
name: 'worktree.forceDeleteBranch',
permission: 'workspace',
params: WorktreeForceDeleteBranch,
handler: async (params, { runtime }) => {
const hostId = resolvePairedCallerHostId(
() => runtime.listRepos(),
params.worktree,
params.hostId
)
return hostId
? runtime.forceDeletePreservedBranch(
params.worktree,
params.branchName,
params.expectedHead,
hostId
)
: runtime.forceDeletePreservedBranch(
params.worktree,
params.branchName,
params.expectedHead
)
}
})
]