import { useMemo, type JSX, type MouseEvent } from "react";
import type { JSONContent } from "@tiptap/core";
import { renderToReactElement } from "@tiptap/static-renderer";
import { cn } from "../app/renderer/lib/utils";
import { plainTextToAgentRichTextDoc } from "../agent-gui/agentGuiNode/agentRichText/agentRichTextDocument";
import { AGENT_RICH_TEXT_CARET_ANCHOR } from "../agent-gui/agentGuiNode/agentRichText/agentRichTextCaretAnchor";
import { createAgentRichTextReadonlyExtensions } from "../agent-gui/agentGuiNode/agentRichText/agentRichTextExtensions";
import { AgentMentionReadonlyView } from "../agent-gui/agentGuiNode/agentRichText/AgentMentionNodeView";
import type { AgentMessageMarkdownWorkspaceAppIcon } from "./AgentMessageMarkdown";
import type { AgentGUIProviderSkillOption } from "../agent-gui/agentGuiNode/model/agentGuiNodeTypes";
import {
useAgentTargetPresentations,
type AgentMessageMarkdownAgentTarget
} from "./AgentTargetPresentationContext";
import { readParsedDocumentCache } from "./parsedDocumentCache";
const EMPTY_WORKSPACE_APP_ICONS: readonly AgentMessageMarkdownWorkspaceAppIcon[] =
[];
const EMPTY_SKILLS: readonly AgentGUIProviderSkillOption[] = [];
interface AgentRichTextReadonlyProps {
value: string;
documentCacheKey?: string;
className?: string;
editorClassName?: string;
onLinkClick?: (href: string) => void;
availableSkills?: readonly AgentGUIProviderSkillOption[];
workspaceAppIcons?: readonly AgentMessageMarkdownWorkspaceAppIcon[];
agentTargets?: readonly AgentMessageMarkdownAgentTarget[];
}
export function AgentRichTextReadonly({
value,
documentCacheKey,
className,
editorClassName,
onLinkClick,
availableSkills = EMPTY_SKILLS,
workspaceAppIcons = EMPTY_WORKSPACE_APP_ICONS,
agentTargets
}: AgentRichTextReadonlyProps): JSX.Element {
"use memo";
const contextAgentTargets = useAgentTargetPresentations();
const effectiveAgentTargets = agentTargets ?? contextAgentTargets;
const skillsCacheSource = useMemo(
() => JSON.stringify(availableSkills),
[availableSkills]
);
const parsedDoc = useMemo(
() =>
readParsedDocumentCache({
namespace: "rich-text",
identity: documentCacheKey?.trim() || "content",
source: `${value}\u0000${skillsCacheSource}`,
create: () =>
plainTextToAgentRichTextDoc(value, { skills: availableSkills })
}),
[availableSkills, documentCacheKey, skillsCacheSource, value]
);
const contentDoc = useMemo(
() =>
workspaceAppIcons.length > 0
? hydrateWorkspaceAppMentionIcons(parsedDoc, workspaceAppIcons)
: parsedDoc,
[parsedDoc, workspaceAppIcons]
);
const isMentionOnly = isMentionOnlyRichTextDoc(contentDoc);
const extensions = useMemo(
() =>
createAgentRichTextReadonlyExtensions({
skills: availableSkills
}),
[availableSkills]
);
const renderedContent = useMemo(
() =>
renderToReactElement({
content: contentDoc,
extensions,
options: {
nodeMapping: {
agentFileMention: ({ node }) => (
)
}
}
}),
[contentDoc, effectiveAgentTargets, extensions]
);
const handleClick = (event: MouseEvent) => {
if (!onLinkClick || !(event.target instanceof Element)) {
return;
}
const mention = event.target.closest(
'[data-agent-file-mention="true"][data-agent-mention-href]'
);
if (!(mention instanceof HTMLElement)) {
return;
}
const href = mention.getAttribute("data-agent-mention-href") || "";
if (!href) {
return;
}
event.preventDefault();
event.stopPropagation();
onLinkClick(href);
};
return (
);
}
function isMentionOnlyRichTextDoc(doc: JSONContent): boolean {
if (doc.type !== "doc") {
return false;
}
const blocks = doc.content ?? [];
if (blocks.length !== 1) {
return false;
}
const paragraph = blocks[0];
if (paragraph?.type !== "paragraph") {
return false;
}
const inlineContent = (paragraph.content ?? []).filter(
(node) =>
!(
node.type === "text" &&
(node.text ?? "").replaceAll(AGENT_RICH_TEXT_CARET_ANCHOR, "")
.length === 0
)
);
return (
inlineContent.length === 1 && inlineContent[0]?.type === "agentFileMention"
);
}
function hydrateWorkspaceAppMentionIcons(
node: JSONContent,
workspaceAppIcons: readonly AgentMessageMarkdownWorkspaceAppIcon[]
): JSONContent {
const nextContent = node.content?.map((child) =>
hydrateWorkspaceAppMentionIcons(child, workspaceAppIcons)
);
if (node.type !== "agentFileMention") {
return nextContent ? { ...node, content: nextContent } : node;
}
const attrs = node.attrs ?? {};
const kind = typeof attrs.kind === "string" ? attrs.kind : "";
const isWorkspaceAppMention = kind === "workspace-app";
const isAppWorkspaceReferenceMention =
kind === "workspace-reference" && attrs.source === "app";
if (!isWorkspaceAppMention && !isAppWorkspaceReferenceMention) {
return nextContent ? { ...node, content: nextContent } : node;
}
const workspaceId =
typeof attrs.workspaceId === "string" ? attrs.workspaceId.trim() : "";
const appId =
isWorkspaceAppMention && typeof attrs.appId === "string"
? attrs.appId.trim()
: typeof attrs.targetId === "string"
? attrs.targetId.trim()
: "";
const iconUrl = resolveWorkspaceAppIconUrl({
appId,
workspaceId,
workspaceAppIcons
});
if (!iconUrl) {
return nextContent ? { ...node, content: nextContent } : node;
}
return {
...node,
attrs: {
...node.attrs,
iconUrl
},
...(nextContent ? { content: nextContent } : {})
};
}
function resolveWorkspaceAppIconUrl(input: {
appId: string;
workspaceId: string;
workspaceAppIcons: readonly AgentMessageMarkdownWorkspaceAppIcon[];
}): string | undefined {
if (!input.appId) {
return undefined;
}
const exactMatch = input.workspaceAppIcons.find(
(icon) =>
icon.appId.trim() === input.appId &&
(icon.workspaceId?.trim() ?? "") === input.workspaceId &&
icon.iconUrl?.trim()
);
const fallbackMatch = input.workspaceAppIcons.find(
(icon) => icon.appId.trim() === input.appId && icon.iconUrl?.trim()
);
return (
exactMatch?.iconUrl?.trim() || fallbackMatch?.iconUrl?.trim() || undefined
);
}