diff --git a/src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts b/src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts new file mode 100644 index 00000000000..18a3b7df437 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { canCommandCodeOutputOwnPane } from './command-code-output-ownership' + +describe('canCommandCodeOutputOwnPane', () => { + it('allows the banner fallback when no stronger identity exists', () => { + expect(canCommandCodeOutputOwnPane({})).toBe(true) + expect(canCommandCodeOutputOwnPane({ paneOwnerAgent: 'unknown' })).toBe(true) + }) + + it('allows Command Code ownership evidence', () => { + expect(canCommandCodeOutputOwnPane({ paneOwnerAgent: 'command-code' })).toBe(true) + expect(canCommandCodeOutputOwnPane({ foregroundAgent: 'command-code' })).toBe(true) + }) + + it('rejects another agent owner or foreground process', () => { + expect(canCommandCodeOutputOwnPane({ paneOwnerAgent: 'claude' })).toBe(false) + expect(canCommandCodeOutputOwnPane({ foregroundAgent: 'claude' })).toBe(false) + expect( + canCommandCodeOutputOwnPane({ + paneOwnerAgent: 'unknown', + retainedPaneOwnerAgent: 'claude' + }) + ).toBe(false) + }) + + it('prefers the current foreground process over stale pane ownership', () => { + expect( + canCommandCodeOutputOwnPane({ + foregroundAgent: 'command-code', + paneOwnerAgent: 'claude', + retainedPaneOwnerAgent: 'claude' + }) + ).toBe(true) + expect( + canCommandCodeOutputOwnPane({ + foregroundAgent: 'claude', + paneOwnerAgent: 'command-code' + }) + ).toBe(false) + }) + + it('rejects output observed at a confirmed shell prompt', () => { + expect(canCommandCodeOutputOwnPane({ shellForeground: true })).toBe(false) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts b/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts new file mode 100644 index 00000000000..65f5f196ca0 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts @@ -0,0 +1,21 @@ +import type { AgentType } from '../../../../shared/agent-status-types' +import type { TuiAgent } from '../../../../shared/types' + +export function canCommandCodeOutputOwnPane(args: { + foregroundAgent?: TuiAgent | null + shellForeground?: boolean + paneOwnerAgent?: AgentType | null + retainedPaneOwnerAgent?: AgentType | null +}): boolean { + if (args.foregroundAgent) { + return args.foregroundAgent === 'command-code' + } + if (args.shellForeground) { + return false + } + const paneOwnerAgent = + args.paneOwnerAgent && args.paneOwnerAgent !== 'unknown' + ? args.paneOwnerAgent + : (args.retainedPaneOwnerAgent ?? args.paneOwnerAgent) + return !paneOwnerAgent || paneOwnerAgent === 'unknown' || paneOwnerAgent === 'command-code' +} diff --git a/src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts b/src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts index 21d912dd7c0..959a8069db3 100644 --- a/src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts +++ b/src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts @@ -406,7 +406,7 @@ describe('startParkedTerminalByteWatcher', () => { it('feeds Command Code output through the parked byte detector', async () => { const { dispose } = await startWatcher() - emit('# Command Code v0.27.2') + emit('# Command Code v0.27.2\r\n') emit('⌘ Parsing...') expect(commandStatusPolicy.onCommandCodeWorking).toHaveBeenCalledTimes(1) diff --git a/src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts b/src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts index 24991e01914..e9005fdd11f 100644 --- a/src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts +++ b/src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { AgentStatusEntry } from '../../../../shared/agent-status-types' +import type { AgentStatusEntry, AgentType } from '../../../../shared/agent-status-types' +import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent' const PTY_ID_LOCAL = 'pty-1' const PTY_ID_SSH = 'ssh:target-1@@pty-9' @@ -13,7 +14,11 @@ const DONE_SETTLE_MS = 1500 const ROUTING = { connectionId: null } type MockStoreState = { + tabsByWorktree: Record agentStatusByPaneKey: Record + retainedAgentsByPaneKey: Record + paneForegroundAgentByPaneKey: Record + agentLaunchConfigByPaneKey: Record runtimePaneTitlesByTabId: Record> setAgentStatus: ReturnType dropAgentStatus: ReturnType @@ -40,7 +45,11 @@ vi.mock('@/lib/connection-owner-resolution', () => ({ function makeMockStoreState(): MockStoreState { return { + tabsByWorktree: { [WORKTREE_ID]: [{ id: TAB_ID }] }, agentStatusByPaneKey: {}, + retainedAgentsByPaneKey: {}, + paneForegroundAgentByPaneKey: {}, + agentLaunchConfigByPaneKey: {}, runtimePaneTitlesByTabId: { [TAB_ID]: { [PANE_ID]: '✳ Build feature' } }, setAgentStatus: vi.fn(), dropAgentStatus: vi.fn(), @@ -110,6 +119,80 @@ describe('createParkedTerminalCommandStatusPolicy', () => { policy.dispose() }) + it('rejects scrape status when a Claude hook owns the pane', async () => { + mockStoreState.agentStatusByPaneKey[PANE_KEY] = makeStatusEntry({ state: 'done' }) + const policy = await createPolicy(PTY_ID_LOCAL) + + policy.onCommandCodeWorking('False prompt') + policy.onCommandCodeDone('False prompt') + vi.advanceTimersByTime(DONE_SETTLE_MS) + + expect(mockStoreState.setAgentStatus).not.toHaveBeenCalled() + policy.dispose() + }) + + it('rejects scrape status when retained Claude identity owns the pane', async () => { + mockStoreState.retainedAgentsByPaneKey[PANE_KEY] = { agentType: 'claude' } + mockStoreState.agentStatusByPaneKey[PANE_KEY] = makeStatusEntry({ agentType: 'unknown' }) + const policy = await createPolicy(PTY_ID_LOCAL) + + policy.onCommandCodeWorking('False prompt') + policy.onCommandCodeDone('False prompt') + vi.advanceTimersByTime(DONE_SETTLE_MS) + + expect(mockStoreState.setAgentStatus).not.toHaveBeenCalled() + policy.dispose() + }) + + it('rejects scrape status when Claude launch metadata owns the pane', async () => { + mockStoreState.tabsByWorktree[WORKTREE_ID] = [{ id: TAB_ID, launchAgent: 'claude' }] + const policy = await createPolicy(PTY_ID_LOCAL) + + policy.onCommandCodeWorking('False prompt') + + expect(mockStoreState.setAgentStatus).not.toHaveBeenCalled() + policy.dispose() + }) + + it('rejects scrape status when Claude is the foreground process', async () => { + mockStoreState.paneForegroundAgentByPaneKey[PANE_KEY] = { + agent: 'claude', + shellForeground: false + } + const policy = await createPolicy(PTY_ID_LOCAL) + + policy.onCommandCodeWorking('False prompt') + + expect(mockStoreState.setAgentStatus).not.toHaveBeenCalled() + policy.dispose() + }) + + it('lets a current Command Code process reclaim stale Claude ownership', async () => { + mockStoreState.tabsByWorktree[WORKTREE_ID] = [{ id: TAB_ID, launchAgent: 'claude' }] + mockStoreState.agentStatusByPaneKey[PANE_KEY] = makeStatusEntry({ state: 'done' }) + mockStoreState.retainedAgentsByPaneKey[PANE_KEY] = { agentType: 'claude' } + mockStoreState.paneForegroundAgentByPaneKey[PANE_KEY] = { + agent: 'command-code', + shellForeground: false + } + const policy = await createPolicy(PTY_ID_LOCAL) + + policy.onCommandCodeWorking('New Command Code prompt') + + expect(mockStoreState.setAgentStatus).toHaveBeenCalledWith( + PANE_KEY, + { + state: 'working', + prompt: 'New Command Code prompt', + agentType: 'command-code' + }, + '✳ Build feature', + undefined, + ROUTING + ) + policy.dispose() + }) + it('leaves a settled done row alone when working repeats the same prompt', async () => { mockStoreState.agentStatusByPaneKey[PANE_KEY] = makeStatusEntry({ state: 'done', diff --git a/src/renderer/src/components/terminal-pane/parked-terminal-command-status.ts b/src/renderer/src/components/terminal-pane/parked-terminal-command-status.ts index 781625318bd..9f54e02e87f 100644 --- a/src/renderer/src/components/terminal-pane/parked-terminal-command-status.ts +++ b/src/renderer/src/components/terminal-pane/parked-terminal-command-status.ts @@ -5,6 +5,7 @@ * (foreground process-confirm ladder, key-intent interrupt inference) stay with the mounted pane. */ import type { AgentStatusEntry } from '../../../../shared/agent-status-types' +import { resolvePaneAgentOwner } from '../../../../shared/pane-agent-owner' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import { dispatchTerminalCommandFinishedEvent } from '@/hooks/terminal-command-finished-event' import { resolveLiveAgentStatusConnectionRouting } from '@/lib/agent-status-connection-ownership' @@ -15,6 +16,7 @@ import { openCommandCodeDoneSettle, setCommandCodeDoneSettleExecutor } from './command-code-done-settle' +import { canCommandCodeOutputOwnPane } from './command-code-output-ownership' export type ParkedTerminalCommandStatusPolicy = { onCommandFinished: (bestEffortExitCode: number | null) => void @@ -60,6 +62,23 @@ export function createParkedTerminalCommandStatusPolicy(options: { }) } + const canApplyCommandCodeOutputStatus = (): boolean => { + const state = useAppStore.getState() + const tab = (state.tabsByWorktree[worktreeId] ?? []).find((entry) => entry.id === tabId) + const foreground = state.paneForegroundAgentByPaneKey[paneKey] + const paneOwnerAgent = resolvePaneAgentOwner({ + launchAgent: tab?.launchAgent, + startupLaunchAgent: state.agentLaunchConfigByPaneKey[paneKey]?.identity.agentType, + hookAgent: state.agentStatusByPaneKey[paneKey]?.agentType + }) + return canCommandCodeOutputOwnPane({ + foregroundAgent: foreground?.agent, + shellForeground: foreground?.shellForeground, + paneOwnerAgent, + retainedPaneOwnerAgent: state.retainedAgentsByPaneKey[paneKey]?.agentType + }) + } + // Port of pty-connection's dropCommandFinishedStatusIfSameTurn, minus the interrupt-inference // option: parked panes receive no key events, so inference never has evidence here. const dropCommandFinishedStatusIfSameTurn = (entry: AgentStatusEntry | undefined): void => { @@ -145,6 +164,9 @@ export function createParkedTerminalCommandStatusPolicy(options: { // Port of pty-connection's seedCommandCodeOutputWorkingStatus (store-level only). onCommandCodeWorking: (prompt: string): void => { + if (!canApplyCommandCodeOutputStatus()) { + return + } clearCommandCodeOutputDoneTimer() const routing = resolveRouting() if (!routing) { @@ -178,6 +200,9 @@ export function createParkedTerminalCommandStatusPolicy(options: { // Port of pty-connection's scheduleCommandCodeOutputDoneStatus: Command Code keeps rendering // the composer while tools run, so only complete the row if no active repaint arrives. onCommandCodeDone: (prompt: string): void => { + if (!canApplyCommandCodeOutputStatus()) { + return + } const normalizedPrompt = prompt.trim() if (!normalizedPrompt) { cancelCommandCodeDoneSettle(paneKey) diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index 8bb448ed11a..5f5cf3a0afc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -17,6 +17,7 @@ import { TERMINAL_PASTE_DIRECT_MAX_BYTES } from './terminal-paste-coordinator' import { resolveWindowsShiftEnterEncodingForPane } from './terminal-windows-shift-enter' import type * as UseNotificationDispatchModule from './use-notification-dispatch' import { getEagerPtyBufferHandle } from './pty-dispatcher' +import type { AgentType } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' import { toAppSshPtyId } from '../../../../shared/ssh-pty-id' import type { SshConnectionState } from '../../../../shared/ssh-types' @@ -227,6 +228,7 @@ type StoreState = { consumePendingSnapshot: ReturnType runtimePaneTitlesByTabId: Record> agentStatusByPaneKey: Record + retainedAgentsByPaneKey: Record paneForegroundAgentByPaneKey: Record sleepingAgentSessionsByPaneKey: Record suppressedPtyExitIds: Record @@ -928,6 +930,7 @@ describe('connectPanePty', () => { consumePendingSnapshot: vi.fn(() => null), runtimePaneTitlesByTabId: {}, agentStatusByPaneKey: {}, + retainedAgentsByPaneKey: {}, paneForegroundAgentByPaneKey: {}, sleepingAgentSessionsByPaneKey: {}, suppressedPtyExitIds: {}, @@ -18999,6 +19002,90 @@ describe('connectPanePty', () => { }) }) + it('rejects Command Code facts when Claude owns the pane', async () => { + enableMainAuthority() + const { connectPanePty } = await import('./pty-connection') + const handler = await import('./terminal-side-effect-facts-handler') + const transport = createMockTransport() + transportFactoryQueue.push(transport) + vi.useFakeTimers() + const paneKey = makePaneKey('tab-1', LEAF_1) + const claudeStatus = { + paneKey, + state: 'done' as const, + prompt: 'Previous Claude turn', + updatedAt: Date.now(), + stateStartedAt: Date.now(), + agentType: 'claude' as const, + stateHistory: [] + } + mockStoreState.tabsByWorktree = { + 'wt-1': [{ id: 'tab-1', ptyId: null, launchAgent: 'claude' }] + } + mockStoreState.agentStatusByPaneKey[paneKey] = claudeStatus + mockStoreState.paneForegroundAgentByPaneKey[paneKey] = { + agent: 'claude', + shellForeground: false + } + + connectPanePty(createPane(1) as never, createManager(1) as never, createDeps() as never) + const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as (ptyId: string) => void + onPtySpawn('pty-fact-false-cc') + mockStoreState.setAgentStatus.mockClear() + + handler._dispatchTerminalSideEffectBatchForTest({ + ptyId: 'pty-fact-false-cc', + seq: 1, + facts: [ + { kind: 'command-code-working', prompt: 'False prompt' }, + { kind: 'command-code-done', prompt: 'False prompt' } + ] + }) + vi.advanceTimersByTime(2000) + + expect(mockStoreState.setAgentStatus).not.toHaveBeenCalled() + expect(mockStoreState.agentStatusByPaneKey[paneKey]).toBe(claudeStatus) + }) + + it('rejects Command Code facts when retained Claude identity owns the pane', async () => { + enableMainAuthority() + const { connectPanePty } = await import('./pty-connection') + const handler = await import('./terminal-side-effect-facts-handler') + const transport = createMockTransport() + transportFactoryQueue.push(transport) + vi.useFakeTimers() + const paneKey = makePaneKey('tab-1', LEAF_1) + const unknownStatus = { + paneKey, + state: 'working' as const, + prompt: '', + updatedAt: Date.now(), + stateStartedAt: Date.now(), + agentType: 'unknown' as const, + stateHistory: [] + } + mockStoreState.retainedAgentsByPaneKey[paneKey] = { agentType: 'claude' } + mockStoreState.agentStatusByPaneKey[paneKey] = unknownStatus + + connectPanePty(createPane(1) as never, createManager(1) as never, createDeps() as never) + const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as (ptyId: string) => void + onPtySpawn('pty-fact-retained-false-cc') + mockStoreState.setAgentStatus.mockClear() + + handler._dispatchTerminalSideEffectBatchForTest({ + ptyId: 'pty-fact-retained-false-cc', + seq: 1, + facts: [ + { kind: 'command-code-working', prompt: 'False prompt' }, + { kind: 'command-code-done', prompt: 'False prompt' } + ] + }) + vi.advanceTimersByTime(2000) + + expect(mockStoreState.setAgentStatus).not.toHaveBeenCalled() + expect(mockStoreState.agentStatusByPaneKey[paneKey]).toBe(unknownStatus) + }) + it('keeps Command Code working when a working fact lands before the done settles', async () => { enableMainAuthority() const { connectPanePty } = await import('./pty-connection') diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index b1eb0900865..d0b0c2a3338 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -267,6 +267,7 @@ import { openCommandCodeDoneSettle, setCommandCodeDoneSettleExecutor } from './command-code-done-settle' +import { canCommandCodeOutputOwnPane } from './command-code-output-ownership' import { isTerminalTabParked } from './terminal-parked-watcher-registry' import { getExecutionHostIdForWorktree, @@ -2818,7 +2819,21 @@ export function connectPanePty( .setAgentStatus(cacheKey, statusPayload, terminalTitle, undefined, routing) } + const canApplyCommandCodeOutputStatus = (): boolean => { + const state = useAppStore.getState() + const foreground = state.paneForegroundAgentByPaneKey[cacheKey] + return canCommandCodeOutputOwnPane({ + foregroundAgent: foreground?.agent, + shellForeground: foreground?.shellForeground, + paneOwnerAgent: getAuthoritativePaneAgent(), + retainedPaneOwnerAgent: state.retainedAgentsByPaneKey[cacheKey]?.agentType + }) + } + const seedCommandCodeOutputWorkingStatus = (prompt: string): void => { + if (!canApplyCommandCodeOutputStatus()) { + return + } clearCommandCodeOutputDoneTimer() const routing = resolveCurrentAgentStatusRouting() if (!routing) { @@ -2885,6 +2900,9 @@ export function connectPanePty( ) const clearCommandCodeOutputDoneTimer = (): void => cancelCommandCodeDoneSettle(cacheKey) const scheduleCommandCodeOutputDoneStatus = (prompt: string): void => { + if (!canApplyCommandCodeOutputStatus()) { + return + } const normalizedPrompt = prompt.trim() if (!normalizedPrompt) { cancelCommandCodeDoneSettle(cacheKey) diff --git a/src/shared/command-code-output-status.test.ts b/src/shared/command-code-output-status.test.ts index a86c89a174b..77a8005056e 100644 --- a/src/shared/command-code-output-status.test.ts +++ b/src/shared/command-code-output-status.test.ts @@ -29,7 +29,7 @@ describe('createCommandCodeOutputStatusDetector', () => { }) expect(detector.observe('Thinking about unrelated shell output')).toBe(false) - expect(detector.observe('# Command Code v0.27.2')).toBe(false) + expect(detector.observe('# Command Code v0.27.2\r\n')).toBe(false) expect(detector.observe('⌘ Parsing...')).toBe(true) expect(onWorking).toHaveBeenCalledWith('') @@ -43,7 +43,7 @@ describe('createCommandCodeOutputStatusDetector', () => { }) expect(detector.observe('# Command')).toBe(false) - expect(detector.observe(' Code v0.27.2')).toBe(false) + expect(detector.observe(' Code v0.27.2\r\n')).toBe(false) expect(detector.observe('⌘ Parsing...')).toBe(true) expect(onWorking).toHaveBeenCalledWith('') @@ -56,7 +56,7 @@ describe('createCommandCodeOutputStatusDetector', () => { onWorking }) - expect(detector.observe('# C\x1b[35mommand Co\x1b[0mde v0.27.2')).toBe(false) + expect(detector.observe('# C\x1b[35mommand Co\x1b[0mde v0.27.2\r\n')).toBe(false) expect(detector.observe('⌘ Parsing...')).toBe(true) expect(onWorking).toHaveBeenCalledWith('') @@ -74,6 +74,59 @@ describe('createCommandCodeOutputStatusDetector', () => { expect(onWorking).not.toHaveBeenCalled() }) + it('does not arm when another agent merely discusses Command Code', () => { + const onWorking = vi.fn() + const detector = createCommandCodeOutputStatusDetector({ + startupCommand: null, + onWorking + }) + + expect(detector.observe('// Why: Command Code exposes transcript prompts')).toBe(false) + expect(detector.observe('\r\n✻ Thinking...')).toBe(false) + + expect(onWorking).not.toHaveBeenCalled() + }) + + it.each([ + '# Command Code v1.2\r\n', + '# Command Code v1.2.3.4\r\n', + '# Command Code v01.2.3\r\n', + '# Command Code v1.2.3suffix\r\n', + '#\nCommand Code v1.2.3\r\n' + ])('does not arm for noncanonical banner %j', (banner) => { + const onWorking = vi.fn() + const detector = createCommandCodeOutputStatusDetector({ startupCommand: null, onWorking }) + + expect(detector.observe(banner)).toBe(false) + expect(detector.observe('✻ Thinking...')).toBe(false) + + expect(onWorking).not.toHaveBeenCalled() + }) + + it('does not arm from a four-part version split at a PTY chunk boundary', () => { + const onWorking = vi.fn() + const detector = createCommandCodeOutputStatusDetector({ startupCommand: null, onWorking }) + + expect(detector.observe('# Command Code v1.2.3')).toBe(false) + expect(detector.observe('.4\r\n')).toBe(false) + expect(detector.observe('✻ Thinking...')).toBe(false) + + expect(onWorking).not.toHaveBeenCalled() + }) + + it.each(['# Command Code v1.2.3\r\n', '# Command Code v1.2.3-beta.1+build.9\n'])( + 'arms for canonical banner %j', + (banner) => { + const onWorking = vi.fn() + const detector = createCommandCodeOutputStatusDetector({ startupCommand: null, onWorking }) + + expect(detector.observe(banner)).toBe(false) + expect(detector.observe('✻ Thinking...')).toBe(true) + + expect(onWorking).toHaveBeenCalledWith('') + } + ) + it.each([ 'Pondering', 'Contemplating', diff --git a/src/shared/command-code-output-status.ts b/src/shared/command-code-output-status.ts index 41e4c9eefc1..af862b5abc7 100644 --- a/src/shared/command-code-output-status.ts +++ b/src/shared/command-code-output-status.ts @@ -112,7 +112,16 @@ const ACTIVE_EXECUTION_STATUS_RE = new RegExp( `(?:^|[\\r\\n])\\s*(?:${COMMAND_CODE_STATUS_GLYPH_RE_SOURCE}\\s*)?(?:Executing:\\s+\\S|Running\\s*\\()` ) const IDLE_PROMPT_RE = /(?:^|[\r\n])\s*[❯>]\s+Ask your question\.\.\./ -const COMMAND_CODE_BANNER_RE = /\bCommand Code\b/ +const SEMVER_NUMBER_RE_SOURCE = '(?:0|[1-9]\\d*)' +const SEMVER_PRERELEASE_IDENTIFIER_RE_SOURCE = '(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)' +const SEMVER_BUILD_IDENTIFIER_RE_SOURCE = '[0-9A-Za-z-]+' +const COMMAND_CODE_BANNER_RE = new RegExp( + `(?:^|[\\r\\n])[ \\t]*#[ \\t]+Command Code[ \\t]+v` + + `${SEMVER_NUMBER_RE_SOURCE}\\.${SEMVER_NUMBER_RE_SOURCE}\\.${SEMVER_NUMBER_RE_SOURCE}` + + `(?:-${SEMVER_PRERELEASE_IDENTIFIER_RE_SOURCE}(?:\\.${SEMVER_PRERELEASE_IDENTIFIER_RE_SOURCE})*)?` + + `(?:\\+${SEMVER_BUILD_IDENTIFIER_RE_SOURCE}(?:\\.${SEMVER_BUILD_IDENTIFIER_RE_SOURCE})*)?` + + `(?=[ \\t]*[\\r\\n])` +) function cleanPromptCandidate(value: string): string { return cleanCommandCodePromptCandidate(stripTerminalControl(value)) @@ -233,17 +242,19 @@ export function createCommandCodeOutputStatusDetector(args: { : scanRawText if (!hasSeenCommandCodeUi) { - if ( - !rawTextMayContainCommandCodeBanner(scanRawText) && - !rawTextMayContainCommandCodeBanner(scanRawTextWithChunkBoundary) - ) { + if (!rawTextMayContainCommandCodeBanner(scanRawText)) { return false } const scanText = stripTerminalControl(scanRawText) const scanTextWithChunkBoundary = stripTerminalControl(scanRawTextWithChunkBoundary) + const previousTextWithChunkBoundaryLength = previousRawText + ? stripTerminalControl(`${previousRawText}\n`).length + : 0 if ( !COMMAND_CODE_BANNER_RE.test(scanText) && - !COMMAND_CODE_BANNER_RE.test(scanTextWithChunkBoundary) + !COMMAND_CODE_BANNER_RE.test( + scanTextWithChunkBoundary.slice(previousTextWithChunkBoundaryLength) + ) ) { return false }