diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 3ff0d02f4..024a5f1f7 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; import { XMarkIcon } from "@heroicons/react/24/outline"; +import { Marked } from "marked"; import { StageSidebar } from "../components/stage-sidebar"; import type { Stage } from "../components/stage-sidebar"; @@ -167,6 +168,50 @@ function oneLine(text: string): string { return `${collapsed.slice(0, SUMMARY_MAX_CHARS - 1)}…`; } +const SAFE_HTTP_URL_RE = /^https?:\/\//i; +const SAFE_MAILTO_URL_RE = /^mailto:/i; + +function isSafeMarkdownHref(href: string): boolean { + return ( + SAFE_HTTP_URL_RE.test(href) || + SAFE_MAILTO_URL_RE.test(href) || + href.startsWith("#") || + (href.startsWith("/") && !href.startsWith("//")) + ); +} + +const markedSafe = new Marked(); +markedSafe.use({ + async: false, + walkTokens(token) { + if ( + (token.type === "link" || token.type === "image") && + typeof token.href === "string" && + !isSafeMarkdownHref(token.href) + ) { + token.href = ""; + } + }, + renderer: { + html() { + return ""; + }, + }, +}); + +function Markdown({ content }: { content: string }) { + const html = useMemo( + () => markedSafe.parse(content, { async: false }) as string, + [content], + ); + return ( +
+ ); +} + const TOOL_NAME_DISPLAY: Record = { read_file: "Read", write_file: "Write", @@ -296,7 +341,7 @@ function EventDetails({ turn, runStart }: { turn: TurnType; runStart: string | u {(turn.kind === "system" || turn.kind === "assistant") && ( - {turn.content} + )}