import { Popover } from "@mantine/core"; import { createContext, useContext, useEffect, useMemo, useRef, useState, useId, type ReactNode } from "react"; import Markdown, { type Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import type { PhrasingContent, Root, RootContent } from "mdast"; // Saved replies are untrusted text. Do not execute HTML or load remote images. function replyUrl(url: string): string { return /^(https?:|mailto:)/i.test(url) || url.startsWith("#") ? url : ""; } function localImagePath(url: string): boolean { return (url.startsWith("/") && !url.startsWith("//")) || /^[A-Za-z]:[\\/]/.test(url); } // Generated request labels add no information to cards or prompt bubbles. // Keep labels inside quoted examples and code intact. function requestLabelText(node: PhrasingContent): string | null { if (node.type === "text") return node.value; if ((node.type === "strong" || node.type === "emphasis") && node.children.length === 1 && node.children[0].type === "text") return node.children[0].value; return null; } function hideCardRequestLabels() { return (tree: Root) => { const leading = tree.children[0]; if (leading && (leading.type === "heading" || leading.type === "paragraph") && leading.children.length === 1 && /^My requests?:\s*$/i.test(requestLabelText(leading.children[0]) ?? "")) tree.children.shift(); const first = tree.children[0]; if (first?.type === "paragraph" && first.children[0]) { const label = first.children[0]; if (label.type === "text") { label.value = label.value.replace(/^My requests?:[ \t]*(?:\r?\n)?/i, ""); if (!label.value) first.children.shift(); } else if (/^My requests?:\s*$/i.test(requestLabelText(label) ?? "")) { first.children.shift(); } else return; if (first.children[0]?.type === "break") first.children.shift(); } }; } // Transform text nodes only: quoted code and existing links remain literal. function annotationLinks() { return (tree: Root) => { const visit = (parent: Root | RootContent) => { if (!("children" in parent) || parent.type === "link") return; parent.children = parent.children.flatMap((child): (RootContent | PhrasingContent)[] => { if (child.type !== "text") { visit(child as RootContent); return [child]; } const parts: PhrasingContent[] = []; let end = 0; for (const match of child.value.matchAll(/:codex-annotation\{index="([1-9]\d*)"\}/g)) { if (match.index! > end) parts.push({ type: "text", value: child.value.slice(end, match.index) }); parts.push({ type: "link", url: `#taskchef-annotation-${match[1]}`, children: [{ type: "text", value: `Annotation ${match[1]}` }] }); end = match.index! + match[0].length; } if (!parts.length) return [child]; if (end < child.value.length) parts.push({ type: "text", value: child.value.slice(end) }); return parts; }) as typeof parent.children; }; visit(tree); }; } type Annotation = { text: string; annotation: string }; type AnnotationOpenChange = (id: string, opened: boolean) => void; const AnnotationContext = createContext<{ items: Annotation[]; onOpenChange?: AnnotationOpenChange }>({ items: [] }); function AnnotationLink({ index }: { index: number }) { const { items, onOpenChange } = useContext(AnnotationContext); const annotation = items[index - 1]; const popupId = useId(); const label = annotation ?