From 4f8876b9419b2f99fec550ac65754e2eea2c7a28 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 30 Jun 2026 00:20:17 -0700 Subject: [PATCH 1/2] keep steer echo from clearing pending messages --- packages/core/src/sessions/sessionService.ts | 7 +- .../sessions/sessionServiceHost.test.ts | 106 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index 4c34ac180..7baa7e76c 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -1634,7 +1634,12 @@ export class SessionService { }); } - if (isUserPromptEcho) { + // A steer rides on `session/prompt` but has no optimistic placeholder of its + // own (sendSteerPrompt skips applyOptimisticPrompt). Replacing here would + // wipe *other* in-flight messages' placeholders (e.g. a follow-up sent + // moments later), making them vanish until their own echo lands. Append it + // instead so it renders without disturbing pending placeholders. + if (isUserPromptEcho && !this.isSteerMessage(acpMsg.message)) { this.d.store.replaceOptimisticWithEvent(taskRunId, acpMsg); } else { this.d.store.appendEvents(taskRunId, [acpMsg]); diff --git a/packages/ui/src/features/sessions/sessionServiceHost.test.ts b/packages/ui/src/features/sessions/sessionServiceHost.test.ts index 0ef8a284d..06e7611aa 100644 --- a/packages/ui/src/features/sessions/sessionServiceHost.test.ts +++ b/packages/ui/src/features/sessions/sessionServiceHost.test.ts @@ -4581,6 +4581,112 @@ describe("SessionService", () => { }); }); + describe("steer echo routing", () => { + async function connectAndCaptureOnData(): Promise< + (payload: unknown) => void + > { + const service = getSessionService(); + + let session: AgentSession | undefined; + mockSessionStoreSetters.getSessionByTaskId.mockImplementation( + () => session, + ); + mockSessionStoreSetters.getSessions.mockImplementation(() => + session ? { "run-123": session } : {}, + ); + mockSessionStoreSetters.updateSession.mockImplementation( + (_taskRunId, updates) => { + if (session) session = { ...session, ...updates }; + }, + ); + mockSessionStoreSetters.setSession.mockImplementation((next) => { + session = next as AgentSession; + }); + + mockBuildAuthenticatedClient.mockReturnValue({ + ...mockAuthenticatedClient, + createTaskRun: vi.fn().mockResolvedValue({ id: "run-123" }), + appendTaskRunLog: vi.fn(), + }); + mockTrpcAgent.start.mutate.mockResolvedValue({ + channel: "agent-event:run-123", + configOptions: [], + }); + + await service.connectToTask({ + task: createMockTask(), + repoPath: "/repo", + }); + + session = createMockSession({ + taskRunId: "run-123", + taskId: "task-123", + status: "connected", + isCloud: false, + adapter: "claude", + currentPromptId: 42, + isPromptPending: true, + }); + + const onData = mockTrpcAgent.onSessionEvent.subscribe.mock.calls.at( + -1, + )?.[1]?.onData as ((payload: unknown) => void) | undefined; + expect(onData).toBeDefined(); + return onData as (payload: unknown) => void; + } + + it("appends a steer echo without clearing pending optimistic placeholders", async () => { + const onData = await connectAndCaptureOnData(); + mockSessionStoreSetters.appendEvents.mockClear(); + mockSessionStoreSetters.replaceOptimisticWithEvent.mockClear(); + + const steerEcho = { + type: "acp_message", + ts: 1700000001, + message: { + jsonrpc: "2.0", + id: 101, + method: "session/prompt", + params: { + prompt: [{ type: "text", text: "steer me" }], + _meta: { steer: true }, + }, + }, + }; + onData(steerEcho); + + expect(mockSessionStoreSetters.appendEvents).toHaveBeenCalledWith( + "run-123", + [steerEcho], + ); + expect( + mockSessionStoreSetters.replaceOptimisticWithEvent, + ).not.toHaveBeenCalled(); + }); + + it("replaces the optimistic placeholder for a normal prompt echo", async () => { + const onData = await connectAndCaptureOnData(); + mockSessionStoreSetters.appendEvents.mockClear(); + mockSessionStoreSetters.replaceOptimisticWithEvent.mockClear(); + + const normalEcho = { + type: "acp_message", + ts: 1700000002, + message: { + jsonrpc: "2.0", + id: 102, + method: "session/prompt", + params: { prompt: [{ type: "text", text: "normal msg" }] }, + }, + }; + onData(normalEcho); + + expect( + mockSessionStoreSetters.replaceOptimisticWithEvent, + ).toHaveBeenCalledWith("run-123", normalEcho); + }); + }); + describe("steerQueuedMessage", () => { const queuedMessage = { id: "q-1", From b28def91c7f60c4cc58a30f7bb11308e2718982e Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 30 Jun 2026 00:29:00 -0700 Subject: [PATCH 2/2] parameterise steer echo tests, drop comment --- packages/core/src/sessions/sessionService.ts | 5 -- .../sessions/sessionServiceHost.test.ts | 62 +++++++++---------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index 7baa7e76c..5cf400034 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -1634,11 +1634,6 @@ export class SessionService { }); } - // A steer rides on `session/prompt` but has no optimistic placeholder of its - // own (sendSteerPrompt skips applyOptimisticPrompt). Replacing here would - // wipe *other* in-flight messages' placeholders (e.g. a follow-up sent - // moments later), making them vanish until their own echo lands. Append it - // instead so it renders without disturbing pending placeholders. if (isUserPromptEcho && !this.isSteerMessage(acpMsg.message)) { this.d.store.replaceOptimisticWithEvent(taskRunId, acpMsg); } else { diff --git a/packages/ui/src/features/sessions/sessionServiceHost.test.ts b/packages/ui/src/features/sessions/sessionServiceHost.test.ts index 06e7611aa..b1f0deb88 100644 --- a/packages/ui/src/features/sessions/sessionServiceHost.test.ts +++ b/packages/ui/src/features/sessions/sessionServiceHost.test.ts @@ -4635,12 +4635,21 @@ describe("SessionService", () => { return onData as (payload: unknown) => void; } - it("appends a steer echo without clearing pending optimistic placeholders", async () => { + it.each([ + { + name: "appends a steer echo without clearing pending optimistic placeholders", + steer: true, + }, + { + name: "replaces the optimistic placeholder for a normal prompt echo", + steer: false, + }, + ])("$name", async ({ steer }) => { const onData = await connectAndCaptureOnData(); mockSessionStoreSetters.appendEvents.mockClear(); mockSessionStoreSetters.replaceOptimisticWithEvent.mockClear(); - const steerEcho = { + const echo = { type: "acp_message", ts: 1700000001, message: { @@ -4648,42 +4657,27 @@ describe("SessionService", () => { id: 101, method: "session/prompt", params: { - prompt: [{ type: "text", text: "steer me" }], - _meta: { steer: true }, + prompt: [{ type: "text", text: "hello" }], + ...(steer ? { _meta: { steer: true } } : {}), }, }, }; - onData(steerEcho); - - expect(mockSessionStoreSetters.appendEvents).toHaveBeenCalledWith( - "run-123", - [steerEcho], - ); - expect( - mockSessionStoreSetters.replaceOptimisticWithEvent, - ).not.toHaveBeenCalled(); - }); - - it("replaces the optimistic placeholder for a normal prompt echo", async () => { - const onData = await connectAndCaptureOnData(); - mockSessionStoreSetters.appendEvents.mockClear(); - mockSessionStoreSetters.replaceOptimisticWithEvent.mockClear(); - - const normalEcho = { - type: "acp_message", - ts: 1700000002, - message: { - jsonrpc: "2.0", - id: 102, - method: "session/prompt", - params: { prompt: [{ type: "text", text: "normal msg" }] }, - }, - }; - onData(normalEcho); + onData(echo); - expect( - mockSessionStoreSetters.replaceOptimisticWithEvent, - ).toHaveBeenCalledWith("run-123", normalEcho); + if (steer) { + expect(mockSessionStoreSetters.appendEvents).toHaveBeenCalledWith( + "run-123", + [echo], + ); + expect( + mockSessionStoreSetters.replaceOptimisticWithEvent, + ).not.toHaveBeenCalled(); + } else { + expect( + mockSessionStoreSetters.replaceOptimisticWithEvent, + ).toHaveBeenCalledWith("run-123", echo); + expect(mockSessionStoreSetters.appendEvents).not.toHaveBeenCalled(); + } }); });