authorgravatar for 92423096+nateEc@users.noreply.github.comYukun Shan <92423096+nateEc@users.noreply.github.com> 2026-08-15 18:43:50+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:43:50+02:00
log1e87029261f9b81061a2a7420849b9eeaf1a2ebe
tree32ed0135c7c965c55539d75d0e6514228336ee02
parent672216d7e152241213a8757892f281e1f4434e8a
signature Signed by PGP key B5690EEEBB952194

fix(web): preserve Claude insight line breaks (#4344)


3 files changed, 18 insertions(+), 0 deletions(-)

apps/web/src/components/chat/MessagesTimeline.logic.test.ts+12
...@@ -5,8 +5,20 @@ import {...@@ -5,8 +5,20 @@ import {
5 deriveMessagesTimelineRows,5 deriveMessagesTimelineRows,
6 normalizeCompactToolLabel,6 normalizeCompactToolLabel,
7 resolveAssistantMessageCopyState,7 resolveAssistantMessageCopyState,
8 shouldPreserveAssistantLineBreaks,
8} from "./MessagesTimeline.logic";9} from "./MessagesTimeline.logic";
910
11describe("shouldPreserveAssistantLineBreaks", () => {
12 it("preserves Claude insight formatting without changing regular markdown", () => {
13 expect(
14 shouldPreserveAssistantLineBreaks(
15 "★ Insight ─────────────────\\nFirst observation\\nSecond observation\\n─────────────────",
16 ),
17 ).toBe(true);
18 expect(shouldPreserveAssistantLineBreaks("A normal\\nmarkdown paragraph")).toBe(false);
19 });
20});
21
10describe("computeMessageDurationStart", () => {22describe("computeMessageDurationStart", () => {
11 it("returns message createdAt when there is no preceding user message", () => {23 it("returns message createdAt when there is no preceding user message", () => {
12 const result = computeMessageDurationStart([24 const result = computeMessageDurationStart([
apps/web/src/components/chat/MessagesTimeline.logic.ts+4
...@@ -52,6 +52,10 @@ export function resolveTimelineIsAtEnd(...@@ -52,6 +52,10 @@ export function resolveTimelineIsAtEnd(
52 return contentLength - scroll - scrollLength - endInset <= TIMELINE_FOLLOW_REARM_THRESHOLD_PX;52 return contentLength - scroll - scrollLength - endInset <= TIMELINE_FOLLOW_REARM_THRESHOLD_PX;
53}53}
5454
55export function shouldPreserveAssistantLineBreaks(text: string): boolean {
56 return /^★ Insight(?:\s|─)/mu.test(text);
57}
58
55export function resolveTimelineMinimapHeightStyle(itemCount: number): string {59export function resolveTimelineMinimapHeightStyle(itemCount: number): string {
56 const naturalHeight = Math.max(1, (itemCount - 1) * TIMELINE_MINIMAP_ITEM_SPACING);60 const naturalHeight = Math.max(1, (itemCount - 1) * TIMELINE_MINIMAP_ITEM_SPACING);
57 return `min(${naturalHeight}px, ${TIMELINE_MINIMAP_MAX_HEIGHT_CSS})`;61 return `min(${naturalHeight}px, ${TIMELINE_MINIMAP_MAX_HEIGHT_CSS})`;
apps/web/src/components/chat/MessagesTimeline.tsx+2
...@@ -83,6 +83,7 @@ import {...@@ -83,6 +83,7 @@ import {
83 resolveTimelineMinimapIndexFromPointer,83 resolveTimelineMinimapIndexFromPointer,
84 resolveTimelineMinimapInteractiveWidth,84 resolveTimelineMinimapInteractiveWidth,
85 resolveTimelineMinimapTopPercent,85 resolveTimelineMinimapTopPercent,
86 shouldPreserveAssistantLineBreaks,
86 type StableMessagesTimelineRowsState,87 type StableMessagesTimelineRowsState,
87 type MessagesTimelineRow,88 type MessagesTimelineRow,
88 TIMELINE_MINIMAP_MIN_ITEMS,89 TIMELINE_MINIMAP_MIN_ITEMS,
...@@ -1113,6 +1114,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess...@@ -1113,6 +1114,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
1113 cwd={ctx.markdownCwd}1114 cwd={ctx.markdownCwd}
1114 threadRef={ctx.threadRef ?? undefined}1115 threadRef={ctx.threadRef ?? undefined}
1115 isStreaming={Boolean(row.message.streaming)}1116 isStreaming={Boolean(row.message.streaming)}
1117 lineBreaks={shouldPreserveAssistantLineBreaks(messageText)}
1116 skills={ctx.skills}1118 skills={ctx.skills}
1117 />1119 />
1118 <AssistantChangedFilesSection1120 <AssistantChangedFilesSection