diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index e54020b99c2..6f89ef6c6a5 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -3242,6 +3242,29 @@ describe("TaskChatThread runtime transcript selection", () => { expect(container.textContent).not.toContain( "Waiting for user answers on the interview question card.", ); + + // 4. Answered: the card leaves pending and the turn becomes an accepted + // response wake. The authored reply must keep its slot, not be replaced + // by the wait summary. + renderThread({ + comments: [], + onAdd: async () => {}, + issueStatus: "in_progress", + interactions: [ + { + ...question, + status: "answered", + resolvedAt: new Date("2026-08-25T18:01:00.000Z"), + updatedAt: new Date("2026-08-25T18:01:00.000Z"), + } as IssueThreadInteraction, + ], + activeRun: undefined, + linkedRuns: [linkedRun("succeeded")], + }); + expect(container.textContent).toContain("I've posted a question card"); + expect(container.textContent).not.toContain( + "Waiting for user answers on the interview question card.", + ); }); it("keeps a yielded question turn out of the final-response slot", () => { diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index b6f6148d49d..603ee5d962a 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -1716,14 +1716,17 @@ export function TaskChatThread(props: TaskChatThreadProps) { agentName: meta?.agentName, running: false, }); - const sourceHasPendingAttention = (interactions ?? []).some( + // A run that posted a card yielded to that card. Its wake summary + // describes the wait, not an answer, so it never becomes the turn's + // reply, even after the card is answered. + const sourceHasAttentionCard = (interactions ?? []).some( (interaction) => interaction.sourceRunId === source.id && - interaction.status === "pending", + !isSuppressedThreadInteraction(interaction), ); const sourceAcceptedResponseWake = source.status === "succeeded" && - !sourceHasPendingAttention && + !sourceHasAttentionCard && paperclipRunnerAcceptedResponseWake(parsedSource, source.id); const sourceYielded = (acceptedSemanticResultDisposition(meta?.resultJson) === "yielded" || diff --git a/ui/src/components/task-chat/transcript-adapter.test.ts b/ui/src/components/task-chat/transcript-adapter.test.ts index eeec30b38a4..cd8019de60b 100644 --- a/ui/src/components/task-chat/transcript-adapter.test.ts +++ b/ui/src/components/task-chat/transcript-adapter.test.ts @@ -1237,6 +1237,14 @@ describe("buildTurnTimelineRows (DOT-217)", () => { } satisfies TaskChatItem; expect(paperclipRunnerFinalResponse([reply, result])).toBe(reply); expect(paperclipRunnerFinalResponse([reply], { yielded: true })).toBe(reply); + // A provider may echo the wait stub after its authored reply. + expect( + paperclipRunnerFinalResponse([ + reply, + { ...reply, id: "stub", text: result.summary }, + result, + ]), + ).toBe(reply); expect( paperclipRunnerFinalResponse( [{ ...reply, text: "Waiting for Which tools should the storefront use?." }], diff --git a/ui/src/components/task-chat/transcript-adapter.ts b/ui/src/components/task-chat/transcript-adapter.ts index 6dbd57519ff..566ce94fe2d 100644 --- a/ui/src/components/task-chat/transcript-adapter.ts +++ b/ui/src/components/task-chat/transcript-adapter.ts @@ -1483,7 +1483,8 @@ export function paperclipRunnerFinalResponse( // The wait itself is not an answer, but prose the agent wrote for the user // before yielding (e.g. explaining the question card it just posted) is. // It streamed into view while the turn was live; dropping it on settle - // makes the conversation flash and lose that reply. + // makes the conversation flash and lose that reply. Wait stubs are + // skipped, not terminal: a provider may echo one after its reply. const waitSummaries = [ options?.waitSummary, ...yieldedResults.map((result) => result.summary), @@ -1491,7 +1492,7 @@ export function paperclipRunnerFinalResponse( for (let index = parsed.length - 1; index >= 0; index -= 1) { const item = parsed[index]; if (item.kind !== "message" || item.channel !== "final") continue; - if (isYieldedWaitStub(item.text, waitSummaries)) return undefined; + if (isYieldedWaitStub(item.text, waitSummaries)) continue; return item; } return undefined;