mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Recover exported terminal handles from live PTYs (#7162)
* Recover exported terminal handles from live PTYs * Guard terminal-handle adoption: first-wins, no collisions Discovery adoption of ORCA_TERMINAL_HANDLE is now skipped when the pty already has a handle bound this session (re-keying would strand waiters registered under the old handle) or when the reported handle is already bound to a different pty (provider-reported values are not trusted to be collision-free). Also adds the relay why-comment and PtyProcessSummary type alias from review feedback. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -19,7 +19,12 @@ import {
|
||||
type SessionInfo,
|
||||
type TakePendingOutputResult
|
||||
} from './types'
|
||||
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
|
||||
import type {
|
||||
IPtyProvider,
|
||||
PtyProcessInfo,
|
||||
PtySpawnOptions,
|
||||
PtySpawnResult
|
||||
} from '../providers/types'
|
||||
import { isShellProcess } from '../../shared/agent-detection'
|
||||
import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition'
|
||||
import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery'
|
||||
@@ -517,7 +522,7 @@ export class DaemonPtyAdapter implements IPtyProvider {
|
||||
return { alive, killed }
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
|
||||
async listProcesses(): Promise<PtyProcessInfo[]> {
|
||||
await this.ensureConnected()
|
||||
const result = await this.client.request<ListSessionsResult>('listSessions', undefined)
|
||||
return result.sessions
|
||||
@@ -525,7 +530,8 @@ export class DaemonPtyAdapter implements IPtyProvider {
|
||||
.map((s) => ({
|
||||
id: s.sessionId,
|
||||
cwd: s.cwd ?? '',
|
||||
title: 'shell'
|
||||
title: 'shell',
|
||||
...(s.terminalHandle ? { terminalHandle: s.terminalHandle } : {})
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { DaemonPtyAdapter } from './daemon-pty-adapter'
|
||||
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
|
||||
import type {
|
||||
IPtyProvider,
|
||||
PtyProcessInfo,
|
||||
PtySpawnOptions,
|
||||
PtySpawnResult
|
||||
} from '../providers/types'
|
||||
|
||||
export class DaemonPtyRouter implements IPtyProvider {
|
||||
private current: DaemonPtyAdapter
|
||||
@@ -124,7 +129,7 @@ export class DaemonPtyRouter implements IPtyProvider {
|
||||
await this.current.revive(state)
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
|
||||
async listProcesses(): Promise<PtyProcessInfo[]> {
|
||||
// Why: runtime exact-stop/liveness flows must fail closed if any adapter
|
||||
// cannot provide a trustworthy process list.
|
||||
const results = await Promise.all(this.allAdapters().map((adapter) => adapter.listProcesses()))
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { DaemonPtyAdapter } from './daemon-pty-adapter'
|
||||
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
|
||||
import type {
|
||||
IPtyProvider,
|
||||
PtyProcessInfo,
|
||||
PtySpawnOptions,
|
||||
PtySpawnResult
|
||||
} from '../providers/types'
|
||||
|
||||
type ManagedPtyProvider = IPtyProvider & {
|
||||
disconnectOnly?: () => Promise<void>
|
||||
@@ -135,7 +140,7 @@ export class DegradedDaemonPtyProvider implements IPtyProvider {
|
||||
await this.fallback.revive(state)
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
|
||||
async listProcesses(): Promise<PtyProcessInfo[]> {
|
||||
const results = await Promise.all(
|
||||
this.allProviders().map((provider) => provider.listProcesses())
|
||||
)
|
||||
|
||||
@@ -56,6 +56,7 @@ export type SessionOptions = {
|
||||
sessionId: string
|
||||
cols: number
|
||||
rows: number
|
||||
terminalHandle?: string
|
||||
subprocess: SubprocessHandle
|
||||
shellReadySupported: boolean
|
||||
shellReadyTimeoutMs?: number
|
||||
@@ -76,6 +77,7 @@ type AttachedClient = {
|
||||
|
||||
export class Session {
|
||||
readonly sessionId: string
|
||||
readonly terminalHandle: string | null
|
||||
private _state: SessionState = 'running'
|
||||
private _shellState: ShellReadyState
|
||||
private _exitCode: number | null = null
|
||||
@@ -97,6 +99,7 @@ export class Session {
|
||||
|
||||
constructor(opts: SessionOptions) {
|
||||
this.sessionId = opts.sessionId
|
||||
this.terminalHandle = opts.terminalHandle ?? null
|
||||
this.subprocess = opts.subprocess
|
||||
this.onSessionExit = opts.onExit
|
||||
const size = normalizePtySize(opts.cols, opts.rows)
|
||||
|
||||
@@ -136,6 +136,7 @@ export class TerminalHost {
|
||||
sessionId: opts.sessionId,
|
||||
cols: size.cols,
|
||||
rows: size.rows,
|
||||
terminalHandle: opts.env?.ORCA_TERMINAL_HANDLE,
|
||||
subprocess,
|
||||
shellReadySupported: opts.shellReadySupported ?? false,
|
||||
// Why: reap the dead session (dispose emulator + drop from the map) the
|
||||
@@ -301,6 +302,7 @@ export class TerminalHost {
|
||||
state: session.state,
|
||||
shellState: session.shellState,
|
||||
isAlive: true,
|
||||
...(session.terminalHandle ? { terminalHandle: session.terminalHandle } : {}),
|
||||
pid: session.pid,
|
||||
cwd: session.getCwd(),
|
||||
cols: size?.cols ?? 0,
|
||||
|
||||
@@ -324,6 +324,7 @@ export type SessionInfo = {
|
||||
state: SessionState
|
||||
shellState: ShellReadyState
|
||||
isAlive: boolean
|
||||
terminalHandle?: string
|
||||
pid: number | null
|
||||
cwd: string | null
|
||||
cols: number
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
updateHistFileForFallback,
|
||||
logHistoryInjection
|
||||
} from '../terminal-history'
|
||||
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from './types'
|
||||
import type { IPtyProvider, PtyProcessInfo, PtySpawnOptions, PtySpawnResult } from './types'
|
||||
import {
|
||||
ensureNodePtySpawnHelperExecutable,
|
||||
validateWorkingDirectory,
|
||||
@@ -67,6 +67,7 @@ let ptyCounter = 0
|
||||
const ptyProcesses = new Map<string, pty.IPty>()
|
||||
const ptyShellName = new Map<string, string>()
|
||||
const ptyAgentForegroundContextPaths = new Map<string, string[]>()
|
||||
const ptyTerminalHandle = new Map<string, string>()
|
||||
// Why: node-pty's onData/onExit register native NAPI ThreadSafeFunction
|
||||
// callbacks. If the PTY is killed without disposing these listeners, the
|
||||
// stale callbacks survive into node::FreeEnvironment() where NAPI attempts
|
||||
@@ -187,6 +188,7 @@ function clearPtyState(id: string): void {
|
||||
ptyProcesses.delete(id)
|
||||
ptyShellName.delete(id)
|
||||
ptyAgentForegroundContextPaths.delete(id)
|
||||
ptyTerminalHandle.delete(id)
|
||||
ptyLoadGeneration.delete(id)
|
||||
}
|
||||
|
||||
@@ -680,6 +682,9 @@ export class LocalPtyProvider implements IPtyProvider {
|
||||
const proc = spawnResult.process
|
||||
ptyProcesses.set(id, proc)
|
||||
ptyShellName.set(id, getSpawnedShellName(shellPath))
|
||||
if (finalEnv.ORCA_TERMINAL_HANDLE) {
|
||||
ptyTerminalHandle.set(id, finalEnv.ORCA_TERMINAL_HANDLE)
|
||||
}
|
||||
ptyAgentForegroundContextPaths.set(
|
||||
id,
|
||||
getAgentForegroundContextPaths({ cwd: args.cwd, worktreeId: args.worktreeId })
|
||||
@@ -945,11 +950,12 @@ export class LocalPtyProvider implements IPtyProvider {
|
||||
/* re-spawning handles local revival */
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
|
||||
async listProcesses(): Promise<PtyProcessInfo[]> {
|
||||
return Array.from(ptyProcesses.entries()).map(([id, proc]) => ({
|
||||
id,
|
||||
cwd: '',
|
||||
title: proc.process || ptyShellName.get(id) || 'shell'
|
||||
title: proc.process || ptyShellName.get(id) || 'shell',
|
||||
...(ptyTerminalHandle.get(id) ? { terminalHandle: ptyTerminalHandle.get(id) } : {})
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
|
||||
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from './types'
|
||||
import type { IPtyProvider, PtyProcessInfo, PtySpawnOptions, PtySpawnResult } from './types'
|
||||
import { toAppSshPtyId, toRelaySshPtyId } from './ssh-pty-id'
|
||||
import { seedPowerlevel10kWizardEnv } from '../pty/powerlevel10k-wizard-env'
|
||||
|
||||
@@ -254,9 +254,9 @@ export class SshPtyProvider implements IPtyProvider {
|
||||
await this.mux.request('pty.revive', { state })
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
|
||||
async listProcesses(): Promise<PtyProcessInfo[]> {
|
||||
const result = await this.mux.request('pty.listProcesses')
|
||||
return (result as { id: string; cwd: string; title: string }[]).map((session) => ({
|
||||
return (result as PtyProcessInfo[]).map((session) => ({
|
||||
...session,
|
||||
id: this.toAppPtyId(session.id)
|
||||
}))
|
||||
|
||||
@@ -99,6 +99,14 @@ export type PtySpawnResult = {
|
||||
}
|
||||
}
|
||||
|
||||
export type PtyProcessInfo = {
|
||||
id: string
|
||||
cwd: string
|
||||
title: string
|
||||
/** Trusted ORCA_TERMINAL_HANDLE exported into this PTY, when known. */
|
||||
terminalHandle?: string
|
||||
}
|
||||
|
||||
export type IPtyProvider = {
|
||||
spawn(opts: PtySpawnOptions): Promise<PtySpawnResult>
|
||||
attach(id: string): Promise<void>
|
||||
@@ -128,7 +136,7 @@ export type IPtyProvider = {
|
||||
getForegroundProcess(id: string): Promise<string | null>
|
||||
serialize(ids: string[]): Promise<string>
|
||||
revive(state: string): Promise<void>
|
||||
listProcesses(): Promise<{ id: string; cwd: string; title: string }[]>
|
||||
listProcesses(): Promise<PtyProcessInfo[]>
|
||||
getDefaultShell(): Promise<string>
|
||||
getProfiles(): Promise<{ name: string; path: string }[]>
|
||||
onData(callback: (payload: { id: string; data: string }) => void): () => void
|
||||
|
||||
@@ -10497,6 +10497,112 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(read.tail).toEqual(['ready'])
|
||||
})
|
||||
|
||||
it('recovers exported ORCA_TERMINAL_HANDLE from discovered live PTY sessions', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const writes: string[] = []
|
||||
runtime.setPtyController({
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [
|
||||
{
|
||||
id: 'pty-1',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
title: 'claude',
|
||||
terminalHandle: 'term_exported'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const listed = await runtime.listTerminals()
|
||||
expect(listed.terminals[0]?.handle).toBe('term_exported')
|
||||
|
||||
runtime.onPtyData('pty-1', 'after restart\n', 100)
|
||||
await expect(runtime.readTerminal('term_exported')).resolves.toMatchObject({
|
||||
handle: 'term_exported',
|
||||
tail: ['after restart']
|
||||
})
|
||||
await expect(
|
||||
runtime.sendTerminal('term_exported', { text: 'still writable' })
|
||||
).resolves.toMatchObject({
|
||||
handle: 'term_exported',
|
||||
accepted: true
|
||||
})
|
||||
expect(writes).toEqual(['still writable'])
|
||||
})
|
||||
|
||||
it('does not adopt a discovered terminal handle already bound to another live PTY', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const writesByPty = new Map<string, string[]>()
|
||||
runtime.setPtyController({
|
||||
write: (ptyId, data) => {
|
||||
writesByPty.set(ptyId, [...(writesByPty.get(ptyId) ?? []), data])
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [
|
||||
{
|
||||
id: 'pty-victim',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
title: 'claude',
|
||||
terminalHandle: 'term_victim'
|
||||
},
|
||||
{
|
||||
id: 'pty-imposter',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
title: 'claude',
|
||||
terminalHandle: 'term_victim'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const listed = await runtime.listTerminals()
|
||||
const handles = listed.terminals.map((terminal) => terminal.handle)
|
||||
expect(handles).toContain('term_victim')
|
||||
expect(new Set(handles).size).toBe(handles.length)
|
||||
|
||||
await expect(
|
||||
runtime.sendTerminal('term_victim', { text: 'for victim' })
|
||||
).resolves.toMatchObject({ accepted: true })
|
||||
expect(writesByPty.get('pty-victim')).toEqual(['for victim'])
|
||||
expect(writesByPty.has('pty-imposter')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps an already-bound terminal handle when discovery reports a different exported one', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const writes: string[] = []
|
||||
runtime.setPtyController({
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [
|
||||
{
|
||||
id: 'pty-1',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
title: 'claude',
|
||||
terminalHandle: 'term_from_env'
|
||||
}
|
||||
]
|
||||
})
|
||||
runtime.registerPreAllocatedHandleForPty('pty-1', 'term_already_bound')
|
||||
|
||||
const listed = await runtime.listTerminals()
|
||||
expect(listed.terminals[0]?.handle).toBe('term_already_bound')
|
||||
await expect(
|
||||
runtime.sendTerminal('term_already_bound', { text: 'still routed' })
|
||||
).resolves.toMatchObject({ accepted: true })
|
||||
expect(writes).toEqual(['still routed'])
|
||||
// the reported-but-not-adopted handle must not resolve to the live pty
|
||||
await expect(runtime.readTerminal('term_from_env')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('binds advertised URLs for renderer-restored PTYs that skip registerPty', () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
|
||||
|
||||
@@ -661,7 +661,7 @@ import { closeLocalWatcherForWorktreePath } from '../ipc/filesystem-watcher'
|
||||
import { HeadlessEmulator, type HeadlessEmulatorOptions } from '../daemon/headless-emulator'
|
||||
import { killAllProcessesForWorktree } from './worktree-teardown'
|
||||
import { MOBILE_SUBSCRIBE_SCROLLBACK_ROWS } from './scrollback-limits'
|
||||
import type { IFilesystemProvider, IPtyProvider } from '../providers/types'
|
||||
import type { IFilesystemProvider, IPtyProvider, PtyProcessInfo } from '../providers/types'
|
||||
import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
|
||||
import {
|
||||
assertFolderWorkspacePathUsable,
|
||||
@@ -1037,7 +1037,7 @@ type RuntimePtyController = {
|
||||
hasChildProcesses?(ptyId: string): Promise<boolean>
|
||||
clearBuffer?(ptyId: string): Promise<void>
|
||||
resize?(ptyId: string, cols: number, rows: number): boolean
|
||||
listProcesses?(): Promise<{ id: string; cwd: string; title: string }[]>
|
||||
listProcesses?(): Promise<PtyProcessInfo[]>
|
||||
serializeBuffer?(
|
||||
ptyId: string,
|
||||
opts?: { scrollbackRows?: number; altScreenForcesZeroRows?: boolean }
|
||||
@@ -5041,6 +5041,46 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
private adoptControllerTerminalHandle(ptyId: string, handle: string | undefined): void {
|
||||
const trimmed = handle?.trim()
|
||||
if (!trimmed || !trimmed.startsWith('term_')) {
|
||||
return
|
||||
}
|
||||
if (this.isTerminalHandleAdoptionBlocked(ptyId, trimmed)) {
|
||||
return
|
||||
}
|
||||
// Why: after an app/runtime restart, the live PTY child still has its
|
||||
// original ORCA_TERMINAL_HANDLE, but the runtime's in-memory map is gone.
|
||||
this.registerPreAllocatedHandleForPty(ptyId, trimmed)
|
||||
}
|
||||
|
||||
// Why: adoption is best-effort restart recovery and must be first-wins.
|
||||
// Re-keying a pty that already has a handle this session would strand
|
||||
// waiters registered under the old handle, and provider-reported values
|
||||
// are not trusted to be collision-free — a handle bound to a different
|
||||
// pty must never be stolen by a later report.
|
||||
private isTerminalHandleAdoptionBlocked(ptyId: string, handle: string): boolean {
|
||||
if (this.handleByPtyId.get(ptyId) ?? this.findHandleForPtyRecord(ptyId)) {
|
||||
return true
|
||||
}
|
||||
for (const leaf of this.getLeavesForPty(ptyId)) {
|
||||
const issued = this.handleByLeafKey.get(this.getLeafKey(leaf.tabId, leaf.leafId))
|
||||
if (issued && issued !== handle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
const existingRecord = this.handles.get(handle)
|
||||
if (existingRecord && existingRecord.ptyId !== ptyId) {
|
||||
return true
|
||||
}
|
||||
for (const [otherPtyId, otherHandle] of this.handleByPtyId) {
|
||||
if (otherHandle === handle && otherPtyId !== ptyId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onPtySpawned(ptyId: string): void {
|
||||
const pty = this.getOrCreatePtyWorktreeRecord(ptyId)
|
||||
if (pty) {
|
||||
@@ -17789,6 +17829,7 @@ export class OrcaRuntimeService {
|
||||
const sessions = sessionsResult.value
|
||||
const livePtyIds = new Set(sessions.map((session) => session.id))
|
||||
for (const session of sessions) {
|
||||
this.adoptControllerTerminalHandle(session.id, session.terminalHandle)
|
||||
const worktreeId =
|
||||
inferWorktreeIdFromPtyId(session.id) ??
|
||||
findResolvedWorktreeIdForPath(resolvedWorktrees, session.cwd)
|
||||
|
||||
@@ -62,6 +62,7 @@ type ManagedPty = {
|
||||
paneKey?: string
|
||||
tabId?: string
|
||||
worktreeId?: string
|
||||
terminalHandle?: string
|
||||
startupCommand?: ManagedStartupCommand
|
||||
}
|
||||
|
||||
@@ -156,6 +157,13 @@ function resolvePtyShellOverride(shellOverride: string): string {
|
||||
return resolveWindowsGitBashShellPath(shellOverride) ?? shellOverride
|
||||
}
|
||||
|
||||
type PtyProcessSummary = {
|
||||
id: string
|
||||
cwd: string
|
||||
title: string
|
||||
terminalHandle?: string
|
||||
}
|
||||
|
||||
type SerializedPtyEntry = {
|
||||
id: string
|
||||
pid: number
|
||||
@@ -165,6 +173,7 @@ type SerializedPtyEntry = {
|
||||
paneKey?: string
|
||||
tabId?: string
|
||||
worktreeId?: string
|
||||
terminalHandle?: string
|
||||
}
|
||||
|
||||
export type PtyExitListener = (event: { id: string; paneKey?: string }) => void
|
||||
@@ -539,6 +548,10 @@ export class PtyHandler {
|
||||
// for overlay resolution; runtime-owned PTYs opt into relay delivery
|
||||
// because no renderer TerminalPane exists to type the command.
|
||||
const paneKey = typeof env?.ORCA_PANE_KEY === 'string' ? env.ORCA_PANE_KEY : undefined
|
||||
// Why: kept so a restarted runtime can re-adopt this live PTY under its
|
||||
// originally-exported handle (reported via listProcesses, survives revive).
|
||||
const terminalHandle =
|
||||
typeof env?.ORCA_TERMINAL_HANDLE === 'string' ? env.ORCA_TERMINAL_HANDLE : undefined
|
||||
const command = typeof params.command === 'string' ? params.command : undefined
|
||||
const terminalWindowsWslDistro =
|
||||
typeof params.terminalWindowsWslDistro === 'string' ? params.terminalWindowsWslDistro : null
|
||||
@@ -589,6 +602,7 @@ export class PtyHandler {
|
||||
paneKey,
|
||||
tabId,
|
||||
worktreeId,
|
||||
...(terminalHandle ? { terminalHandle } : {}),
|
||||
...(shouldProviderDeliverCommand
|
||||
? {
|
||||
startupCommand: {
|
||||
@@ -819,12 +833,17 @@ export class PtyHandler {
|
||||
return await getForegroundProcessName(managed.pty.pid, managed.pty.process || null)
|
||||
}
|
||||
|
||||
private async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
|
||||
const results: { id: string; cwd: string; title: string }[] = []
|
||||
private async listProcesses(): Promise<PtyProcessSummary[]> {
|
||||
const results: PtyProcessSummary[] = []
|
||||
for (const [id, managed] of this.ptys) {
|
||||
const title =
|
||||
(await getForegroundProcessName(managed.pty.pid, managed.pty.process || null)) || 'shell'
|
||||
results.push({ id, cwd: managed.initialCwd, title })
|
||||
results.push({
|
||||
id,
|
||||
cwd: managed.initialCwd,
|
||||
title,
|
||||
...(managed.terminalHandle ? { terminalHandle: managed.terminalHandle } : {})
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -846,7 +865,8 @@ export class PtyHandler {
|
||||
cwd: managed.initialCwd,
|
||||
paneKey: managed.paneKey,
|
||||
tabId: managed.tabId,
|
||||
worktreeId: managed.worktreeId
|
||||
worktreeId: managed.worktreeId,
|
||||
...(managed.terminalHandle ? { terminalHandle: managed.terminalHandle } : {})
|
||||
})
|
||||
}
|
||||
return JSON.stringify(entries)
|
||||
@@ -884,6 +904,9 @@ export class PtyHandler {
|
||||
if (entry.worktreeId) {
|
||||
revivedEnv.ORCA_WORKTREE_ID = entry.worktreeId
|
||||
}
|
||||
if (entry.terminalHandle) {
|
||||
revivedEnv.ORCA_TERMINAL_HANDLE = entry.terminalHandle
|
||||
}
|
||||
const shell = resolveDefaultShell()
|
||||
// Why: `command` is intentionally absent from this revive path because
|
||||
// SerializedPtyEntry (see line 99) does not persist it — ManagedPty
|
||||
@@ -914,7 +937,8 @@ export class PtyHandler {
|
||||
buffered: '',
|
||||
paneKey: entry.paneKey,
|
||||
tabId: entry.tabId,
|
||||
worktreeId: entry.worktreeId
|
||||
worktreeId: entry.worktreeId,
|
||||
...(entry.terminalHandle ? { terminalHandle: entry.terminalHandle } : {})
|
||||
})
|
||||
|
||||
// Why: nextId starts at 1 and is only incremented by spawn(). Revived
|
||||
|
||||
Reference in New Issue
Block a user