mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(terminal): use fact attribution for resume retirement
This commit is contained in:
@@ -10441,6 +10441,31 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(batches.flatMap((batch) => batch.facts)).not.toContainEqual({ kind: 'agent-exited' })
|
||||
})
|
||||
|
||||
it('does not confirm when the SSH host becomes unverifiable during the foreground read', async () => {
|
||||
const { runtime, batches } = createSideEffectRuntime()
|
||||
runtime.registerPty('pty-1', TEST_WORKTREE_ID, 'conn-1')
|
||||
syncSinglePty(runtime)
|
||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;Codex ready\x07')
|
||||
let resolveForeground!: (process: string) => void
|
||||
const foregroundRead = new Promise<string>((resolve) => {
|
||||
resolveForeground = resolve
|
||||
})
|
||||
const getForegroundProcess = vi.fn(() => foregroundRead)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess
|
||||
})
|
||||
|
||||
runtime.onPtyData('pty-1', '\x1b]0;bichir\x07', 100)
|
||||
await vi.waitFor(() => expect(getForegroundProcess).toHaveBeenCalledOnce())
|
||||
runtime.markPtyLivenessUnverifiable('pty-1', 'the SSH provider disconnected')
|
||||
resolveForeground('zsh')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(batches.flatMap((batch) => batch.facts)).not.toContainEqual({ kind: 'agent-exited' })
|
||||
})
|
||||
|
||||
it('still confirms a local agent exit if the PTY disconnects during the foreground read', async () => {
|
||||
const { runtime, batches } = createSideEffectRuntime()
|
||||
runtime.registerPty('pty-1', TEST_WORKTREE_ID)
|
||||
|
||||
@@ -19660,19 +19660,19 @@ export class OrcaRuntimeService {
|
||||
return entry.promise
|
||||
}
|
||||
|
||||
private canConfirmAgentExitFromHost(
|
||||
private isPtyAgentExitUnverifiable(
|
||||
pty: RuntimePtyWorktreeRecord | undefined,
|
||||
ptyId: string
|
||||
): boolean {
|
||||
if (this.getPtyLivenessVerdict(ptyId)?.status === 'unverifiable') {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
return !(pty?.connectionId && !pty.connected)
|
||||
return Boolean(pty?.connectionId && !pty.connected)
|
||||
}
|
||||
|
||||
private confirmPtyAgentExit(ptyId: string): void {
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
if (!this.canConfirmAgentExitFromHost(pty, ptyId)) {
|
||||
if (this.isPtyAgentExitUnverifiable(pty, ptyId)) {
|
||||
return
|
||||
}
|
||||
const titleObservedAt = pty?.lastOscTitleAt ?? null
|
||||
@@ -19691,7 +19691,7 @@ export class OrcaRuntimeService {
|
||||
if (current !== pty) {
|
||||
return
|
||||
}
|
||||
if (!this.canConfirmAgentExitFromHost(current, ptyId)) {
|
||||
if (this.isPtyAgentExitUnverifiable(current, ptyId)) {
|
||||
return
|
||||
}
|
||||
if (!current.connected) {
|
||||
|
||||
@@ -27,11 +27,7 @@ import {
|
||||
import { dispatchTerminalNotification } from './use-notification-dispatch'
|
||||
import { acquireHiddenRendererPtyDeliveryClaim } from './pty-renderer-delivery-claims'
|
||||
import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection'
|
||||
import {
|
||||
bindConfirmedAgentExitResumePane,
|
||||
retireConfirmedAgentExitResumeAuthority,
|
||||
scheduleUnbindConfirmedAgentExitResumePane
|
||||
} from '@/lib/confirmed-agent-exit-resume-retirement'
|
||||
import { retireConfirmedAgentExitResumeAuthority } from '@/lib/confirmed-agent-exit-resume-retirement'
|
||||
|
||||
// Why: keep the live path's BEL-vs-completion race window so notification behavior is identical whether a tab is parked or mounted.
|
||||
const PARKED_NOTIFICATION_GRACE_MS = AGENT_TASK_COMPLETE_NOTIFICATION_GRACE_MS
|
||||
@@ -74,7 +70,6 @@ export function startParkedTerminalByteWatcher(
|
||||
|
||||
// Why: one watcher per PTY — a stale watcher from a previous park cycle would double-fire bell/completion for the same bytes.
|
||||
parkedWatcherDisposersByPtyId.get(ptyId)?.()
|
||||
bindConfirmedAgentExitResumePane(ptyId, paneKey)
|
||||
|
||||
let disposed = false
|
||||
let pendingBellNotification = false
|
||||
@@ -289,7 +284,6 @@ export function startParkedTerminalByteWatcher(
|
||||
return
|
||||
}
|
||||
disposed = true
|
||||
scheduleUnbindConfirmedAgentExitResumePane(ptyId)
|
||||
// Why first: each park/reveal cycle owns a distinct processor gauge, and the store/IPC
|
||||
// teardown below must not be able to throw its way past the census drop.
|
||||
processor?.disposePendingSideEffectGauge()
|
||||
|
||||
+6
@@ -22,6 +22,7 @@ type MockStoreState = {
|
||||
agentStatusByPaneKey: Record<string, never>
|
||||
sleepingAgentSessionsByPaneKey: Record<string, SleepingAgentSessionRecord>
|
||||
clearSleepingAgentSession: ReturnType<typeof vi.fn>
|
||||
clearSleepingAgentSessionsByPaneKey: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
let mockStoreState: MockStoreState
|
||||
@@ -65,6 +66,11 @@ describe('parked terminal confirmed agent-exit resume retirement', () => {
|
||||
sleepingAgentSessionsByPaneKey: {},
|
||||
clearSleepingAgentSession: vi.fn((paneKey: string) => {
|
||||
delete mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]
|
||||
}),
|
||||
clearSleepingAgentSessionsByPaneKey: vi.fn((paneKeys: readonly string[]) => {
|
||||
for (const paneKey of paneKeys) {
|
||||
delete mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
;(globalThis as { window: typeof window }).window = {
|
||||
|
||||
+34
-6
@@ -443,16 +443,17 @@ describe('connectPanePty', () => {
|
||||
const onPtyExit = createdTransportOptions[0]?.onPtyExit as (ptyId: string) => void
|
||||
onPtyExit('pty-agent-exit')
|
||||
|
||||
expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledWith(paneKey)
|
||||
expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledWith(duplicatePaneKey)
|
||||
expect(mockStoreState.clearSleepingAgentSessionsByPaneKey).toHaveBeenCalledWith([
|
||||
paneKey,
|
||||
duplicatePaneKey
|
||||
])
|
||||
expect(mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined()
|
||||
expect(mockStoreState.sleepingAgentSessionsByPaneKey[duplicatePaneKey]).toBeUndefined()
|
||||
expect(mockStoreState.sleepingAgentSessionsByPaneKey[siblingPaneKey]).toBe(siblingRecord)
|
||||
expect(deps.onAgentExitedRef.current).toHaveBeenCalledWith(LEAF_1)
|
||||
const clearCallOrders = mockStoreState.clearSleepingAgentSession.mock.invocationCallOrder
|
||||
expect(clearCallOrders.at(-1)).toBeLessThan(
|
||||
deps.onAgentExitedRef.current.mock.invocationCallOrder[0]
|
||||
)
|
||||
const clearCallOrder =
|
||||
mockStoreState.clearSleepingAgentSessionsByPaneKey.mock.invocationCallOrder[0]
|
||||
expect(clearCallOrder).toBeLessThan(deps.onAgentExitedRef.current.mock.invocationCallOrder[0])
|
||||
})
|
||||
|
||||
it('retires resume authority when the agent-exited fact arrives after PTY teardown', async () => {
|
||||
@@ -485,6 +486,7 @@ describe('connectPanePty', () => {
|
||||
handler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-late-exit',
|
||||
seq: 1,
|
||||
paneKey,
|
||||
facts: [{ kind: 'agent-exited' }]
|
||||
})
|
||||
|
||||
@@ -776,6 +778,32 @@ describe('connectPanePty', () => {
|
||||
const onBell = createdTransportOptions[0]?.onBell as () => void
|
||||
onBell()
|
||||
expect(deps.markWorktreeUnread).toHaveBeenCalledTimes(1)
|
||||
|
||||
const paneKey = makePaneKey('tab-1', LEAF_1)
|
||||
const record = {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'claude' as const,
|
||||
providerSession: { key: 'session_id' as const, id: 'session-byte-mode' },
|
||||
prompt: 'finish the task',
|
||||
state: 'working' as const,
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
origin: 'live' as const
|
||||
}
|
||||
mockStoreState.sleepingAgentSessionsByPaneKey = { [paneKey]: record }
|
||||
const onPtyExit = createdTransportOptions[0]?.onPtyExit as (ptyId: string) => void
|
||||
onPtyExit('pty-prehydration')
|
||||
|
||||
// Why: main still emits the confirmed fact after the byte-mode pane consumer is gone.
|
||||
handler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-prehydration',
|
||||
seq: 2,
|
||||
paneKey,
|
||||
facts: [{ kind: 'agent-exited' }]
|
||||
})
|
||||
expect(mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,6 +64,12 @@ export function createInitialStoreState(getState: () => StoreState): StoreState
|
||||
clearSleepingAgentSession: vi.fn((paneKey: string) => {
|
||||
delete getState().sleepingAgentSessionsByPaneKey[paneKey]
|
||||
}),
|
||||
clearSleepingAgentSessionsByPaneKey: vi.fn((paneKeys: readonly string[]) => {
|
||||
for (const paneKey of paneKeys) {
|
||||
delete getState().sleepingAgentSessionsByPaneKey[paneKey]
|
||||
delete getState().agentLaunchConfigByPaneKey[paneKey]
|
||||
}
|
||||
}),
|
||||
registerAgentLaunchConfig: vi.fn(),
|
||||
clearAgentLaunchConfig: vi.fn(),
|
||||
markWorktreeUnread: vi.fn(),
|
||||
|
||||
@@ -109,6 +109,7 @@ export type StoreState = {
|
||||
getAgentLaunchConfigForStatusEntry: ReturnType<typeof vi.fn>
|
||||
getAgentLaunchConfigForStatusMetadata: ReturnType<typeof vi.fn>
|
||||
clearSleepingAgentSession: ReturnType<typeof vi.fn>
|
||||
clearSleepingAgentSessionsByPaneKey: ReturnType<typeof vi.fn>
|
||||
registerAgentLaunchConfig: ReturnType<typeof vi.fn>
|
||||
clearAgentLaunchConfig: ReturnType<typeof vi.fn>
|
||||
markWorktreeUnread: ReturnType<typeof vi.fn>
|
||||
|
||||
@@ -19,7 +19,8 @@ import type {
|
||||
TerminalSideEffectBatch,
|
||||
TerminalSideEffectFact
|
||||
} from '../../../../shared/terminal-side-effect-facts'
|
||||
import { retireConfirmedAgentExitResumeForPty } from '@/lib/confirmed-agent-exit-resume-retirement'
|
||||
import { useAppStore } from '@/store'
|
||||
import { retireConfirmedAgentExitResumeAuthority } from '@/lib/confirmed-agent-exit-resume-retirement'
|
||||
|
||||
// Why: cached once per session — the blocking read should only ever run on
|
||||
// the pre-hydration startup path, never per pane bind.
|
||||
@@ -259,18 +260,17 @@ function drainHandoffFactBuffer(ptyId: string, entry: ConsumerEntry): void {
|
||||
}
|
||||
|
||||
export function dispatchTerminalSideEffectBatch(batch: TerminalSideEffectBatch): void {
|
||||
// Why: the pane consumer unregisters on PTY teardown, but a confirmed
|
||||
// agent-exited fact can still arrive after that. Resume retirement must
|
||||
// not depend on the pane still being mounted.
|
||||
if (!batch.replay) {
|
||||
for (const fact of batch.facts) {
|
||||
if (fact.kind === 'agent-exited') {
|
||||
retireConfirmedAgentExitResumeForPty(batch.ptyId)
|
||||
}
|
||||
}
|
||||
}
|
||||
const entry = consumersByPtyId.get(batch.ptyId)
|
||||
if (!entry) {
|
||||
// Why: a confirmed exit can race pane teardown or arrive for byte-mode
|
||||
// panes that never register here; main's fact attribution outlives both.
|
||||
if (
|
||||
!batch.replay &&
|
||||
batch.paneKey &&
|
||||
batch.facts.some((fact) => fact.kind === 'agent-exited')
|
||||
) {
|
||||
retireConfirmedAgentExitResumeAuthority(useAppStore.getState(), batch.paneKey)
|
||||
}
|
||||
bufferHandoffFactBatch(batch)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
import { retireConfirmedAgentExitResumeAuthority } from './confirmed-agent-exit-resume-retirement'
|
||||
import {
|
||||
bindConfirmedAgentExitResumePane,
|
||||
retireConfirmedAgentExitResumeAuthority,
|
||||
retireConfirmedAgentExitResumeForPty,
|
||||
_resetConfirmedAgentExitResumeBindingsForTest
|
||||
} from './confirmed-agent-exit-resume-retirement'
|
||||
_dispatchTerminalSideEffectBatchForTest,
|
||||
_resetTerminalSideEffectFactConsumersForTest
|
||||
} from '@/components/terminal-pane/terminal-side-effect-facts-handler'
|
||||
|
||||
const initialState = useAppStore.getState()
|
||||
|
||||
afterEach(() => {
|
||||
_resetConfirmedAgentExitResumeBindingsForTest()
|
||||
_resetTerminalSideEffectFactConsumersForTest()
|
||||
useAppStore.setState(initialState, true)
|
||||
})
|
||||
|
||||
@@ -82,6 +81,48 @@ describe('confirmed agent-exit resume retirement', () => {
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[sibling.paneKey]).toEqual(sibling)
|
||||
})
|
||||
|
||||
it('uses the Prime session file to isolate provider-session aliases', () => {
|
||||
const record = makeRecord({
|
||||
agent: 'prime-agent',
|
||||
providerSession: {
|
||||
key: 'session_id',
|
||||
id: 'prime-session',
|
||||
transcriptPath: 'prime-session-a.jsonl'
|
||||
}
|
||||
})
|
||||
const duplicate = makeRecord({
|
||||
paneKey: 'tab-1:legacy',
|
||||
agent: 'prime-agent',
|
||||
providerSession: {
|
||||
key: 'session_id',
|
||||
id: 'prime-session',
|
||||
transcriptPath: 'prime-session-a.jsonl'
|
||||
}
|
||||
})
|
||||
const sibling = makeRecord({
|
||||
paneKey: 'tab-1:leaf-2',
|
||||
agent: 'prime-agent',
|
||||
providerSession: {
|
||||
key: 'session_id',
|
||||
id: 'prime-session',
|
||||
transcriptPath: 'prime-session-b.jsonl'
|
||||
}
|
||||
})
|
||||
useAppStore.setState({
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
[record.paneKey]: record,
|
||||
[duplicate.paneKey]: duplicate,
|
||||
[sibling.paneKey]: sibling
|
||||
}
|
||||
} as never)
|
||||
|
||||
retireConfirmedAgentExitResumeAuthority(useAppStore.getState(), record.paneKey)
|
||||
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[record.paneKey]).toBeUndefined()
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[duplicate.paneKey]).toBeUndefined()
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[sibling.paneKey]).toEqual(sibling)
|
||||
})
|
||||
|
||||
it('retires a clean-exit record so the next launch does not resume it', () => {
|
||||
seedOrphanRecord(makeRecord())
|
||||
retireConfirmedAgentExitResumeAuthority(useAppStore.getState(), 'tab-1:leaf-1')
|
||||
@@ -104,14 +145,39 @@ describe('confirmed agent-exit resume retirement', () => {
|
||||
expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(1)
|
||||
})
|
||||
|
||||
it('retires by PTY binding so a late confirmed-exit fact still finds the pane', () => {
|
||||
it('retires from fact attribution when no pane consumer is registered', () => {
|
||||
const record = makeRecord()
|
||||
seedOrphanRecord(record)
|
||||
bindConfirmedAgentExitResumePane('pty-late', record.paneKey)
|
||||
|
||||
retireConfirmedAgentExitResumeForPty('pty-late')
|
||||
_dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-late',
|
||||
seq: 1,
|
||||
paneKey: record.paneKey,
|
||||
facts: [{ kind: 'agent-exited' }]
|
||||
})
|
||||
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[record.paneKey]).toBeUndefined()
|
||||
expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps resume authority for unattributed or replayed exit facts', () => {
|
||||
const record = makeRecord()
|
||||
seedOrphanRecord(record)
|
||||
|
||||
_dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-unattributed',
|
||||
seq: 1,
|
||||
facts: [{ kind: 'agent-exited' }]
|
||||
})
|
||||
_dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-replay',
|
||||
seq: 2,
|
||||
paneKey: record.paneKey,
|
||||
replay: true,
|
||||
facts: [{ kind: 'agent-exited' }]
|
||||
})
|
||||
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[record.paneKey]).toEqual(record)
|
||||
expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,57 +1,19 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
agentProviderSessionsEqual,
|
||||
type SleepingAgentSessionRecord
|
||||
} from '../../../shared/agent-session-resume'
|
||||
|
||||
type ResumeRetirementStore = {
|
||||
sleepingAgentSessionsByPaneKey?: Record<string, SleepingAgentSessionRecord>
|
||||
clearSleepingAgentSession?: (paneKey: string) => void
|
||||
}
|
||||
|
||||
const paneKeyByPtyId = new Map<string, string>()
|
||||
const unbindTimersByPtyId = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
/** Matches the side-effect handoff buffer so a late confirmed-exit fact still finds the pane. */
|
||||
const UNBIND_GRACE_MS = 15_000
|
||||
|
||||
function clearUnbindTimer(ptyId: string): void {
|
||||
const timer = unbindTimersByPtyId.get(ptyId)
|
||||
if (!timer) {
|
||||
return
|
||||
}
|
||||
clearTimeout(timer)
|
||||
unbindTimersByPtyId.delete(ptyId)
|
||||
}
|
||||
|
||||
export function bindConfirmedAgentExitResumePane(ptyId: string, paneKey: string): void {
|
||||
clearUnbindTimer(ptyId)
|
||||
paneKeyByPtyId.set(ptyId, paneKey)
|
||||
}
|
||||
|
||||
export function scheduleUnbindConfirmedAgentExitResumePane(ptyId: string): void {
|
||||
if (!paneKeyByPtyId.has(ptyId)) {
|
||||
return
|
||||
}
|
||||
clearUnbindTimer(ptyId)
|
||||
const timer = setTimeout(() => {
|
||||
unbindTimersByPtyId.delete(ptyId)
|
||||
paneKeyByPtyId.delete(ptyId)
|
||||
}, UNBIND_GRACE_MS)
|
||||
timer.unref?.()
|
||||
unbindTimersByPtyId.set(ptyId, timer)
|
||||
sleepingAgentSessionsByPaneKey: Record<string, SleepingAgentSessionRecord>
|
||||
clearSleepingAgentSessionsByPaneKey: (paneKeys: readonly string[]) => void
|
||||
}
|
||||
|
||||
export function retireConfirmedAgentExitResumeRecord(
|
||||
state: ResumeRetirementStore,
|
||||
consumed: { paneKey: string; record: SleepingAgentSessionRecord }
|
||||
): void {
|
||||
if (typeof state.clearSleepingAgentSession !== 'function') {
|
||||
return
|
||||
}
|
||||
const records = state.sleepingAgentSessionsByPaneKey ?? {}
|
||||
const duplicatePaneKeys: string[] = []
|
||||
for (const [paneKey, record] of Object.entries(records)) {
|
||||
const paneKeys = [consumed.paneKey]
|
||||
for (const [paneKey, record] of Object.entries(state.sleepingAgentSessionsByPaneKey)) {
|
||||
if (
|
||||
paneKey !== consumed.paneKey &&
|
||||
record.worktreeId === consumed.record.worktreeId &&
|
||||
@@ -62,42 +24,19 @@ export function retireConfirmedAgentExitResumeRecord(
|
||||
consumed.record.providerSession
|
||||
)
|
||||
) {
|
||||
duplicatePaneKeys.push(paneKey)
|
||||
paneKeys.push(paneKey)
|
||||
}
|
||||
}
|
||||
state.clearSleepingAgentSession(consumed.paneKey)
|
||||
for (const paneKey of duplicatePaneKeys) {
|
||||
state.clearSleepingAgentSession(paneKey)
|
||||
}
|
||||
state.clearSleepingAgentSessionsByPaneKey(paneKeys)
|
||||
}
|
||||
|
||||
export function retireConfirmedAgentExitResumeAuthority(
|
||||
state: ResumeRetirementStore,
|
||||
paneKey: string
|
||||
): void {
|
||||
const record = state.sleepingAgentSessionsByPaneKey?.[paneKey]
|
||||
const record = state.sleepingAgentSessionsByPaneKey[paneKey]
|
||||
if (!record) {
|
||||
return
|
||||
}
|
||||
retireConfirmedAgentExitResumeRecord(state, { paneKey, record })
|
||||
}
|
||||
|
||||
export function retireConfirmedAgentExitResumeForPty(ptyId: string): void {
|
||||
const paneKey = paneKeyByPtyId.get(ptyId)
|
||||
if (!paneKey) {
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
if (typeof state.clearSleepingAgentSession !== 'function') {
|
||||
return
|
||||
}
|
||||
retireConfirmedAgentExitResumeAuthority(state, paneKey)
|
||||
}
|
||||
|
||||
export function _resetConfirmedAgentExitResumeBindingsForTest(): void {
|
||||
for (const timer of unbindTimersByPtyId.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
unbindTimersByPtyId.clear()
|
||||
paneKeyByPtyId.clear()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user