@@ -341,6 +341,102 @@ export function diffLines(a: string, b: string): DiffLine[] | null {
341341 return out ;
342342}
343343
344+ export type CollapsedDiffLine = DiffLine | { type : "skip" ; text : string } ;
345+
346+ // Keep `context` unchanged lines around each change and collapse the rest, so
347+ // the removed (red) / added (green) lines are visible the moment the row opens
348+ // instead of buried under hundreds of identical context lines.
349+ const DIFF_CONTEXT = 3 ;
350+
351+ export function collapseDiff ( diff : DiffLine [ ] , context = DIFF_CONTEXT ) : CollapsedDiffLine [ ] {
352+ const keep = new Array ( diff . length ) . fill ( false ) ;
353+ for ( let i = 0 ; i < diff . length ; i ++ ) {
354+ if ( diff [ i ] . type === "same" ) continue ;
355+ for ( let j = Math . max ( 0 , i - context ) ; j <= Math . min ( diff . length - 1 , i + context ) ; j ++ ) {
356+ keep [ j ] = true ;
357+ }
358+ }
359+ const out : CollapsedDiffLine [ ] = [ ] ;
360+ let i = 0 ;
361+ while ( i < diff . length ) {
362+ if ( diff [ i ] . type !== "same" || keep [ i ] ) {
363+ out . push ( diff [ i ] ) ;
364+ i ++ ;
365+ continue ;
366+ }
367+ let j = i ;
368+ while ( j < diff . length && diff [ j ] . type === "same" && ! keep [ j ] ) j ++ ;
369+ const n = j - i ;
370+ out . push ( { type : "skip" , text : `... ${ n } unchanged line${ n === 1 ? "" : "s" } ` } ) ;
371+ i = j ;
372+ }
373+ return out ;
374+ }
375+
376+ // Unified line diff of original vs compressed request bodies, so pruned content
377+ // (red) and inserted truncation markers (green) pop instead of two near-identical
378+ // dumps. Returns dt/dd fragment for the detail grid. Shared by the transformation
379+ // and record rows.
380+ function CompressionDiff ( {
381+ requestMessages,
382+ compressedMessages,
383+ inputTokensOriginal,
384+ inputTokensOptimized
385+ } : {
386+ requestMessages : TransformationRequestMessage [ ] ;
387+ compressedMessages : TransformationRequestMessage [ ] ;
388+ inputTokensOriginal ?: number | null ;
389+ inputTokensOptimized ?: number | null ;
390+ } ) {
391+ const original = formatRequestMessages ( requestMessages ) ;
392+ const compressed = formatRequestMessages ( compressedMessages ) ;
393+ const diff = diffLines ( original , compressed ) ;
394+ if ( ! diff ) {
395+ // Too large to diff — fall back to side-by-side dumps.
396+ return (
397+ < >
398+ < dt > Request (original)</ dt >
399+ < dd >
400+ < pre className = "activity-feed__message-dump" > { original } </ pre >
401+ </ dd >
402+ < dt > Request (compressed)</ dt >
403+ < dd >
404+ < pre className = "activity-feed__message-dump" > { compressed } </ pre >
405+ </ dd >
406+ </ >
407+ ) ;
408+ }
409+ return (
410+ < >
411+ < dt >
412+ Compression diff
413+ { inputTokensOriginal != null && inputTokensOptimized != null
414+ ? ` (${ inputTokensOriginal . toLocaleString ( ) } → ${ inputTokensOptimized . toLocaleString ( ) } tokens)`
415+ : "" }
416+ </ dt >
417+ < dd >
418+ < pre className = "activity-feed__message-dump activity-feed__diff" >
419+ { collapseDiff ( diff ) . map ( ( line , idx ) => (
420+ < div
421+ key = { idx }
422+ className = { `activity-feed__diff-line activity-feed__diff-line--${ line . type } ` }
423+ >
424+ { line . type === "del"
425+ ? "- "
426+ : line . type === "add"
427+ ? "+ "
428+ : line . type === "skip"
429+ ? ""
430+ : " " }
431+ { line . text }
432+ </ div >
433+ ) ) }
434+ </ pre >
435+ </ dd >
436+ </ >
437+ ) ;
438+ }
439+
344440function TransformationRow ( { event } : { event : TransformationFeedEvent } ) {
345441 const saved = event . tokensSaved ?? 0 ;
346442 const pct = event . savingsPercent ?? 0 ;
@@ -409,55 +505,12 @@ function TransformationRow({ event }: { event: TransformationFeedEvent }) {
409505 </ >
410506 ) : null }
411507 { hasRequestMessages && hasCompressedMessages ? (
412- // New proxy shape: both sides present. Render a unified line diff so
413- // pruned content (red) and inserted truncation markers (green) pop —
414- // the two near-identical dumps were hard to tell apart at a glance.
415- ( ( ) => {
416- const original = formatRequestMessages ( event . requestMessages ! ) ;
417- const compressed = formatRequestMessages ( event . compressedMessages ! ) ;
418- const diff = diffLines ( original , compressed ) ;
419- const label = (
420- < dt >
421- Compression diff
422- { event . inputTokensOriginal != null && event . inputTokensOptimized != null
423- ? ` (${ event . inputTokensOriginal . toLocaleString ( ) } → ${ event . inputTokensOptimized . toLocaleString ( ) } tokens)`
424- : "" }
425- </ dt >
426- ) ;
427- if ( ! diff ) {
428- // Too large to diff — fall back to side-by-side dumps.
429- return (
430- < >
431- < dt > Request (original)</ dt >
432- < dd >
433- < pre className = "activity-feed__message-dump" > { original } </ pre >
434- </ dd >
435- < dt > Request (compressed)</ dt >
436- < dd >
437- < pre className = "activity-feed__message-dump" > { compressed } </ pre >
438- </ dd >
439- </ >
440- ) ;
441- }
442- return (
443- < >
444- { label }
445- < dd >
446- < pre className = "activity-feed__message-dump activity-feed__diff" >
447- { diff . map ( ( line , idx ) => (
448- < div
449- key = { idx }
450- className = { `activity-feed__diff-line activity-feed__diff-line--${ line . type } ` }
451- >
452- { line . type === "del" ? "- " : line . type === "add" ? "+ " : " " }
453- { line . text }
454- </ div >
455- ) ) }
456- </ pre >
457- </ dd >
458- </ >
459- ) ;
460- } ) ( )
508+ < CompressionDiff
509+ requestMessages = { event . requestMessages ! }
510+ compressedMessages = { event . compressedMessages ! }
511+ inputTokensOriginal = { event . inputTokensOriginal }
512+ inputTokensOptimized = { event . inputTokensOptimized }
513+ />
461514 ) : hasRequestMessages ? (
462515 // Legacy proxy shape: only `requestMessages` exists. Its content may
463516 // actually be the post-compression list (field was inconsistent
@@ -785,20 +838,12 @@ function RecordRow({ event }: { event: RecordEvent }) {
785838 </ >
786839 ) : null }
787840 { hasRequestMessages && hasCompressedMessages ? (
788- < >
789- < dt > Request (original)</ dt >
790- < dd >
791- < pre className = "activity-feed__message-dump" >
792- { formatRequestMessages ( event . requestMessages ! ) }
793- </ pre >
794- </ dd >
795- < dt > Request (compressed)</ dt >
796- < dd >
797- < pre className = "activity-feed__message-dump" >
798- { formatRequestMessages ( event . compressedMessages ! ) }
799- </ pre >
800- </ dd >
801- </ >
841+ < CompressionDiff
842+ requestMessages = { event . requestMessages ! }
843+ compressedMessages = { event . compressedMessages ! }
844+ inputTokensOriginal = { event . inputTokensOriginal }
845+ inputTokensOptimized = { event . inputTokensOptimized }
846+ />
802847 ) : hasRequestMessages ? (
803848 < >
804849 < dt > Request</ dt >
0 commit comments