mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 02:32:47 +00:00
fix(markdown): restore file link hover tooltips on web
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { waitForPermissionPrompt } from "../support/helpers/permissions";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
@@ -70,6 +71,11 @@ test("chat and file preview share the library renderer and resolve file links",
|
||||
await expect(preview.locator(".enrm-text")).toContainText("Second paragraph.");
|
||||
await expect(preview.getByRole("table")).toContainText("42");
|
||||
await page.getByRole("button", { name: "Shared Markdown", exact: true }).click();
|
||||
await expectFileLinkTooltip(
|
||||
page,
|
||||
message.getByRole("link", { name: "src/说明.md:2-3" }),
|
||||
"packages/app/src/说明.md:2-3",
|
||||
);
|
||||
await message.getByRole("link", { name: "src/说明.md:2-3" }).click();
|
||||
await expect(page.getByText("Unicode suffix lookup succeeded.", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Shared Markdown", exact: true }).click();
|
||||
@@ -90,3 +96,8 @@ test("chat and file preview share the library renderer and resolve file links",
|
||||
await session.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
async function expectFileLinkTooltip(page: Page, link: Locator, path: string) {
|
||||
await link.hover();
|
||||
await expect(page.getByRole("tooltip")).toHaveText(path);
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@ export {
|
||||
type AssistantFileLinkResolverProviderProps,
|
||||
} from "./provider";
|
||||
export type { AssistantFileLinkSource } from "./resolver";
|
||||
export { useAssistantFileLinkActions } from "./use-file-link";
|
||||
export { useAssistantFileLinkActions, useFileLink } from "./use-file-link";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import type { OpenFileDisposition } from "@/workspace/file-open";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import { formatFileLinkTooltipPath } from "./tooltip-path";
|
||||
import type { InlinePathTarget } from "./parse";
|
||||
import {
|
||||
useAssistantFileLinkResolverContext,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
|
||||
export interface UseFileLinkResult {
|
||||
target: InlinePathTarget | null;
|
||||
tooltipPath: string | null;
|
||||
onHoverIn: () => void;
|
||||
onPress: () => void;
|
||||
open: (source: AssistantFileLinkSource, disposition: OpenFileDisposition) => void;
|
||||
@@ -134,7 +136,11 @@ export function useFileLink(source: AssistantFileLinkSource): UseFileLinkResult
|
||||
return query.data ?? null;
|
||||
}, [query.data, resolution]);
|
||||
|
||||
return useMemo(() => ({ target, onHoverIn, onPress, open }), [target, onHoverIn, onPress, open]);
|
||||
const tooltipPath = target ? formatFileLinkTooltipPath({ target, workspaceRoot }) : null;
|
||||
return useMemo(
|
||||
() => ({ target, tooltipPath, onHoverIn, onPress, open }),
|
||||
[target, tooltipPath, onHoverIn, onPress, open],
|
||||
);
|
||||
}
|
||||
|
||||
export function useAssistantFileLinkActions(): AssistantFileLinkActions {
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { View } from "react-native";
|
||||
import { markdownCopyImageSourcesDataSet } from "@/assistant-selection-copy/markup";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import {
|
||||
EnrichedMarkdownText,
|
||||
type LinkPressEvent,
|
||||
type CodeBlockRendererProps,
|
||||
} from "react-native-enriched-markdown";
|
||||
import { EnrichedMarkdownText, type LinkPressEvent } from "react-native-enriched-markdown";
|
||||
import { withUnistyles } from "react-native-unistyles";
|
||||
import {
|
||||
useAssistantFileLinkActions,
|
||||
useAssistantFileLinkEnvironment,
|
||||
} from "@/assistant-file-links";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { HighlightedCodeBlock } from "@/components/highlighted-code-block";
|
||||
import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { WEB_RENDERERS } from "./renderers";
|
||||
import { createMarkdownStyles } from "./styles";
|
||||
import { useMarkdownSource, type MarkdownImageContext } from "./images";
|
||||
|
||||
@@ -26,21 +22,7 @@ const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: true }
|
||||
|
||||
const regularStyle = (theme: Theme) => createMarkdownStyles(theme);
|
||||
const compactStyle = (theme: Theme) => createMarkdownStyles(theme, true);
|
||||
const ThemedCodeBlock = withUnistyles(HighlightedCodeBlock);
|
||||
const codeStyle = (theme: Theme) => ({
|
||||
inheritedStyles: {
|
||||
color: theme.colors.foreground,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
},
|
||||
textStyle: { backgroundColor: theme.colors.surface2, padding: 12, borderRadius: 8 },
|
||||
});
|
||||
function WebCodeBlock({ code, language }: CodeBlockRendererProps) {
|
||||
if (language?.toLowerCase() === "mermaid") return <MermaidFence code={code} />;
|
||||
return <ThemedCodeBlock code={code} language={language} uniProps={codeStyle} />;
|
||||
}
|
||||
const WEB_CONTAINER = { userSelect: "text" as const };
|
||||
const WEB_RENDERERS = { codeBlock: WebCodeBlock };
|
||||
|
||||
export interface MarkdownRendererProps extends MarkdownImageContext {
|
||||
text: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { MarkdownRenderers } from "react-native-enriched-markdown";
|
||||
|
||||
export const WEB_RENDERERS: MarkdownRenderers | undefined = undefined;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Text, View, type ViewStyle } from "react-native";
|
||||
import type {
|
||||
CodeBlockRendererProps,
|
||||
LinkRendererProps,
|
||||
MarkdownRenderers,
|
||||
} from "react-native-enriched-markdown";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useFileLink } from "@/assistant-file-links";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { HighlightedCodeBlock } from "@/components/highlighted-code-block";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { MermaidFence } from "./fence/mermaid";
|
||||
import { markdownLinkSource } from "./source";
|
||||
|
||||
const ThemedCodeBlock = withUnistyles(HighlightedCodeBlock);
|
||||
const codeStyle = (theme: Theme) => ({
|
||||
inheritedStyles: {
|
||||
color: theme.colors.foreground,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
},
|
||||
textStyle: { backgroundColor: theme.colors.surface2, padding: 12, borderRadius: 8 },
|
||||
});
|
||||
function WebCodeBlock({ code, language }: CodeBlockRendererProps) {
|
||||
if (language?.toLowerCase() === "mermaid") return <MermaidFence code={code} />;
|
||||
return <ThemedCodeBlock code={code} language={language} uniProps={codeStyle} />;
|
||||
}
|
||||
|
||||
function WebLink({ link, anchorProps, children }: LinkRendererProps) {
|
||||
const { tooltipPath, onHoverIn } = useFileLink(markdownLinkSource(link.url, link.sourceType));
|
||||
const anchor = <a {...anchorProps}>{children}</a>;
|
||||
return (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild onHoverIn={onHoverIn}>
|
||||
<View style={INLINE_LINK}>{anchor}</View>
|
||||
</TooltipTrigger>
|
||||
{tooltipPath ? (
|
||||
<TooltipContent side="top" align="start" maxWidth={520}>
|
||||
<Text selectable={false} style={styles.tooltipPath}>
|
||||
{tooltipPath}
|
||||
</Text>
|
||||
</TooltipContent>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const INLINE_LINK: ViewStyle = { display: "inline-flex" as ViewStyle["display"] };
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
tooltipPath: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
}));
|
||||
|
||||
export const WEB_RENDERERS: MarkdownRenderers = { codeBlock: WebCodeBlock, link: WebLink };
|
||||
@@ -18,17 +18,19 @@ function linkMetadata() {
|
||||
String.raw`^(?:${FILE_PATTERN.source})(?:\(\d+(?:,\d+)?(?:-\d+(?:,\d+)?)?\)| lines? \d+(?:-\d+)?)?$`,
|
||||
),
|
||||
},
|
||||
linkSource(
|
||||
url: string,
|
||||
sourceType?: "markdown" | "text" | "inline-code",
|
||||
): AssistantFileLinkSource {
|
||||
if (sourceType === "inline-code") return { href: url, text: url, sourceType };
|
||||
if (sourceType === "text") return { href: url, text: url, markup: "linkify" };
|
||||
return { href: url, text: url };
|
||||
},
|
||||
linkSource: markdownLinkSource,
|
||||
};
|
||||
}
|
||||
|
||||
export function markdownLinkSource(
|
||||
url: string,
|
||||
sourceType?: "markdown" | "text" | "inline-code",
|
||||
): AssistantFileLinkSource {
|
||||
if (sourceType === "inline-code") return { href: url, text: url, sourceType };
|
||||
if (sourceType === "text") return { href: url, text: url, markup: "linkify" };
|
||||
return { href: url, text: url };
|
||||
}
|
||||
|
||||
export interface MarkdownImage {
|
||||
source: string;
|
||||
alt: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user