fix(ui): unify apply_patch diagnostics matching
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user