Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions packages/ui/src/features/canvas/components/ActivityPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
CaretRightIcon,
XIcon,
} from "@phosphor-icons/react";
import { Button, cn, Tabs, TabsList, TabsTrigger } from "@posthog/quill";
import { Button, Tabs, TabsList, TabsTrigger } from "@posthog/quill";
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
import type { Task } from "@posthog/shared/domain-types";
import { ActivityTimeline } from "@posthog/ui/features/canvas/components/ActivityTimeline";
Expand Down Expand Up @@ -35,9 +35,6 @@ const TABS_WITH_COMPOSER: ReadonlySet<ActivityTab> = new Set([
"comments",
]);

const TIMESTAMP_END_CLASS =
"[&_[data-slot=thread-item-timestamp]]:ml-auto [&_[data-slot=thread-item-timestamp]]:shrink-0 [&_[data-slot=thread-item-timestamp]]:pl-2";

/** The 32px row this panel leads with: the tabs are the header, so the strip
* lines up with the tab bar of the pane on its left (TabbedPanel) and the
* review toolbar, which are the same fixed height and border. */
Expand Down Expand Up @@ -119,13 +116,15 @@ function ActivityConversation({
onToggleCollapsed,
onOpenFull,
showTaskSummary,
canOpenInPlace,
}: {
task: Task;
channelId: string;
onClose?: () => void;
onToggleCollapsed?: () => void;
onOpenFull?: () => void;
showTaskSummary: boolean;
canOpenInPlace?: boolean;
}) {
const taskId = task.id;
const {
Expand Down Expand Up @@ -183,7 +182,13 @@ function ActivityConversation({

const body = () => {
if (tab === "artifacts") {
return <TaskArtifactsList task={task} timeline={timeline} />;
return (
<TaskArtifactsList
task={task}
timeline={timeline}
canOpenInPlace={canOpenInPlace}
/>
);
}
if (tab === "comments") {
return (
Expand All @@ -209,19 +214,15 @@ function ActivityConversation({
currentUserEmail={currentUser?.email}
isTaskAuthor={isTaskAuthor}
canForward={canForward}
canOpenInPlace={canOpenInPlace}
onSendToAgent={sendMessageToAgent}
onDelete={deleteMessage}
/>
);
};

return (
<div
className={cn(
"flex h-full min-w-0 flex-col bg-gray-1",
TIMESTAMP_END_CLASS,
)}
>
<div className="flex h-full min-w-0 flex-col bg-gray-1">
<ActivityHeader
tab={tab}
onTabChange={handleTabChange}
Expand Down Expand Up @@ -269,6 +270,7 @@ export function ActivityPanel({
onToggleCollapsed,
onOpenFull,
showTaskSummary = true,
canOpenInPlace,
}: {
taskId: string;
channelId: string;
Expand All @@ -278,6 +280,7 @@ export function ActivityPanel({
onToggleCollapsed?: () => void;
onOpenFull?: () => void;
showTaskSummary?: boolean;
canOpenInPlace?: boolean;
}) {
const { data: fetchedTask } = useQuery({
...taskDetailQuery(taskId),
Expand Down Expand Up @@ -315,6 +318,7 @@ export function ActivityPanel({
onToggleCollapsed={onToggleCollapsed}
onOpenFull={onOpenFull}
showTaskSummary={showTaskSummary}
canOpenInPlace={canOpenInPlace}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Task } from "@posthog/shared/domain-types";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("@posthog/ui/features/git-interaction/usePrDetails", () => ({
usePrDetails: () => ({
meta: { state: "open", merged: false, draft: false },
}),
}));

import { useThreadNavigationStore } from "@posthog/ui/features/sessions/threadNavigationStore";
import { ActivityTimeline } from "./ActivityTimeline";

const task = {
id: "task-1",
created_at: "2026-07-17T09:00:00Z",
updated_at: "2026-07-17T09:00:00Z",
created_by: { uuid: "u1", first_name: "Shy", last_name: "Alter" },
latest_run: null,
} as unknown as Task;

// Every conversation row is attributed to the task's creator, which is exactly why
// an author-derived accessible name would be identical on all of them.
const conversationItems = [
{
type: "user_message" as const,
id: "turn-1-1-user",
content: "first thing\nand more detail",
timestamp: Date.parse("2026-07-17T09:05:00Z"),
},
{
type: "user_message" as const,
id: "turn-2-2-user",
content: "second thing",
timestamp: Date.parse("2026-07-17T09:10:00Z"),
},
];

function renderTimeline(canOpenInPlace?: boolean) {
return render(
<ActivityTimeline
task={task}
timeline={[]}
// biome-ignore lint/suspicious/noExplicitAny: narrow fixture for the rows under test
conversationItems={conversationItems as any}
isTaskAuthor
canForward={false}
canOpenInPlace={canOpenInPlace}
onSendToAgent={() => {}}
onDelete={() => {}}
/>,
);
}

beforeEach(() => {
useThreadNavigationStore.setState({ scrollRequests: {} });
});

describe("ActivityTimeline", () => {
it("names each message row by its own content, not a shared template", () => {
renderTimeline(true);

expect(screen.getAllByRole("button")).toHaveLength(2);
// `name` here is the computed accessible name, so this is what a screen reader
// announces: each row carries the content a sighted user sees, which is what
// makes the two distinguishable — the author is the same on every row.
expect(
screen.getByRole("button", { name: /Shy Alter.*first thing/ }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Shy Alter.*second thing/ }),
).toBeInTheDocument();
// The avatar is decorative, so its initials stay out of the name.
expect(screen.queryByRole("button", { name: /SA/ })).toBeNull();
});

it("asks the transcript to scroll to the clicked message", () => {
renderTimeline(true);

fireEvent.click(screen.getAllByRole("button")[1]);

expect(useThreadNavigationStore.getState().scrollRequests["task-1"]).toBe(
"turn-2-2-user",
);
});

it("leaves rows inert with no transcript alongside to drive", () => {
renderTimeline();

expect(screen.queryAllByRole("button")).toHaveLength(0);
expect(screen.getByText(/first thing/)).toBeInTheDocument();
});
});
Loading
Loading