authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-07 21:18:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 01:18:01-03:00
log6f83358ae6d85b5dee7d3280e645e0e9a0aeff62
tree1f65d1623fb0f6354a3bae8f5e16fe169722a64d
parent0f09627a111f7e9adc22c256c725565672676f1a
signature Signed by PGP key B5690EEEBB952194

fix(web): messages with quotes or links no longer collapse when short (#16624)


5 files changed, 78 insertions(+), 17 deletions(-)

apps/web/src/components/chat/AssistantCitationChip.tsx+5-3
......@@ -1,5 +1,8 @@
11import type { AssistantCitation } from "@t3tools/contracts";
2import { serializeAssistantCitation } from "@t3tools/shared/assistantCitations";
2import {
3 assistantCitationLabel,
4 serializeAssistantCitation,
5} from "@t3tools/shared/assistantCitations";
36import { Link, useNavigate } from "@tanstack/react-router";
47import { PencilIcon, QuoteIcon } from "lucide-react";
58import {
......@@ -94,8 +97,7 @@ export function AssistantCitationChip({
9497 },
9598 }
9699 : undefined;
97 const preview = (citation.comment?.trim() || citation.text).replace(/\s+/g, " ");
98 const label = preview.length > 64 ? `${preview.slice(0, 64)}…` : preview;
100 const label = assistantCitationLabel(citation);
99101 const sourceLinkProps = {
100102 to: "/$environmentId/$threadId" as const,
101103 params: { environmentId: citation.environmentId, threadId: citation.threadId },
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+36-1
......@@ -21,12 +21,14 @@ import {
2121import { makeStreamingTimelineFixture } from "../../test-fixtures";
2222import type { TurnDiffSummary } from "../../types";
2323import { describe, expect, it } from "vite-plus/test";
24import { MessageId, RunId } from "@t3tools/contracts";
24import { EnvironmentId, MessageId, RunId } from "@t3tools/contracts";
25import { serializeAssistantCitation } from "@t3tools/shared/assistantCitations";
2526import {
2627 computeStableMessagesTimelineRows,
2728 computeMessageDurationStart,
2829 deriveMessagesTimelineRows,
2930 deriveMessagesTimelineRowsWithState,
31 shouldCollapseUserMessage,
3032 liveWorkEntryLabel,
3133 normalizeCompactToolLabel,
3234 resolveAssistantMessageCopyState,
......@@ -5162,3 +5164,36 @@ describe("live subagents after their parent turn settles", () => {
51625164 expect(rows.map((row) => row.id)).toEqual([`attempt-fold:${attempt.id}`, "child"]);
51635165 });
51645166});
5167
5168describe("shouldCollapseUserMessage", () => {
5169 it("measures a quote chip by its label, not its encoded link", () => {
5170 const quote = "A long assistant paragraph that the user quoted. ".repeat(40);
5171 const citation = serializeAssistantCitation({
5172 version: 1,
5173 environmentId: EnvironmentId.make("environment"),
5174 threadId: ThreadId.make("thread"),
5175 messageId: MessageId.make("source"),
5176 text: quote,
5177 comment: "Why does this matter?",
5178 start: 0,
5179 end: quote.length,
5180 prefix: "",
5181 suffix: "",
5182 });
5183
5184 expect(shouldCollapseUserMessage(`${citation} Can you expand on this?`)).toBe(false);
5185 expect(shouldCollapseUserMessage(`${citation} ${"More text. ".repeat(60)}`)).toBe(true);
5186 });
5187
5188 it("measures file links and context chips by their label", () => {
5189 const links = Array.from(
5190 { length: 8 },
5191 (_, index) =>
5192 `[file${index}.ts](/workspace/projects/example/packages/some/deeply/nested/directory/file${index}.ts)`,
5193 );
5194 const text = `Compare ${links.join(", ")} with [terminal 1](t3-context://v1/terminal/${"a".repeat(36)}).`;
5195
5196 expect(text.length).toBeGreaterThan(600);
5197 expect(shouldCollapseUserMessage(text)).toBe(false);
5198 });
5199});
apps/web/src/components/chat/MessagesTimeline.logic.ts+31
......@@ -2,6 +2,10 @@ import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setu
22export { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";
33import { isLiveSubagentTurnItem } from "@t3tools/client-runtime/state/subagentRuntime";
44import * as Equal from "effect/Equal";
5import {
6 assistantCitationLabel,
7 collectAssistantCitations,
8} from "@t3tools/shared/assistantCitations";
59import { shallow } from "zustand/vanilla/shallow";
610import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives";
711import {
......@@ -2226,3 +2230,30 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean
22262230 }
22272231 }
22282232}
2233
2234const MAX_COLLAPSED_USER_MESSAGE_LINES = 8;
2235const MAX_COLLAPSED_USER_MESSAGE_LENGTH = 600;
2236const MARKDOWN_LINK = /!?\[([^\]\n]*)\]\((?:<[^>\n]*>|[^\s)]*)\)/g;
2237
2238function visibleUserMessageText(text: string): string {
2239 const linkLabels = (segment: string) => segment.replace(MARKDOWN_LINK, "$1");
2240 let visible = "";
2241 let cursor = 0;
2242 for (const match of collectAssistantCitations(text)) {
2243 visible += linkLabels(text.slice(cursor, match.start)) + assistantCitationLabel(match.citation);
2244 cursor = match.end;
2245 }
2246 return visible + linkLabels(text.slice(cursor));
2247}
2248
2249export function shouldCollapseUserMessage(text: string): boolean {
2250 const visible = visibleUserMessageText(text);
2251 if (visible.trim().length === 0) {
2252 return false;
2253 }
2254
2255 return (
2256 visible.length > MAX_COLLAPSED_USER_MESSAGE_LENGTH ||
2257 visible.split("\n").length > MAX_COLLAPSED_USER_MESSAGE_LINES
2258 );
2259}
apps/web/src/components/chat/MessagesTimeline.tsx+1-13
......@@ -207,6 +207,7 @@ import {
207207 resolveTimelineMinimapNavigationInteractive,
208208 resolveTimelineMinimapTopPercent,
209209 resolveWorkGroupScrollIndex,
210 shouldCollapseUserMessage,
210211 shouldFollowWorkGroupAppend,
211212 shouldPreserveAssistantLineBreaks,
212213 threadReadLabelPrefix,
......@@ -4522,22 +4523,9 @@ function UserMessageContextReferenceChip(props: {
45224523 });
45234524}
45244525
4525const MAX_COLLAPSED_USER_MESSAGE_LINES = 8;
4526const MAX_COLLAPSED_USER_MESSAGE_LENGTH = 600;
45274526const COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM = 1.75;
45284527const COLLAPSED_USER_MESSAGE_FADE_MASK = `linear-gradient(to bottom, black calc(100% - ${COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM}rem), transparent)`;
45294528
4530function shouldCollapseUserMessage(text: string): boolean {
4531 if (text.trim().length === 0) {
4532 return false;
4533 }
4534
4535 return (
4536 text.length > MAX_COLLAPSED_USER_MESSAGE_LENGTH ||
4537 text.split("\n").length > MAX_COLLAPSED_USER_MESSAGE_LINES
4538 );
4539}
4540
45414529const CollapsibleUserMessageBody = memo(function CollapsibleUserMessageBody(props: {
45424530 text: string;
45434531 renderContextReference: (reference: ChatMarkdownContextReference) => ReactNode;
packages/shared/src/assistantCitations.ts+5
......@@ -98,6 +98,11 @@ export function parseAssistantCitationHref(href: string): AssistantCitation | nu
9898 }
9999}
100100
101export function assistantCitationLabel(citation: AssistantCitation): string {
102 const preview = (citation.comment?.trim() || citation.text).replace(/\s+/g, " ");
103 return preview.length > 64 ? `${preview.slice(0, 64)}…` : preview;
104}
105
101106export function serializeAssistantCitation(citation: AssistantCitation): string {
102107 return `[Assistant quote](${formatAssistantCitationHref(citation)})`;
103108}