diff --git a/packages/ui/src/features/sessions/components/chat-thread/ChatThread.tsx b/packages/ui/src/features/sessions/components/chat-thread/ChatThread.tsx index b9fdafb61d..abe9043f0b 100644 --- a/packages/ui/src/features/sessions/components/chat-thread/ChatThread.tsx +++ b/packages/ui/src/features/sessions/components/chat-thread/ChatThread.tsx @@ -1,6 +1,5 @@ import { CaretDown, - ChatCircle, Check, Copy, FileText, @@ -9,7 +8,6 @@ import { import { WorkerPoolContextProvider } from "@pierre/diffs/react"; import { useService } from "@posthog/di/react"; import { - Button, ChatBubble, ChatBubbleContent, ChatMarker, @@ -49,12 +47,23 @@ import { type PromptRecallHandler, } from "@posthog/ui/features/sessions/components/chat-thread/composerPromptRecall"; import { MessageJumpPicker } from "@posthog/ui/features/sessions/components/chat-thread/MessageJumpPicker"; -import { - ToolGroup, - type ToolGroupItem, -} from "@posthog/ui/features/sessions/components/chat-thread/ToolGroup"; +import { StickyHeaderOverlay } from "@posthog/ui/features/sessions/components/chat-thread/ThreadStickyHeader"; +import { ToolGroup } from "@posthog/ui/features/sessions/components/chat-thread/ToolGroup"; import { THREAD_HOTKEY_OPTIONS } from "@posthog/ui/features/sessions/components/chat-thread/threadHotkeys"; +import { + type AgentTurn, + CHAT_THREAD_VIRTUALIZATION_THRESHOLD, + completedTurnTimestamp, + countFlatRows, + type FlatThreadRow, + flattenTurnRows, + SCROLL_PREVIOUS_ITEM_PEEK, + type ThreadItem, + type ThreadScrollResume, + type TurnRow, +} from "@posthog/ui/features/sessions/components/chat-thread/threadVirtualization"; import { usePromptRecallSource } from "@posthog/ui/features/sessions/components/chat-thread/usePromptRecallSource"; +import { VirtualThreadScrollBody } from "@posthog/ui/features/sessions/components/chat-thread/VirtualThreadScrollBody"; import { GitActionMessage } from "@posthog/ui/features/sessions/components/GitActionMessage"; import { GitActionResult } from "@posthog/ui/features/sessions/components/GitActionResult"; import { mergeConversationItems } from "@posthog/ui/features/sessions/components/mergeConversationItems"; @@ -90,7 +99,6 @@ import { type DiffWorkerFactory, } from "@posthog/ui/shell/diffWorkerHost"; import { IconButton, Tooltip } from "@radix-ui/themes"; -import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { memo, type ReactNode, @@ -104,18 +112,6 @@ import { } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -/** A row is either a parsed conversation item or a synthesized group of tool calls. */ -type ThreadItem = ConversationItem | ToolGroupItem; - -/** - * A contiguous run of non-user rows (assistant prose, tools, git actions, ...) shown as one - * `bg-muted/30` block with tight internal spacing. Broken only by a user message. - */ -type AgentTurn = { type: "agent_turn"; id: string; items: ThreadItem[] }; - -/** Top-level row: a standalone user message, or a grouped agent turn. */ -type TurnRow = ThreadItem | AgentTurn; - type SessionUpdateItem = Extract; function isToolCallItem(item: ConversationItem): item is SessionUpdateItem { @@ -472,119 +468,6 @@ function MessageCopyButton({ ); } -/** - * "Fake sticky" header. A real `position: sticky` row can't hand off in this flat list (every row - * shares one containing block, so they'd pile at the top) and sticking causes reflow. Instead we - * overlay a single header, out of flow, pinned over the viewport top — showing the current turn's - * user message (the engine's anchor) once the real one has scrolled off. Click to scroll back to it. - * - * Only this small component subscribes to the engine's per-scroll visibility state, so the rows - * themselves never re-render on scroll. - */ -function StickyHeaderOverlay({ items }: { items: ConversationItem[] }) { - const { currentAnchorId } = useChatMessageScrollerVisibility(); - const { scrollToMessage } = useChatMessageScroller(); - const shouldReduceMotion = useReducedMotion(); - const [dismissedId, setDismissedId] = useState(null); - const [offscreen, setOffscreen] = useState(false); - // Anchor element used only to locate the enclosing scroller/viewport in the DOM. - const probeRef = useRef(null); - - const active = items.find( - (i): i is Extract => - i.id === currentAnchorId && i.type === "user_message", - ); - const activeId = active?.id ?? null; - - // The engine's `visibleMessageIds` can't be used here: its IntersectionObserver excludes a band of - // `scrollPreviousItemPeek` px at the viewport top, which is exactly where a freshly-anchored turn - // message lands — so it reads as "not visible" while plainly on screen. Measure real geometry - // instead: the message is off-screen only once its bottom scrolls above the viewport top. - useEffect(() => { - // No reset when there's no anchor: the overlay render already guards on `active != null`, so a - // stale `offscreen` is never shown, and a fresh anchor re-measures synchronously below. (Avoids - // the prop-sync-in-effect pattern react-doctor flags.) - if (activeId == null) return; - const viewport = probeRef.current - ?.closest('[data-slot="chat-message-scroller"]') - ?.querySelector('[data-slot="chat-message-scroller-viewport"]'); - if (!viewport) return; - - const measure = () => { - const el = viewport.querySelector( - `[data-message-id="${CSS.escape(activeId)}"]`, - ); - if (!el) { - setOffscreen(false); - return; - } - const messageBottom = el.getBoundingClientRect().bottom; - const viewportTop = viewport.getBoundingClientRect().top; - setOffscreen(messageBottom <= viewportTop + 4); - }; - - measure(); - viewport.addEventListener("scroll", measure, { passive: true }); - return () => viewport.removeEventListener("scroll", measure); - }, [activeId]); - - // Once the real message is back on screen, clear the dismissal so the header can return later. - useEffect(() => { - if (!offscreen) setDismissedId(null); - }, [offscreen]); - - const dismiss = (id: string) => { - // Hide immediately on click (don't wait for the scroll to bring the message into view), then - // jump to it. - setDismissedId(id); - scrollToMessage(id); - }; - - return ( - <> -