/** `ast_edit` — structural AST rewrites: per-op pattern/replacement pairs, replacement counts, diffs. */ import type { ReactNode } from "react"; import { Badge, CodeBlock, DiffBlock, InvalidArg, Note, Output, PathText, ResultText, Row } from "../parts"; import type { ToolRenderer, ToolRenderProps } from "../types"; import { detailsRecord, isRecord, languageFromPath, num, shortenPath, str } from "../util"; interface AstEditOp { pat: string; out: string; } interface AstEditDetails { totalReplacements: number | null; filesTouched: number | null; filesSearched: number | null; limitReached: boolean; scopePath: string | null; fileReplacements: Array<{ path: string; count: number | null }>; parseErrors: string[]; parseErrorsTotal: number | null; displayContent: string | null; } function pathsOf(args: Record): string[] { if (!Array.isArray(args.paths)) return []; const paths: string[] = []; for (const p of args.paths) { const s = str(p); if (s) paths.push(s); } return paths; } function opsOf(args: Record): AstEditOp[] { if (!Array.isArray(args.ops)) return []; const ops: AstEditOp[] = []; for (const op of args.ops) { if (!isRecord(op)) continue; ops.push({ pat: str(op.pat) ?? "", out: str(op.out) ?? "" }); } return ops; } function detailsOf(result: ToolRenderProps["result"]): AstEditDetails | null { const d = detailsRecord(result); if (!d) return null; const fileReplacements: AstEditDetails["fileReplacements"] = []; if (Array.isArray(d.fileReplacements)) { for (const fr of d.fileReplacements) { if (!isRecord(fr)) continue; const path = str(fr.path); if (path) fileReplacements.push({ path, count: num(fr.count) }); } } const parseErrors: string[] = []; if (Array.isArray(d.parseErrors)) { for (const e of d.parseErrors) { const s = str(e); if (s) parseErrors.push(s); } } return { totalReplacements: num(d.totalReplacements), filesTouched: num(d.filesTouched), filesSearched: num(d.filesSearched), limitReached: d.limitReached === true, scopePath: str(d.scopePath), fileReplacements, parseErrors, parseErrorsTotal: num(d.parseErrorsTotal), displayContent: str(d.displayContent), }; } function Summary({ args, result }: ToolRenderProps): ReactNode { const paths = pathsOf(args); const first = paths[0]; const opCount = Array.isArray(args.ops) ? args.ops.length : 0; const details = detailsOf(result); const total = details?.totalReplacements; return ( <> {first ? : } {paths.length > 1 && +{paths.length - 1} more} {opCount} op{opCount === 1 ? "" : "s"} {total != null && ( 0 ? "ok" : "warn"}> {total} replacement{total === 1 ? "" : "s"} )} {details?.limitReached && limit} ); } function OpCell({ op, lang }: { op: AstEditOp; lang: string | null }): ReactNode { return (
{op.pat ? ( ) : ( )} {op.out ? ( ) : (
deletion — matched code is removed
)}
); } function Body({ args, result }: ToolRenderProps): ReactNode { const paths = pathsOf(args); const first = paths[0]; const ops = opsOf(args); const details = result?.isError ? null : detailsOf(result); const lang = first ? languageFromPath(first) : null; const parseErrorsTotal = details ? (details.parseErrorsTotal ?? details.parseErrors.length) : 0; return ( <> {paths.length > 1 && (
{paths.map((p, i) => ( ))}
)} {ops.length > 0 && (
{ops.map((op, i) => ( ))}
)} {details && ( {details.totalReplacements != null && ( 0 ? "ok" : "warn"}> {details.totalReplacements} replacement{details.totalReplacements === 1 ? "" : "s"} )} {details.filesTouched != null && ( {details.filesTouched} file{details.filesTouched === 1 ? "" : "s"} )} {details.filesSearched != null && searched {details.filesSearched}} {details.scopePath && in {shortenPath(details.scopePath)}} {details.limitReached && limit reached} )} {details && details.fileReplacements.length > 0 && (
{details.fileReplacements.map((fr, i) => ( ))}
)} {details?.limitReached && limit reached; narrow path} {details && details.parseErrors.length > 0 && ( )} {details?.displayContent ? ( ) : ( )} ); } export const astEditRenderer: ToolRenderer = { Summary, Body };