diff --git a/src/main/runtime/launch-agent-terminal-followup.test.ts b/src/main/runtime/launch-agent-terminal-followup.test.ts new file mode 100644 index 00000000000..0fb1fe09101 --- /dev/null +++ b/src/main/runtime/launch-agent-terminal-followup.test.ts @@ -0,0 +1,78 @@ +// A headless automation launches on an existing workspace through launchAgentTerminal; agents that +// read their prompt after they start never got it because the startup follow-up was dropped. +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ + BrowserWindow: { fromId: vi.fn(() => null) }, + webContents: { fromId: vi.fn(() => null) }, + ipcMain: { on: vi.fn(), removeListener: vi.fn() }, + app: { getPath: vi.fn(() => '/tmp'), isPackaged: false } +})) + +import { OrcaRuntimeService } from './orca-runtime' + +function makeRuntime() { + const repo = { id: 'repo-1', path: '/home/me/app' } + const runtime = new OrcaRuntimeService() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reads only these protected members; each is assigned or stubbed before launchAgentTerminal runs. + const internals = runtime as unknown as { + store: Record + resolveWorktreeSelector: (selector: string) => Promise + createTerminal: (selector: string, opts: unknown) => Promise + sendStartupFollowupWhenReady: (handle: string, followup: unknown) => void + } + internals.store = { + getSettings: () => ({ disabledTuiAgents: [], workspaceDir: '/tmp/workspaces' }), + getProjectHostSetups: () => [], + getRepos: () => [repo], + getRepo: (id: string) => (id === repo.id ? repo : undefined) + } + vi.spyOn(internals, 'resolveWorktreeSelector').mockResolvedValue({ + id: 'repo-1::/home/me/app-feature', + repoId: 'repo-1', + path: '/home/me/app-feature' + }) + const createTerminal = vi.spyOn(internals, 'createTerminal').mockResolvedValue({ + handle: 'term_x', + worktreeId: 'repo-1::/home/me/app-feature', + title: null + }) + const sendFollowup = vi + .spyOn(internals, 'sendStartupFollowupWhenReady') + .mockImplementation(() => undefined) + return { runtime, createTerminal, sendFollowup } +} + +describe('launchAgentTerminal startup follow-up', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it('types the prompt into an agent that reads it after it starts', async () => { + const { runtime, sendFollowup } = makeRuntime() + + const terminal = await runtime.launchAgentTerminal('id:repo-1::/home/me/app-feature', { + agent: 'aider', + prompt: 'fix the flaky test' + }) + + expect(terminal).toMatchObject({ handle: 'term_x' }) + expect(sendFollowup).toHaveBeenCalledTimes(1) + expect(sendFollowup).toHaveBeenCalledWith( + 'term_x', + expect.objectContaining({ prompt: 'fix the flaky test' }) + ) + }) + + it('sends no follow-up when the prompt rides on the launch command', async () => { + const { runtime, createTerminal, sendFollowup } = makeRuntime() + + await runtime.launchAgentTerminal('id:repo-1::/home/me/app-feature', { + agent: 'claude', + prompt: 'fix the flaky test' + }) + + expect(createTerminal).toHaveBeenCalledTimes(1) + expect(sendFollowup).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts b/src/main/runtime/orca-runtime-terminal-create-deduplication.ts index 289a4fd4b7b..d5c8bf0498e 100644 --- a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts +++ b/src/main/runtime/orca-runtime-terminal-create-deduplication.ts @@ -167,7 +167,7 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC undefined, opts.extraAgentArgs ? { extraAgentArgs: opts.extraAgentArgs } : undefined ) - return await this.createTerminal(`id:${worktree.id}`, { + const terminal = await this.createTerminal(`id:${worktree.id}`, { command: startup.startup.command, env: startup.startup.env, ...(startup.startup.launchConfig ? { launchConfig: startup.startup.launchConfig } : {}), @@ -176,6 +176,11 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC telemetry: startup.startup.telemetry, title: opts.title }) + // Why: agents that read the prompt after they start get it typed in, not on argv. + if (startup.followup) { + this.sendStartupFollowupWhenReady(terminal.handle, startup.followup) + } + return terminal } // Why: dedupes a worktree.create whose response was lost when a mobile