Skip to content

Commit fd09e7a

Browse files
committed
better display for structure.
1 parent 7de1f84 commit fd09e7a

File tree

1 file changed

+163
-9
lines changed

1 file changed

+163
-9
lines changed

widget/src/traceDisplay.tsx

Lines changed: 163 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ function dequalify(s: string): string {
7676
// Numbers (including decimals) are kept as is to avoid mis-splitting
7777
if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
7878

79+
// Check for _IndT pattern and keep everything after it
80+
const indTMatch = t.match(/_IndT\.(.*)$/);
81+
if (indTMatch) {
82+
// Return only the part after "_IndT."
83+
return indTMatch[1];
84+
}
85+
7986
// Remove namespace prefixes for identifiers containing dots, keeping only the last segment
8087
if (t.includes(".")) {
8188
const segs = t.split(".");
@@ -211,6 +218,103 @@ function joinNodes(nodes: React.ReactNode[], sep: React.ReactNode = ', '): React
211218
return <>{out}</>;
212219
}
213220

221+
/** Collapsible wrapper for object field values */
222+
const CollapsibleObjectField: React.FC<{ value: unknown }> = ({ value }) => {
223+
const [expanded, setExpanded] = React.useState(true);
224+
225+
// Check if value is collapsible (object, array, or long string)
226+
const isCollapsible = React.useMemo(() => {
227+
if (Array.isArray(value)) return true;
228+
if (typeof value === 'object' && value !== null) return true;
229+
if (typeof value === 'string' && value.length > 60) return true;
230+
return false;
231+
}, [value]);
232+
233+
const previewText = React.useMemo(() => {
234+
if (Array.isArray(value)) {
235+
return `Array(${value.length})`;
236+
}
237+
if (typeof value === 'object' && value !== null) {
238+
return `{...}`;
239+
}
240+
if (typeof value === 'string') {
241+
const processed = dequalify(value);
242+
return processed.length > 60 ? processed.slice(0, 60) + '...' : processed;
243+
}
244+
return String(value);
245+
}, [value]);
246+
247+
if (!isCollapsible) {
248+
return <>{renderObjectValue(value)}</>;
249+
}
250+
251+
return (
252+
<div className="collapsible-field">
253+
<button
254+
className="field-toggle"
255+
type="button"
256+
onClick={() => setExpanded(e => !e)}
257+
aria-label={expanded ? "Collapse" : "Expand"}
258+
>
259+
{expanded ? "▼" : "▶"}
260+
</button>
261+
{expanded ? renderObjectValue(value) : <code className="field-preview">{previewText}</code>}
262+
</div>
263+
);
264+
};
265+
266+
/** Collapsible wrapper for rendering entire objects */
267+
const CollapsibleObject: React.FC<{ obj: Record<string, unknown> }> = ({ obj }) => {
268+
const [expanded, setExpanded] = React.useState(true);
269+
const entries = Object.entries(obj);
270+
271+
return (
272+
<div className="collapsible-object">
273+
<button
274+
className="object-toggle"
275+
type="button"
276+
onClick={() => setExpanded(e => !e)}
277+
aria-label={expanded ? "Collapse object" : "Expand object"}
278+
>
279+
{expanded ? "▼" : "▶"}
280+
</button>
281+
<code>{'{'}</code>
282+
{expanded ? (
283+
<ul className="list nested-object">
284+
{entries.map(([key, val]) => (
285+
<li key={key}>
286+
<code>{key}</code>: <CollapsibleObjectField value={val} />
287+
</li>
288+
))}
289+
</ul>
290+
) : (
291+
<code className="object-preview">...{entries.length} fields</code>
292+
)}
293+
<code>{'}'}</code>
294+
</div>
295+
);
296+
};
297+
298+
/** Helper function to render values within objects */
299+
function renderObjectValue(v: unknown): React.ReactNode {
300+
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
301+
// Nested object - render with collapsible wrapper
302+
const obj = v as Record<string, unknown>;
303+
return <CollapsibleObject obj={obj} />;
304+
}
305+
306+
if (Array.isArray(v)) {
307+
// Array within object - will be handled by renderValue which adds brackets
308+
return renderValue(v);
309+
}
310+
311+
if (typeof v === 'string') {
312+
return <code>{dequalify(v)}</code>;
313+
}
314+
315+
return <code>{String(v)}</code>;
316+
}
317+
214318
/** Render any value "inline" (arrays also inline as [a, b, ...], not converted to <ul>) */
215319
function renderValueInline(x: unknown, changedIndices?: Set<number>, index?: number): React.ReactNode {
216320
const isChanged = changedIndices !== undefined && index !== undefined && changedIndices.has(index);
@@ -220,8 +324,10 @@ function renderValueInline(x: unknown, changedIndices?: Set<number>, index?: num
220324
return <code>[{joinNodes(elements)}]</code>;
221325
}
222326
if (typeof x === 'object' && x !== null) {
223-
const content = JSON.stringify(x);
224-
return isChanged ? <code className="changed-element">{content}</code> : <code>{content}</code>;
327+
// Render object with collapsible wrapper
328+
const obj = x as Record<string, unknown>;
329+
const objContent = <CollapsibleObject obj={obj} />;
330+
return isChanged ? <span className="changed-element">{objContent}</span> : objContent;
225331
}
226332
if (typeof x === 'string') {
227333
const content = dequalify(x);
@@ -233,16 +339,16 @@ function renderValueInline(x: unknown, changedIndices?: Set<number>, index?: num
233339

234340

235341
/** Render a row from an inner array:
236-
* - If it's a flat tuple (all elements are primitives), render as `(a, b, c)` with parentheses
342+
* - If it's a flat tuple (all elements are primitives and not empty), render as `(a, b, c)` with parentheses
237343
* - Otherwise, render the inner array inline as `[a, b, ...]`
238344
* If not an array, fallback to regular rendering
239345
*/
240346
function renderRowFromInnerArray(e: unknown, changedIndices?: Set<number>, index?: number): React.ReactNode {
241347
const isChanged = changedIndices !== undefined && index !== undefined && changedIndices.has(index);
242348

243349
if (Array.isArray(e)) {
244-
// Check if this is a flat tuple (all elements are primitives, not arrays)
245-
const isFlatTuple = e.every(el => !Array.isArray(el));
350+
// Check if this is a flat tuple (non-empty, all elements are primitives, not arrays or objects)
351+
const isFlatTuple = e.length > 0 && e.every(el => !Array.isArray(el) && (typeof el !== 'object' || el === null));
246352
if (isFlatTuple) {
247353
const parts: React.ReactNode[] = [];
248354
e.forEach((el, i) => {
@@ -252,7 +358,7 @@ function renderRowFromInnerArray(e: unknown, changedIndices?: Set<number>, index
252358
const code = <code>({parts})</code>;
253359
return isChanged ? <span className="changed-element">{code}</span> : code;
254360
} else {
255-
const content = renderValueInline(e); // ← Nested arrays, inline as [ ... ]
361+
const content = renderValueInline(e); // ← Nested arrays or empty arrays, inline as [ ... ]
256362
return isChanged ? <span className="changed-element">{content}</span> : content;
257363
}
258364
}
@@ -262,8 +368,8 @@ function renderRowFromInnerArray(e: unknown, changedIndices?: Set<number>, index
262368
/** Render a single removed element inline */
263369
function renderRemovedElement(e: unknown, index: number): React.ReactNode {
264370
if (Array.isArray(e)) {
265-
// Check if this is a flat tuple (all elements are primitives, not arrays)
266-
const isFlatTuple = e.every(el => !Array.isArray(el));
371+
// Check if this is a flat tuple (non-empty, all elements are primitives, not arrays or objects)
372+
const isFlatTuple = e.length > 0 && e.every(el => !Array.isArray(el) && (typeof el !== 'object' || el === null));
267373
if (isFlatTuple) {
268374
const parts: React.ReactNode[] = [];
269375
e.forEach((el, i) => {
@@ -332,7 +438,9 @@ function renderValue(v: unknown, changedIndices?: Set<number>, removedElements?:
332438
}
333439

334440
if (typeof v === 'object' && v !== null) {
335-
return <code>{JSON.stringify(v)}</code>;
441+
const obj = v as Record<string, unknown>;
442+
// Render object with collapsible wrapper
443+
return <CollapsibleObject obj={obj} />;
336444
}
337445

338446
if (typeof v === 'string') {
@@ -1145,6 +1253,52 @@ const ModelCheckerView: React.FC<ModelCheckerViewProps> = ({
11451253
.json-number { color: var(--vscode-symbolIcon-numberForeground, #b5cea8); }
11461254
.json-boolean { color: var(--vscode-symbolIcon-booleanForeground, #569cd6); }
11471255
.json-null { color: var(--vscode-symbolIcon-nullForeground, #569cd6); }
1256+
.collapsible-field {
1257+
display: inline-flex;
1258+
align-items: flex-start;
1259+
gap: 4px;
1260+
}
1261+
.field-toggle {
1262+
border: none;
1263+
background: transparent;
1264+
font-size: 10px;
1265+
line-height: 1;
1266+
cursor: pointer;
1267+
color: var(--vscode-descriptionForeground);
1268+
padding: 0 2px;
1269+
margin-top: 2px;
1270+
}
1271+
.field-toggle:hover {
1272+
color: var(--vscode-foreground);
1273+
}
1274+
.field-preview {
1275+
color: var(--vscode-descriptionForeground);
1276+
font-style: italic;
1277+
}
1278+
.collapsible-object {
1279+
display: inline-flex;
1280+
align-items: flex-start;
1281+
gap: 4px;
1282+
flex-wrap: wrap;
1283+
}
1284+
.object-toggle {
1285+
border: none;
1286+
background: transparent;
1287+
font-size: 10px;
1288+
line-height: 1;
1289+
cursor: pointer;
1290+
color: var(--vscode-descriptionForeground);
1291+
padding: 0 2px;
1292+
margin-top: 2px;
1293+
}
1294+
.object-toggle:hover {
1295+
color: var(--vscode-foreground);
1296+
}
1297+
.object-preview {
1298+
color: var(--vscode-descriptionForeground);
1299+
font-style: italic;
1300+
margin-left: 4px;
1301+
}
11481302
`;
11491303

11501304
const prettyJson = JSON.stringify(result, null, 2);

0 commit comments

Comments
 (0)