Improve diagnostics accordion
This commit is contained in:
@@ -141,6 +141,34 @@ function getRelativePath(path: string): string {
|
|||||||
|
|
||||||
const diffCapableTools = new Set(["edit", "patch"])
|
const diffCapableTools = new Set(["edit", "patch"])
|
||||||
|
|
||||||
|
interface LspRangePosition {
|
||||||
|
line?: number
|
||||||
|
character?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LspRange {
|
||||||
|
start?: LspRangePosition
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LspDiagnostic {
|
||||||
|
message?: string
|
||||||
|
severity?: number
|
||||||
|
range?: LspRange
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiagnosticEntry {
|
||||||
|
id: string
|
||||||
|
severity: number
|
||||||
|
tone: "error" | "warning" | "info"
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
message: string
|
||||||
|
filePath: string
|
||||||
|
displayPath: string
|
||||||
|
line: number
|
||||||
|
column: number
|
||||||
|
}
|
||||||
|
|
||||||
interface DiffPayload {
|
interface DiffPayload {
|
||||||
diffText: string
|
diffText: string
|
||||||
filePath?: string
|
filePath?: string
|
||||||
@@ -180,6 +208,140 @@ function extractDiffPayload(toolName: string, state: ToolState): DiffPayload | n
|
|||||||
return { diffText, filePath }
|
return { diffText, filePath }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDiagnosticPath(path: string) {
|
||||||
|
return path.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 extractDiagnostics(toolName: string, state: ToolState | undefined): DiagnosticEntry[] {
|
||||||
|
if (!state) return []
|
||||||
|
const supportsMetadata = isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state)
|
||||||
|
if (!supportsMetadata) return []
|
||||||
|
|
||||||
|
const metadata = (state.metadata || {}) as Record<string, unknown>
|
||||||
|
const input = (state.input || {}) as Record<string, unknown>
|
||||||
|
const diagnosticsMap = metadata?.diagnostics as Record<string, LspDiagnostic[] | undefined> | 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
|
||||||
|
|
||||||
|
const normalizedPreferred = preferredPath ? normalizeDiagnosticPath(preferredPath) : undefined
|
||||||
|
const candidateEntries = Object.entries(diagnosticsMap).filter(([, items]) => Array.isArray(items) && items.length > 0)
|
||||||
|
if (candidateEntries.length === 0) return []
|
||||||
|
|
||||||
|
const prioritizedEntries = (() => {
|
||||||
|
if (!normalizedPreferred) return candidateEntries
|
||||||
|
const matched = candidateEntries.filter(([path]) => {
|
||||||
|
const normalized = normalizeDiagnosticPath(path)
|
||||||
|
if (normalized === normalizedPreferred) return true
|
||||||
|
if (normalized.endsWith(`/${normalizedPreferred}`)) return true
|
||||||
|
const normalizedBase = normalized.split("/").pop()
|
||||||
|
const preferredBase = normalizedPreferred.split("/").pop()
|
||||||
|
return normalizedBase && preferredBase ? normalizedBase === preferredBase : false
|
||||||
|
})
|
||||||
|
return matched.length > 0 ? matched : candidateEntries
|
||||||
|
})()
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.sort((a, b) => a.severity - b.severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnosticFileName(entries: DiagnosticEntry[]) {
|
||||||
|
const first = entries[0]
|
||||||
|
return first ? first.displayPath : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiagnosticsSection(
|
||||||
|
entries: DiagnosticEntry[],
|
||||||
|
expanded: boolean,
|
||||||
|
toggle: () => void,
|
||||||
|
toolIcon: string,
|
||||||
|
fileLabel: string,
|
||||||
|
) {
|
||||||
|
if (entries.length === 0) return null
|
||||||
|
return (
|
||||||
|
<div class="tool-call-diagnostics-wrapper">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-diagnostics-heading"
|
||||||
|
aria-expanded={expanded}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span class="tool-call-icon" aria-hidden="true">
|
||||||
|
{expanded ? "▼" : "▶"}
|
||||||
|
</span>
|
||||||
|
<span class="tool-call-emoji" aria-hidden="true">🛠</span>
|
||||||
|
<span class="tool-call-summary">Diagnostics</span>
|
||||||
|
<span class="tool-call-diagnostics-file" title={fileLabel}>{fileLabel}</span>
|
||||||
|
</button>
|
||||||
|
<Show when={expanded}>
|
||||||
|
<div class="tool-call-diagnostics" role="region" aria-label="Diagnostics">
|
||||||
|
<div class="tool-call-diagnostics-body" role="list">
|
||||||
|
<For each={entries}>
|
||||||
|
{(entry) => (
|
||||||
|
<div class="tool-call-diagnostic-row" role="listitem">
|
||||||
|
<span class={`tool-call-diagnostic-chip tool-call-diagnostic-${entry.tone}`}>
|
||||||
|
<span class="tool-call-diagnostic-chip-icon">{entry.icon}</span>
|
||||||
|
<span>{entry.label}</span>
|
||||||
|
</span>
|
||||||
|
<span class="tool-call-diagnostic-path" title={entry.filePath}>
|
||||||
|
{entry.displayPath}
|
||||||
|
<span class="tool-call-diagnostic-coords">
|
||||||
|
:L{entry.line || "-"}:C{entry.column || "-"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="tool-call-diagnostic-message">{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function ToolCall(props: ToolCallProps) {
|
export default function ToolCall(props: ToolCallProps) {
|
||||||
const { preferences, setDiffViewMode } = useConfig()
|
const { preferences, setDiffViewMode } = useConfig()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
@@ -195,6 +357,13 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
})
|
})
|
||||||
const [permissionSubmitting, setPermissionSubmitting] = createSignal(false)
|
const [permissionSubmitting, setPermissionSubmitting] = createSignal(false)
|
||||||
const [permissionError, setPermissionError] = createSignal<string | null>(null)
|
const [permissionError, setPermissionError] = createSignal<string | null>(null)
|
||||||
|
const [diagnosticsExpanded, setDiagnosticsExpanded] = createSignal(true)
|
||||||
|
const diagnosticsEntries = createMemo(() => {
|
||||||
|
const tool = props.toolCall?.tool || ""
|
||||||
|
const state = props.toolCall?.state
|
||||||
|
if (!state) return []
|
||||||
|
return extractDiagnostics(tool, state)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
let scrollContainerRef: HTMLDivElement | undefined
|
let scrollContainerRef: HTMLDivElement | undefined
|
||||||
@@ -998,6 +1167,7 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
<Show when={expanded()}>
|
<Show when={expanded()}>
|
||||||
<div class="tool-call-details">
|
<div class="tool-call-details">
|
||||||
{renderToolBody()}
|
{renderToolBody()}
|
||||||
|
|
||||||
{renderError()}
|
{renderError()}
|
||||||
|
|
||||||
{renderPermissionBlock()}
|
{renderPermissionBlock()}
|
||||||
@@ -1010,6 +1180,16 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={diagnosticsEntries().length}>
|
||||||
|
{renderDiagnosticsSection(
|
||||||
|
diagnosticsEntries(),
|
||||||
|
diagnosticsExpanded(),
|
||||||
|
() => setDiagnosticsExpanded((prev) => !prev),
|
||||||
|
getToolIcon(toolName()),
|
||||||
|
diagnosticFileName(diagnosticsEntries()),
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,6 +401,165 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
border-top: 1px solid var(--border-base);
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-wrapper {
|
||||||
|
border-top: 1px solid var(--border-base);
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
margin-top: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-heading {
|
||||||
|
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--message-tool-border);
|
||||||
|
background-color: var(--surface-code);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-heading:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-file {
|
||||||
|
@apply inline-flex items-center;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tool-call-diagnostics-heading {
|
||||||
|
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--message-tool-border);
|
||||||
|
background-color: var(--surface-code);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-heading:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-file {
|
||||||
|
@apply inline-flex items-center;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-caret {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: var(--space-sm) var(--space-md) var(--space-sm) var(--space-md);
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostics-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
max-height: calc(4 * var(--tool-call-line-unit, 1.4em));
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding-right: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0 var(--space-sm);
|
||||||
|
min-height: 20px;
|
||||||
|
border-radius: var(--pill-radius);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-error {
|
||||||
|
background-color: var(--status-error-bg);
|
||||||
|
color: var(--status-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-warning {
|
||||||
|
background-color: var(--status-starting-bg);
|
||||||
|
color: var(--status-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-info {
|
||||||
|
background-color: var(--badge-neutral-bg);
|
||||||
|
color: var(--badge-neutral-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-path {
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-coords {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diagnostic-message {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-section pre {
|
.tool-call-section pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user