loop-benchmarking

Controlled experiments across agentic coding configurations. Same task, one variable, what actually works.
git clone https://git.shiptheloop.com/loop-benchmarking.git
Log | Files | Refs | README

TranscriptViewer.tsx (19205B)


      1 import { useState, type ReactNode } from "react";
      2 
      3 interface TranscriptViewerProps {
      4   lines: string[];
      5 }
      6 
      7 // -- Theme tokens (inline, no CSS file needed) --
      8 
      9 const theme = {
     10   bg: "#0f1117",
     11   bgCard: "#1a1d27",
     12   border: "#2d3045",
     13   text: "#e5e7eb",
     14   textMuted: "#9ca3af",
     15   accent: "#6366f1",
     16   green: "#22c55e",
     17   red: "#ef4444",
     18   yellow: "#eab308",
     19   fontMono: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
     20 };
     21 
     22 // -- Types for parsed transcript events --
     23 
     24 interface ContentBlockThinking {
     25   type: "thinking";
     26   thinking: string;
     27 }
     28 
     29 interface ContentBlockText {
     30   type: "text";
     31   text: string;
     32 }
     33 
     34 interface ContentBlockToolUse {
     35   type: "tool_use";
     36   id?: string;
     37   name: string;
     38   input: Record<string, unknown>;
     39 }
     40 
     41 type ContentBlock = ContentBlockThinking | ContentBlockText | ContentBlockToolUse;
     42 
     43 interface AssistantMessage {
     44   content: ContentBlock[];
     45 }
     46 
     47 interface ToolUseResult {
     48   stdout?: string;
     49   stderr?: string;
     50   [key: string]: unknown;
     51 }
     52 
     53 interface TranscriptEvent {
     54   type?: string;
     55   subtype?: string;
     56   // system init
     57   model?: string;
     58   tools?: unknown[];
     59   // assistant
     60   message?: AssistantMessage;
     61   // user (tool results)
     62   tool_use_result?: ToolUseResult | string;
     63   // result
     64   result?: string;
     65   total_cost_usd?: number;
     66   num_turns?: number;
     67   usage?: Record<string, unknown>;
     68   [key: string]: unknown;
     69 }
     70 
     71 // -- Minimal markdown renderer --
     72 
     73 function renderMarkdown(text: string): ReactNode[] {
     74   // Split by code blocks first
     75   const parts = text.split(/(```[\s\S]*?```)/g);
     76   const nodes: ReactNode[] = [];
     77 
     78   parts.forEach((part, i) => {
     79     if (part.startsWith("```")) {
     80       // Fenced code block
     81       const match = part.match(/^```(\w*)\n?([\s\S]*?)```$/);
     82       const code = match ? match[2] : part.slice(3, -3);
     83       nodes.push(
     84         <pre
     85           key={i}
     86           style={{
     87             background: "#12141c",
     88             padding: "10px 12px",
     89             borderRadius: 4,
     90             fontSize: "0.8rem",
     91             fontFamily: theme.fontMono,
     92             overflow: "auto",
     93             margin: "6px 0",
     94             color: theme.text,
     95             border: `1px solid ${theme.border}`,
     96           }}
     97         >
     98           {code}
     99         </pre>
    100       );
    101     } else {
    102       // Inline formatting
    103       nodes.push(...renderInlineMarkdown(part, i));
    104     }
    105   });
    106 
    107   return nodes;
    108 }
    109 
    110 function renderInlineMarkdown(text: string, keyBase: number): ReactNode[] {
    111   // Handle bold, inline code
    112   const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
    113   return parts.map((part, j) => {
    114     const key = `${keyBase}-${j}`;
    115     if (part.startsWith("**") && part.endsWith("**")) {
    116       return (
    117         <strong key={key} style={{ color: theme.text }}>
    118           {part.slice(2, -2)}
    119         </strong>
    120       );
    121     }
    122     if (part.startsWith("`") && part.endsWith("`")) {
    123       return (
    124         <code
    125           key={key}
    126           style={{
    127             background: "#12141c",
    128             padding: "1px 5px",
    129             borderRadius: 3,
    130             fontSize: "0.8rem",
    131             fontFamily: theme.fontMono,
    132             color: "#c4b5fd",
    133           }}
    134         >
    135           {part.slice(1, -1)}
    136         </code>
    137       );
    138     }
    139     return <span key={key}>{part}</span>;
    140   });
    141 }
    142 
    143 // -- Collapsible wrapper --
    144 
    145 function Collapsible({
    146   label,
    147   defaultOpen = false,
    148   children,
    149   labelColor,
    150 }: {
    151   label: string;
    152   defaultOpen?: boolean;
    153   children: ReactNode;
    154   labelColor?: string;
    155   muted?: boolean;
    156 }) {
    157   const [open, setOpen] = useState(defaultOpen);
    158   return (
    159     <div>
    160       <button
    161         onClick={() => setOpen(!open)}
    162         style={{
    163           background: "none",
    164           border: "none",
    165           color: labelColor || theme.textMuted,
    166           cursor: "pointer",
    167           fontSize: "0.75rem",
    168           fontFamily: theme.fontMono,
    169           padding: "2px 0",
    170           display: "flex",
    171           alignItems: "center",
    172           gap: 4,
    173         }}
    174       >
    175         <span style={{ display: "inline-block", width: 12, textAlign: "center", fontSize: "0.65rem" }}>
    176           {open ? "\u25BC" : "\u25B6"}
    177         </span>
    178         {label}
    179       </button>
    180       {open && <div style={{ marginTop: 4 }}>{children}</div>}
    181     </div>
    182   );
    183 }
    184 
    185 // -- Code block display --
    186 
    187 function CodeBlock({
    188   children,
    189   maxHeight,
    190   color,
    191 }: {
    192   children: string;
    193   maxHeight?: string;
    194   color?: string;
    195 }) {
    196   return (
    197     <pre
    198       style={{
    199         background: "#12141c",
    200         padding: "8px 12px",
    201         borderRadius: 4,
    202         fontSize: "0.78rem",
    203         fontFamily: theme.fontMono,
    204         overflow: "auto",
    205         maxHeight: maxHeight || "300px",
    206         margin: "4px 0 0",
    207         color: color || theme.textMuted,
    208         border: `1px solid ${theme.border}`,
    209         whiteSpace: "pre-wrap",
    210         wordBreak: "break-word",
    211       }}
    212     >
    213       {children}
    214     </pre>
    215   );
    216 }
    217 
    218 // -- Tool header (terminal-style bar) --
    219 
    220 function ToolHeader({ name, detail }: { name: string; detail?: string }) {
    221   return (
    222     <div
    223       style={{
    224         display: "flex",
    225         alignItems: "center",
    226         gap: 8,
    227         marginBottom: 4,
    228       }}
    229     >
    230       <span
    231         style={{
    232           fontSize: "0.7rem",
    233           fontFamily: theme.fontMono,
    234           color: theme.yellow,
    235           fontWeight: 600,
    236           textTransform: "uppercase",
    237           letterSpacing: "0.05em",
    238         }}
    239       >
    240         {name}
    241       </span>
    242       {detail && (
    243         <span
    244           style={{
    245             fontSize: "0.75rem",
    246             fontFamily: theme.fontMono,
    247             color: theme.textMuted,
    248             overflow: "hidden",
    249             textOverflow: "ellipsis",
    250             whiteSpace: "nowrap",
    251           }}
    252         >
    253           {detail}
    254         </span>
    255       )}
    256     </div>
    257   );
    258 }
    259 
    260 // -- Event card wrapper --
    261 
    262 function EventCard({
    263   borderColor,
    264   bgTint,
    265   children,
    266   compact,
    267 }: {
    268   borderColor: string;
    269   bgTint?: string;
    270   children: ReactNode;
    271   compact?: boolean;
    272 }) {
    273   return (
    274     <div
    275       style={{
    276         padding: compact ? "6px 12px" : "10px 14px",
    277         borderLeft: `3px solid ${borderColor}`,
    278         marginBottom: 4,
    279         background: bgTint || "transparent",
    280         borderRadius: "0 4px 4px 0",
    281       }}
    282     >
    283       {children}
    284     </div>
    285   );
    286 }
    287 
    288 // -- Detect file path from heredoc or cat > writes --
    289 
    290 function extractFileWriteTarget(command: string): string | null {
    291   // cat > filename, cat << ... > filename
    292   const catWrite = command.match(/cat\s+>+\s*([^\s<]+)/);
    293   if (catWrite) return catWrite[1];
    294   const heredoc = command.match(/cat\s*<<['"\\]*(\w+)['"]*\s*>\s*([^\s]+)/);
    295   if (heredoc) return heredoc[2];
    296   return null;
    297 }
    298 
    299 // -- Render individual content blocks --
    300 
    301 function renderContentBlock(block: ContentBlock, index: number): ReactNode {
    302   switch (block.type) {
    303     case "thinking":
    304       return (
    305         <EventCard key={index} borderColor="#4b5563" bgTint="rgba(75, 85, 99, 0.05)">
    306           <Collapsible label="thinking..." labelColor="#6b7280">
    307             <div
    308               style={{
    309                 fontStyle: "italic",
    310                 color: theme.textMuted,
    311                 fontSize: "0.82rem",
    312                 whiteSpace: "pre-wrap",
    313                 lineHeight: 1.5,
    314                 maxHeight: "300px",
    315                 overflow: "auto",
    316               }}
    317             >
    318               {block.thinking}
    319             </div>
    320           </Collapsible>
    321         </EventCard>
    322       );
    323 
    324     case "text":
    325       return (
    326         <EventCard key={index} borderColor={theme.accent} bgTint="rgba(99, 102, 241, 0.04)">
    327           <div style={{ fontSize: "0.875rem", lineHeight: 1.6, color: theme.text, whiteSpace: "pre-wrap" }}>
    328             {renderMarkdown(block.text)}
    329           </div>
    330         </EventCard>
    331       );
    332 
    333     case "tool_use":
    334       return renderToolUse(block, index);
    335 
    336     default:
    337       return null;
    338   }
    339 }
    340 
    341 function renderToolUse(block: ContentBlockToolUse, index: number): ReactNode {
    342   const name = block.name;
    343   const input = block.input || {};
    344 
    345   if (name === "Bash") {
    346     const command = (input.command as string) || JSON.stringify(input, null, 2);
    347     const fileTarget = extractFileWriteTarget(command);
    348     return (
    349       <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)">
    350         <ToolHeader name="Bash" detail={fileTarget ? `writing to ${fileTarget}` : undefined} />
    351         <CodeBlock>{command}</CodeBlock>
    352       </EventCard>
    353     );
    354   }
    355 
    356   if (name === "Read") {
    357     const filePath = (input.file_path as string) || "";
    358     return (
    359       <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)">
    360         <ToolHeader name="Read" detail={filePath} />
    361       </EventCard>
    362     );
    363   }
    364 
    365   if (name === "Write") {
    366     const filePath = (input.file_path as string) || "";
    367     const content = (input.content as string) || "";
    368     return (
    369       <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)">
    370         <ToolHeader name="Write" detail={filePath} />
    371         {content && <CodeBlock maxHeight="250px">{content}</CodeBlock>}
    372       </EventCard>
    373     );
    374   }
    375 
    376   if (name === "Edit") {
    377     const filePath = (input.file_path as string) || "";
    378     const oldStr = (input.old_string as string) || "";
    379     const newStr = (input.new_string as string) || "";
    380     return (
    381       <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)">
    382         <ToolHeader name="Edit" detail={filePath} />
    383         {oldStr && (
    384           <div style={{ marginTop: 4 }}>
    385             <span style={{ fontSize: "0.7rem", color: theme.red, fontFamily: theme.fontMono }}>- old</span>
    386             <CodeBlock maxHeight="150px" color={theme.red}>{oldStr}</CodeBlock>
    387           </div>
    388         )}
    389         {newStr && (
    390           <div style={{ marginTop: 4 }}>
    391             <span style={{ fontSize: "0.7rem", color: theme.green, fontFamily: theme.fontMono }}>+ new</span>
    392             <CodeBlock maxHeight="150px" color={theme.green}>{newStr}</CodeBlock>
    393           </div>
    394         )}
    395       </EventCard>
    396     );
    397   }
    398 
    399   // Generic tool
    400   return (
    401     <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)">
    402       <ToolHeader name={name} />
    403       <CodeBlock maxHeight="200px">{JSON.stringify(input, null, 2)}</CodeBlock>
    404     </EventCard>
    405   );
    406 }
    407 
    408 // -- Render a full event (top-level JSON line) --
    409 
    410 function renderEvent(event: TranscriptEvent, index: number): ReactNode {
    411   const type = event.type;
    412 
    413   // System init
    414   if (type === "system" && event.subtype === "init") {
    415     const model = (event.model as string) || "unknown model";
    416     const toolCount = Array.isArray(event.tools) ? event.tools.length : 0;
    417     return (
    418       <div
    419         key={index}
    420         style={{
    421           display: "inline-flex",
    422           alignItems: "center",
    423           gap: 8,
    424           background: "#252836",
    425           borderRadius: 999,
    426           padding: "4px 14px",
    427           marginBottom: 6,
    428           fontSize: "0.75rem",
    429           color: theme.textMuted,
    430           fontFamily: theme.fontMono,
    431         }}
    432       >
    433         <span>{model}</span>
    434         {toolCount > 0 && (
    435           <>
    436             <span style={{ color: theme.border }}>|</span>
    437             <span>{toolCount} tools</span>
    438           </>
    439         )}
    440       </div>
    441     );
    442   }
    443 
    444   // API retry / rate limit
    445   if (type === "system" && event.subtype === "api_retry") {
    446     const attempt = (event.attempt as number) || 0;
    447     const maxRetries = (event.max_retries as number) || 10;
    448     const delay = (event.retry_delay_ms as number) || 0;
    449     const errorStatus = (event.error_status as number) || 0;
    450     const error = (event.error as string) || "";
    451     return (
    452       <EventCard key={index} borderColor={theme.red} bgTint="rgba(239, 68, 68, 0.06)" compact>
    453         <div style={{ display: "flex", alignItems: "center", gap: 10, fontSize: "0.75rem", fontFamily: theme.fontMono }}>
    454           <span style={{ color: theme.red, fontWeight: 700, fontSize: "0.7rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
    455             RATE LIMITED
    456           </span>
    457           <span style={{ color: theme.textMuted }}>
    458             {errorStatus} {error}
    459           </span>
    460           <span style={{ color: theme.textMuted }}>
    461             attempt {attempt}/{maxRetries}
    462           </span>
    463           <span style={{ color: theme.textMuted }}>
    464             retry in {(delay / 1000).toFixed(1)}s
    465           </span>
    466         </div>
    467       </EventCard>
    468     );
    469   }
    470 
    471   // Harness config event
    472   if (type === "harness" && event.subtype === "config") {
    473     const tools = Array.isArray(event.tools) ? (event.tools as string[]).join(", ") : "";
    474     return (
    475       <EventCard key={index} borderColor="#4b5563" bgTint="rgba(75, 85, 99, 0.05)" compact>
    476         <div style={{ fontSize: "0.7rem", fontFamily: theme.fontMono, color: theme.textMuted, display: "flex", flexWrap: "wrap", gap: "12px" }}>
    477           <span>model: <span style={{ color: theme.text }}>{event.model as string}</span></span>
    478           <span>effort: <span style={{ color: theme.text }}>{event.effort as string}</span></span>
    479           <span>lang: <span style={{ color: theme.text }}>{event.language as string}</span></span>
    480           <span>budget: <span style={{ color: theme.text }}>${event.max_budget_usd as number}</span></span>
    481           <span>tools: <span style={{ color: theme.text }}>{tools}</span></span>
    482         </div>
    483       </EventCard>
    484     );
    485   }
    486 
    487   // Assistant message - contains content blocks
    488   if (type === "assistant" && event.message?.content) {
    489     const blocks = event.message.content;
    490     return (
    491       <div key={index} style={{ marginBottom: 2 }}>
    492         {blocks.map((block, bi) => renderContentBlock(block, bi))}
    493       </div>
    494     );
    495   }
    496 
    497   // User prompt (injected by harness)
    498   if (type === "user" && event.subtype === "prompt") {
    499     const content = event.message?.content as string || "";
    500     return (
    501       <EventCard key={index} borderColor="#818cf8" bgTint="rgba(129, 140, 248, 0.08)">
    502         <div style={{ fontSize: "0.7rem", fontFamily: theme.fontMono, color: "#818cf8", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 6 }}>
    503           PROMPT
    504         </div>
    505         <div style={{ fontSize: "0.875rem", lineHeight: 1.6, color: theme.text, whiteSpace: "pre-wrap" }}>
    506           {renderMarkdown(content)}
    507         </div>
    508       </EventCard>
    509     );
    510   }
    511 
    512   // Context file (injected by harness)
    513   if (type === "user" && event.subtype === "context") {
    514     const content = event.message?.content as string || "";
    515     return (
    516       <EventCard key={index} borderColor="#4b5563" bgTint="rgba(75, 85, 99, 0.05)">
    517         <Collapsible label="context file" labelColor="#6b7280" defaultOpen={false}>
    518           <div style={{ fontSize: "0.82rem", color: theme.textMuted, whiteSpace: "pre-wrap" }}>
    519             {renderMarkdown(content)}
    520           </div>
    521         </Collapsible>
    522       </EventCard>
    523     );
    524   }
    525 
    526   // User event (tool results)
    527   if (type === "user") {
    528     const result = event.tool_use_result;
    529     if (!result) return null;
    530 
    531     let stdout = "";
    532     let stderr = "";
    533 
    534     if (typeof result === "string") {
    535       stdout = result;
    536     } else {
    537       stdout = (result.stdout as string) || "";
    538       stderr = (result.stderr as string) || "";
    539     }
    540 
    541     const content = stdout || stderr;
    542     if (!content) return null;
    543 
    544     const lineCount = content.split("\n").length;
    545     const shouldCollapse = lineCount > 10;
    546 
    547     return (
    548       <EventCard key={index} borderColor={theme.green} bgTint="rgba(34, 197, 94, 0.03)" compact>
    549         {shouldCollapse ? (
    550           <Collapsible label={`result (${lineCount} lines)`} labelColor={theme.green}>
    551             {stderr && <CodeBlock maxHeight="200px" color={theme.red}>{stderr}</CodeBlock>}
    552             {stdout && <CodeBlock maxHeight="300px">{stdout}</CodeBlock>}
    553           </Collapsible>
    554         ) : (
    555           <>
    556             {stderr && <CodeBlock maxHeight="200px" color={theme.red}>{stderr}</CodeBlock>}
    557             {stdout && <CodeBlock maxHeight="200px">{stdout}</CodeBlock>}
    558           </>
    559         )}
    560       </EventCard>
    561     );
    562   }
    563 
    564   // Final result
    565   if (type === "result") {
    566     return (
    567       <div
    568         key={index}
    569         style={{
    570           background: "rgba(99, 102, 241, 0.08)",
    571           border: `1px solid ${theme.accent}`,
    572           borderRadius: 6,
    573           padding: "14px 16px",
    574           marginTop: 8,
    575         }}
    576       >
    577         <div
    578           style={{
    579             fontSize: "0.7rem",
    580             color: theme.accent,
    581             fontFamily: theme.fontMono,
    582             fontWeight: 600,
    583             marginBottom: 8,
    584             textTransform: "uppercase",
    585             letterSpacing: "0.05em",
    586           }}
    587         >
    588           Result
    589         </div>
    590         {event.result && (
    591           <div style={{ fontSize: "0.875rem", color: theme.text, whiteSpace: "pre-wrap", lineHeight: 1.6 }}>
    592             {renderMarkdown(event.result)}
    593           </div>
    594         )}
    595         <div
    596           style={{
    597             display: "flex",
    598             gap: 16,
    599             marginTop: 10,
    600             fontSize: "0.75rem",
    601             fontFamily: theme.fontMono,
    602             color: theme.textMuted,
    603             flexWrap: "wrap",
    604           }}
    605         >
    606           {event.total_cost_usd != null && <span>cost: ${event.total_cost_usd.toFixed(4)}</span>}
    607           {event.num_turns != null && <span>turns: {event.num_turns}</span>}
    608         </div>
    609       </div>
    610     );
    611   }
    612 
    613   // Fallback: render compact raw JSON
    614   return (
    615     <EventCard key={index} borderColor={theme.border} compact>
    616       <pre
    617         style={{
    618           fontSize: "0.7rem",
    619           color: theme.textMuted,
    620           overflow: "auto",
    621           maxHeight: "80px",
    622           margin: 0,
    623           fontFamily: theme.fontMono,
    624         }}
    625       >
    626         {JSON.stringify(event, null, 2)}
    627       </pre>
    628     </EventCard>
    629   );
    630 }
    631 
    632 // -- Main component --
    633 
    634 export default function TranscriptViewer({ lines }: TranscriptViewerProps) {
    635   if (lines.length === 0) {
    636     return (
    637       <div
    638         style={{
    639           textAlign: "center",
    640           padding: "40px",
    641           color: theme.textMuted,
    642           background: theme.bgCard,
    643           borderRadius: 8,
    644           border: `1px solid ${theme.border}`,
    645         }}
    646       >
    647         No transcript available for this run.
    648       </div>
    649     );
    650   }
    651 
    652   const events: TranscriptEvent[] = lines
    653     .map((line) => {
    654       try {
    655         return JSON.parse(line);
    656       } catch {
    657         return null;
    658       }
    659     })
    660     .filter(Boolean);
    661 
    662   return (
    663     <div
    664       style={{
    665         maxHeight: "80vh",
    666         overflow: "auto",
    667         background: theme.bgCard,
    668         borderRadius: 8,
    669         border: `1px solid ${theme.border}`,
    670         padding: "14px 16px",
    671       }}
    672     >
    673       <div
    674         style={{
    675           position: "sticky",
    676           top: 0,
    677           background: theme.bgCard,
    678           paddingBottom: 8,
    679           zIndex: 1,
    680           borderBottom: `1px solid ${theme.border}`,
    681           marginBottom: 10,
    682         }}
    683       >
    684         <h3 style={{ margin: 0, fontSize: "0.95rem", color: theme.text }}>
    685           Transcript
    686           <span style={{ color: theme.textMuted, fontWeight: 400, fontSize: "0.8rem", marginLeft: 8 }}>
    687             {events.length} events
    688           </span>
    689         </h3>
    690       </div>
    691       {events.map((event, i) => renderEvent(event, i))}
    692     </div>
    693   );
    694 }

Impressum · Datenschutz