From fc40ca2bdf7c7ad369612e36192ba4783132e875 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 10 Jul 2026 05:48:10 -0400 Subject: [PATCH] fix(remote): deliver stdin-after-start prompt to interpreter-wrapped agents (aider/vibe) Co-authored-by: Orca --- .../src/lib/agent-followup-delivery.test.ts | 93 +++++++++++++++++++ .../src/lib/agent-followup-delivery.ts | 22 ++++- 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/lib/agent-followup-delivery.test.ts diff --git a/src/renderer/src/lib/agent-followup-delivery.test.ts b/src/renderer/src/lib/agent-followup-delivery.test.ts new file mode 100644 index 00000000000..11fa32bbdc2 --- /dev/null +++ b/src/renderer/src/lib/agent-followup-delivery.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { sendFollowupPromptWhenAgentReady } from './agent-followup-delivery' +import { + inspectRuntimeTerminalProcess, + sendRuntimePtyInputVerified +} from '@/runtime/runtime-terminal-inspection' +import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' + +vi.mock('@/runtime/runtime-terminal-inspection', () => ({ + inspectRuntimeTerminalProcess: vi.fn(), + sendRuntimePtyInputVerified: vi.fn() +})) + +// The interpreter-wrapped agents that deliver their prompt over stdin after the +// process starts. These are pip console-scripts, so the PTY foreground comm is +// python/python3 — never the agent's own name. +const INTERPRETER_WRAPPED_AGENTS = [ + { agent: 'aider', expectedProcess: TUI_AGENT_CONFIG.aider.expectedProcess }, + { agent: 'mistral-vibe', expectedProcess: TUI_AGENT_CONFIG['mistral-vibe'].expectedProcess } +] as const + +describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('globalThis', globalThis) + // Deliver the prompt write eagerly so the test does not depend on retries. + vi.mocked(sendRuntimePtyInputVerified).mockResolvedValue(true) + }) + + it('sanity: config keeps aider/vibe as stdin-after-start with python-style expected process', () => { + expect(TUI_AGENT_CONFIG.aider.promptInjectionMode).toBe('stdin-after-start') + expect(TUI_AGENT_CONFIG['mistral-vibe'].promptInjectionMode).toBe('stdin-after-start') + }) + + for (const { agent, expectedProcess } of INTERPRETER_WRAPPED_AGENTS) { + it(`types the prompt once ${agent} is up behind a python3 wrapper with a live child`, async () => { + // The console-script agent is running: foreground comm is python3 and the + // PTY has a non-shell child. The exact agent name never appears. + vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({ + foregroundProcess: 'python3', + hasChildProcesses: true + }) + + const delivered = await sendFollowupPromptWhenAgentReady({ + ptyId: 'pty-1', + expectedProcess, + prompt: 'ship it', + settings: null + }) + + expect(delivered).toBe(true) + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(null, 'pty-1', 'ship it\r') + }) + + it(`still refuses to type into a bare ${agent} shell foreground`, async () => { + // No agent yet: foreground is a plain shell with no non-shell child. The + // guard must NOT write user text into an arbitrary shell. + vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({ + foregroundProcess: 'zsh', + hasChildProcesses: false + }) + + const delivered = await sendFollowupPromptWhenAgentReady({ + ptyId: 'pty-1', + expectedProcess, + prompt: 'ship it', + settings: null + }) + + expect(delivered).toBe(false) + expect(sendRuntimePtyInputVerified).not.toHaveBeenCalled() + }) + } + + it('types immediately when the resolver already returns the agent name (local ps path)', async () => { + // On local desktop the ps-table resolver usually resolves python3 → aider + // before we poll; the exact-match path must keep working. + vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({ + foregroundProcess: 'aider', + hasChildProcesses: true + }) + + const delivered = await sendFollowupPromptWhenAgentReady({ + ptyId: 'pty-1', + expectedProcess: 'aider', + prompt: 'ship it', + settings: null + }) + + expect(delivered).toBe(true) + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(null, 'pty-1', 'ship it\r') + }) +}) diff --git a/src/renderer/src/lib/agent-followup-delivery.ts b/src/renderer/src/lib/agent-followup-delivery.ts index d5a4c6ff9b9..0042cb0f2e4 100644 --- a/src/renderer/src/lib/agent-followup-delivery.ts +++ b/src/renderer/src/lib/agent-followup-delivery.ts @@ -2,7 +2,11 @@ import { inspectRuntimeTerminalProcess, sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-inspection' -import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition' +import { + isAgentForegroundWrapperProcess, + isExpectedAgentProcess +} from '../../../shared/agent-process-recognition' +import { isShellProcess } from '../../../shared/shell-process-detection' import type { GlobalSettings } from '../../../shared/types' type RuntimeOwnerSettings = Pick | null | undefined @@ -25,7 +29,7 @@ export async function sendFollowupPromptWhenAgentReady(args: { } // Why: delayed follow-ups must not type into an arbitrary shell. Require a -// positive expected-process match before writing user/task text to the PTY. +// positive readiness signal before writing user/task text to the PTY. async function waitForAgentForeground( ptyId: string, expectedProcess: string, @@ -41,6 +45,20 @@ async function waitForAgentForeground( if (isExpectedAgentProcess(foreground, expectedProcess)) { return true } + // Why: interpreter-wrapped agents (aider, mistral-vibe are pip console + // scripts) surface a python/node foreground comm, so the exact-name check + // never matches — locally when the ps-table resolver can't pin the child, + // and over SSH when the relay falls back to the bare interpreter name. If + // the foreground is a known agent wrapper (not a shell) with a live + // non-shell child, the agent has taken over the PTY and can accept input. + if ( + attempt >= 4 && + isAgentForegroundWrapperProcess(foreground) && + !isShellProcess(foreground) && + process.hasChildProcesses + ) { + return true + } } catch { // Ignore transient PTY inspection failures and keep polling. }