fix(ui): unify apply_patch diagnostics matching

This commit is contained in:
Shantur Rathore
2026-04-20 21:08:33 +01:00
parent 662a6b94b0
commit 68551f6731
2 changed files with 66 additions and 135 deletions

View File

@@ -17,6 +17,8 @@ interface LspDiagnostic {
range?: LspRange range?: LspRange
} }
export type DiagnosticsMap = Record<string, LspDiagnostic[] | undefined>
export interface DiagnosticEntry { export interface DiagnosticEntry {
id: string id: string
severity: number severity: number
@@ -30,7 +32,7 @@ export interface DiagnosticEntry {
column: number column: number
} }
function normalizeDiagnosticPath(path: string) { export function normalizeDiagnosticPath(path: string) {
return path.replace(/\\/g, "/") return path.replace(/\\/g, "/")
} }
@@ -53,49 +55,71 @@ export function extractDiagnostics(state: ToolState | undefined): DiagnosticEntr
const metadata = (state.metadata || {}) as Record<string, unknown> const metadata = (state.metadata || {}) as Record<string, unknown>
const input = (state.input || {}) as Record<string, unknown> const input = (state.input || {}) as Record<string, unknown>
const diagnosticsMap = metadata?.diagnostics as Record<string, LspDiagnostic[] | undefined> | undefined const diagnosticsMap = metadata?.diagnostics as DiagnosticsMap | undefined
if (!diagnosticsMap) return [] if (!diagnosticsMap) return []
const preferredPath = [input.filePath, metadata.filePath, metadata.filepath, input.path].find( return buildDiagnosticEntries(diagnosticsMap, [input.filePath, metadata.filePath, metadata.filepath, input.path])
(value) => typeof value === "string" && value.length > 0, }
) as string | undefined
const normalizedPreferred = preferredPath ? normalizeDiagnosticPath(preferredPath) : undefined export function resolveDiagnosticsKey(diagnostics: DiagnosticsMap, preferredPaths: Array<string | undefined>): string | undefined {
if (!normalizedPreferred) return [] if (Object.keys(diagnostics).length === 0) return undefined
const candidateEntries = Object.entries(diagnosticsMap).filter(([, items]) => Array.isArray(items) && items.length > 0)
if (candidateEntries.length === 0) return []
const prioritizedEntries = candidateEntries.filter(([path]) => { const normalizedPreferred = preferredPaths
const normalized = normalizeDiagnosticPath(path) .filter((value): value is string => typeof value === "string" && value.length > 0)
return normalized === normalizedPreferred .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<string | undefined>): DiagnosticEntry[] {
const key = resolveDiagnosticsKey(diagnostics, preferredPaths)
if (!key) return []
const list = diagnostics[key]
if (!Array.isArray(list) || list.length === 0) return []
const entries: DiagnosticEntry[] = [] const entries: DiagnosticEntry[] = []
for (const [pathKey, list] of prioritizedEntries) { const normalizedPath = normalizeDiagnosticPath(key)
if (!Array.isArray(list)) continue for (let index = 0; index < list.length; index++) {
const normalizedPath = normalizeDiagnosticPath(pathKey) const diagnostic = list[index]
for (let index = 0; index < list.length; index++) { if (!diagnostic || typeof diagnostic.message !== "string") continue
const diagnostic = list[index] const tone = determineSeverityTone(typeof diagnostic.severity === "number" ? diagnostic.severity : undefined)
if (!diagnostic || typeof diagnostic.message !== "string") continue const severityMeta = getSeverityMeta(tone)
const tone = determineSeverityTone(typeof diagnostic.severity === "number" ? diagnostic.severity : undefined) const line = typeof diagnostic.range?.start?.line === "number" ? diagnostic.range.start.line + 1 : 0
const severityMeta = getSeverityMeta(tone) const column = typeof diagnostic.range?.start?.character === "number" ? diagnostic.range.start.character + 1 : 0
const line = typeof diagnostic.range?.start?.line === "number" ? diagnostic.range.start.line + 1 : 0 entries.push({
const column = typeof diagnostic.range?.start?.character === "number" ? diagnostic.range.start.character + 1 : 0 id: `${normalizedPath}-${index}-${diagnostic.message}`,
entries.push({ severity: severityMeta.rank,
id: `${normalizedPath}-${index}-${diagnostic.message}`, tone,
severity: severityMeta.rank, label: severityMeta.label,
tone, icon: severityMeta.icon,
label: severityMeta.label, message: diagnostic.message,
icon: severityMeta.icon, filePath: normalizedPath,
message: diagnostic.message, displayPath: getRelativePath(normalizedPath),
filePath: normalizedPath, line,
displayPath: getRelativePath(normalizedPath), column,
line, })
column,
})
}
} }
return entries.sort((a, b) => a.severity - b.severity) return entries.sort((a, b) => a.severity - b.severity)

View File

@@ -1,107 +1,14 @@
import { For, Show, createMemo } from "solid-js" import { For, Show, createMemo } from "solid-js"
import type { ToolRenderer } from "../types" import type { ToolRenderer } from "../types"
import { getRelativePath, getToolName, isToolStateCompleted, readToolStatePayload } from "../utils" import { getRelativePath, getToolName, isToolStateCompleted, readToolStatePayload } from "../utils"
import type { DiagnosticEntry } from "../diagnostics" import { buildDiagnosticEntries, type DiagnosticEntry, type DiagnosticsMap } from "../diagnostics"
type LspRangePosition = {
line?: number
character?: number
}
type LspRange = {
start?: LspRangePosition
}
type LspDiagnostic = {
message?: string
severity?: number
range?: LspRange
}
type ApplyPatchFile = { type ApplyPatchFile = {
filePath?: string filePath?: string
relativePath?: string relativePath?: string
type?: string type?: string
diff?: string diff?: string
} patch?: 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, unknown>) => 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<string, LspDiagnostic[] | undefined>,
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<string, LspDiagnostic[] | undefined>,
file: ApplyPatchFile,
t: (key: string, params?: Record<string, unknown>) => 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)
} }
function DiagnosticsInline(props: { entries: DiagnosticEntry[]; label: string; t: (key: string, params?: Record<string, unknown>) => string }) { function DiagnosticsInline(props: { entries: DiagnosticEntry[]; label: string; t: (key: string, params?: Record<string, unknown>) => string }) {
@@ -164,7 +71,7 @@ export const applyPatchRenderer: ToolRenderer = {
}) })
const diagnosticsMap = createMemo(() => { const diagnosticsMap = createMemo(() => {
const value = (payload.metadata as any).diagnostics const value = (payload.metadata as any).diagnostics
return value && typeof value === "object" ? (value as Record<string, LspDiagnostic[] | undefined>) : {} return value && typeof value === "object" ? (value as DiagnosticsMap) : {}
}) })
if (files().length === 0) { if (files().length === 0) {
@@ -178,9 +85,9 @@ export const applyPatchRenderer: ToolRenderer = {
<For each={files()}> <For each={files()}>
{(file, index) => { {(file, index) => {
const labelBase = file.relativePath || file.filePath || t("toolCall.applyPatch.fileFallback", { number: index() + 1 }) 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 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 ( return (
<div class="tool-call-apply-patch-file"> <div class="tool-call-apply-patch-file">