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.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 4536e9a250386..cd3e4eeec5c9f 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, counted here as a mark, with its comment under it.
+ visible += linkLabels(text.slice(cursor, match.start));
+ 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 77a0874c21336..ea3604c4e1610 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,10 +110,11 @@ 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",
- ]);
- const commented = serializeAssistantCitation({ ...citation, comment: "my note" });
- expect(searchableMessageSegments({ role: "user", text: commented, streaming: false })).toEqual([
- "my note",
+ "Please fix ",
+ " thanks",
]);
+ const commented = serializeAssistantCitation({ ...citation, comment: "my\nnote" });
+ 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 3afec6e04729e..8d235e161e94d 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,9 @@ function markdownThreadFindText(
const citation =
node.tagName === "a" && typeof href === "string" ? parseAssistantCitationHref(href) : null;
if (citation) {
- text += assistantCitationLabel(citation);
+ // 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;
}
{