From 9903a756b9dd0f9146a48ac5f75aa64287ea49c9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 03:24:21 +0000 Subject: [PATCH 1/2] test(web): adjacent subagent previews share the hover delay Reproduces #34144: timeline subagent previews have no shared Tooltip.Provider, so moving to an adjacent subagent waits the full hover delay again instead of opening instantly like sidebar rows. AI-assisted; verified with the tests and tools listed above. --- .../chat/MessagesTimeline.browser.test.tsx | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) 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", () => { From 9a96e82529d8d360bc8393f9bceab6fb2cef675e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 03:44:21 +0000 Subject: [PATCH 2/2] fix(web): adjacent subagent previews open without repeating the hover delay Timeline subagent rows and the thread details lineage rows each had their own 200 ms trigger delay but no shared Tooltip.Provider, so Base UI could not group them and every adjacent hover waited the full delay. Wrap the timeline and the relationships panel in a TooltipProvider; triggers keep their own first-open delay, and neighbours now open instantly like the sidebar's thread previews. Fixes pingdotgg/t3code#34144 AI-assisted; verified with the tests and tools listed above. --- .../src/components/chat/MessagesTimeline.tsx | 154 +++++++++--------- .../chat/ThreadRelationshipsControl.tsx | 6 +- 2 files changed, 81 insertions(+), 79 deletions(-) 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} - + ); }