From 167deb96970059c5b15547731dc88ebda862ab3d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 10 Jul 2026 05:51:53 -0400 Subject: [PATCH] fix(remote): surface a toast when agent startup prompt/draft delivery is dropped Co-authored-by: Orca --- src/renderer/src/lib/new-workspace.test.ts | 77 ++++++++++++++++++++-- src/renderer/src/lib/new-workspace.ts | 12 +++- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/lib/new-workspace.test.ts b/src/renderer/src/lib/new-workspace.test.ts index 8306ca0d62c..dc70d24841f 100644 --- a/src/renderer/src/lib/new-workspace.test.ts +++ b/src/renderer/src/lib/new-workspace.test.ts @@ -4,6 +4,7 @@ const { mockInspectRuntimeTerminalProcess, mockSendRuntimePtyInputVerified, mockPasteDraftToAgentPtyWhenReady, + mockShowAutomationPromptNotSentToast, mockTrack, store, storeListeners, @@ -12,6 +13,7 @@ const { mockInspectRuntimeTerminalProcess: vi.fn(), mockSendRuntimePtyInputVerified: vi.fn(), mockPasteDraftToAgentPtyWhenReady: vi.fn(), + mockShowAutomationPromptNotSentToast: vi.fn(), mockTrack: vi.fn(), storeListeners: new Set<(state: unknown, previousState: unknown) => void>(), startupLeafId: '11111111-1111-4111-8111-111111111111', @@ -88,6 +90,10 @@ vi.mock('@/lib/telemetry', () => ({ track: mockTrack })) +vi.mock('@/lib/agent-background-session-timeout-toast', () => ({ + showAutomationPromptNotSentToast: mockShowAutomationPromptNotSentToast +})) + import { ensureAgentStartupInTerminal, getSetupConfig, @@ -298,6 +304,65 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { expect(mockTrack).not.toHaveBeenCalledWith('agent_prompt_sent', expect.anything()) }) + it('surfaces the not-sent toast when a follow-up prompt is dropped', async () => { + // Foreground never becomes a recognized agent and there is no live child, + // so the readiness wait times out and the prompt is not delivered. + mockInspectRuntimeTerminalProcess.mockResolvedValue({ + foregroundProcess: 'zsh', + hasChildProcesses: false + }) + + await ensureAgentStartupInTerminal({ + worktreeId: 'wt-1', + startup: { + agent: 'aider', + launchCommand: 'aider', + expectedProcess: 'aider', + followupPrompt: 'fix the spinner', + launchConfig: { agentArgs: '', agentEnv: {} } + } + }) + + expect(mockSendRuntimePtyInputVerified).not.toHaveBeenCalled() + expect(mockShowAutomationPromptNotSentToast).toHaveBeenCalledWith('aider') + }) + + it('does not toast when a follow-up prompt is delivered', async () => { + await ensureAgentStartupInTerminal({ + worktreeId: 'wt-1', + startup: { + agent: 'aider', + launchCommand: 'aider', + expectedProcess: 'aider', + followupPrompt: 'fix the spinner', + launchConfig: { agentArgs: '', agentEnv: {} } + } + }) + + expect(mockShowAutomationPromptNotSentToast).not.toHaveBeenCalled() + }) + + it('passes an onTimeout that surfaces the not-sent toast to the draft paste path', async () => { + await ensureAgentStartupInTerminal({ + worktreeId: 'wt-1', + startup: { + agent: 'claude', + launchCommand: 'claude', + expectedProcess: 'claude', + followupPrompt: null, + launchConfig: { agentArgs: '', agentEnv: {} }, + draftPrompt: 'review this before sending' + } + }) + + const call = mockPasteDraftToAgentPtyWhenReady.mock.calls.at(-1)?.[0] as + | { onTimeout?: () => void } + | undefined + expect(call?.onTimeout).toBeTypeOf('function') + call?.onTimeout?.() + expect(mockShowAutomationPromptNotSentToast).toHaveBeenCalledWith('claude') + }) + it('does not track when follow-up prompt delivery rejects', async () => { mockSendRuntimePtyInputVerified.mockRejectedValue(new Error('runtime timeout')) @@ -335,7 +400,8 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { ptyId: 'pty-1', content: 'review this before sending', agent: 'claude', - forcePaste: true + forcePaste: true, + onTimeout: expect.any(Function) }) expect(mockTrack).not.toHaveBeenCalledWith('agent_prompt_sent', expect.anything()) }) @@ -378,7 +444,8 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { ptyId: 'agent-pty', content: 'Linear context draft', agent: 'codex', - forcePaste: true + forcePaste: true, + onTimeout: expect.any(Function) }) }) @@ -434,7 +501,8 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { ptyId: 'pty-delayed', content: 'https://github.com/stablyai/orca/pull/2051', agent: 'codex', - forcePaste: true + forcePaste: true, + onTimeout: expect.any(Function) }) }) @@ -515,7 +583,8 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { ptyId: 'startup-pty', content: 'linked draft', agent: 'codex', - forcePaste: true + forcePaste: true, + onTimeout: expect.any(Function) }) }) diff --git a/src/renderer/src/lib/new-workspace.ts b/src/renderer/src/lib/new-workspace.ts index 149c0e7708f..0a7505762d0 100644 --- a/src/renderer/src/lib/new-workspace.ts +++ b/src/renderer/src/lib/new-workspace.ts @@ -4,6 +4,7 @@ import { pasteDraftToAgentPtyWhenReady } from '@/lib/agent-paste-draft' import { sendFollowupPromptWhenAgentReady } from '@/lib/agent-followup-delivery' +import { showAutomationPromptNotSentToast } from '@/lib/agent-background-session-timeout-toast' import type { AgentStartupPlan } from '@/lib/tui-agent-startup' import type { LinkedWorkItemContext } from '@/lib/linked-work-item-context' import { @@ -302,12 +303,17 @@ async function deliverAgentStartupToTerminal( // (aider, goose, etc.) that need their initial prompt typed into the live // session and submitted. Wait until the agent owns the PTY before writing. if (startup.followupPrompt) { - await sendFollowupPromptWhenAgentReady({ + const delivered = await sendFollowupPromptWhenAgentReady({ ptyId, expectedProcess: startup.expectedProcess, prompt: startup.followupPrompt, settings: runtimeSettings }) + // Why: a dropped follow-up is otherwise silent — surface the same toast the + // draft path uses so the user knows to open the workspace and paste it. + if (!delivered) { + showAutomationPromptNotSentToast(startup.agent) + } } // Why: draftPrompt uses bracketed-paste so the URL lands atomically in the @@ -321,7 +327,9 @@ async function deliverAgentStartupToTerminal( agent: startup.agent, // Why: startup.draftPrompt is only attached after native draft launch // planning is unavailable, so this paste is the first delivery attempt. - forcePaste: true + forcePaste: true, + // Why: surface a dropped draft instead of silently losing it. + onTimeout: () => showAutomationPromptNotSentToast(startup.agent) }) } }