fix: suppress unread signals during PTY replay (#569)

This commit is contained in:
Neil
2026-04-12 21:49:34 -07:00
committed by GitHub
parent f9e803082c
commit 91c88fad6b
2 changed files with 50 additions and 5 deletions
@@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createIpcPtyTransport } from './pty-transport'
describe('createIpcPtyTransport', () => {
const originalWindow = (globalThis as { window?: typeof window }).window
@@ -10,6 +9,7 @@ describe('createIpcPtyTransport', () => {
| null = null
beforeEach(() => {
vi.resetModules()
onData = null
onExit = null
onOpenCodeStatus = null
@@ -57,6 +57,7 @@ describe('createIpcPtyTransport', () => {
})
it('maps OpenCode status events into the existing working to idle agent lifecycle', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const onTitleChange = vi.fn()
const onAgentBecameWorking = vi.fn()
const onAgentBecameIdle = vi.fn()
@@ -86,4 +87,33 @@ describe('createIpcPtyTransport', () => {
expect(onData).not.toBeNull()
expect(onExit).not.toBeNull()
})
it('does not fire unread-side effects when replaying buffered data during attach', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
const onTitleChange = vi.fn()
const onAgentBecameIdle = vi.fn()
const onBell = vi.fn()
const handle = registerEagerPtyBuffer('pty-restored', vi.fn())
onData?.({
id: 'pty-restored',
data: '\u001b]0;. Claude working\u0007\u001b]0;* Claude done\u0007\u0007'
})
const transport = createIpcPtyTransport({
onTitleChange,
onAgentBecameIdle,
onBell
})
transport.attach({
existingPtyId: 'pty-restored',
callbacks: {}
})
expect(handle.flush()).toBe('')
expect(onTitleChange).toHaveBeenCalledWith('* Claude done', '* Claude done')
expect(onAgentBecameIdle).not.toHaveBeenCalled()
expect(onBell).not.toHaveBeenCalled()
})
})
@@ -177,6 +177,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
let pendingEscape = false
let inOsc = false
let pendingOscEscape = false
let suppressAttentionEvents = false
let lastEmittedTitle: string | null = null
let lastObservedTerminalTitle: string | null = null
let openCodeStatus: OpenCodeStatusEvent['status'] | null = null
@@ -184,7 +185,11 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
const agentTracker =
onAgentBecameIdle || onAgentBecameWorking || onAgentExited
? createAgentStatusTracker(
onAgentBecameIdle ?? (() => {}),
(title) => {
if (!suppressAttentionEvents) {
onAgentBecameIdle?.(title)
}
},
onAgentBecameWorking,
onAgentExited
)
@@ -316,7 +321,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
}, STALE_TITLE_TIMEOUT)
}
}
if (onBell && chunkContainsBell(data)) {
if (onBell && chunkContainsBell(data) && !suppressAttentionEvents) {
onBell()
}
})
@@ -390,7 +395,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
}, STALE_TITLE_TIMEOUT)
}
}
if (onBell && chunkContainsBell(data)) {
if (onBell && chunkContainsBell(data) && !suppressAttentionEvents) {
onBell()
}
})
@@ -419,7 +424,17 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
if (bufferHandle) {
const buffered = bufferHandle.flush()
if (buffered) {
ptyDataHandlers.get(id)?.(buffered)
// Why: eager PTY buffers contain output produced before the pane
// attached, often from a previous app session. We still replay that
// data so titles and scrollback restore correctly, but it must not
// generate fresh unread badges or notifications for unrelated
// worktrees just because Orca is reconnecting background terminals.
suppressAttentionEvents = true
try {
ptyDataHandlers.get(id)?.(buffered)
} finally {
suppressAttentionEvents = false
}
}
bufferHandle.dispose()
}