Files
tutti/packages/agent/gui/shared/agentMessageMarkdownRuntime.ts
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

202 lines
4.9 KiB
TypeScript

import type { KeyboardEvent, MouseEvent, PointerEvent } from "react";
const COLLAPSED_LINE_LIMIT = 8;
const APPROX_CHARS_PER_LINE = 34;
export interface StreamingMarkdownBlock {
content: string;
initialKeyContent: string;
}
export function splitStreamingMarkdownBlocks(
content: string
): StreamingMarkdownBlock[] {
const normalized = content.replace(/\r\n?/g, "\n");
if (!normalized) {
return [{ content: "", initialKeyContent: "" }];
}
const lines = normalized.split("\n");
const blocks: StreamingMarkdownBlock[] = [];
const current: string[] = [];
let fence: { marker: string; length: number } | null = null;
for (const line of lines) {
current.push(line);
const lineFence = parseStreamingFence(line);
if (lineFence) {
if (!fence) {
fence = lineFence;
} else if (
lineFence.marker === fence.marker &&
lineFence.length >= fence.length
) {
fence = null;
}
continue;
}
if (!fence && line.trim() === "") {
pushStreamingMarkdownBlock(blocks, current);
}
}
pushStreamingMarkdownBlock(blocks, current);
return blocks.length > 0
? blocks
: [{ content: normalized, initialKeyContent: normalized }];
}
export function pushStreamingMarkdownBlock(
blocks: StreamingMarkdownBlock[],
lines: string[]
): void {
if (lines.length === 0) {
return;
}
const content = lines.join("\n");
if (!content) {
lines.length = 0;
return;
}
blocks.push({
content,
initialKeyContent: content
});
lines.length = 0;
}
export function parseStreamingFence(
line: string
): { marker: string; length: number } | null {
const trimmed = line.trimStart();
const marker = trimmed[0];
if (marker !== "`" && marker !== "~") {
return null;
}
let length = 0;
while (trimmed[length] === marker) {
length += 1;
}
return length >= 3 ? { marker, length } : null;
}
export function hasOpenPotentialMermaidFence(content: string): boolean {
let openFence: {
marker: string;
length: number;
isPotentialMermaid: boolean;
} | null = null;
for (const line of content.replace(/\r\n?/g, "\n").split("\n")) {
const lineFence = parseStreamingFence(line);
if (!lineFence) {
continue;
}
const trimmed = line.trimStart();
const suffix = trimmed.slice(lineFence.length).trim();
if (!openFence) {
const language = suffix.split(/\s+/, 1)[0]?.toLowerCase() ?? "";
openFence = {
...lineFence,
isPotentialMermaid:
language.length > 0 && "mermaid".startsWith(language)
};
continue;
}
if (
suffix === "" &&
lineFence.marker === openFence.marker &&
lineFence.length >= openFence.length
) {
openFence = null;
}
}
return openFence?.isPotentialMermaid ?? false;
}
export function resolveMarkdownAnchorHref(
target: EventTarget | null
): string | null {
if (!(target instanceof Element)) {
return null;
}
const link = target.closest("[data-agent-link-href],a[href]");
if (!(link instanceof HTMLElement)) {
return null;
}
const dataHref = link.dataset.agentLinkHref?.trim();
if (dataHref) {
return dataHref;
}
if (link instanceof HTMLAnchorElement) {
return link.getAttribute("href")?.trim() || null;
}
return null;
}
export function activateMarkdownLink(
event:
| KeyboardEvent<HTMLElement>
| MouseEvent<HTMLElement>
| PointerEvent<HTMLElement>,
href: string,
onLinkClick?: (href: string) => void
): void {
const target = href.trim();
if (!target) {
return;
}
event.preventDefault();
event.stopPropagation();
onLinkClick?.(target);
}
export function activateMarkdownLinkFromKey(
event: KeyboardEvent<HTMLElement>,
href: string,
onLinkClick?: (href: string) => void
): void {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
activateMarkdownLink(event, href, onLinkClick);
}
export function activateMarkdownLinkFromPointer(
event: PointerEvent<HTMLElement>,
href: string,
onLinkClick?: (href: string) => void
): void {
if (event.button !== 0) {
return;
}
activateMarkdownLink(event, href, onLinkClick);
}
export function hashMarkdownProfilerContent(content: string): string {
let hash = 0;
for (let index = 0; index < content.length; index += 1) {
hash = (hash * 31 + content.charCodeAt(index)) | 0;
}
return `${content.length}:${Math.abs(hash)}`;
}
export function isLikelyLongerThanLineLimit(content: string): boolean {
const normalizedLines = content.replace(/\r\n?/g, "\n").split("\n");
if (normalizedLines.length > COLLAPSED_LINE_LIMIT) {
return true;
}
const estimatedLineCount = normalizedLines.reduce((total, line) => {
const trimmed = line.trim();
const blockSpacing = /^(#{1,6}\s|\s*[-*+]\s|\s*\d+\.\s|>)/.test(trimmed)
? 1
: 0;
return (
total +
Math.max(1, Math.ceil(trimmed.length / APPROX_CHARS_PER_LINE)) +
blockSpacing
);
}, 0);
return estimatedLineCount > COLLAPSED_LINE_LIMIT;
}