mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 00:03:37 +00:00
fix(terminal): preserve unverifiable host scope
This commit is contained in:
@@ -19,7 +19,7 @@ import type {
|
||||
export function formatTerminalList(result: RuntimeTerminalListResult): string {
|
||||
const scope = formatTerminalListHostScope(result.hostScope)
|
||||
if (result.terminals.length === 0) {
|
||||
return `No live terminals.\n${scope}`
|
||||
return `No terminals listed.\n${scope}`
|
||||
}
|
||||
const body = result.terminals
|
||||
.map(
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('formatTerminalList scope declaration', () => {
|
||||
})
|
||||
)
|
||||
|
||||
expect(output).toContain('No live terminals')
|
||||
expect(output).toContain('No terminals listed')
|
||||
expect(output).toContain('scope: local')
|
||||
expect(output).toContain('not covered: ssh:box-1')
|
||||
})
|
||||
|
||||
+24
-6
@@ -17,7 +17,11 @@ import type { PtyBindingSourceExpectation, Store } from '../persistence'
|
||||
import { retireTerminalSurfaceFromPersistence } from '../runtime/mobile-session-terminal-persistence-retirement'
|
||||
import type { GlobalSettings } from '../../shared/global-settings-types'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { toSshExecutionHostId } from '../../shared/execution-host'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../shared/execution-host'
|
||||
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
|
||||
import { terminalOutputBacklogCapChars } from '../../shared/terminal-scrollback-policy'
|
||||
import type {
|
||||
@@ -72,6 +76,7 @@ import {
|
||||
} from '../../shared/pi-agent-kind'
|
||||
import { isPwshAvailableAsync } from '../pwsh'
|
||||
import { LocalPtyProvider } from '../providers/local-pty-provider'
|
||||
import type { PtyProcessInfo } from '../providers/pty-process-info'
|
||||
import { normalizeWindowsTerminalCwd } from '../providers/windows-shell-args'
|
||||
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
|
||||
import { isPtyWriteUnavailableError } from '../providers/pty-write-unavailable-error'
|
||||
@@ -259,6 +264,22 @@ function registeredPtyProviders(): RegisteredPtyProvider[] {
|
||||
]
|
||||
}
|
||||
|
||||
async function listRegisteredPtyProcessesWithHostScope(): Promise<{
|
||||
processes: PtyProcessInfo[]
|
||||
hostIds: ExecutionHostId[]
|
||||
}> {
|
||||
const providers = registeredPtyProviders()
|
||||
const providerSessions = await Promise.all(
|
||||
providers.map(({ provider }) => provider.listProcesses())
|
||||
)
|
||||
return {
|
||||
processes: providerSessions.flat(),
|
||||
hostIds: providers.map(({ connectionId }) =>
|
||||
connectionId ? toSshExecutionHostId(connectionId) : LOCAL_EXECUTION_HOST_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const SYNTHETIC_KILL_EXIT_DUPLICATE_WINDOW_MS = 30_000
|
||||
// Why: kill switch — flip to disable producer flow control (pause/resume) without untangling the wiring.
|
||||
const PRODUCER_FLOW_CONTROL_ENABLED = true
|
||||
@@ -5747,12 +5768,9 @@ export function registerPtyHandlers(
|
||||
if (connectionId !== undefined) {
|
||||
return getProvider(connectionId).listProcesses()
|
||||
}
|
||||
const providerSessions = await Promise.all([
|
||||
localProvider.listProcesses(),
|
||||
...Array.from(sshProviders.values(), (provider) => provider.listProcesses())
|
||||
])
|
||||
return providerSessions.flat()
|
||||
return (await listRegisteredPtyProcessesWithHostScope()).processes
|
||||
},
|
||||
listProcessesWithHostScope: listRegisteredPtyProcessesWithHostScope,
|
||||
serializeBuffer: (ptyId, opts) => {
|
||||
// Why: mobile xterm must start from the desktop's exact screen state/dimensions before live TUI chunks render correctly.
|
||||
return requestSerializedBuffer(ptyId, opts)
|
||||
|
||||
@@ -1869,6 +1869,10 @@ type RuntimePtyController = {
|
||||
// Why: exact-id mobile polls should not enumerate every local and SSH PTY.
|
||||
hasPty?(ptyId: string): boolean | null
|
||||
listProcesses?(connectionId?: string | null): Promise<PtyProcessInfo[]>
|
||||
listProcessesWithHostScope?(): Promise<{
|
||||
processes: PtyProcessInfo[]
|
||||
hostIds: ExecutionHostId[]
|
||||
}>
|
||||
serializeBuffer?(
|
||||
ptyId: string,
|
||||
opts?: { scrollbackRows?: number; altScreenForcesZeroRows?: boolean }
|
||||
@@ -1913,6 +1917,7 @@ type PtyControllerInventory = Readonly<{
|
||||
// must consult the unscoped inventory or a misattributed live PTY reads as dead.
|
||||
allLivePtyIds: ReadonlySet<string>
|
||||
terminalIdentityByPtyId: ReadonlyMap<string, PtyControllerTerminalIdentity>
|
||||
queriedHostIds: ReadonlySet<ExecutionHostId>
|
||||
}>
|
||||
|
||||
type WorktreeStartupDraftPaste = {
|
||||
@@ -5818,16 +5823,38 @@ export class OrcaRuntimeService {
|
||||
return worktreeIds
|
||||
}
|
||||
|
||||
// Every execution host this runtime can answer for; 'local' is always one of
|
||||
// them because the runtime itself runs somewhere.
|
||||
private listKnownExecutionHostIds(): Set<ExecutionHostId> {
|
||||
// Every execution host known to this runtime; knowledge is not coverage.
|
||||
private listKnownExecutionHostIds(
|
||||
additionalHostIds: Iterable<ExecutionHostId> = [],
|
||||
includeConfiguredHosts = true
|
||||
): Set<ExecutionHostId> {
|
||||
const hostIds = new Set<ExecutionHostId>([LOCAL_EXECUTION_HOST_ID])
|
||||
for (const repo of this.store?.getRepos?.() ?? []) {
|
||||
hostIds.add(getRepoExecutionHostId(repo))
|
||||
}
|
||||
for (const hostId of this.store?.getWorkspaceSessionHostIds?.() ?? []) {
|
||||
hostIds.add(hostId)
|
||||
}
|
||||
for (const hostId of additionalHostIds) {
|
||||
hostIds.add(hostId)
|
||||
}
|
||||
if (!includeConfiguredHosts) {
|
||||
return hostIds
|
||||
}
|
||||
const repos = this.store?.getRepos?.() ?? []
|
||||
for (const repo of repos) {
|
||||
hostIds.add(getRepoExecutionHostId(repo))
|
||||
}
|
||||
const projectGroups = this.store?.getProjectGroups?.() ?? []
|
||||
for (const workspace of this.store?.getFolderWorkspaces?.() ?? []) {
|
||||
const connection = inferFolderWorkspacePathConnection({
|
||||
folderPath: workspace.folderPath,
|
||||
projectGroupId: workspace.projectGroupId,
|
||||
connectionId: workspace.connectionId ?? null,
|
||||
projectGroups,
|
||||
repos
|
||||
})
|
||||
if (connection.kind === 'ssh') {
|
||||
hostIds.add(toSshExecutionHostId(connection.connectionId))
|
||||
}
|
||||
}
|
||||
return hostIds
|
||||
}
|
||||
|
||||
@@ -16653,7 +16680,12 @@ export class OrcaRuntimeService {
|
||||
|
||||
return {
|
||||
terminals: listedTerminals,
|
||||
hostScope: this.buildTerminalListHostScope(targetWorktreeId, matchingTerminals),
|
||||
hostScope: this.buildTerminalListHostScope(
|
||||
targetWorktreeId,
|
||||
matchingTerminals,
|
||||
worktreesById.values(),
|
||||
controllerInventory?.queriedHostIds ?? new Set()
|
||||
),
|
||||
...(visualLayouts.length > 0 ? { visualLayouts } : {}),
|
||||
topologyRevisions: Object.fromEntries(
|
||||
[...new Set(matchingTerminals.map((terminal) => terminal.worktreeId))].map((worktreeId) => [
|
||||
@@ -16667,25 +16699,38 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
|
||||
// A worktree-scoped request answers for one host only, so name the hosts it
|
||||
// skipped: absence from a scoped listing is not evidence a worker died.
|
||||
// skipped: absence from a scoped listing is not evidence a worker exited.
|
||||
private buildTerminalListHostScope(
|
||||
targetWorktreeId: string | null,
|
||||
terminals: readonly RuntimeTerminalSummary[]
|
||||
terminals: readonly RuntimeTerminalSummary[],
|
||||
worktrees: Iterable<ResolvedWorktree>,
|
||||
queriedHostIds: ReadonlySet<ExecutionHostId>
|
||||
): RuntimeTerminalListHostScope {
|
||||
const knownHostIds = this.listKnownExecutionHostIds()
|
||||
const knownHostIds = this.listKnownExecutionHostIds(
|
||||
queriedHostIds,
|
||||
targetWorktreeId !== FLOATING_TERMINAL_WORKTREE_ID
|
||||
)
|
||||
for (const worktree of worktrees) {
|
||||
if (worktree.hostId) {
|
||||
knownHostIds.add(worktree.hostId)
|
||||
}
|
||||
}
|
||||
for (const terminal of terminals) {
|
||||
if (terminal.executionHostId) {
|
||||
knownHostIds.add(terminal.executionHostId)
|
||||
}
|
||||
}
|
||||
const scopedHostId = targetWorktreeId
|
||||
? this.tryGetWorkspaceSessionHostIdForWorktree(targetWorktreeId)
|
||||
: null
|
||||
const coveredHostIds = new Set<ExecutionHostId>(
|
||||
targetWorktreeId ? (scopedHostId ? [scopedHostId] : []) : knownHostIds
|
||||
const candidates = targetWorktreeId ? (scopedHostId ? [scopedHostId] : []) : knownHostIds
|
||||
// Paired runtimes own a separate control plane. Mirrored rows are evidence
|
||||
// for those rows only; this runtime cannot claim their complete inventory.
|
||||
const coveredHostIds = new Set(
|
||||
[...candidates].filter(
|
||||
(hostId) => queriedHostIds.has(hostId) && parseExecutionHostId(hostId)?.kind !== 'runtime'
|
||||
)
|
||||
)
|
||||
// A listed row's host is covered by definition, even when it is a paired
|
||||
// runtime this host would not otherwise enumerate.
|
||||
for (const terminal of terminals) {
|
||||
if (terminal.executionHostId) {
|
||||
coveredHostIds.add(terminal.executionHostId)
|
||||
}
|
||||
}
|
||||
return {
|
||||
hostIds: [...coveredHostIds].sort(),
|
||||
omittedHostIds: [...knownHostIds].filter((hostId) => !coveredHostIds.has(hostId)).sort()
|
||||
@@ -31249,7 +31294,8 @@ export class OrcaRuntimeService {
|
||||
return {
|
||||
livePtyIds: targetedLiveness,
|
||||
allLivePtyIds: targetedLiveness,
|
||||
terminalIdentityByPtyId: new Map()
|
||||
terminalIdentityByPtyId: new Map(),
|
||||
queriedHostIds: new Set([LOCAL_EXECUTION_HOST_ID])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31264,8 +31310,28 @@ export class OrcaRuntimeService {
|
||||
} else {
|
||||
this.ptyControllerInventoryGenerationByProvider.set(providerKey, inventoryGeneration)
|
||||
}
|
||||
const processInventory =
|
||||
connectionId === undefined && this.ptyController.listProcessesWithHostScope
|
||||
? this.ptyController.listProcessesWithHostScope()
|
||||
: this.ptyController.listProcesses(connectionId).then((processes) => {
|
||||
const hostIds = new Set<ExecutionHostId>()
|
||||
if (connectionId === undefined || connectionId === null) {
|
||||
hostIds.add(LOCAL_EXECUTION_HOST_ID)
|
||||
} else {
|
||||
hostIds.add(toSshExecutionHostId(connectionId))
|
||||
}
|
||||
if (connectionId === undefined) {
|
||||
for (const process of processes) {
|
||||
const ssh = parseAppSshPtyId(process.id)
|
||||
if (ssh) {
|
||||
hostIds.add(toSshExecutionHostId(ssh.connectionId))
|
||||
}
|
||||
}
|
||||
}
|
||||
return { processes, hostIds: [...hostIds] }
|
||||
})
|
||||
const sessionsResult = await withTimeoutResult(
|
||||
this.ptyController.listProcesses(connectionId),
|
||||
processInventory,
|
||||
deadline === undefined
|
||||
? PTY_CONTROLLER_LIST_TIMEOUT_MS
|
||||
: Math.max(1, Math.min(PTY_CONTROLLER_LIST_TIMEOUT_MS, deadline - Date.now()))
|
||||
@@ -31286,7 +31352,8 @@ export class OrcaRuntimeService {
|
||||
if (!isCurrentInventory) {
|
||||
return null
|
||||
}
|
||||
const sessions = sessionsResult.value
|
||||
const sessions = sessionsResult.value.processes
|
||||
const queriedHostIds = new Set(sessionsResult.value.hostIds)
|
||||
const controllerIdentityByPtyId = new Map<string, PtyControllerTerminalIdentity>()
|
||||
const ptyIdByControllerHandle = new Map<string, string>()
|
||||
const ambiguousControllerPtyIds = new Set<string>()
|
||||
@@ -31465,7 +31532,8 @@ export class OrcaRuntimeService {
|
||||
return {
|
||||
livePtyIds: targetWorktreeId ? selectedLivePtyIds : allLivePtyIds,
|
||||
allLivePtyIds,
|
||||
terminalIdentityByPtyId: controllerIdentityByPtyId
|
||||
terminalIdentityByPtyId: controllerIdentityByPtyId,
|
||||
queriedHostIds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import { folderWorkspaceKey } from '../../shared/workspace-scope'
|
||||
|
||||
// An agent once declared a task dead because `terminal list` came back without
|
||||
// its worker: the worker was alive on an SSH host, and nothing in the response
|
||||
// An agent once declared a task exited because `terminal list` came back without
|
||||
// its worker: the worker was live on an SSH host, and nothing in the response
|
||||
// said the listing was scoped or which host each row ran on.
|
||||
|
||||
const LOCAL_WORKTREE_ID = 'repo-local::/tmp/local-worktree'
|
||||
@@ -35,6 +36,9 @@ function makeStore() {
|
||||
const session: WorkspaceSessionState = getDefaultWorkspaceSession()
|
||||
return {
|
||||
getWorkspaceSession: vi.fn(() => session),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local', 'ssh:box-1']),
|
||||
getFolderWorkspaces: vi.fn(() => []),
|
||||
getProjectGroups: vi.fn(() => []),
|
||||
setWorkspaceSession: vi.fn(),
|
||||
getRepos: vi.fn(() => REPOS),
|
||||
getRepo: vi.fn((id: string) => REPOS.find((repo) => repo.id === id)),
|
||||
@@ -47,10 +51,29 @@ function makeStore() {
|
||||
}
|
||||
}
|
||||
|
||||
function makeSshFolderWorkspace() {
|
||||
return {
|
||||
id: 'folder-ssh',
|
||||
projectGroupId: 'group-1',
|
||||
name: 'SSH folder',
|
||||
folderPath: '/remote/folder',
|
||||
connectionId: 'box-2',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
createdAt: 0,
|
||||
updatedAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
type GraphLeaf = { worktreeId: string; leafId: string; ptyId: string }
|
||||
|
||||
function makeRuntime(leaves: GraphLeaf[]): OrcaRuntimeService {
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
function makeRuntime(leaves: GraphLeaf[], store = makeStore()): OrcaRuntimeService {
|
||||
const runtime = new OrcaRuntimeService(store as never)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'never' })),
|
||||
write: () => true,
|
||||
@@ -132,6 +155,157 @@ describe('listTerminals scope declaration', () => {
|
||||
expect(result.hostScope?.omittedHostIds).toEqual([])
|
||||
})
|
||||
|
||||
it('does not claim a paired runtime was covered from a mirrored row', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{
|
||||
worktreeId: LOCAL_WORKTREE_ID,
|
||||
leafId: REMOTE_LEAF_ID,
|
||||
ptyId: 'remote:env-7@@handle-1'
|
||||
}
|
||||
])
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-7', 'ssh:box-1'])
|
||||
})
|
||||
|
||||
it('keeps a repo-known paired runtime omitted without a mirrored row', async () => {
|
||||
const baseStore = makeStore()
|
||||
const repos = [
|
||||
REPOS[0]!,
|
||||
{
|
||||
id: 'repo-runtime',
|
||||
path: '/remote/runtime-worktree',
|
||||
displayName: 'runtime',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 0,
|
||||
executionHostId: 'runtime:env-9' as const
|
||||
}
|
||||
]
|
||||
const runtime = makeRuntime([], {
|
||||
...baseStore,
|
||||
getRepos: vi.fn(() => repos),
|
||||
getRepo: vi.fn((id: string) => repos.find((repo) => repo.id === id)),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local'])
|
||||
})
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-9'])
|
||||
})
|
||||
|
||||
it('keeps a repo-known paired runtime omitted in a worktree-scoped listing', async () => {
|
||||
const baseStore = makeStore()
|
||||
const repos = [
|
||||
REPOS[0]!,
|
||||
{
|
||||
id: 'repo-runtime',
|
||||
path: '/remote/runtime-worktree',
|
||||
displayName: 'runtime',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 0,
|
||||
executionHostId: 'runtime:env-9' as const
|
||||
}
|
||||
]
|
||||
const runtime = makeRuntime([], {
|
||||
...baseStore,
|
||||
getRepos: vi.fn(() => repos),
|
||||
getRepo: vi.fn((id: string) => repos.find((repo) => repo.id === id)),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local'])
|
||||
})
|
||||
|
||||
const result = await runtime.listTerminals(`id:${LOCAL_WORKTREE_ID}`)
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-9'])
|
||||
})
|
||||
|
||||
it('covers a queried SSH host used only by a folder workspace', async () => {
|
||||
const baseStore = makeStore()
|
||||
const folderWorkspace = makeSshFolderWorkspace()
|
||||
const runtime = makeRuntime([], {
|
||||
...baseStore,
|
||||
getRepos: vi.fn(() => [REPOS[0]!]),
|
||||
getRepo: vi.fn((id: string) => (id === REPOS[0]!.id ? REPOS[0] : undefined)),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local']),
|
||||
getFolderWorkspaces: vi.fn(() => [folderWorkspace])
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'never' })),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
listProcesses: vi.fn(async () => []),
|
||||
listProcessesWithHostScope: vi.fn(async () => ({
|
||||
processes: [],
|
||||
hostIds: ['local', 'ssh:box-2']
|
||||
}))
|
||||
} as never)
|
||||
|
||||
const result = await runtime.listTerminals(`id:${folderWorkspaceKey(folderWorkspace.id)}`)
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['ssh:box-2'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['local'])
|
||||
})
|
||||
|
||||
it('keeps a disconnected folder-workspace SSH host omitted', async () => {
|
||||
const baseStore = makeStore()
|
||||
const folderWorkspace = makeSshFolderWorkspace()
|
||||
const runtime = makeRuntime([], {
|
||||
...baseStore,
|
||||
getRepos: vi.fn(() => [REPOS[0]!]),
|
||||
getRepo: vi.fn((id: string) => (id === REPOS[0]!.id ? REPOS[0] : undefined)),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local']),
|
||||
getFolderWorkspaces: vi.fn(() => [folderWorkspace])
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'never' })),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
listProcesses: vi.fn(async () => {
|
||||
throw new Error('relay unavailable')
|
||||
})
|
||||
} as never)
|
||||
|
||||
const result = await runtime.listTerminals(`id:${folderWorkspaceKey(folderWorkspace.id)}`)
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual([])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['local', 'ssh:box-2'])
|
||||
})
|
||||
|
||||
it('keeps a disconnected SSH host omitted when only local inventory answered', async () => {
|
||||
const runtime = makeRuntime([])
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'never' })),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
listProcesses: vi.fn(async () => [])
|
||||
} as never)
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['ssh:box-1'])
|
||||
})
|
||||
|
||||
it('marks every known host omitted when process inventory is unverifiable', async () => {
|
||||
const runtime = makeRuntime([])
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'never' })),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
listProcesses: vi.fn(async () => {
|
||||
throw new Error('relay unavailable')
|
||||
})
|
||||
} as never)
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual([])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['local', 'ssh:box-1'])
|
||||
})
|
||||
|
||||
it('reports the hosts a worktree-scoped listing skipped, so an empty result is not absolute', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{ worktreeId: SSH_WORKTREE_ID, leafId: SSH_LEAF_ID, ptyId: 'ssh:box-1@@pty-7' }
|
||||
|
||||
@@ -521,8 +521,8 @@ export type RuntimeTerminalVisualLayout = {
|
||||
* answering runtime names them — `_meta.runtimeId` says which runtime that is. */
|
||||
export type RuntimeTerminalListHostScope = {
|
||||
hostIds: ExecutionHostId[]
|
||||
/** Known hosts this listing skipped; a live terminal on one of them is absent
|
||||
* from `terminals` without being dead. */
|
||||
/** Known hosts this listing skipped; a live terminal on one of them can be
|
||||
* absent from `terminals` without having exited. */
|
||||
omittedHostIds: ExecutionHostId[]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user