@@ -37,7 +37,11 @@ import {
3737 useChatMessageScroller ,
3838} from "@posthog/quill" ;
3939import { formatRelativeTimeShort , getLocalDayDiff } from "@posthog/shared" ;
40- import type { Task , TaskRunStatus } from "@posthog/shared/domain-types" ;
40+ import type {
41+ Task ,
42+ TaskRunStatus ,
43+ UserBasic ,
44+ } from "@posthog/shared/domain-types" ;
4145import { getUserInitials } from "@posthog/ui/features/auth/userInitials" ;
4246import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon" ;
4347import type { ChannelFeedSystemMessage } from "@posthog/ui/features/canvas/hooks/useChannelFeedMessages" ;
@@ -410,24 +414,31 @@ function ReplyFooter({
410414 ) ;
411415}
412416
413- const FeedItem = memo ( function FeedItem ( {
417+ // The human who kicked a task off in the channel, or null when the row should
418+ // stay agent-attributed. Only channel-started tasks (user_created) are a
419+ // person's doing; other origins (Slack, automations) carry a created_by who
420+ // didn't start it here, so they keep the agent identity — which also leaves
421+ // that identity free for genuine agent-authored messages in the future.
422+ function channelTaskStarter ( task : Task ) : UserBasic | null {
423+ return task . origin_product === "user_created"
424+ ? ( task . created_by ?? null )
425+ : null ;
426+ }
427+
428+ // The presentational feed row: avatar + attribution header + body. The task
429+ // card and reply footer are supplied as `children` (they fetch their own data,
430+ // so keeping them out of here leaves the row pure and storyable); `actions` is
431+ // the hover toolbar.
432+ export function TaskFeedRow ( {
414433 task,
415- channelId,
416- inView,
417- onOpenTask,
418- onOpenThread,
434+ actions,
435+ children,
419436} : {
420437 task : Task ;
421- channelId : string ;
422- inView : boolean ;
423- onOpenTask : ( task : Task ) => void ;
424- onOpenThread : ( task : Task ) => void ;
438+ actions ?: ReactNode ;
439+ children ?: ReactNode ;
425440} ) {
426- // Only attribute channel-started tasks to a human: other origins (Slack,
427- // automations) carry a created_by who didn't start it here, so those stay
428- // agent-attributed to leave room for genuine agent-authored messages.
429- const starter =
430- task . origin_product === "user_created" ? task . created_by : null ;
441+ const starter = channelTaskStarter ( task ) ;
431442
432443 return (
433444 < ThreadItem className = "rounded-none py-1 pr-8 hover:bg-fill-hover/50" >
@@ -456,30 +467,55 @@ const FeedItem = memo(function FeedItem({
456467 { starter ? "started a new task" : "A new task was started" }
457468 </ ThreadItemBody >
458469
459- < TaskCard
460- task = { task }
461- channelId = { channelId }
462- onOpen = { ( ) => onOpenThread ( task ) }
463- />
464- < ReplyFooter
465- taskId = { task . id }
466- inView = { inView }
467- onOpenThread = { ( ) => onOpenThread ( task ) }
468- />
470+ { children }
469471 </ ThreadItemContent >
470472
471- { /* Replying now lives in the always-visible ReplyFooter, so the hover
472- toolbar only carries the distinct "Open task" action. Actions anchor
473- to the row's top-right corner; a top tooltip there overhangs the panel
474- edge and gets clipped by the scroll container, so open tooltips toward
475- the content instead. */ }
476- < ThreadItemActions aria-label = "Message actions" className = "inset-bs-2" >
477- < ThreadItemAction label = "Open task" onClick = { ( ) => onOpenTask ( task ) } >
478- < ArrowSquareOutIcon size = { 15 } />
479- </ ThreadItemAction >
480- </ ThreadItemActions >
473+ { actions }
481474 </ ThreadItem >
482475 ) ;
476+ }
477+
478+ const FeedItem = memo ( function FeedItem ( {
479+ task,
480+ channelId,
481+ inView,
482+ onOpenTask,
483+ onOpenThread,
484+ } : {
485+ task : Task ;
486+ channelId : string ;
487+ inView : boolean ;
488+ onOpenTask : ( task : Task ) => void ;
489+ onOpenThread : ( task : Task ) => void ;
490+ } ) {
491+ return (
492+ < TaskFeedRow
493+ task = { task }
494+ actions = {
495+ // Replying now lives in the always-visible ReplyFooter, so the hover
496+ // toolbar only carries the distinct "Open task" action. Actions anchor
497+ // to the row's top-right corner; a top tooltip there overhangs the panel
498+ // edge and gets clipped by the scroll container, so open tooltips toward
499+ // the content instead.
500+ < ThreadItemActions aria-label = "Message actions" className = "inset-bs-2" >
501+ < ThreadItemAction label = "Open task" onClick = { ( ) => onOpenTask ( task ) } >
502+ < ArrowSquareOutIcon size = { 15 } />
503+ </ ThreadItemAction >
504+ </ ThreadItemActions >
505+ }
506+ >
507+ < TaskCard
508+ task = { task }
509+ channelId = { channelId }
510+ onOpen = { ( ) => onOpenThread ( task ) }
511+ />
512+ < ReplyFooter
513+ taskId = { task . id }
514+ inView = { inView }
515+ onOpenThread = { ( ) => onOpenThread ( task ) }
516+ />
517+ </ TaskFeedRow >
518+ ) ;
483519} ) ;
484520
485521// One feed row: owns the scroller item (the `content-visibility` boundary, so
0 commit comments