From eb9e3645ed182127b439a1c272e596ec89b50a93 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 25 Oct 2026 02:58:09 +0100 Subject: [PATCH] fix(app): dismiss file link tooltips when leaving their panel Retained chats suspend while their tooltip portals remain mounted. Remove the surface before suspension and clear open state and pending hover timers so returning to the chat does not revive it. --- .../assistant-file-link-tooltip.spec.ts | 69 +++++++++++++++++++ packages/app/src/components/ui/tooltip.tsx | 14 +++- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/app/e2e/browser/assistant-file-link-tooltip.spec.ts diff --git a/packages/app/e2e/browser/assistant-file-link-tooltip.spec.ts b/packages/app/e2e/browser/assistant-file-link-tooltip.spec.ts new file mode 100644 index 0000000000..69cccc52dd --- /dev/null +++ b/packages/app/e2e/browser/assistant-file-link-tooltip.spec.ts @@ -0,0 +1,69 @@ +import { writeFile } from "node:fs/promises"; +import path from "node:path"; +import { test, expect, type Page } from "../support/fixtures"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; + +async function readFileLinkTooltip(page: Page): Promise { + const link = page.getByRole("link", { name: "the guide", exact: true }); + await link.hover(); + await expect(page.getByRole("tooltip")).toContainText("guide.md"); +} + +async function leaveFileLink(page: Page): Promise { + await page.mouse.move(5, 5); + await expect(page.getByRole("tooltip")).toHaveCount(0); +} + +async function openLinkedFile(page: Page): Promise { + await page.getByRole("link", { name: "the guide", exact: true }).click(); + await expect(page.getByTestId("markdown-preview-frame")).toContainText("Example guide"); + await leaveFileLink(page); +} + +async function returnToAgent(page: Page): Promise { + await page.getByRole("button", { name: "Tooltip regression", exact: true }).click(); + await expect(page.getByRole("link", { name: "the guide", exact: true })).toBeVisible(); + await leaveFileLink(page); +} + +async function switchToFileTab(page: Page): Promise { + await page.getByRole("button", { name: "guide.md", exact: true }).click(); + await expect(page.getByTestId("markdown-preview-frame")).toContainText("Example guide"); + await leaveFileLink(page); +} + +test("opening an assistant file link dismisses its tooltip over the file tab", async ({ + page, +}, testInfo) => { + testInfo.setTimeout(120_000); + const agent = await seedMockAgentWorkspace({ + repoPrefix: "tooltip-5811-", + title: "Tooltip regression", + featureValues: { + mockStreamingAssistantResponse: "Read [the guide](./guide.md).", + mockStreamingAssistantIntervalMs: 5, + }, + }); + try { + await writeFile( + path.join(agent.cwd, "guide.md"), + "# Example guide\n\n" + "Plain paragraph.\n\n".repeat(150), + ); + await openAgentRoute(page, agent); + await agent.client.sendAgentMessage(agent.agentId, "Show the file link."); + await agent.client.waitForFinish(agent.agentId, 30_000); + + await readFileLinkTooltip(page); + await leaveFileLink(page); + await readFileLinkTooltip(page); + await openLinkedFile(page); + await returnToAgent(page); + await readFileLinkTooltip(page); + await switchToFileTab(page); + await returnToAgent(page); + await readFileLinkTooltip(page); + await leaveFileLink(page); + } finally { + await agent.cleanup(); + } +}); diff --git a/packages/app/src/components/ui/tooltip.tsx b/packages/app/src/components/ui/tooltip.tsx index ae00768a34..697a80fdcf 100644 --- a/packages/app/src/components/ui/tooltip.tsx +++ b/packages/app/src/components/ui/tooltip.tsx @@ -27,6 +27,7 @@ import { FadeIn, FadeOut } from "react-native-reanimated"; import { StyleSheet } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; import { FloatingSurface } from "@/components/ui/floating"; +import { useRetainedPanelActive } from "@/components/retained-panel"; import { isNative, isWeb } from "@/constants/platform"; import { getOverlayRoot, OVERLAY_Z } from "@/lib/overlay-root"; @@ -256,8 +257,15 @@ export function Tooltip({ }); const isCompact = useIsCompactFormFactor(); + const active = useRetainedPanelActive(); const opensOnPress = isNative || isCompact; - const enabled = opensOnPress ? enabledOnMobile : enabledOnDesktop; + const enabled = active && (opensOnPress ? enabledOnMobile : enabledOnDesktop); + + // Remove the portal in the inactive commit, before the retained chat freezes. + // Closing also prevents the tooltip from returning when its panel is shown again. + useEffect(() => { + if (!active && isOpen) setIsOpen(false); + }, [active, isOpen, setIsOpen]); const value = useMemo( () => ({ @@ -329,6 +337,10 @@ export function TooltipTrigger({ }; }, [clearOpenTimer]); + useEffect(() => { + if (!ctx.enabled) clearOpenTimer(); + }, [ctx.enabled, clearOpenTimer]); + const handleHoverIn = useCallback( (e?: unknown) => { if (isCallable(onHoverIn)) onHoverIn(e);