fix(cli): report which hosts a worktree listing covered, and stop the cap starving remote ones (#18417)

`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.
This commit is contained in:
Neil
2026-09-03 14:43:09 -07:00
committed by GitHub
parent 9bed758e36
commit 95eed52801
22 changed files with 895 additions and 65 deletions
+14 -6
View File
@@ -32,6 +32,10 @@ import {
getOptionalStringFlag,
getRequiredStringFlag
} from '../flags'
import {
annotateOmittedHostScope,
type WithAnnotatedHostScope
} from '../omitted-host-scope-selectors'
import { RuntimeClientError } from '../runtime-client'
import {
getBrowserWorktreeSelector,
@@ -90,12 +94,16 @@ const terminalFocusHandler: CommandHandler = async ({ flags, client, cwd, json }
export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
'terminal list': async ({ flags, client, cwd, json }) => {
const result = await client.call<RuntimeTerminalListResult>('terminal.list', {
worktree: await getOptionalWorktreeSelector(flags, 'worktree', cwd, client),
limit: getOptionalPositiveIntegerFlag(flags, 'limit'),
// Why: agent JSON calls dominate; topology stays available through an explicit opt-in.
includeVisualLayouts: !json || flags.has('include-visual-layouts')
})
const result = await client.call<WithAnnotatedHostScope<RuntimeTerminalListResult>>(
'terminal.list',
{
worktree: await getOptionalWorktreeSelector(flags, 'worktree', cwd, client),
limit: getOptionalPositiveIntegerFlag(flags, 'limit'),
// Why: agent JSON calls dominate; topology stays available through an explicit opt-in.
includeVisualLayouts: !json || flags.has('include-visual-layouts')
}
)
await annotateOmittedHostScope(client, result.result)
printResult(result, json, formatTerminalList)
},
'terminal show': async ({ flags, client, cwd, json }) => {
+17 -7
View File
@@ -7,6 +7,10 @@ import type {
} from '../../shared/runtime-types'
import type { CommandHandler } from '../dispatch'
import { formatWorktreeList, formatWorktreePs, formatWorktreeShow, printResult } from '../format'
import {
annotateOmittedHostScope,
type WithAnnotatedHostScope
} from '../omitted-host-scope-selectors'
import { RuntimeClientError } from '../runtime-client'
import {
getOptionalNullableNumberFlag,
@@ -171,16 +175,22 @@ async function getCreateRepoSelector(
export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
'worktree ps': async ({ flags, client, json }) => {
const result = await client.call<RuntimeWorktreePsResult>('worktree.ps', {
limit: getOptionalPositiveIntegerFlag(flags, 'limit')
})
const result = await client.call<WithAnnotatedHostScope<RuntimeWorktreePsResult>>(
'worktree.ps',
{ limit: getOptionalPositiveIntegerFlag(flags, 'limit') }
)
await annotateOmittedHostScope(client, result.result)
printResult(result, json, formatWorktreePs)
},
'worktree list': async ({ flags, client, json }) => {
const result = await client.call<RuntimeWorktreeListResult>('worktree.list', {
repo: getOptionalStringFlag(flags, 'repo'),
limit: getOptionalPositiveIntegerFlag(flags, 'limit')
})
const result = await client.call<WithAnnotatedHostScope<RuntimeWorktreeListResult>>(
'worktree.list',
{
repo: getOptionalStringFlag(flags, 'repo'),
limit: getOptionalPositiveIntegerFlag(flags, 'limit')
}
)
await annotateOmittedHostScope(client, result.result)
printResult(result, json, formatWorktreeList)
},
'worktree show': async ({ flags, client, cwd, json }) => {
@@ -0,0 +1,246 @@
import { describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
listEnvironments: listEnvironmentsMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main } from './index'
import { okFixture, queueFixtures } from './test-fixtures'
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
const TERMINAL_ROW = {
handle: 'term_1',
ptyId: 'pty-1',
worktreeId: 'repo::/wt',
worktreePath: '/wt',
branch: 'main',
tabId: 'tab-1',
leafId: 'leaf-1',
title: 'worker',
connected: true,
writable: true,
lastOutputAt: null,
preview: '',
executionHostId: 'local'
}
describe('omittedHostIds selector annotation', () => {
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('marks a stale runtime host that no caller can select', async () => {
// Why: `omittedHostIds` is built from the runtime's own bookkeeping, so it names `runtime:`
// ids for servers that are no longer paired. An agent looping over the list to complete a
// partial listing hard-errors on those — 6 of 9 in the recorded QA run.
pairRuntimeEnvironment(listEnvironmentsMock, 'env-paired', 'm4air')
queueFixtures(
callMock,
okFixture('req_terminal_list', {
terminals: [TERMINAL_ROW],
totalCount: 1,
truncated: false,
hostScope: {
hostIds: ['local'],
omittedHostIds: ['runtime:env-paired', 'runtime:env-retired']
}
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['terminal', 'list', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.result.hostScope.omittedHostIds).toEqual([
'runtime:env-paired',
'runtime:env-retired'
])
expect(printed.result.hostScope.omittedHostSelectors).toEqual([
{ hostId: 'runtime:env-paired', selector: '--environment m4air' },
{ hostId: 'runtime:env-retired', selector: null }
])
})
it('says which omitted hosts are not selectable in the human listing', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-paired', 'm4air')
queueFixtures(
callMock,
okFixture('req_terminal_list', {
terminals: [TERMINAL_ROW],
totalCount: 1,
truncated: false,
hostScope: {
hostIds: ['local'],
omittedHostIds: ['runtime:env-paired', 'runtime:env-retired']
}
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['terminal', 'list'], '/tmp/repo')
const printed = String(logSpy.mock.calls[0]?.[0])
expect(printed).toContain('runtime:env-paired (--environment m4air)')
expect(printed).toContain('runtime:env-retired (not selectable from this machine)')
})
it('resolves an omitted SSH host against the targets the runtime actually knows', async () => {
listEnvironmentsMock.mockReturnValue([])
queueFixtures(
callMock,
okFixture('req_terminal_list', {
terminals: [TERMINAL_ROW],
totalCount: 1,
truncated: false,
hostScope: { hostIds: ['local'], omittedHostIds: ['ssh:box-1', 'ssh:box-gone'] }
}),
okFixture('req_ssh_targets', { targets: [{ id: 'box-1', label: 'openclaw' }] })
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['terminal', 'list', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.result.hostScope.omittedHostSelectors).toEqual([
{ hostId: 'ssh:box-1', selector: '--host ssh:box-1' },
{ hostId: 'ssh:box-gone', selector: null }
])
})
it('never keeps a host id out of omittedHostIds', async () => {
// Why: filtering the unreachable ones would shrink what the listing admits it did not cover.
// The gap is real whether or not this machine can name the host that owns it.
listEnvironmentsMock.mockReturnValue([])
queueFixtures(
callMock,
okFixture('req_terminal_list', {
terminals: [],
totalCount: 0,
truncated: false,
hostScope: { hostIds: [], omittedHostIds: ['runtime:env-retired'] }
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['terminal', 'list', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.result.hostScope.omittedHostIds).toEqual(['runtime:env-retired'])
})
it('costs no extra round trip when nothing was omitted', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_list', {
terminals: [TERMINAL_ROW],
totalCount: 1,
truncated: false,
hostScope: { hostIds: ['local'], omittedHostIds: [] }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['terminal', 'list', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledTimes(1)
})
})
describe('worktree listings report their host coverage', () => {
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('prints a host column and the scope line for `worktree list`', async () => {
listEnvironmentsMock.mockReturnValue([])
queueFixtures(
callMock,
okFixture('req_worktree_list', {
worktrees: [
{
id: 'repo-ssh::/remote/wt',
branch: 'main',
path: '/remote/wt',
hostId: 'ssh:box-1',
displayName: 'remote',
parentWorktreeId: null,
childWorktreeIds: [],
linkedIssue: null,
comment: ''
}
],
totalCount: 521,
truncated: true,
hostScope: { hostIds: ['ssh:box-1'], omittedHostIds: ['runtime:env-retired'] }
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'list'], '/tmp/repo')
const printed = String(logSpy.mock.calls[0]?.[0])
expect(printed).toContain('host=ssh:box-1')
expect(printed).toContain('scope: ssh:box-1')
expect(printed).toContain('runtime:env-retired (not selectable from this machine)')
expect(printed).toContain('truncated: showing 1 of 521')
})
it('does not claim a scope for `worktree ps` when the host reported none', async () => {
queueFixtures(
callMock,
okFixture('req_worktree_ps', { worktrees: [], totalCount: 0, truncated: false })
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'ps'], '/tmp/repo')
const printed = String(logSpy.mock.calls[0]?.[0])
expect(printed).toContain('scope: unverifiable')
})
})
@@ -88,7 +88,10 @@ describe('orca terminal list host scope', () => {
expect(printed.result.terminals[0].executionHostId).toBe('ssh:box-1')
expect(printed.result.hostScope).toEqual({
hostIds: ['ssh:box-1'],
omittedHostIds: ['local']
omittedHostIds: ['local'],
// The CLI annotates each omitted host with the flag that reaches it; see
// index-omitted-host-scope-selectors.test.ts.
omittedHostSelectors: [{ hostId: 'local', selector: '--host local' }]
})
})
+126
View File
@@ -0,0 +1,126 @@
import {
parseExecutionHostId,
type ExecutionHostId,
type ParsedExecutionHost
} from '../shared/execution-host'
import type { RuntimeListingHostScope } from '../shared/runtime-listing-host-scope'
import {
findEnvironmentByName,
findSshTargetByName,
listSshTargets,
type SshTargetSummary
} from './host-selector-alternatives'
import type { RuntimeClient } from './runtime-client'
export type OmittedHostScopeSelector = {
hostId: ExecutionHostId
/** The flag that routes a follow-up query to this host, or null when it names nothing here. */
selector: string | null
}
/** A host scope annotated on this machine. The runtime never sends `omittedHostSelectors`. */
export type ListingHostScopeWithSelectors = RuntimeListingHostScope & {
omittedHostSelectors?: OmittedHostScopeSelector[]
}
export type WithAnnotatedHostScope<TResult> = Omit<TResult, 'hostScope'> & {
hostScope?: ListingHostScopeWithSelectors
}
/**
* Resolves how to reach each host a listing did not cover.
*
* `hostScope` is the documented way to complete a partial listing, but `omittedHostIds` is built
* from the runtime's own bookkeeping — repos, folder workspaces, and workspace sessions — so it
* names `runtime:` ids for servers that are no longer paired. An agent looping over the list to
* finish the job hard-errors on those.
*
* The ids are kept rather than filtered: dropping one would shrink what the listing admits it did
* not cover, and `docs/reference/ssh-execution-boundary.md` requires a listing to name its gaps.
* A `null` selector marks the ones this machine cannot name, which is the part a caller needs.
* Only the local pairing store and SSH-target registry are consulted, so this answers "can I
* select it", never "is it up" — no host is claimed live or exited on this path.
*/
export async function resolveOmittedHostScopeSelectors(
client: RuntimeClient,
omittedHostIds: readonly ExecutionHostId[]
): Promise<OmittedHostScopeSelector[]> {
const parsed = omittedHostIds.map((hostId) => ({
hostId,
host: parseExecutionHostId(hostId)
}))
const environments = parsed.some((entry) => entry.host?.kind === 'runtime')
? await listPairedEnvironments()
: []
// Why: SSH targets need a round trip, so only pay for it when an ssh host was actually omitted.
const sshTargets = parsed.some((entry) => entry.host?.kind === 'ssh')
? await listSshTargets(client)
: []
return parsed.map(({ hostId, host }) => ({
hostId,
selector: resolveSelector(host, environments, sshTargets)
}))
}
async function listPairedEnvironments(): Promise<{ id: string; name: string }[]> {
const [{ listEnvironments }, { getDefaultUserDataPath }] = await Promise.all([
import('./runtime/environments.js'),
import('./runtime-client.js')
])
return listEnvironments(getDefaultUserDataPath()).map((environment) => ({
id: environment.id,
name: environment.name
}))
}
function resolveSelector(
host: ParsedExecutionHost | null,
environments: readonly { id: string; name: string }[],
sshTargets: readonly SshTargetSummary[]
): string | null {
if (host?.kind === 'local') {
return '--host local'
}
if (host?.kind === 'ssh') {
return findSshTargetByName(sshTargets, host.targetId) ? `--host ssh:${host.targetId}` : null
}
if (host?.kind === 'runtime') {
const environment = findEnvironmentByName(environments, host.environmentId)
return environment ? `--environment ${environment.name}` : null
}
return null
}
/** Renders a scope line; an absent scope means the host never reported one, not full coverage. */
export function formatListingHostScope(scope: ListingHostScopeWithSelectors | undefined): string {
if (!scope) {
return 'scope: unverifiable — this host does not report which hosts it lists'
}
const covered = scope.hostIds.length > 0 ? scope.hostIds.join(', ') : 'none'
if (scope.omittedHostIds.length === 0) {
return `scope: ${covered}`
}
const selectorByHostId = new Map(
(scope.omittedHostSelectors ?? []).map((entry) => [entry.hostId, entry.selector])
)
const omitted = scope.omittedHostIds.map((hostId) => {
if (!selectorByHostId.has(hostId)) {
return hostId
}
const selector = selectorByHostId.get(hostId)
return selector ? `${hostId} (${selector})` : `${hostId} (not selectable from this machine)`
})
return `scope: ${covered} — not covered: ${omitted.join(', ')}`
}
/** Attaches the resolved selectors in place; a listing with no omitted hosts pays nothing. */
export async function annotateOmittedHostScope(
client: RuntimeClient,
result: { hostScope?: ListingHostScopeWithSelectors }
): Promise<void> {
const scope = result.hostScope
if (!scope || scope.omittedHostIds.length === 0) {
return
}
scope.omittedHostSelectors = await resolveOmittedHostScopeSelectors(client, scope.omittedHostIds)
}
+5 -2
View File
@@ -1,5 +1,6 @@
import type { CommandSpec } from '../args'
import { GLOBAL_FLAGS } from '../args'
import { WORKTREE_LISTING_SCOPE_NOTES } from './worktree-listing-scope-notes'
import { SERVE_COMMAND_SPECS } from './serve'
import { TERMINAL_CLOSE_COMMAND_SPEC } from './terminal-close'
@@ -65,7 +66,8 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
path: ['worktree', 'list'],
summary: 'List Orca-managed worktrees',
usage: 'orca worktree list [--repo <selector>] [--limit <n>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'repo', 'limit']
allowedFlags: [...GLOBAL_FLAGS, 'repo', 'limit'],
notes: [...WORKTREE_LISTING_SCOPE_NOTES]
},
{
path: ['worktree', 'show'],
@@ -180,7 +182,8 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
path: ['worktree', 'ps'],
summary: 'Show a compact orchestration summary across worktrees',
usage: 'orca worktree ps [--limit <n>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'limit']
allowedFlags: [...GLOBAL_FLAGS, 'limit'],
notes: [...WORKTREE_LISTING_SCOPE_NOTES]
},
{
path: ['terminal', 'list'],
@@ -0,0 +1,6 @@
/** Shared by `worktree list` and `worktree ps`, which report host coverage the same way. */
export const WORKTREE_LISTING_SCOPE_NOTES: readonly string[] = [
'Each row carries the execution host that owns it (`host=`), and the trailing `scope:` line names every host the page covers plus the ones it does not.',
'A host named under `not covered` may still have workspaces; an empty answer for it is not evidence that it has none. Each is annotated with the flag that reaches it, or marked not selectable from this machine.',
'The row cap is shared across hosts, so a host whose rows sort last is not starved out of the page.'
]
+5 -15
View File
@@ -1,10 +1,10 @@
import { PTY_LIVE_NOTE, describeUnconfirmedStop } from '../shared/pty-liveness-verdict'
import { structuredChatPtyWriteRefusalCopy } from '../shared/agent-session-pty-write-refusal-copy'
import { formatListingHostScope, type WithAnnotatedHostScope } from './omitted-host-scope-selectors'
import type {
RuntimeTerminalClose,
RuntimeTerminalCreate,
RuntimeTerminalFocus,
RuntimeTerminalListHostScope,
RuntimeTerminalListResult,
RuntimeTerminalVisualLayout,
RuntimeTerminalVisualLayoutNode,
@@ -18,8 +18,10 @@ import type {
RuntimeTerminalWait
} from '../shared/runtime-types'
export function formatTerminalList(result: RuntimeTerminalListResult): string {
const scope = formatTerminalListHostScope(result.hostScope)
export function formatTerminalList(
result: WithAnnotatedHostScope<RuntimeTerminalListResult>
): string {
const scope = formatListingHostScope(result.hostScope)
if (result.terminals.length === 0) {
return `No terminals listed.\n${scope}`
}
@@ -37,18 +39,6 @@ export function formatTerminalList(result: RuntimeTerminalListResult): string {
: bodyWithScope
}
// Why: a listing that does not say what it covers reads as absolute, and an
// absent scope means the host is too old to know — not that it covered everything.
function formatTerminalListHostScope(scope: RuntimeTerminalListHostScope | undefined): string {
if (!scope) {
return 'scope: unverifiable — this host does not report which hosts it lists'
}
const covered = scope.hostIds.length > 0 ? scope.hostIds.join(', ') : 'none'
const omitted =
scope.omittedHostIds.length > 0 ? ` — not covered: ${scope.omittedHostIds.join(', ')}` : ''
return `scope: ${covered}${omitted}`
}
function formatTerminalVisualLayouts(
layouts: readonly RuntimeTerminalVisualLayout[] | undefined
): string | null {
+17 -10
View File
@@ -7,6 +7,7 @@ import type {
RuntimeWorktreeRecord
} from '../shared/runtime-types'
import type { MemorySnapshot, WorktreeMemory } from '../shared/process-stats-types'
import { formatListingHostScope, type WithAnnotatedHostScope } from './omitted-host-scope-selectors'
export function formatMemorySnapshot(snapshot: MemorySnapshot): string {
const topWorktrees = [...snapshot.worktrees].sort((a, b) => b.memory - a.memory).slice(0, 10)
@@ -130,19 +131,21 @@ export function formatEnvironment(environment: PublicKnownRuntimeEnvironment): s
].join('\n')
}
export function formatWorktreePs(result: RuntimeWorktreePsResult): string {
export function formatWorktreePs(result: WithAnnotatedHostScope<RuntimeWorktreePsResult>): string {
const scope = formatListingHostScope(result.hostScope)
if (result.worktrees.length === 0) {
return 'No worktrees found.'
return `No worktrees found.\n${scope}`
}
const body = result.worktrees
.map(
(worktree) =>
`${worktree.repo} ${worktree.branch} live:${worktree.liveTerminalCount} pty:${worktree.hasAttachedPty ? 'yes' : 'no'} unread:${worktree.unread ? 'yes' : 'no'}\n${worktree.path}${worktree.preview ? `\npreview: ${worktree.preview}` : ''}`
`${worktree.repo} ${worktree.branch} host=${worktree.hostId ?? 'unverifiable'} live:${worktree.liveTerminalCount} pty:${worktree.hasAttachedPty ? 'yes' : 'no'} unread:${worktree.unread ? 'yes' : 'no'}\n${worktree.path}${worktree.preview ? `\npreview: ${worktree.preview}` : ''}`
)
.join('\n\n')
const bodyWithScope = `${body}\n\n${scope}`
return result.truncated
? `${body}\n\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
: body
? `${bodyWithScope}\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
: bodyWithScope
}
export function formatRepoList(result: RuntimeRepoList): string {
@@ -168,19 +171,23 @@ export function formatRepoRefs(result: RuntimeRepoSearchRefs): string {
return result.truncated ? `${result.refs.join('\n')}\n\ntruncated: yes` : result.refs.join('\n')
}
export function formatWorktreeList(result: RuntimeWorktreeListResult): string {
export function formatWorktreeList(
result: WithAnnotatedHostScope<RuntimeWorktreeListResult>
): string {
const scope = formatListingHostScope(result.hostScope)
if (result.worktrees.length === 0) {
return 'No worktrees found.'
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)} ${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 ?? '')}`
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
? `${body}\n\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
: body
? `${bodyWithScope}\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
: bodyWithScope
}
export function formatWorktreeShow(result: { worktree: RuntimeWorktreeRecord }): string {