From 20c51fe263749445aff9807cd0a1a2a6c01774b3 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Mon, 27 Jul 2026 20:44:32 +0200 Subject: [PATCH 1/2] refactor(canvas): share thread and PR artifact logic Extract the task thread conversation behavior and PR artifact presentation used by existing channel surfaces. This is behavior-neutral groundwork for the structured Activity view. Generated-By: PostHog Code Task-Id: 6190e713-9b80-43d9-a05c-5e3b1ecdf297 --- .../canvas/components/ThreadPanel.tsx | 250 ++++-------------- .../components/WebsiteChannelArtifacts.tsx | 52 ++-- .../canvas/hooks/useThreadConversation.ts | 213 +++++++++++++++ .../features/git-interaction/usePrArtifact.ts | 38 +++ packages/ui/src/utils/posthogLinks.ts | 14 + 5 files changed, 333 insertions(+), 234 deletions(-) create mode 100644 packages/ui/src/features/canvas/hooks/useThreadConversation.ts create mode 100644 packages/ui/src/features/git-interaction/usePrArtifact.ts diff --git a/packages/ui/src/features/canvas/components/ThreadPanel.tsx b/packages/ui/src/features/canvas/components/ThreadPanel.tsx index 8b242509ee..6e54d9e7f8 100644 --- a/packages/ui/src/features/canvas/components/ThreadPanel.tsx +++ b/packages/ui/src/features/canvas/components/ThreadPanel.tsx @@ -7,19 +7,11 @@ import { TrashIcon, XIcon, } from "@phosphor-icons/react"; -import { - buildThreadTimeline, - deriveThreadAgentStatus, - hasAgentMention, - shouldSuspendThreadSession, - type ThreadAgentStatus, - type ThreadArtifact, - type ThreadTimelineRow, +import type { + ThreadAgentStatus, + ThreadArtifact, + ThreadTimelineRow, } from "@posthog/core/canvas/threadTimeline"; -import { - getPrVisualConfig, - parsePrNumber, -} from "@posthog/core/git-interaction/prStatus"; import { Avatar, AvatarFallback, @@ -47,44 +39,27 @@ import { ThreadItemGutter, ThreadItemHeader, } from "@posthog/quill"; -import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import type { Task, TaskThreadMessage, UserBasic, } from "@posthog/shared/domain-types"; -import { isTerminalStatus } from "@posthog/shared/domain-types"; -import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient"; import { UserAvatar } from "@posthog/ui/features/auth/UserAvatar"; -import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser"; import { TaskCard } from "@posthog/ui/features/canvas/components/ChannelFeedView"; import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon"; import { MentionComposer } from "@posthog/ui/features/canvas/components/MentionComposer"; import { MentionText } from "@posthog/ui/features/canvas/components/MentionText"; import { ThreadTimestamp } from "@posthog/ui/features/canvas/components/ThreadTimestamp"; -import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers"; -import { - useDeleteTaskThreadMessage, - usePostTaskThreadMessage, - usePostTaskThreadMessageToAgent, - useSendTaskThreadMessageToAgent, - useTaskThread, -} from "@posthog/ui/features/canvas/hooks/useTaskThread"; +import { useThreadConversation } from "@posthog/ui/features/canvas/hooks/useThreadConversation"; import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay"; -import { getPrVisualIcon } from "@posthog/ui/features/git-interaction/prIcon"; -import { usePrDetails } from "@posthog/ui/features/git-interaction/usePrDetails"; -import { useSessionConnection } from "@posthog/ui/features/sessions/hooks/useSessionConnection"; -import { useSessionViewState } from "@posthog/ui/features/sessions/hooks/useSessionViewState"; -import { usePendingPermissionsForTask } from "@posthog/ui/features/sessions/sessionStore"; +import { usePrArtifact } from "@posthog/ui/features/git-interaction/usePrArtifact"; import { taskDetailQuery } from "@posthog/ui/features/tasks/queries"; -import { toast } from "@posthog/ui/primitives/toast"; -import { track } from "@posthog/ui/shell/analytics"; import { openExternalUrl } from "@posthog/ui/shell/openExternal"; -import { parseShareLink } from "@posthog/ui/utils/posthogLinks"; +import { parseHttpsUrl, parseShareLink } from "@posthog/ui/utils/posthogLinks"; import { navigateToShareTarget } from "@posthog/ui/utils/shareLinks"; import { getPostHogUrl } from "@posthog/ui/utils/urls"; import { useQuery } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; export function ThreadMessageRow({ message, @@ -215,15 +190,6 @@ function ArtifactCardButton({ ); } -function parseHttpsUrl(url: string): URL | null { - try { - const parsedUrl = new URL(url); - return parsedUrl.protocol === "https:" ? parsedUrl : null; - } catch { - return null; - } -} - function CanvasArtifactCard({ name, url, @@ -257,28 +223,19 @@ function CanvasArtifactCard({ } function PrArtifactCard({ url }: { url: string }) { - const parsedUrl = parseHttpsUrl(url); - const safeUrl = - parsedUrl?.origin === "https://github.com" ? parsedUrl.href : null; - const { - meta: { state, merged, draft }, - } = usePrDetails(safeUrl); - const config = getPrVisualConfig(state ?? "open", merged, draft); - const PrIcon = getPrVisualIcon(config.icon); - const prNumber = safeUrl ? parsePrNumber(safeUrl) : null; + const { safeUrl, title, stateLabel, Icon, iconColor } = usePrArtifact(url); return ( } - title={prNumber ? `Pull request #${prNumber}` : "Pull request"} - // Only show the resolved state once we have it, to avoid a flash of "Open". - detail={state ? config.label : null} + title={title} + detail={stateLabel} onOpen={safeUrl ? () => openExternalUrl(safeUrl) : undefined} /> ); @@ -319,7 +276,7 @@ export function ThreadArtifactRow({ ); } -function ThreadLoadingState() { +export function ThreadLoadingState() { return ( @@ -332,11 +289,15 @@ function ThreadLoadingState() { ); } -function ThreadHeader({ +/** The panel's title row and window controls. Shared with ActivityPanel, which + * is the same chrome under a different title. */ +export function ThreadPanelHeader({ + title, onClose, onToggleCollapsed, onOpenFull, }: { + title: string; onClose?: () => void; onToggleCollapsed?: () => void; onOpenFull?: () => void; @@ -344,7 +305,7 @@ function ThreadHeader({ return (
- Thread + {title}
{onOpenFull && (
); diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelArtifacts.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelArtifacts.tsx index d91d91d5b4..b55529a42a 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelArtifacts.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelArtifacts.tsx @@ -1,10 +1,6 @@ import { CaretRightIcon } from "@phosphor-icons/react"; import type { ChannelTaskRecord } from "@posthog/core/canvas/channelTaskSchemas"; import type { DashboardSummary } from "@posthog/core/canvas/dashboardSchemas"; -import { - getPrVisualConfig, - parsePrNumber, -} from "@posthog/core/git-interaction/prStatus"; import { formatRelativeTimeShort } from "@posthog/shared"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTaskIds"; @@ -12,8 +8,7 @@ import { ChannelHeader } from "@posthog/ui/features/canvas/components/ChannelHea import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon"; import { useChannelTasks } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; import { useDashboards } from "@posthog/ui/features/canvas/hooks/useDashboards"; -import { getPrVisualIcon } from "@posthog/ui/features/git-interaction/prIcon"; -import { usePrDetails } from "@posthog/ui/features/git-interaction/usePrDetails"; +import { usePrArtifact } from "@posthog/ui/features/git-interaction/usePrArtifact"; import { useTasks } from "@posthog/ui/features/tasks/useTasks"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { track } from "@posthog/ui/shell/analytics"; @@ -159,7 +154,7 @@ export function WebsiteChannelArtifacts({ channelId }: { channelId: string }) { title={item.title} prUrl={item.prUrl} ts={item.ts} - onClick={() => openPr(item.prUrl)} + onClick={openPr} /> ), )} @@ -171,8 +166,8 @@ export function WebsiteChannelArtifacts({ channelId }: { channelId: string }) { } // A PR artifact row. The PR's lifecycle state (open / draft / merged / closed) -// is fetched per-URL (deduped + cached by usePrDetails) so the icon and label -// reflect the live state. +// comes from usePrArtifact, which also gates the URL — PR links come from run +// output, so a row must not fetch from whatever host that names. function PrArtifactRow({ title, prUrl, @@ -182,37 +177,28 @@ function PrArtifactRow({ title: string; prUrl: string; ts: number; - onClick: () => void; + onClick: (safeUrl: string) => void; }) { const { - meta: { state, merged, draft }, - } = usePrDetails(prUrl); - const config = getPrVisualConfig(state ?? "open", merged, draft); - const PrIcon = getPrVisualIcon(config.icon); - const prNumber = parsePrNumber(prUrl); + safeUrl, + title: prTitle, + stateLabel, + Icon, + iconColor, + accentColor, + } = usePrArtifact(prUrl); - const subtitle = [ - prNumber ? `Pull request #${prNumber}` : "Pull request", - // Only show the resolved state once we have it, to avoid a flash of "Open". - state ? config.label : null, - formatRelativeTimeShort(ts), - ] + const subtitle = [prTitle, stateLabel, formatRelativeTimeShort(ts)] .filter(Boolean) .join(" · "); return ( - } + accent={accentColor} + icon={} title={title} subtitle={subtitle} - onClick={onClick} + onClick={safeUrl ? () => onClick(safeUrl) : undefined} /> ); } @@ -228,13 +214,15 @@ function ArtifactRow({ accent: string; title: string; subtitle: string; - onClick: () => void; + /** Absent for a row with nowhere safe to go — a non-github PR link. */ + onClick?: () => void; }) { return (