updateFade(event.currentTarget)}
- className={cn(
- "max-h-64 min-h-0 space-y-3 self-stretch overflow-y-auto whitespace-pre-wrap wrap-break-word",
- getVirtualizedScrollFadeClassName(fade),
- )}
- >
-
- {citation.text}
-
- {citation.comment ?
{citation.comment}
: null}
-
- );
-}
diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts
index 4536e9a250386..ab6638ac52936 100644
--- a/apps/web/src/components/chat/MessagesTimeline.logic.ts
+++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts
@@ -3,10 +3,7 @@ import { resolveUserMessagePresentation } from "@t3tools/client-runtime/user-mes
import { isLiveSubagentTurnItem } from "@t3tools/client-runtime/state/subagentRuntime";
import * as DateTime from "effect/DateTime";
import * as Equal from "effect/Equal";
-import {
- assistantCitationLabel,
- collectAssistantCitations,
-} from "@t3tools/shared/assistantCitations";
+import { collectAssistantCitations } from "@t3tools/shared/assistantCitations";
import { shallow } from "zustand/vanilla/shallow";
import { claudeResumeCompactionAge } from "@t3tools/shared/claudeCompaction";
import {
@@ -2654,7 +2651,9 @@ function visibleUserMessageText(text: string): string {
let visible = "";
let cursor = 0;
for (const match of collectAssistantCitations(text)) {
- visible += linkLabels(text.slice(cursor, match.start)) + assistantCitationLabel(match.citation);
+ // A quote shows as a two-line card with its comment under it.
+ visible += linkLabels(text.slice(cursor, match.start));
+ visible += "\n\n" + (match.citation.comment ?? "") + "\n";
cursor = match.end;
}
return visible + linkLabels(text.slice(cursor));
diff --git a/packages/shared/src/threadFindText.test.ts b/packages/shared/src/threadFindText.test.ts
index 77a0874c21336..2976559316a45 100644
--- a/packages/shared/src/threadFindText.test.ts
+++ b/packages/shared/src/threadFindText.test.ts
@@ -96,7 +96,7 @@ it("indexes nested disclosure summaries and bodies in rendered order", () => {
).toEqual(["Outer", "first", "Inner", "second"]);
});
-it("indexes the citation chip label instead of its link text", () => {
+it("indexes a citation by its comment, not its quote or link text", () => {
const citation = {
version: 1 as const,
environmentId: EnvironmentId.make("environment"),
@@ -110,9 +110,9 @@ it("indexes the citation chip label instead of its link text", () => {
};
const text = `Please fix ${serializeAssistantCitation(citation)} thanks`;
expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([
- "Please fix cited needle thanks",
+ "Please fix thanks",
]);
- const commented = serializeAssistantCitation({ ...citation, comment: "my note" });
+ const commented = serializeAssistantCitation({ ...citation, comment: "my\nnote" });
expect(searchableMessageSegments({ role: "user", text: commented, streaming: false })).toEqual([
"my note",
]);
diff --git a/packages/shared/src/threadFindText.ts b/packages/shared/src/threadFindText.ts
index 3afec6e04729e..fd315a293cca4 100644
--- a/packages/shared/src/threadFindText.ts
+++ b/packages/shared/src/threadFindText.ts
@@ -25,7 +25,7 @@ import {
isAttachmentOnlyBootstrapPrompt,
parseComposerContextHref,
} from "./composerContextReferences.ts";
-import { assistantCitationLabel, parseAssistantCitationHref } from "./assistantCitations.ts";
+import { parseAssistantCitationHref } from "./assistantCitations.ts";
import type { OrchestrationV2ConversationMessage } from "@t3tools/contracts";
import { proposedPlanTitle, stripDisplayedPlanMarkdown } from "./proposedPlanText.ts";
import { unified } from "unified";
@@ -141,7 +141,8 @@ function markdownThreadFindText(
const citation =
node.tagName === "a" && typeof href === "string" ? parseAssistantCitationHref(href) : null;
if (citation) {
- text += assistantCitationLabel(citation);
+ // The quote card is not searched; its text is found in the message it came from.
+ text += (citation.comment ?? "").replace(/\r?\n/g, " ");
return;
}
{
From 4841735ee33de689c3a5978ca4ba2fac9730e9a2 Mon Sep 17 00:00:00 2001
From: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
Date: Wed, 11 Nov 2026 05:14:35 +0000
Subject: [PATCH 2/3] fix(web): find and collapse count a quote card the way it
renders
Find indexed the text before a quote together with the quote's comment, so a query spanning both counted a match it could not highlight. A message made only of uncommented quotes had no visible text and never collapsed.
---
.../components/chat/MessagesTimeline.logic.test.ts | 13 ++++++++-----
.../src/components/chat/MessagesTimeline.logic.ts | 4 ++--
packages/shared/src/threadFindText.test.ts | 9 +++++----
packages/shared/src/threadFindText.ts | 3 ++-
4 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts
index ba0f508ebeecd..8152deef89f42 100644
--- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts
+++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts
@@ -4538,22 +4538,25 @@ describe("subagent continued by a later turn", () => {
});
describe("shouldCollapseUserMessage", () => {
- it("measures a quote chip by its label, not its encoded link", () => {
+ it("measures a quote by its card, not its encoded link", () => {
const quote = "A long assistant paragraph that the user quoted. ".repeat(40);
- const citation = serializeAssistantCitation({
- version: 1,
+ const source = {
+ version: 1 as const,
environmentId: EnvironmentId.make("environment"),
threadId: ThreadId.make("thread"),
messageId: MessageId.make("source"),
text: quote,
- comment: "Why does this matter?",
start: 0,
end: quote.length,
prefix: "",
suffix: "",
- });
+ };
+ const citation = serializeAssistantCitation({ ...source, comment: "Why does this matter?" });
+ const bare = serializeAssistantCitation(source);
expect(shouldCollapseUserMessage(`${citation} Can you expand on this?`)).toBe(false);
expect(shouldCollapseUserMessage(`${citation} ${"More text. ".repeat(60)}`)).toBe(true);
+ expect(shouldCollapseUserMessage(bare.repeat(2))).toBe(false);
+ expect(shouldCollapseUserMessage(bare.repeat(10))).toBe(true);
});
});
diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts
index ab6638ac52936..cd3e4eeec5c9f 100644
--- a/apps/web/src/components/chat/MessagesTimeline.logic.ts
+++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts
@@ -2651,9 +2651,9 @@ function visibleUserMessageText(text: string): string {
let visible = "";
let cursor = 0;
for (const match of collectAssistantCitations(text)) {
- // A quote shows as a two-line card with its comment under it.
+ // A quote shows as a two-line card, counted here as a mark, with its comment under it.
visible += linkLabels(text.slice(cursor, match.start));
- visible += "\n\n" + (match.citation.comment ?? "") + "\n";
+ visible += "\n\u275D\n" + (match.citation.comment ?? "") + "\n";
cursor = match.end;
}
return visible + linkLabels(text.slice(cursor));
diff --git a/packages/shared/src/threadFindText.test.ts b/packages/shared/src/threadFindText.test.ts
index 2976559316a45..ea3604c4e1610 100644
--- a/packages/shared/src/threadFindText.test.ts
+++ b/packages/shared/src/threadFindText.test.ts
@@ -110,10 +110,11 @@ it("indexes a citation by its comment, not its quote or link text", () => {
};
const text = `Please fix ${serializeAssistantCitation(citation)} thanks`;
expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([
- "Please fix thanks",
+ "Please fix ",
+ " thanks",
]);
const commented = serializeAssistantCitation({ ...citation, comment: "my\nnote" });
- expect(searchableMessageSegments({ role: "user", text: commented, streaming: false })).toEqual([
- "my note",
- ]);
+ expect(
+ searchableMessageSegments({ role: "user", text: `fix ${commented} thanks`, streaming: false }),
+ ).toEqual(["fix ", "my note thanks"]);
});
diff --git a/packages/shared/src/threadFindText.ts b/packages/shared/src/threadFindText.ts
index fd315a293cca4..8d235e161e94d 100644
--- a/packages/shared/src/threadFindText.ts
+++ b/packages/shared/src/threadFindText.ts
@@ -141,7 +141,8 @@ function markdownThreadFindText(
const citation =
node.tagName === "a" && typeof href === "string" ? parseAssistantCitationHref(href) : null;
if (citation) {
- // The quote card is not searched; its text is found in the message it came from.
+ // The quote card is not searched, and it splits the text before it from its comment.
+ flush();
text += (citation.comment ?? "").replace(/\r?\n/g, " ");
return;
}
From 2589f5edf33a0aafad5c8e6849af6df3610c9e42 Mon Sep 17 00:00:00 2001
From: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
Date: Wed, 11 Nov 2026 05:20:08 +0000
Subject: [PATCH 3/3] fix(web): copying a selection inside a quote card keeps
the quote link
A selection that covered only a card and its comment cloned neither the card's copy boundary nor a link that carried one, so the copy lost the quote's source.
---
apps/web/src/components/chat/AssistantCitationChip.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/components/chat/AssistantCitationChip.tsx b/apps/web/src/components/chat/AssistantCitationChip.tsx
index 4cc400bea6fdb..803779b995d52 100644
--- a/apps/web/src/components/chat/AssistantCitationChip.tsx
+++ b/apps/web/src/components/chat/AssistantCitationChip.tsx
@@ -65,15 +65,17 @@ export function AssistantCitationEmbed({
const wholeResponse = citation.prefix === "" && citation.suffix === "";
const flat = (text: string) => text.replace(/\s+/g, " ");
const { comment, ...quoted } = citation;
- const { "data-markdown-copy": markdownCopy, ...linkProps } = useCitationSourceLinkProps(citation);
+ const linkProps = useCitationSourceLinkProps(citation);
return (
// One copy boundary for the card and its comment, so a copied quote carries the comment once.