import type { SuperagentToolCall } from '../../types'; /** Tool that delivers a sandbox image to the user as a native image. Rendered as * a visual widget (single image or gallery) instead of a generic tool-timeline * row — see {@link isSendImageTool} and the gallery chunk in `ToolCallSummary`. */ export const SEND_IMAGE_TOOL_NAME = 'send_image'; export function isSendImageTool(toolCall: SuperagentToolCall): boolean { return toolCall.name === SEND_IMAGE_TOOL_NAME; } /** A single image delivered by a `send_image` tool call, normalized for the * gallery widget. `url` is null while the upload is still in flight or when it * failed (`failed` distinguishes the two). */ export interface SentImage { /** Stable key for React lists — the tool call id, falling back to the index. */ key: string; /** Public image URL once the upload resolves, else null. */ url: string | null; /** Source file name from the tool args (e.g. `slide5.png`); used for alt text. */ fileName: string; /** True once the tool finished with an error. */ failed: boolean; /** True while the tool is still running / no URL has resolved yet. */ loading: boolean; } // A send_image call is only "loading" while the tool is genuinely in flight // (or just streaming in, before a status arrives). Any other status without a // resolved URL — success with malformed results, a stopped/cancelled upload, // an error — is terminal and shows the failed tile rather than a spinner that // would otherwise hang forever. const PENDING_STATUSES = new Set(['', 'pending', 'running', 'in_progress', 'streaming']); function extractImageUrl(results: unknown): string | null { // send_image returns `json.dumps({"image_url": ...})` as its result message, // so results is a JSON string. Be lenient: it may also arrive pre-parsed. if (!results) return null; if (typeof results === 'object') { const url = (results as Record).image_url; return typeof url === 'string' ? url : null; } if (typeof results !== 'string') return null; try { const parsed = JSON.parse(results); const url = parsed?.image_url; return typeof url === 'string' ? url : null; } catch { return null; } } function fileNameFromArgs(toolCall: SuperagentToolCall): string { const raw = toolCall.arguments_string ?? toolCall.arguments ?? null; let filePath = ''; if (raw && typeof raw === 'object') { const value = (raw as Record).file_path; if (typeof value === 'string') filePath = value; } else if (typeof raw === 'string') { try { const parsed = JSON.parse(raw); if (parsed && typeof parsed.file_path === 'string') filePath = parsed.file_path; } catch { // arguments_string can be partial mid-stream; pull file_path out leniently. const match = raw.match(/"file_path"\s*:\s*"([^"]+)"/); if (match) filePath = match[1]; } } // Show only the basename — sandbox paths can be deep (`/app/slides/s5.png`). return filePath.split('/').filter(Boolean).pop() || filePath; } export function parseSentImage(toolCall: SuperagentToolCall, index: number): SentImage { const status = (toolCall.status || '').toLowerCase(); const url = extractImageUrl(toolCall.results); const loading = !url && PENDING_STATUSES.has(status); return { key: toolCall.id ?? `send-image-${index}`, url, fileName: fileNameFromArgs(toolCall), failed: !url && !loading, loading, }; } export function parseSentImages(toolCalls: SuperagentToolCall[]): SentImage[] { return toolCalls.map(parseSentImage); }