Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

180 lines
4.9 KiB
TypeScript

import type { Editor, JSONContent } from "@tiptap/core";
import {
attrsToMentionItem,
formatAgentMentionMarkdown,
mentionItemToAttrs,
parseAgentMentionMarkdown
} from "./agentFileMentionExtension";
import type { AgentGUIProviderSkillOption } from "../model/agentGuiNodeTypes";
import { parseAgentSkillToken } from "./agentSkillTokenExtension";
import {
parseAgentCapabilityToken,
type AgentCapabilityTokenOption
} from "./agentCapabilityTokenExtension";
import { AGENT_RICH_TEXT_CARET_ANCHOR } from "./agentRichTextCaretAnchor";
export interface AgentRichTextDocumentOptions {
capabilities?: readonly AgentCapabilityTokenOption[];
skills?: readonly AgentGUIProviderSkillOption[];
}
function createEmptyDocument(): JSONContent {
return {
type: "doc",
content: [{ type: "paragraph" }]
};
}
function isVisualLineStart(content: readonly JSONContent[]): boolean {
return content.length === 0 || content.at(-1)?.type === "hardBreak";
}
function createParagraphFromText(
text: string,
options: AgentRichTextDocumentOptions = {}
): JSONContent {
const content: JSONContent[] = [];
let index = 0;
let textBuffer = "";
const flushTextBuffer = (): void => {
if (textBuffer.length === 0) {
return;
}
content.push({ type: "text", text: textBuffer });
textBuffer = "";
};
while (index < text.length) {
const current = text[index];
if (current === "\n") {
flushTextBuffer();
content.push({ type: "hardBreak" });
index += 1;
continue;
}
const parsedMention = parseAgentMentionMarkdown(text, index);
if (parsedMention) {
flushTextBuffer();
if (isVisualLineStart(content)) {
content.push({ type: "text", text: AGENT_RICH_TEXT_CARET_ANCHOR });
}
content.push({
type: "agentFileMention",
// 转成规范 node attrs(如 workspace-reference 的 source/groupId/fileCount),
// 否则只读回显里的 reference chip 拿不到文件数 → 不显示「N 个文件」角标。
attrs: mentionItemToAttrs(parsedMention.item)
});
index = parsedMention.end;
continue;
}
const parsedCapabilityToken = parseAgentCapabilityToken(
text,
index,
options.capabilities
);
if (parsedCapabilityToken) {
flushTextBuffer();
content.push({
type: "agentCapabilityToken",
attrs: parsedCapabilityToken.attrs
});
index = parsedCapabilityToken.end;
continue;
}
const parsedSkillToken = parseAgentSkillToken(text, index, options.skills);
if (parsedSkillToken) {
flushTextBuffer();
content.push({
type: "agentSkillToken",
attrs: parsedSkillToken.attrs
});
index = parsedSkillToken.end;
continue;
}
textBuffer += current;
index += 1;
}
flushTextBuffer();
return content.length > 0
? { type: "paragraph", content }
: { type: "paragraph" };
}
export function plainTextToAgentRichTextDoc(
text: string,
options: AgentRichTextDocumentOptions = {}
): JSONContent {
if (text.length === 0) {
return createEmptyDocument();
}
const normalized = text.replace(/\r\n?/g, "\n");
return {
type: "doc",
content: [createParagraphFromText(normalized, options)]
};
}
export function plainTextToAgentRichTextInlineContent(
text: string,
options: AgentRichTextDocumentOptions = {}
): JSONContent[] {
const paragraph = createParagraphFromText(
text.replace(/\r\n?/g, "\n"),
options
);
return paragraph.content ?? [];
}
export function agentRichTextDocToPromptText(doc: JSONContent): string {
if (doc.type !== "doc") {
return nodeToPromptText(doc);
}
return agentRichTextContentToPromptText(doc.content ?? []);
}
export function editorToPromptText(editor: Editor): string {
return agentRichTextDocToPromptText(editor.getJSON());
}
export function agentRichTextContentToPromptText(
content: readonly JSONContent[]
): string {
if (content.length === 0) {
return "";
}
const separator = content.some((node) => isBlockPromptNode(node)) ? "\n" : "";
return content.map((node) => nodeToPromptText(node)).join(separator);
}
function isBlockPromptNode(node: JSONContent): boolean {
return node.type === "doc" || node.type === "paragraph";
}
function nodeToPromptText(node: JSONContent): string {
if (node.type === "text") {
return (node.text ?? "").replaceAll(AGENT_RICH_TEXT_CARET_ANCHOR, "");
}
if (node.type === "agentFileMention") {
return formatAgentMentionMarkdown(attrsToMentionItem(node.attrs ?? {}));
}
if (node.type === "agentSkillToken") {
return typeof node.attrs?.trigger === "string" ? node.attrs.trigger : "";
}
if (node.type === "agentCapabilityToken") {
return typeof node.attrs?.trigger === "string" ? node.attrs.trigger : "";
}
if (node.type === "hardBreak") {
return "\n";
}
if (!node.content || node.content.length === 0) {
return "";
}
return node.content.map((child) => nodeToPromptText(child)).join("");
}