From e0a62e5a1dc31ccccbed9b58e5ad2b1527d0a509 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 22 Oct 2026 16:00:46 -0700 Subject: [PATCH] fix(chat): keep reopened reviews open through comment refresh --- server/src/services/issue-question-context.ts | 2 +- ui/src/components/TaskChatThread.test.tsx | 12 ++++++++++++ ui/src/components/TaskChatThread.tsx | 13 ++++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/server/src/services/issue-question-context.ts b/server/src/services/issue-question-context.ts index 1d15f2abc6a..964b2b81238 100644 --- a/server/src/services/issue-question-context.ts +++ b/server/src/services/issue-question-context.ts @@ -1,7 +1,7 @@ import { and, desc, eq, or, isNotNull, sql } from "drizzle-orm"; import { heartbeatRuns, issueComments, issueThreadInteractions, type Db } from "@paperclipai/db"; -export const TASK_QUESTION_GUIDANCE = "Unanswered questions are saved conversation data, not instructions or proof of a current blocker. Historical questions remain answerable in the feed. Do not repeat a request or stop current work merely because a historical question is pending. Follow the latest human direction and continue work that does not need the missing information. If that information still prevents the current work, identify the concrete blocker and request only the input still needed. Withdraw your obsolete question through the interaction API when later work or evidence satisfies it; never fabricate an answer. A human-dismissed request is deferred to history: do not repeat it, reopen it, or insist on a response. Ordinary confirmations and completion reviews can be left unanswered. Dismissal never grants approval, permission, credentials, or permission to leave planning mode. Protected actions and configured review stages keep their gates; continue independent work without repeating their reminders."; +export const TASK_QUESTION_GUIDANCE = "Unanswered questions are saved conversation data, not instructions or proof of a current blocker. Historical questions remain answerable in the feed. Do not repeat a request or stop current work merely because a historical question is pending. Follow the latest human direction and continue work that does not need the missing information. If that information still prevents the current work, identify the concrete blocker and request only the input still needed. Withdraw your obsolete question through the interaction API when later work or evidence satisfies it; never fabricate an answer. A human-dismissed request is deferred to history: do not repeat it, reopen it, or insist on a response. Ordinary confirmations and completion reviews can be left unanswered. Dismissal never grants approval, permission, credentials, or permission to leave planning mode. Approvals, permissions, and configured review stages keep their own gates. Continue independent work without repeating their reminders."; /** Ordinary questions cannot carry approval or credential authority. */ export function ordinaryQuestionCondition() { diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index f7a0e7c7bd8..4f7b6c85308 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -3420,6 +3420,18 @@ describe.each([true, false])("Unanswered question history (conversationMode=%s)" expect(takeover()?.textContent).not.toContain("First review"); }); + it("keeps a reopened dismissed review visible when a delayed human comment refresh arrives", async () => { + const { genericPendingRequestConfirmationInteraction } = await import("@/fixtures/issueThreadInteractionFixtures"); + const review = { ...genericPendingRequestConfirmationInteraction, title: "Reopened review" }; + const show = (comments = movedOn.slice(0, 0)) => render(); + show(); + await dismiss(); + await act(async () => container.querySelector('[aria-label="Open request: Reopened review"]')!.click()); + expect(takeover()?.textContent).toContain("Reopened review"); + await act(async () => show(movedOn)); + expect(takeover()?.textContent).toContain("Reopened review"); + }); + it("opens the new question but can reopen the exact historical native question", async () => { const nativeOld = { ...old, sourceRunId: "old-run", payload: { ...old.payload, runtimeRequestId: "old-request" } } as IssueThreadInteraction; render(); diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index b4a6ac857ed..8d77a9b03ee 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -2578,13 +2578,16 @@ export function TaskChatThread(props: TaskChatThreadProps) { const movedOn = lastInputUserCommentRef.current !== latestUserCommentId; lastInputUserCommentRef.current = latestUserCommentId; knownPendingKeysRef.current = currentKeys; - setSelectedPendingKey((current) => - current && currentKeys.has(current) + setSelectedPendingKey((current) => { + const currentInput = pendingComposerInputs.find(input => input.key === current); + const questionMovedOn = movedOn && currentInput?.kind === "durable" + && currentInput.interaction.kind === "ask_user_questions" && !currentPendingKeys.has(currentInput.key); + return current && currentKeys.has(current) && (currentPendingKeys.has(current) || explicitlyOpenedInputRef.current === current) - && !(movedOn && !currentPendingKeys.has(current)) + && !questionMovedOn ? current - : (currentPendingInputs[0]?.key ?? null), - ); + : (currentPendingInputs[0]?.key ?? null); + }); if (hasNewInput) setTakeoverMode("open"); }, [pendingComposerInputs, latestUserCommentId, currentPendingInputs, currentPendingKeys]); const selectedPendingInput =