// SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 "use client"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { getAuthToken } from "@/features/auth/session"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { code as codePlugin } from "@streamdown/code"; import { CheckIcon, CodeIcon, CopyIcon, LoaderIcon } from "lucide-react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Streamdown } from "streamdown"; import { ToolFallbackContent, ToolFallbackRoot, ToolFallbackTrigger, } from "./tool-fallback"; interface StructuredResult { text: string; images: string[]; sessionId: string; } const MAX_DISPLAY = 10_000; const COPY_RESET_MS = 2000; const SHIKI_THEME = ["github-light", "github-dark"] as ["github-light", "github-dark"]; function truncate(text: string): string { return text.length <= MAX_DISPLAY ? text : `${text.slice(0, MAX_DISPLAY)}\n... (truncated)`; } function CopyBtn({ text }: { text: string }) { const [copied, setCopied] = useState(false); const timer = useRef | null>(null); useEffect(() => { return () => { if (timer.current) { clearTimeout(timer.current); } }; }, []); const copy = useCallback(async () => { if (await copyToClipboard(text)) { setCopied(true); if (timer.current) { clearTimeout(timer.current); } timer.current = setTimeout(() => setCopied(false), COPY_RESET_MS); } }, [text]); return ( ); } /** Render code with syntax highlighting via Streamdown + shiki. No extra borders — inherits parent container. */ function HighlightedCode({ code: source, language }: { code: string; language: string }) { const markdown = useMemo( () => `\`\`\`${language}\n${truncate(source)}\n\`\`\``, [source, language], ); return (
{markdown}
); } function isStructuredResult(val: unknown): val is StructuredResult { return ( typeof val === "object" && val !== null && "text" in val && "images" in val && "sessionId" in val ); } const PythonToolUIImpl: ToolCallMessagePartComponent = ({ args, result, status, }) => { const code = (args as { code?: string })?.code ?? ""; const firstLine = code.split("\n")[0]?.slice(0, 60) ?? ""; const isRunning = status?.type === "running"; let output: string; let images: string[] = []; let sessionId = ""; if (isStructuredResult(result)) { output = result.text; images = result.images; sessionId = result.sessionId; } else if (typeof result === "string") { output = result; } else if (result) { output = JSON.stringify(result, null, 2); } else { output = ""; } const authToken = getAuthToken(); return (
{/* Code + copy */} {code && (
)} {code && } {/* Output */} {isRunning ? (
Running…
) : output ? (
output
                {truncate(output)}
              
) : null} {/* Images from Python tool execution */} {images.length > 0 && sessionId && (
{images.map((filename) => ( {filename} ))}
)}
); }; export const PythonToolUI = memo( PythonToolUIImpl, ) as unknown as ToolCallMessagePartComponent; PythonToolUI.displayName = "PythonToolUI";