mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
fix: preserve live agent PTYs through graph hydration (#11789)
This commit is contained in:
+320
-218
@@ -527,6 +527,7 @@ import type {
|
||||
IFilesystemProvider,
|
||||
IPtyProvider,
|
||||
PtyProcessInfo,
|
||||
PtySpawnResult,
|
||||
PtyTransientFact
|
||||
} from '../providers/types'
|
||||
import { ClaudeAgentTeamsService } from './claude-agent-teams-service'
|
||||
@@ -1531,6 +1532,32 @@ type HeadlessSeedMetadata = {
|
||||
}
|
||||
|
||||
type RuntimePtyController = {
|
||||
claimStablePaneCreate?(args: {
|
||||
worktreeId: string
|
||||
connectionId: string | null
|
||||
tabId: string
|
||||
leafId: string
|
||||
}): () => void
|
||||
adoptStablePane?(opts: {
|
||||
cols: number
|
||||
rows: number
|
||||
cwd?: string
|
||||
connectionId: string | null
|
||||
worktreeId: string
|
||||
preAllocatedHandle: string
|
||||
tabId: string
|
||||
leafId: string
|
||||
}): Promise<{
|
||||
result: PtySpawnResult
|
||||
owner: {
|
||||
handle?: string
|
||||
tabId: string
|
||||
leafId: string
|
||||
ptyId: string
|
||||
incarnationId?: string
|
||||
}
|
||||
materialized?: true
|
||||
} | null>
|
||||
spawn?(opts: {
|
||||
cols: number
|
||||
rows: number
|
||||
@@ -1558,10 +1585,22 @@ type RuntimePtyController = {
|
||||
agentSessionCreateOperationId?: string
|
||||
signal?: AbortSignal
|
||||
onPtySpawnCommitted?: () => void
|
||||
adoptedStablePane?: {
|
||||
result: PtySpawnResult
|
||||
owner: {
|
||||
handle?: string
|
||||
tabId: string
|
||||
leafId: string
|
||||
ptyId: string
|
||||
incarnationId?: string
|
||||
}
|
||||
materialized?: true
|
||||
}
|
||||
}): Promise<{
|
||||
id: string
|
||||
incarnationId?: PtyIncarnationId
|
||||
wslDistro?: string
|
||||
stablePaneOwner?: { handle: string; tabId: string; leafId: string }
|
||||
agentSessionEnsure?: AgentSessionClaimedSpawnResult
|
||||
}>
|
||||
write(ptyId: string, data: string): boolean
|
||||
@@ -15756,6 +15795,7 @@ export class OrcaRuntimeService {
|
||||
tabId: parsed?.tabId ?? record?.tabId ?? '',
|
||||
leafId: parsed?.leafId ?? record?.leafId ?? '',
|
||||
ptyId: record?.ptyId ?? null,
|
||||
connected: pty?.connected === true,
|
||||
...(worktreeId ? { worktreeId } : {}),
|
||||
...this.getPtyExecutionHostMetadata(record?.ptyId ?? pty?.ptyId ?? null)
|
||||
}
|
||||
@@ -24440,236 +24480,298 @@ export class OrcaRuntimeService {
|
||||
let tabId = canAdoptPaneIdentity ? (hintedTabId as string) : randomUUID()
|
||||
let leafId = canAdoptPaneIdentity ? (launchOpts.leafId as string) : randomUUID()
|
||||
let paneKey = makePaneKey(tabId, leafId)
|
||||
const launchToken = launchOpts.launchConfig
|
||||
? (launchOpts.launchToken ?? randomUUID())
|
||||
: undefined
|
||||
const baseEnv = {
|
||||
...launchOpts.env,
|
||||
...(launchToken ? { ORCA_AGENT_LAUNCH_TOKEN: launchToken } : {})
|
||||
}
|
||||
const claudeAgentTeamsSourceCommand =
|
||||
launchOpts.claudeAgentTeamsSourceCommand?.trim() || launchOpts.command?.trim() || undefined
|
||||
const claudeAgentTeamsMode = this.store?.getSettings?.().claudeAgentTeamsMode
|
||||
const effectiveClaudeAgentTeamsMode = inferCapturedClaudeAgentTeamsMode(
|
||||
launchOpts.launchConfig,
|
||||
claudeAgentTeamsSourceCommand,
|
||||
claudeAgentTeamsMode
|
||||
)
|
||||
const agentTeamsPlan = await buildClaudeAgentTeamsLaunchPlan({
|
||||
command: claudeAgentTeamsSourceCommand,
|
||||
mode: effectiveClaudeAgentTeamsMode,
|
||||
baseEnv: {
|
||||
...process.env,
|
||||
...baseEnv
|
||||
},
|
||||
createTeamEnv: (shimDir, shimBin) =>
|
||||
this.claudeAgentTeams.createLaunchEnv({
|
||||
leaderHandle: preAllocatedHandle,
|
||||
baseEnv: {
|
||||
...process.env,
|
||||
...baseEnv
|
||||
},
|
||||
shimDir,
|
||||
shimBin
|
||||
}).env
|
||||
})
|
||||
const sequencedStartupCommand =
|
||||
agentTeamsPlan &&
|
||||
claudeAgentTeamsSourceCommand &&
|
||||
launchOpts.command &&
|
||||
claudeAgentTeamsSourceCommand !== launchOpts.command
|
||||
? agentTeamsPlan.command
|
||||
: undefined
|
||||
const effectiveLaunchConfig =
|
||||
launchOpts.launchConfig && agentTeamsPlan
|
||||
? {
|
||||
...launchOpts.launchConfig,
|
||||
agentCommand: launchOpts.launchConfig.agentCommand
|
||||
? effectiveClaudeAgentTeamsMode === 'in-process' || process.platform === 'win32'
|
||||
? addClaudeTeammateModeInProcess(launchOpts.launchConfig.agentCommand)
|
||||
: addClaudeTeammateModeAuto(launchOpts.launchConfig.agentCommand)
|
||||
: agentTeamsPlan.command,
|
||||
agentEnv: {
|
||||
...launchOpts.launchConfig.agentEnv,
|
||||
...agentTeamsPlan.env
|
||||
}
|
||||
}
|
||||
: launchOpts.launchConfig
|
||||
// Why: setup/agent sequencing wraps the PTY launch in a wait shell before
|
||||
// Claude Agent Teams runs. Preserve the direct Claude command separately
|
||||
// so the wrapper can exec the teammate-mode variant after setup completes.
|
||||
const env = this.buildTerminalWorkspaceEnv(
|
||||
workspace,
|
||||
{
|
||||
...baseEnv,
|
||||
...(sequencedStartupCommand
|
||||
? { [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: sequencedStartupCommand }
|
||||
: {})
|
||||
},
|
||||
paneKey,
|
||||
tabId,
|
||||
agentTeamsPlan?.env
|
||||
)
|
||||
const terminalColorQueryReplies =
|
||||
launchOpts.terminalColorQueryReplies ?? getTerminalViewColorQueryReplyColors()
|
||||
if (launchOpts.signal?.aborted) {
|
||||
throw new Error('client_disconnected')
|
||||
}
|
||||
const result = await this.ptyController.spawn({
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
cwd,
|
||||
command: sequencedStartupCommand
|
||||
? launchOpts.command
|
||||
: (agentTeamsPlan?.command ?? launchOpts.command),
|
||||
launchAgent: launchOpts.launchAgent,
|
||||
commandDelivery: 'provider',
|
||||
startupCommandDelivery: launchOpts.startupCommandDelivery,
|
||||
env,
|
||||
envToDelete: mergeTerminalEnvDeletionKeys(
|
||||
launchOpts.envToDelete,
|
||||
agentTeamsPlan?.envToDelete
|
||||
),
|
||||
resumeProviderSession: launchOpts.resumeProviderSession,
|
||||
telemetry: launchOpts.telemetry,
|
||||
connectionId: workspace.connectionId,
|
||||
const claimedStablePaneCreate = this.ptyController.claimStablePaneCreate?.({
|
||||
worktreeId: workspace.id,
|
||||
preAllocatedHandle,
|
||||
connectionId: workspace.connectionId,
|
||||
tabId,
|
||||
leafId,
|
||||
...(terminalColorQueryReplies ? { terminalColorQueryReplies } : {}),
|
||||
...(launchOpts.agentSessionClaim
|
||||
? {
|
||||
agentSessionEnsure: {
|
||||
claim: launchOpts.agentSessionClaim,
|
||||
surface: {
|
||||
worktreeId: workspace.id,
|
||||
tabId,
|
||||
leafId,
|
||||
terminalHandle: preAllocatedHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(launchOpts.agentSessionCreateOperationId
|
||||
? { agentSessionCreateOperationId: launchOpts.agentSessionCreateOperationId }
|
||||
: {}),
|
||||
...(launchOpts.signal ? { signal: launchOpts.signal } : {}),
|
||||
...(launchOpts.onPtySpawnCommitted ? { onPtySpawnCommitted: reportPtySpawnCommitted } : {}),
|
||||
...(launchOpts.sessionId ? { sessionId: launchOpts.sessionId } : {}),
|
||||
// Why: a headless-created pane has no renderer session writer. Persist
|
||||
// its tab/leaf binding at spawn so a later promoted window reattaches
|
||||
// the live daemon or SSH PTY instead of replacing it with a fresh one.
|
||||
// Re-check freshly: the entry-time snapshot can go stale across the
|
||||
// awaits above if the authoritative window is destroyed mid-spawn.
|
||||
...(launchOpts.persistHostSessionBinding || this.getAvailableAuthoritativeWindow() === null
|
||||
? { persistHostSessionBinding: true }
|
||||
: {})
|
||||
leafId
|
||||
})
|
||||
reportPtySpawnCommitted()
|
||||
if (result.agentSessionEnsure) {
|
||||
const canonicalSurface = result.agentSessionEnsure.owner.surface
|
||||
preAllocatedHandle = canonicalSurface.terminalHandle
|
||||
tabId = canonicalSurface.tabId
|
||||
leafId = canonicalSurface.leafId
|
||||
paneKey = makePaneKey(tabId, leafId)
|
||||
let stablePaneCreateReleased = false
|
||||
const releaseStablePaneCreate = (): void => {
|
||||
if (stablePaneCreateReleased) {
|
||||
return
|
||||
}
|
||||
stablePaneCreateReleased = true
|
||||
claimedStablePaneCreate?.()
|
||||
}
|
||||
try {
|
||||
this.assertPtyDidNotExitBeforeRegistration(result.id, result.incarnationId)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'agent_session_exited_during_start') {
|
||||
this.releaseRejectedPtyRegistrationFence(result.id, result.incarnationId)
|
||||
if (launchOpts.signal?.aborted) {
|
||||
throw new Error('client_disconnected')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
this.registerPreAllocatedHandleForPty(result.id, preAllocatedHandle)
|
||||
if (result.wslDistro) {
|
||||
this.preparePtyExecutionContext(result.id, result.wslDistro)
|
||||
}
|
||||
this.registerPty(result.id, workspace.id, workspace.connectionId, {
|
||||
tabId,
|
||||
leafId,
|
||||
...(result.incarnationId ? { incarnationId: result.incarnationId } : {})
|
||||
})
|
||||
const pty = this.getOrCreatePtyWorktreeRecord(result.id)
|
||||
if (pty) {
|
||||
if (launchOpts.persistHostSessionBinding) {
|
||||
pty.runtimeSessionOwned = true
|
||||
const adoptedBeforeLaunch = await this.ptyController.adoptStablePane?.({
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
cwd,
|
||||
connectionId: workspace.connectionId,
|
||||
worktreeId: workspace.id,
|
||||
preAllocatedHandle,
|
||||
tabId,
|
||||
leafId
|
||||
})
|
||||
const launchToken = launchOpts.launchConfig
|
||||
? (launchOpts.launchToken ?? randomUUID())
|
||||
: undefined
|
||||
const baseEnv = {
|
||||
...launchOpts.env,
|
||||
...(launchToken ? { ORCA_AGENT_LAUNCH_TOKEN: launchToken } : {})
|
||||
}
|
||||
if (launchOpts.title) {
|
||||
const observedAt = this.nextTitleObservationSequence()
|
||||
pty.title = launchOpts.title
|
||||
pty.titleUpdatedAt = observedAt
|
||||
this.setPtyManagementTitleFromObservedTitle(pty, launchOpts.title, observedAt)
|
||||
} else {
|
||||
pty.title = null
|
||||
pty.titleUpdatedAt = null
|
||||
const claudeAgentTeamsSourceCommand =
|
||||
launchOpts.claudeAgentTeamsSourceCommand?.trim() ||
|
||||
launchOpts.command?.trim() ||
|
||||
undefined
|
||||
const claudeAgentTeamsMode = this.store?.getSettings?.().claudeAgentTeamsMode
|
||||
const effectiveClaudeAgentTeamsMode = inferCapturedClaudeAgentTeamsMode(
|
||||
launchOpts.launchConfig,
|
||||
claudeAgentTeamsSourceCommand,
|
||||
claudeAgentTeamsMode
|
||||
)
|
||||
let agentTeamsPlan: Awaited<ReturnType<typeof buildClaudeAgentTeamsLaunchPlan>> | undefined
|
||||
try {
|
||||
agentTeamsPlan = adoptedBeforeLaunch
|
||||
? undefined
|
||||
: await buildClaudeAgentTeamsLaunchPlan({
|
||||
command: claudeAgentTeamsSourceCommand,
|
||||
mode: effectiveClaudeAgentTeamsMode,
|
||||
baseEnv: {
|
||||
...process.env,
|
||||
...baseEnv
|
||||
},
|
||||
createTeamEnv: (shimDir, shimBin) =>
|
||||
this.claudeAgentTeams.createLaunchEnv({
|
||||
leaderHandle: preAllocatedHandle,
|
||||
baseEnv: {
|
||||
...process.env,
|
||||
...baseEnv
|
||||
},
|
||||
shimDir,
|
||||
shimBin
|
||||
}).env
|
||||
})
|
||||
} catch (error) {
|
||||
releaseStablePaneCreate?.()
|
||||
throw error
|
||||
}
|
||||
pty.tabId = tabId
|
||||
pty.paneKey = paneKey
|
||||
pty.launchConfig = effectiveLaunchConfig
|
||||
? copySleepingAgentLaunchConfig(effectiveLaunchConfig)
|
||||
: null
|
||||
pty.launchToken = launchToken ?? null
|
||||
pty.launchAgent = launchOpts.launchAgent ?? null
|
||||
}
|
||||
const handle = pty ? this.issuePtyHandle(pty) : preAllocatedHandle
|
||||
if (pty && launchOpts.deferMobileSessionPublish !== true) {
|
||||
this.publishPtyBackedMobileSessionTerminal(workspace.id, pty, {
|
||||
const sequencedStartupCommand =
|
||||
agentTeamsPlan &&
|
||||
claudeAgentTeamsSourceCommand &&
|
||||
launchOpts.command &&
|
||||
claudeAgentTeamsSourceCommand !== launchOpts.command
|
||||
? agentTeamsPlan.command
|
||||
: undefined
|
||||
const effectiveLaunchConfig =
|
||||
launchOpts.launchConfig && agentTeamsPlan
|
||||
? {
|
||||
...launchOpts.launchConfig,
|
||||
agentCommand: launchOpts.launchConfig.agentCommand
|
||||
? effectiveClaudeAgentTeamsMode === 'in-process' || process.platform === 'win32'
|
||||
? addClaudeTeammateModeInProcess(launchOpts.launchConfig.agentCommand)
|
||||
: addClaudeTeammateModeAuto(launchOpts.launchConfig.agentCommand)
|
||||
: agentTeamsPlan.command,
|
||||
agentEnv: {
|
||||
...launchOpts.launchConfig.agentEnv,
|
||||
...agentTeamsPlan.env
|
||||
}
|
||||
}
|
||||
: launchOpts.launchConfig
|
||||
// Why: setup/agent sequencing wraps the PTY launch in a wait shell before
|
||||
// Claude Agent Teams runs. Preserve the direct Claude command separately
|
||||
// so the wrapper can exec the teammate-mode variant after setup completes.
|
||||
const env = this.buildTerminalWorkspaceEnv(
|
||||
workspace,
|
||||
{
|
||||
...baseEnv,
|
||||
...(sequencedStartupCommand
|
||||
? { [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: sequencedStartupCommand }
|
||||
: {})
|
||||
},
|
||||
paneKey,
|
||||
tabId,
|
||||
agentTeamsPlan?.env
|
||||
)
|
||||
const terminalColorQueryReplies =
|
||||
launchOpts.terminalColorQueryReplies ?? getTerminalViewColorQueryReplyColors()
|
||||
if (launchOpts.signal?.aborted) {
|
||||
throw new Error('client_disconnected')
|
||||
}
|
||||
const persistHostSessionBinding =
|
||||
launchOpts.persistHostSessionBinding ||
|
||||
launchOpts.surfaceOwner === false ||
|
||||
this.getAvailableAuthoritativeWindow() === null
|
||||
let result: Awaited<ReturnType<NonNullable<RuntimePtyController['spawn']>>>
|
||||
try {
|
||||
result = await this.ptyController.spawn({
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
cwd,
|
||||
command: sequencedStartupCommand
|
||||
? launchOpts.command
|
||||
: (agentTeamsPlan?.command ?? launchOpts.command),
|
||||
launchAgent: launchOpts.launchAgent,
|
||||
commandDelivery: 'provider',
|
||||
startupCommandDelivery: launchOpts.startupCommandDelivery,
|
||||
env,
|
||||
envToDelete: mergeTerminalEnvDeletionKeys(
|
||||
launchOpts.envToDelete,
|
||||
agentTeamsPlan?.envToDelete
|
||||
),
|
||||
resumeProviderSession: launchOpts.resumeProviderSession,
|
||||
telemetry: launchOpts.telemetry,
|
||||
connectionId: workspace.connectionId,
|
||||
worktreeId: workspace.id,
|
||||
preAllocatedHandle,
|
||||
tabId,
|
||||
leafId,
|
||||
...(terminalColorQueryReplies ? { terminalColorQueryReplies } : {}),
|
||||
...(launchOpts.agentSessionClaim
|
||||
? {
|
||||
agentSessionEnsure: {
|
||||
claim: launchOpts.agentSessionClaim,
|
||||
surface: {
|
||||
worktreeId: workspace.id,
|
||||
tabId,
|
||||
leafId,
|
||||
terminalHandle: preAllocatedHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(launchOpts.agentSessionCreateOperationId
|
||||
? { agentSessionCreateOperationId: launchOpts.agentSessionCreateOperationId }
|
||||
: {}),
|
||||
...(launchOpts.signal ? { signal: launchOpts.signal } : {}),
|
||||
...(launchOpts.onPtySpawnCommitted
|
||||
? { onPtySpawnCommitted: reportPtySpawnCommitted }
|
||||
: {}),
|
||||
...(adoptedBeforeLaunch ? { adoptedStablePane: adoptedBeforeLaunch } : {}),
|
||||
...(launchOpts.sessionId ? { sessionId: launchOpts.sessionId } : {}),
|
||||
// Why: a headless-created pane has no renderer session writer. Persist
|
||||
// its tab/leaf binding at spawn so a later promoted window reattaches
|
||||
// the live daemon or SSH PTY instead of replacing it with a fresh one.
|
||||
// Re-check freshly: the entry-time snapshot can go stale across the
|
||||
// awaits above if the authoritative window is destroyed mid-spawn.
|
||||
...(persistHostSessionBinding ? { persistHostSessionBinding: true } : {})
|
||||
})
|
||||
} finally {
|
||||
releaseStablePaneCreate?.()
|
||||
}
|
||||
if (!result.stablePaneOwner) {
|
||||
reportPtySpawnCommitted()
|
||||
}
|
||||
const adoptedStablePane = Boolean(result.stablePaneOwner)
|
||||
if (result.agentSessionEnsure) {
|
||||
const canonicalSurface = result.agentSessionEnsure.owner.surface
|
||||
preAllocatedHandle = canonicalSurface.terminalHandle
|
||||
tabId = canonicalSurface.tabId
|
||||
leafId = canonicalSurface.leafId
|
||||
paneKey = makePaneKey(tabId, leafId)
|
||||
} else if (result.stablePaneOwner) {
|
||||
preAllocatedHandle = result.stablePaneOwner.handle
|
||||
tabId = result.stablePaneOwner.tabId
|
||||
leafId = result.stablePaneOwner.leafId
|
||||
paneKey = makePaneKey(tabId, leafId)
|
||||
}
|
||||
try {
|
||||
this.assertPtyDidNotExitBeforeRegistration(result.id, result.incarnationId)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'agent_session_exited_during_start') {
|
||||
this.releaseRejectedPtyRegistrationFence(result.id, result.incarnationId)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
this.registerPreAllocatedHandleForPty(result.id, preAllocatedHandle)
|
||||
if (result.wslDistro) {
|
||||
this.preparePtyExecutionContext(result.id, result.wslDistro)
|
||||
}
|
||||
this.registerPty(result.id, workspace.id, workspace.connectionId, {
|
||||
tabId,
|
||||
leafId,
|
||||
title: launchOpts.title ?? null,
|
||||
activate: presentation === 'focused',
|
||||
// Why: explicit background presentation may carry legacy activate
|
||||
// metadata from an already-owned renderer pane; don't select it on mobile.
|
||||
selectIfNoActiveTab: presentation !== 'background',
|
||||
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
|
||||
...(cwd !== workspace.path ? { startupCwd: cwd } : {})
|
||||
...(result.incarnationId ? { incarnationId: result.incarnationId } : {})
|
||||
})
|
||||
}
|
||||
let surface: RuntimeTerminalCreate['surface'] = 'background'
|
||||
let warning: string | undefined
|
||||
if (presentation !== 'background' && this.notifier?.revealTerminalSession) {
|
||||
try {
|
||||
// Why: after the PTY is spawned, renderer tab adoption is best-effort;
|
||||
// failing here must not strand a live process without returning a handle.
|
||||
// Pass the pre-minted tabId so the renderer adopts under the same id
|
||||
// already baked into the PTY env — keeps paneKey hook attribution intact.
|
||||
await this.notifier.revealTerminalSession(workspace.id, {
|
||||
ptyId: result.id,
|
||||
title: launchOpts.title ?? null,
|
||||
...(cwd !== workspace.path ? { cwd } : {}),
|
||||
...(effectiveLaunchConfig ? { launchConfig: effectiveLaunchConfig } : {}),
|
||||
...(launchToken ? { launchToken } : {}),
|
||||
...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}),
|
||||
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
|
||||
activate: presentation === 'focused',
|
||||
...(presentation ? { presentation } : {}),
|
||||
...ownerSurfacing(opts.surfaceOwner !== false),
|
||||
tabId,
|
||||
leafId
|
||||
})
|
||||
surface = 'visible'
|
||||
} catch (err) {
|
||||
console.warn(`[terminal-create] failed to create inactive tab for ${result.id}:`, err)
|
||||
warning = createTerminalRevealWarning(handle, err)
|
||||
const pty = this.getOrCreatePtyWorktreeRecord(result.id)
|
||||
if (pty) {
|
||||
if (persistHostSessionBinding) {
|
||||
pty.runtimeSessionOwned = true
|
||||
}
|
||||
if (!adoptedStablePane) {
|
||||
if (launchOpts.title) {
|
||||
const observedAt = this.nextTitleObservationSequence()
|
||||
pty.title = launchOpts.title
|
||||
pty.titleUpdatedAt = observedAt
|
||||
this.setPtyManagementTitleFromObservedTitle(pty, launchOpts.title, observedAt)
|
||||
} else {
|
||||
pty.title = null
|
||||
pty.titleUpdatedAt = null
|
||||
}
|
||||
pty.launchConfig = effectiveLaunchConfig
|
||||
? copySleepingAgentLaunchConfig(effectiveLaunchConfig)
|
||||
: null
|
||||
pty.launchToken = launchToken ?? null
|
||||
pty.launchAgent = launchOpts.launchAgent ?? null
|
||||
}
|
||||
pty.tabId = tabId
|
||||
pty.paneKey = paneKey
|
||||
}
|
||||
} else if (presentation !== 'background') {
|
||||
warning = createTerminalRevealWarning(handle)
|
||||
}
|
||||
return {
|
||||
handle,
|
||||
tabId,
|
||||
paneKey,
|
||||
ptyId: result.id,
|
||||
worktreeId: workspace.id,
|
||||
title: launchOpts.title ?? null,
|
||||
...this.getPtyExecutionHostMetadata(result.id),
|
||||
surface,
|
||||
...(result.agentSessionEnsure
|
||||
? { agentSessionDisposition: result.agentSessionEnsure.disposition }
|
||||
: {}),
|
||||
...(warning ? { warning } : {})
|
||||
const handle = pty ? this.issuePtyHandle(pty) : preAllocatedHandle
|
||||
if (pty && !adoptedStablePane && launchOpts.deferMobileSessionPublish !== true) {
|
||||
this.publishPtyBackedMobileSessionTerminal(workspace.id, pty, {
|
||||
tabId,
|
||||
leafId,
|
||||
title: launchOpts.title ?? null,
|
||||
activate: presentation === 'focused',
|
||||
// Why: explicit background presentation may carry legacy activate
|
||||
// metadata from an already-owned renderer pane; don't select it on mobile.
|
||||
selectIfNoActiveTab: presentation !== 'background',
|
||||
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
|
||||
...(cwd !== workspace.path ? { startupCwd: cwd } : {})
|
||||
})
|
||||
}
|
||||
let surface: RuntimeTerminalCreate['surface'] = 'background'
|
||||
let warning: string | undefined
|
||||
if (presentation !== 'background' && this.notifier?.revealTerminalSession) {
|
||||
try {
|
||||
// Why: after the PTY is spawned, renderer tab adoption is best-effort;
|
||||
// failing here must not strand a live process without returning a handle.
|
||||
// Pass the pre-minted tabId so the renderer adopts under the same id
|
||||
// already baked into the PTY env — keeps paneKey hook attribution intact.
|
||||
await this.notifier.revealTerminalSession(workspace.id, {
|
||||
ptyId: result.id,
|
||||
title: launchOpts.title ?? null,
|
||||
...(cwd !== workspace.path ? { cwd } : {}),
|
||||
...(effectiveLaunchConfig ? { launchConfig: effectiveLaunchConfig } : {}),
|
||||
...(launchToken ? { launchToken } : {}),
|
||||
...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}),
|
||||
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
|
||||
activate: presentation === 'focused',
|
||||
...(presentation ? { presentation } : {}),
|
||||
...ownerSurfacing(opts.surfaceOwner !== false),
|
||||
tabId,
|
||||
leafId
|
||||
})
|
||||
surface = 'visible'
|
||||
} catch (err) {
|
||||
console.warn(`[terminal-create] failed to create inactive tab for ${result.id}:`, err)
|
||||
warning = createTerminalRevealWarning(handle, err)
|
||||
}
|
||||
} else if (presentation !== 'background') {
|
||||
warning = createTerminalRevealWarning(handle)
|
||||
}
|
||||
return {
|
||||
handle,
|
||||
tabId,
|
||||
paneKey,
|
||||
ptyId: result.id,
|
||||
worktreeId: workspace.id,
|
||||
title: pty?.title ?? launchOpts.title ?? null,
|
||||
...this.getPtyExecutionHostMetadata(result.id),
|
||||
surface,
|
||||
...(result.agentSessionEnsure
|
||||
? { agentSessionDisposition: result.agentSessionEnsure.disposition }
|
||||
: {}),
|
||||
...(adoptedStablePane ? { isReattach: true as const } : {}),
|
||||
...(warning ? { warning } : {})
|
||||
}
|
||||
} finally {
|
||||
releaseStablePaneCreate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user