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