@@ -7,19 +7,11 @@ import {
77 TrashIcon ,
88 XIcon ,
99} from "@phosphor-icons/react" ;
10- import {
11- buildThreadTimeline ,
12- deriveThreadAgentStatus ,
13- hasAgentMention ,
14- shouldSuspendThreadSession ,
15- type ThreadAgentStatus ,
16- type ThreadArtifact ,
17- type ThreadTimelineRow ,
10+ import type {
11+ ThreadAgentStatus ,
12+ ThreadArtifact ,
13+ ThreadTimelineRow ,
1814} from "@posthog/core/canvas/threadTimeline" ;
19- import {
20- getPrVisualConfig ,
21- parsePrNumber ,
22- } from "@posthog/core/git-interaction/prStatus" ;
2315import {
2416 Avatar ,
2517 AvatarFallback ,
@@ -47,44 +39,27 @@ import {
4739 ThreadItemGutter ,
4840 ThreadItemHeader ,
4941} from "@posthog/quill" ;
50- import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events" ;
5142import type {
5243 Task ,
5344 TaskThreadMessage ,
5445 UserBasic ,
5546} from "@posthog/shared/domain-types" ;
56- import { isTerminalStatus } from "@posthog/shared/domain-types" ;
57- import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient" ;
5847import { UserAvatar } from "@posthog/ui/features/auth/UserAvatar" ;
59- import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser" ;
6048import { TaskCard } from "@posthog/ui/features/canvas/components/ChannelFeedView" ;
6149import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon" ;
6250import { MentionComposer } from "@posthog/ui/features/canvas/components/MentionComposer" ;
6351import { MentionText } from "@posthog/ui/features/canvas/components/MentionText" ;
6452import { ThreadTimestamp } from "@posthog/ui/features/canvas/components/ThreadTimestamp" ;
65- import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers" ;
66- import {
67- useDeleteTaskThreadMessage ,
68- usePostTaskThreadMessage ,
69- usePostTaskThreadMessageToAgent ,
70- useSendTaskThreadMessageToAgent ,
71- useTaskThread ,
72- } from "@posthog/ui/features/canvas/hooks/useTaskThread" ;
53+ import { useThreadConversation } from "@posthog/ui/features/canvas/hooks/useThreadConversation" ;
7354import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay" ;
74- import { getPrVisualIcon } from "@posthog/ui/features/git-interaction/prIcon" ;
75- import { usePrDetails } from "@posthog/ui/features/git-interaction/usePrDetails" ;
76- import { useSessionConnection } from "@posthog/ui/features/sessions/hooks/useSessionConnection" ;
77- import { useSessionViewState } from "@posthog/ui/features/sessions/hooks/useSessionViewState" ;
78- import { usePendingPermissionsForTask } from "@posthog/ui/features/sessions/sessionStore" ;
55+ import { usePrArtifact } from "@posthog/ui/features/git-interaction/usePrArtifact" ;
7956import { taskDetailQuery } from "@posthog/ui/features/tasks/queries" ;
80- import { toast } from "@posthog/ui/primitives/toast" ;
81- import { track } from "@posthog/ui/shell/analytics" ;
8257import { openExternalUrl } from "@posthog/ui/shell/openExternal" ;
83- import { parseShareLink } from "@posthog/ui/utils/posthogLinks" ;
58+ import { parseHttpsUrl , parseShareLink } from "@posthog/ui/utils/posthogLinks" ;
8459import { navigateToShareTarget } from "@posthog/ui/utils/shareLinks" ;
8560import { getPostHogUrl } from "@posthog/ui/utils/urls" ;
8661import { useQuery } from "@tanstack/react-query" ;
87- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
62+ import { useEffect , useRef } from "react" ;
8863
8964export function ThreadMessageRow ( {
9065 message,
@@ -215,15 +190,6 @@ function ArtifactCardButton({
215190 ) ;
216191}
217192
218- function parseHttpsUrl ( url : string ) : URL | null {
219- try {
220- const parsedUrl = new URL ( url ) ;
221- return parsedUrl . protocol === "https:" ? parsedUrl : null ;
222- } catch {
223- return null ;
224- }
225- }
226-
227193function CanvasArtifactCard ( {
228194 name,
229195 url,
@@ -257,28 +223,19 @@ function CanvasArtifactCard({
257223}
258224
259225function PrArtifactCard ( { url } : { url : string } ) {
260- const parsedUrl = parseHttpsUrl ( url ) ;
261- const safeUrl =
262- parsedUrl ?. origin === "https://github.com" ? parsedUrl . href : null ;
263- const {
264- meta : { state, merged, draft } ,
265- } = usePrDetails ( safeUrl ) ;
266- const config = getPrVisualConfig ( state ?? "open" , merged , draft ) ;
267- const PrIcon = getPrVisualIcon ( config . icon ) ;
268- const prNumber = safeUrl ? parsePrNumber ( safeUrl ) : null ;
226+ const { safeUrl, title, stateLabel, Icon, iconColor } = usePrArtifact ( url ) ;
269227 return (
270228 < ArtifactCardButton
271229 icon = {
272- < PrIcon
230+ < Icon
273231 size = { 14 }
274232 weight = "bold"
275233 className = "shrink-0"
276- style = { { color : `var(-- ${ config . color } -9)` } }
234+ style = { { color : iconColor } }
277235 />
278236 }
279- title = { prNumber ? `Pull request #${ prNumber } ` : "Pull request" }
280- // Only show the resolved state once we have it, to avoid a flash of "Open".
281- detail = { state ? config . label : null }
237+ title = { title }
238+ detail = { stateLabel }
282239 onOpen = { safeUrl ? ( ) => openExternalUrl ( safeUrl ) : undefined }
283240 />
284241 ) ;
@@ -319,7 +276,7 @@ export function ThreadArtifactRow({
319276 ) ;
320277}
321278
322- function ThreadLoadingState ( ) {
279+ export function ThreadLoadingState ( ) {
323280 return (
324281 < Empty className = "h-full border-0" >
325282 < EmptyHeader >
@@ -332,19 +289,23 @@ function ThreadLoadingState() {
332289 ) ;
333290}
334291
335- function ThreadHeader ( {
292+ /** The panel's title row and window controls. Shared with ActivityPanel, which
293+ * is the same chrome under a different title. */
294+ export function ThreadPanelHeader ( {
295+ title,
336296 onClose,
337297 onToggleCollapsed,
338298 onOpenFull,
339299} : {
300+ title : string ;
340301 onClose ?: ( ) => void ;
341302 onToggleCollapsed ?: ( ) => void ;
342303 onOpenFull ?: ( ) => void ;
343304} ) {
344305 return (
345306 < div className = "flex items-center gap-1 border-border border-b px-3 py-2" >
346307 < div className = "min-w-0 flex-1" >
347- < span className = "block font-medium text-sm" > Thread </ span >
308+ < span className = "block font-medium text-sm" > { title } </ span >
348309 </ div >
349310 { onOpenFull && (
350311 < Button
@@ -360,7 +321,7 @@ function ThreadHeader({
360321 < Button
361322 variant = "default"
362323 size = "icon-sm"
363- aria-label = " Collapse thread"
324+ aria-label = { ` Collapse ${ title . toLowerCase ( ) } ` }
364325 onClick = { onToggleCollapsed }
365326 >
366327 < CaretRightIcon size = { 14 } />
@@ -370,7 +331,7 @@ function ThreadHeader({
370331 < Button
371332 variant = "default"
372333 size = "icon-sm"
373- aria-label = " Close thread"
334+ aria-label = { ` Close ${ title . toLowerCase ( ) } ` }
374335 onClick = { onClose }
375336 >
376337 < XIcon size = { 14 } />
@@ -380,7 +341,7 @@ function ThreadHeader({
380341 ) ;
381342}
382343
383- function ThreadTimeline ( {
344+ export function ThreadTimeline ( {
384345 timeline,
385346 isReady,
386347 currentUserUuid,
@@ -446,7 +407,7 @@ function ThreadTimeline({
446407 ) ;
447408}
448409
449- function ThreadReplyComposer ( {
410+ export function ThreadReplyComposer ( {
450411 draft,
451412 onDraftChange,
452413 onSubmit,
@@ -509,151 +470,36 @@ function ThreadConversation({
509470 onOpenFull ?: ( ) => void ;
510471 showTaskSummary : boolean ;
511472} ) {
512- const taskId = task . id ;
513- const client = useOptionalAuthenticatedClient ( ) ;
514- const { data : currentUser } = useCurrentUser ( { client } ) ;
515-
516- const { messages, isLoading } = useTaskThread ( taskId ) ;
517- const { postMessage, isPosting } = usePostTaskThreadMessage ( taskId ) ;
518- const { postMessageToAgent, isPostingToAgent } =
519- usePostTaskThreadMessageToAgent ( taskId ) ;
520- const { deleteMessage } = useDeleteTaskThreadMessage ( taskId ) ;
521- const { sendToAgent, isSending } = useSendTaskThreadMessageToAgent ( taskId ) ;
522- const isSendingToAgent = isPostingToAgent || isSending ;
523- const { members } = useOrgMembers ( ) ;
524-
525- const {
526- session,
527- repoPath,
528- isCloud,
529- events,
530- cloudStatus,
531- isPromptPending,
532- isInitializing,
533- hasError,
534- errorTitle,
535- } = useSessionViewState ( taskId , task ) ;
536- useSessionConnection ( {
537- taskId,
538- task,
539- session,
540- repoPath,
541- isCloud,
542- isSuspended : shouldSuspendThreadSession ( {
543- isCloud,
544- hasRun : Boolean ( task . latest_run ?. id ) ,
545- hasSession : Boolean ( session ) ,
546- } ) ,
473+ const conversation = useThreadConversation ( task , {
474+ surface : "thread_panel" ,
547475 } ) ;
548- const pendingPermissions = usePendingPermissionsForTask ( taskId ) ;
549-
550- const agentStatus = useMemo (
551- ( ) =>
552- deriveThreadAgentStatus ( {
553- hasActivity : events . length > 0 || ! ! task . latest_run ,
554- hasError,
555- cloudStatus,
556- errorTitle,
557- pendingPermissionCount : pendingPermissions . size ,
558- isPromptPending,
559- isInitializing,
560- } ) ,
561- [
562- events . length ,
563- task . latest_run ,
564- hasError ,
565- cloudStatus ,
566- errorTitle ,
567- pendingPermissions . size ,
568- isPromptPending ,
569- isInitializing ,
570- ] ,
571- ) ;
572-
573- const timeline = useMemo ( ( ) => buildThreadTimeline ( messages ) , [ messages ] ) ;
476+ const {
477+ timeline,
478+ agentStatus,
479+ isReady,
480+ members,
481+ currentUser,
482+ isTaskAuthor,
483+ canForward,
484+ draft,
485+ setDraft,
486+ isSubmitDisabled,
487+ submit,
488+ sendMessageToAgent,
489+ deleteMessage,
490+ onMentionInsert,
491+ } = conversation ;
574492
575- const [ draft , setDraft ] = useState ( "" ) ;
576493 const scrollRef = useRef < HTMLDivElement > ( null ) ;
577-
578- const handleMentionInsert = useCallback (
579- ( member : UserBasic ) => {
580- track ( ANALYTICS_EVENTS . CHANNEL_ACTION , {
581- action_type : "mention_member" ,
582- surface : "thread_panel" ,
583- task_id : taskId ,
584- mentioned_user_id : member . uuid ,
585- } ) ;
586- } ,
587- [ taskId ] ,
588- ) ;
589-
590494 // biome-ignore lint/correctness/useExhaustiveDependencies: scroll when rendered thread content changes
591495 useEffect ( ( ) => {
592496 scrollRef . current ?. scrollTo ( { top : scrollRef . current . scrollHeight } ) ;
593497 } , [ timeline , agentStatus ?. phase ] ) ;
594498
595- const isTaskAuthor =
596- ! ! currentUser ?. uuid && currentUser . uuid === task . created_by ?. uuid ;
597- const canForward =
598- ! ! task . latest_run &&
599- ! isTerminalStatus ( task . latest_run . status ) &&
600- ! isSendingToAgent ;
601-
602- const submit = async ( ) => {
603- const content = draft . trim ( ) ;
604- if ( ! content || isPosting || isSendingToAgent ) return ;
605- const sendToAgentRequested = hasAgentMention ( content ) ;
606- if ( sendToAgentRequested && ( ! isTaskAuthor || ! canForward ) ) {
607- toast . error ( "Couldn't send to agent" , {
608- description :
609- "Only the task author can @agent while the task has an active run." ,
610- } ) ;
611- return ;
612- }
613- setDraft ( "" ) ;
614- try {
615- if ( sendToAgentRequested ) {
616- const { sendError } = await postMessageToAgent ( content ) ;
617- if ( sendError ) {
618- toast . error ( "Message posted, but couldn't send it to the agent" , {
619- description :
620- sendError instanceof Error
621- ? sendError . message
622- : String ( sendError ) ,
623- } ) ;
624- }
625- } else {
626- await postMessage ( content ) ;
627- }
628- } catch ( error ) {
629- setDraft ( content ) ;
630- toast . error ( "Couldn't post message" , {
631- description : error instanceof Error ? error . message : String ( error ) ,
632- } ) ;
633- }
634- } ;
635-
636- const handleSendToAgent = ( messageId : string ) => {
637- sendToAgent ( messageId ) . catch ( ( error : unknown ) => {
638- toast . error ( "Couldn't send message to agent" , {
639- description : error instanceof Error ? error . message : String ( error ) ,
640- } ) ;
641- } ) ;
642- } ;
643-
644- const handleDelete = ( messageId : string ) => {
645- deleteMessage ( messageId ) . catch ( ( error : unknown ) => {
646- toast . error ( "Couldn't delete message" , {
647- description : error instanceof Error ? error . message : String ( error ) ,
648- } ) ;
649- } ) ;
650- } ;
651-
652- const isReady = ! isInitializing && ! isLoading ;
653-
654499 return (
655500 < div className = "flex h-full min-w-0 flex-col bg-gray-1" >
656- < ThreadHeader
501+ < ThreadPanelHeader
502+ title = "Thread"
657503 onOpenFull = { onOpenFull }
658504 onToggleCollapsed = { onToggleCollapsed }
659505 onClose = { onClose }
@@ -672,8 +518,8 @@ function ThreadConversation({
672518 currentUserEmail = { currentUser ?. email }
673519 isTaskAuthor = { isTaskAuthor }
674520 canForward = { canForward }
675- onSendToAgent = { handleSendToAgent }
676- onDelete = { handleDelete }
521+ onSendToAgent = { sendMessageToAgent }
522+ onDelete = { deleteMessage }
677523 />
678524 </ div >
679525
@@ -685,8 +531,8 @@ function ThreadConversation({
685531 onSubmit = { submit }
686532 members = { members }
687533 allowAgentMention = { isTaskAuthor && canForward }
688- onMentionInsert = { handleMentionInsert }
689- disabled = { ! draft . trim ( ) || isPosting || isSendingToAgent }
534+ onMentionInsert = { onMentionInsert }
535+ disabled = { isSubmitDisabled }
690536 />
691537 </ div >
692538 ) ;
0 commit comments