Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 3aecd7b

Browse files
Merging 17de0f3 into trunk-temp/pr-3910/bc6be952-7e2a-45cd-9590-1a43de53b758
2 parents 925b067 + 17de0f3 commit 3aecd7b

15 files changed

Lines changed: 582 additions & 145 deletions

File tree

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
CaretRightIcon,
44
XIcon,
55
} from "@phosphor-icons/react";
6-
import { Button, cn, Tabs, TabsList, TabsTrigger } from "@posthog/quill";
6+
import { Button, Tabs, TabsList, TabsTrigger } from "@posthog/quill";
77
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
88
import type { Task } from "@posthog/shared/domain-types";
99
import { ActivityTimeline } from "@posthog/ui/features/canvas/components/ActivityTimeline";
@@ -35,9 +35,6 @@ const TABS_WITH_COMPOSER: ReadonlySet<ActivityTab> = new Set([
3535
"comments",
3636
]);
3737

38-
const TIMESTAMP_END_CLASS =
39-
"[&_[data-slot=thread-item-timestamp]]:ml-auto [&_[data-slot=thread-item-timestamp]]:shrink-0 [&_[data-slot=thread-item-timestamp]]:pl-2";
40-
4138
/** The 32px row this panel leads with: the tabs are the header, so the strip
4239
* lines up with the tab bar of the pane on its left (TabbedPanel) and the
4340
* review toolbar, which are the same fixed height and border. */
@@ -119,13 +116,15 @@ function ActivityConversation({
119116
onToggleCollapsed,
120117
onOpenFull,
121118
showTaskSummary,
119+
canOpenInPlace,
122120
}: {
123121
task: Task;
124122
channelId: string;
125123
onClose?: () => void;
126124
onToggleCollapsed?: () => void;
127125
onOpenFull?: () => void;
128126
showTaskSummary: boolean;
127+
canOpenInPlace?: boolean;
129128
}) {
130129
const taskId = task.id;
131130
const {
@@ -183,7 +182,13 @@ function ActivityConversation({
183182

184183
const body = () => {
185184
if (tab === "artifacts") {
186-
return <TaskArtifactsList task={task} timeline={timeline} />;
185+
return (
186+
<TaskArtifactsList
187+
task={task}
188+
timeline={timeline}
189+
canOpenInPlace={canOpenInPlace}
190+
/>
191+
);
187192
}
188193
if (tab === "comments") {
189194
return (
@@ -209,19 +214,15 @@ function ActivityConversation({
209214
currentUserEmail={currentUser?.email}
210215
isTaskAuthor={isTaskAuthor}
211216
canForward={canForward}
217+
canOpenInPlace={canOpenInPlace}
212218
onSendToAgent={sendMessageToAgent}
213219
onDelete={deleteMessage}
214220
/>
215221
);
216222
};
217223

218224
return (
219-
<div
220-
className={cn(
221-
"flex h-full min-w-0 flex-col bg-gray-1",
222-
TIMESTAMP_END_CLASS,
223-
)}
224-
>
225+
<div className="flex h-full min-w-0 flex-col bg-gray-1">
225226
<ActivityHeader
226227
tab={tab}
227228
onTabChange={handleTabChange}
@@ -269,6 +270,7 @@ export function ActivityPanel({
269270
onToggleCollapsed,
270271
onOpenFull,
271272
showTaskSummary = true,
273+
canOpenInPlace,
272274
}: {
273275
taskId: string;
274276
channelId: string;
@@ -278,6 +280,7 @@ export function ActivityPanel({
278280
onToggleCollapsed?: () => void;
279281
onOpenFull?: () => void;
280282
showTaskSummary?: boolean;
283+
canOpenInPlace?: boolean;
281284
}) {
282285
const { data: fetchedTask } = useQuery({
283286
...taskDetailQuery(taskId),
@@ -315,6 +318,7 @@ export function ActivityPanel({
315318
onToggleCollapsed={onToggleCollapsed}
316319
onOpenFull={onOpenFull}
317320
showTaskSummary={showTaskSummary}
321+
canOpenInPlace={canOpenInPlace}
318322
/>
319323
);
320324
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { Task } from "@posthog/shared/domain-types";
2+
import { fireEvent, render, screen } from "@testing-library/react";
3+
import { beforeEach, describe, expect, it, vi } from "vitest";
4+
5+
vi.mock("@posthog/ui/features/git-interaction/usePrDetails", () => ({
6+
usePrDetails: () => ({
7+
meta: { state: "open", merged: false, draft: false },
8+
}),
9+
}));
10+
11+
import { useThreadNavigationStore } from "@posthog/ui/features/sessions/threadNavigationStore";
12+
import { ActivityTimeline } from "./ActivityTimeline";
13+
14+
const task = {
15+
id: "task-1",
16+
created_at: "2026-07-17T09:00:00Z",
17+
updated_at: "2026-07-17T09:00:00Z",
18+
created_by: { uuid: "u1", first_name: "Shy", last_name: "Alter" },
19+
latest_run: null,
20+
} as unknown as Task;
21+
22+
// Every conversation row is attributed to the task's creator, which is exactly why
23+
// an author-derived accessible name would be identical on all of them.
24+
const conversationItems = [
25+
{
26+
type: "user_message" as const,
27+
id: "turn-1-1-user",
28+
content: "first thing\nand more detail",
29+
timestamp: Date.parse("2026-07-17T09:05:00Z"),
30+
},
31+
{
32+
type: "user_message" as const,
33+
id: "turn-2-2-user",
34+
content: "second thing",
35+
timestamp: Date.parse("2026-07-17T09:10:00Z"),
36+
},
37+
];
38+
39+
function renderTimeline(canOpenInPlace?: boolean) {
40+
return render(
41+
<ActivityTimeline
42+
task={task}
43+
timeline={[]}
44+
// biome-ignore lint/suspicious/noExplicitAny: narrow fixture for the rows under test
45+
conversationItems={conversationItems as any}
46+
isTaskAuthor
47+
canForward={false}
48+
canOpenInPlace={canOpenInPlace}
49+
onSendToAgent={() => {}}
50+
onDelete={() => {}}
51+
/>,
52+
);
53+
}
54+
55+
beforeEach(() => {
56+
useThreadNavigationStore.setState({ scrollRequests: {} });
57+
});
58+
59+
describe("ActivityTimeline", () => {
60+
it("names each message row by its own content, not a shared template", () => {
61+
renderTimeline(true);
62+
63+
expect(screen.getAllByRole("button")).toHaveLength(2);
64+
// `name` here is the computed accessible name, so this is what a screen reader
65+
// announces: each row carries the content a sighted user sees, which is what
66+
// makes the two distinguishable — the author is the same on every row.
67+
expect(
68+
screen.getByRole("button", { name: /Shy Alter.*first thing/ }),
69+
).toBeInTheDocument();
70+
expect(
71+
screen.getByRole("button", { name: /Shy Alter.*second thing/ }),
72+
).toBeInTheDocument();
73+
// The avatar is decorative, so its initials stay out of the name.
74+
expect(screen.queryByRole("button", { name: /SA/ })).toBeNull();
75+
});
76+
77+
it("asks the transcript to scroll to the clicked message", () => {
78+
renderTimeline(true);
79+
80+
fireEvent.click(screen.getAllByRole("button")[1]);
81+
82+
expect(useThreadNavigationStore.getState().scrollRequests["task-1"]).toBe(
83+
"turn-2-2-user",
84+
);
85+
});
86+
87+
it("leaves rows inert with no transcript alongside to drive", () => {
88+
renderTimeline();
89+
90+
expect(screen.queryAllByRole("button")).toHaveLength(0);
91+
expect(screen.getByText(/first thing/)).toBeInTheDocument();
92+
});
93+
});

0 commit comments

Comments
 (0)