diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index cfbdd290b1..b7488a0054 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -116,6 +116,30 @@ snapshots: hash: v1.k4693efd2.1d199b2c4bba8034cb18fb5b49866b64a6eb3311591add5f58ec288710dce2e9.FCc9egSaO1Onih417yciSYKNROe8zqDZyyS6xuXCZGw billing-usagemeter--zero-spend-limit--light: hash: v1.k4693efd2.b50c18736bbde45fc89f4c0ac7fd616286e5c1ecc2a56910fffca13eaa13d8a2.31z4F0JmvMeR4yqk5pjs_OQ3MypnM2miZI9al4wuc8c + channels-taskfeedrow--agent-origin--dark: + hash: v1.k4693efd2.82f8c70a399c9ea768201933e202fcc2fde74332c5153aeabf6835ace79beee6.-SVboDRIZ_-nh4PtMliCv7e7iCtdUm-1m2xQ9Xzcdfw + channels-taskfeedrow--agent-origin--light: + hash: v1.k4693efd2.4c1db3470d5ec9b4db9872d0e048314d0078979e8fbdb602b8c648ba70d7209f._KPUvj46p33u7A4Xndz-git-4rp7VKca1MpeTztlB6w + channels-taskfeedrow--human-email-only--dark: + hash: v1.k4693efd2.0f4dece4643b7375b77746347658f4fe65b37817cdfa9ad1ba697067e619f428.f0jKZ4OYqsmeJjXffjt-amK9cwE81JF0lgSMqtn37PA + channels-taskfeedrow--human-email-only--light: + hash: v1.k4693efd2.14db0510f7ea5c57985cf0cb60ab08f5e146c7243eeb1b0b963f74e85c7d60f4.ijHQLK0lZjjUlOQuzYzisImU9DWsNmZM7i5GYfWU4E0 + channels-taskfeedrow--human-started--dark: + hash: v1.k4693efd2.e0abdae2e8ac2ef29793ea7afc2a2580c6610e07a3c640ad782af7fd3c12592a.YUgF_BTPzZoqnWXvyLBRs3w9C_P3Yfh4-cP4mUqs2NQ + channels-taskfeedrow--human-started--light: + hash: v1.k4693efd2.84c26deb1a587fe061238b3982b555167575893bacc9cd2667d4d3f74646261f.abqc0Voe6FIQFflzDJTv1KewcZ4XxcDz1a0mFlJu7oM + channels-taskfeedrow--long-prompt--dark: + hash: v1.k4693efd2.2547e88d76889aa7290c5221ff5ecb674cdb1cd9f1ae5efd211fb648a88037b6.4c9RO1upuz3VijYp2xU2MTNrW_87zWQLX5kRKCYABq4 + channels-taskfeedrow--long-prompt--light: + hash: v1.k4693efd2.6b0f609e0155bc3b6e3149714b70d33ef83f7c2ca7b72e6d3a495b8db36828d0.eC5ZQDilDw7Y9EJqhfIdlk186GJjgGbYolZ9cm9Xtt0 + channels-taskfeedrow--no-prompt--dark: + hash: v1.k4693efd2.9fa967f1a9acdeba0c50a9e45ae649f118ee26938037dbefd1bb0577067b03d4.va1lGsscqLW86-5yCKc3H6pQ8Az7_HBItkgGTnTQiYU + channels-taskfeedrow--no-prompt--light: + hash: v1.k4693efd2.a02339aecdb6fc327fdf6586e28ceeebf8490442fe217eb8bb46f1ce66bff78a.2l7XdvdW3D0GlLmPwDdTFgjZWdav68bZX_LYResVKwo + channels-taskfeedrow--no-starter--dark: + hash: v1.k4693efd2.66fff212f14afa9dd3bc532698a042383014a0cabd4331d30eff878148e1773d.RiectZxFTqk1husLYI-UV5ko1uZ4lpbjWstE7Hy3MJE + channels-taskfeedrow--no-starter--light: + hash: v1.k4693efd2.9ee4ad64ed7d2c5c62d0cd682d903b09da643d462d49aae0858afa500e92256b.OhGj64l-o7iWtxzJo1qt0A_CU9ME0ZRr_5e455RGUJQ components-permissions-permissionselector--create-new-file--dark: hash: v1.k4693efd2.c54203a4e636b83b3d24d7ed9c4ace8659db87cd8f231d9dc2ecc03320e31646.epDm7LebiLzlp0uuZBrE-Obt_anAn0xsE8bHFnm5vos components-permissions-permissionselector--create-new-file--light: diff --git a/packages/ui/src/features/canvas/components/ChannelFeedView.stories.tsx b/packages/ui/src/features/canvas/components/ChannelFeedView.stories.tsx new file mode 100644 index 0000000000..8ff99ea14a --- /dev/null +++ b/packages/ui/src/features/canvas/components/ChannelFeedView.stories.tsx @@ -0,0 +1,111 @@ +import type { Task, UserBasic } from "@posthog/shared/domain-types"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { TaskFeedRow } from "./ChannelFeedView"; + +function MockTaskCard({ title }: { title: string }) { + return ( +
+
+ {title} + + Ready + +
+
+ ); +} + +const user = (overrides: Partial = {}): UserBasic => ({ + id: 1, + uuid: "user-1", + email: "adam@posthog.com", + first_name: "Adam", + last_name: "Bowker", + ...overrides, +}); + +const task = (overrides: Partial = {}): Task => ({ + id: "task-1", + task_number: 1, + slug: "task-1", + title: "Add feedback modal to channels view", + description: + "Add a feedback modal to the channels view so people can share thoughts without leaving the feed", + created_at: "2026-07-17T12:00:00.000Z", + updated_at: "2026-07-17T12:00:00.000Z", + origin_product: "user_created", + created_by: user(), + ...overrides, +}); + +const meta: Meta = { + title: "Channels/TaskFeedRow", + component: TaskFeedRow, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const HumanStarted: Story = { + args: { + task: task(), + children: , + }, +}; + +export const HumanEmailOnly: Story = { + args: { + task: task({ + created_by: user({ first_name: undefined, last_name: undefined }), + title: "Make background color configurable", + description: "Make the channel background color configurable in settings", + }), + children: , + }, +}; + +export const AgentOrigin: Story = { + args: { + task: task({ + origin_product: "slack", + title: "Investigate signup drop-off", + description: "Investigate the signup drop-off we saw over the weekend", + }), + children: , + }, +}; + +export const LongPrompt: Story = { + args: { + task: task({ + description: + "Rework the channel feed so each row reads as the person who started the task rather than the agent, show a preview of their prompt under the header, keep the task card below, and make sure long prompts truncate cleanly instead of pushing the card down the feed", + }), + children: , + }, +}; + +export const NoPrompt: Story = { + args: { + task: task({ description: "", title: "Untitled task" }), + children: , + }, +}; + +export const NoStarter: Story = { + args: { + task: task({ + created_by: null, + title: "Untitled task", + description: "Summarize this week's shipped changes", + }), + children: , + }, +}; diff --git a/packages/ui/src/features/canvas/components/ChannelFeedView.tsx b/packages/ui/src/features/canvas/components/ChannelFeedView.tsx index af127b60ab..1905a1aa95 100644 --- a/packages/ui/src/features/canvas/components/ChannelFeedView.tsx +++ b/packages/ui/src/features/canvas/components/ChannelFeedView.tsx @@ -5,6 +5,7 @@ import { RobotIcon, } from "@phosphor-icons/react"; import { taskFeedRunStatus } from "@posthog/core/canvas/channelFeed"; +import { xmlToPlainText } from "@posthog/core/message-editor/content"; import { Avatar, AvatarFallback, @@ -37,10 +38,13 @@ import { useChatMessageScroller, } from "@posthog/quill"; import { formatRelativeTimeShort, getLocalDayDiff } from "@posthog/shared"; -import type { Task, TaskRunStatus } from "@posthog/shared/domain-types"; +import type { + Task, + TaskRunStatus, + UserBasic, +} from "@posthog/shared/domain-types"; import { getUserInitials } from "@posthog/ui/features/auth/userInitials"; import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon"; -import { mentionChipClass } from "@posthog/ui/features/canvas/components/MentionText"; import type { ChannelFeedSystemMessage } from "@posthog/ui/features/canvas/hooks/useChannelFeedMessages"; import { useChannelTaskData } from "@posthog/ui/features/canvas/hooks/useChannelTaskData"; import { useTaskThread } from "@posthog/ui/features/canvas/hooks/useTaskThread"; @@ -411,33 +415,43 @@ function ReplyFooter({ ); } -const FeedItem = memo(function FeedItem({ +function channelTaskStarter(task: Task): UserBasic | null { + return task.origin_product === "user_created" + ? (task.created_by ?? null) + : null; +} + +export function TaskFeedRow({ task, - channelId, - inView, - onOpenTask, - onOpenThread, + actions, + children, }: { task: Task; - channelId: string; - inView: boolean; - onOpenTask: (task: Task) => void; - onOpenThread: (task: Task) => void; + actions?: ReactNode; + children?: ReactNode; }) { + const starter = channelTaskStarter(task); + const prompt = useMemo( + () => xmlToPlainText(task.description ?? "").trim(), + [task.description], + ); + return ( - + {starter ? getUserInitials(starter) : } - PostHog - Agent + + {starter ? userDisplayName(starter) : "PostHog"} + + {!starter && Agent} @@ -445,47 +459,60 @@ const FeedItem = memo(function FeedItem({ - - {/* Only attribute channel-started tasks: other origins (Slack, - automations) carry a created_by who didn't start it here. */} - {task.origin_product === "user_created" && task.created_by ? ( - <> - {/* Mention-styled but rendered inert: the starter shouldn't be - notified about their own task. */} - - @{userDisplayName(task.created_by)} - {" "} - started a new task - - ) : ( - "A new task was started" - )} + + {prompt || + (starter ? "started a new task" : "A new task was started")} - onOpenThread(task)} - /> - onOpenThread(task)} - /> + {children} - {/* Replying now lives in the always-visible ReplyFooter, so the hover - toolbar only carries the distinct "Open task" action. Actions anchor - to the row's top-right corner; a top tooltip there overhangs the panel - edge and gets clipped by the scroll container, so open tooltips toward - the content instead. */} - - onOpenTask(task)}> - - - + {actions} ); +} + +const FeedItem = memo(function FeedItem({ + task, + channelId, + inView, + onOpenTask, + onOpenThread, +}: { + task: Task; + channelId: string; + inView: boolean; + onOpenTask: (task: Task) => void; + onOpenThread: (task: Task) => void; +}) { + return ( + + onOpenTask(task)}> + + + + } + > + onOpenThread(task)} + /> + onOpenThread(task)} + /> + + ); }); // One feed row: owns the scroller item (the `content-visibility` boundary, so