diff --git a/src/main/stats/agent-detector.test.ts b/src/main/stats/agent-detector.test.ts index cadb2440f..8ed3c785c 100644 --- a/src/main/stats/agent-detector.test.ts +++ b/src/main/stats/agent-detector.test.ts @@ -19,6 +19,35 @@ describe('AgentDetector', () => { expect(stats.onAgentStop).not.toHaveBeenCalled() }) + it('does not inspect unknown non-agent output for meaningful content', () => { + const stats = { + onAgentStart: vi.fn(), + onAgentStop: vi.fn() + } + const detectMeaningfulContent = vi.fn(() => true) + const detector = new AgentDetector(stats as never, detectMeaningfulContent) + + detector.onData('pty-1', '\x1b[2K\x1b[1G'.repeat(100), 100) + + expect(detectMeaningfulContent).not.toHaveBeenCalled() + expect(stats.onAgentStart).not.toHaveBeenCalled() + expect(stats.onAgentStop).not.toHaveBeenCalled() + }) + + it('inspects content once when an agent title starts a session', () => { + const stats = { + onAgentStart: vi.fn(), + onAgentStop: vi.fn() + } + const detectMeaningfulContent = vi.fn(() => true) + const detector = new AgentDetector(stats as never, detectMeaningfulContent) + + detector.onData('pty-1', `${oscTitle('⠂ Writing patch')}real output`, 100) + + expect(detectMeaningfulContent).toHaveBeenCalledTimes(1) + expect(stats.onAgentStart).toHaveBeenCalledWith('pty-1', 100) + }) + it('stops a session on working to idle transition using the last meaningful output time', () => { const stats = { onAgentStart: vi.fn(), diff --git a/src/main/stats/agent-detector.ts b/src/main/stats/agent-detector.ts index b16f3720b..371b79619 100644 --- a/src/main/stats/agent-detector.ts +++ b/src/main/stats/agent-detector.ts @@ -18,6 +18,8 @@ type PtyRecord = { lastMeaningfulOutputAt: number | null } +type MeaningfulContentDetector = (chunk: string) => boolean + /** * Lightweight normalization to detect whether a PTY data chunk contains * meaningful (non-ANSI, non-OSC) output. Mirrors the regex passes in @@ -72,9 +74,11 @@ function hasMeaningfulContent(chunk: string): boolean { export class AgentDetector { private ptys = new Map() private stats: StatsCollector + private meaningfulContentDetector: MeaningfulContentDetector - constructor(stats: StatsCollector) { + constructor(stats: StatsCollector, meaningfulContentDetector = hasMeaningfulContent) { this.stats = stats + this.meaningfulContentDetector = meaningfulContentDetector } /** @@ -99,8 +103,13 @@ export class AgentDetector { return } - const hasMeaningfulOutput = hasMeaningfulContent(rawData) - if (record.sessionOpen && hasMeaningfulOutput) { + let hasMeaningfulOutput: boolean | null = null + const getHasMeaningfulOutput = (): boolean => { + hasMeaningfulOutput ??= this.meaningfulContentDetector(rawData) + return hasMeaningfulOutput + } + + if (record.sessionOpen && getHasMeaningfulOutput()) { record.lastMeaningfulOutputAt = at } @@ -119,7 +128,7 @@ export class AgentDetector { record.lastStatus = status record.sessionOpen = true record.sessionStartAt = at - record.lastMeaningfulOutputAt = hasMeaningfulOutput ? at : null + record.lastMeaningfulOutputAt = getHasMeaningfulOutput() ? at : null this.stats.onAgentStart(ptyId, at) return } @@ -139,7 +148,7 @@ export class AgentDetector { // title is the start boundary for the next tracked session. record.sessionOpen = true record.sessionStartAt = at - record.lastMeaningfulOutputAt = hasMeaningfulOutput ? at : null + record.lastMeaningfulOutputAt = getHasMeaningfulOutput() ? at : null this.stats.onAgentStart(ptyId, at) }