@@ -398,6 +398,121 @@ describe("messageMapper", () => {
398398 } ) ;
399399 } ) ;
400400
401+ describe ( "pieceToAttachment size handling" , ( ) => {
402+ function makeMediaMessage (
403+ converted_value : string ,
404+ converted_value_data_type = "image_path" ,
405+ converted_value_mime_type : string | undefined = "image/png" ,
406+ ) : BackendMessage {
407+ return {
408+ turn_number : 1 ,
409+ role : "assistant" ,
410+ pieces : [
411+ {
412+ piece_id : "p1abcdef" ,
413+ original_value_data_type : "text" ,
414+ converted_value_data_type,
415+ original_value : "prompt" ,
416+ converted_value,
417+ converted_value_mime_type,
418+ scores : [ ] ,
419+ response_error : "none" ,
420+ } ,
421+ ] ,
422+ created_at : "2026-02-15T00:00:00Z" ,
423+ } ;
424+ }
425+
426+ it ( "omits size for /api/media path URLs (the originally reported bug)" , ( ) => {
427+ // Reproduces the screenshot: a backend-served PNG referenced via
428+ // /api/media?path=... would otherwise produce a chip like "(119 B)"
429+ // because value.length was used as size.
430+ const url = "/api/media?path=C%3A%5CUsers%5Cromanlutz%5Cgit%5CPyRIT%5Cdbdata%5Cprompt-memory-entries%5Cimages%5Cimage_150c901b9db7.png" ;
431+ expect ( url . length ) . toBeGreaterThan ( 50 ) ; // sanity: long enough for the old bug to bite
432+
433+ const result = backendMessageToFrontend ( makeMediaMessage ( url ) ) ;
434+
435+ expect ( result . attachments ) . toHaveLength ( 1 ) ;
436+ expect ( result . attachments ! [ 0 ] . url ) . toBe ( url ) ;
437+ expect ( result . attachments ! [ 0 ] . size ) . toBeUndefined ( ) ;
438+ } ) ;
439+
440+ it ( "omits size for Windows absolute paths" , ( ) => {
441+ const result = backendMessageToFrontend (
442+ makeMediaMessage ( "C:\\Users\\me\\dbdata\\prompt-memory-entries\\images\\img.png" )
443+ ) ;
444+ expect ( result . attachments ! [ 0 ] . size ) . toBeUndefined ( ) ;
445+ } ) ;
446+
447+ it ( "omits size for Unix absolute paths" , ( ) => {
448+ const result = backendMessageToFrontend (
449+ makeMediaMessage ( "/dbdata/prompt-memory-entries/images/img.png" )
450+ ) ;
451+ expect ( result . attachments ! [ 0 ] . size ) . toBeUndefined ( ) ;
452+ } ) ;
453+
454+ it ( "omits size for data: / blob: / file: URI schemes" , ( ) => {
455+ for ( const url of [
456+ "data:image/png;base64,aGVsbG8=" ,
457+ "blob:https://example.com/abc-123" ,
458+ "file:///tmp/img.png" ,
459+ ] ) {
460+ const result = backendMessageToFrontend ( makeMediaMessage ( url ) ) ;
461+ expect ( result . attachments ! [ 0 ] . size ) . toBeUndefined ( ) ;
462+ }
463+ } ) ;
464+
465+ it ( "sets size to the decoded byte count for base64-inlined media" , ( ) => {
466+ // btoa("hello world") = "aGVsbG8gd29ybGQ=" — 11 decoded bytes.
467+ const base64 = "aGVsbG8gd29ybGQ=" ;
468+ const result = backendMessageToFrontend ( makeMediaMessage ( base64 ) ) ;
469+
470+ expect ( result . attachments ! [ 0 ] . url ) . toBe ( `data:image/png;base64,${ base64 } ` ) ;
471+ expect ( result . attachments ! [ 0 ] . size ) . toBe ( 11 ) ;
472+ } ) ;
473+
474+ it ( "computes base64 size correctly without padding" , ( ) => {
475+ // 16-char base64 with no padding decodes to floor(16 * 3 / 4) = 12 bytes.
476+ const base64 = "YWJjZGVmZ2hpamtsbW5vcA" ; // "abcdefghijklmnop" without '=' padding
477+ const result = backendMessageToFrontend ( makeMediaMessage ( base64 ) ) ;
478+ expect ( result . attachments ! [ 0 ] . size ) . toBe ( 16 ) ;
479+ } ) ;
480+
481+ it ( "omits size on the original-side attachment for path values" , ( ) => {
482+ // Same /api/media URL on original_value — used when originalAttachments
483+ // is populated via pieceToAttachment(piece, 'original').
484+ const url = "/api/media?path=output%2Fimage.png" ;
485+ const msg : BackendMessage = {
486+ turn_number : 1 ,
487+ role : "assistant" ,
488+ pieces : [
489+ {
490+ piece_id : "p1abcdef" ,
491+ original_value_data_type : "image_path" ,
492+ converted_value_data_type : "image_path" ,
493+ original_value : url ,
494+ converted_value : "aW1hZ2VkYXRhYWFhYWFh" , // 20 chars base64
495+ converted_value_mime_type : "image/png" ,
496+ original_value_mime_type : "image/png" ,
497+ scores : [ ] ,
498+ response_error : "none" ,
499+ } ,
500+ ] ,
501+ created_at : "2026-02-15T00:00:00Z" ,
502+ } ;
503+
504+ const result = backendMessageToFrontend ( msg ) ;
505+
506+ // Converted attachment should be present with computed size.
507+ expect ( result . attachments ) . toHaveLength ( 1 ) ;
508+ expect ( result . attachments ! [ 0 ] . size ) . toBe ( 15 ) ; // floor(20 * 3 / 4)
509+
510+ // originalAttachments only populated when URLs differ — they do here.
511+ expect ( result . originalAttachments ) . toHaveLength ( 1 ) ;
512+ expect ( result . originalAttachments ! [ 0 ] . size ) . toBeUndefined ( ) ;
513+ } ) ;
514+ } ) ;
515+
401516 describe ( "backendMessagesToFrontend" , ( ) => {
402517 it ( "should convert multiple messages" , ( ) => {
403518 const messages : BackendMessage [ ] = [
0 commit comments