/** * Render Helpers * * Shared rendering functions for tool input and output. */ import React from 'react'; import { COLOR_TEXT, COLOR_TEXT_MUTED, DIFF_ADDED_TEXT, DIFF_REMOVED_TEXT, } from '@renderer/constants/cssVariables'; import { highlightLines } from '@renderer/utils/syntaxHighlighter'; import { getAgentToolDisplayDetails } from '@shared/utils/toolSummary'; export interface RenderInputLabels { replaceAll: string; agentAction: string; agentTeammate: string; agentTeam: string; agentRuntime: string; agentType: string; startupInstructionsHidden: string; noInputRecorded: string; } /** * Renders the input section based on tool type with theme-aware styling. */ export function renderInput( toolName: string, input: Record, labels: RenderInputLabels ): React.ReactElement { const normalizedToolName = toolName.toLowerCase(); // Special rendering for Edit tool - show diff-like format if (normalizedToolName === 'edit') { const filePath = readInputString(input, ['file_path', 'filePath', 'path']); const oldString = readInputString(input, ['old_string', 'oldString']); const newString = readInputString(input, ['new_string', 'newString']); const replaceAll = input.replace_all as boolean | undefined; return (
{filePath && (
{filePath} {replaceAll && ( {labels.replaceAll} )}
)} {oldString && (
{oldString.split('\n').map((line, i) => (
- {line}
))}
)} {newString && (
{newString.split('\n').map((line, i) => (
+ {line}
))}
)}
); } // Special rendering for Bash tool if (normalizedToolName === 'bash') { const command = readInputString(input, ['command']); const description = readInputString(input, ['description']); const highlighted = command ? highlightLines(command, 'command.sh') : null; return (
{description && (
{description}
)} {highlighted ? ( {highlighted.map((html, i) => (
))} ) : null}
); } // Special rendering for Read tool if (normalizedToolName === 'read') { const filePath = readInputString(input, ['file_path', 'filePath', 'path']); const offset = input.offset as number | undefined; const limit = input.limit as number | undefined; return (
{filePath}
{(offset !== undefined || limit !== undefined) && (
{offset !== undefined && `offset: ${offset}`} {offset !== undefined && limit !== undefined && ', '} {limit !== undefined && `limit: ${limit}`}
)}
); } // Special rendering for Agent tool - do not leak full bootstrap prompts in UI logs. if (toolName === 'Agent') { const details = getAgentToolDisplayDetails(input); return (
{labels.agentAction}
{details.action}
{details.teammateName && (
{labels.agentTeammate}
{details.teammateName}
)} {details.teamName && (
{labels.agentTeam}
{details.teamName}
)} {details.runtime && (
{labels.agentRuntime}
{details.runtime}
)} {details.subagentType && (
{labels.agentType}
{details.subagentType}
)}
{labels.startupInstructionsHidden}
); } // Default: key-value format with readable string values return (
{Object.entries(input).length > 0 ? ( Object.entries(input).map(([key, value]) => (
{key}
{formatInputValue(value)}
)) ) : (
{labels.noInputRecorded}
)}
); } function readInputString(input: Record, keys: string[]): string | undefined { for (const key of keys) { const value = input[key]; if (typeof value === 'string' && value.length > 0) { return value; } } return undefined; } function formatInputValue(value: unknown): string { if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } return JSON.stringify(value, null, 2); } /** * Renders the output section with theme-aware styling. */ /** * Extracts display text from tool output content. * Handles content block arrays from the API by extracting text fields * and pretty-printing JSON when possible. */ export function extractOutputText(content: string | unknown[]): string { let displayText: string; // Normalize: if content is a string that parses to an array of content blocks, treat as array let normalizedContent: string | unknown[] = content; if (typeof content === 'string') { try { const parsed: unknown = JSON.parse(content); if (Array.isArray(parsed) && parsed.length > 0 && isContentBlock(parsed[0])) { normalizedContent = parsed as unknown[]; } } catch { // Not JSON, keep as string } } if (typeof normalizedContent === 'string') { displayText = normalizedContent; } else if (Array.isArray(normalizedContent)) { // Extract text from content blocks (e.g. [{"type":"text","text":"..."}]) displayText = normalizedContent .map((block) => typeof block === 'object' && block !== null && 'text' in block ? (block as { text: string }).text : JSON.stringify(block, null, 2) ) .join('\n'); } else { displayText = JSON.stringify(normalizedContent, null, 2); } // Try to pretty-print if the extracted text is valid JSON try { const parsed: unknown = JSON.parse(displayText); displayText = JSON.stringify(parsed, null, 2); } catch { // Not JSON, use as-is } return displayText; } export function formatToolOutputForDisplay( toolName: string, content: string | unknown[] ): string | unknown[] { if (!isAgentTeamsToolName(toolName)) { return content; } const parsed = parseJsonObject(extractOutputText(content)); if (!parsed) { return content; } const unwrapped = unwrapAgentTeamsResponse(parsed); if (!unwrapped) { return content; } const lines = formatAgentTeamsResponse(toolName, unwrapped.wrapperKey, unwrapped.payload); return lines.length > 0 ? lines.join('\n') : content; } function isAgentTeamsToolName(toolName: string): boolean { return ( toolName.startsWith('agent-teams_') || toolName.startsWith('agent_teams_') || toolName.startsWith('mcp__agent-teams__') || toolName.startsWith('mcp__agent_teams__') ); } function parseJsonObject(text: string): Record | null { try { const parsed: unknown = JSON.parse(text); return asRecord(parsed); } catch { return null; } } function unwrapAgentTeamsResponse( parsed: Record ): { wrapperKey: string | null; payload: Record } | null { const wrapperKey = Object.keys(parsed).find( (key) => key.startsWith('agent_teams_') && key.endsWith('_response') ) ?? null; const payload = wrapperKey ? asRecord(parsed[wrapperKey]) : parsed; return payload ? { wrapperKey, payload } : null; } function formatAgentTeamsResponse( toolName: string, wrapperKey: string | null, payload: Record ): string[] { if (hasErrorPayload(payload)) { return []; } const lines: string[] = [getAgentTeamsResponseTitle(toolName, wrapperKey)]; appendField(lines, 'Team', readString(payload.teamName)); appendField(lines, 'Task ID', readString(payload.taskId)); appendField(lines, 'Message ID', readString(payload.messageId)); const comment = asRecord(payload.comment); if (comment) { appendField(lines, 'Comment ID', readString(comment.id)); appendField(lines, 'Author', readString(comment.author)); appendField(lines, 'Created', readString(comment.createdAt)); appendBody(lines, readString(comment.text)); return lines; } const message = asRecord(payload.message); if (message) { appendField(lines, 'From', readString(message.from)); appendField(lines, 'To', readString(message.to)); appendField(lines, 'Created', readString(message.createdAt)); appendBody(lines, readString(message.text) ?? readString(message.summary)); return lines; } const task = asRecord(payload.task); if (task) { appendField(lines, 'Task', readString(task.title) ?? readString(task.name)); appendField(lines, 'Status', readString(task.status)); appendField(lines, 'Owner', readString(task.owner)); appendBody(lines, readString(task.description)); return lines; } appendField(lines, 'Status', readString(payload.status)); appendBody(lines, readString(payload.text) ?? readString(payload.summary)); return lines.length > 1 ? lines : []; } function hasErrorPayload(payload: Record): boolean { return ( typeof payload.error === 'string' || typeof payload.errorMessage === 'string' || payload.ok === false || payload.success === false ); } function getAgentTeamsResponseTitle(toolName: string, wrapperKey: string | null): string { const key = `${toolName} ${wrapperKey ?? ''}`; if (key.includes('task_add_comment')) return 'Task comment added'; if (key.includes('task_complete')) return 'Task completed'; if (key.includes('task_start')) return 'Task started'; if (key.includes('task_set_owner')) return 'Task owner updated'; if (key.includes('task_set_clarification')) return 'Task clarification updated'; if (key.includes('task_attach_comment_file')) return 'Task comment file attached'; if (key.includes('message_send')) return 'Message sent'; if (key.includes('task_get')) return 'Task loaded'; return 'Agent Teams tool result'; } function appendField(lines: string[], label: string, value: string | null | undefined): void { if (!value || value.trim().length === 0) { return; } lines.push(`${label}: ${value.trim()}`); } function appendBody(lines: string[], value: string | null | undefined): void { if (!value || value.trim().length === 0) { return; } lines.push(''); lines.push(value.trim()); } function readString(value: unknown): string | null { return typeof value === 'string' ? value : null; } function asRecord(value: unknown): Record | null { return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record) : null; } function isContentBlock(value: unknown): boolean { return ( typeof value === 'object' && value !== null && 'type' in value && typeof (value as Record).type === 'string' ); } export function renderOutput(content: string | unknown[]): React.ReactElement { const displayText = extractOutputText(content); return (
      {displayText}
    
); }