mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
130 lines
6.3 KiB
Diff
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
|