theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

ToolResultRenderer.tsx (3431B)


      1 import type { ReactNode } from "react";
      2 import { MarkdownRenderer } from "../Message/MarkdownRenderer";
      3 import { JsonResult } from "./JsonResult";
      4 import { DiffResult } from "./DiffResult";
      5 
      6 export type ContentType = "json" | "diff" | "markdown";
      7 
      8 export interface OperationCategory {
      9   label: string;
     10   color: string;
     11 }
     12 
     13 export interface PreambleResult {
     14   preamble: string;
     15   jsonContent: string | null;
     16 }
     17 
     18 const CATEGORY_MAP: Record<string, OperationCategory> = {
     19   read_file: { label: "READ", color: "blue" },
     20   write_file: { label: "WRITE", color: "green" },
     21   patch: { label: "PATCH", color: "yellow" },
     22   search_files: { label: "SEARCH", color: "purple" },
     23   terminal: { label: "EXEC", color: "orange" },
     24 };
     25 
     26 const HUNK_HEADER_RE = /^@@\s+[-+]\d+/m;
     27 
     28 function hasDiffMarkers(content: string): boolean {
     29   return HUNK_HEADER_RE.test(content);
     30 }
     31 
     32 function tryParseJson(content: string): { parsed: unknown; jsonStart: number } | null {
     33   const firstBrace = content.indexOf("{");
     34   const firstBracket = content.indexOf("[");
     35   const jsonStart =
     36     firstBrace === -1 ? firstBracket :
     37     firstBracket === -1 ? firstBrace :
     38     Math.min(firstBrace, firstBracket);
     39 
     40   if (jsonStart === -1) return null;
     41 
     42   const candidate = content.slice(jsonStart);
     43   try {
     44     const parsed = JSON.parse(candidate);
     45     return { parsed, jsonStart };
     46   } catch {
     47     return null;
     48   }
     49 }
     50 
     51 /** Detect whether content is a unified diff, JSON, or plain markdown. */
     52 export function detectContentType(content: string): ContentType {
     53   const trimmed = content.trim();
     54   if (!trimmed) return "markdown";
     55 
     56   // Diff first — diffs contain { / [ in code which would false-trigger JSON
     57   if (hasDiffMarkers(trimmed)) return "diff";
     58 
     59   // JSON — find first { or [, parse from there
     60   const result = tryParseJson(trimmed);
     61   if (result && (typeof result.parsed === "object" && result.parsed !== null)) {
     62     return "json";
     63   }
     64 
     65   return "markdown";
     66 }
     67 
     68 /** Split content into optional preamble text and JSON content. */
     69 export function extractPreamble(content: string): PreambleResult {
     70   const trimmed = content.trim();
     71   const result = tryParseJson(trimmed);
     72 
     73   if (!result || result.jsonStart === 0) {
     74     return result
     75       ? { preamble: "", jsonContent: trimmed }
     76       : { preamble: trimmed, jsonContent: null };
     77   }
     78 
     79   return {
     80     preamble: trimmed.slice(0, result.jsonStart).trim(),
     81     jsonContent: trimmed.slice(result.jsonStart),
     82   };
     83 }
     84 
     85 /** Map a tool name to an operation category with label and color. */
     86 export function getOperationCategory(toolName: string): OperationCategory {
     87   return CATEGORY_MAP[toolName] ?? { label: toolName, color: "gray" };
     88 }
     89 
     90 interface ToolResultRendererProps {
     91   content: string;
     92   toolName?: string;
     93 }
     94 
     95 /** Route tool result content to the appropriate renderer. */
     96 export function ToolResultRenderer({ content }: ToolResultRendererProps): ReactNode {
     97   const contentType = detectContentType(content);
     98 
     99   if (contentType === "diff") {
    100     return <DiffResult content={content} />;
    101   }
    102 
    103   if (contentType === "json") {
    104     const { preamble, jsonContent } = extractPreamble(content);
    105     try {
    106       const parsed = JSON.parse(jsonContent!);
    107       return (
    108         <>
    109           {preamble && <MarkdownRenderer content={preamble} />}
    110           <JsonResult data={parsed} />
    111         </>
    112       );
    113     } catch {
    114       // Malformed — fall through to markdown
    115     }
    116   }
    117 
    118   return <MarkdownRenderer content={content} />;
    119 }