mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
fix(runtime): bind serve-host terminals to their workspace at create (#38864)
Terminals a paired desktop or mobile client opened on a headless serve host got a workspace-less serve-<uuid> session id, so the host inferred their owner from the cwd. Folder workspaces that share a path then traded those terminals whenever one was listed, and deleting a workspace stopped terminals shown in another. Serve ids now carry the workspace like every other daemon id. Inventory refresh keeps a persisted layout binding even when a targeted listing does not include its workspace, so restored serve-<uuid> terminals stay put. A cwd guess is recorded as such and never drives a worktree-scoped stop. Fixes #36716 Reported-by: Guy Shechter (@shechter) Co-authored-by: Bing.Z <zzb@gxsmjx.com>
This commit is contained in:
@@ -65,7 +65,7 @@ describe('PTY inventory refresh keeps recorded floating ownership', () => {
|
||||
await runtime.listTerminals(`id:${folderWorkspaceKey}`)
|
||||
const listed = await runtime.listTerminals()
|
||||
|
||||
const terminal = listed.terminals.find((entry) => entry.ptyId?.startsWith('serve-'))
|
||||
const terminal = listed.terminals.find((entry) => entry.ptyId?.includes('@@serve-'))
|
||||
expect(terminal?.worktreeId).toBe(FLOATING_TERMINAL_WORKTREE_ID)
|
||||
expect(terminal?.worktreeId).not.toBe(folderWorkspaceKey)
|
||||
const floating = await runtime.listMobileSessionTabs(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('OrcaRuntimeService', () => {
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
tabId: 'host-tab',
|
||||
leafId: HEADLESS_LEAF_ID,
|
||||
sessionId: expect.stringMatching(/^serve-/),
|
||||
sessionId: expect.stringMatching(/@@serve-/),
|
||||
isNewSession: true,
|
||||
persistHostSessionBinding: true
|
||||
})
|
||||
|
||||
@@ -124,7 +124,12 @@ describe('OrcaRuntimeService', () => {
|
||||
|
||||
it('reports a PTY that remains live after acknowledged worktree sleep', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const liveProcess = { id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }
|
||||
const liveProcess = {
|
||||
id: 'pty-1',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Claude'
|
||||
}
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => false,
|
||||
@@ -163,7 +168,16 @@ describe('OrcaRuntimeService', () => {
|
||||
if (listCount === 2) {
|
||||
throw new Error('daemon unavailable')
|
||||
}
|
||||
return listCount === 1 ? [{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }] : []
|
||||
return listCount === 1
|
||||
? [
|
||||
{
|
||||
id: 'pty-1',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Claude'
|
||||
}
|
||||
]
|
||||
: []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -197,7 +211,8 @@ describe('OrcaRuntimeService', () => {
|
||||
|
||||
it('coalesces two clients sleeping the same host worktree', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const initialInventory = deferred<{ id: string; cwd: string; title: string }[]>()
|
||||
const initialInventory =
|
||||
deferred<{ id: string; cwd: string; worktreeId: string; title: string }[]>()
|
||||
const listProcesses = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => initialInventory.promise)
|
||||
@@ -216,7 +231,9 @@ describe('OrcaRuntimeService', () => {
|
||||
|
||||
const first = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)
|
||||
const second = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)
|
||||
initialInventory.resolve([{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }])
|
||||
initialInventory.resolve([
|
||||
{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Claude' }
|
||||
])
|
||||
|
||||
const [firstResult, secondResult] = await Promise.all([first, second])
|
||||
expect(secondResult).toEqual(firstResult)
|
||||
@@ -231,7 +248,14 @@ describe('OrcaRuntimeService', () => {
|
||||
runtime.onClientEvent((event) => events.push(event))
|
||||
const listProcesses = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([{ id: 'pty-before-sleep', cwd: TEST_WORKTREE_PATH, title: 'Shell' }])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: 'pty-before-sleep',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Shell'
|
||||
}
|
||||
])
|
||||
.mockResolvedValueOnce([])
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
@@ -341,7 +365,11 @@ describe('OrcaRuntimeService', () => {
|
||||
}
|
||||
})
|
||||
runtime.onClientEvent((event) => secondListenerEvents.push(event))
|
||||
const processLists = [[{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }], [], []]
|
||||
const processLists = [
|
||||
[{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Claude' }],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => false,
|
||||
@@ -400,7 +428,14 @@ describe('OrcaRuntimeService', () => {
|
||||
const events: RuntimeClientEvent[] = []
|
||||
runtime.onClientEvent((event) => events.push(event))
|
||||
const processLists = [
|
||||
[{ id: 'pty-preserved-history', cwd: TEST_WORKTREE_PATH, title: 'Shell' }],
|
||||
[
|
||||
{
|
||||
id: 'pty-preserved-history',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Shell'
|
||||
}
|
||||
],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
@@ -437,12 +472,17 @@ describe('OrcaRuntimeService', () => {
|
||||
runtime.onClientEvent((event) => events.push(event))
|
||||
}
|
||||
const secondStop = deferred<boolean>()
|
||||
const failedPty = { id: 'pty-fails', cwd: TEST_WORKTREE_PATH, title: 'Claude' }
|
||||
const failedPty = {
|
||||
id: 'pty-fails',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Claude'
|
||||
}
|
||||
const listProcesses = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([
|
||||
failedPty,
|
||||
{ id: 'pty-slow', cwd: TEST_WORKTREE_PATH, title: 'Shell' }
|
||||
{ id: 'pty-slow', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Shell' }
|
||||
])
|
||||
.mockResolvedValueOnce([failedPty])
|
||||
.mockResolvedValueOnce([failedPty])
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('OrcaRuntimeService', () => {
|
||||
stopAndWait,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [
|
||||
{ id: 'pty-run', cwd: TEST_WORKTREE_PATH, title: 'Claude' },
|
||||
{ id: 'pty-user', cwd: TEST_WORKTREE_PATH, title: 'Shell' }
|
||||
{ id: 'pty-run', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Claude' },
|
||||
{ id: 'pty-user', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Shell' }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -142,10 +142,10 @@ describe('OrcaRuntimeService', () => {
|
||||
const stopped: string[] = []
|
||||
const processLists = [
|
||||
[
|
||||
{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' },
|
||||
{ id: 'pty-shell', cwd: TEST_WORKTREE_PATH, title: 'Shell' }
|
||||
{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Claude' },
|
||||
{ id: 'pty-shell', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Shell' }
|
||||
],
|
||||
[{ id: 'pty-shell', cwd: TEST_WORKTREE_PATH, title: 'Shell' }]
|
||||
[{ id: 'pty-shell', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, title: 'Shell' }]
|
||||
]
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
|
||||
@@ -18,6 +18,16 @@ import {
|
||||
syncSinglePty
|
||||
} from './orca-runtime-test-fixtures.spec'
|
||||
|
||||
// A live PTY as a host lists it: the daemon reports its workspace, never just a cwd (#36716).
|
||||
function listedPty(
|
||||
id: string,
|
||||
title: string,
|
||||
cwd = TEST_WORKTREE_PATH,
|
||||
worktreeId = TEST_WORKTREE_ID
|
||||
) {
|
||||
return { id, cwd, worktreeId, title }
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeService', () => {
|
||||
it('durably closes every terminal in one workspace without touching a sibling', async () => {
|
||||
const otherWorktreeId = `${TEST_REPO_ID}::/tmp/worktree-b`
|
||||
@@ -604,13 +614,7 @@ describe('OrcaRuntimeService', () => {
|
||||
it('sleeps every freshly discovered worktree PTY without a hydrated renderer graph', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const stopped: string[] = []
|
||||
const processLists = [
|
||||
[
|
||||
{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' },
|
||||
{ id: 'pty-2', cwd: TEST_WORKTREE_PATH, title: 'Shell' }
|
||||
],
|
||||
[]
|
||||
]
|
||||
const processLists = [[listedPty('pty-1', 'Claude'), listedPty('pty-2', 'Shell')], []]
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => false,
|
||||
@@ -806,7 +810,7 @@ describe('OrcaRuntimeService', () => {
|
||||
kill: () => false,
|
||||
stopAndWait: vi.fn(async () => false),
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }]
|
||||
listProcesses: async () => [listedPty('pty-1', 'Claude')]
|
||||
})
|
||||
|
||||
await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).rejects.toThrow(
|
||||
@@ -828,11 +832,9 @@ describe('OrcaRuntimeService', () => {
|
||||
])
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const stopped: string[] = []
|
||||
const otherPty = { id: 'pty-other', cwd: otherWorktreePath, title: 'Other' }
|
||||
const processLists = [
|
||||
[{ id: 'pty-target', cwd: TEST_WORKTREE_PATH, title: 'Target' }, otherPty],
|
||||
[otherPty]
|
||||
]
|
||||
const otherId = `${TEST_REPO_ID}::${otherWorktreePath}`
|
||||
const otherPty = listedPty('pty-other', 'Other', otherWorktreePath, otherId)
|
||||
const processLists = [[listedPty('pty-target', 'Target'), otherPty], [otherPty]]
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => false,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
import { parsePtySessionId } from '../../shared/terminal/pty/lifecycle/pty-session-id-format'
|
||||
import { withBoundPane } from './orca-runtime-tests/bind-first-pane.test-fixture'
|
||||
|
||||
// Preserve side-effect order: mocks, lifecycle, fixtures.
|
||||
await import('./orca-runtime-tests/runtime-test-module-registration.spec')
|
||||
const { OrcaRuntimeService } = await import('./orca-runtime')
|
||||
await import('./orca-runtime-tests/orca-runtime-test-lifecycle.spec')
|
||||
const { createFolderWorkspaceRuntimeStore, makeFolderProjectGroup, makeFolderWorkspace } =
|
||||
await import('./orca-runtime-tests/orca-runtime-test-fixtures.spec')
|
||||
|
||||
const FOLDER = process.cwd()
|
||||
const H = '10000000-0000-4000-8000-000000000001'
|
||||
const B = '10000000-0000-4000-8000-000000000002'
|
||||
|
||||
// Same-path folder workspaces on a headless serve host (#36716).
|
||||
function makeServeHost(seed: { ptyId: string; worktreeId?: string }[] = []) {
|
||||
const folders = [H, B].map((id) => makeFolderWorkspace({ id, folderPath: FOLDER }))
|
||||
let session = getDefaultWorkspaceSession()
|
||||
for (const [index, { ptyId, worktreeId }] of seed.entries()) {
|
||||
if (worktreeId) {
|
||||
session = withBoundPane(session, {
|
||||
worktreeId,
|
||||
tabId: `tab-${index}`,
|
||||
leafId: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
|
||||
ptyId
|
||||
})
|
||||
}
|
||||
}
|
||||
const store = {
|
||||
...createFolderWorkspaceRuntimeStore(
|
||||
folders[0],
|
||||
makeFolderProjectGroup({ parentPath: FOLDER })
|
||||
),
|
||||
getFolderWorkspaces: () => folders,
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession: (next: typeof session) => {
|
||||
session = next
|
||||
},
|
||||
flushOrThrow: () => {}
|
||||
}
|
||||
const live = new Set(seed.map(({ ptyId }) => ptyId))
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const spawn = vi.fn(
|
||||
async (args: {
|
||||
sessionId?: string
|
||||
worktreeId?: string
|
||||
tabId?: string
|
||||
leafId?: string
|
||||
persistHostSessionBinding?: boolean
|
||||
}) => {
|
||||
if (!args.worktreeId || !args.tabId || !args.leafId) {
|
||||
throw new Error('missing identity')
|
||||
}
|
||||
const id = args.sessionId ?? `${args.worktreeId}@@control`
|
||||
live.add(id)
|
||||
if (args.persistHostSessionBinding) {
|
||||
session = withBoundPane(session, {
|
||||
worktreeId: args.worktreeId,
|
||||
tabId: args.tabId,
|
||||
leafId: args.leafId,
|
||||
ptyId: id
|
||||
})
|
||||
}
|
||||
return { id }
|
||||
}
|
||||
)
|
||||
const stopAndWait = vi.fn(async (id: string) => {
|
||||
live.delete(id)
|
||||
runtime.onPtyExit(id, 0)
|
||||
return true
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
stopAndWait,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
hasPty: (id: string) => live.has(id),
|
||||
getForegroundProcess: async () => null,
|
||||
// Like the daemon, report only the workspace a session id names; every shell shares one cwd.
|
||||
listProcesses: async () =>
|
||||
[...live].map((id) => {
|
||||
const { worktreeId } = parsePtySessionId(id)
|
||||
return {
|
||||
id,
|
||||
incarnationId: `${id}-incarnation`,
|
||||
terminalHandle: `term_${id}`,
|
||||
wslDistro: null,
|
||||
cwd: FOLDER,
|
||||
title: 'shell',
|
||||
...(worktreeId ? { worktreeId } : {})
|
||||
}
|
||||
})
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
const ownerOf = async (ptyId: string) =>
|
||||
(await runtime.listTerminals()).terminals.find((entry) => entry.ptyId === ptyId)?.worktreeId
|
||||
return { runtime, spawn, stopAndWait, live, ownerOf }
|
||||
}
|
||||
|
||||
describe('serve host terminal workspace ownership', () => {
|
||||
it('keeps a paired client terminal in its workspace when a same-path sibling refreshes', async () => {
|
||||
const { runtime, spawn, stopAndWait, live, ownerOf } = makeServeHost()
|
||||
await runtime.createMobileSessionTerminal(`id:folder:${H}`, { activate: false })
|
||||
const spawned = await spawn.mock.results.at(-1)?.value
|
||||
const id = spawned?.id ?? ''
|
||||
expect(id).toMatch(new RegExp(`^folder:${H}@@serve-`))
|
||||
|
||||
await runtime.listMobileSessionTabs(`id:folder:${B}`)
|
||||
expect((await runtime.listTerminals(`id:folder:${B}`)).terminals).toEqual([])
|
||||
expect(await ownerOf(id)).toBe(`folder:${H}`)
|
||||
|
||||
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
|
||||
expect(stopAndWait).not.toHaveBeenCalled()
|
||||
expect(live.has(id)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps a restored workspace-less serve terminal with its persisted binding', async () => {
|
||||
const legacy = 'serve-00000000-0000-4000-8000-0000000000aa'
|
||||
const { runtime, stopAndWait, live, ownerOf } = makeServeHost([
|
||||
{ ptyId: legacy, worktreeId: `folder:${H}` }
|
||||
])
|
||||
|
||||
await runtime.listMobileSessionTabs(`id:folder:${B}`)
|
||||
expect((await runtime.listTerminals(`id:folder:${B}`)).terminals).toEqual([])
|
||||
expect(await ownerOf(legacy)).toBe(`folder:${H}`)
|
||||
|
||||
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
|
||||
expect(stopAndWait).not.toHaveBeenCalled()
|
||||
expect(live.has(legacy)).toBe(true)
|
||||
})
|
||||
|
||||
it('never stops an unbound terminal that a workspace only claims by cwd', async () => {
|
||||
const unbound = 'unbound-shell'
|
||||
const { runtime, stopAndWait, live } = makeServeHost([{ ptyId: unbound }])
|
||||
|
||||
await runtime.listMobileSessionTabs(`id:folder:${B}`)
|
||||
const listed = await runtime.listTerminals(`id:folder:${B}`)
|
||||
expect(listed.terminals.map((entry) => entry.ptyId)).toEqual([unbound])
|
||||
|
||||
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
|
||||
await runtime.sleepTerminalsForWorktree(`id:folder:${B}`)
|
||||
expect(stopAndWait).not.toHaveBeenCalled()
|
||||
expect(live.has(unbound)).toBe(true)
|
||||
})
|
||||
|
||||
it('stops a cwd-filed terminal with its workspace once a pane adopts it', async () => {
|
||||
const orphan = 'unbound-shell'
|
||||
const { runtime, stopAndWait, live } = makeServeHost([{ ptyId: orphan }])
|
||||
const before = await runtime.listTerminals(`id:folder:${B}`)
|
||||
|
||||
await runtime.adoptTerminalOrphans({
|
||||
worktree: `id:folder:${B}`,
|
||||
expectedTopologyRevision: before.topologyRevisions?.[`folder:${B}`] ?? 0,
|
||||
claims: [
|
||||
{
|
||||
terminal: `term_${orphan}`,
|
||||
ptyId: orphan,
|
||||
incarnationId: `${orphan}-incarnation`,
|
||||
tabId: 'adopted-tab',
|
||||
leafId: '00000000-0000-4000-8000-0000000000ad'
|
||||
}
|
||||
]
|
||||
})
|
||||
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
|
||||
|
||||
expect(stopAndWait).toHaveBeenCalledWith(orphan)
|
||||
expect(live.has(orphan)).toBe(false)
|
||||
})
|
||||
})
|
||||
+2
-1
@@ -22,6 +22,7 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session
|
||||
import type { RuntimeStore } from '../../runtime-store-contract'
|
||||
import { SSH_PANE_RECOVERY_GRACE_MS } from '../../terminal/recovery/ssh-pane-recovery-window'
|
||||
import { findTerminalTabIdForLeaf } from '../../../persistence/terminal-topology/terminal-topology-membership'
|
||||
import { isServeMintedPtySessionId } from '../../../../shared/terminal/pty/lifecycle/pty-session-id-format'
|
||||
|
||||
export abstract class OrcaRuntimeWithReconcileHeadlessMobileSessionBrowserTabs extends OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspaceSession {
|
||||
protected abstract getAcceptedRendererIdentityKeysForMobileSessionSnapshot(
|
||||
@@ -181,7 +182,7 @@ export abstract class OrcaRuntimeWithReconcileHeadlessMobileSessionBrowserTabs e
|
||||
}
|
||||
|
||||
protected isServeOwnedPtyId(ptyId: string | null | undefined): boolean {
|
||||
return typeof ptyId === 'string' && ptyId.startsWith('serve-')
|
||||
return typeof ptyId === 'string' && isServeMintedPtySessionId(ptyId)
|
||||
}
|
||||
|
||||
protected isSshOwnedPtyId(ptyId: string | null | undefined): boolean {
|
||||
|
||||
@@ -69,6 +69,7 @@ export abstract class OrcaRuntimeWithRecordPtyWorktree extends RuntimeWorkspaceR
|
||||
| 'wslDistro'
|
||||
| 'incarnationId'
|
||||
| 'agentSessionOwners'
|
||||
| 'worktreeInferredFromCwd'
|
||||
>
|
||||
> = {}
|
||||
): RuntimePtyWorktreeRecord {
|
||||
@@ -95,6 +96,7 @@ export abstract class OrcaRuntimeWithRecordPtyWorktree extends RuntimeWorkspaceR
|
||||
ptyId,
|
||||
incarnationId: state.incarnationId ?? null,
|
||||
worktreeId,
|
||||
...(state.worktreeInferredFromCwd ? { worktreeInferredFromCwd: true } : {}),
|
||||
connectionId,
|
||||
runtimeSessionOwned: state.runtimeSessionOwned ?? false,
|
||||
isWsl: state.isWsl ?? null,
|
||||
@@ -165,7 +167,7 @@ export abstract class OrcaRuntimeWithRecordPtyWorktree extends RuntimeWorkspaceR
|
||||
return pty
|
||||
}
|
||||
|
||||
this.terminalIdentity.setPtyWorktreeId(pty, worktreeId)
|
||||
this.terminalIdentity.setPtyWorktree(pty, worktreeId, state.worktreeInferredFromCwd === true)
|
||||
if (
|
||||
state.incarnationId !== undefined &&
|
||||
pty.incarnationId !== null &&
|
||||
|
||||
@@ -66,61 +66,45 @@ export class TerminalIdentitySource {
|
||||
return record
|
||||
}
|
||||
|
||||
setPtyWorktreeId(record: RuntimePtyWorktreeRecord, value: string): void {
|
||||
/** `inferredFromCwd` marks a guess that must never drive a worktree-scoped stop (#36716). */
|
||||
setPtyWorktree(record: RuntimePtyWorktreeRecord, worktreeId: string, inferredFromCwd: boolean) {
|
||||
this.setPtyField(record, 'worktreeId', worktreeId)
|
||||
this.setPtyField(record, 'worktreeInferredFromCwd', inferredFromCwd || undefined)
|
||||
}
|
||||
|
||||
/** A binding (pane, layout) confirms the recorded worktree, so a cwd guess no longer guards it. */
|
||||
confirmPtyWorktree(record: Pick<RuntimePtyWorktreeRecord, 'ptyId'>): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.worktreeId !== value) {
|
||||
owned.worktreeId = value
|
||||
if (owned?.worktreeInferredFromCwd) {
|
||||
delete owned.worktreeInferredFromCwd
|
||||
this.changed()
|
||||
}
|
||||
}
|
||||
|
||||
setPtyIncarnationId(record: RuntimePtyWorktreeRecord, value: string | null): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.incarnationId !== value) {
|
||||
owned.incarnationId = value
|
||||
this.changed()
|
||||
}
|
||||
this.setPtyField(record, 'incarnationId', value)
|
||||
}
|
||||
|
||||
setPtyConnectionId(record: RuntimePtyWorktreeRecord, value: string | null): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.connectionId !== value) {
|
||||
owned.connectionId = value
|
||||
this.changed()
|
||||
}
|
||||
this.setPtyField(record, 'connectionId', value)
|
||||
}
|
||||
|
||||
setPtyConnected(record: RuntimePtyWorktreeRecord, value: boolean): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.connected !== value) {
|
||||
owned.connected = value
|
||||
this.changed()
|
||||
}
|
||||
this.setPtyField(record, 'connected', value)
|
||||
}
|
||||
|
||||
setPtyTabId(record: RuntimePtyWorktreeRecord, value: string | null | undefined): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.tabId !== value) {
|
||||
owned.tabId = value
|
||||
this.changed()
|
||||
}
|
||||
this.setPtyField(record, 'tabId', value)
|
||||
}
|
||||
|
||||
setPtyIsWsl(record: RuntimePtyWorktreeRecord, value: boolean | null): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.isWsl !== value) {
|
||||
owned.isWsl = value
|
||||
this.changed()
|
||||
}
|
||||
this.setPtyField(record, 'isWsl', value)
|
||||
}
|
||||
|
||||
setPtyWslDistro(record: RuntimePtyWorktreeRecord, value: string | null): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned.wslDistro !== value) {
|
||||
owned.wslDistro = value
|
||||
this.changed()
|
||||
}
|
||||
this.setPtyField(record, 'wslDistro', value)
|
||||
}
|
||||
|
||||
setRecordedSurface(
|
||||
record: Pick<
|
||||
RuntimePtyWorktreeRecord,
|
||||
@@ -302,6 +286,18 @@ export class TerminalIdentitySource {
|
||||
this.changed()
|
||||
}
|
||||
|
||||
private setPtyField<K extends keyof RuntimePtyWorktreeRecord>(
|
||||
record: RuntimePtyWorktreeRecord,
|
||||
key: K,
|
||||
value: RuntimePtyWorktreeRecord[K]
|
||||
): void {
|
||||
const owned = this.ownedPtys.get(record)
|
||||
if (owned && owned[key] !== value) {
|
||||
owned[key] = value
|
||||
this.changed()
|
||||
}
|
||||
}
|
||||
|
||||
private bind(map: Map<string, string>, key: string, value: string): void {
|
||||
const previous = map.get(key)
|
||||
if (previous !== value) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { randomUUID } from 'node:crypto'
|
||||
import { copySleepingAgentLaunchConfig } from './runtime-agent-launch-resolution'
|
||||
import { deriveRemoteRuntimeTerminalCreateHandle } from '../identity/remote-runtime-terminal-create-identity'
|
||||
import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
|
||||
import { mintServePtySessionId } from '../../../../shared/terminal/pty/lifecycle/pty-session-id-format'
|
||||
import { placeCreatedSessionTab } from '../../../../shared/session-tab-placement'
|
||||
import { insertTabIdIntoOrder } from '../../../../shared/workspace-layout/tab-order'
|
||||
import type { OrcaRuntimeWithBuildPtyTerminalSummary as PtyTerminalSummaryMethods } from '../query/pty-terminal-summary'
|
||||
@@ -68,7 +69,8 @@ export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal ext
|
||||
const cwd = this.resolveWorkspaceTerminalStartupCwd(workspace, opts.cwd)
|
||||
// Why: an SSH sessionId means reattach; only synthesize local serve ids so SSH fresh terminals still call pty.spawn.
|
||||
const stableSessionId =
|
||||
opts.identity?.sessionId ?? (workspace.connectionId ? undefined : `serve-${randomUUID()}`)
|
||||
opts.identity?.sessionId ??
|
||||
(workspace.connectionId ? undefined : mintServePtySessionId(workspace.id, randomUUID()))
|
||||
const isNewSession = stableSessionId !== undefined && opts.identity?.sessionId === undefined
|
||||
const mutationHandle = opts.createMutation
|
||||
? deriveRemoteRuntimeTerminalCreateHandle(
|
||||
|
||||
@@ -134,16 +134,25 @@ export abstract class OrcaRuntimeWithRefreshPtyWorktreeRecordsWithControllerInve
|
||||
inferredWorktreeId &&
|
||||
worktreeIdsEqual(session.worktreeId, inferredWorktreeId)
|
||||
)
|
||||
const recordedPty = this.ptysById.get(session.id)
|
||||
// Explicit unresolved provider ownership wins unless the session id proves it predates a persisted rename.
|
||||
const worktreeId = providerWorktree
|
||||
const boundWorktreeId = providerWorktree
|
||||
? providerWorktree.id
|
||||
: hasMigrationEvidence
|
||||
? (persistedWorktree?.id ?? null)
|
||||
: (session.worktreeId ??
|
||||
persistedWorktree?.id ??
|
||||
// A binding outside a targeted list belongs to that sibling; cwd must not re-file it (#36716).
|
||||
persistedWorktreeId ??
|
||||
(recordedFloating ? FLOATING_TERMINAL_WORKTREE_ID : undefined) ??
|
||||
inferredWorktreeId ??
|
||||
findResolvedWorktreeIdForPath(resolvedWorktrees, session.cwd, targetWorktreeId))
|
||||
(recordedPty && !recordedPty.worktreeInferredFromCwd
|
||||
? recordedPty.worktreeId
|
||||
: undefined))
|
||||
const worktreeInferredFromCwd = boundWorktreeId === undefined
|
||||
const worktreeId = worktreeInferredFromCwd
|
||||
? findResolvedWorktreeIdForPath(resolvedWorktrees, session.cwd, targetWorktreeId)
|
||||
: boundWorktreeId
|
||||
const persistedSurface = persistedIndexes.surfaceByPtyId.get(session.id)
|
||||
const restoresExactSurface =
|
||||
persistedSurface &&
|
||||
@@ -174,6 +183,7 @@ export abstract class OrcaRuntimeWithRefreshPtyWorktreeRecordsWithControllerInve
|
||||
...(session.incarnationId ? { incarnationId: session.incarnationId } : {}),
|
||||
agentSessionOwners: session.incarnationId ? (session.agentSessionOwners ?? []) : [],
|
||||
spawnPaneKey: session.paneKey ?? null,
|
||||
...(worktreeInferredFromCwd ? { worktreeInferredFromCwd: true } : {}),
|
||||
...(session.wslDistro !== undefined
|
||||
? { isWsl: Boolean(session.wslDistro), wslDistro: session.wslDistro }
|
||||
: {}),
|
||||
|
||||
@@ -68,6 +68,8 @@ export type RuntimePtyWorktreeRecord = RuntimeTerminalTailState & {
|
||||
readonly ptyId: string
|
||||
readonly incarnationId: PtyIncarnationId | null
|
||||
readonly worktreeId: string
|
||||
/** Set only when no binding named the workspace; worktree-scoped stops never act on a cwd guess (#36716). */
|
||||
readonly worktreeInferredFromCwd?: true
|
||||
readonly connectionId: string | null
|
||||
runtimeSessionOwned: boolean
|
||||
readonly isWsl: boolean | null
|
||||
|
||||
@@ -26,6 +26,7 @@ export function collectWorktreePtyIds(
|
||||
for (const pty of ptysById.values()) {
|
||||
if (
|
||||
worktreeIdsEqual(pty.worktreeId, worktreeId) &&
|
||||
!pty.worktreeInferredFromCwd &&
|
||||
(includeDisconnected || pty.connected) &&
|
||||
worktreePtyBelongsToHost(pty.ptyId, pty.connectionId, hostFence)
|
||||
) {
|
||||
@@ -34,3 +35,11 @@ export function collectWorktreePtyIds(
|
||||
}
|
||||
return ptyIds
|
||||
}
|
||||
|
||||
/** Drops PTYs whose workspace only a cwd guess named; worktree-scoped stops never act on one (#36716). */
|
||||
export function withoutCwdGuesses(
|
||||
ptysById: ReadonlyMap<string, RuntimePtyWorktreeRecord>,
|
||||
ptyIds: ReadonlySet<string>
|
||||
): Set<string> {
|
||||
return new Set([...ptyIds].filter((ptyId) => !ptysById.get(ptyId)?.worktreeInferredFromCwd))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { RuntimeWorktreeTerminalSleepResult } from '../../../../shared/runt
|
||||
import { teardownRpcDeadline } from '../../workspaces/removal/worktree-teardown-deadline'
|
||||
import { sleptNothing } from './worktree-terminal-sleep-result'
|
||||
import type { WorktreeSleepScope } from './worktree-terminal-sleep-key'
|
||||
import { withoutCwdGuesses } from './worktree-pty-collection'
|
||||
// Renderer waits 15s; release the spawn fence and return verified failure before its deadline.
|
||||
const WORKTREE_TERMINAL_SLEEP_TIMEOUT_MS = 12_000
|
||||
|
||||
@@ -23,10 +24,10 @@ export abstract class OrcaRuntimeWithSleepResolvedWorktreeTerminals extends Orca
|
||||
'exclusive',
|
||||
sleepDeadline
|
||||
)
|
||||
const { key } = scope
|
||||
// Why fenced: a same-id twin's PTYs are another host's to sleep, never filed under `key`.
|
||||
const { key, fence } = scope
|
||||
// Fenced: a same-id twin's PTYs are another host's; a cwd guess may be a same-path sibling's (#36716).
|
||||
const liveHere = (fresh: ReadonlySet<string>) =>
|
||||
this.getLivePtyIdsForWorktree(worktree.id, fresh, scope.fence)
|
||||
withoutCwdGuesses(this.ptysById, this.getLivePtyIdsForWorktree(worktree.id, fresh, fence))
|
||||
const existingSleepState = this.terminalSleepStateByHostWorktree.get(key)
|
||||
if (existingSleepState?.phase === 'sleeping') {
|
||||
try {
|
||||
|
||||
@@ -19,7 +19,7 @@ export function spawnSurfaceClaimSequence(graphSequence: number): number {
|
||||
}
|
||||
|
||||
/** Write pane and graph standing together; main-owned layouts store no duplicate claim. */
|
||||
type SurfaceWrites = Pick<TerminalIdentitySource, 'setRecordedSurface'>
|
||||
type SurfaceWrites = Pick<TerminalIdentitySource, 'setRecordedSurface' | 'confirmPtyWorktree'>
|
||||
|
||||
export function recordPtySurfaceClaim(
|
||||
source: SurfaceWrites,
|
||||
@@ -48,6 +48,8 @@ export function recordPtySurface(
|
||||
paneKey: string,
|
||||
graphSequence: number
|
||||
): void {
|
||||
// A pane is a binding, so worktree stops and sleep act on it even if a cwd guess filed it (#36716).
|
||||
source.confirmPtyWorktree(pty)
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { FolderWorkspace } from '../../../../shared/folder-workspaces/folder-workspace-types'
|
||||
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
|
||||
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
const FOLDER: FolderWorkspace = {
|
||||
id: 'notes-1',
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Notes',
|
||||
folderPath: '/fixture/notes',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
const FOLDER_KEY = folderWorkspaceKey(FOLDER.id)
|
||||
const PLACED_LEAF = '33333333-3333-4333-8333-333333333333'
|
||||
|
||||
function runtimeWithCwdOnlyTerminal() {
|
||||
// The host reports no workspace for this terminal, so only its cwd places it (#36716).
|
||||
const live = [{ id: 'pty-agent', cwd: FOLDER.folderPath, title: 'claude' }]
|
||||
const store = {
|
||||
getRepos: () => [],
|
||||
getProjectGroups: () => [{ id: 'group-1', parentPath: '/fixture' }],
|
||||
getFolderWorkspaces: () => [FOLDER],
|
||||
getSettings: () => ({}),
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getWorkspaceSessionHostIds: () => ['local'],
|
||||
getWorkspaceSession: () => getDefaultWorkspaceSession(),
|
||||
applyLayoutCommand: vi.fn(async () => ({
|
||||
ok: true as const,
|
||||
result: { tabId: 'tab-new', leafId: PLACED_LEAF, paneKey: `tab-new:${PLACED_LEAF}` }
|
||||
}))
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the startup pass reads only these store methods.
|
||||
const runtime = new OrcaRuntimeService(store as never)
|
||||
const stopAndWait = vi.fn(async () => true)
|
||||
runtime.setPtyController({
|
||||
stopAndWait,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [...live],
|
||||
listProcessesWithHostScope: async () => ({
|
||||
processes: [...live],
|
||||
hostIds: ['local' as const]
|
||||
})
|
||||
})
|
||||
return { runtime, stopAndWait }
|
||||
}
|
||||
|
||||
describe('startup placement of a cwd-filed live terminal', () => {
|
||||
it('stops the terminal with its workspace once a pane binds it', async () => {
|
||||
const { runtime, stopAndWait } = runtimeWithCwdOnlyTerminal()
|
||||
|
||||
expect(await runtime.placeUnshownLiveTerminals()).toEqual({
|
||||
placed: ['pty-agent'],
|
||||
declined: []
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
await runtime.stopTerminalsForWorktree(`id:${FOLDER_KEY}`)
|
||||
|
||||
expect(stopAndWait).toHaveBeenCalledWith('pty-agent')
|
||||
})
|
||||
})
|
||||
@@ -26,6 +26,8 @@ const nestedSshStore = {
|
||||
getRepos: () => [nestedSshRepo]
|
||||
}
|
||||
|
||||
// A cwd alone never files a terminal for a worktree-scoped stop (#36716); the daemon id names the owner.
|
||||
const LOCAL_PTY_ID = `${TEST_WORKTREE_ID}@@local`
|
||||
const TWIN_PTY_ID = toAppSshPtyId('ssh-twin', 'relay-pty-1')
|
||||
const twinPty = {
|
||||
id: TWIN_PTY_ID,
|
||||
@@ -40,7 +42,7 @@ async function sleepLocalCopy(runtimeStore: typeof store = store, twinLive = fal
|
||||
runtime.onClientEvent((event) => events.push(event))
|
||||
const twin = twinLive ? [twinPty] : []
|
||||
const processLists = [
|
||||
[{ id: 'pty-local', cwd: TEST_WORKTREE_PATH, title: 'Shell' }, ...twin],
|
||||
[{ id: LOCAL_PTY_ID, cwd: TEST_WORKTREE_PATH, title: 'Shell' }, ...twin],
|
||||
twin
|
||||
]
|
||||
const stopAndWait = vi.fn(async (ptyId: string) => {
|
||||
@@ -119,9 +121,9 @@ describe('worktree terminal sleep across same-id host twins', () => {
|
||||
it("sleeping this computer's copy leaves the SSH twin's live terminal running and unfiled", async () => {
|
||||
const { runtime, phases, stopAndWait } = await sleepLocalCopy(store, true)
|
||||
|
||||
expect(stopAndWait.mock.calls.map(([ptyId]) => ptyId)).toEqual(['pty-local'])
|
||||
expect(stopAndWait.mock.calls.map(([ptyId]) => ptyId)).toEqual([LOCAL_PTY_ID])
|
||||
expect(runtime.getTerminalSleepClientEventSnapshot()).toEqual([
|
||||
expect.objectContaining({ phase: 'committed', ptyIds: ['pty-local'] })
|
||||
expect.objectContaining({ phase: 'committed', ptyIds: [LOCAL_PTY_ID] })
|
||||
])
|
||||
const release = await runtime.acquireWorktreeTerminalSpawn(
|
||||
spawnWorktreeTarget(TEST_WORKTREE_ID, 'ssh-twin')
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isPtySessionIdOwnedByWorktree,
|
||||
isServeMintedPtySessionId,
|
||||
mintServePtySessionId,
|
||||
parsePtySessionId
|
||||
} from './pty-session-id-format'
|
||||
|
||||
const UUID = '10000000-0000-4000-8000-0000000000aa'
|
||||
|
||||
// The parsers are unchanged since v1.4.243, so released clients read serve ids the same way (#36716).
|
||||
describe('serve-minted pty session ids', () => {
|
||||
it.each([
|
||||
['folder', 'folder:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'],
|
||||
['POSIX worktree', 'repo-1::/Users/me/src/app:v2/wt'],
|
||||
['SSH worktree', 'repo-ssh::/home/dev/a@b/wt'],
|
||||
['Windows worktree', 'repo-2::C:\\Users\\me\\wt'],
|
||||
['WSL worktree', 'repo-3::\\\\wsl.localhost\\Ubuntu\\home\\me\\wt']
|
||||
])('round-trips a %s workspace id', (_kind, worktreeId) => {
|
||||
const id = mintServePtySessionId(worktreeId, UUID)
|
||||
|
||||
expect(parsePtySessionId(id)).toEqual({ worktreeId })
|
||||
expect(isServeMintedPtySessionId(id)).toBe(true)
|
||||
expect(isPtySessionIdOwnedByWorktree(id, worktreeId)).toBe(true)
|
||||
expect(isPtySessionIdOwnedByWorktree(id, `${worktreeId}-sibling`)).toBe(false)
|
||||
})
|
||||
|
||||
it('still recognizes the workspace-less ids older hosts minted', () => {
|
||||
const legacy = `serve-${UUID}`
|
||||
|
||||
expect(isServeMintedPtySessionId(legacy)).toBe(true)
|
||||
expect(parsePtySessionId(legacy)).toEqual({ worktreeId: null })
|
||||
})
|
||||
|
||||
it('does not treat a workspace path containing serve- as a serve id', () => {
|
||||
expect(isServeMintedPtySessionId('repo::/srv/serve-app@@deadbeef')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -28,3 +28,20 @@ export function isPtySessionIdOwnedByWorktree(sessionId: string, worktreeId: str
|
||||
const separatorIdx = sessionId.lastIndexOf(PTY_SESSION_ID_SEPARATOR)
|
||||
return separatorIdx === -1 || sessionId.slice(0, separatorIdx) === worktreeId
|
||||
}
|
||||
|
||||
const SERVE_PTY_SESSION_TAG = 'serve-'
|
||||
|
||||
/** Serve ids carry their workspace like daemon-minted ids so inventory never re-files them by cwd (#36716). */
|
||||
export function mintServePtySessionId(worktreeId: string, uuid: string): string {
|
||||
return `${worktreeId}${PTY_SESSION_ID_SEPARATOR}${SERVE_PTY_SESSION_TAG}${uuid}`
|
||||
}
|
||||
|
||||
/** Accepts the workspace-less `serve-<uuid>` ids that older hosts minted and still restore. */
|
||||
export function isServeMintedPtySessionId(sessionId: string): boolean {
|
||||
const separatorIdx = sessionId.lastIndexOf(PTY_SESSION_ID_SEPARATOR)
|
||||
const tag =
|
||||
separatorIdx === -1
|
||||
? sessionId
|
||||
: sessionId.slice(separatorIdx + PTY_SESSION_ID_SEPARATOR.length)
|
||||
return tag.startsWith(SERVE_PTY_SESSION_TAG)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { expect, type Page } from '@stablyai/playwright-test'
|
||||
import { quotePosixShell } from '../../../src/shared/shell/launch/shell-argument-quoting'
|
||||
import { isServeMintedPtySessionId } from '../../../src/shared/terminal/pty/lifecycle/pty-session-id-format'
|
||||
|
||||
export type RuntimeRpcCall = <TResult>(method: string, params: unknown) => Promise<TResult>
|
||||
|
||||
@@ -39,7 +40,7 @@ const HOST_TERMINAL_SURFACE_SEPARATOR = '::'
|
||||
function isDaemonPtyIdForm(ptyId: string, worktreeId: string): boolean {
|
||||
return (
|
||||
ptyId.startsWith(`${worktreeId}@@`) &&
|
||||
!ptyId.startsWith('serve-') &&
|
||||
!isServeMintedPtySessionId(ptyId) &&
|
||||
!ptyId.startsWith('ssh-') &&
|
||||
ptyId.slice(`${worktreeId}@@`.length).length > 0
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { decodePairingOffer } from '../../../src/shared/remote-runtime/pairing/p
|
||||
import { pairedRuntimeConnectionFromOffer } from '../../../src/shared/remote-runtime/pairing/paired-runtime-connection'
|
||||
import { subscribeRemoteRuntimeRequest } from '../../../src/shared/remote-runtime/remote-runtime-client'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../src/shared/runtime-session-contracts'
|
||||
import { isServeMintedPtySessionId } from '../../../src/shared/terminal/pty/lifecycle/pty-session-id-format'
|
||||
import type {
|
||||
RuntimeTerminalListResult,
|
||||
RuntimeTerminalRead
|
||||
@@ -47,7 +48,7 @@ export class IdLabels {
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
const kind = id.includes('@@') ? 'pty@@' : id.startsWith('serve-') ? 'ptyServe' : 'pty'
|
||||
const kind = isServeMintedPtySessionId(id) ? 'ptyServe' : id.includes('@@') ? 'pty@@' : 'pty'
|
||||
return this.label(kind, id)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user