|
5 | 5 | RobotIcon, |
6 | 6 | } from "@phosphor-icons/react"; |
7 | 7 | import { taskFeedRunStatus } from "@posthog/core/canvas/channelFeed"; |
| 8 | +import { xmlToPlainText } from "@posthog/core/message-editor/content"; |
8 | 9 | import { |
9 | 10 | Avatar, |
10 | 11 | AvatarFallback, |
@@ -37,10 +38,13 @@ import { |
37 | 38 | useChatMessageScroller, |
38 | 39 | } from "@posthog/quill"; |
39 | 40 | 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"; |
41 | 46 | import { getUserInitials } from "@posthog/ui/features/auth/userInitials"; |
42 | 47 | import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon"; |
43 | | -import { mentionChipClass } from "@posthog/ui/features/canvas/components/MentionText"; |
44 | 48 | import type { ChannelFeedSystemMessage } from "@posthog/ui/features/canvas/hooks/useChannelFeedMessages"; |
45 | 49 | import { useChannelTaskData } from "@posthog/ui/features/canvas/hooks/useChannelTaskData"; |
46 | 50 | import { useTaskThread } from "@posthog/ui/features/canvas/hooks/useTaskThread"; |
@@ -411,81 +415,104 @@ function ReplyFooter({ |
411 | 415 | ); |
412 | 416 | } |
413 | 417 |
|
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({ |
415 | 425 | task, |
416 | | - channelId, |
417 | | - inView, |
418 | | - onOpenTask, |
419 | | - onOpenThread, |
| 426 | + actions, |
| 427 | + children, |
420 | 428 | }: { |
421 | 429 | task: Task; |
422 | | - channelId: string; |
423 | | - inView: boolean; |
424 | | - onOpenTask: (task: Task) => void; |
425 | | - onOpenThread: (task: Task) => void; |
| 430 | + actions?: ReactNode; |
| 431 | + children?: ReactNode; |
426 | 432 | }) { |
| 433 | + const starter = channelTaskStarter(task); |
| 434 | + const prompt = useMemo( |
| 435 | + () => xmlToPlainText(task.description ?? "").trim(), |
| 436 | + [task.description], |
| 437 | + ); |
| 438 | + |
427 | 439 | return ( |
428 | 440 | <ThreadItem className="rounded-none py-1 pr-8 hover:bg-fill-hover/50"> |
429 | 441 | <ThreadItemGutter> |
430 | 442 | <Avatar> |
431 | 443 | <AvatarFallback> |
432 | | - <RobotIcon size={16} /> |
| 444 | + {starter ? getUserInitials(starter) : <RobotIcon size={16} />} |
433 | 445 | </AvatarFallback> |
434 | 446 | </Avatar> |
435 | 447 | </ThreadItemGutter> |
436 | 448 |
|
437 | 449 | <ThreadItemContent className="min-w-0"> |
438 | 450 | <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>} |
441 | 455 | <ThreadItemTimestamp |
442 | 456 | dateTime={new Date(task.created_at).toISOString()} |
443 | 457 | > |
444 | 458 | {formatRelativeTimeShort(task.created_at)} |
445 | 459 | </ThreadItemTimestamp> |
446 | 460 | </ThreadItemHeader> |
447 | 461 |
|
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")} |
463 | 465 | </ThreadItemBody> |
464 | 466 |
|
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} |
475 | 468 | </ThreadItemContent> |
476 | 469 |
|
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} |
487 | 471 | </ThreadItem> |
488 | 472 | ); |
| 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 | + ); |
489 | 516 | }); |
490 | 517 |
|
491 | 518 | // One feed row: owns the scroller item (the `content-visibility` boundary, so |
|
0 commit comments