From 0855de336ea43cf21968ec5c925ea71d32b5f9a3 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 14:30:18 +0200 Subject: [PATCH 1/5] fix(ui): render structured thread references natively Generated-By: PostHog Code Task-Id: 6b917101-5b31-4195-81cf-f258a46a56a2 --- .../canvas/components/MentionText.test.tsx | 34 +++++++++ .../canvas/components/MentionText.tsx | 74 +++++++++++++++++-- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/features/canvas/components/MentionText.test.tsx b/packages/ui/src/features/canvas/components/MentionText.test.tsx index e924736b4e..f2cb18e513 100644 --- a/packages/ui/src/features/canvas/components/MentionText.test.tsx +++ b/packages/ui/src/features/canvas/components/MentionText.test.tsx @@ -82,4 +82,38 @@ describe("MentionText", () => { expect(screen.getByText("A thread reply")).not.toHaveClass("text-xs"); }); + + it("renders structured references as inline native chips", () => { + const { container } = render( + with using ' + } + />, + ); + + expect(screen.getByText("#73874 - Loading…")).toBeInTheDocument(); + expect(screen.getByText("@src/loading.tsx")).toBeInTheDocument(); + expect(screen.getByText("/review")).toBeInTheDocument(); + expect(container.querySelector("p")).toBeNull(); + expect(screen.getByText("@src/loading.tsx").parentElement).toHaveClass( + "inline-flex", + "whitespace-nowrap", + ); + }); + + it("leaves malformed and unsafe structured references as readable text", () => { + render( + and ' + } + />, + ); + + expect(screen.getByText(/#12 - Unsafe/).parentElement).toHaveTextContent( + '@#12 - Unsafe and ', + ); + expect(screen.queryByRole("button")).toBeNull(); + }); }); diff --git a/packages/ui/src/features/canvas/components/MentionText.tsx b/packages/ui/src/features/canvas/components/MentionText.tsx index 8fbe4bea7f..ffe4a4554f 100644 --- a/packages/ui/src/features/canvas/components/MentionText.tsx +++ b/packages/ui/src/features/canvas/components/MentionText.tsx @@ -1,5 +1,19 @@ +import { + ChartLineIcon, + FileTextIcon, + FlagIcon, + FlaskIcon, + FolderIcon, + TerminalIcon, + WarningIcon, +} from "@phosphor-icons/react"; +import type { MentionChip } from "@posthog/core/message-editor/content"; +import { xmlToContent } from "@posthog/core/message-editor/content"; +import { Chip } from "@posthog/quill"; import { splitMentionSegments } from "@posthog/shared"; import { splitLinkSegments } from "@posthog/ui/features/canvas/utils/linkify"; +import { GithubRefChip } from "@posthog/ui/features/editor/components/GithubRefChip"; +import { parseGithubIssueUrl } from "@posthog/ui/features/message-editor/githubIssueUrl"; import { handleShareLinkClick } from "@posthog/ui/utils/shareLinks"; import { Fragment, useMemo } from "react"; import "./mention-chip.css"; @@ -8,7 +22,46 @@ type RenderSegment = | { type: "text"; text: string } | { type: "link"; text: string; href: string } | { type: "agent"; text: string } - | { type: "mention"; name: string; email: string }; + | { type: "mention"; name: string; email: string } + | { type: "chip"; chip: MentionChip }; + +const chipIcons = { + file: FileTextIcon, + folder: FolderIcon, + command: TerminalIcon, + error: WarningIcon, + experiment: FlaskIcon, + insight: ChartLineIcon, + feature_flag: FlagIcon, +} as const; + +function StructuredChip({ chip }: { chip: MentionChip }) { + if (chip.type === "github_issue" || chip.type === "github_pr") { + const githubRef = parseGithubIssueUrl(chip.id); + if (githubRef) { + return ( + + {chip.label} + + ); + } + } + + const Icon = chipIcons[chip.type as keyof typeof chipIcons]; + if (!Icon) return <>@{chip.label}; + return ( + + + + {chip.type === "command" ? "/" : "@"} + {chip.label} + + + ); +} // The plain (not-the-viewer) mention chip look, also used by surfaces that // render a mention-styled name without real mention semantics (e.g. the @@ -66,11 +119,17 @@ export function MentionText({ } } }; - for (const segment of splitLinkSegments(content)) { - if (segment.type === "link") { - push(segment, segment.text.length); - } else { - pushMentions(segment.text); + for (const contentSegment of xmlToContent(content).segments) { + if (contentSegment.type === "chip") { + push({ type: "chip", chip: contentSegment.chip }, 1); + continue; + } + for (const segment of splitLinkSegments(contentSegment.text)) { + if (segment.type === "link") { + push(segment, segment.text.length); + } else { + pushMentions(segment.text); + } } } return entries; @@ -79,6 +138,9 @@ export function MentionText({ return ( {segments.map(({ segment, key }) => { + if (segment.type === "chip") { + return ; + } if (segment.type === "agent") { return ( From 714b8620b7e3a155507beff6307b4be7c7f1c9e7 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 14:44:47 +0200 Subject: [PATCH 2/5] fix(ui): render references in timeline previews Generated-By: PostHog Code Task-Id: 6b917101-5b31-4195-81cf-f258a46a56a2 --- .../components/ActivityTimeline.test.tsx | 19 +++++++++++++++++-- .../canvas/components/ActivityTimeline.tsx | 3 ++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx index c1f625b0fe..81a58e1ec4 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx @@ -36,13 +36,13 @@ const conversationItems = [ }, ]; -function renderTimeline(canOpenInPlace?: boolean) { +function renderTimeline(canOpenInPlace?: boolean, items = conversationItems) { return render( { expect(screen.queryAllByRole("button")).toHaveLength(0); expect(screen.getByText(/first thing/)).toBeInTheDocument(); }); + + it("renders structured references natively in conversation previews", () => { + renderTimeline(false, [ + { + type: "user_message", + id: "pr-message", + content: + '', + timestamp: Date.parse("2026-07-17T09:05:00Z"), + }, + ]); + + expect(screen.getByText("#73874 - Loading…")).toBeInTheDocument(); + expect(screen.queryByText(/ - {content} + From 58be39a6ddf171f14bc8397ca4de735c68a7e47d Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 14:53:23 +0200 Subject: [PATCH 3/5] fix(ui): collapse newlines in timeline previews Generated-By: PostHog Code Task-Id: 6b917101-5b31-4195-81cf-f258a46a56a2 --- .../components/ActivityTimeline.test.tsx | 4 +++- .../canvas/components/ActivityTimeline.tsx | 4 +--- .../canvas/components/ThreadPanel.test.tsx | 24 +++++++++++++++++++ .../canvas/components/ThreadPanel.tsx | 4 +--- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx index 81a58e1ec4..1eb23f29d8 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx @@ -88,7 +88,9 @@ describe("ActivityTimeline", () => { renderTimeline(); expect(screen.queryAllByRole("button")).toHaveLength(0); - expect(screen.getByText(/first thing/)).toBeInTheDocument(); + expect(screen.getByText(/first thing/).closest("[data-slot]")).toHaveClass( + "whitespace-normal", + ); }); it("renders structured references natively in conversation previews", () => { diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.tsx index 81ce72432f..4333456eb3 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.tsx @@ -109,9 +109,7 @@ function UserMessageRow({ {name} - {/* `whitespace-pre-wrap` makes the clamp land on the first *written* - line rather than the first wrapped one. */} - + diff --git a/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx b/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx index 35b16ac5cd..8116cfe5e3 100644 --- a/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx +++ b/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx @@ -96,6 +96,30 @@ describe("ThreadMessageRow", () => { screen.getByText(/Second line with more detail/), ).toBeInTheDocument(); }); + + it("collapses authored newlines in one-line timeline previews", () => { + render( + {}} + onDelete={() => {}} + />, + ); + + expect( + screen.getByText(/Second line with more detail/).parentElement, + ).toHaveClass("line-clamp-1", "whitespace-normal"); + }); }); describe("ThreadArtifactRow", () => { diff --git a/packages/ui/src/features/canvas/components/ThreadPanel.tsx b/packages/ui/src/features/canvas/components/ThreadPanel.tsx index 807f3fbd01..5243dda59b 100644 --- a/packages/ui/src/features/canvas/components/ThreadPanel.tsx +++ b/packages/ui/src/features/canvas/components/ThreadPanel.tsx @@ -101,9 +101,7 @@ export function ThreadMessageRow({ Date: Wed, 29 Jul 2026 15:41:03 +0200 Subject: [PATCH 4/5] fix(ui): show full messages in activity timeline Generated-By: PostHog Code Task-Id: 6b917101-5b31-4195-81cf-f258a46a56a2 --- .../canvas/components/ActivityTimeline.test.tsx | 6 +++--- .../features/canvas/components/ActivityTimeline.tsx | 2 +- .../features/canvas/components/ThreadPanel.test.tsx | 10 ++++------ .../ui/src/features/canvas/components/ThreadPanel.tsx | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx index 1eb23f29d8..f04a6a57c9 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx @@ -88,9 +88,9 @@ describe("ActivityTimeline", () => { renderTimeline(); expect(screen.queryAllByRole("button")).toHaveLength(0); - expect(screen.getByText(/first thing/).closest("[data-slot]")).toHaveClass( - "whitespace-normal", - ); + const body = screen.getByText(/first thing/).closest("[data-slot]"); + expect(body).toHaveClass("whitespace-pre-wrap", "break-words"); + expect(body).not.toHaveClass("line-clamp-1"); }); it("renders structured references natively in conversation previews", () => { diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.tsx index 4333456eb3..dfff740247 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.tsx @@ -109,7 +109,7 @@ function UserMessageRow({ {name} - + diff --git a/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx b/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx index 8116cfe5e3..e964599ba6 100644 --- a/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx +++ b/packages/ui/src/features/canvas/components/ThreadPanel.test.tsx @@ -72,8 +72,6 @@ describe("ThreadMessageRow", () => { const multiline = "First line\n\nSecond line with more detail"; - // `preview` only adds a CSS clamp, so the full message is in the DOM either - // way. What's worth pinning is that no code path slices the text away. it("renders the whole message, so a comment is never cut short", () => { render( { ).toBeInTheDocument(); }); - it("collapses authored newlines in one-line timeline previews", () => { + it("shows the full message in timeline previews", () => { render( { />, ); - expect( - screen.getByText(/Second line with more detail/).parentElement, - ).toHaveClass("line-clamp-1", "whitespace-normal"); + const body = screen.getByText(/Second line with more detail/).parentElement; + expect(body).toHaveClass("whitespace-pre-wrap", "break-words"); + expect(body).not.toHaveClass("line-clamp-1"); }); }); diff --git a/packages/ui/src/features/canvas/components/ThreadPanel.tsx b/packages/ui/src/features/canvas/components/ThreadPanel.tsx index 5243dda59b..bb12bec4d8 100644 --- a/packages/ui/src/features/canvas/components/ThreadPanel.tsx +++ b/packages/ui/src/features/canvas/components/ThreadPanel.tsx @@ -78,7 +78,7 @@ export function ThreadMessageRow({ isOwnMessage: boolean; currentUserEmail?: string | null; canForward: boolean; - /** Timeline rows show one truncated line; the Comments tab shows it all. */ + /** Timeline rows preserve authored whitespace while showing the full message. */ preview?: boolean; onSendToAgent: () => void; onDelete: () => void; @@ -101,7 +101,7 @@ export function ThreadMessageRow({ Date: Wed, 29 Jul 2026 15:54:45 +0200 Subject: [PATCH 5/5] fix(canvas): fold channel context in activity timeline Generated-By: PostHog Code Task-Id: 6b917101-5b31-4195-81cf-f258a46a56a2 --- .../components/ActivityTimeline.test.tsx | 21 ++++++++++++ .../canvas/components/ActivityTimeline.tsx | 32 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx index f04a6a57c9..b6ea0cb52d 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.test.tsx @@ -107,4 +107,25 @@ describe("ActivityTimeline", () => { expect(screen.getByText("#73874 - Loading…")).toBeInTheDocument(); expect(screen.queryByText(/ { + renderTimeline(true, [ + { + type: "user_message", + id: "context-message", + content: + 'Review this\n\nSaved workspace context', + timestamp: Date.parse("2026-07-17T09:05:00Z"), + }, + ]); + + expect(screen.getByText("Review this")).toBeInTheDocument(); + expect(screen.queryByText(/ void; }) { const name = author ? userDisplayName(author) : "You"; + const channelContext = useMemo( + () => extractChannelContext(content), + [content], + ); + const displayContent = channelContext?.stripped ?? content; // The row itself is the hit target. `ThreadItem` renders an
, which a //