@@ -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 ( / _ I n d T \. ( .* ) $ / ) ;
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>) */
215319function 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*/
240346function 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 */
263369function 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