Skip to content

Commit d55d24e

Browse files
adboioposthog[bot]
andauthored
Attribute channel task rows to the human who started them (#3548)
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: adboio <23323033+adboio@users.noreply.github.com>
1 parent 351fd79 commit d55d24e

3 files changed

Lines changed: 211 additions & 49 deletions

File tree

apps/code/snapshots.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,30 @@ snapshots:
116116
hash: v1.k4693efd2.1d199b2c4bba8034cb18fb5b49866b64a6eb3311591add5f58ec288710dce2e9.FCc9egSaO1Onih417yciSYKNROe8zqDZyyS6xuXCZGw
117117
billing-usagemeter--zero-spend-limit--light:
118118
hash: v1.k4693efd2.b50c18736bbde45fc89f4c0ac7fd616286e5c1ecc2a56910fffca13eaa13d8a2.31z4F0JmvMeR4yqk5pjs_OQ3MypnM2miZI9al4wuc8c
119+
channels-taskfeedrow--agent-origin--dark:
120+
hash: v1.k4693efd2.82f8c70a399c9ea768201933e202fcc2fde74332c5153aeabf6835ace79beee6.-SVboDRIZ_-nh4PtMliCv7e7iCtdUm-1m2xQ9Xzcdfw
121+
channels-taskfeedrow--agent-origin--light:
122+
hash: v1.k4693efd2.4c1db3470d5ec9b4db9872d0e048314d0078979e8fbdb602b8c648ba70d7209f._KPUvj46p33u7A4Xndz-git-4rp7VKca1MpeTztlB6w
123+
channels-taskfeedrow--human-email-only--dark:
124+
hash: v1.k4693efd2.0f4dece4643b7375b77746347658f4fe65b37817cdfa9ad1ba697067e619f428.f0jKZ4OYqsmeJjXffjt-amK9cwE81JF0lgSMqtn37PA
125+
channels-taskfeedrow--human-email-only--light:
126+
hash: v1.k4693efd2.14db0510f7ea5c57985cf0cb60ab08f5e146c7243eeb1b0b963f74e85c7d60f4.ijHQLK0lZjjUlOQuzYzisImU9DWsNmZM7i5GYfWU4E0
127+
channels-taskfeedrow--human-started--dark:
128+
hash: v1.k4693efd2.e0abdae2e8ac2ef29793ea7afc2a2580c6610e07a3c640ad782af7fd3c12592a.YUgF_BTPzZoqnWXvyLBRs3w9C_P3Yfh4-cP4mUqs2NQ
129+
channels-taskfeedrow--human-started--light:
130+
hash: v1.k4693efd2.84c26deb1a587fe061238b3982b555167575893bacc9cd2667d4d3f74646261f.abqc0Voe6FIQFflzDJTv1KewcZ4XxcDz1a0mFlJu7oM
131+
channels-taskfeedrow--long-prompt--dark:
132+
hash: v1.k4693efd2.2547e88d76889aa7290c5221ff5ecb674cdb1cd9f1ae5efd211fb648a88037b6.4c9RO1upuz3VijYp2xU2MTNrW_87zWQLX5kRKCYABq4
133+
channels-taskfeedrow--long-prompt--light:
134+
hash: v1.k4693efd2.6b0f609e0155bc3b6e3149714b70d33ef83f7c2ca7b72e6d3a495b8db36828d0.eC5ZQDilDw7Y9EJqhfIdlk186GJjgGbYolZ9cm9Xtt0
135+
channels-taskfeedrow--no-prompt--dark:
136+
hash: v1.k4693efd2.9fa967f1a9acdeba0c50a9e45ae649f118ee26938037dbefd1bb0577067b03d4.va1lGsscqLW86-5yCKc3H6pQ8Az7_HBItkgGTnTQiYU
137+
channels-taskfeedrow--no-prompt--light:
138+
hash: v1.k4693efd2.a02339aecdb6fc327fdf6586e28ceeebf8490442fe217eb8bb46f1ce66bff78a.2l7XdvdW3D0GlLmPwDdTFgjZWdav68bZX_LYResVKwo
139+
channels-taskfeedrow--no-starter--dark:
140+
hash: v1.k4693efd2.66fff212f14afa9dd3bc532698a042383014a0cabd4331d30eff878148e1773d.RiectZxFTqk1husLYI-UV5ko1uZ4lpbjWstE7Hy3MJE
141+
channels-taskfeedrow--no-starter--light:
142+
hash: v1.k4693efd2.9ee4ad64ed7d2c5c62d0cd682d903b09da643d462d49aae0858afa500e92256b.OhGj64l-o7iWtxzJo1qt0A_CU9ME0ZRr_5e455RGUJQ
119143
components-permissions-permissionselector--create-new-file--dark:
120144
hash: v1.k4693efd2.c54203a4e636b83b3d24d7ed9c4ace8659db87cd8f231d9dc2ecc03320e31646.epDm7LebiLzlp0uuZBrE-Obt_anAn0xsE8bHFnm5vos
121145
components-permissions-permissionselector--create-new-file--light:
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { Task, UserBasic } from "@posthog/shared/domain-types";
2+
import type { Meta, StoryObj } from "@storybook/react-vite";
3+
import { TaskFeedRow } from "./ChannelFeedView";
4+
5+
function MockTaskCard({ title }: { title: string }) {
6+
return (
7+
<div className="mt-1.5 rounded-sm border border-border-primary px-3 py-2.5">
8+
<div className="flex items-center justify-between gap-2">
9+
<span className="font-medium text-sm">{title}</span>
10+
<span className="rounded-full bg-fill-secondary px-2 py-0.5 text-muted-foreground text-xs">
11+
Ready
12+
</span>
13+
</div>
14+
</div>
15+
);
16+
}
17+
18+
const user = (overrides: Partial<UserBasic> = {}): UserBasic => ({
19+
id: 1,
20+
uuid: "user-1",
21+
email: "adam@posthog.com",
22+
first_name: "Adam",
23+
last_name: "Bowker",
24+
...overrides,
25+
});
26+
27+
const task = (overrides: Partial<Task> = {}): Task => ({
28+
id: "task-1",
29+
task_number: 1,
30+
slug: "task-1",
31+
title: "Add feedback modal to channels view",
32+
description:
33+
"Add a feedback modal to the channels view so people can share thoughts without leaving the feed",
34+
created_at: "2026-07-17T12:00:00.000Z",
35+
updated_at: "2026-07-17T12:00:00.000Z",
36+
origin_product: "user_created",
37+
created_by: user(),
38+
...overrides,
39+
});
40+
41+
const meta: Meta<typeof TaskFeedRow> = {
42+
title: "Channels/TaskFeedRow",
43+
component: TaskFeedRow,
44+
decorators: [
45+
(Story) => (
46+
<div className="max-w-xl">
47+
<Story />
48+
</div>
49+
),
50+
],
51+
};
52+
53+
export default meta;
54+
type Story = StoryObj<typeof TaskFeedRow>;
55+
56+
export const HumanStarted: Story = {
57+
args: {
58+
task: task(),
59+
children: <MockTaskCard title="Add feedback modal to channels view" />,
60+
},
61+
};
62+
63+
export const HumanEmailOnly: Story = {
64+
args: {
65+
task: task({
66+
created_by: user({ first_name: undefined, last_name: undefined }),
67+
title: "Make background color configurable",
68+
description: "Make the channel background color configurable in settings",
69+
}),
70+
children: <MockTaskCard title="Make background color configurable" />,
71+
},
72+
};
73+
74+
export const AgentOrigin: Story = {
75+
args: {
76+
task: task({
77+
origin_product: "slack",
78+
title: "Investigate signup drop-off",
79+
description: "Investigate the signup drop-off we saw over the weekend",
80+
}),
81+
children: <MockTaskCard title="Investigate signup drop-off" />,
82+
},
83+
};
84+
85+
export const LongPrompt: Story = {
86+
args: {
87+
task: task({
88+
description:
89+
"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",
90+
}),
91+
children: <MockTaskCard title="Rework the channel feed attribution" />,
92+
},
93+
};
94+
95+
export const NoPrompt: Story = {
96+
args: {
97+
task: task({ description: "", title: "Untitled task" }),
98+
children: <MockTaskCard title="Untitled task" />,
99+
},
100+
};
101+
102+
export const NoStarter: Story = {
103+
args: {
104+
task: task({
105+
created_by: null,
106+
title: "Untitled task",
107+
description: "Summarize this week's shipped changes",
108+
}),
109+
children: <MockTaskCard title="Summarize this week's shipped changes" />,
110+
},
111+
};

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

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
RobotIcon,
66
} from "@phosphor-icons/react";
77
import { taskFeedRunStatus } from "@posthog/core/canvas/channelFeed";
8+
import { xmlToPlainText } from "@posthog/core/message-editor/content";
89
import {
910
Avatar,
1011
AvatarFallback,
@@ -37,10 +38,13 @@ import {
3738
useChatMessageScroller,
3839
} from "@posthog/quill";
3940
import { formatRelativeTimeShort, getLocalDayDiff } from "@posthog/shared";
40-
import type { Task, TaskRunStatus } from "@posthog/shared/domain-types";
41+
import type {
42+
Task,
43+
TaskRunStatus,
44+
UserBasic,
45+
} from "@posthog/shared/domain-types";
4146
import { getUserInitials } from "@posthog/ui/features/auth/userInitials";
4247
import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon";
43-
import { mentionChipClass } from "@posthog/ui/features/canvas/components/MentionText";
4448
import type { ChannelFeedSystemMessage } from "@posthog/ui/features/canvas/hooks/useChannelFeedMessages";
4549
import { useChannelTaskData } from "@posthog/ui/features/canvas/hooks/useChannelTaskData";
4650
import { useTaskThread } from "@posthog/ui/features/canvas/hooks/useTaskThread";
@@ -411,81 +415,104 @@ function ReplyFooter({
411415
);
412416
}
413417

414-
const FeedItem = memo(function FeedItem({
418+
function channelTaskStarter(task: Task): UserBasic | null {
419+
return task.origin_product === "user_created"
420+
? (task.created_by ?? null)
421+
: null;
422+
}
423+
424+
export function TaskFeedRow({
415425
task,
416-
channelId,
417-
inView,
418-
onOpenTask,
419-
onOpenThread,
426+
actions,
427+
children,
420428
}: {
421429
task: Task;
422-
channelId: string;
423-
inView: boolean;
424-
onOpenTask: (task: Task) => void;
425-
onOpenThread: (task: Task) => void;
430+
actions?: ReactNode;
431+
children?: ReactNode;
426432
}) {
433+
const starter = channelTaskStarter(task);
434+
const prompt = useMemo(
435+
() => xmlToPlainText(task.description ?? "").trim(),
436+
[task.description],
437+
);
438+
427439
return (
428440
<ThreadItem className="rounded-none py-1 pr-8 hover:bg-fill-hover/50">
429441
<ThreadItemGutter>
430442
<Avatar>
431443
<AvatarFallback>
432-
<RobotIcon size={16} />
444+
{starter ? getUserInitials(starter) : <RobotIcon size={16} />}
433445
</AvatarFallback>
434446
</Avatar>
435447
</ThreadItemGutter>
436448

437449
<ThreadItemContent className="min-w-0">
438450
<ThreadItemHeader>
439-
<ThreadItemAuthor>PostHog</ThreadItemAuthor>
440-
<Badge variant="info">Agent</Badge>
451+
<ThreadItemAuthor>
452+
{starter ? userDisplayName(starter) : "PostHog"}
453+
</ThreadItemAuthor>
454+
{!starter && <Badge variant="info">Agent</Badge>}
441455
<ThreadItemTimestamp
442456
dateTime={new Date(task.created_at).toISOString()}
443457
>
444458
{formatRelativeTimeShort(task.created_at)}
445459
</ThreadItemTimestamp>
446460
</ThreadItemHeader>
447461

448-
<ThreadItemBody className="wrap-break-word">
449-
{/* Only attribute channel-started tasks: other origins (Slack,
450-
automations) carry a created_by who didn't start it here. */}
451-
{task.origin_product === "user_created" && task.created_by ? (
452-
<>
453-
{/* Mention-styled but rendered inert: the starter shouldn't be
454-
notified about their own task. */}
455-
<span className={mentionChipClass}>
456-
@{userDisplayName(task.created_by)}
457-
</span>{" "}
458-
started a new task
459-
</>
460-
) : (
461-
"A new task was started"
462-
)}
462+
<ThreadItemBody className="wrap-break-word line-clamp-2 whitespace-pre-wrap">
463+
{prompt ||
464+
(starter ? "started a new task" : "A new task was started")}
463465
</ThreadItemBody>
464466

465-
<TaskCard
466-
task={task}
467-
channelId={channelId}
468-
onOpen={() => onOpenThread(task)}
469-
/>
470-
<ReplyFooter
471-
taskId={task.id}
472-
inView={inView}
473-
onOpenThread={() => onOpenThread(task)}
474-
/>
467+
{children}
475468
</ThreadItemContent>
476469

477-
{/* Replying now lives in the always-visible ReplyFooter, so the hover
478-
toolbar only carries the distinct "Open task" action. Actions anchor
479-
to the row's top-right corner; a top tooltip there overhangs the panel
480-
edge and gets clipped by the scroll container, so open tooltips toward
481-
the content instead. */}
482-
<ThreadItemActions aria-label="Message actions" className="inset-bs-2">
483-
<ThreadItemAction label="Open task" onClick={() => onOpenTask(task)}>
484-
<ArrowSquareOutIcon size={15} />
485-
</ThreadItemAction>
486-
</ThreadItemActions>
470+
{actions}
487471
</ThreadItem>
488472
);
473+
}
474+
475+
const FeedItem = memo(function FeedItem({
476+
task,
477+
channelId,
478+
inView,
479+
onOpenTask,
480+
onOpenThread,
481+
}: {
482+
task: Task;
483+
channelId: string;
484+
inView: boolean;
485+
onOpenTask: (task: Task) => void;
486+
onOpenThread: (task: Task) => void;
487+
}) {
488+
return (
489+
<TaskFeedRow
490+
task={task}
491+
actions={
492+
// Replying now lives in the always-visible ReplyFooter, so the hover
493+
// toolbar only carries the distinct "Open task" action. Actions anchor
494+
// to the row's top-right corner; a top tooltip there overhangs the panel
495+
// edge and gets clipped by the scroll container, so open tooltips toward
496+
// the content instead.
497+
<ThreadItemActions aria-label="Message actions" className="inset-bs-2">
498+
<ThreadItemAction label="Open task" onClick={() => onOpenTask(task)}>
499+
<ArrowSquareOutIcon size={15} />
500+
</ThreadItemAction>
501+
</ThreadItemActions>
502+
}
503+
>
504+
<TaskCard
505+
task={task}
506+
channelId={channelId}
507+
onOpen={() => onOpenThread(task)}
508+
/>
509+
<ReplyFooter
510+
taskId={task.id}
511+
inView={inView}
512+
onOpenThread={() => onOpenThread(task)}
513+
/>
514+
</TaskFeedRow>
515+
);
489516
});
490517

491518
// One feed row: owns the scroller item (the `content-visibility` boundary, so

0 commit comments

Comments
 (0)