Files
orca/docs/audits/pending-split-close/fix.patch
T

130 lines
6.3 KiB
Diff

diff --git a/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts b/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts
index 3023236de0..52b983c51f 100644
--- a/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts
+++ b/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts
@@ -27,6 +27,7 @@ type IpcPtyConnectContext = {
/** True only for the one buffered exit consumed by this connect attempt. */
isExpectedExitCurrent: () => boolean
ownsPtyId: (id: string) => boolean
+ handleExplicitlyClosedConnect?: (id: string) => boolean
bind: (id: string) => void
isCurrent: (id: string) => boolean
setCallbacks: (callbacks: PtyConnectOptions['callbacks']) => void
@@ -89,6 +90,9 @@ export async function connectIpcPty(
const priorIncarnationFence = currentPreHandlerPtySequence()
const spawnResult = await spawnIpcPty(transportOptions, options, admittedSessionId)
const retireFreshSpawn = async (): Promise<void> => {
+ if (context.handleExplicitlyClosedConnect?.(spawnResult.id)) {
+ return
+ }
// A newer generation may already own a recycled id; an id-only kill would retire its PTY.
if (
!spawnResult.isReattach &&
diff --git a/src/renderer/src/components/terminal-pane/pty-transport-types.ts b/src/renderer/src/components/terminal-pane/pty-transport-types.ts
index 4d7eaf7358..2f6ed734bb 100644
--- a/src/renderer/src/components/terminal-pane/pty-transport-types.ts
+++ b/src/renderer/src/components/terminal-pane/pty-transport-types.ts
@@ -232,7 +232,10 @@ export type PtyTransport = {
* it also drops the transport's output processor from the pty side-effect memory census,
* so a reattached one would run untracked. Create a new transport instead. */
detach?: (options?: { preserveExitObserver?: boolean }) => void
- destroy?: () => void | Promise<void>
+ destroy?: (options?: {
+ /** Explicit close can retain retirement intent until an unbound connect settles. */
+ onAbandonedConnect?: (ptyId: string) => boolean
+ }) => void | Promise<void>
}
export type IpcPtyTransportOptions = {
diff --git a/src/renderer/src/components/terminal-pane/pty-transport.ts b/src/renderer/src/components/terminal-pane/pty-transport.ts
index f794b9a1e4..7731e75eac 100644
--- a/src/renderer/src/components/terminal-pane/pty-transport.ts
+++ b/src/renderer/src/components/terminal-pane/pty-transport.ts
@@ -44,6 +44,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
} = opts
let connected = false
let destroyed = false
+ let onAbandonedConnect: ((ptyId: string) => boolean) | undefined
let ptyId: string | null = null
let lifecycleGeneration = 0
let lastExitGeneration: number | null = null
@@ -137,6 +138,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
lastExitGeneration === lifecycleGeneration &&
lifecycleGeneration === connectGeneration + 1,
ownsPtyId: (id) => !destroyed && connected && ptyId === id,
+ handleExplicitlyClosedConnect: (id) => destroyed && (onAbandonedConnect?.(id) ?? false),
bind,
isCurrent: (id) => lifecycleGeneration === connectGeneration && connected && ptyId === id,
setCallbacks,
@@ -268,7 +270,8 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
: { ...(opts.cwd ? { cwd: opts.cwd } : {}), ...(shellOverride ? { shellOverride } : {}) },
resetCrossChunkParserState: outputProcessor.resetAgentStatusCarry,
- destroy() {
+ destroy(options) {
+ onAbandonedConnect ??= options?.onAbandonedConnect
destroyed = true
try {
this.disconnect()
diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts
index 330b42166c..f3966b1f75 100644
--- a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts
+++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts
@@ -13,6 +13,7 @@ import {
} from './terminal-pane-tab-detach'
import { clearPaneTerminalError } from './terminal-error-accumulation'
import type { TerminalPaneBindingController } from './use-terminal-pane-layout-bindings'
+import { retireUnboundIpcTerminalPane } from './retire-unbound-ipc-terminal-pane'
export function useTerminalPaneCloseActions(controller: TerminalPaneBindingController) {
const {
@@ -46,6 +47,13 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr
clearSessionRestoredBannerForPane(paneId)
const leafId = manager.getLeafId(paneId)
if (leafId) {
+ retireUnboundIpcTerminalPane({
+ getState: useAppStore.getState,
+ tabId,
+ leafId,
+ transport: paneTransportsRef.current.get(paneId),
+ getTransports: () => paneTransportsRef.current
+ })
useAppStore.getState().setCacheTimerStartedAt(makePaneKey(tabId, leafId), null)
useAppStore.getState().dropAgentStatus(makePaneKey(tabId, leafId), { paneRemoved: true })
}
diff --git a/src/renderer/src/store/slices/terminal-tab-retirement.ts b/src/renderer/src/store/slices/terminal-tab-retirement.ts
index 80e6782469..e2e034fa6a 100644
--- a/src/renderer/src/store/slices/terminal-tab-retirement.ts
+++ b/src/renderer/src/store/slices/terminal-tab-retirement.ts
@@ -135,6 +135,30 @@ export function isTerminalTabPresent(
return locateTerminalTab(state.tabsByWorktree, tabId) !== null
}
+export function hasTerminalPtyOwnerOutsidePane(
+ state: TerminalTabRetirementState,
+ identity: string,
+ tabId: string,
+ excludedLeafId?: string
+): boolean {
+ for (const [ownerTabId, owner] of collectLiveTerminalTabs(state)) {
+ const ids =
+ ownerTabId === tabId
+ ? Object.entries(state.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId ?? {})
+ .filter(([leafId]) => leafId !== excludedLeafId)
+ .map(([, ptyId]) => ptyId)
+ : collectPtyIdsForTab(state, ownerTabId, owner.rowPtyId)
+ if (
+ ids.some(
+ (ptyId) => getTerminalPtyOwnershipIdentity(state, ptyId, owner.worktreeId) === identity
+ )
+ ) {
+ return true
+ }
+ }
+ return false
+}
+
export function buildTerminalTabRetirementPlan(
state: TerminalTabRetirementState,
tabId: string