mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(kimi): deliver Kimi's first prompt on Windows and after it is ready (#33581)
Co-authored-by: innocarpe <2222333+innocarpe@users.noreply.github.com>
This commit is contained in:
@@ -51,7 +51,7 @@ function harness(options: { startupTerminal?: Record<string, unknown> | null } =
|
||||
),
|
||||
getTerminalPtyIdForHandle: vi.fn((_handle: string) => 'pty-prev'),
|
||||
deliverStartupFollowup: vi.fn(
|
||||
async (_handle: string, _followup: { expectedProcess: string; prompt: string }) => true
|
||||
async (_handle: string, _followup: { agent: string; prompt: string }) => true
|
||||
)
|
||||
}
|
||||
return runtime
|
||||
@@ -172,7 +172,7 @@ describe('headless automation agent launch', () => {
|
||||
)
|
||||
expect(runtime.deliverStartupFollowup).toHaveBeenCalledTimes(1)
|
||||
expect(runtime.deliverStartupFollowup).toHaveBeenCalledWith('term_existing', {
|
||||
expectedProcess: 'aider',
|
||||
agent: 'aider',
|
||||
prompt: 'fix the flaky test'
|
||||
})
|
||||
finishDelivery(true)
|
||||
@@ -221,7 +221,7 @@ describe('headless automation agent launch', () => {
|
||||
expect(runtime.createManagedWorktree.mock.calls[0]![0]).not.toHaveProperty('startupPrompt')
|
||||
expect(runtime.deliverStartupFollowup).toHaveBeenCalledTimes(1)
|
||||
expect(runtime.deliverStartupFollowup).toHaveBeenCalledWith('term_new', {
|
||||
expectedProcess: 'goose',
|
||||
agent: 'goose',
|
||||
prompt: 'fix the flaky test'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
import { executeAgentLaunch } from '../agent-launch/agent-launch-executor'
|
||||
import type { Automation, AutomationRun } from '../../shared/automations-types'
|
||||
import { findReusableAutomationRun } from '../../shared/automation-session-reuse'
|
||||
import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
|
||||
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
import { deliverTerminalAgentLaunchPrompt } from '../runtime/rpc/methods/agent-launch-terminal-prompt'
|
||||
import type { HeadlessAutomationDispatchLaunch } from './headless-dispatch'
|
||||
@@ -116,10 +115,7 @@ export async function launchHeadlessAutomationAgent(
|
||||
}
|
||||
},
|
||||
deliverTerminalPrompt: ({ handle, prompt }) =>
|
||||
runtime.deliverStartupFollowup(handle, {
|
||||
expectedProcess: TUI_AGENT_CONFIG[agent].expectedProcess,
|
||||
prompt: prompt.text
|
||||
})
|
||||
runtime.deliverStartupFollowup(handle, { agent, prompt: prompt.text })
|
||||
}
|
||||
}
|
||||
await launchUntilPublished(execution)
|
||||
|
||||
@@ -13,6 +13,23 @@ import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
/** agent permits writing; shell drops readiness; unknown proves neither. */
|
||||
export type LaunchedAgentForeground = 'agent' | 'shell' | 'unknown'
|
||||
|
||||
export type LaunchedAgentHost = {
|
||||
remote: boolean
|
||||
windows: boolean
|
||||
/** Only a local non-WSL Windows pane can prove its shell alone in its job; wsl.exe and SSH reads stay unknown. */
|
||||
shellAloneProvable: boolean
|
||||
}
|
||||
|
||||
export function describeLaunchedAgentHost(
|
||||
pty: { connectionId: string | null; isWsl: boolean | null } | undefined,
|
||||
remotePathFlavor: () => 'posix' | 'win32'
|
||||
): LaunchedAgentHost {
|
||||
const remote = !!pty?.connectionId
|
||||
// A local WSL pane still runs on a Windows host, whose process reads cannot see into it.
|
||||
const windows = remote ? remotePathFlavor() === 'win32' : process.platform === 'win32'
|
||||
return { remote, windows, shellAloneProvable: windows && !remote && pty?.isWsl === false }
|
||||
}
|
||||
|
||||
function isLaunchShell(processName: string): boolean {
|
||||
return isShellProcess(processName.replace(/^-/, ''))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ export type LaunchedAgentWriteGuardRuntime = Pick<
|
||||
OrcaRuntimeService,
|
||||
'readLaunchedAgentForeground' | 'subscribeToTerminalData'
|
||||
> &
|
||||
Partial<Pick<OrcaRuntimeService, 'launchedAgentHostProvesAgent'>>
|
||||
Partial<
|
||||
Pick<OrcaRuntimeService, 'launchedAgentHostProvesAgent' | 'launchedAgentHostProvesShellAlone'>
|
||||
>
|
||||
|
||||
/** refuse: write only on agent proof. write-unless-shell: permit unknown. write-if-shell-provable: permit unknown only where a lone shell is provable. */
|
||||
export type UnprovableHostPolicy = 'refuse' | 'write-unless-shell' | 'write-if-shell-provable'
|
||||
|
||||
export type LaunchedAgentWriteGuard = {
|
||||
beforeWrite: (ptyId: string) => Promise<void>
|
||||
@@ -22,8 +27,7 @@ export type LaunchedAgentWriteGuard = {
|
||||
export function createLaunchedAgentWriteGuard(
|
||||
runtime: LaunchedAgentWriteGuardRuntime,
|
||||
agent: TuiAgent,
|
||||
/** Windows compatibility: write-unless-shell permits unknown; only proven shell refuses. */
|
||||
{ unprovableHost = 'refuse' }: { unprovableHost?: 'refuse' | 'write-unless-shell' } = {}
|
||||
{ unprovableHost = 'refuse' }: { unprovableHost?: UnprovableHostPolicy } = {}
|
||||
): LaunchedAgentWriteGuard {
|
||||
let cleared: { ptyId: string; shellMayHaveReturned: boolean; unsubscribe: () => void } | null =
|
||||
null
|
||||
@@ -61,7 +65,9 @@ export function createLaunchedAgentWriteGuard(
|
||||
if (
|
||||
foreground === 'shell' ||
|
||||
unprovableHost === 'refuse' ||
|
||||
runtime.launchedAgentHostProvesAgent?.(ptyId) !== false
|
||||
runtime.launchedAgentHostProvesAgent?.(ptyId) !== false ||
|
||||
(unprovableHost === 'write-if-shell-provable' &&
|
||||
runtime.launchedAgentHostProvesShellAlone?.(ptyId) !== true)
|
||||
) {
|
||||
throw new Error('agent_not_in_foreground')
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ describe('OrcaRuntimeService', () => {
|
||||
terminalDriverChanged: vi.fn()
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
// Delivery waits for composer readiness and pastes; its behaviour is covered with the delivery itself.
|
||||
const deliver = vi.spyOn(runtime, 'deliverStartupFollowup').mockResolvedValue(true)
|
||||
|
||||
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-cli-aider-startup')
|
||||
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-cli-aider-startup')
|
||||
@@ -94,8 +96,9 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
)
|
||||
await vi.waitFor(() => {
|
||||
expect(write).toHaveBeenCalledWith('pty-cli-aider-startup', 'fix it\r', 'launch')
|
||||
expect(deliver).toHaveBeenCalledWith(expect.any(String), { agent: 'aider', prompt: 'fix it' })
|
||||
})
|
||||
expect(write).not.toHaveBeenCalledWith('pty-cli-aider-startup', 'fix it\r', 'launch')
|
||||
})
|
||||
|
||||
it('does not send stdin-after-start prompts into a shell when the agent never starts', async () => {
|
||||
|
||||
@@ -189,15 +189,25 @@ export function agentLaunchSurfaceFactory(
|
||||
...(promptRodeLaunchCommand ? { promptRodeLaunchCommand } : {})
|
||||
}
|
||||
},
|
||||
deliverTerminalPrompt: async ({ handle, agent, freshLaunch, prompt, signal }) =>
|
||||
deliverTerminalAgentLaunchPrompt({
|
||||
runtime: context.runtime,
|
||||
deliverTerminalPrompt: async ({ handle, agent, freshLaunch, prompt, signal }) => {
|
||||
const { runtime } = context
|
||||
// A stdin-after-start prompt cannot ride a Windows launch line, so it needs the startup path's Windows proof (#10336).
|
||||
if (freshLaunch && typeof runtime.deliverStartupFollowup === 'function') {
|
||||
return runtime.deliverStartupFollowup(
|
||||
handle,
|
||||
{ agent, prompt: prompt.text },
|
||||
{ agentLaunch: true, ...(signal ? { signal } : {}) }
|
||||
)
|
||||
}
|
||||
return deliverTerminalAgentLaunchPrompt({
|
||||
runtime,
|
||||
handle,
|
||||
agent,
|
||||
freshLaunch,
|
||||
text: prompt.text,
|
||||
...(signal ? { signal } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ function runtimeStub(overrides: {
|
||||
foreground?: 'agent' | 'shell' | 'unknown'
|
||||
/** False on a host that cannot find the agent in front (Windows); default: it can. */
|
||||
hostProvesAgent?: boolean
|
||||
/** True on a local non-WSL Windows pane, whose lone shell is provable; default: not. */
|
||||
hostProvesShellAlone?: boolean
|
||||
}) {
|
||||
const queued = [...(overrides.waits ?? [])]
|
||||
const waitForTerminal = vi.fn(
|
||||
@@ -38,6 +40,7 @@ function runtimeStub(overrides: {
|
||||
const readLaunchedAgentForeground = vi.fn(async () => overrides.foreground ?? 'agent')
|
||||
const subscribeToTerminalData = vi.fn(() => () => {})
|
||||
const launchedAgentHostProvesAgent = vi.fn(() => overrides.hostProvesAgent ?? true)
|
||||
const launchedAgentHostProvesShellAlone = vi.fn(() => overrides.hostProvesShellAlone ?? false)
|
||||
const sendTerminalAgentPrompt = vi.fn<SendFn>(
|
||||
overrides.send ?? (async () => ({ handle: 'term_1', accepted: true, bytesWritten: 12 }))
|
||||
)
|
||||
@@ -53,7 +56,8 @@ function runtimeStub(overrides: {
|
||||
sendTerminalAgentPrompt,
|
||||
readLaunchedAgentForeground,
|
||||
subscribeToTerminalData,
|
||||
launchedAgentHostProvesAgent
|
||||
launchedAgentHostProvesAgent,
|
||||
launchedAgentHostProvesShellAlone
|
||||
} as unknown as Parameters<typeof deliverTerminalAgentLaunchPrompt>[0]['runtime']
|
||||
}
|
||||
}
|
||||
@@ -422,6 +426,40 @@ describe('writing a launch prompt into a terminal agent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// WSL's wsl.exe and SSH-to-Windows read as unknown even when a shell is alone, so only local Windows may write on unknown.
|
||||
describe('write-if-shell-provable', () => {
|
||||
it.each([
|
||||
['writes on a local Windows pane whose lone shell is provable', 'unknown', true, true],
|
||||
['refuses a shell proven alone', 'shell', true, false],
|
||||
['refuses an unknown WSL or SSH-to-Windows pane', 'unknown', false, false]
|
||||
] as const)('%s', async (_label, foreground, hostProvesShellAlone, expected) => {
|
||||
const stub = runtimeStub({
|
||||
composerSignal: true,
|
||||
foreground,
|
||||
hostProvesAgent: false,
|
||||
hostProvesShellAlone
|
||||
})
|
||||
stub.sendTerminalAgentPrompt.mockImplementation(async (handle, _text, options) => {
|
||||
const { beforeWrite } = options
|
||||
if (typeof beforeWrite === 'function') {
|
||||
await beforeWrite('pty-1')
|
||||
}
|
||||
return { handle, accepted: true, bytesWritten: 12 }
|
||||
})
|
||||
|
||||
await expect(
|
||||
deliverTerminalAgentLaunchPrompt({
|
||||
runtime: stub.runtime,
|
||||
handle: 'term_1',
|
||||
agent: 'kimi',
|
||||
freshLaunch: true,
|
||||
text: 'fix the flaky test',
|
||||
unprovableHost: 'write-if-shell-provable'
|
||||
})
|
||||
).resolves.toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelling a launch prompt', () => {
|
||||
it('pastes nothing when cancelled during the readiness wait, and forwards the signal', async () => {
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -13,7 +13,8 @@ import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { assertAgentPromptRequestActive } from '../../orca-runtime-core'
|
||||
import {
|
||||
createLaunchedAgentWriteGuard,
|
||||
type LaunchedAgentWriteGuardRuntime
|
||||
type LaunchedAgentWriteGuardRuntime,
|
||||
type UnprovableHostPolicy
|
||||
} from '../../launched-agent-write-guard'
|
||||
|
||||
/** The same budget orchestration gives a worker to reach its composer before dispatching to it. */
|
||||
@@ -21,7 +22,7 @@ const AGENT_READY_TIMEOUT_MS = 60_000
|
||||
/** How often a launch re-checks a blocking prompt the user may still dismiss. */
|
||||
const BLOCKED_RECHECK_MS = 1_000
|
||||
|
||||
type TerminalPromptRuntime = LaunchedAgentReadinessRuntime &
|
||||
export type TerminalPromptRuntime = LaunchedAgentReadinessRuntime &
|
||||
LaunchedAgentWriteGuardRuntime &
|
||||
Pick<OrcaRuntimeService, 'sendTerminalAgentPrompt'>
|
||||
|
||||
@@ -82,7 +83,7 @@ export async function deliverTerminalAgentLaunchPrompt(args: {
|
||||
freshLaunch: boolean
|
||||
text: string
|
||||
/** See `createLaunchedAgentWriteGuard`; default `refuse`. */
|
||||
unprovableHost?: 'refuse' | 'write-unless-shell'
|
||||
unprovableHost?: UnprovableHostPolicy
|
||||
clock?: ReadinessClock
|
||||
/** Aborting ends the readiness wait and refuses the paste; text already handed over stays so. */
|
||||
signal?: AbortSignal
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { RuntimeStore } from './runtime-store-contract'
|
||||
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
|
||||
|
||||
export type WorktreeStartupDraftPaste = { agent: TuiAgent; content: string }
|
||||
export type WorktreeStartupFollowup = { expectedProcess: string; prompt: string }
|
||||
export type WorktreeStartupFollowup = { agent: TuiAgent; prompt: string }
|
||||
|
||||
/** Require agent_started attribution on every host-built fresh launch. */
|
||||
type AttributedWorktreeStartupLaunch = WorktreeStartupLaunch & {
|
||||
@@ -210,10 +210,7 @@ export function buildWorktreeStartupForAgent(
|
||||
launchRuntime,
|
||||
...(startupPlan.followupPrompt
|
||||
? {
|
||||
followup: {
|
||||
expectedProcess: startupPlan.expectedProcess,
|
||||
prompt: startupPlan.followupPrompt
|
||||
}
|
||||
followup: { agent, prompt: startupPlan.followupPrompt }
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
@@ -3,15 +3,20 @@ import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OPENCODE_AGENT_ROW_GRACE_MS } from '../../shared/opencode-agent-row-scanner'
|
||||
import {
|
||||
deliverWorktreeStartupFollowup,
|
||||
deliverStartupFollowupPrompt,
|
||||
waitForWorktreeStartupDraft,
|
||||
type WorktreeStartupReadinessHost
|
||||
} from './runtime-worktree-startup-readiness'
|
||||
|
||||
describe('startup follow-up delivery', () => {
|
||||
describe('startup follow-up prompt', () => {
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
function followupHost(foreground: string) {
|
||||
function fixture(
|
||||
foreground: string,
|
||||
hostProvesAgent: boolean,
|
||||
hostProvesShellAlone = !hostProvesAgent,
|
||||
launchedForeground: 'agent' | 'unknown' = 'agent'
|
||||
) {
|
||||
const write = vi.fn()
|
||||
const host: WorktreeStartupReadinessHost = {
|
||||
getPtyId: () => 'pty-1',
|
||||
@@ -21,35 +26,96 @@ describe('startup follow-up delivery', () => {
|
||||
readRecentOutput: () => undefined,
|
||||
write
|
||||
}
|
||||
return { host, write }
|
||||
const sendTerminalAgentPrompt = vi.fn(async (handle: string) => ({
|
||||
handle,
|
||||
accepted: true,
|
||||
bytesWritten: 12
|
||||
}))
|
||||
const waitForFreshWorkerComposer = vi.fn(async () => ({ satisfied: true, status: 'running' }))
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: delivery reaches only these runtime methods; any other would throw.
|
||||
const runtime = {
|
||||
waitForFreshWorkerComposer,
|
||||
waitForTerminal: vi.fn(async () => ({ satisfied: true, status: 'idle' })),
|
||||
sendTerminalAgentPrompt,
|
||||
readLaunchedAgentForeground: vi.fn(async () => launchedForeground),
|
||||
subscribeToTerminalData: vi.fn(() => () => {}),
|
||||
launchedAgentHostProvesAgent: vi.fn(() => hostProvesAgent),
|
||||
launchedAgentHostProvesShellAlone: vi.fn(() => hostProvesShellAlone)
|
||||
} as unknown as Parameters<typeof deliverStartupFollowupPrompt>[0]
|
||||
return { host, write, runtime, sendTerminalAgentPrompt, waitForFreshWorkerComposer }
|
||||
}
|
||||
|
||||
it('types the prompt once the agent is in front and reports it written', async () => {
|
||||
const { host, write } = followupHost('aider')
|
||||
// Kimi is in front before its TUI reads input; text typed then is echoed and lost (#10336).
|
||||
it('pastes and submits through composer readiness, never typing raw input', async () => {
|
||||
const f = fixture('kimi', true)
|
||||
|
||||
await expect(
|
||||
deliverWorktreeStartupFollowup(host, 'term-1', {
|
||||
expectedProcess: 'aider',
|
||||
deliverStartupFollowupPrompt(f.runtime, f.host, 'term-1', {
|
||||
agent: 'kimi',
|
||||
prompt: 'fix the flaky test'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
expect(write).toHaveBeenCalledOnce()
|
||||
expect(write).toHaveBeenCalledWith('pty-1', 'fix the flaky test\r', 'launch')
|
||||
expect(f.waitForFreshWorkerComposer).toHaveBeenCalled()
|
||||
expect(f.sendTerminalAgentPrompt).toHaveBeenCalledWith(
|
||||
'term-1',
|
||||
'fix the flaky test',
|
||||
expect.objectContaining({ inputKind: 'launch', composerReady: true })
|
||||
)
|
||||
expect(f.write).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('writes nothing and reports false when the agent never comes to the front', async () => {
|
||||
// WSL and SSH-to-Windows reads cannot prove a lone shell, so they keep main's behaviour per caller.
|
||||
it('types the follow-up on WSL or SSH-to-Windows once the agent is in front, as before', async () => {
|
||||
const f = fixture('kimi', false, false, 'unknown')
|
||||
|
||||
await expect(
|
||||
deliverStartupFollowupPrompt(f.runtime, f.host, 'term-1', { agent: 'kimi', prompt: 'go' })
|
||||
).resolves.toBe(true)
|
||||
expect(f.write).toHaveBeenCalledWith('pty-1', 'go\r', 'launch')
|
||||
expect(f.sendTerminalAgentPrompt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still refuses an agent.launch prompt on WSL or SSH-to-Windows, as before', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const f = fixture('kimi', false, false, 'unknown')
|
||||
f.sendTerminalAgentPrompt.mockImplementation(async (handle: string, ...rest: unknown[]) => {
|
||||
const options = rest[1]
|
||||
if (options && typeof options === 'object' && 'beforeWrite' in options) {
|
||||
const { beforeWrite } = options
|
||||
if (typeof beforeWrite === 'function') {
|
||||
await beforeWrite('pty-1')
|
||||
}
|
||||
}
|
||||
return { handle, accepted: true, bytesWritten: 2 }
|
||||
})
|
||||
|
||||
await expect(
|
||||
deliverStartupFollowupPrompt(
|
||||
f.runtime,
|
||||
f.host,
|
||||
'term-1',
|
||||
{ agent: 'kimi', prompt: 'go' },
|
||||
{ agentLaunch: true }
|
||||
)
|
||||
).resolves.toBe(false)
|
||||
expect(f.write).not.toHaveBeenCalled()
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('writes nothing on a Windows host while the shell stays in front', async () => {
|
||||
vi.useFakeTimers()
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { host, write } = followupHost('zsh')
|
||||
const f = fixture('pwsh', false)
|
||||
|
||||
const delivered = deliverWorktreeStartupFollowup(host, 'term-1', {
|
||||
expectedProcess: 'aider',
|
||||
const delivered = deliverStartupFollowupPrompt(f.runtime, f.host, 'term-1', {
|
||||
agent: 'kimi',
|
||||
prompt: 'fix the flaky test'
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(30 * 150)
|
||||
|
||||
await expect(delivered).resolves.toBe(false)
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
expect(f.sendTerminalAgentPrompt).not.toHaveBeenCalled()
|
||||
expect(f.write).not.toHaveBeenCalled()
|
||||
warn.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,10 @@ import type {
|
||||
WorktreeStartupDraftPaste,
|
||||
WorktreeStartupFollowup
|
||||
} from './runtime-worktree-agent-startup'
|
||||
import {
|
||||
deliverTerminalAgentLaunchPrompt,
|
||||
type TerminalPromptRuntime
|
||||
} from './rpc/methods/agent-launch-terminal-prompt'
|
||||
|
||||
const BRACKETED_PASTE_BEGIN = '\x1b[200~'
|
||||
const BRACKETED_PASTE_END = '\x1b[201~'
|
||||
@@ -46,30 +50,43 @@ export function pasteWorktreeStartupDraftWhenReady(
|
||||
.catch((error) => console.warn('[worktree-create] failed to paste startup draft:', error))
|
||||
}
|
||||
|
||||
export function sendWorktreeStartupFollowupWhenReady(
|
||||
/** A post-start agent's first prompt: its process in front can still drop typed text (#10336). */
|
||||
export async function deliverStartupFollowupPrompt(
|
||||
runtime: TerminalPromptRuntime,
|
||||
host: WorktreeStartupReadinessHost,
|
||||
handle: string,
|
||||
followup: WorktreeStartupFollowup
|
||||
): void {
|
||||
void deliverWorktreeStartupFollowup(host, handle, followup)
|
||||
}
|
||||
|
||||
/** Types the follow-up once the agent process is in front; true when it was written. */
|
||||
export async function deliverWorktreeStartupFollowup(
|
||||
host: WorktreeStartupReadinessHost,
|
||||
handle: string,
|
||||
followup: WorktreeStartupFollowup
|
||||
{ agent, prompt }: WorktreeStartupFollowup,
|
||||
options: { signal?: AbortSignal; agentLaunch?: boolean } = {}
|
||||
): Promise<boolean> {
|
||||
const { signal } = options
|
||||
try {
|
||||
const ptyId = await waitForWorktreeStartupFollowup(host, handle, followup.expectedProcess)
|
||||
if (!ptyId) {
|
||||
console.warn('[worktree-create] agent did not become ready for follow-up prompt')
|
||||
return false
|
||||
const ptyId = host.getPtyId(handle)
|
||||
// Where no read proves the agent in front (Windows), its process must be seen first: a lone shell's prompt also reads as ready.
|
||||
if (ptyId && runtime.launchedAgentHostProvesAgent?.(ptyId) === false) {
|
||||
const expected = TUI_AGENT_CONFIG[agent].expectedProcess
|
||||
if (!(await waitForWorktreeStartupFollowup(host, handle, expected, signal))) {
|
||||
console.warn(
|
||||
'[agent-startup] agent did not come to the front; its prompt was not delivered'
|
||||
)
|
||||
return false
|
||||
}
|
||||
// WSL and SSH-to-Windows cannot prove a lone shell either: keep their typed follow-up; agent.launch refused there.
|
||||
if (!options.agentLaunch && runtime.launchedAgentHostProvesShellAlone?.(ptyId) !== true) {
|
||||
host.write(ptyId, `${prompt}\r`, 'launch')
|
||||
return true
|
||||
}
|
||||
}
|
||||
host.write(ptyId, `${followup.prompt}\r`, 'launch')
|
||||
return true
|
||||
return await deliverTerminalAgentLaunchPrompt({
|
||||
runtime,
|
||||
handle,
|
||||
agent,
|
||||
freshLaunch: true,
|
||||
text: prompt,
|
||||
unprovableHost: 'write-if-shell-provable',
|
||||
...(signal ? { signal } : {})
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[worktree-create] failed to send startup follow-up prompt:', error)
|
||||
console.warn('[agent-startup] failed to deliver the startup prompt:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -77,7 +94,8 @@ export async function deliverWorktreeStartupFollowup(
|
||||
export async function waitForWorktreeStartupFollowup(
|
||||
host: WorktreeStartupReadinessHost,
|
||||
handle: string,
|
||||
expectedProcess: string
|
||||
expectedProcess: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<string | null> {
|
||||
const ptyId = host.getPtyId(handle)
|
||||
if (!ptyId) {
|
||||
@@ -87,6 +105,9 @@ export async function waitForWorktreeStartupFollowup(
|
||||
if (attempt > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
}
|
||||
if (signal?.aborted) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const foregroundProcess = await host.getForegroundProcess(ptyId)
|
||||
if (isExpectedAgentProcess(foregroundProcess, expectedProcess)) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { sendTerminalStreamInput } from './rpc/methods/terminal/terminal-input-d
|
||||
import { makeStore } from './runtime-rpc-worktree-store-fixtures'
|
||||
import {
|
||||
pasteWorktreeStartupDraftWhenReady,
|
||||
sendWorktreeStartupFollowupWhenReady,
|
||||
type WorktreeStartupReadinessHost
|
||||
} from './runtime-worktree-startup-readiness'
|
||||
|
||||
@@ -235,20 +234,16 @@ describe('run facts: a created worktree’s startup writes', () => {
|
||||
}
|
||||
}
|
||||
|
||||
it('reads a run whose only input was its create-time draft and follow-up as untyped', async () => {
|
||||
it('reads a run whose only input was its create-time draft as untyped', async () => {
|
||||
vi.useFakeTimers()
|
||||
const run = await createFreshRun()
|
||||
const host = readinessHost(run)
|
||||
|
||||
pasteWorktreeStartupDraftWhenReady(host, run.handle, { agent: 'aider', content: 'plan it' })
|
||||
sendWorktreeStartupFollowupWhenReady(host, run.handle, {
|
||||
expectedProcess: 'aider',
|
||||
prompt: 'and ship it'
|
||||
})
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
expect(run.kinds).toEqual(['launch', 'launch'])
|
||||
expect(run.writes).toHaveLength(2)
|
||||
expect(run.kinds).toEqual(['launch'])
|
||||
expect(run.writes).toHaveLength(1)
|
||||
expect(run.firstUserInputAt()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,9 +30,8 @@ import type {
|
||||
} from '../../../shared/worktree/lineage-types'
|
||||
import { recordCreatedWorktreeLineage as recordCreatedWorktreeLineageState } from '../runtime-worktree-lineage-recording'
|
||||
import {
|
||||
deliverWorktreeStartupFollowup,
|
||||
deliverStartupFollowupPrompt,
|
||||
pasteWorktreeStartupDraftWhenReady,
|
||||
sendWorktreeStartupFollowupWhenReady,
|
||||
waitForWorktreeStartupDraft,
|
||||
type WorktreeStartupReadinessHost
|
||||
} from '../runtime-worktree-startup-readiness'
|
||||
@@ -44,8 +43,10 @@ import {
|
||||
import { readFreshComposerHold } from '../launched-agent-composer-readiness'
|
||||
import { buildTerminalWaitText } from '../terminal-wait-tail-state'
|
||||
import {
|
||||
describeLaunchedAgentHost,
|
||||
readLaunchedAgentForeground,
|
||||
type LaunchedAgentForeground
|
||||
type LaunchedAgentForeground,
|
||||
type LaunchedAgentHost
|
||||
} from '../launched-agent-foreground'
|
||||
import { worktreeRowTarget } from '../../persistence/host-qualified-worktree-meta'
|
||||
|
||||
@@ -271,12 +272,8 @@ export abstract class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntime
|
||||
|
||||
/** What holds the terminal a launch started its agent in, read fresh from the execution host. */
|
||||
readLaunchedAgentForeground(ptyId: string, agent: TuiAgent): Promise<LaunchedAgentForeground> {
|
||||
return readLaunchedAgentForeground(
|
||||
this.ptyController,
|
||||
this.launchedAgentHost(ptyId),
|
||||
ptyId,
|
||||
agent
|
||||
)
|
||||
const host = this.launchedAgentHost(ptyId)
|
||||
return readLaunchedAgentForeground(this.ptyController, host, ptyId, agent)
|
||||
}
|
||||
|
||||
/** Whether the pane's execution host can find a launched agent in front: a Windows one cannot. */
|
||||
@@ -284,23 +281,26 @@ export abstract class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntime
|
||||
return !this.launchedAgentHost(ptyId).windows
|
||||
}
|
||||
|
||||
private launchedAgentHost(ptyId: string): { remote: boolean; windows: boolean } {
|
||||
launchedAgentHostProvesShellAlone(ptyId: string): boolean {
|
||||
return this.launchedAgentHost(ptyId).shellAloneProvable
|
||||
}
|
||||
|
||||
private launchedAgentHost(ptyId: string): LaunchedAgentHost {
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
const remote = !!pty?.connectionId
|
||||
// A local WSL pane still runs on a Windows host, whose process reads cannot see into it.
|
||||
return {
|
||||
remote,
|
||||
windows: remote ? this.pathFlavorForPty(pty) === 'win32' : process.platform === 'win32'
|
||||
}
|
||||
return describeLaunchedAgentHost(pty, () => this.pathFlavorForPty(pty))
|
||||
}
|
||||
|
||||
protected sendStartupFollowupWhenReady(handle: string, followup: WorktreeStartupFollowup): void {
|
||||
sendWorktreeStartupFollowupWhenReady(this.getWorktreeStartupReadinessHost(), handle, followup)
|
||||
void this.deliverStartupFollowup(handle, followup)
|
||||
}
|
||||
|
||||
/** The same follow-up write, awaited: true once the prompt was typed into the agent. */
|
||||
deliverStartupFollowup(handle: string, followup: WorktreeStartupFollowup): Promise<boolean> {
|
||||
return deliverWorktreeStartupFollowup(this.getWorktreeStartupReadinessHost(), handle, followup)
|
||||
deliverStartupFollowup(
|
||||
handle: string,
|
||||
followup: WorktreeStartupFollowup,
|
||||
options?: { signal?: AbortSignal; agentLaunch?: boolean }
|
||||
): Promise<boolean> {
|
||||
const host = this.getWorktreeStartupReadinessHost()
|
||||
return deliverStartupFollowupPrompt(this, host, handle, followup, options)
|
||||
}
|
||||
|
||||
async provisionManagedWorktreeTerminals(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { sendFollowupPromptWhenAgentReady } from './agent-followup-delivery'
|
||||
import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspection'
|
||||
import { sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-verified-input'
|
||||
import { pasteDraftToAgentPtyWhenReady } from '@/lib/agent-paste-draft'
|
||||
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
|
||||
|
||||
vi.mock('@/runtime/runtime-terminal-inspection', () => ({
|
||||
inspectRuntimeTerminalProcess: vi.fn()
|
||||
}))
|
||||
vi.mock('@/runtime/runtime-terminal-verified-input', () => ({
|
||||
sendRuntimePtyInputVerified: vi.fn()
|
||||
vi.mock('@/lib/agent-paste-draft', () => ({
|
||||
pasteDraftToAgentPtyWhenReady: vi.fn()
|
||||
}))
|
||||
|
||||
// Pip console-script agents expose python/python3 foreground comm rather than their agent name.
|
||||
@@ -16,13 +16,22 @@ const INTERPRETER_WRAPPED_AGENTS = [
|
||||
{ agent: 'aider', expectedProcess: TUI_AGENT_CONFIG.aider.expectedProcess }
|
||||
] as const
|
||||
|
||||
// The prompt is pasted and submitted through composer readiness, never typed with a raw CR.
|
||||
const SUBMITTED_PASTE = {
|
||||
ptyId: 'pty-1',
|
||||
content: 'ship it',
|
||||
agent: 'aider',
|
||||
submit: true,
|
||||
forcePaste: true
|
||||
}
|
||||
|
||||
describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
vi.stubGlobal('globalThis', globalThis)
|
||||
// Deliver the prompt write eagerly so the test does not depend on retries.
|
||||
vi.mocked(sendRuntimePtyInputVerified).mockResolvedValue(true)
|
||||
vi.mocked(pasteDraftToAgentPtyWhenReady).mockResolvedValue(true)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -40,6 +49,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
|
||||
const delivery = sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
agent,
|
||||
expectedProcess,
|
||||
prompt: 'ship it'
|
||||
})
|
||||
@@ -48,10 +58,8 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
const delivered = await delivery
|
||||
|
||||
expect(delivered).toBe(true)
|
||||
expect(sendRuntimePtyInputVerified).toHaveBeenCalledExactlyOnceWith(
|
||||
'pty-1',
|
||||
'ship it\r',
|
||||
'launch'
|
||||
expect(pasteDraftToAgentPtyWhenReady).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining(SUBMITTED_PASTE)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -64,6 +72,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
|
||||
const delivery = sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
agent,
|
||||
expectedProcess,
|
||||
prompt: 'ship it'
|
||||
})
|
||||
@@ -72,7 +81,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
const delivered = await delivery
|
||||
|
||||
expect(delivered).toBe(false)
|
||||
expect(sendRuntimePtyInputVerified).not.toHaveBeenCalled()
|
||||
expect(pasteDraftToAgentPtyWhenReady).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it(`refuses to type into a ${agent} wrapper without a live child`, async () => {
|
||||
@@ -83,6 +92,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
|
||||
const delivery = sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
agent,
|
||||
expectedProcess,
|
||||
prompt: 'ship it'
|
||||
})
|
||||
@@ -91,7 +101,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
const delivered = await delivery
|
||||
|
||||
expect(delivered).toBe(false)
|
||||
expect(sendRuntimePtyInputVerified).not.toHaveBeenCalled()
|
||||
expect(pasteDraftToAgentPtyWhenReady).not.toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,11 +114,33 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () =
|
||||
|
||||
const delivered = await sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
agent: 'aider',
|
||||
expectedProcess: 'aider',
|
||||
prompt: 'ship it'
|
||||
})
|
||||
|
||||
expect(delivered).toBe(true)
|
||||
expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', 'ship it\r', 'launch')
|
||||
expect(pasteDraftToAgentPtyWhenReady).toHaveBeenCalledWith(
|
||||
expect.objectContaining(SUBMITTED_PASTE)
|
||||
)
|
||||
})
|
||||
|
||||
it('pastes nothing when the agent left the terminal during the readiness wait', async () => {
|
||||
vi.mocked(inspectRuntimeTerminalProcess)
|
||||
.mockResolvedValueOnce({ foregroundProcess: 'aider', hasChildProcesses: true })
|
||||
.mockResolvedValue({ foregroundProcess: 'zsh', hasChildProcesses: false })
|
||||
// Run the real last check the paste would make once the composer reads ready.
|
||||
vi.mocked(pasteDraftToAgentPtyWhenReady).mockImplementation(async ({ beforePaste }) =>
|
||||
beforePaste ? await beforePaste() : true
|
||||
)
|
||||
|
||||
const delivered = await sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
agent: 'aider',
|
||||
expectedProcess: 'aider',
|
||||
prompt: 'ship it'
|
||||
})
|
||||
|
||||
expect(delivered).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspection'
|
||||
import { sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-verified-input'
|
||||
import { pasteDraftToAgentPtyWhenReady } from '@/lib/agent-paste-draft'
|
||||
import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
import {
|
||||
isAgentForegroundWrapperProcess,
|
||||
isExpectedAgentProcess
|
||||
@@ -8,15 +9,25 @@ import { isShellProcess } from '../../../shared/shell-process-detection'
|
||||
|
||||
export async function sendFollowupPromptWhenAgentReady(args: {
|
||||
ptyId: string
|
||||
agent: TuiAgent
|
||||
expectedProcess: string
|
||||
prompt: string
|
||||
}): Promise<boolean> {
|
||||
const { ptyId, expectedProcess, prompt } = args
|
||||
const { ptyId, agent, expectedProcess, prompt } = args
|
||||
if (!(await waitForAgentForeground(ptyId, expectedProcess))) {
|
||||
return false
|
||||
}
|
||||
// Why: an agent in front may not read input yet; Kimi drops text typed before its composer is up (#10336).
|
||||
try {
|
||||
return await sendRuntimePtyInputVerified(ptyId, `${prompt}\r`, 'launch')
|
||||
return await pasteDraftToAgentPtyWhenReady({
|
||||
ptyId,
|
||||
content: prompt,
|
||||
agent,
|
||||
submit: true,
|
||||
forcePaste: true,
|
||||
// An agent that exited during the readiness wait must not have its prompt pasted into the shell.
|
||||
beforePaste: () => agentHoldsPty(ptyId, expectedProcess, true)
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@@ -28,24 +39,31 @@ async function waitForAgentForeground(ptyId: string, expectedProcess: string): P
|
||||
if (attempt > 0) {
|
||||
await new Promise((resolve) => globalThis.setTimeout(resolve, 150))
|
||||
}
|
||||
try {
|
||||
const process = await inspectRuntimeTerminalProcess(ptyId)
|
||||
const foreground = process.foregroundProcess?.toLowerCase() ?? ''
|
||||
if (isExpectedAgentProcess(foreground, expectedProcess)) {
|
||||
return true
|
||||
}
|
||||
// Wrapped agents may expose only python/node comm locally or over SSH; require a known wrapper plus a live non-shell child.
|
||||
if (
|
||||
attempt >= 4 &&
|
||||
isAgentForegroundWrapperProcess(foreground) &&
|
||||
!isShellProcess(foreground) &&
|
||||
process.hasChildProcesses
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// Ignore transient PTY inspection failures and keep polling.
|
||||
if (await agentHoldsPty(ptyId, expectedProcess, attempt >= 4)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function agentHoldsPty(
|
||||
ptyId: string,
|
||||
expectedProcess: string,
|
||||
acceptWrapper: boolean
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const process = await inspectRuntimeTerminalProcess(ptyId)
|
||||
const foreground = process.foregroundProcess?.toLowerCase() ?? ''
|
||||
// Wrapped agents may expose only python/node comm locally or over SSH; require a known wrapper plus a live non-shell child.
|
||||
return (
|
||||
isExpectedAgentProcess(foreground, expectedProcess) ||
|
||||
(acceptWrapper &&
|
||||
isAgentForegroundWrapperProcess(foreground) &&
|
||||
!isShellProcess(foreground) &&
|
||||
process.hasChildProcesses === true)
|
||||
)
|
||||
} catch {
|
||||
// Transient PTY inspection failures prove nothing.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ export async function pasteDraftToAgentPtyWhenReady(args: {
|
||||
timeoutMs?: number
|
||||
onTimeout?: () => void
|
||||
onUnconfirmedDelivery?: () => void
|
||||
/** Last check after readiness; false drops the paste. */
|
||||
beforePaste?: () => Promise<boolean>
|
||||
}): Promise<boolean> {
|
||||
const { ptyId, content, agent, submit, forcePaste, timeoutMs, onTimeout, onUnconfirmedDelivery } =
|
||||
args
|
||||
@@ -132,6 +134,9 @@ export async function pasteDraftToAgentPtyWhenReady(args: {
|
||||
}
|
||||
onUnconfirmedDelivery?.()
|
||||
}
|
||||
if (args.beforePaste && !(await args.beforePaste())) {
|
||||
return false
|
||||
}
|
||||
|
||||
return await sendBracketedPasteToAgent({
|
||||
ptyId,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockInspectRuntimeTerminalProcess,
|
||||
mockSendRuntimePtyInputVerified,
|
||||
mockPasteDraftToAgentPtyWhenReady,
|
||||
mockShowAutomationPromptNotSentToast,
|
||||
mockTrack,
|
||||
@@ -11,7 +10,6 @@ const {
|
||||
startupLeafId
|
||||
} = vi.hoisted(() => ({
|
||||
mockInspectRuntimeTerminalProcess: vi.fn(),
|
||||
mockSendRuntimePtyInputVerified: vi.fn(),
|
||||
mockPasteDraftToAgentPtyWhenReady: vi.fn(),
|
||||
mockShowAutomationPromptNotSentToast: vi.fn(),
|
||||
mockTrack: vi.fn(),
|
||||
@@ -75,9 +73,6 @@ vi.mock('@/store', () => ({
|
||||
vi.mock('@/runtime/runtime-terminal-inspection', () => ({
|
||||
inspectRuntimeTerminalProcess: mockInspectRuntimeTerminalProcess
|
||||
}))
|
||||
vi.mock('@/runtime/runtime-terminal-verified-input', () => ({
|
||||
sendRuntimePtyInputVerified: mockSendRuntimePtyInputVerified
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/agent-paste-draft', () => ({
|
||||
pasteDraftToAgentPtyWhenReady: mockPasteDraftToAgentPtyWhenReady
|
||||
@@ -277,7 +272,6 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => {
|
||||
foregroundProcess: 'aider',
|
||||
hasChildProcesses: true
|
||||
})
|
||||
mockSendRuntimePtyInputVerified.mockResolvedValue(true)
|
||||
mockPasteDraftToAgentPtyWhenReady.mockResolvedValue(true)
|
||||
})
|
||||
|
||||
@@ -298,16 +292,20 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(mockSendRuntimePtyInputVerified).toHaveBeenCalledWith(
|
||||
'pty-1',
|
||||
'fix the spinner\r',
|
||||
'launch'
|
||||
expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ptyId: 'pty-1',
|
||||
content: 'fix the spinner',
|
||||
agent: 'aider',
|
||||
submit: true,
|
||||
forcePaste: true
|
||||
})
|
||||
)
|
||||
expect(mockTrack).not.toHaveBeenCalledWith('agent_prompt_sent', expect.anything())
|
||||
})
|
||||
|
||||
it('does not track when follow-up prompt delivery is rejected by the terminal runtime', async () => {
|
||||
mockSendRuntimePtyInputVerified.mockResolvedValue(false)
|
||||
mockPasteDraftToAgentPtyWhenReady.mockResolvedValue(false)
|
||||
|
||||
await ensureAgentStartupInTerminal({
|
||||
worktreeId: 'wt-1',
|
||||
@@ -341,7 +339,7 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(mockSendRuntimePtyInputVerified).not.toHaveBeenCalled()
|
||||
expect(mockPasteDraftToAgentPtyWhenReady).not.toHaveBeenCalled()
|
||||
expect(mockShowAutomationPromptNotSentToast).toHaveBeenCalledWith('aider')
|
||||
})
|
||||
|
||||
@@ -382,7 +380,7 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => {
|
||||
})
|
||||
|
||||
it('does not track when follow-up prompt delivery rejects', async () => {
|
||||
mockSendRuntimePtyInputVerified.mockRejectedValue(new Error('runtime timeout'))
|
||||
mockPasteDraftToAgentPtyWhenReady.mockRejectedValue(new Error('runtime timeout'))
|
||||
|
||||
await expect(
|
||||
ensureAgentStartupInTerminal({
|
||||
@@ -806,7 +804,7 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => {
|
||||
await vi.advanceTimersByTimeAsync(5_000)
|
||||
await delivery
|
||||
|
||||
expect(mockSendRuntimePtyInputVerified).not.toHaveBeenCalled()
|
||||
expect(mockPasteDraftToAgentPtyWhenReady).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -231,6 +231,7 @@ async function deliverAgentStartupToTerminal(
|
||||
if (startup.followupPrompt) {
|
||||
const delivered = await sendFollowupPromptWhenAgentReady({
|
||||
ptyId,
|
||||
agent: startup.agent,
|
||||
expectedProcess: startup.expectedProcess,
|
||||
prompt: startup.followupPrompt
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user