diff --git a/src/renderer/src/components/terminal-pane/pty-transport.test.ts b/src/renderer/src/components/terminal-pane/pty-transport.test.ts index 96894cf0aec..a824985f53e 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport.test.ts @@ -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() + }) }) diff --git a/src/renderer/src/components/terminal-pane/pty-transport.ts b/src/renderer/src/components/terminal-pane/pty-transport.ts index 39d5dbe001a..15ae1fa0aa9 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport.ts @@ -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() }