mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 21:08:34 +03:00
feat(session): link agent references
This commit is contained in:
@@ -16,6 +16,10 @@ import { Streamdown, type Components, type StreamdownProps } from "streamdown";
|
||||
|
||||
import { AttachmentTile } from "@/components/AttachmentTile";
|
||||
import { CodeBlock } from "@/components/CodeBlock";
|
||||
import {
|
||||
INLINE_TOKEN_HIGHLIGHT_COLOR,
|
||||
InlineTokenHighlight,
|
||||
} from "@/components/InlineTokenHighlight";
|
||||
import {
|
||||
useFilePreviewAvailabilityResolver,
|
||||
type FilePreviewAvailabilityResolver,
|
||||
@@ -348,6 +352,17 @@ function fileReferenceFromLink(href: string | undefined): string | null {
|
||||
return isPreviewableFileTarget(target) ? target : null;
|
||||
}
|
||||
|
||||
function sessionReferenceHref(href: string): string | null {
|
||||
if (!href.startsWith("#/chat/")) return null;
|
||||
try {
|
||||
const sessionKey = decodeURIComponent(href.slice("#/chat/".length)).trim();
|
||||
if (!sessionKey.startsWith("websocket:") || sessionKey === "websocket:") return null;
|
||||
return `#/chat/${encodeURIComponent(sessionKey)}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function linkPreviewParts(value: ReactNode): { text: string; href?: string } {
|
||||
let text = "";
|
||||
let href: string | undefined;
|
||||
@@ -592,6 +607,20 @@ export default function MarkdownTextRenderer({
|
||||
if (href === "streamdown:incomplete-link") {
|
||||
return <>{markdownChildren}</>;
|
||||
}
|
||||
const sessionHref = sessionReferenceHref(href);
|
||||
if (sessionHref) {
|
||||
return (
|
||||
<a
|
||||
href={sessionHref}
|
||||
className="rounded-sm underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
||||
>
|
||||
<InlineTokenHighlight color={INLINE_TOKEN_HIGHLIGHT_COLOR}>
|
||||
{markdownChildren}
|
||||
</InlineTokenHighlight>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (href.startsWith("#/chat/")) return <>{markdownChildren}</>;
|
||||
const filePath = fileReferenceFromLink(href);
|
||||
if (filePath) {
|
||||
const label = nodeText(markdownChildren).trim();
|
||||
|
||||
@@ -13,6 +13,29 @@ describe("MarkdownTextRenderer", () => {
|
||||
expect(link).toHaveClass("text-blue-500", "dark:text-blue-300");
|
||||
});
|
||||
|
||||
it("renders canonical session references as same-tab links", () => {
|
||||
render(
|
||||
<MarkdownTextRenderer>
|
||||
{"We discussed this in [收费设计](#/chat/websocket%3Apricing)."}
|
||||
</MarkdownTextRenderer>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "收费设计" });
|
||||
expect(link).toHaveAttribute("href", "#/chat/websocket%3Apricing");
|
||||
expect(link).not.toHaveAttribute("target");
|
||||
});
|
||||
|
||||
it("does not link non-WebUI session references", () => {
|
||||
const { container } = render(
|
||||
<MarkdownTextRenderer>
|
||||
{"[private channel](#/chat/telegram%3Aprivate)"}
|
||||
</MarkdownTextRenderer>,
|
||||
);
|
||||
|
||||
expect(container).toHaveTextContent("private channel");
|
||||
expect(container.querySelector("a")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render active URL protocols from untrusted markdown", () => {
|
||||
const { container } = render(
|
||||
<MarkdownTextRenderer>
|
||||
|
||||
Reference in New Issue
Block a user