@@ -10,14 +10,8 @@ import {
1010 AvatarFallback ,
1111 AvatarGroup ,
1212 Badge ,
13- Button ,
14- ButtonGroup ,
1513 Card ,
1614 CardContent ,
17- ChatMessage ,
18- ChatMessageAvatar ,
19- ChatMessageContent ,
20- ChatMessageHeader ,
2115 ChatMessageScroller ,
2216 ChatMessageScrollerButton ,
2317 ChatMessageScrollerContent ,
@@ -26,10 +20,18 @@ import {
2620 ChatMessageScrollerViewport ,
2721 cn ,
2822 Spinner ,
29- Tooltip ,
30- TooltipContent ,
31- TooltipProvider ,
32- TooltipTrigger ,
23+ ThreadItem ,
24+ ThreadItemAction ,
25+ ThreadItemActions ,
26+ ThreadItemAuthor ,
27+ ThreadItemBody ,
28+ ThreadItemContent ,
29+ ThreadItemGutter ,
30+ ThreadItemHeader ,
31+ ThreadItemReplies ,
32+ ThreadItemRepliesLabel ,
33+ ThreadItemRepliesMeta ,
34+ ThreadItemTimestamp ,
3335} from "@posthog/quill" ;
3436import { formatRelativeTimeShort } from "@posthog/shared" ;
3537import type { Task , TaskRunStatus } from "@posthog/shared/domain-types" ;
@@ -255,9 +257,9 @@ function TaskCard({ task, onOpen }: { task: Task; onOpen: () => void }) {
255257 < Card
256258 size = "sm"
257259 className = { cn (
258- "mt-1.5 w-full cursor-pointer py-0 transition-colors hover:bg-fill-hover" ,
260+ "mt-1.5 w-full max-w-[820px] cursor-pointer rounded-sm py-0 transition-none hover:bg-fill-hover" ,
259261 statusDisplay . isMerged
260- ? "border-transparent bg-(--purple-a1 ) shadow-[0_0_0_1px_var(--purple-8)]"
262+ ? "border-transparent bg-(--purple-a2 ) shadow-[0_0_0_1px_var(--purple-8)] hover:bg-(--purple-a3) dark:bg-(--purple-a1) dark:hover:bg-(--purple-a2) "
261263 : "hover:border-border-primary" ,
262264 ) }
263265 onClick = { onOpen }
@@ -328,25 +330,21 @@ function RepliesRow({
328330 const last = messages [ messages . length - 1 ] ;
329331
330332 return (
331- < button
332- type = "button"
333- onClick = { onOpenThread }
334- className = "mt-1 flex w-fit items-center gap-2 rounded-md px-1.5 py-1 hover:bg-fill-secondary"
335- >
333+ < ThreadItemReplies onClick = { onOpenThread } className = "mt-1" >
336334 < AvatarGroup size = "xs" stacked >
337335 { authors . map ( ( author , index ) => (
338336 < Avatar key = { author ?. uuid ?? index } size = "xs" >
339337 < AvatarFallback > { getUserInitials ( author ) } </ AvatarFallback >
340338 </ Avatar >
341339 ) ) }
342340 </ AvatarGroup >
343- < Text size = "1" weight = "medium" className = "text-accent-11" >
341+ < ThreadItemRepliesLabel >
344342 { messages . length } { messages . length === 1 ? "reply" : "replies" }
345- </ Text >
346- < Text size = "1" className = "text-muted-foreground" >
343+ </ ThreadItemRepliesLabel >
344+ < ThreadItemRepliesMeta >
347345 Last reply { formatRelativeTimeShort ( last . created_at ) }
348- </ Text >
349- </ button >
346+ </ ThreadItemRepliesMeta >
347+ </ ThreadItemReplies >
350348 ) ;
351349}
352350
@@ -363,11 +361,8 @@ function FeedItem({
363361 const isAgent = ! task . created_by || task . origin_product !== "user_created" ;
364362
365363 return (
366- < ChatMessage className = "group relative rounded-md px-3 py-2 hover:bg-fill-secondary/50" >
367- { /* Quill's avatar slot bottom-aligns for bubble chats; a Slack feed
368- anchors it beside the name row. The slot draws its own circle, so
369- drop its background and let the inner Avatar render. */ }
370- < ChatMessageAvatar className = "self-start bg-transparent" >
364+ < ThreadItem className = "py-4 pr-8 hover:bg-fill-hover/10" >
365+ < ThreadItemGutter >
371366 < Avatar >
372367 < AvatarFallback >
373368 { isAgent && ! task . created_by ? (
@@ -377,64 +372,49 @@ function FeedItem({
377372 ) }
378373 </ AvatarFallback >
379374 </ Avatar >
380- </ ChatMessageAvatar >
381- < ChatMessageContent className = "min-w-0 gap-0.5" >
382- < ChatMessageHeader className = "items-baseline gap-2 px-0" >
383- < Text size = "2" weight = "bold" className = "truncate" >
375+ </ ThreadItemGutter >
376+
377+ < ThreadItemContent className = "min-w-0" >
378+ < ThreadItemHeader >
379+ < ThreadItemAuthor >
384380 { task . created_by ? userDisplayName ( task . created_by ) : "Agent" }
385- </ Text >
381+ </ ThreadItemAuthor >
386382 { isAgent && < Badge variant = "info" > Agent</ Badge > }
387- < Text size = "1" className = "shrink-0 text-muted-foreground" >
383+ < ThreadItemTimestamp
384+ dateTime = { new Date ( task . created_at ) . toISOString ( ) }
385+ >
388386 { formatRelativeTimeShort ( task . created_at ) }
389- </ Text >
390- </ ChatMessageHeader >
387+ </ ThreadItemTimestamp >
388+ </ ThreadItemHeader >
391389
392- < Text size = "2" className = "line-clamp-4 whitespace-pre-wrap break-words " >
390+ < ThreadItemBody className = "wrap-break-word line-clamp-4 whitespace-pre-wrap" >
393391 { prompt }
394- </ Text >
392+ </ ThreadItemBody >
395393
396394 < TaskCard task = { task } onOpen = { ( ) => onOpenTask ( task ) } />
397395 < RepliesRow taskId = { task . id } onOpenThread = { ( ) => onOpenThread ( task ) } />
398- </ ChatMessageContent >
396+ </ ThreadItemContent >
399397
400- { /* Hover toolbar, storybook-style: floats top-right of the row. */ }
401- < div className = "absolute top-1 right-2 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100" >
402- < TooltipProvider delay = { 400 } >
403- < ButtonGroup className = "rounded-md border border-border bg-surface shadow-sm" >
404- < Tooltip >
405- < TooltipTrigger
406- render = {
407- < Button
408- variant = "default"
409- size = "icon-sm"
410- aria-label = "Reply in thread"
411- onClick = { ( ) => onOpenThread ( task ) }
412- >
413- < ChatCircleIcon size = { 15 } />
414- </ Button >
415- }
416- />
417- < TooltipContent side = "top" > Reply in thread</ TooltipContent >
418- </ Tooltip >
419- < Tooltip >
420- < TooltipTrigger
421- render = {
422- < Button
423- variant = "default"
424- size = "icon-sm"
425- aria-label = "Open task"
426- onClick = { ( ) => onOpenTask ( task ) }
427- >
428- < ArrowSquareOutIcon size = { 15 } />
429- </ Button >
430- }
431- />
432- < TooltipContent side = "top" > Open task</ TooltipContent >
433- </ Tooltip >
434- </ ButtonGroup >
435- </ TooltipProvider >
436- </ div >
437- </ ChatMessage >
398+ { /* Actions anchor to the row's top-right corner; a top tooltip there
399+ overhangs the panel edge and gets clipped by the scroll container, so
400+ open tooltips toward the content instead. */ }
401+ < ThreadItemActions aria-label = "Message actions" className = "inset-bs-2" >
402+ < ThreadItemAction
403+ label = "Reply in thread"
404+ tooltipSide = "left"
405+ onClick = { ( ) => onOpenThread ( task ) }
406+ >
407+ < ChatCircleIcon size = { 15 } />
408+ </ ThreadItemAction >
409+ < ThreadItemAction
410+ label = "Open task"
411+ tooltipSide = "left"
412+ onClick = { ( ) => onOpenTask ( task ) }
413+ >
414+ < ArrowSquareOutIcon size = { 15 } />
415+ </ ThreadItemAction >
416+ </ ThreadItemActions >
417+ </ ThreadItem >
438418 ) ;
439419}
440420
@@ -470,9 +450,22 @@ export function ChannelFeedView({
470450 < ChatMessageScrollerProvider defaultScrollPosition = "end" >
471451 < ChatMessageScroller className = "min-h-0 flex-1" >
472452 < ChatMessageScrollerViewport >
473- < ChatMessageScrollerContent className = "mx-auto w-full max-w-[820px] px-1 py-3" >
453+ { /* Horizontal padding is load-bearing: ThreadItem's actions float at
454+ the row's top-right corner (absolute, past the row edge). Without a
455+ gutter they hug the scroll container and get clipped. */ }
456+ < ChatMessageScrollerContent className = "mx-auto w-full gap-0 py-4" >
474457 { tasks . map ( ( task ) => (
475- < ChatMessageScrollerItem key = { task . id } messageId = { task . id } >
458+ < ChatMessageScrollerItem
459+ key = { task . id }
460+ messageId = { task . id }
461+ // Rows already get `content-visibility:auto` from quill, but its
462+ // default `contain-intrinsic-size` (10rem) under-reserves a feed
463+ // row (message + task card + replies ≈ 13rem), so off-screen
464+ // rows collapse too small and the scrollbar jumps as they paint
465+ // in. A closer estimate keeps scrolling stable; `auto` still
466+ // remembers each row's real height after its first render.
467+ className = "[contain-intrinsic-size:auto_13rem] hover:bg-fill-hover/50"
468+ >
476469 < FeedItem
477470 task = { task }
478471 onOpenTask = { onOpenTask }
0 commit comments