perf: skip no-op terminal side-effect drains
Skip empty terminal side-effect drains for ordinary PTY output while preserving title/status/BEL handling and stale working-title recovery.
This commit is contained in:
parent
d349679d37
commit
9ee0d88b59
|
|
@ -93,6 +93,26 @@ describe('createIpcPtyTransport', () => {
|
|||
transport.disconnect()
|
||||
})
|
||||
|
||||
it('does not schedule PTY side-effect drains for ordinary output with no working title', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { createPtyOutputProcessor } = await import('./pty-transport')
|
||||
const onTitleChange = vi.fn()
|
||||
const onBell = vi.fn()
|
||||
const processor = createPtyOutputProcessor({ onTitleChange, onBell })
|
||||
const callbacks = { onData: vi.fn() }
|
||||
|
||||
processor.processData('plain command output\r\n'.repeat(50), callbacks)
|
||||
|
||||
expect(callbacks.onData).toHaveBeenCalledTimes(1)
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
expect(onTitleChange).not.toHaveBeenCalled()
|
||||
expect(onBell).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves stale-title detection after compacting deferred side effects', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export function createPtyOutputProcessor({
|
|||
let sideEffectDrainTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pendingSideEffects: PendingPtySideEffect[] = []
|
||||
let pendingSideEffectIndex = 0
|
||||
let pendingWorkingTitleSideEffects = 0
|
||||
const agentTracker =
|
||||
onAgentBecameIdle || onAgentBecameWorking || onAgentExited
|
||||
? createAgentStatusTracker(
|
||||
|
|
@ -112,6 +113,20 @@ export function createPtyOutputProcessor({
|
|||
)
|
||||
: null
|
||||
|
||||
function isWorkingTitle(title: string | null): boolean {
|
||||
return title !== null && detectAgentStatusFromTitle(title) === 'working'
|
||||
}
|
||||
|
||||
function countWorkingTitles(titles: string[]): number {
|
||||
let count = 0
|
||||
for (const title of titles) {
|
||||
if (isWorkingTitle(normalizeTerminalTitle(title))) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
function applyObservedTerminalTitle(title: string, suppressAgentTracker = false): void {
|
||||
// Why: cursor-agent's native OSC title is the literal string "Cursor Agent"
|
||||
// and it re-emits that title many times per turn (on every internal redraw)
|
||||
|
|
@ -152,6 +167,7 @@ export function createPtyOutputProcessor({
|
|||
}
|
||||
|
||||
function enqueuePtySideEffect(next: PendingPtySideEffect): void {
|
||||
const workingTitleCount = countWorkingTitles(next.titles)
|
||||
const prior = pendingSideEffects.at(-1)
|
||||
if (
|
||||
prior &&
|
||||
|
|
@ -164,9 +180,11 @@ export function createPtyOutputProcessor({
|
|||
!next.containsBell
|
||||
) {
|
||||
prior.scannedForTitles ||= next.scannedForTitles
|
||||
pendingWorkingTitleSideEffects += workingTitleCount
|
||||
return
|
||||
}
|
||||
pendingSideEffects.push(next)
|
||||
pendingWorkingTitleSideEffects += workingTitleCount
|
||||
}
|
||||
|
||||
function schedulePtySideEffects(
|
||||
|
|
@ -174,14 +192,22 @@ export function createPtyOutputProcessor({
|
|||
payloads: ReturnType<typeof processAgentStatusChunk>['payloads'],
|
||||
suppressAttentionEvents: boolean
|
||||
): void {
|
||||
const scannedForTitles = Boolean(onTitleChange && data.length > 0)
|
||||
const scannedForTitles = Boolean(onTitleChange && data.includes('\x1b]'))
|
||||
const titles = scannedForTitles ? extractAllOscTitles(data) : []
|
||||
const deliveredPayloads =
|
||||
onAgentStatus && !suppressAttentionEvents && payloads.length > 0 ? payloads : []
|
||||
const containsBell = Boolean(
|
||||
onBell && !suppressAttentionEvents && bellDetector.chunkContainsBell(data)
|
||||
)
|
||||
if (!scannedForTitles && deliveredPayloads.length === 0 && !containsBell) {
|
||||
const needsStaleTitleProbe = Boolean(
|
||||
onTitleChange &&
|
||||
data.length > 0 &&
|
||||
titles.length === 0 &&
|
||||
!suppressAttentionEvents &&
|
||||
(isWorkingTitle(lastEmittedTitle) || pendingWorkingTitleSideEffects > 0)
|
||||
)
|
||||
const shouldEmitEmptyTitleScan = scannedForTitles || needsStaleTitleProbe
|
||||
if (!shouldEmitEmptyTitleScan && deliveredPayloads.length === 0 && !containsBell) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +218,7 @@ export function createPtyOutputProcessor({
|
|||
enqueuePtySideEffect({
|
||||
payloads: [],
|
||||
titles: [],
|
||||
scannedForTitles,
|
||||
scannedForTitles: shouldEmitEmptyTitleScan,
|
||||
containsBell,
|
||||
suppressAttentionEvents
|
||||
})
|
||||
|
|
@ -206,11 +232,11 @@ export function createPtyOutputProcessor({
|
|||
suppressAttentionEvents
|
||||
})
|
||||
}
|
||||
if (titles.length === 0 && scannedForTitles) {
|
||||
if (titles.length === 0 && shouldEmitEmptyTitleScan) {
|
||||
enqueuePtySideEffect({
|
||||
payloads: [],
|
||||
titles: [],
|
||||
scannedForTitles: true,
|
||||
scannedForTitles: shouldEmitEmptyTitleScan,
|
||||
containsBell: false,
|
||||
suppressAttentionEvents
|
||||
})
|
||||
|
|
@ -260,6 +286,10 @@ export function createPtyOutputProcessor({
|
|||
}
|
||||
|
||||
function applyPtySideEffect(next: PendingPtySideEffect): void {
|
||||
pendingWorkingTitleSideEffects -= countWorkingTitles(next.titles)
|
||||
if (pendingWorkingTitleSideEffects < 0) {
|
||||
pendingWorkingTitleSideEffects = 0
|
||||
}
|
||||
if (onAgentStatus) {
|
||||
for (const payload of next.payloads) {
|
||||
onAgentStatus(payload)
|
||||
|
|
@ -367,6 +397,7 @@ export function createPtyOutputProcessor({
|
|||
clearSideEffectDrainTimer()
|
||||
pendingSideEffects.length = 0
|
||||
pendingSideEffectIndex = 0
|
||||
pendingWorkingTitleSideEffects = 0
|
||||
clearStaleTitleTimer()
|
||||
agentTracker?.reset()
|
||||
bellDetector.reset()
|
||||
|
|
|
|||
Loading…
Reference in New Issue