diff --git a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx
index 0ca1f46f5d7ea..fad8dae5e869b 100644
--- a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx
@@ -2,7 +2,7 @@ import { injectHtmlRenderBootstrap } from "@t3tools/shared/htmlRender";
import { HtmlRenderDocument } from "../files/BrowserDocumentFrame";
import { flushSync } from "react-dom";
import { AudioPreview } from "../files/AudioPreview";
-import { cdp, page } from "vite-plus/test/browser";
+import { cdp, page, userEvent } from "vite-plus/test/browser";
import {
ComposerContextId,
EnvironmentId,
@@ -434,6 +434,70 @@ describe("MessagesTimeline work rows", () => {
}
},
);
+
+ it("opens adjacent subagent previews without repeating the hover delay", async () => {
+ setup();
+ const now = DateTime.makeUnsafe(CREATED_AT);
+ const task = (id: string, title: string): OrchestrationV2Subagent => ({
+ id: NodeId.make(id),
+ threadId: ThreadId.make("thread-1"),
+ runId: null,
+ parentNodeId: NodeId.make("parent-root"),
+ origin: "app_owned",
+ createdBy: "agent",
+ driver: ProviderDriverKind.make("claudeAgent"),
+ providerInstanceId: ProviderInstanceId.make("claudeAgent"),
+ providerThreadId: null,
+ childThreadId: ThreadId.make(`child-${id}`),
+ nativeTaskRef: null,
+ prompt: title,
+ title,
+ model: null,
+ status: "completed",
+ result: `${title} result`,
+ startedAt: now,
+ completedAt: now,
+ updatedAt: now,
+ });
+ const tasks = [task("first", "Review parser"), task("second", "Review renderer")];
+ using _threadAtom = vi.spyOn(environmentThreadDetails, "threadAtom").mockReturnValue(
+ Atom.make({
+ projection: { subagents: tasks, turnItems: [], visibleTurnItems: [] },
+ }) as never,
+ );
+ await using screen = await renderTimeline(
+ tasks.map((agent) =>
+ work(`notification-${agent.id}`, {
+ label: "Notification fallback",
+ tone: "info",
+ itemType: "notification",
+ projectedItem: v2Item("notification", {
+ id: `notification-${agent.id}`,
+ source: {
+ kind: "delegated_task",
+ childThreadId: agent.childThreadId!,
+ taskIds: [agent.id],
+ },
+ outcome: "completed",
+ summary: "Notification fallback",
+ }),
+ }),
+ ),
+ );
+ const openPopup = (title: string) => {
+ const popup = [...document.querySelectorAll('[data-slot="tooltip-popup"][data-open]')].find(
+ (element) => element.textContent?.includes(title),
+ );
+ if (!popup) throw new Error(`No open preview for ${title}`);
+ return popup;
+ };
+ await userEvent.hover(screen.button("Open Review parser"));
+ await vi.waitFor(() => openPopup("Review parser"));
+ await userEvent.hover(screen.button("Open Review renderer"));
+ const popup = await vi.waitFor(() => openPopup("Review renderer"));
+ // Base UI marks a popup that a Tooltip.Provider group opened without its delay.
+ expect(popup.getAttribute("data-instant")).toBe("delay");
+ });
});
describe("MessagesTimeline messages", () => {
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
index 3b8db4484132d..6f4c699ce55ac 100644
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -90,7 +90,7 @@ import {
} from "./MessagesTimeline.logic";
import { type ThreadFindMatch } from "./threadFind";
import { useThreadFindNavigation } from "./useThreadFindNavigation";
-import { TooltipScrollDismissArea } from "../ui/tooltip";
+import { TooltipProvider, TooltipScrollDismissArea } from "../ui/tooltip";
import { WorktreeSetupCard } from "./WorktreeSetupCard";
import { useOpenPrLink } from "~/lib/openPullRequestLink";
import { useMediaQuery } from "~/hooks/useMediaQuery";
@@ -1293,82 +1293,84 @@ const ConversationTimeline = memo(function ConversationTimeline({
{activityAnnouncer}
-
- {onCiteAssistantText && citationThreadRef ? (
-
+
+ {onCiteAssistantText && citationThreadRef ? (
+
+ ) : null}
+
+ ref={setTimelineList}
+ data={rows}
+ extraData={listExtraData}
+ keyExtractor={keyExtractor}
+ getItemType={getItemType}
+ renderItem={renderItem}
+ estimatedItemSize={90}
+ initialScrollAtEnd={
+ !findActive && citationRequest === null && rememberedPosition?.atEnd !== false
+ }
+ // Legend needs a data refresh to mount new pins without a scroll event.
+ dataVersion={`${readyCitationRequest?.key ?? listIdentityKey}:${alwaysRenderKeys.join(",")}`}
+ {...(alwaysRender ? { alwaysRender } : {})}
+ onLoad={handleListLoad}
+ {...(anchoredEndSpace ? { anchoredEndSpace } : {})}
+ contentInsetEndAdjustment={anchoredEndSpace ? contentInsetEndAdjustment : 0}
+ maintainScrollAtEnd={
+ citationPositioning ||
+ (restoringThreadPosition && rememberedPosition?.atEnd === false) ||
+ anchoredEndSpace ||
+ !liveFollowEnabled ||
+ fullscreenAppRowId !== null
+ ? false
+ : isWorking && !prefersReducedMotion && settlingListIdentity === null
+ ? TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH
+ : TIMELINE_MAINTAIN_SCROLL_AT_END
+ }
+ maintainVisibleContentPosition={
+ findActive ||
+ citationPositioning ||
+ (restoringThreadPosition && rememberedPosition?.atEnd === false)
+ ? false
+ : maintainVisibleContentPosition
+ }
+ maintainScrollAtEndThreshold={1}
+ onScroll={handleScroll}
+ onItemSizeChanged={scheduleMinimapRefresh}
+ onMetricsChange={scheduleMinimapRefresh}
+ className={cn(
+ "messages-timeline-scroll scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain [overflow-anchor:none]",
+ topFadeEnabled && "topbar-scroll-fade",
+ )}
+ ListHeaderComponent={listHeader}
+ ListFooterComponent={timelineListFooter}
/>
- ) : null}
-
- ref={setTimelineList}
- data={rows}
- extraData={listExtraData}
- keyExtractor={keyExtractor}
- getItemType={getItemType}
- renderItem={renderItem}
- estimatedItemSize={90}
- initialScrollAtEnd={
- !findActive && citationRequest === null && rememberedPosition?.atEnd !== false
- }
- // Legend needs a data refresh to mount new pins without a scroll event.
- dataVersion={`${readyCitationRequest?.key ?? listIdentityKey}:${alwaysRenderKeys.join(",")}`}
- {...(alwaysRender ? { alwaysRender } : {})}
- onLoad={handleListLoad}
- {...(anchoredEndSpace ? { anchoredEndSpace } : {})}
- contentInsetEndAdjustment={anchoredEndSpace ? contentInsetEndAdjustment : 0}
- maintainScrollAtEnd={
- citationPositioning ||
- (restoringThreadPosition && rememberedPosition?.atEnd === false) ||
- anchoredEndSpace ||
- !liveFollowEnabled ||
- fullscreenAppRowId !== null
- ? false
- : isWorking && !prefersReducedMotion && settlingListIdentity === null
- ? TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH
- : TIMELINE_MAINTAIN_SCROLL_AT_END
- }
- maintainVisibleContentPosition={
- findActive ||
- citationPositioning ||
- (restoringThreadPosition && rememberedPosition?.atEnd === false)
- ? false
- : maintainVisibleContentPosition
- }
- maintainScrollAtEndThreshold={1}
- onScroll={handleScroll}
- onItemSizeChanged={scheduleMinimapRefresh}
- onMetricsChange={scheduleMinimapRefresh}
- className={cn(
- "messages-timeline-scroll scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain [overflow-anchor:none]",
- topFadeEnabled && "topbar-scroll-fade",
- )}
- ListHeaderComponent={listHeader}
- ListFooterComponent={timelineListFooter}
- />
- {
- navigateTimeline();
- void listRef.current?.scrollToIndex({
- index: item.rowIndex,
- animated: !window.matchMedia("(prefers-reduced-motion: reduce)").matches,
- viewOffset: 24,
- });
- }}
- />
-
+ {
+ navigateTimeline();
+ void listRef.current?.scrollToIndex({
+ index: item.rowIndex,
+ animated: !window.matchMedia("(prefers-reduced-motion: reduce)").matches,
+ viewOffset: 24,
+ });
+ }}
+ />
+
+
>
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
index 102d9734ded92..753552b9be311 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
@@ -84,7 +84,7 @@ import { ThreadLineageWorkflowRow } from "./ThreadLineageWorkflowRow";
import { openSideChatTab } from "../../lib/sideChatPane";
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu";
-import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { toastManager } from "../ui/toast";
import { Spinner } from "../ui/spinner";
import { cn } from "~/lib/utils";
@@ -941,7 +941,7 @@ export function ThreadRelationshipsPanel(props: {
};
return (
- <>
+
{lineageVisible ? (
) : null}
- >
+
);
}