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 @@...@@ -1,5 +1,8 @@
1import type { AssistantCitation } from "@t3tools/contracts";1import type { AssistantCitation } from "@t3tools/contracts";
2import { serializeAssistantCitation } from "@t3tools/shared/assistantCitations";2import {
3 assistantCitationLabel,
4 serializeAssistantCitation,
5} from "@t3tools/shared/assistantCitations";
3import { Link, useNavigate } from "@tanstack/react-router";6import { Link, useNavigate } from "@tanstack/react-router";
4import { PencilIcon, QuoteIcon } from "lucide-react";7import { PencilIcon, QuoteIcon } from "lucide-react";
5import {8import {
...@@ -94,8 +97,7 @@ export function AssistantCitationChip({...@@ -94,8 +97,7 @@ export function AssistantCitationChip({
94 },97 },
95 }98 }
96 : undefined;99 : undefined;
97 const preview = (citation.comment?.trim() || citation.text).replace(/\s+/g, " ");100 const label = assistantCitationLabel(citation);
98 const label = preview.length > 64 ? `${preview.slice(0, 64)}…` : preview;
99 const sourceLinkProps = {101 const sourceLinkProps = {
100 to: "/$environmentId/$threadId" as const,102 to: "/$environmentId/$threadId" as const,
101 params: { environmentId: citation.environmentId, threadId: citation.threadId },103 params: { environmentId: citation.environmentId, threadId: citation.threadId },
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+36-1
...@@ -21,12 +21,14 @@ import {...@@ -21,12 +21,14 @@ import {
21import { makeStreamingTimelineFixture } from "../../test-fixtures";21import { makeStreamingTimelineFixture } from "../../test-fixtures";
22import type { TurnDiffSummary } from "../../types";22import type { TurnDiffSummary } from "../../types";
23import { describe, expect, it } from "vite-plus/test";23import { 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";
25import {26import {
26 computeStableMessagesTimelineRows,27 computeStableMessagesTimelineRows,
27 computeMessageDurationStart,28 computeMessageDurationStart,
28 deriveMessagesTimelineRows,29 deriveMessagesTimelineRows,
29 deriveMessagesTimelineRowsWithState,30 deriveMessagesTimelineRowsWithState,
31 shouldCollapseUserMessage,
30 liveWorkEntryLabel,32 liveWorkEntryLabel,
31 normalizeCompactToolLabel,33 normalizeCompactToolLabel,
32 resolveAssistantMessageCopyState,34 resolveAssistantMessageCopyState,
...@@ -5162,3 +5164,36 @@ describe("live subagents after their parent turn settles", () => {...@@ -5162,3 +5164,36 @@ describe("live subagents after their parent turn settles", () => {
5162 expect(rows.map((row) => row.id)).toEqual([`attempt-fold:${attempt.id}`, "child"]);5164 expect(rows.map((row) => row.id)).toEqual([`attempt-fold:${attempt.id}`, "child"]);
5163 });5165 });
5164});5166});
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...@@ -2,6 +2,10 @@ import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setu
2export { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";2export { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";
3import { isLiveSubagentTurnItem } from "@t3tools/client-runtime/state/subagentRuntime";3import { isLiveSubagentTurnItem } from "@t3tools/client-runtime/state/subagentRuntime";
4import * as Equal from "effect/Equal";4import * as Equal from "effect/Equal";
5import {
6 assistantCitationLabel,
7 collectAssistantCitations,
8} from "@t3tools/shared/assistantCitations";
5import { shallow } from "zustand/vanilla/shallow";9import { shallow } from "zustand/vanilla/shallow";
6import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives";10import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives";
7import {11import {
...@@ -2226,3 +2230,30 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean...@@ -2226,3 +2230,30 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean
2226 }2230 }
2227 }2231 }
2228}2232}
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 {...@@ -207,6 +207,7 @@ import {
207 resolveTimelineMinimapNavigationInteractive,207 resolveTimelineMinimapNavigationInteractive,
208 resolveTimelineMinimapTopPercent,208 resolveTimelineMinimapTopPercent,
209 resolveWorkGroupScrollIndex,209 resolveWorkGroupScrollIndex,
210 shouldCollapseUserMessage,
210 shouldFollowWorkGroupAppend,211 shouldFollowWorkGroupAppend,
211 shouldPreserveAssistantLineBreaks,212 shouldPreserveAssistantLineBreaks,
212 threadReadLabelPrefix,213 threadReadLabelPrefix,
...@@ -4522,22 +4523,9 @@ function UserMessageContextReferenceChip(props: {...@@ -4522,22 +4523,9 @@ function UserMessageContextReferenceChip(props: {
4522 });4523 });
4523}4524}
45244525
4525const MAX_COLLAPSED_USER_MESSAGE_LINES = 8;
4526const MAX_COLLAPSED_USER_MESSAGE_LENGTH = 600;
4527const COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM = 1.75;4526const COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM = 1.75;
4528const COLLAPSED_USER_MESSAGE_FADE_MASK = `linear-gradient(to bottom, black calc(100% - ${COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM}rem), transparent)`;4527const 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
4541const CollapsibleUserMessageBody = memo(function CollapsibleUserMessageBody(props: {4529const CollapsibleUserMessageBody = memo(function CollapsibleUserMessageBody(props: {
4542 text: string;4530 text: string;
4543 renderContextReference: (reference: ChatMarkdownContextReference) => ReactNode;4531 renderContextReference: (reference: ChatMarkdownContextReference) => ReactNode;
packages/shared/src/assistantCitations.ts+5
...@@ -98,6 +98,11 @@ export function parseAssistantCitationHref(href: string): AssistantCitation | nu...@@ -98,6 +98,11 @@ export function parseAssistantCitationHref(href: string): AssistantCitation | nu
98 }98 }
99}99}
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
101export function serializeAssistantCitation(citation: AssistantCitation): string {106export function serializeAssistantCitation(citation: AssistantCitation): string {
102 return `[Assistant quote](${formatAssistantCitationHref(citation)})`;107 return `[Assistant quote](${formatAssistantCitationHref(citation)})`;
103}108}