From 68551f673148f5a7522221b16aa7f9f4a4746fc6 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 20 Apr 2026 21:08:33 +0100 Subject: [PATCH] fix(ui): unify apply_patch diagnostics matching --- .../src/components/tool-call/diagnostics.ts | 98 ++++++++++------- .../tool-call/renderers/apply-patch.tsx | 103 +----------------- 2 files changed, 66 insertions(+), 135 deletions(-) diff --git a/packages/ui/src/components/tool-call/diagnostics.ts b/packages/ui/src/components/tool-call/diagnostics.ts index 304e92b8..d48b3975 100644 --- a/packages/ui/src/components/tool-call/diagnostics.ts +++ b/packages/ui/src/components/tool-call/diagnostics.ts @@ -17,6 +17,8 @@ interface LspDiagnostic { range?: LspRange } +export type DiagnosticsMap = Record + export interface DiagnosticEntry { id: string severity: number @@ -30,7 +32,7 @@ export interface DiagnosticEntry { column: number } -function normalizeDiagnosticPath(path: string) { +export function normalizeDiagnosticPath(path: string) { return path.replace(/\\/g, "/") } @@ -53,49 +55,71 @@ export function extractDiagnostics(state: ToolState | undefined): DiagnosticEntr const metadata = (state.metadata || {}) as Record const input = (state.input || {}) as Record - const diagnosticsMap = metadata?.diagnostics as Record | undefined + const diagnosticsMap = metadata?.diagnostics as DiagnosticsMap | undefined if (!diagnosticsMap) return [] - const preferredPath = [input.filePath, metadata.filePath, metadata.filepath, input.path].find( - (value) => typeof value === "string" && value.length > 0, - ) as string | undefined + return buildDiagnosticEntries(diagnosticsMap, [input.filePath, metadata.filePath, metadata.filepath, input.path]) +} - const normalizedPreferred = preferredPath ? normalizeDiagnosticPath(preferredPath) : undefined - if (!normalizedPreferred) return [] - const candidateEntries = Object.entries(diagnosticsMap).filter(([, items]) => Array.isArray(items) && items.length > 0) - if (candidateEntries.length === 0) return [] +export function resolveDiagnosticsKey(diagnostics: DiagnosticsMap, preferredPaths: Array): string | undefined { + if (Object.keys(diagnostics).length === 0) return undefined - const prioritizedEntries = candidateEntries.filter(([path]) => { - const normalized = normalizeDiagnosticPath(path) - return normalized === normalizedPreferred - }) + const normalizedPreferred = preferredPaths + .filter((value): value is string => typeof value === "string" && value.length > 0) + .map((value) => normalizeDiagnosticPath(value)) - if (prioritizedEntries.length === 0) return [] + if (normalizedPreferred.length === 0) return undefined + + for (const preferred of normalizedPreferred) { + if (diagnostics[preferred]) return preferred + } + + const keys = Object.keys(diagnostics) + + for (const preferred of normalizedPreferred) { + const direct = keys.find((key) => normalizeDiagnosticPath(key) === preferred) + if (direct) return direct + } + + for (const preferred of normalizedPreferred) { + const suffixMatch = keys.find((key) => { + const normalized = normalizeDiagnosticPath(key) + return normalized === preferred || normalized.endsWith("/" + preferred) + }) + if (suffixMatch) return suffixMatch + } + + return undefined +} + +export function buildDiagnosticEntries(diagnostics: DiagnosticsMap, preferredPaths: Array): DiagnosticEntry[] { + const key = resolveDiagnosticsKey(diagnostics, preferredPaths) + if (!key) return [] + + const list = diagnostics[key] + if (!Array.isArray(list) || list.length === 0) return [] const entries: DiagnosticEntry[] = [] - for (const [pathKey, list] of prioritizedEntries) { - if (!Array.isArray(list)) continue - const normalizedPath = normalizeDiagnosticPath(pathKey) - 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: `${normalizedPath}-${index}-${diagnostic.message}`, - severity: severityMeta.rank, - tone, - label: severityMeta.label, - icon: severityMeta.icon, - message: diagnostic.message, - filePath: normalizedPath, - displayPath: getRelativePath(normalizedPath), - line, - column, - }) - } + const normalizedPath = normalizeDiagnosticPath(key) + 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: `${normalizedPath}-${index}-${diagnostic.message}`, + severity: severityMeta.rank, + tone, + label: severityMeta.label, + icon: severityMeta.icon, + message: diagnostic.message, + filePath: normalizedPath, + displayPath: getRelativePath(normalizedPath), + line, + column, + }) } return entries.sort((a, b) => a.severity - b.severity) diff --git a/packages/ui/src/components/tool-call/renderers/apply-patch.tsx b/packages/ui/src/components/tool-call/renderers/apply-patch.tsx index abd749df..e2dcaf50 100644 --- a/packages/ui/src/components/tool-call/renderers/apply-patch.tsx +++ b/packages/ui/src/components/tool-call/renderers/apply-patch.tsx @@ -1,107 +1,14 @@ 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 -} +import { buildDiagnosticEntries, type DiagnosticEntry, type DiagnosticsMap } from "../diagnostics" 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"], t: (key: string, params?: Record) => string) { - if (tone === "error") return { label: t("toolCall.diagnostics.severity.error.short"), icon: "!", rank: 0 } - if (tone === "warning") return { label: t("toolCall.diagnostics.severity.warning.short"), icon: "!", rank: 1 } - return { label: t("toolCall.diagnostics.severity.info.short"), 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, - t: (key: string, params?: Record) => string, -): 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, t) - 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) + patch?: string } function DiagnosticsInline(props: { entries: DiagnosticEntry[]; label: string; t: (key: string, params?: Record) => string }) { @@ -164,7 +71,7 @@ export const applyPatchRenderer: ToolRenderer = { }) const diagnosticsMap = createMemo(() => { const value = (payload.metadata as any).diagnostics - return value && typeof value === "object" ? (value as Record) : {} + return value && typeof value === "object" ? (value as DiagnosticsMap) : {} }) if (files().length === 0) { @@ -178,9 +85,9 @@ export const applyPatchRenderer: ToolRenderer = { {(file, index) => { const labelBase = file.relativePath || file.filePath || t("toolCall.applyPatch.fileFallback", { number: index() + 1 }) - const diffText = typeof file.diff === "string" ? file.diff : "" + const diffText = typeof file.diff === "string" ? file.diff : typeof file.patch === "string" ? file.patch : "" const filePath = typeof file.filePath === "string" ? file.filePath : file.relativePath - const entries = createMemo(() => buildDiagnostics(diagnosticsMap(), file, t)) + const entries = createMemo(() => buildDiagnosticEntries(diagnosticsMap(), [file.filePath, file.relativePath])) return (