mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
* feat(app): link #123 issue and pull request references in Markdown Agents cite issues and pull requests by number; reading one meant looking up the URL by hand. Markdown in a workspace now links forge shorthand against the workspace's origin remote: `#N`, GitLab and Gitea `!N`, and cross-repository `owner/repo#N` on the same host. Each forge's URL grammar declares which sigil maps to which path, so the syntax stays per forge: GitHub has no `!`, and its `#` targets /issues/, which GitHub redirects to the pull request. Recognition goes through the Markdown engine's text link regex so code spans, fences and authored links stay plain without a second Markdown parse while streaming. The reference alternative sits before path recognition so `group/project#12` is not read as a file path. * fix(app): keep http:// for self-hosted forge web links A self-hosted forge cloned over http:// serves its web UI over http on the same origin. Forcing https made reference, blob and tree links fail against an HTTP-only port. Cloud hosts and SSH remotes stay on https.
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { Text } from "react-native";
|
|
import { forwardRef, useCallback, useMemo, useState, type ComponentProps } from "react";
|
|
import type {
|
|
CodeBlockRendererProps,
|
|
LinkRendererProps,
|
|
MarkdownRenderers,
|
|
} from "react-native-enriched-markdown";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import { useFileLink } from "@/assistant-file-links";
|
|
import { useForgeShorthand } from "@/git/forge-shorthand";
|
|
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} />;
|
|
}
|
|
|
|
// The library styles links inline, so a stylesheet :hover rule cannot underline them.
|
|
const LINK_HOVER_STYLE = { textDecoration: "underline" } as const;
|
|
|
|
function WebLink({ link, anchorProps, children }: LinkRendererProps) {
|
|
const forgeShorthand = useForgeShorthand();
|
|
// A forge reference like `#12` carries its web URL so hover, copy and new-tab opens work.
|
|
const forgeUrl = link.sourceType === "text" ? forgeShorthand?.resolve(link.url) : null;
|
|
const { tooltipPath, onHoverIn } = useFileLink(
|
|
forgeUrl ? markdownLinkSource(forgeUrl) : markdownLinkSource(link.url, link.sourceType),
|
|
);
|
|
const tooltip = forgeUrl ?? tooltipPath;
|
|
const [hovered, setHovered] = useState(false);
|
|
const handleMouseEnter = useCallback(() => setHovered(true), []);
|
|
const handleMouseLeave = useCallback(() => setHovered(false), []);
|
|
const anchorStyle = useMemo(
|
|
() => ({
|
|
...anchorProps.style,
|
|
overflowWrap: "anywhere" as const,
|
|
...(hovered ? LINK_HOVER_STYLE : null),
|
|
}),
|
|
[anchorProps.style, hovered],
|
|
);
|
|
const anchor = (
|
|
<a
|
|
{...anchorProps}
|
|
href={forgeUrl ?? anchorProps.href}
|
|
style={anchorStyle}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
{children}
|
|
</a>
|
|
);
|
|
return (
|
|
<Tooltip delayDuration={400}>
|
|
<TooltipTrigger asChild onHoverIn={onHoverIn}>
|
|
<InlineLinkTrigger>{anchor}</InlineLinkTrigger>
|
|
</TooltipTrigger>
|
|
{tooltip ? (
|
|
<TooltipContent side="top" align="start" maxWidth={520}>
|
|
<Text selectable={false} style={styles.tooltipPath}>
|
|
{tooltip}
|
|
</Text>
|
|
</TooltipContent>
|
|
) : null}
|
|
</Tooltip>
|
|
);
|
|
}
|
|
// TooltipTrigger also supplies native Pressable handlers; forward only DOM events.
|
|
// A View renders a div, which is invalid inside the library's prose paragraphs.
|
|
const InlineLinkTrigger = forwardRef<HTMLSpanElement, ComponentProps<"span">>(
|
|
function InlineLinkTrigger({ children, onMouseEnter, onMouseLeave, onFocus, onBlur }, ref) {
|
|
return (
|
|
<span
|
|
ref={ref}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
onFocus={onFocus}
|
|
onBlur={onBlur}
|
|
>
|
|
{children}
|
|
</span>
|
|
);
|
|
},
|
|
);
|
|
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 };
|