Skip to content

Commit 3b560b4

Browse files
authored
feat(canvas): show only human messages and artifacts in thread timeline (#3694)
1 parent 61f1d0f commit 3b560b4

5 files changed

Lines changed: 505 additions & 425 deletions

File tree

packages/core/src/canvas/threadTimeline.test.ts

Lines changed: 152 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
buildThreadTimeline,
44
deriveThreadAgentStatus,
55
hasAgentMention,
6-
normalizeAgentPromptText,
76
shouldSuspendThreadSession,
7+
threadMessageArtifact,
88
} from "./threadTimeline";
99

1010
describe("hasAgentMention", () => {
@@ -19,96 +19,176 @@ describe("hasAgentMention", () => {
1919
});
2020
});
2121

22-
describe("normalizeAgentPromptText", () => {
22+
describe("threadMessageArtifact", () => {
23+
it("maps a canvas_created message to a canvas artifact", () => {
24+
expect(
25+
threadMessageArtifact({
26+
id: "m1",
27+
content:
28+
"[Signups](https://us.posthog.com/code/canvas/c/d) has been created",
29+
created_at: "2026-07-17T00:00:00Z",
30+
author_kind: "agent",
31+
event: "canvas_created",
32+
payload: {
33+
canvas_name: "Signups",
34+
canvas_url: "https://us.posthog.com/code/canvas/c/d",
35+
},
36+
}),
37+
).toEqual({
38+
kind: "canvas",
39+
name: "Signups",
40+
url: "https://us.posthog.com/code/canvas/c/d",
41+
});
42+
});
43+
44+
it("falls back to a default canvas name and no url", () => {
45+
expect(
46+
threadMessageArtifact({
47+
id: "m1",
48+
content: "Canvas has been created",
49+
created_at: "2026-07-17T00:00:00Z",
50+
author_kind: "agent",
51+
event: "canvas_created",
52+
payload: {},
53+
}),
54+
).toEqual({ kind: "canvas", name: "Canvas", url: null });
55+
});
56+
57+
it("maps a pr_created message to a pr artifact", () => {
58+
expect(
59+
threadMessageArtifact({
60+
id: "m2",
61+
content: "Pull request opened",
62+
created_at: "2026-07-17T00:00:00Z",
63+
author_kind: "agent",
64+
event: "pr_created",
65+
payload: { pr_url: "https://github.com/org/repo/pull/123" },
66+
}),
67+
).toEqual({ kind: "pr", url: "https://github.com/org/repo/pull/123" });
68+
});
69+
2370
it.each([
2471
[
25-
"forwarded thread comment",
26-
"[Thread comment from Peter Kirkham] @agent which model are you?",
27-
"which model are you?",
72+
"a pr_created message without a url",
73+
{
74+
id: "m3",
75+
content: "Pull request opened",
76+
created_at: "2026-07-17T00:00:00Z",
77+
author_kind: "agent" as const,
78+
event: "pr_created",
79+
payload: {},
80+
},
2881
],
29-
["direct prompt", "which model are you?", "which model are you?"],
3082
[
31-
"direct prompt with mention",
32-
"@agent which model are you?",
33-
"which model are you?",
83+
"a turn_complete message",
84+
{
85+
id: "m4",
86+
content: "@[Casey](casey@example.com) Done.",
87+
created_at: "2026-07-17T00:00:00Z",
88+
author_kind: "agent" as const,
89+
event: "turn_complete",
90+
payload: { run_id: "run" },
91+
},
3492
],
35-
])("normalizes a %s", (_name, content, expected) => {
36-
expect(normalizeAgentPromptText(content)).toBe(expected);
93+
[
94+
"a human message",
95+
{
96+
id: "m5",
97+
content: "Looks good",
98+
created_at: "2026-07-17T00:00:00Z",
99+
},
100+
],
101+
])("returns no artifact for %s", (_name, message) => {
102+
expect(threadMessageArtifact(message)).toBeNull();
37103
});
38104
});
39105

40106
describe("buildThreadTimeline", () => {
41-
it("omits the session echo of a forwarded thread message", () => {
42-
const timeline = buildThreadTimeline({
43-
prompts: [
44-
{
45-
id: "prompt",
46-
text: "[Thread comment from Peter Kirkham] @agent which model are you?",
47-
timestamp: 200,
48-
},
49-
],
50-
humanMessages: [
51-
{
52-
id: "human",
53-
content: "@agent which model are you?",
54-
createdAt: "1970-01-01T00:00:00.100Z",
55-
forwardedToAgent: true,
56-
},
57-
],
58-
agentMessages: [],
59-
});
107+
it("keeps only human messages and artifacts", () => {
108+
const timeline = buildThreadTimeline([
109+
{
110+
id: "human",
111+
content: "Kicking this off",
112+
created_at: "1970-01-01T00:00:00.100Z",
113+
},
114+
{
115+
id: "turn",
116+
content: "@[Casey](casey@example.com) Shipped it.",
117+
created_at: "1970-01-01T00:00:00.200Z",
118+
author_kind: "agent",
119+
event: "turn_complete",
120+
payload: { run_id: "run" },
121+
},
122+
{
123+
id: "system",
124+
content: "Status changed",
125+
created_at: "1970-01-01T00:00:00.250Z",
126+
author_kind: "system",
127+
event: "status_changed",
128+
},
129+
{
130+
id: "canvas",
131+
content: "Canvas has been created",
132+
created_at: "1970-01-01T00:00:00.300Z",
133+
author_kind: "agent",
134+
event: "canvas_created",
135+
payload: { canvas_name: "Signups", canvas_url: null },
136+
},
137+
]);
60138

61-
expect(timeline.map((row) => row.kind)).toEqual(["human"]);
139+
expect(timeline.map((row) => row.kind)).toEqual(["human", "artifact"]);
62140
});
63141

64-
it("keeps a thread-comment prompt without a matching forwarded message", () => {
65-
const timeline = buildThreadTimeline({
66-
prompts: [
67-
{
68-
id: "prompt",
69-
text: "[Thread comment from Peter Kirkham] @agent which model are you?",
70-
timestamp: 200,
71-
},
72-
],
73-
humanMessages: [],
74-
agentMessages: [],
75-
});
142+
it("orders human messages and artifacts chronologically", () => {
143+
const timeline = buildThreadTimeline([
144+
{
145+
id: "pr",
146+
content: "Pull request opened",
147+
created_at: "1970-01-01T00:00:00.200Z",
148+
author_kind: "agent",
149+
event: "pr_created",
150+
payload: { pr_url: "https://github.com/org/repo/pull/1" },
151+
},
152+
{
153+
id: "human",
154+
content: "Reply",
155+
created_at: "1970-01-01T00:00:00.100Z",
156+
},
157+
]);
76158

77-
expect(timeline.map((row) => row.kind)).toEqual(["prompt"]);
159+
expect(timeline.map((row) => row.message.id)).toEqual(["human", "pr"]);
78160
});
79161

80-
it("interleaves prompts, human replies, and agent turns chronologically", () => {
81-
const timeline = buildThreadTimeline({
82-
prompts: [{ id: "prompt", text: "Start", timestamp: 100 }],
83-
humanMessages: [
84-
{
85-
id: "human",
86-
content: "Reply",
87-
createdAt: "1970-01-01T00:00:00.150Z",
88-
},
89-
],
90-
agentMessages: [{ id: "agent", text: "Done", timestamp: 200 }],
91-
});
92-
93-
expect(timeline.map((row) => row.kind)).toEqual([
94-
"prompt",
95-
"human",
96-
"agent",
162+
it("keeps malformed timestamps at the end", () => {
163+
const timeline = buildThreadTimeline([
164+
{ id: "broken", content: "Reply", created_at: "invalid" },
165+
{
166+
id: "human",
167+
content: "Reply",
168+
created_at: "1970-01-01T00:00:00.100Z",
169+
},
97170
]);
98-
});
99171

100-
it("keeps malformed timestamps at the end", () => {
101-
const timeline = buildThreadTimeline({
102-
prompts: [{ id: "prompt", text: "Start", timestamp: 100 }],
103-
humanMessages: [{ id: "human", content: "Reply", createdAt: "invalid" }],
104-
agentMessages: [{ id: "agent", text: "Done", timestamp: 200 }],
105-
});
172+
expect(timeline.map((row) => row.message.id)).toEqual(["human", "broken"]);
173+
});
106174

107-
expect(timeline.map((row) => row.kind)).toEqual([
108-
"prompt",
109-
"agent",
110-
"human",
175+
it("exposes the artifact and the source message on artifact rows", () => {
176+
const [row] = buildThreadTimeline([
177+
{
178+
id: "pr",
179+
content: "Pull request opened",
180+
created_at: "1970-01-01T00:00:00.200Z",
181+
author_kind: "agent",
182+
event: "pr_created",
183+
payload: { pr_url: "https://github.com/org/repo/pull/1" },
184+
},
111185
]);
186+
187+
expect(row).toMatchObject({
188+
kind: "artifact",
189+
artifact: { kind: "pr", url: "https://github.com/org/repo/pull/1" },
190+
message: { id: "pr" },
191+
});
112192
});
113193
});
114194

0 commit comments

Comments
 (0)