import { For, Show, createMemo } from "solid-js" import type { ToolRenderer } from "../types" import { getRelativePath, getToolName, isToolStateCompleted, readToolStatePayload } from "../utils" import type { DiagnosticEntry } from "../diagnostics" type LspRangePosition = { line?: number character?: number } type LspRange = { start?: LspRangePosition } type LspDiagnostic = { message?: string severity?: number range?: LspRange } type ApplyPatchFile = { filePath?: string relativePath?: string type?: string diff?: string } function normalizePath(value: string): string { return value.replace(/\\/g, "/") } function determineSeverityTone(severity?: number): DiagnosticEntry["tone"] { if (severity === 1) return "error" if (severity === 2) return "warning" return "info" } function getSeverityMeta(tone: DiagnosticEntry["tone"]) { if (tone === "error") return { label: "ERR", icon: "!", rank: 0 } if (tone === "warning") return { label: "WARN", icon: "!", rank: 1 } return { label: "INFO", icon: "i", rank: 2 } } function resolveDiagnosticsKey( diagnostics: Record, file: ApplyPatchFile, ): string | undefined { const absolute = typeof file.filePath === "string" ? normalizePath(file.filePath) : "" const relative = typeof file.relativePath === "string" ? normalizePath(file.relativePath) : "" if (absolute && diagnostics[absolute]) return absolute if (relative && diagnostics[relative]) return relative if (absolute) { const direct = Object.keys(diagnostics).find((key) => normalizePath(key) === absolute) if (direct) return direct } if (relative) { const suffixMatch = Object.keys(diagnostics).find((key) => { const normalized = normalizePath(key) return normalized === relative || normalized.endsWith("/" + relative) }) if (suffixMatch) return suffixMatch } return undefined } function buildDiagnostics( diagnostics: Record, file: ApplyPatchFile, ): DiagnosticEntry[] { const key = resolveDiagnosticsKey(diagnostics, file) if (!key) return [] const list = diagnostics[key] if (!Array.isArray(list) || list.length === 0) return [] const normalizedKey = normalizePath(key) const entries: DiagnosticEntry[] = [] for (let index = 0; index < list.length; index++) { const diagnostic = list[index] if (!diagnostic || typeof diagnostic.message !== "string") continue const tone = determineSeverityTone(typeof diagnostic.severity === "number" ? diagnostic.severity : undefined) const severityMeta = getSeverityMeta(tone) const line = typeof diagnostic.range?.start?.line === "number" ? diagnostic.range.start.line + 1 : 0 const column = typeof diagnostic.range?.start?.character === "number" ? diagnostic.range.start.character + 1 : 0 entries.push({ id: `${normalizedKey}-${index}-${diagnostic.message}`, severity: severityMeta.rank, tone, label: severityMeta.label, icon: severityMeta.icon, message: diagnostic.message, filePath: normalizedKey, displayPath: getRelativePath(normalizedKey), line, column, }) } return entries.sort((a, b) => a.severity - b.severity) } function DiagnosticsInline(props: { entries: DiagnosticEntry[]; label: string }) { return ( 0}>
{(entry) => (
{entry.icon} {entry.label} {entry.displayPath} :L{entry.line || "-"}:C{entry.column || "-"} {entry.message}
)}
) } export const applyPatchRenderer: ToolRenderer = { tools: ["apply_patch"], getAction: () => "Preparing apply_patch...", getTitle({ toolState }) { const state = toolState() if (!state) return undefined if (state.status === "pending") return getToolName("apply_patch") const { metadata } = readToolStatePayload(state) const files = Array.isArray((metadata as any).files) ? ((metadata as any).files as ApplyPatchFile[]) : [] if (files.length > 0) { return `${getToolName("apply_patch")} (${files.length} file${files.length === 1 ? "" : "s"})` } return getToolName("apply_patch") }, renderBody({ toolState, renderDiff, renderMarkdown }) { const state = toolState() if (!state || state.status === "pending") return null const payload = readToolStatePayload(state) const files = createMemo(() => { const list = (payload.metadata as any).files return Array.isArray(list) ? (list as ApplyPatchFile[]) : [] }) const diagnosticsMap = createMemo(() => { const value = (payload.metadata as any).diagnostics return value && typeof value === "object" ? (value as Record) : {} }) if (files().length === 0) { const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null if (!fallback) return null return renderMarkdown({ content: fallback, size: "large", disableHighlight: state.status === "running" }) } return (
{(file, index) => { const labelBase = file.relativePath || file.filePath || `File ${index() + 1}` const diffText = typeof file.diff === "string" ? file.diff : "" const filePath = typeof file.filePath === "string" ? file.filePath : file.relativePath const entries = createMemo(() => buildDiagnostics(diagnosticsMap(), file)) return (
0}> {renderDiff( { diffText, filePath }, { label: `Diff ยท ${getRelativePath(labelBase)}`, cacheKey: `apply_patch:${labelBase}:${index()}`, }, )}
) }}
) }, }