feat(inspect-ui): render extractor-change breadcrumbs at violations

Show prev -> curr for each extractor whose value changed on the
selected step, anchored under the violation row in ActionList.
Long values collapse into <details> so the inline diff stays
readable while the full payload is one click away.
This commit is contained in:
pj committed 2026-05-30 21:58:13 +05:30
1 parent e44ad56749
commit d0cf5a98c7
3 files changed
+125

No files matched your search

+51
View File
@@ -130,3 +130,54 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.action-list-extractor-changes {
grid-column: 2;
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid var(--surface-border);
}
.action-list-extractor-changes-heading {
color: var(--text-muted);
text-transform: lowercase;
letter-spacing: 0.5px;
}
.action-list-extractor-change-row {
display: grid;
grid-template-columns: minmax(80px, max-content) 1fr;
gap: 6px;
color: var(--text-primary);
}
.action-list-extractor-change-name {
color: var(--text-muted);
}
.action-list-extractor-change-diff {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-list-extractor-change-row-collapsed > summary {
display: grid;
grid-template-columns: minmax(80px, max-content) 1fr;
gap: 6px;
cursor: pointer;
}
.action-list-extractor-change-full {
grid-column: 1 / -1;
margin: 4px 0 0;
white-space: pre-wrap;
word-break: break-word;
background: var(--surface-base);
padding: 4px 6px;
border-radius: 2px;
font-size: 10px;
}
+68
View File
@@ -121,6 +121,24 @@ function contentTextForStep(step: Step): string {
return ""; return "";
} }
const MAX_INLINE_VALUE = 80;
function formatExtractorValue(value: unknown): string {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "string") return JSON.stringify(value);
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function truncateForInline(text: string): string {
if (text.length <= MAX_INLINE_VALUE) return text;
return `${text.slice(0, MAX_INLINE_VALUE - 1)}...`;
}
function positionTextForStep(step: Step): string | null { function positionTextForStep(step: Step): string | null {
const action = step.action; const action = step.action;
if (!action) { if (!action) {
@@ -289,6 +307,56 @@ export default function ActionList({
{contentText ? contentText : `""`} {contentText ? contentText : `""`}
</span> </span>
</div> </div>
{step.has_violations && selectedStep?.extractor_changes
? (() => {
const entries = Object.entries(selectedStep.extractor_changes);
if (entries.length === 0) return null;
entries.sort(([a], [b]) => a.localeCompare(b));
return (
<div className="action-list-extractor-changes">
<div className="action-list-extractor-changes-heading">
extractor changes
</div>
{entries.map(([name, change]) => {
const prev = formatExtractorValue(change.prev);
const curr = formatExtractorValue(change.curr);
const inlineFits =
prev.length + curr.length <= MAX_INLINE_VALUE * 2;
if (inlineFits) {
return (
<div key={name} className="action-list-extractor-change-row">
<span className="action-list-extractor-change-name">
{name}
</span>
<span className="action-list-extractor-change-diff">
{truncateForInline(prev)} → {truncateForInline(curr)}
</span>
</div>
);
}
return (
<details
key={name}
className="action-list-extractor-change-row action-list-extractor-change-row-collapsed"
>
<summary>
<span className="action-list-extractor-change-name">
{name}
</span>
<span className="action-list-extractor-change-diff">
{truncateForInline(prev)} → {truncateForInline(curr)}
</span>
</summary>
<pre className="action-list-extractor-change-full">
{`prev: ${prev}\ncurr: ${curr}`}
</pre>
</details>
);
})}
</div>
);
})()
: null}
</div> </div>
) : null} ) : null}
</li> </li>
+6
View File
@@ -115,6 +115,11 @@ export interface Metrics {
total_memory_bytes?: number; total_memory_bytes?: number;
} }
export interface ExtractorChange {
prev: unknown;
curr: unknown;
}
export interface Step { export interface Step {
step: number; step: number;
timestamp: string; timestamp: string;
@@ -126,4 +131,5 @@ export interface Step {
hierarchy?: Hierarchy; hierarchy?: Hierarchy;
residuals?: Record<string, ResidualNode>; residuals?: Record<string, ResidualNode>;
metrics?: Metrics; metrics?: Metrics;
extractor_changes?: Record<string, ExtractorChange>;
} }