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) => (
  • {item.task ? ( {item.checked ? "☑ " : "☐ "} ) : null} {child(item.tokens)}
  • )); return list.ordered ? (
      {items}
    ) : ( ); } case "table": { const table = token as Tokens.Table; return (
    {table.header.map((cell, i) => ( ))} {table.rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))}
    {child(cell.tokens)}
    {child(cell.tokens)}
    ); } default: return {token.raw}; } }); } export function hasCodeBlock(text: string) { return Lexer.lex(text, { gfm: true }).some((token) => token.type === "code"); } function Markdown({ text, streaming }: { text: string; streaming: boolean }) { const tokens = useMemo(() => Lexer.lex(text, { gfm: true }), [text]); return
    {renderTokens(tokens, streaming)}
    ; } type ToolPart = UIMessage["parts"][number] & { toolCallId: string; state: string; toolName?: string; input?: unknown; output?: unknown; errorText?: string; }; function resourceAction(part: ToolPart) { if ( part.state !== "output-available" || !part.output || typeof part.output !== "object" ) return; const result = part.output as { id?: unknown; url?: unknown; error?: unknown; }; if (result.error) return; const toolName = part.toolName || part.type.replace(/^tool-/, ""); if (toolName === "remember" || toolName === "updateMemory") { try { validateMemoryId(result.id); return { href: `/settings#memory-${result.id}`, label: "View memory" }; } catch { return; } } if (typeof result.url !== "string") return; const href = safeChatUrl(result.url); if (href?.startsWith("/tasks/")) return { href, label: "View scheduled task" }; if (href?.startsWith("/conversations/")) return { href, label: "View conversation" }; } function ToolRow({ part, activity, }: { part: ToolPart; activity?: ToolActivity; }) { const scriptOutput = part.state === "output-available" ? skillScriptOutput(part.output) : undefined; const fallback: ToolActivityStatus = part.state === "output-error" || part.state === "output-denied" ? "failed" : part.state === "output-available" ? scriptOutput && !scriptOutput.result.ok ? "failed" : "succeeded" : part.state === "input-streaming" ? "pending" : "running"; const status = activity?.status ?? fallback; const loading = status === "pending" || status === "running"; const label = { pending: "Pending", running: "Running", succeeded: "Succeeded", failed: "Failed", cancelled: "Cancelled", }[status]; const result = part.output && typeof part.output === "object" ? (part.output as { url?: unknown; status?: unknown }) : undefined; const action = resourceAction(part); return (