Skip to content

Commit 90c7a61

Browse files
authored
Respect thread author kinds and markdown link boundaries
Treat structured author kinds as authoritative so system rows render as System and legacy authorless rows retain human behavior. Parse markdown links before mentions so labels containing @agent remain intact, with regression coverage for both review findings. Generated-By: PostHog Code Task-Id: 152cf34b-23c8-4e87-be9e-12d727e08edd
1 parent 911f0d9 commit 90c7a61

4 files changed

Lines changed: 98 additions & 19 deletions

File tree

packages/ui/src/features/canvas/components/MentionText.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ describe("MentionText", () => {
3131
expect(screen.queryByText("@agent")).not.toBeInTheDocument();
3232
});
3333

34+
it("keeps agent text inside a markdown link label", () => {
35+
render(
36+
<MentionText content="[My @agent report](https://posthog.com/report) has been created" />,
37+
);
38+
39+
expect(
40+
screen.getByRole("link", { name: "My @agent report" }),
41+
).toHaveAttribute("href", "https://posthog.com/report");
42+
expect(screen.queryByText("@agent")).not.toBeInTheDocument();
43+
});
44+
3445
it("inherits the surrounding message text size", () => {
3546
render(<MentionText content="A thread reply" />);
3647

packages/ui/src/features/canvas/components/MentionText.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,40 @@ export function MentionText({
3636
entries.push({ segment, key: `${offset}` });
3737
offset += length;
3838
};
39-
const pushText = (text: string) => {
39+
const pushAgentMentions = (text: string) => {
4040
let cursor = 0;
4141
for (const match of text.matchAll(/(^|\s)(@agent)\b/gi)) {
4242
const mentionStart = (match.index ?? 0) + match[1].length;
43-
for (const part of splitLinkSegments(
44-
text.slice(cursor, mentionStart),
45-
)) {
46-
push(part, part.text.length);
43+
if (mentionStart > cursor) {
44+
push(
45+
{ type: "text", text: text.slice(cursor, mentionStart) },
46+
mentionStart - cursor,
47+
);
4748
}
4849
push({ type: "agent", text: match[2] }, match[2].length);
4950
cursor = mentionStart + match[2].length;
5051
}
51-
for (const part of splitLinkSegments(text.slice(cursor))) {
52-
push(part, part.text.length);
52+
if (cursor < text.length) {
53+
push({ type: "text", text: text.slice(cursor) }, text.length - cursor);
54+
}
55+
};
56+
const pushMentions = (text: string) => {
57+
for (const segment of splitMentionSegments(text)) {
58+
if (segment.type === "mention") {
59+
push(
60+
{ type: "mention", name: segment.name, email: segment.email },
61+
segment.text.length,
62+
);
63+
} else {
64+
pushAgentMentions(segment.text);
65+
}
5366
}
5467
};
55-
for (const segment of splitMentionSegments(content)) {
56-
if (segment.type === "mention") {
57-
push(
58-
{ type: "mention", name: segment.name, email: segment.email },
59-
segment.text.length,
60-
);
68+
for (const segment of splitLinkSegments(content)) {
69+
if (segment.type === "link") {
70+
push(segment, segment.text.length);
6171
} else {
62-
pushText(segment.text);
72+
pushMentions(segment.text);
6373
}
6474
}
6575
return entries;

packages/ui/src/features/canvas/components/ThreadPanel.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,57 @@ describe("ThreadMessageRow", () => {
7474
expect(screen.getByText("Agent")).toBeInTheDocument();
7575
expect(screen.queryByText("Unknown")).not.toBeInTheDocument();
7676
});
77+
78+
it("renders system announcements as System without human actions", () => {
79+
render(
80+
<ThreadMessageRow
81+
message={{
82+
id: "system-announcement",
83+
task: "task",
84+
author_kind: "system",
85+
event: "status_changed",
86+
payload: {},
87+
content: "Status changed",
88+
created_at: "2026-07-17T00:00:00Z",
89+
author: null,
90+
}}
91+
isTaskAuthor
92+
isOwnMessage={false}
93+
canForward
94+
onSendToAgent={() => {}}
95+
onDelete={() => {}}
96+
/>,
97+
);
98+
99+
expect(screen.getByText("System")).toBeInTheDocument();
100+
expect(
101+
screen.queryByRole("button", { name: "Message actions" }),
102+
).not.toBeInTheDocument();
103+
});
104+
105+
it("keeps legacy authorless rows as human messages", () => {
106+
render(
107+
<ThreadMessageRow
108+
message={{
109+
id: "legacy-message",
110+
task: "task",
111+
content: "Author removed",
112+
created_at: "2026-07-17T00:00:00Z",
113+
author: null,
114+
}}
115+
isTaskAuthor
116+
isOwnMessage={false}
117+
canForward
118+
onSendToAgent={() => {}}
119+
onDelete={() => {}}
120+
/>,
121+
);
122+
123+
expect(screen.getByText("Unknown")).toBeInTheDocument();
124+
expect(
125+
screen.getByRole("button", { name: "Message actions" }),
126+
).toBeInTheDocument();
127+
});
77128
});
78129

79130
describe("UserPromptRow", () => {

packages/ui/src/features/canvas/components/ThreadPanel.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ export function ThreadMessageRow({
105105
onDelete: () => void;
106106
}) {
107107
const forwarded = !!message.forwarded_to_agent_at;
108-
const isAgent =
109-
message.author_kind === "agent" ||
110-
(!message.author_kind && !message.author);
111-
const showMenu = !isAgent && ((isTaskAuthor && !forwarded) || isOwnMessage);
108+
const authorKind = message.author_kind ?? "human";
109+
const isAgent = authorKind === "agent";
110+
const isSystem = authorKind === "system";
111+
const showMenu =
112+
authorKind === "human" && ((isTaskAuthor && !forwarded) || isOwnMessage);
112113

113114
return (
114115
<ThreadItem>
@@ -117,6 +118,8 @@ export function ThreadMessageRow({
117118
<AvatarFallback>
118119
{isAgent ? (
119120
<RobotIcon size={14} />
121+
) : isSystem ? (
122+
"S"
120123
) : (
121124
getUserInitials(message.author)
122125
)}
@@ -126,7 +129,11 @@ export function ThreadMessageRow({
126129
<ThreadItemContent>
127130
<ThreadItemHeader>
128131
<ThreadItemAuthor>
129-
{isAgent ? "Agent" : userDisplayName(message.author)}
132+
{isAgent
133+
? "Agent"
134+
: isSystem
135+
? "System"
136+
: userDisplayName(message.author)}
130137
</ThreadItemAuthor>
131138
<ThreadTimestamp dateTime={message.created_at} />
132139
</ThreadItemHeader>

0 commit comments

Comments
 (0)