Skip to content

Commit 2f661a1

Browse files
authored
refactor(canvas): align thread UX with architecture
Generated-By: PostHog Code Task-Id: 87a79e23-20c4-440b-818b-28154924ffbc
1 parent 021a701 commit 2f661a1

14 files changed

Lines changed: 223 additions & 198 deletions

File tree

apps/code/src/renderer/desktop-contributions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { agentChatCoreModule } from "@posthog/core/agent-chat/agentChat.module";
22
import { autoresearchCoreModule } from "@posthog/core/autoresearch/autoresearch.module";
33
import { billingCoreModule } from "@posthog/core/billing/billing.module";
4+
import { taskThreadCoreModule } from "@posthog/core/canvas/taskThread.module";
45
import { inboxCoreModule } from "@posthog/core/inbox/inbox.module";
56
import { githubConnectModule } from "@posthog/core/integrations/githubConnect.module";
67
import { onboardingModule } from "@posthog/core/onboarding/onboarding.module";
@@ -36,6 +37,7 @@ export function registerDesktopContributions(): void {
3637
autoresearchCoreModule,
3738
billingUiModule,
3839
billingCoreModule,
40+
taskThreadCoreModule,
3941
browserTabsUiModule,
4042
cloneUiModule,
4143
connectivityUiModule,

apps/web/src/web-container.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "reflect-metadata";
22
import { TypedContainer } from "@inversifyjs/strongly-typed";
3+
import { taskThreadCoreModule } from "@posthog/core/canvas/taskThread.module";
34
import { setRootContainer } from "@posthog/di/container";
45
import { ROOT_LOGGER, type RootLogger } from "@posthog/di/logger";
56
import {
@@ -96,4 +97,6 @@ container.bind(MCP_SANDBOX_PROXY_URL).toConstantValue(() => {
9697
return sandboxProxyUrl;
9798
});
9899

100+
container.load(taskThreadCoreModule);
101+
99102
setRootContainer(container);

packages/api-client/src/posthog-client.test.ts

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { TaskThreadMessage } from "@posthog/shared/domain-types";
21
import { describe, expect, it, vi } from "vitest";
32
import { PostHogAPIClient } from "./posthog-client";
43

@@ -1697,66 +1696,3 @@ describe("PostHogAPIClient", () => {
16971696
});
16981697
});
16991698
});
1700-
1701-
describe("task thread messages", () => {
1702-
function message(
1703-
overrides: Partial<TaskThreadMessage> = {},
1704-
): TaskThreadMessage {
1705-
return {
1706-
id: "message-id",
1707-
task: "task-id",
1708-
content: "@agent investigate this",
1709-
created_at: "2026-07-16T00:00:00Z",
1710-
...overrides,
1711-
};
1712-
}
1713-
1714-
it("creates a message before forwarding it to the agent", async () => {
1715-
const client = new PostHogAPIClient(
1716-
"http://localhost:8000",
1717-
async () => "token",
1718-
async () => "token",
1719-
123,
1720-
);
1721-
const create = vi
1722-
.spyOn(client, "createTaskThreadMessage")
1723-
.mockResolvedValue(message());
1724-
const send = vi
1725-
.spyOn(client, "sendTaskThreadMessageToAgent")
1726-
.mockResolvedValue(
1727-
message({ forwarded_to_agent_at: "2026-07-16T00:00:01Z" }),
1728-
);
1729-
1730-
await client.createTaskThreadMessageForAgent(
1731-
"task-id",
1732-
"@agent investigate this",
1733-
);
1734-
1735-
expect(create).toHaveBeenCalledWith("task-id", "@agent investigate this");
1736-
expect(send).toHaveBeenCalledWith("task-id", "message-id");
1737-
expect(create.mock.invocationCallOrder[0]).toBeLessThan(
1738-
send.mock.invocationCallOrder[0],
1739-
);
1740-
});
1741-
1742-
it("returns the created message when forwarding fails", async () => {
1743-
const client = new PostHogAPIClient(
1744-
"http://localhost:8000",
1745-
async () => "token",
1746-
async () => "token",
1747-
123,
1748-
);
1749-
const sendError = new Error("No active run");
1750-
vi.spyOn(client, "createTaskThreadMessage").mockResolvedValue(message());
1751-
vi.spyOn(client, "sendTaskThreadMessageToAgent").mockRejectedValue(
1752-
sendError,
1753-
);
1754-
1755-
await expect(
1756-
client.createTaskThreadMessageForAgent(
1757-
"task-id",
1758-
"@agent investigate this",
1759-
),
1760-
).resolves.toEqual({ message: message(), sendError });
1761-
});
1762-
});

packages/api-client/src/posthog-client.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,21 +2630,6 @@ export class PostHogAPIClient {
26302630
return (await response.json()) as TaskThreadMessage;
26312631
}
26322632

2633-
async createTaskThreadMessageForAgent(
2634-
taskId: string,
2635-
content: string,
2636-
): Promise<{ message: TaskThreadMessage; sendError: unknown | null }> {
2637-
const message = await this.createTaskThreadMessage(taskId, content);
2638-
try {
2639-
return {
2640-
message: await this.sendTaskThreadMessageToAgent(taskId, message.id),
2641-
sendError: null,
2642-
};
2643-
} catch (sendError) {
2644-
return { message, sendError };
2645-
}
2646-
}
2647-
26482633
async deleteTaskThreadMessage(
26492634
taskId: string,
26502635
messageId: string,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ContainerModule } from "inversify";
2+
import { TASK_THREAD_SERVICE, TaskThreadService } from "./taskThreadService";
3+
4+
export const taskThreadCoreModule = new ContainerModule(({ bind }) => {
5+
bind(TaskThreadService).toSelf().inSingletonScope();
6+
bind(TASK_THREAD_SERVICE).toService(TaskThreadService);
7+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { PostHogAPIClient } from "@posthog/api-client/posthog-client";
2+
import type { TaskThreadMessage } from "@posthog/shared/domain-types";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { TaskThreadService } from "./taskThreadService";
5+
6+
function message(
7+
overrides: Partial<TaskThreadMessage> = {},
8+
): TaskThreadMessage {
9+
return {
10+
id: "message-id",
11+
task: "task-id",
12+
content: "@agent investigate this",
13+
created_at: "2026-07-16T00:00:00Z",
14+
...overrides,
15+
};
16+
}
17+
18+
function client(overrides: Partial<PostHogAPIClient> = {}): PostHogAPIClient {
19+
return overrides as PostHogAPIClient;
20+
}
21+
22+
describe("TaskThreadService", () => {
23+
it("creates a message before forwarding it to the agent", async () => {
24+
const createTaskThreadMessage = vi.fn().mockResolvedValue(message());
25+
const sendTaskThreadMessageToAgent = vi
26+
.fn()
27+
.mockResolvedValue(
28+
message({ forwarded_to_agent_at: "2026-07-16T00:00:01Z" }),
29+
);
30+
const service = new TaskThreadService();
31+
32+
await service.postMessageToAgent(
33+
client({ createTaskThreadMessage, sendTaskThreadMessageToAgent }),
34+
"task-id",
35+
"@agent investigate this",
36+
);
37+
38+
expect(createTaskThreadMessage).toHaveBeenCalledWith(
39+
"task-id",
40+
"@agent investigate this",
41+
);
42+
expect(sendTaskThreadMessageToAgent).toHaveBeenCalledWith(
43+
"task-id",
44+
"message-id",
45+
);
46+
expect(createTaskThreadMessage.mock.invocationCallOrder[0]).toBeLessThan(
47+
sendTaskThreadMessageToAgent.mock.invocationCallOrder[0],
48+
);
49+
});
50+
51+
it("returns the created message when forwarding fails", async () => {
52+
const sendError = new Error("No active run");
53+
const service = new TaskThreadService();
54+
55+
await expect(
56+
service.postMessageToAgent(
57+
client({
58+
createTaskThreadMessage: vi.fn().mockResolvedValue(message()),
59+
sendTaskThreadMessageToAgent: vi.fn().mockRejectedValue(sendError),
60+
}),
61+
"task-id",
62+
"@agent investigate this",
63+
),
64+
).resolves.toEqual({ message: message(), sendError });
65+
});
66+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { PostHogAPIClient } from "@posthog/api-client/posthog-client";
2+
import type { TaskThreadMessage } from "@posthog/shared/domain-types";
3+
import { injectable } from "inversify";
4+
5+
export const TASK_THREAD_SERVICE = Symbol.for(
6+
"posthog.core.canvas.taskThreadService",
7+
);
8+
9+
export interface PostMessageToAgentResult {
10+
message: TaskThreadMessage;
11+
sendError: unknown | null;
12+
}
13+
14+
@injectable()
15+
export class TaskThreadService {
16+
async postMessageToAgent(
17+
client: PostHogAPIClient,
18+
taskId: string,
19+
content: string,
20+
): Promise<PostMessageToAgentResult> {
21+
const message = await client.createTaskThreadMessage(taskId, content);
22+
try {
23+
return {
24+
message: await client.sendTaskThreadMessageToAgent(taskId, message.id),
25+
sendError: null,
26+
};
27+
} catch (sendError) {
28+
return { message, sendError };
29+
}
30+
}
31+
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,7 @@ function ReplyFooter({
396396
</AvatarFallback>
397397
</Avatar>
398398
</AvatarGroup>
399-
<ThreadItemRepliesLabel className="text-(--muted-foreground)">
400-
Reply
401-
</ThreadItemRepliesLabel>
399+
<ThreadItemRepliesLabel>Reply</ThreadItemRepliesLabel>
402400
</ThreadItemReplies>
403401
);
404402
}

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
filterComposerMentionCandidates,
1010
} from "@posthog/ui/features/canvas/utils/mentionComposer";
1111
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
12-
import { Text } from "@radix-ui/themes";
1312
import Mention, { type MentionNodeAttrs } from "@tiptap/extension-mention";
1413
import Placeholder from "@tiptap/extension-placeholder";
1514
import { EditorContent, useEditor } from "@tiptap/react";
@@ -274,19 +273,16 @@ export function MentionComposer({
274273
)}
275274
</AvatarFallback>
276275
</Avatar>
277-
<Text size="1" weight="medium" className="truncate">
276+
<span className="truncate font-medium text-xs">
278277
{candidate.kind === "agent"
279278
? "Agent"
280279
: userDisplayName(candidate.member)}
281-
</Text>
282-
<Text
283-
size="1"
284-
className="ml-auto shrink-0 truncate text-muted-foreground"
285-
>
280+
</span>
281+
<span className="ml-auto shrink-0 truncate text-muted-foreground text-xs">
286282
{candidate.kind === "agent"
287283
? "Send to agent"
288284
: candidate.member.email}
289-
</Text>
285+
</span>
290286
</button>
291287
))}
292288
</div>

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import { Theme } from "@radix-ui/themes";
21
import { render, screen } from "@testing-library/react";
32
import { describe, expect, it } from "vitest";
43
import { MentionText } from "./MentionText";
54

65
describe("MentionText", () => {
76
it("uses the shared mention styles and emphasizes the current user", () => {
87
render(
9-
<Theme>
10-
<MentionText
11-
content="@[Alice](alice@example.com) and @[Bob](bob@example.com)"
12-
currentUserEmail="bob@example.com"
13-
/>
14-
</Theme>,
8+
<MentionText
9+
content="@[Alice](alice@example.com) and @[Bob](bob@example.com)"
10+
currentUserEmail="bob@example.com"
11+
/>,
1512
);
1613

1714
expect(screen.getByText("@Alice")).toHaveClass("mention-chip");

0 commit comments

Comments
 (0)