diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index a607126f14..5d93040e53 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -344,14 +344,6 @@ snapshots: hash: v1.k4693efd2.602ee87d6d7a745d4b09d9e49bd12a70e52299b78ca97a48b5ca73a01532d801.F_1XKDzCxzMm_opEt9FvzczBb_2n-vyvOyGFXHGJmTk features-sessions-conversationview--with-pending-prompt--light: hash: v1.k4693efd2.1636dfc3ee0dd9d28065166f2fff0193f2d499692c74749d3afa7585aae898b5.YxTPa4CsqI2YuGLUdck8340ITZQrpna_C6z-SgII4Ws - features-sessions-scrollbarrail--pure--dark: - hash: v1.k4693efd2.6baec21b1942848a3a376d2b90ca106b2c1b94de74382b4cd05673695d2649ee.UDDPtLDf1J2E3d3Oiy9is77Nm5xRSIfB7q5z0uUZRSQ - features-sessions-scrollbarrail--pure--light: - hash: v1.k4693efd2.33f02943fdbb1ea9774881f8909b580ab03ba02749614c533af1bf51dd817dca.ieP2bhDz0U_PTB4M6Zplvit0XlSTFiUn083GYBFNuC0 - features-sessions-scrollbarrail--scrollable-conversation--dark: - hash: v1.k4693efd2.203b62b52d6365d6ec222f7529c35b3f46b17cd07ca792a8ce2de594af4d589b.3dfpnVM8yxor1PNO5aPk0pxn5THZjK4otA6mbz3XhtA - features-sessions-scrollbarrail--scrollable-conversation--light: - hash: v1.k4693efd2.fa7ef9cbdb09aef6108efcd3606cee32d889b3ad3b8f8be1ac637aed4e80cc72.0XcIKzM-sZHVG9hTZkAHdnBIPSrBiKmjq56QRQ6nasc features-sessions-toolcallblock--create-new-file--dark: hash: v1.k4693efd2.8847f96037682460aba47f0e0f5113037918235fc98596933f2d1d06fd6f8f92.SHetexoVOtozsZGZQMcVbmpBlbk7M5D5Q0R_TJvPnyc features-sessions-toolcallblock--create-new-file--light: diff --git a/packages/ui/src/features/sessions/components/ConversationView.tsx b/packages/ui/src/features/sessions/components/ConversationView.tsx index eed2c93f66..5a049e930a 100644 --- a/packages/ui/src/features/sessions/components/ConversationView.tsx +++ b/packages/ui/src/features/sessions/components/ConversationView.tsx @@ -34,8 +34,6 @@ import type { CollapseMode } from "@posthog/ui/features/sessions/components/new- import { createIncrementalThreadGrouper } from "@posthog/ui/features/sessions/components/new-thread/incrementalThreadGrouping"; import { ToolCallGroupChip } from "@posthog/ui/features/sessions/components/new-thread/ToolCallGroupChip"; import { SessionFooter } from "@posthog/ui/features/sessions/components/SessionFooter"; -import { MessageScrollbarRail } from "@posthog/ui/features/sessions/components/scrollbar-rail/MessageScrollbarRail"; -import { useMessageRailMarkers } from "@posthog/ui/features/sessions/components/scrollbar-rail/useMessageRailMarkers"; import { type RenderItem, SessionUpdateView, @@ -75,8 +73,8 @@ import { useCallback, useEffect, useMemo, - useReducer, useRef, + useState, } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -108,57 +106,6 @@ export interface ConversationViewProps { promptRecallRef?: RefObject; } -interface ConversationViewState { - showScrollButton: boolean; - jumpPickerOpen: boolean; - keyboardFocusedMessageId: string | null; - scrollElements: { - scrollEl: HTMLElement | null; - contentEl: HTMLElement | null; - }; -} - -type ConversationViewAction = - | { type: "set-show-scroll-button"; value: boolean } - | { type: "set-jump-picker-open"; value: boolean } - | { type: "set-keyboard-focused-message"; value: string | null } - | { - type: "set-scroll-elements"; - value: ConversationViewState["scrollElements"]; - }; - -const INITIAL_CONVERSATION_VIEW_STATE: ConversationViewState = { - showScrollButton: false, - jumpPickerOpen: false, - keyboardFocusedMessageId: null, - scrollElements: { scrollEl: null, contentEl: null }, -}; - -function conversationViewReducer( - state: ConversationViewState, - action: ConversationViewAction, -): ConversationViewState { - switch (action.type) { - case "set-show-scroll-button": - return state.showScrollButton === action.value - ? state - : { ...state, showScrollButton: action.value }; - case "set-jump-picker-open": - return state.jumpPickerOpen === action.value - ? state - : { ...state, jumpPickerOpen: action.value }; - case "set-keyboard-focused-message": - return state.keyboardFocusedMessageId === action.value - ? state - : { ...state, keyboardFocusedMessageId: action.value }; - case "set-scroll-elements": - return state.scrollElements.scrollEl === action.value.scrollEl && - state.scrollElements.contentEl === action.value.contentEl - ? state - : { ...state, scrollElements: action.value }; - } -} - export function ConversationView({ events, isPromptPending, @@ -187,10 +134,7 @@ export function ConversationView({ const listRef = useRef(null); const isAtBottomRef = useRef(true); - const [viewState, dispatchViewState] = useReducer( - conversationViewReducer, - INITIAL_CONVERSATION_VIEW_STATE, - ); + const [showScrollButton, setShowScrollButton] = useState(false); const debugLogsCloudRuns = useSettingsStore((s) => s.debugLogsCloudRuns); const showDebugLogs = debugLogsCloudRuns; @@ -225,16 +169,14 @@ export function ConversationView({ } const firstUserMessageId = firstUserMessageIdRef.current; - const initialItemIdsRef = useRef | null>(null); - if (initialItemIdsRef.current === null) { - initialItemIdsRef.current = new Set(); - for (const item of conversationItems) { - if (item.type === "user_message") { - initialItemIdsRef.current.add(item.id); - } - } - } - const initialItemIds = initialItemIdsRef.current; + const [initialItemIds] = useState( + () => + new Set( + conversationItems + .filter((i) => i.type === "user_message") + .map((i) => i.id), + ), + ); const pendingPermissions = usePendingPermissionsForTask(taskId ?? ""); const pendingPermissionsCount = pendingPermissions.size; @@ -293,10 +235,6 @@ export function ConversationView({ id != null ? itemIdToRowIndexRef.current.get(id) : undefined; listRef.current?.scrollToIndex(rowIdx ?? index); }, - // Search doesn't read scroll/content geometry, so these forward to the real - // handle purely to satisfy the (now-extended) VirtualizedListHandle shape. - getScrollElement: () => listRef.current?.getScrollElement() ?? null, - getContentElement: () => listRef.current?.getContentElement() ?? null, }); const search = useConversationSearch({ @@ -305,8 +243,10 @@ export function ConversationView({ listRef: searchListRef, }); - const { jumpPickerOpen, keyboardFocusedMessageId, scrollElements } = - viewState; + const [jumpPickerOpen, setJumpPickerOpen] = useState(false); + const [keyboardFocusedMessageId, setKeyboardFocusedMessageId] = useState< + string | null + >(null); const userMessages = useMemo(() => { const result: Array<{ id: string; index: number; content: string }> = []; @@ -352,10 +292,7 @@ export function ConversationView({ if (!nextMessage) return; useSettingsStore.getState().markHintLearned(PROMPT_RECALL_HINT_KEY); - dispatchViewState({ - type: "set-keyboard-focused-message", - value: nextMessage.id, - }); + setKeyboardFocusedMessageId(nextMessage.id); scrollToUserMessage(nextMessage.id, nextMessage.index); }, [keyboardFocusedMessageId, userMessages, scrollToUserMessage], @@ -363,11 +300,7 @@ export function ConversationView({ useHotkeys( SHORTCUTS.MESSAGE_JUMP, - () => - dispatchViewState({ - type: "set-jump-picker-open", - value: !jumpPickerOpen, - }), + () => setJumpPickerOpen((prev) => !prev), THREAD_HOTKEY_OPTIONS, ); @@ -384,73 +317,34 @@ export function ConversationView({ ); const clearKeyboardFocus = useCallback(() => { - dispatchViewState({ type: "set-keyboard-focused-message", value: null }); + setKeyboardFocusedMessageId(null); }, []); const handleJumpToMessage = useCallback( (id: string) => { const message = userMessages.find((entry) => entry.id === id); if (!message) return; - dispatchViewState({ type: "set-keyboard-focused-message", value: id }); + setKeyboardFocusedMessageId(id); scrollToUserMessage(id, message.index); }, [userMessages, scrollToUserMessage], ); - // The scrollbar marker rail needs the VirtualizedList's scroll + content - // elements. Resolve them from the list ref callback so the rail renders as - // soon as the imperative handle is attached, without mount-time state setup. - const setListRef = useCallback((handle: VirtualizedListHandle | null) => { - listRef.current = handle; - dispatchViewState({ - type: "set-scroll-elements", - value: { - scrollEl: handle?.getScrollElement() ?? null, - contentEl: handle?.getContentElement() ?? null, - }, - }); - }, []); - - const railUserMessages = useMemo( - () => - userMessages.map((m) => ({ - id: m.id, - content: m.content, - index: m.index, - })), - [userMessages], - ); - const railMarkers = useMessageRailMarkers({ - contentEl: scrollElements.contentEl, - scrollEl: scrollElements.scrollEl, - userMessages: railUserMessages, - onJump: (id) => { - const message = userMessages.find((entry) => entry.id === id); - if (!message) return; - dispatchViewState({ type: "set-keyboard-focused-message", value: id }); - scrollToUserMessage(id, message.index); - }, - activeId: keyboardFocusedMessageId, - }); - const handleScrollStateChange = useCallback((isAtBottom: boolean) => { isAtBottomRef.current = isAtBottom; - dispatchViewState({ - type: "set-show-scroll-button", - value: !isAtBottom, - }); + setShowScrollButton(!isAtBottom); }, []); const scrollToBottom = useCallback(() => { listRef.current?.scrollToBottom(); - dispatchViewState({ type: "set-show-scroll-button", value: false }); + setShowScrollButton(false); }, []); useEffect(() => { const handleVisibilityChange = () => { if (!document.hidden && isAtBottomRef.current) { listRef.current?.scrollToBottom(); - dispatchViewState({ type: "set-show-scroll-button", value: false }); + setShowScrollButton(false); } }; @@ -607,16 +501,14 @@ export function ConversationView({ - dispatchViewState({ type: "set-jump-picker-open", value: open }) - } + onOpenChange={setJumpPickerOpen} items={items} onJumpToMessage={handleJumpToMessage} /> - ref={setListRef} + ref={listRef} items={threadRows} getItemKey={getRowKey} renderItem={renderRow} @@ -629,8 +521,7 @@ export function ConversationView({ scrollX={scrollX} /> - - {viewState.showScrollButton && ( + {showScrollButton && ( { export interface VirtualizedListHandle { scrollToBottom: () => void; scrollToIndex: (index: number) => void; - /** The scrolling element (the `overflow-y-auto` viewport). Exposed so a - * scrollbar marker rail can read `scrollTop`/`scrollHeight` and refresh on - * scroll. `null` until the list mounts. */ - getScrollElement: () => HTMLDivElement | null; - /** The inner content element whose height == the virtual total size. Rows - * (and their `data-conversation-item-id` stamps) live inside this, so a - * marker rail measures row offsets against it. `null` until the list mounts. */ - getContentElement: () => HTMLDivElement | null; } const AT_BOTTOM_THRESHOLD = 50; @@ -71,7 +63,6 @@ function VirtualizedListInner( ref: React.ForwardedRef, ) { const parentRef = useRef(null); - const contentRef = useRef(null); const footerRef = useRef(null); const initializedRef = useRef(false); const isAtBottomRef = useRef(true); @@ -165,8 +156,6 @@ function VirtualizedListInner( isAtBottomRef.current = false; virtualizer.scrollToIndex(index, { align: "center" }); }, - getScrollElement: () => parentRef.current, - getContentElement: () => contentRef.current, }), [virtualizer, settleAtEnd], ); @@ -269,7 +258,6 @@ function VirtualizedListInner( style={{ scrollbarGutter: "stable" }} >
-