mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Prevent Command Code output from hijacking agent icons (#12573)
* fix terminal agent icon ownership * fix terminal output ownership gaps
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string, { id: string; launchAgent?: AgentType }[]>
|
||||
agentStatusByPaneKey: Record<string, AgentStatusEntry | undefined>
|
||||
retainedAgentsByPaneKey: Record<string, { agentType: AgentType } | undefined>
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry>
|
||||
agentLaunchConfigByPaneKey: Record<string, { identity: { agentType?: AgentType } } | undefined>
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string | undefined>>
|
||||
setAgentStatus: ReturnType<typeof vi.fn>
|
||||
dropAgentStatus: ReturnType<typeof vi.fn>
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<typeof vi.fn>
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string>>
|
||||
agentStatusByPaneKey: Record<string, unknown>
|
||||
retainedAgentsByPaneKey: Record<string, { agentType: AgentType }>
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry>
|
||||
sleepingAgentSessionsByPaneKey: Record<string, unknown>
|
||||
suppressedPtyExitIds: Record<string, true>
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user