import type { SuperagentToolCall } from '../types'; /** * Pure artifact-tool helpers — native port of the web's artifact-constants.ts * (frontend/apps/builder/src/pages/agent-editor/components/). Same shape * detection so both platforms route the identical calls to the artifact card. */ /** * Display payload the backend `artifacts` tool attaches as the client-only * `client_artifact` field (never part of the LLM-facing results). `text` is * the HTML/SVG markup; `title` rides `_meta` so the chrome can show it without * parsing the body. */ export type ArtifactResource = { uri: string; text: string; _meta?: { title?: string }; }; /** * Canonical name + all backend aliases the LLM is known to hallucinate. * Must stay in sync with ARTIFACT_TOOL_NAMES in backend/app/ai/tool_constants.py. */ export const ARTIFACT_TOOL_NAMES: ReadonlySet = new Set([ 'artifacts', 'display_widget', 'render_widget', 'show_widget', 'render_ui', 'display_ui', 'show_artifact', 'create_artifact', 'render_artifact', ]); /** Validate a `client_artifact` payload; null for anything unrecognisable. */ export function extractArtifact(clientArtifact: unknown): ArtifactResource | null { if (!clientArtifact || typeof clientArtifact !== 'object') return null; const resource = clientArtifact as { uri?: unknown; text?: unknown; _meta?: { title?: string } }; if (typeof resource.uri !== 'string' || typeof resource.text !== 'string') return null; return { uri: resource.uri, text: resource.text, _meta: resource._meta }; } /** * True when a tool call is genuinely the artifacts tool — same shape guard as * the web's isArtifactToolCall. The definitive signal is a `client_artifact` * payload; only the real backend tool attaches one, so a user backend function * or MCP tool that happens to be named `artifacts` (or an alias like * `render_ui`) keeps the generic tool UI: * - payload present → artifact, for any listed name; * - payload-less alias → NOT an artifact (a genuine alias call is * canonicalized to `artifacts` on the backend, so this is a real tool); * - payload-less canonical `artifacts` → artifact only while still in flight * (mid-stream the payload isn't attached yet); once concluded without a * payload it's a real tool named `artifacts`. */ export function isArtifactToolCall(toolCall: SuperagentToolCall): boolean { const name = toolCall?.name; if (!name || !ARTIFACT_TOOL_NAMES.has(name)) return false; if (extractArtifact(toolCall.client_artifact)) return true; if (name !== 'artifacts') return false; const status = toolCall.status; const concluded = (toolCall.results != null && toolCall.results !== '') || status === 'success' || status === 'error' || status === 'failed' || status === 'stopped' || status === 'cancelled' || status === 'canceled'; return !concluded; } export type ArtifactRenderState = 'running' | 'failed' | 'stopped' | 'unavailable' | 'rendered'; /** * Card-state matrix for the artifact widget. A delivered `client_artifact` is * the only proof anything rendered (web parity: MessageArtifacts renders only * extractable payloads, and isArtifactToolCall keeps concluded payload-less * calls on the generic UI) — so a settled turn whose payload never arrived is * 'unavailable', never a success: the args `content` is what the model ASKED * to render, not what rendered. */ export function getArtifactRenderState( rawStatus: string, hasArtifact: boolean, isActionable: boolean, ): ArtifactRenderState { const status = (rawStatus || '').toLowerCase(); if (status === 'error' || status === 'failed') return 'failed'; if (hasArtifact) return 'rendered'; if (status === 'stopped' || status === 'cancelled' || status === 'canceled') return 'stopped'; if (status !== 'success' && isActionable) return 'running'; return 'unavailable'; } /** MIME `type` arg → short label ("text/html" → "HTML"). */ export function formatArtifactType(type: string): string { if (type === 'text/html') return 'HTML'; if (type === 'image/svg+xml') return 'SVG'; return type; }