From e13e5e8b7d2e357592a539dffd04a83c69a7b38d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:07:42 -0700 Subject: [PATCH] fix(orchestration): submit mail pointers to Cursor CLI instead of leaving them in the composer (#33347) Orchestration mailbox pointers now get their delayed Enter for Cursor too (the Cursor no-Enter skip from #3156 left mail delivered but never acted on). Verified with real cursor-agent. Fixes #19760. Supersedes #19761. Co-authored-by: Maple <54388301+zleo-ai@users.noreply.github.com> --- .../construction/runtime-service-state.ts | 1 - ...erminal-output-and-worker-recovery.spec.ts | 48 +++---------------- .../mailbox-pointer-delivery-contract.ts | 1 - .../mailbox-pointer-stage.test.ts | 26 +++++++++- .../orchestration/mailbox-pointer-stage.ts | 11 ----- .../helpers/orchestration-mail-pane-agent.ts | 2 +- .../orchestration-idle-mail-delivery.spec.ts | 11 ++--- 7 files changed, 36 insertions(+), 64 deletions(-) diff --git a/src/main/runtime/construction/runtime-service-state.ts b/src/main/runtime/construction/runtime-service-state.ts index bdc502dfd16..bf88b295f79 100644 --- a/src/main/runtime/construction/runtime-service-state.ts +++ b/src/main/runtime/construction/runtime-service-state.ts @@ -246,7 +246,6 @@ export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWith isAgentSettledForDelivery: (leaf) => this.checkDeliverySettledAndArmRecheck(leaf), isPtyAwaitingUserInput: (ptyId) => this.isPtyAwaitingUserInput(ptyId), getMessageWaiters: (mailboxHandle) => this.messageWaiters.get(mailboxHandle), - getTabTitle: (tabId) => this.tabs.get(tabId)?.title, getCliCommand: (terminalHandle) => this.getTerminalOrchestrationCliCommand(terminalHandle), getTerminalHandleForLeafKey: (leafKey) => this.handleByLeafKey.get(leafKey), resolveSubmitTarget: (leaf, ptyId) => this.resolveOrchestrationPointerSubmitTarget(leaf, ptyId), diff --git a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery.spec.ts index 2bb9fcb786b..d09d6d3239e 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery.spec.ts @@ -579,7 +579,7 @@ describe('OrcaRuntimeService', () => { } }) - it('injects pending orchestration messages into Cursor Agent without auto-submitting', async () => { + it('submits the mail pointer to Cursor Agent only after the paste-burst delay', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) @@ -606,11 +606,12 @@ describe('OrcaRuntimeService', () => { 'pty-1', expect.stringContaining('You have 1 orchestration message') ) - await vi.advanceTimersByTimeAsync(500) - const submitWrites = write.mock.calls.filter( - ([ptyId, text]) => ptyId === 'pty-1' && text === '\r' - ) - expect(submitWrites).toHaveLength(0) + const isSubmit = ([ptyId, text]: unknown[]): boolean => ptyId === 'pty-1' && text === '\r' + // Why: Cursor reads Enter within a 35ms keystroke burst as a newline, so it must trail the pointer. + await vi.advanceTimersByTimeAsync(100) + expect(write.mock.calls.filter(isSubmit)).toHaveLength(0) + await vi.advanceTimersByTimeAsync(400) + expect(write.mock.calls.filter(isSubmit)).toHaveLength(1) const unread = db.getUnreadMessages(mailbox) expect(unread).toHaveLength(1) @@ -622,41 +623,6 @@ describe('OrcaRuntimeService', () => { } }) - it('still auto-submits to a non-Cursor agent when its idle title mentions Cursor Agent', async () => { - vi.useFakeTimers() - try { - const runtime = new OrcaRuntimeService(store) - const db = new InMemoryOrchestrationMessages() - const write = vi.fn().mockReturnValue(true) - setInMemoryOrchestrationMessages(runtime, db) - runtime.setPtyController({ - write, - writeWithSettlement: settledWriteStub(write), - kill: vi.fn(), - getForegroundProcess: async () => null - }) - syncSinglePty(runtime, 'pty-1', { tabTitle: 'cursor-repro-branch' }) - - const [terminal] = (await runtime.listTerminals()).terminals - bindSinglePtyRun(db, terminal.handle) - runtime.onPtyData('pty-1', '\x1b]0;. Investigate Cursor Agent\x07', 100) - runtime.onPtyData('pty-1', '\x1b]0;* Investigate Cursor Agent\x07', 101) - db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello claude' }) - - runtime.deliverPendingMessagesForHandle(terminal.handle) - await vi.advanceTimersByTimeAsync(500) - - expect(write).toHaveBeenCalledWith( - 'pty-1', - expect.stringContaining('You have 1 orchestration message') - ) - expect(write).toHaveBeenCalledWith('pty-1', '\r') - db.close() - } finally { - vi.useRealTimers() - } - }) - it('does not replay an already-delivered message on a later idle transition', async () => { vi.useFakeTimers() try { diff --git a/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts b/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts index e59f84689a7..9716c33cfb3 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts @@ -26,7 +26,6 @@ export type PointerDeliveryDependencies boolean isPtyAwaitingUserInput: (ptyId: string) => boolean getMessageWaiters: (mailboxHandle: string) => ReadonlySet | undefined - getTabTitle: (tabId: string) => string | null | undefined getCliCommand: (terminalHandle: string) => OrchestrationCliCommand getTerminalHandleForLeafKey: (leafKey: string) => string | undefined resolveSubmitTarget: ( diff --git a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts index 4e24e4403dd..d1018387d49 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts @@ -19,7 +19,10 @@ const LEAF = { lastOscTitle: null } -function pointerDeps(db: OrchestrationDb, writePty: () => WriteSettlement) { +function pointerDeps( + db: OrchestrationDb, + writePty: (ptyId: string, data: string) => WriteSettlement +) { return { mailboxOwner: { resolve: () => 'run:run-1' }, deliveryTarget: { resolveTerminalHandle: () => 'term-1', deferForAbsenceProbe: () => false }, @@ -30,7 +33,6 @@ function pointerDeps(db: OrchestrationDb, writePty: () => WriteSettlement) { // These cases exercise staging and Enter phases, not the idle gate; the pane is settled. isAgentSettledForDelivery: () => true, getMessageWaiters: () => undefined, - getTabTitle: () => null, getCliCommand: () => 'orca' as const, getTerminalHandleForLeafKey: () => 'term-1', resolveSubmitTarget: () => ({ @@ -204,6 +206,26 @@ describe('mailbox pointer staging watermark', () => { expect(writePty.mock.calls.length).toBeGreaterThan(0) db.close() }) + + // Why: a pointer left unsubmitted in Cursor's composer stacks up and rides along with the user's next prompt. + it('submits the pointer with Enter for a Cursor Agent pane', async () => { + const db = new OrchestrationDb(':memory:') + const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'mail' }) + const writePty = vi.fn((_ptyId: string, _data: string) => WRITE_ACCEPTED) + const state = new OrchestrationMailboxPointerState() + const cursorLeaf = { ...LEAF, lastOscTitle: 'Cursor Agent' } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies only the deps the staging path reads. + stageOrchestrationMailboxPointer({ + ...stageArgs(db, state), + deps: pointerDeps(db, writePty), + leaf: cursorLeaf, + messages: [{ id: message.id, type: 'status', sequence: 1 }] + } as never) + + await vi.waitFor(() => expect(writePty).toHaveBeenLastCalledWith('pty-1', '\r')) + expect(writePty.mock.calls[0]?.[1]).toContain('You have 1 orchestration message') + db.close() + }) }) describe('retiring a pty mid-delivery', () => { diff --git a/src/main/runtime/orchestration/mailbox-pointer-stage.ts b/src/main/runtime/orchestration/mailbox-pointer-stage.ts index b748d47ed6b..67872a323b6 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-stage.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-stage.ts @@ -1,4 +1,3 @@ -import { isCursorAgentTitle } from '../../../shared/agent-detection' import { formatMessagePointer } from './formatter' import type { OrchestrationMailboxPointerMessage, @@ -147,16 +146,6 @@ function finishPointerWriteAndStageEnter submitOrchestrationMailboxPointer( { diff --git a/tests/e2e/helpers/orchestration-mail-pane-agent.ts b/tests/e2e/helpers/orchestration-mail-pane-agent.ts index d84cd8367dd..534b772ea2f 100644 --- a/tests/e2e/helpers/orchestration-mail-pane-agent.ts +++ b/tests/e2e/helpers/orchestration-mail-pane-agent.ts @@ -28,7 +28,7 @@ import path from 'node:path' /** `detectAgentStatusFromTitle` reads these as agent-name + strong keyword. */ export const CODEX_IDLE_TITLE = 'Codex done' export const CODEX_WORKING_TITLE = 'Codex working' -/** Also satisfies `isCursorAgentTitle`, which suppresses the synthesized Enter. */ +/** Cursor Agent idle title (`isCursorAgentTitle`). */ export const CURSOR_IDLE_TITLE = 'Cursor Ready' export type AgentLedgerEntry = { diff --git a/tests/e2e/orchestration-idle-mail-delivery.spec.ts b/tests/e2e/orchestration-idle-mail-delivery.spec.ts index d0b0fc17b75..0831ea94afc 100644 --- a/tests/e2e/orchestration-idle-mail-delivery.spec.ts +++ b/tests/e2e/orchestration-idle-mail-delivery.spec.ts @@ -627,25 +627,22 @@ test.describe('orchestration push-on-idle mail delivery', () => { await expectSubmitted(pane) }) - test('writes the pointer but never Enter for a Cursor agent pane', async ({ + test('writes and submits the pointer for a Cursor agent pane', async ({ orcaPage, electronApp }) => { test.setTimeout(180_000) const { client, openAgentPane } = await setUpMailFixture(orcaPage, electronApp) const pane = await openAgentPane() - // Cursor treats injected PTY text as editable prompt content, so submitting - // has to stay under user control there too. pane.agent.setTitle(CURSOR_IDLE_TITLE) await waitForObservedTitle(client, pane.handle, CURSOR_IDLE_TITLE) - const mailbox = await createRunMailbox(client, pane, 'Cursor no-submit') + const mailbox = await createRunMailbox(client, pane, 'Cursor pointer submit') - const subject = 'Cursor no-submit' + const subject = 'Cursor pointer submit' await sendMail(client, mailbox, { subject }) await expectPointed(pane) - await orcaPage.waitForTimeout(2_000) - expectNotSubmitted(pane) + await expectSubmitted(pane) }) })