Files
orca/src/cli/workspace-format.ts
T
Jinwoo Hong 4fabde28b1 feat(cli): one enclosing-workspace rule, --environment local, copyable addresses, and --parent-environment (#39733)
* feat(cli): one enclosing-workspace rule, --environment local, and copyable workspace addresses

current/active and cwd inference now pick the innermost workspace of either kind (worktree or
folder workspace); at equal depth the caller terminal's own workspace wins, otherwise the choice
is refused as ambiguous. --environment local names this machine's own Orca over an ambient
ORCA_ENVIRONMENT or pairing code. worktree ps/show/current print an environment: line and an
address: line (JSON environment, address, addressUnavailable, parent) that always carries its
--environment, and name a parent's environment only when it is verified.

* test: answer the folder catalog read in agent launch cwd tests

* feat(cli): worktree rm refuses current/active and names the selector to pass

Removal needs an explicit workspace, as git worktree remove needs a path. The refusal runs before
rm decides between a worktree and a folder workspace, and reports what current resolves to now
(one selector, the ambiguous choices, or none) in text and as invalid_argument data.

* fix(cli): keep the remote-environment guidance when rm refuses current

* fix(cli): surface folder-catalog failures for omitted targets; no address for a duplicated folder parent

* fix(cli): surface a worktree-list failure for omitted browser and terminal targets too

* test(cli): folder rm refuses current/active before the folder removal path

* fix(cli): refuse a cwd owner whose folder host can't be derived instead of skipping it

* fix(cli): compare cwd owners by filesystem identity, refuse every equal-path tie, surface worktree-list failures

Paths on the caller's own filesystem compare by realpath (reusing resolveRealPathSync), so /tmp
and /private/tmp aliases find the deeper owner in both the folder prefilter and ranking; other
hosts' paths stay lexical. Equal-depth owners with different repo registrations now refuse without
a caller instead of picking by catalog order. An omitted target no longer swallows worktree.list
method_not_found; only the optional folder reads fall back.

* fix(cli): read the realpath helper from shared so the packaged CLI ships it

* refactor(cli): move the catalog row type and unresolved-owner refusal out of the selection/catalog cycle

* fix(cli): resolve every caller-filesystem workspace path, not only basename matches

A workspace registered through a differently named symlink was skipped by the basename gate.
Containment now realpaths cwd and each row on the caller's own filesystem once per command,
shared by the folder prefilter and ranking; folder hosts are derived first so another host's
paths are never resolved locally.

* test: answer the folder catalog read in the ambiguous orchestration-check caller test

* feat(cli): --parent-environment names where a worktree's parent lives

worktree create and worktree set take --parent-environment <local|saved id|name>
next to --parent-worktree. The parent is resolved in that environment only, and
sent to the child as a verified cross-store ref after the child's cross-machine
capability check. Without the flag, the parent stays in the child's environment;
active/current always mean this machine's own Orca.

Preflight failures happen before any change: environment_not_paired,
environment_unavailable, LINEAGE_PARENT_NOT_FOUND / LINEAGE_PARENT_AMBIGUOUS
naming the searched environment, and incompatible_runtime with data.environment
and data.requires.

* fix(cli): parent lookup errors name and list the environment searched

LINEAGE_PARENT_AMBIGUOUS and LINEAGE_PARENT_NOT_FOUND now always carry
data.environment and a listing command for the environment the parent was
looked up in, for parents in another environment and in the child's own. Steps
inherited from the runtime listed the caller's default route instead.

* docs(cli): a local address names the Orca that printed it
2026-11-08 04:08:07 -05:00

252 lines
9.5 KiB
TypeScript

import { formatAddressLines, formatParentLine, type WorkspaceAnnotation } from './workspace-address'
import type { PublicKnownRuntimeEnvironment } from '../shared/remote-runtime/host-catalog/runtime-environments'
import type {
RuntimeRepoList,
RuntimeRepoSearchRefs,
RuntimeWorktreeListResult,
RuntimeWorktreePsResult,
RuntimeWorktreeRecord
} from '../shared/runtime-types'
import type { MemorySnapshot, WorktreeMemory } from '../shared/process-stats-types'
import { formatListingHostScope, type WithAnnotatedHostScope } from './omitted-host-scope-selectors'
import { formatWorktreePsTerminalFields } from './worktree-ps-terminal-verdict'
import { formatWorktreePsProgramStatus } from './worktree-ps-program-status'
export function formatMemorySnapshot(snapshot: MemorySnapshot): string {
const topWorktrees = [...snapshot.worktrees].sort((a, b) => b.memory - a.memory).slice(0, 10)
const hostAvailable = snapshot.host.availableMemory ?? snapshot.host.freeMemory
const hostAvailableSource = snapshot.host.availableMemorySource ?? 'free-memory'
const lines = [
`collectedAt: ${new Date(snapshot.collectedAt).toISOString()}`,
`totalMemory: ${formatByteCount(snapshot.totalMemory)}`,
`processMemoryMetric: ${formatProcessMemoryMetric(snapshot.processMemoryMetric)}`,
...formatCommitLines(snapshot),
`totalCpu: ${formatCpu(snapshot.totalCpu)}`,
[
`hostUsed: ${formatByteCount(snapshot.host.usedMemory)}`,
`/ ${formatByteCount(snapshot.host.totalMemory)}`,
`(${snapshot.host.memoryUsagePercent.toFixed(1)}%)`
].join(' '),
[`hostAvailable: ${formatByteCount(hostAvailable)}`, `(${hostAvailableSource})`].join(' '),
[
`app: ${formatByteCount(snapshot.app.memory)}`,
`(main ${formatByteCount(snapshot.app.main.memory)},`,
`renderer ${formatByteCount(snapshot.app.renderer.memory)},`,
`other ${formatByteCount(snapshot.app.other.memory)})`
].join(' '),
`worktrees: ${snapshot.worktrees.length}`
]
if (topWorktrees.length === 0) {
lines.push('topWorktrees: none')
return lines.join('\n')
}
lines.push('', 'Top worktrees:')
for (const worktree of topWorktrees) {
lines.push(formatWorktreeMemoryLine(worktree))
}
if (snapshot.worktrees.length > topWorktrees.length) {
lines.push(`... ${snapshot.worktrees.length - topWorktrees.length} more worktrees`)
}
return lines.join('\n')
}
function formatWorktreeMemoryLine(worktree: WorktreeMemory): string {
return [
`- ${worktree.worktreeName}`,
`${formatByteCount(worktree.memory)}`,
...(worktree.privateMemory === undefined
? []
: [`${formatByteCount(worktree.privateMemory)} committed`]),
`${formatCpu(worktree.cpu)}`,
`${worktree.sessions.length} session${worktree.sessions.length === 1 ? '' : 's'}`
].join(' ')
}
// Missing commit fields mean unknown usage, not zero.
function formatCommitLines(snapshot: MemorySnapshot): string[] {
if (typeof snapshot.totalPrivateMemory !== 'number') {
return []
}
return [
`totalPrivateMemory: ${formatByteCount(snapshot.totalPrivateMemory)}`,
`processCommitMetric: ${formatProcessCommitMetric(snapshot.processCommitMetric)}`
]
}
function formatProcessCommitMetric(metric: MemorySnapshot['processCommitMetric']): string {
return metric === 'private-bytes'
? 'summed private bytes; committed memory, counted whether resident or paged out'
: `summed ${metric ?? 'unknown'}`
}
function formatCpu(cpu: number): string {
return `${cpu.toFixed(1)}%`
}
function formatProcessMemoryMetric(metric: MemorySnapshot['processMemoryMetric']): string {
if (metric === 'footprint') {
return 'summed memory footprint (Activity Monitor); includes graphics memory'
}
return metric === 'working-set'
? 'summed working set; shared pages may repeat'
: 'summed RSS; shared or aliased pages may repeat'
}
function formatByteCount(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) {
return '0 B'
}
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let value = bytes
let unitIndex = 0
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024
unitIndex += 1
}
const formatted = value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)
return `${formatted} ${units[unitIndex]}`
}
export function formatEnvironmentList(result: {
environments: PublicKnownRuntimeEnvironment[]
}): string {
if (result.environments.length === 0) {
return 'No saved environments.'
}
return result.environments
.map(
(environment) =>
`${environment.id} ${environment.name} ${environment.endpoints[0]?.endpoint ?? 'no-endpoint'}`
)
.join('\n')
}
export function formatEnvironment(environment: PublicKnownRuntimeEnvironment): string {
return [
`id: ${environment.id}`,
`name: ${environment.name}`,
`runtimeId: ${environment.runtimeId ?? 'unknown'}`,
`lastUsedAt: ${environment.lastUsedAt ?? 'never'}`,
`preferredEndpointId: ${environment.preferredEndpointId}`,
...environment.endpoints.map(
(endpoint) => `endpoint: ${endpoint.id} ${endpoint.kind} ${endpoint.endpoint}`
)
].join('\n')
}
type AnnotatedPsRow = RuntimeWorktreePsResult['worktrees'][number] & Partial<WorkspaceAnnotation>
export function formatWorktreePs(
result: WithAnnotatedHostScope<
Omit<RuntimeWorktreePsResult, 'worktrees'> & { worktrees: AnnotatedPsRow[] }
>
): string {
const scope = formatListingHostScope(result.hostScope)
if (result.worktrees.length === 0) {
return `No worktrees found.\n${scope}`
}
const body = result.worktrees
.map(
(worktree) =>
`${worktree.repo} ${worktree.branch} host=${worktree.hostId ?? 'unverifiable'} ${formatWorktreePsTerminalFields(worktree)} unread:${worktree.unread ? 'yes' : 'no'}${formatWorktreePsProgramStatus(worktree.agents)}\n${worktree.path}${formatWorktreePsLocation(worktree)}${worktree.preview ? `\npreview: ${worktree.preview}` : ''}`
)
.join('\n\n')
const bodyWithScope = `${body}\n\n${scope}`
return result.truncated
? `${bodyWithScope}\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
: bodyWithScope
}
function formatWorktreePsLocation(worktree: AnnotatedPsRow): string {
const annotation = workspaceAnnotationOf(worktree)
return annotation
? `${formatParentLine(annotation)}${formatAddressLines(annotation)}`
: formatWorktreePsParent(worktree)
}
/** The parent line, in the sidebar's precedence: worktree, then folder workspace, then another machine. */
function formatWorktreePsParent(worktree: RuntimeWorktreePsResult['worktrees'][number]): string {
const parent =
worktree.parentWorktreeId ??
worktree.workspaceLineage?.parentWorkspaceKey ??
(worktree.crossStoreParent
? `${worktree.crossStoreParent.parent.workspaceKey} on another machine`
: null)
return parent ? `\nparent: ${parent}` : ''
}
export function formatRepoList(result: RuntimeRepoList): string {
if (result.repos.length === 0) {
return 'No repos found.'
}
return result.repos.map((repo) => `${repo.id} ${repo.displayName} ${repo.path}`).join('\n')
}
export function formatRepoShow(result: { repo: Record<string, unknown> }): string {
return Object.entries(result.repo)
.map(
([key, value]) =>
`${key}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`
)
.join('\n')
}
export function formatRepoRefs(result: RuntimeRepoSearchRefs): string {
if (result.refs.length === 0) {
return 'No refs found.'
}
return result.truncated ? `${result.refs.join('\n')}\n\ntruncated: yes` : result.refs.join('\n')
}
export function formatWorktreeList(
result: WithAnnotatedHostScope<RuntimeWorktreeListResult>
): string {
const scope = formatListingHostScope(result.hostScope)
if (result.worktrees.length === 0) {
return `No worktrees found.\n${scope}`
}
const body = result.worktrees
.map((worktree) => {
const childCount = worktree.childWorktreeIds?.length ?? 0
return `${String(worktree.id)} ${String(worktree.branch)} host=${String(worktree.hostId ?? 'unverifiable')} ${String(worktree.path)}\ndisplayName: ${String(worktree.displayName ?? '')}\nparentWorktreeId: ${String(worktree.parentWorktreeId ?? 'null')}\nchildWorktreeIds: ${childCount > 0 ? worktree.childWorktreeIds.join(',') : '[]'}\nlinkedIssue: ${String(worktree.linkedIssue ?? 'null')}\ncomment: ${String(worktree.comment ?? '')}`
})
.join('\n\n')
const bodyWithScope = `${body}\n\n${scope}`
return result.truncated
? `${bodyWithScope}\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
: bodyWithScope
}
const ANNOTATION_KEYS = new Set(['environment', 'address', 'addressUnavailable', 'parent'])
function workspaceAnnotationOf(record: Partial<WorkspaceAnnotation>): WorkspaceAnnotation | null {
const { environment, address, addressUnavailable, parent } = record
return environment
? {
environment,
address: address ?? null,
parent: parent ?? null,
...(addressUnavailable ? { addressUnavailable } : {})
}
: null
}
export function formatWorktreeShow(result: {
worktree: RuntimeWorktreeRecord & Partial<WorkspaceAnnotation>
}): string {
const worktree = result.worktree
const annotation = workspaceAnnotationOf(worktree)
const fields = Object.entries(worktree)
.filter(([key]) => !annotation || !ANNOTATION_KEYS.has(key))
.map(
([key, value]) =>
`${key}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`
)
.join('\n')
return annotation
? `${fields}${formatParentLine(annotation)}${formatAddressLines(annotation)}`
: fields
}