Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions apps/code/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
157 changes: 24 additions & 133 deletions packages/ui/src/features/sessions/components/ConversationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -75,8 +73,8 @@ import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react";
import { useHotkeys } from "react-hotkeys-hook";

Expand Down Expand Up @@ -108,57 +106,6 @@ export interface ConversationViewProps {
promptRecallRef?: RefObject<PromptRecallHandler | null>;
}

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,
Expand Down Expand Up @@ -187,10 +134,7 @@ export function ConversationView({

const listRef = useRef<VirtualizedListHandle>(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;

Expand Down Expand Up @@ -225,16 +169,14 @@ export function ConversationView({
}
const firstUserMessageId = firstUserMessageIdRef.current;

const initialItemIdsRef = useRef<Set<string> | null>(null);
if (initialItemIdsRef.current === null) {
initialItemIdsRef.current = new Set<string>();
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;
Expand Down Expand Up @@ -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({
Expand All @@ -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 }> = [];
Expand Down Expand Up @@ -352,22 +292,15 @@ 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],
);

useHotkeys(
SHORTCUTS.MESSAGE_JUMP,
() =>
dispatchViewState({
type: "set-jump-picker-open",
value: !jumpPickerOpen,
}),
() => setJumpPickerOpen((prev) => !prev),
THREAD_HOTKEY_OPTIONS,
);

Expand All @@ -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);
}
};

Expand Down Expand Up @@ -607,16 +501,14 @@ export function ConversationView({

<MessageJumpPicker
open={jumpPickerOpen}
onOpenChange={(open) =>
dispatchViewState({ type: "set-jump-picker-open", value: open })
}
onOpenChange={setJumpPickerOpen}
items={items}
onJumpToMessage={handleJumpToMessage}
/>

<SessionTaskIdProvider taskId={taskId}>
<VirtualizedList<ThreadRow>
ref={setListRef}
ref={listRef}
items={threadRows}
getItemKey={getRowKey}
renderItem={renderRow}
Expand All @@ -629,8 +521,7 @@ export function ConversationView({
scrollX={scrollX}
/>
</SessionTaskIdProvider>
<MessageScrollbarRail markers={railMarkers} />
{viewState.showScrollButton && (
{showScrollButton && (
<Box className="absolute right-6 bottom-4 z-10">
<Tooltip>
<TooltipTrigger
Expand Down
12 changes: 0 additions & 12 deletions packages/ui/src/features/sessions/components/VirtualizedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,6 @@ interface VirtualizedListProps<T> {
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;
Expand Down Expand Up @@ -71,7 +63,6 @@ function VirtualizedListInner<T>(
ref: React.ForwardedRef<VirtualizedListHandle>,
) {
const parentRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const footerRef = useRef<HTMLDivElement>(null);
const initializedRef = useRef(false);
const isAtBottomRef = useRef(true);
Expand Down Expand Up @@ -165,8 +156,6 @@ function VirtualizedListInner<T>(
isAtBottomRef.current = false;
virtualizer.scrollToIndex(index, { align: "center" });
},
getScrollElement: () => parentRef.current,
getContentElement: () => contentRef.current,
}),
[virtualizer, settleAtEnd],
);
Expand Down Expand Up @@ -269,7 +258,6 @@ function VirtualizedListInner<T>(
style={{ scrollbarGutter: "stable" }}
>
<div
ref={contentRef}
style={{
height: totalSize,
position: "relative",
Expand Down
Loading
Loading