From 7bf002f8a1a4f4991971f9f008fb32ed6f5b67da Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:05:40 -0600 Subject: [PATCH 1/2] fix(web): Claude thinking shown as a reply can no longer be quoted --- .../chat/AssistantCitationSource.tsx | 3 ++ .../chat/MessagesTimeline.logic.test.ts | 15 +++++++++ .../components/chat/MessagesTimeline.logic.ts | 11 +++++++ .../components/chat/TimelineAssistantRows.tsx | 33 +++++++++++-------- .../src/lib/assistantTextSelection.test.ts | 9 +++++ apps/web/src/lib/assistantTextSelection.ts | 8 ++++- 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/chat/AssistantCitationSource.tsx b/apps/web/src/components/chat/AssistantCitationSource.tsx index cf2e950e96abd..789ce6ebe4746 100644 --- a/apps/web/src/components/chat/AssistantCitationSource.tsx +++ b/apps/web/src/components/chat/AssistantCitationSource.tsx @@ -338,6 +338,7 @@ export function AssistantCitationSource({ itemKey, request, listRef, + quotable, children, }: { messageId: MessageId; @@ -345,6 +346,7 @@ export function AssistantCitationSource({ itemKey: string; request: AssistantCitationTarget | null; listRef: RefObject; + quotable: boolean; children: ReactNode; }) { const rootRef = useRef(null); @@ -362,6 +364,7 @@ export function AssistantCitationSource({ data-assistant-citation-source={messageId} data-assistant-citation-environment={threadRef?.environmentId} data-assistant-citation-thread={threadRef?.threadId} + data-assistant-citation-quotable={quotable} > {children} diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index ba0f508ebeecd..a9cb1ef245f29 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -30,6 +30,7 @@ import { liveAnswerPreview, deriveMessagesTimelineRowsWithState, shouldCollapseUserMessage, + isQuotableAssistantMessage, resolveAssistantMessageCopyState, type MessagesTimelineRow, workEntryDisplayLabel, @@ -54,6 +55,20 @@ describe("resolveAssistantMessageCopyState", () => { }); }); +describe("isQuotableAssistantMessage", () => { + it("does not offer quotes on Claude thinking shown as a reply", () => { + const ref = (driver: string, nativeId: string) => ({ + driver: ProviderDriverKind.make(driver), + nativeId, + strength: "strong" as const, + }); + expect(isQuotableAssistantMessage(ref("claudeAgent", "msg_01:thinking:1"))).toBe(false); + expect(isQuotableAssistantMessage(ref("claudeAgent", "6b1f0c2e-uuid"))).toBe(true); + expect(isQuotableAssistantMessage(ref("codex", "msg_01:thinking:1"))).toBe(true); + expect(isQuotableAssistantMessage(null)).toBe(true); + }); +}); + describe("deriveMessagesTimelineRows", () => { it.each(["alone", "before tool", "before commentary and tool"])( "keeps an unfinished task update static %s", diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index 4536e9a250386..9624d76c0d608 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -57,6 +57,7 @@ import { type OrchestrationV2IdleCompaction, type WorktreeSetupSnapshot, type OrchestrationV2ProjectedTurnItem, + type OrchestrationV2ProviderRef, type RunAttemptId, type RuntimeRequestId, RunId, @@ -684,6 +685,16 @@ export function resolveAssistantMessageCopyState({ }; } +// Claude 5.1+ shows a thinking block before a tool call as a reply. It is still reasoning, +// which no provider lets users quote, and quoting it trips Claude's reasoning_extraction refusal. +export function isQuotableAssistantMessage( + nativeItemRef: OrchestrationV2ProviderRef | null | undefined, +) { + return !( + nativeItemRef?.driver === "claudeAgent" && /:thinking:\d+$/u.test(nativeItemRef.nativeId ?? "") + ); +} + function deriveTerminalAssistantMessageIds(timelineEntries: ReadonlyArray) { const lastAssistantMessageIdByResponseKey = new Map(); let nullTurnResponseIndex = 0; diff --git a/apps/web/src/components/chat/TimelineAssistantRows.tsx b/apps/web/src/components/chat/TimelineAssistantRows.tsx index 458d84b9e982b..68724b42bc023 100644 --- a/apps/web/src/components/chat/TimelineAssistantRows.tsx +++ b/apps/web/src/components/chat/TimelineAssistantRows.tsx @@ -15,7 +15,10 @@ import { useFileContextMenuHandler } from "../../fileContextMenu"; import { useAssistantMessageText, useProject, useThreadShell } from "../../state/entities"; import { MessageCopyButton } from "./MessageCopyButton"; import { AssistantCitationSource } from "./AssistantCitationSource"; -import { resolveAssistantMessageCopyState } from "./MessagesTimeline.logic"; +import { + isQuotableAssistantMessage, + resolveAssistantMessageCopyState, +} from "./MessagesTimeline.logic"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; import { useUiStateStore } from "~/uiStateStore"; @@ -33,6 +36,20 @@ export function AssistantTimelineRow({ row }: { row: Extract + ); return ( <> @@ -45,19 +62,9 @@ export function AssistantTimelineRow({ row }: { row: Extract - + {markdown} {ctx.onOpenTurnDiff ? ( diff --git a/apps/web/src/lib/assistantTextSelection.test.ts b/apps/web/src/lib/assistantTextSelection.test.ts index d06261cb856aa..154d543bebd9d 100644 --- a/apps/web/src/lib/assistantTextSelection.test.ts +++ b/apps/web/src/lib/assistantTextSelection.test.ts @@ -310,6 +310,15 @@ describe("captureAssistantTextSelection", () => { expect(capture(source, nativeSelection([quote, 0], [control, 1]))).toBeNull(); }); + it("does not quote a response marked unquotable", () => { + const quote = textNode("Reasoning shown as a reply."); + const source = new SelectionNode("DIV", "", { + "data-assistant-citation-source": "assistant", + "data-assistant-citation-quotable": "false", + }).append(quote); + expect(capture(source, nativeSelection([quote, 0], [quote, quote.length]))).toBeNull(); + }); + it.each([ new SelectionNode("BUTTON"), new SelectionNode("SPAN", "", { role: "button" }), diff --git a/apps/web/src/lib/assistantTextSelection.ts b/apps/web/src/lib/assistantTextSelection.ts index f1e0f2d2fc16d..1d8c9d2183c4f 100644 --- a/apps/web/src/lib/assistantTextSelection.ts +++ b/apps/web/src/lib/assistantTextSelection.ts @@ -219,7 +219,13 @@ export function captureAssistantTextSelection( const last = selectedTextBoundary(range, range.commonAncestorContainer, true); if (first === null || last === null) return null; const source = first.parentElement?.closest("[data-assistant-citation-source]"); - if (!source || !viewport.contains(source)) return null; + if ( + !source || + !viewport.contains(source) || + source.matches("[data-assistant-citation-quotable=false]") + ) { + return null; + } // Paragraph selection can end at the next block's offset 0 or a parent // boundary. Validate the text actually selected, not that empty endpoint. From 8bf8a30f4e281d4be555eba60530747a5e715daf Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 10:56:10 +0000 Subject: [PATCH 2/2] fix(web): hide Reply to this response on Claude thinking shown as a reply Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/chat/TimelineAssistantRows.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/TimelineAssistantRows.tsx b/apps/web/src/components/chat/TimelineAssistantRows.tsx index 7c0df61ecebc7..fd7fabd04350e 100644 --- a/apps/web/src/components/chat/TimelineAssistantRows.tsx +++ b/apps/web/src/components/chat/TimelineAssistantRows.tsx @@ -237,7 +237,9 @@ function AssistantMessageMeta({ {projectedItem?.item.type === "assistant_message" ? ( ) : null} - {!message.streaming && projectedItem ? ( + {!message.streaming && + projectedItem && + isQuotableAssistantMessage(projectedItem.item.nativeItemRef) ? ( ) : null} {!message.streaming && (