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);