import { isCapabilityAction } from "../../shared/capability-approval";
import { ActivityDetails } from "./activity-details";
import { AttachmentCards } from "./chat-attachments";
import { SkillScriptOutput, skillScriptOutput } from "./skill-script-output";
import { messageAttachments } from "../../shared/attachments";
import { useMemo, useState, useEffect, useRef, type OctaneNode } from "octane";
import { Lexer, type Token, type Tokens } from "marked";
import { decodeHTML } from "entities";
import { CodeHighlighted } from "octane-kumo/code/custom";
import { Button, LinkButton } from "octane-kumo/components/button";
import { Banner } from "octane-kumo/components/banner";
import { Loader } from "octane-kumo/components/loader";
import {
CheckCircleIcon,
WarningCircleIcon,
MinusCircleIcon,
} from "@octanejs/phosphor-icons";
import type { UIMessage } from "ai";
import { validateMemoryId } from "../../shared/memory";
import type {
ToolActivity,
ToolActivityStatus,
} from "../../shared/tool-activity";
/** Application links have a deliberately smaller surface than external links. */
export function safeChatUrl(
value: string,
application = true,
): string | undefined {
const url = decodeHTML(value);
if (/[\u0000-\u0020\\]/.test(url)) return;
if (
application &&
/^\/(tasks|conversations)\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
url,
)
)
return url;
try {
const parsed = new URL(url);
if (
!["http:", "https:"].includes(parsed.protocol) ||
parsed.username ||
parsed.password
)
return;
return parsed.href;
} catch {
return;
}
}
function SafeLink({ href, children }: { href: string; children: OctaneNode }) {
const safe = safeChatUrl(href);
return safe ? (
{children}
) : (
{children}
);
}
function Code({
code,
language,
streaming,
}: {
code: string;
language: string;
streaming: boolean;
}) {
const [notice, setNotice] = useState("");
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
return (
{language || "Plain text"}{notice}
{streaming || code.length > 40_000 ? (
{code}
) : (
)}
);
}
function renderTokens(
tokens: Token[],
streaming: boolean,
depth = 0,
): OctaneNode {
if (depth > 32) return tokens.map((token) => token.raw).join("");
return tokens.map((token, index) => {
const child = (children: Token[]) =>
renderTokens(children, streaming, depth + 1);
switch (token.type) {
case "space":
case "def":
return null;
case "heading":
return
{child((token as Tokens.Heading).tokens)}
;
case "paragraph":
return
{child((token as Tokens.Paragraph).tokens)}
;
case "text": {
const text = token as Tokens.Text;
return text.tokens ? child(text.tokens) : decodeHTML(text.text);
}
case "escape":
return decodeHTML((token as Tokens.Escape).text);
case "html":
return {token.raw};
case "strong":
return (
{child((token as Tokens.Strong).tokens)}
);
case "em":
return {child((token as Tokens.Em).tokens)};
case "del":
return {child((token as Tokens.Del).tokens)};
case "codespan":
return (
{decodeHTML((token as Tokens.Codespan).text)}
);
case "code": {
const code = token as Tokens.Code;
return (
);
}
case "link": {
const link = token as Tokens.Link;
return (
{child(link.tokens)}
);
}
case "image":
return (
[Image: {decodeHTML((token as Tokens.Image).text)}]
);
case "br":
return ;
case "hr":
return ;
case "blockquote":
return (
{child((token as Tokens.Blockquote).tokens)}
);
case "list": {
const list = token as Tokens.List;
const items = list.items.map((item, i) => (