Skip to content
Draft
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
1 change: 0 additions & 1 deletion src/engines/ChatPanel/ChatHistory/ChatHistory.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export interface ChatHistoryProps {
onScrollNavChange?: (state: ScrollNavState) => void;
followAgentNav?: FollowAgentNavState;
browserAddToConversationNav?: BrowserAddToConversationNavState;
onRegisterSearchOpen?: (handler: (() => void) | null) => void;
displayMode?: ChatHistoryDisplayMode;
turnPaginationEnabled?: boolean;
/** Optional external host for pinned/pagination chrome, outside the scroll body subtree. */
Expand Down
105 changes: 94 additions & 11 deletions src/engines/ChatPanel/ChatHistory/components/ChatHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import { PlanningFooter } from "@src/engines/ChatPanel/blocks/primitives";
import { CHAT_PANEL_TRANSCRIPT_TOP_PADDING_PX } from "@src/engines/ChatPanel/header/chatPanelHeaderLayout";

import type { OptimizedChatItem } from "../chatItemPipeline/types";
import {
findChatSearchTargetElement,
scrollSearchTargetIntoView,
} from "../hooks/chatSearch";
import { getUnloadedTurnMeta } from "../hooks/useChatGroups";
import { GroupItemRenderer } from "../renderers";
import { useChatHistoryListActiveGroupReporter } from "./ChatHistoryListActiveGroupReporter";
Expand Down Expand Up @@ -112,6 +116,13 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
flatItemsRef.current = flatItems;
const previousChatItemsRef = useRef<(OptimizedChatItem | undefined)[]>([]);

const turnIdsRef = useRef(turnIds);
turnIdsRef.current = turnIds;
const assistantCopyEventIdsByGroupRef = useRef(
assistantCopyEventIdsByGroup
);
assistantCopyEventIdsByGroupRef.current = assistantCopyEventIdsByGroup;

// When the planning indicator is active, inject it as a virtual item
// in the last group so it renders under the latest turn's header —
// not as the global Virtuoso Footer which visually attaches to the
Expand Down Expand Up @@ -156,6 +167,44 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
groupRenderKeys[index] ?? `chat-group-index:${index}`,
});
const virtualItems = virtualizer.getVirtualItems();
const rowResizeObserverRef = useRef<ResizeObserver | null>(null);
const measuredRowHeightsRef = useRef(new WeakMap<Element, number>());
const observedRowsRef = useRef(new Set<Element>());
const measureVirtualRow = useCallback(
(node: HTMLDivElement | null) => {
virtualizer.measureElement(node);
if (!node) return;
if (!rowResizeObserverRef.current) {
rowResizeObserverRef.current = new ResizeObserver((entries) => {
for (const entry of entries) {
const target = entry.target;
const nextHeight =
entry.borderBoxSize[0]?.blockSize ??
target.getBoundingClientRect().height;
if (measuredRowHeightsRef.current.get(target) === nextHeight) {
continue;
}
measuredRowHeightsRef.current.set(target, nextHeight);
virtualizer.measureElement(target as HTMLElement);
}
});
}
if (!observedRowsRef.current.has(node)) {
observedRowsRef.current.add(node);
rowResizeObserverRef.current.observe(node);
}
},
[virtualizer]
);

useEffect(() => {
const observedRows = observedRowsRef.current;
return () => {
rowResizeObserverRef.current?.disconnect();
rowResizeObserverRef.current = null;
observedRows.clear();
};
}, [virtualListDataKey]);

useEffect(() => {
if (virtualItems.length === 0) return;
Expand All @@ -175,10 +224,6 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
useImperativeHandle(
virtualListRef,
() => ({
scrollToIndex: ({ index, behavior = "auto", align = "center" }) => {
const groupIndex = flatIndexToGroupIndex[index] ?? 0;
virtualizer.scrollToIndex(groupIndex, { align, behavior });
},
scrollToGroup: ({ groupIndex, behavior = "smooth" }) => {
const boundedGroupIndex = Math.max(
0,
Expand All @@ -202,12 +247,55 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
behavior,
});
},
scrollToChatTarget: ({
eventId,
itemId,
flatIndex,
behavior = "auto",
}) => {
const scrollRoot =
virtualScrollerRef.current ?? staticScrollerRef?.current;
if (!scrollRoot) return;

const scrollToDomTarget = (): boolean => {
const target = findChatSearchTargetElement(scrollRoot, {
eventId,
itemId,
flatIndex,
});
if (!target) return false;
scrollSearchTargetIntoView(scrollRoot, target, behavior);
return true;
};

if (scrollToDomTarget()) return;

if (
flatIndex === undefined ||
scrollRoot !== virtualScrollerRef.current
) {
return;
}

const groupIndex = flatIndexToGroupIndex[flatIndex] ?? 0;
virtualizer.scrollToIndex(groupIndex, {
align: "start",
behavior: "auto",
});

window.requestAnimationFrame(() => {
if (!scrollToDomTarget()) {
window.requestAnimationFrame(scrollToDomTarget);
}
});
},
}),
[
flatIndexToGroupIndex,
staticScrollerRef,
virtualGroups.length,
virtualizer,
virtualScrollerRef,
]
);
const rowGroupMeta = useMemo(
Expand All @@ -217,12 +305,6 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
);
const rowGroupMetaRef = useRef<RowGroupMeta[]>(rowGroupMeta);
rowGroupMetaRef.current = rowGroupMeta;
const turnIdsRef = useRef(turnIds);
turnIdsRef.current = turnIds;
const assistantCopyEventIdsByGroupRef = useRef(
assistantCopyEventIdsByGroup
);
assistantCopyEventIdsByGroupRef.current = assistantCopyEventIdsByGroup;

// For each flat index, the nearest preceding qualifying item — non-structural,
// non-unloaded, with an event. Pre-computed once per flatItems change so
Expand Down Expand Up @@ -332,6 +414,7 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
hideActiveGroupHeader,
onActiveGroupIndexChange,
});

const setScrollContainerRef = useCallback(
(node: HTMLDivElement | null) => {
if (useStaticRendering) {
Expand Down Expand Up @@ -439,7 +522,7 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = memo(
groupRenderKeys[group.groupIndex] ??
`chat-group-index:${group.groupIndex}`
}
ref={virtualizer.measureElement}
ref={measureVirtualRow}
data-index={virtualItem.index}
data-chat-group-index={group.groupIndex}
className="absolute left-0 top-0 w-full"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import type { GroupHeaderRenderPart } from "../renderers/GroupHeaderRenderer";
export type EventSummary = NonNullable<OptimizedChatItem["event"]>;

export interface ChatHistoryListHandle {
scrollToIndex: (options: {
index: number;
behavior?: ScrollBehavior;
align?: "start" | "center" | "end" | "auto";
}) => void;
scrollToGroup: (options: {
groupIndex: number;
behavior?: ScrollBehavior;
}) => void;
scrollToChatTarget: (options: {
eventId?: string;
itemId?: string;
flatIndex?: number;
behavior?: ScrollBehavior;
}) => void;
}

export interface ChatHistoryListProps {
Expand Down
40 changes: 22 additions & 18 deletions src/engines/ChatPanel/ChatHistory/components/ChatHistoryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { useChatHistoryItemActions } from "../hooks/useChatHistoryItemActio
import type { useChatHistoryProjectionModel } from "../hooks/useChatHistoryProjectionModel";
import type { UseChatHistoryStateReturn } from "../hooks/useChatHistoryState";
import type { useChatNavigationController } from "../hooks/useChatNavigationController";
import type { UseChatSearchIntegrationReturn } from "../hooks/useChatSearchIntegration";
import type { UseChatSearchReturn } from "../hooks/useChatSearch";
import type { useChatViewportController } from "../hooks/useChatViewportController";
import { useGroupHeaderRenderer } from "../hooks/useGroupHeaderRenderer";
import type { useReloadSession } from "../hooks/useReloadSession";
Expand Down Expand Up @@ -73,7 +73,7 @@ interface ChatHistoryViewProps {
pinnedHeaderPortalHost: HTMLElement | null;
planningIndicatorScope: { sessionId: string; isLive: boolean } | null;
projection: ProjectionModel;
search: UseChatSearchIntegrationReturn;
search: UseChatSearchReturn;
surfaceBgClass: string;
turnPaginationEnabled: boolean;
viewport: ViewportModel;
Expand Down Expand Up @@ -197,12 +197,6 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({
handleRegenerateGroup,
handleSubmitAnswers,
} = actions;
const {
search: searchState,
isSearchVisible,
searchBarRef,
handleCloseSearch,
} = search;

const getIsWpGeneWorking = useCallback(
() => isWpGeneWorkingRef.current ?? false,
Expand All @@ -212,11 +206,11 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({
() => isExploringRef.current ?? false,
[isExploringRef]
);
const hasCloudDownloadProgress = useCloudSessionHasDownloadSurface(activeId);
const assistantCopyEventIdsByGroup = useMemo(
() => displayGroupMeta.map((meta) => meta.assistantCopyEventIds),
[displayGroupMeta]
);
const hasCloudDownloadProgress = useCloudSessionHasDownloadSurface(activeId);

const renderGroupHeader = useGroupHeaderRenderer({
displaySourceGroupIndices,
Expand Down Expand Up @@ -313,6 +307,23 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({
}
/>
);
const pinnedChromeLayer = (
<>
{search.isSearchVisible ? (
<div
className={`flex-shrink-0 border-b border-border-2 ${surfaceBgClass}`}
data-chat-search-chrome
>
<div
className={`mx-auto w-full ${DETAIL_PANEL_TOKENS.contentMaxWidth}`}
>
<ChatSearchBar search={search} />
</div>
</div>
) : null}
{pinnedHeaderLayer}
</>
);

return (
<ChatHistoryDisplayModeProvider value={displayMode}>
Expand All @@ -330,24 +341,17 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({
<SessionHeader sessionInfo={sessionInfo} />
</div>

<ChatSearchBar
ref={searchBarRef}
search={searchState}
isVisible={isSearchVisible}
onClose={handleCloseSearch}
/>

{pinnedHeaderPortalHost
? createPortal(
<div
className="chat-history-portal"
style={chatHistoryContainerStyle}
>
{pinnedHeaderLayer}
{pinnedChromeLayer}
</div>,
pinnedHeaderPortalHost
)
: pinnedHeaderLayer}
: pinnedChromeLayer}

{/* Anchor cloud-download progress to the chat-pane header edge instead
of the virtualized body below SessionHeader. Transcript items and
Expand Down
Loading
Loading