@productmaker/mcp 2.8.3 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -91,120 +91,102 @@
91
91
  .app {
92
92
  width: min(100%, 760px);
93
93
  margin: 0 auto;
94
- padding: 18px;
94
+ padding: 12px;
95
95
  }
96
96
 
97
97
  .card {
98
98
  overflow: hidden;
99
99
  border: 1px solid var(--pm-line);
100
- border-radius: 24px;
100
+ border-radius: 26px;
101
101
  background: var(--pm-card);
102
102
  box-shadow: var(--pm-shadow);
103
103
  }
104
104
 
105
- .hero,
106
- .section {
107
- padding: 22px;
108
- }
109
-
110
105
  .hero {
111
- border-bottom: 1px solid var(--pm-line);
106
+ padding: 19px 22px 14px;
107
+ border-bottom: 1px solid
108
+ color-mix(in srgb, var(--pm-line) 65%, transparent);
112
109
  background:
113
110
  radial-gradient(
114
111
  circle at 100% 0%,
115
112
  var(--pm-primary-soft),
116
- transparent 42%
113
+ transparent 48%
117
114
  ),
118
115
  var(--pm-card);
119
116
  }
120
117
 
121
- .eyebrow {
122
- display: inline-flex;
118
+ .upload-header {
119
+ display: flex;
123
120
  align-items: center;
124
- gap: 7px;
125
- margin: 0 0 10px;
126
- color: var(--pm-primary-strong);
127
- font-size: 12px;
128
- font-weight: 800;
129
- letter-spacing: 0.08em;
130
- text-transform: uppercase;
131
- }
132
-
133
- .eyebrow-dot {
134
- width: 8px;
135
- height: 8px;
136
- border-radius: 999px;
137
- background: var(--pm-primary);
138
- box-shadow: 0 0 0 5px var(--pm-primary-soft);
139
- }
140
-
141
- h1,
142
- h2,
143
- p {
144
- margin-top: 0;
145
- }
146
-
147
- h1 {
148
- margin-bottom: 8px;
149
- font-size: clamp(23px, 5vw, 34px);
150
- line-height: 1.12;
151
- letter-spacing: -0.03em;
152
- }
153
-
154
- h2 {
155
- margin-bottom: 6px;
156
- font-size: 18px;
121
+ justify-content: space-between;
122
+ gap: 12px;
157
123
  }
158
124
 
159
- .muted {
160
- color: var(--pm-muted);
125
+ .upload-title {
126
+ min-width: 0;
127
+ margin: 0;
128
+ overflow: hidden;
129
+ font-size: clamp(17px, 3vw, 24px);
130
+ font-weight: 760;
131
+ letter-spacing: -0.025em;
132
+ text-overflow: ellipsis;
133
+ white-space: nowrap;
161
134
  }
162
135
 
163
- .hero-copy,
164
- .section-copy {
165
- margin-bottom: 0;
166
- font-size: 14px;
167
- line-height: 1.55;
136
+ .brand-mark {
137
+ display: grid;
138
+ width: 34px;
139
+ height: 34px;
140
+ flex: 0 0 auto;
141
+ place-items: center;
142
+ border: 1px solid
143
+ color-mix(in srgb, var(--pm-primary) 20%, var(--pm-line));
144
+ border-radius: 50%;
145
+ background: color-mix(
146
+ in srgb,
147
+ var(--pm-primary-soft) 70%,
148
+ var(--pm-card)
149
+ );
150
+ color: var(--pm-primary-strong);
151
+ font-size: 20px;
152
+ font-weight: 800;
153
+ line-height: 1;
168
154
  }
169
155
 
170
156
  .session-row {
171
157
  display: flex;
172
- flex-wrap: wrap;
158
+ min-height: 0;
173
159
  align-items: center;
174
- gap: 8px 12px;
175
- margin-top: 16px;
160
+ gap: 10px;
161
+ margin-top: 8px;
176
162
  }
177
163
 
178
164
  .session-id {
179
165
  overflow: hidden;
180
166
  max-width: 100%;
181
- padding: 7px 10px;
182
- border-radius: 9px;
183
- background: var(--pm-card-muted);
184
167
  color: var(--pm-muted);
185
168
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
186
- font-size: 11px;
169
+ font-size: 10px;
187
170
  text-overflow: ellipsis;
188
171
  white-space: nowrap;
189
172
  }
190
173
 
191
174
  .fallback-link {
192
175
  color: var(--pm-primary-strong);
193
- font-size: 13px;
194
- font-weight: 750;
176
+ font-size: 12px;
177
+ font-weight: 760;
195
178
  text-decoration-thickness: 1px;
196
179
  text-underline-offset: 3px;
197
180
  }
198
181
 
199
182
  .status {
200
183
  display: none;
201
- margin: 14px 0 0;
202
- padding: 10px 12px;
203
- border-radius: 12px;
204
- background: var(--pm-card-muted);
184
+ margin: 10px 0 0;
185
+ padding: 9px 11px;
186
+ border-radius: 11px;
205
187
  color: var(--pm-muted);
206
- font-size: 13px;
207
- line-height: 1.45;
188
+ font-size: 12px;
189
+ line-height: 1.4;
208
190
  }
209
191
 
210
192
  .status[data-tone="success"] {
@@ -226,7 +208,11 @@
226
208
  }
227
209
 
228
210
  .status[data-tone="neutral"] {
229
- display: block;
211
+ display: none;
212
+ }
213
+
214
+ .section {
215
+ padding: 12px 22px 22px;
230
216
  }
231
217
 
232
218
  .section + .section {
@@ -235,12 +221,16 @@
235
221
 
236
222
  .dropzone {
237
223
  display: grid;
238
- min-height: 190px;
224
+ min-height: 260px;
239
225
  place-items: center;
240
- padding: 24px;
226
+ padding: 20px;
241
227
  border: 2px dashed var(--pm-line);
242
- border-radius: 18px;
243
- background: var(--pm-card-muted);
228
+ border-radius: 21px;
229
+ background: color-mix(
230
+ in srgb,
231
+ var(--pm-card-muted) 62%,
232
+ var(--pm-card)
233
+ );
244
234
  text-align: center;
245
235
  transition:
246
236
  border-color 160ms ease,
@@ -259,31 +249,54 @@
259
249
  opacity: 0.65;
260
250
  }
261
251
 
252
+ .drop-trigger {
253
+ display: grid;
254
+ width: min(100%, 280px);
255
+ min-height: 190px;
256
+ place-items: center;
257
+ align-content: center;
258
+ gap: 16px;
259
+ border: 0;
260
+ border-radius: 18px;
261
+ background: transparent;
262
+ color: var(--pm-text);
263
+ cursor: pointer;
264
+ }
265
+
266
+ .drop-trigger:disabled {
267
+ cursor: not-allowed;
268
+ }
269
+
262
270
  .drop-icon {
263
271
  display: grid;
264
- width: 48px;
265
- height: 48px;
266
- margin: 0 auto 12px;
272
+ width: 58px;
273
+ height: 58px;
267
274
  place-items: center;
268
- border-radius: 15px;
269
- background: var(--pm-primary-soft);
270
- color: var(--pm-primary-strong);
271
- font-size: 24px;
272
- font-weight: 800;
275
+ border: 1px solid var(--pm-line);
276
+ border-radius: 50%;
277
+ background: var(--pm-card);
278
+ box-shadow: 0 7px 20px rgb(27 36 66 / 8%);
279
+ color: var(--pm-text);
280
+ font-size: 31px;
281
+ font-weight: 350;
282
+ line-height: 1;
273
283
  }
274
284
 
275
285
  .drop-title {
276
- margin-bottom: 6px;
277
- font-size: 16px;
278
- font-weight: 780;
286
+ margin: 0;
287
+ font-size: 18px;
288
+ font-weight: 520;
289
+ letter-spacing: -0.02em;
279
290
  }
280
291
 
281
292
  .drop-help {
282
- max-width: 490px;
283
- margin: 0 auto 15px;
284
- color: var(--pm-muted);
285
- font-size: 12px;
286
- line-height: 1.5;
293
+ position: absolute;
294
+ width: 1px;
295
+ height: 1px;
296
+ overflow: hidden;
297
+ clip: rect(0 0 0 0);
298
+ clip-path: inset(50%);
299
+ white-space: nowrap;
287
300
  }
288
301
 
289
302
  .native-input {
@@ -337,17 +350,11 @@
337
350
  background: var(--pm-card-muted);
338
351
  }
339
352
 
340
- .button.danger {
341
- border-color: color-mix(in srgb, var(--pm-danger) 25%, var(--pm-line));
342
- background: transparent;
343
- color: var(--pm-danger);
344
- }
345
-
346
353
  .batch-error {
347
354
  display: none;
348
- margin: 12px 0 0;
355
+ margin: 10px 0 0;
349
356
  color: var(--pm-danger);
350
- font-size: 13px;
357
+ font-size: 12px;
351
358
  }
352
359
 
353
360
  .batch-error:not(:empty) {
@@ -356,85 +363,134 @@
356
363
 
357
364
  .file-list {
358
365
  display: grid;
359
- gap: 10px;
360
- margin-top: 16px;
366
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
367
+ gap: 12px;
368
+ margin-top: 14px;
361
369
  }
362
370
 
363
371
  .file-card {
364
- padding: 13px;
365
- border: 1px solid var(--pm-line);
366
- border-radius: 15px;
367
- background: var(--pm-card);
372
+ min-width: 0;
373
+ background: transparent;
368
374
  }
369
375
 
370
- .file-head,
371
- .file-actions,
372
- .file-meta {
373
- display: flex;
374
- align-items: center;
376
+ .file-preview {
377
+ position: relative;
378
+ aspect-ratio: 1;
379
+ overflow: hidden;
380
+ border: 1px solid var(--pm-line);
381
+ border-radius: 16px;
382
+ background: var(--pm-card-muted);
375
383
  }
376
384
 
377
- .file-head {
378
- justify-content: space-between;
379
- gap: 12px;
385
+ .file-preview img,
386
+ .video-preview {
387
+ display: block;
388
+ width: 100%;
389
+ height: 100%;
390
+ object-fit: cover;
380
391
  }
381
392
 
382
- .file-meta {
383
- min-width: 0;
384
- gap: 10px;
393
+ .video-preview {
394
+ display: grid;
395
+ place-items: center;
396
+ background: linear-gradient(145deg, #242a3d, #515c78);
397
+ color: #ffffff;
398
+ font-size: 37px;
385
399
  }
386
400
 
387
401
  .file-kind {
388
402
  display: grid;
389
- width: 36px;
390
- height: 36px;
391
- flex: 0 0 auto;
403
+ width: 100%;
404
+ height: 100%;
392
405
  place-items: center;
393
- border-radius: 11px;
394
- background: var(--pm-primary-soft);
395
- color: var(--pm-primary-strong);
396
- font-size: 17px;
406
+ color: var(--pm-muted);
407
+ font-size: 34px;
408
+ }
409
+
410
+ .file-head {
411
+ min-width: 0;
412
+ margin-top: 7px;
397
413
  }
398
414
 
399
415
  .file-label {
400
416
  margin: 0;
401
- font-size: 13px;
402
- font-weight: 760;
417
+ overflow: hidden;
418
+ font-size: 12px;
419
+ font-weight: 700;
420
+ text-overflow: ellipsis;
421
+ white-space: nowrap;
403
422
  }
404
423
 
405
424
  .file-state {
406
425
  margin: 3px 0 0;
426
+ overflow: hidden;
407
427
  color: var(--pm-muted);
408
428
  font-size: 11px;
429
+ line-height: 1.35;
430
+ text-overflow: ellipsis;
431
+ white-space: nowrap;
409
432
  }
410
433
 
411
434
  .file-state.error {
412
435
  color: var(--pm-danger);
436
+ white-space: normal;
413
437
  }
414
438
 
415
- .file-actions {
416
- flex: 0 0 auto;
417
- gap: 7px;
439
+ .overlay-button {
440
+ position: absolute;
441
+ z-index: 1;
442
+ border: 0;
443
+ cursor: pointer;
418
444
  }
419
445
 
420
- .icon-button {
421
- min-height: 34px;
422
- padding: 7px 9px;
423
- border: 1px solid var(--pm-line);
424
- border-radius: 9px;
425
- background: transparent;
446
+ .remove-overlay {
447
+ top: 9px;
448
+ right: 9px;
449
+ display: grid;
450
+ width: 30px;
451
+ height: 30px;
452
+ place-items: center;
453
+ border: 1px solid rgb(23 32 51 / 12%);
454
+ border-radius: 50%;
455
+ background: rgb(255 255 255 / 90%);
456
+ color: #172033;
457
+ font-size: 23px;
458
+ font-weight: 350;
459
+ line-height: 1;
460
+ }
461
+
462
+ .remove-overlay:disabled {
463
+ cursor: not-allowed;
464
+ opacity: 0.72;
465
+ }
466
+
467
+ .retry-overlay {
468
+ top: 50%;
469
+ left: 50%;
470
+ padding: 10px 14px;
471
+ border: 1px solid rgb(255 255 255 / 76%);
472
+ border-radius: 11px;
473
+ background: rgb(255 255 255 / 94%);
474
+ box-shadow: 0 8px 24px rgb(27 36 66 / 16%);
426
475
  color: var(--pm-text);
427
- cursor: pointer;
428
- font-size: 11px;
429
- font-weight: 730;
476
+ font-size: 12px;
477
+ font-weight: 780;
478
+ transform: translate(-50%, -50%);
479
+ }
480
+
481
+ .retry-overlay:hover {
482
+ background: var(--pm-primary-soft);
430
483
  }
431
484
 
432
485
  .progress {
486
+ position: absolute;
487
+ right: 10px;
488
+ bottom: 10px;
489
+ left: 10px;
433
490
  height: 6px;
434
- margin-top: 11px;
435
491
  overflow: hidden;
436
492
  border-radius: 999px;
437
- background: var(--pm-card-muted);
493
+ background: rgb(255 255 255 / 75%);
438
494
  }
439
495
 
440
496
  .progress-bar {
@@ -448,20 +504,11 @@
448
504
  .destination-grid {
449
505
  display: grid;
450
506
  grid-template-columns: repeat(3, minmax(0, 1fr));
451
- gap: 12px;
452
- margin-top: 16px;
507
+ gap: 9px;
453
508
  }
454
509
 
455
510
  .field {
456
511
  display: grid;
457
- gap: 6px;
458
- }
459
-
460
- .field label {
461
- color: var(--pm-muted);
462
- font-size: 11px;
463
- font-weight: 760;
464
- letter-spacing: 0.02em;
465
512
  }
466
513
 
467
514
  select,
@@ -474,8 +521,9 @@
474
521
  }
475
522
 
476
523
  select {
477
- min-height: 44px;
478
- padding: 9px 32px 9px 10px;
524
+ min-height: 42px;
525
+ padding: 8px 30px 8px 10px;
526
+ font-size: 12px;
479
527
  }
480
528
 
481
529
  textarea {
@@ -492,19 +540,23 @@
492
540
  display: flex;
493
541
  flex-wrap: wrap;
494
542
  gap: 10px;
495
- margin-top: 16px;
543
+ margin-top: 12px;
496
544
  }
497
545
 
498
- .privacy-note {
499
- display: flex;
500
- gap: 9px;
501
- margin: 16px 0 0;
502
- padding: 11px 12px;
503
- border-radius: 12px;
504
- background: var(--pm-success-soft);
505
- color: var(--pm-success);
506
- font-size: 11px;
507
- line-height: 1.5;
546
+ .compatibility-control {
547
+ /* Retained for older hosts; recovery is visible only through the secure fallback. */
548
+ display: none !important;
549
+ }
550
+
551
+ .sr-only {
552
+ position: absolute;
553
+ width: 1px;
554
+ height: 1px;
555
+ padding: 0;
556
+ overflow: hidden;
557
+ clip: rect(0, 0, 0, 0);
558
+ white-space: nowrap;
559
+ border: 0;
508
560
  }
509
561
 
510
562
  [hidden] {
@@ -513,23 +565,29 @@
513
565
 
514
566
  @media (max-width: 640px) {
515
567
  .app {
516
- padding: 10px;
568
+ padding: 8px;
517
569
  }
518
570
 
519
571
  .card {
520
- border-radius: 18px;
572
+ border-radius: 20px;
521
573
  }
522
574
 
523
575
  .hero,
524
576
  .section {
525
- padding: 17px;
577
+ padding-right: 14px;
578
+ padding-left: 14px;
579
+ }
580
+
581
+ .dropzone {
582
+ min-height: 230px;
526
583
  }
527
584
 
528
585
  .destination-grid {
529
586
  grid-template-columns: 1fr;
530
587
  }
531
588
 
532
- .button {
589
+ .destination-actions .button,
590
+ .handoff-actions .button {
533
591
  width: 100%;
534
592
  }
535
593
  }
@@ -548,16 +606,16 @@
548
606
  <main class="app" data-testid="meta-upload-root">
549
607
  <article class="card">
550
608
  <header class="hero">
551
- <p class="eyebrow">
552
- <span class="eyebrow-dot"></span>ProductMaker · Meta Ads
553
- </p>
554
- <h1>Sube tus creativos de forma segura</h1>
555
- <p class="hero-copy muted">
556
- Los archivos van directo al staging privado de ProductMaker. El chat
557
- solo recibe identificadores seguros para continuar.
558
- </p>
609
+ <div class="upload-header">
610
+ <p class="upload-title" data-testid="meta-upload-title">
611
+ Upload image
612
+ </p>
613
+ <span class="brand-mark" aria-label="ProductMaker" role="img"
614
+ >↗</span
615
+ >
616
+ </div>
559
617
  <div class="session-row">
560
- <code class="session-id" data-testid="meta-upload-session-id"
618
+ <code class="session-id" data-testid="meta-upload-session-id" hidden
561
619
  >Esperando sesión…</code
562
620
  >
563
621
  <a
@@ -577,52 +635,46 @@
577
635
  ></p>
578
636
  </header>
579
637
 
580
- <section class="section" aria-labelledby="upload-heading">
581
- <h2 id="upload-heading">1. Selecciona tus archivos</h2>
582
- <p class="section-copy muted">
583
- Hasta 10 archivos · 350 MB por archivo · videos de máximo 180
584
- segundos · mínimo 600 × 600 px.
585
- </p>
638
+ <section class="section" aria-label="Upload media">
586
639
  <div
587
640
  class="dropzone"
588
641
  data-testid="meta-upload-dropzone"
589
642
  data-dragging="false"
590
643
  aria-disabled="true"
644
+ aria-describedby="meta-upload-constraints"
591
645
  >
592
- <div>
593
- <div class="drop-icon" aria-hidden="true">↑</div>
594
- <p class="drop-title">Arrastra aquí o usa el selector</p>
595
- <p class="drop-help">
596
- JPEG, PNG, WebP, MP4, MOV o WebM. Las imágenes grandes se
597
- comprimen; los videos nunca se transcodifican.
598
- </p>
599
- <button
600
- class="button"
601
- type="button"
602
- data-testid="meta-upload-picker-btn"
603
- disabled
604
- >
605
- Elegir archivos
606
- </button>
607
- <button
608
- class="button secondary"
609
- type="button"
610
- data-testid="meta-upload-library-btn"
611
- hidden
612
- disabled
646
+ <button
647
+ class="drop-trigger"
648
+ type="button"
649
+ data-testid="meta-upload-picker-btn"
650
+ disabled
651
+ >
652
+ <span class="drop-icon" aria-hidden="true">+</span>
653
+ <span class="drop-title" data-testid="meta-upload-drop-title"
654
+ >Upload image</span
613
655
  >
614
- Elegir desde la biblioteca de ChatGPT
615
- </button>
616
- <input
617
- class="native-input"
618
- data-testid="meta-upload-input"
619
- type="file"
620
- accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
621
- multiple
622
- disabled
623
- />
624
- </div>
656
+ </button>
657
+ <button
658
+ class="compatibility-control"
659
+ type="button"
660
+ data-testid="meta-upload-library-btn"
661
+ hidden
662
+ disabled
663
+ aria-label="Choose from ChatGPT library"
664
+ ></button>
665
+ <input
666
+ class="native-input"
667
+ data-testid="meta-upload-input"
668
+ type="file"
669
+ accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
670
+ multiple
671
+ disabled
672
+ />
625
673
  </div>
674
+ <p class="drop-help" id="meta-upload-constraints">
675
+ JPEG, PNG, WebP, MP4, MOV o WebM. Hasta 10 archivos de 350 MB; las
676
+ imágenes grandes se comprimen y los videos no se transcodifican.
677
+ </p>
626
678
  <p
627
679
  class="batch-error"
628
680
  data-testid="meta-upload-batch-error"
@@ -634,58 +686,38 @@
634
686
  data-testid="meta-upload-file-list"
635
687
  aria-live="polite"
636
688
  ></div>
637
- <button
638
- class="button secondary"
639
- type="button"
640
- data-testid="meta-upload-pending-refresh"
641
- hidden
642
- >
643
- Actualizar estado del staging
644
- </button>
645
- <p class="privacy-note">
646
- <span aria-hidden="true">✓</span>
647
- <span
648
- >No enviamos bytes, enlaces temporales, nombres de archivo ni
649
- tokens al modelo.</span
650
- >
651
- </p>
652
689
  </section>
653
690
 
654
691
  <section
655
692
  class="section"
656
693
  data-testid="meta-upload-destination-section"
657
- aria-labelledby="destination-heading"
694
+ aria-label="Meta destination"
658
695
  hidden
659
696
  >
660
- <h2 id="destination-heading">2. Elige el destino de Meta</h2>
661
- <p class="section-copy muted">
662
- La selección es obligatoria. Las cuentas se filtran por el portfolio
663
- elegido.
664
- </p>
665
697
  <div class="destination-grid">
666
698
  <div class="field">
667
- <label for="portfolio-select">Portfolio</label>
699
+ <label class="sr-only" for="portfolio-select">Portfolio</label>
668
700
  <select
669
701
  id="portfolio-select"
670
702
  data-testid="meta-upload-portfolio-select"
671
703
  >
672
- <option value="">Selecciona un portfolio</option>
704
+ <option value="">Portfolio</option>
673
705
  </select>
674
706
  </div>
675
707
  <div class="field">
676
- <label for="ad-account-select">Cuenta publicitaria</label>
708
+ <label class="sr-only" for="ad-account-select">Ad account</label>
677
709
  <select
678
710
  id="ad-account-select"
679
711
  data-testid="meta-upload-ad-account-select"
680
712
  disabled
681
713
  >
682
- <option value="">Selecciona una cuenta</option>
714
+ <option value="">Ad account</option>
683
715
  </select>
684
716
  </div>
685
717
  <div class="field">
686
- <label for="page-select">Página</label>
718
+ <label class="sr-only" for="page-select">Facebook Page</label>
687
719
  <select id="page-select" data-testid="meta-upload-page-select">
688
- <option value="">Selecciona una página</option>
720
+ <option value="">Facebook Page</option>
689
721
  </select>
690
722
  </div>
691
723
  </div>
@@ -696,14 +728,7 @@
696
728
  data-testid="meta-upload-submit-destination"
697
729
  disabled
698
730
  >
699
- Enviar selección al chat
700
- </button>
701
- <button
702
- class="button secondary"
703
- type="button"
704
- data-testid="meta-upload-check-status"
705
- >
706
- Actualizar estado
731
+ Continue
707
732
  </button>
708
733
  </div>
709
734
  </section>
@@ -714,10 +739,8 @@
714
739
  aria-labelledby="handoff-heading"
715
740
  hidden
716
741
  >
717
- <h2 id="handoff-heading">3. Copia el handoff seguro</h2>
718
- <p class="section-copy muted">
719
- Este host no puede devolver el mensaje automáticamente. Copia este
720
- JSON exacto en el chat.
742
+ <p id="handoff-heading" class="section-copy muted">
743
+ Copy this handoff into the chat.
721
744
  </p>
722
745
  <textarea data-testid="meta-upload-handoff-copy" readonly></textarea>
723
746
  <div class="handoff-actions">
@@ -726,7 +749,7 @@
726
749
  type="button"
727
750
  data-testid="meta-upload-copy-btn"
728
751
  >
729
- Copiar handoff
752
+ Copy handoff
730
753
  </button>
731
754
  </div>
732
755
  </section>
@@ -765,6 +788,7 @@
765
788
 
766
789
  const elements = {
767
790
  root: document.querySelector('[data-testid="meta-upload-root"]'),
791
+ title: document.querySelector('[data-testid="meta-upload-title"]'),
768
792
  sessionId: document.querySelector(
769
793
  '[data-testid="meta-upload-session-id"]',
770
794
  ),
@@ -777,6 +801,9 @@
777
801
  dropzone: document.querySelector(
778
802
  '[data-testid="meta-upload-dropzone"]',
779
803
  ),
804
+ dropTitle: document.querySelector(
805
+ '[data-testid="meta-upload-drop-title"]',
806
+ ),
780
807
  pickerButton: document.querySelector(
781
808
  '[data-testid="meta-upload-picker-btn"]',
782
809
  ),
@@ -805,12 +832,6 @@
805
832
  submitDestination: document.querySelector(
806
833
  '[data-testid="meta-upload-submit-destination"]',
807
834
  ),
808
- checkStatus: document.querySelector(
809
- '[data-testid="meta-upload-check-status"]',
810
- ),
811
- pendingRefresh: document.querySelector(
812
- '[data-testid="meta-upload-pending-refresh"]',
813
- ),
814
835
  handoffSection: document.querySelector(
815
836
  '[data-testid="meta-upload-handoff-section"]',
816
837
  ),
@@ -823,12 +844,17 @@
823
844
  };
824
845
 
825
846
  const state = {
847
+ disposed: false,
826
848
  initialized: false,
827
849
  standardHostReady: false,
828
850
  hostCanMessage: false,
829
851
  hostCanServerTools: false,
852
+ hostContainerDimensions: null,
830
853
  rpcId: 0,
831
854
  pendingRpc: new Map(),
855
+ pendingSizeReport: false,
856
+ forceSizeReport: false,
857
+ lastReportedHeight: 0,
832
858
  session: null,
833
859
  toolInput: {},
834
860
  items: [],
@@ -836,7 +862,18 @@
836
862
  destinations: { portfolios: [], adAccounts: [], pages: [] },
837
863
  consumedBridgeIds: new Set(),
838
864
  consumingBridgeFiles: false,
865
+ bridgeConsumptionQueued: false,
866
+ ingestionQueue: Promise.resolve(),
839
867
  busyBatch: false,
868
+ uploadBlocked: false,
869
+ fallbackRequired: false,
870
+ workCancelled: false,
871
+ abortController: null,
872
+ activeXhrs: new Set(),
873
+ resizeObserver: null,
874
+ mutationObserver: null,
875
+ resizeHandler: null,
876
+ hostMessageHandler: null,
840
877
  };
841
878
 
842
879
  function isRecord(value) {
@@ -870,6 +907,29 @@
870
907
  return null;
871
908
  }
872
909
 
910
+ function defaultUploadTitle() {
911
+ return state.session?.mediaKind === "video"
912
+ ? "Upload video"
913
+ : "Upload image";
914
+ }
915
+
916
+ function updateUploadTitle() {
917
+ const namedItems = state.items.filter(
918
+ (item) =>
919
+ (item.file && typeof item.file.name === "string") ||
920
+ (typeof item.displayName === "string" &&
921
+ item.displayName.length > 0),
922
+ );
923
+ const title =
924
+ namedItems.length === 1
925
+ ? namedItems[0].file?.name || namedItems[0].displayName
926
+ : namedItems.length > 1
927
+ ? `${namedItems.length} files`
928
+ : defaultUploadTitle();
929
+ elements.title.textContent = title;
930
+ elements.dropTitle.textContent = defaultUploadTitle();
931
+ }
932
+
873
933
  function safeSessionLimit() {
874
934
  const proposed =
875
935
  state.session && Number.isInteger(state.session.maxFiles)
@@ -887,16 +947,232 @@
887
947
  function setSessionStatus(message, tone = "neutral") {
888
948
  elements.sessionStatus.textContent = message;
889
949
  elements.sessionStatus.dataset.tone = tone;
950
+ scheduleIntrinsicSizeReport(true);
890
951
  }
891
952
 
892
953
  function setBatchError(message) {
893
954
  elements.batchError.textContent = message;
955
+ scheduleIntrinsicSizeReport(true);
956
+ }
957
+
958
+ function parseFinitePositiveNumber(value) {
959
+ return typeof value === "number" &&
960
+ Number.isFinite(value) &&
961
+ value > 0
962
+ ? value
963
+ : null;
964
+ }
965
+
966
+ function parseHostContainerDimensions(value) {
967
+ if (!isRecord(value)) return null;
968
+ const height = parseFinitePositiveNumber(value.height);
969
+ const maxHeight = parseFinitePositiveNumber(value.maxHeight);
970
+ if (!height && !maxHeight) return null;
971
+ return { height, maxHeight };
972
+ }
973
+
974
+ function boundedPositiveHeight(value) {
975
+ const parsed = parseFinitePositiveNumber(value);
976
+ return parsed ? Math.max(1, Math.round(parsed)) : null;
977
+ }
978
+
979
+ function intrinsicHeightLimit() {
980
+ const dimensions = state.hostContainerDimensions;
981
+ if (!dimensions) return null;
982
+ return boundedPositiveHeight(
983
+ dimensions.height || dimensions.maxHeight,
984
+ );
985
+ }
986
+
987
+ function measureIntrinsicWidth() {
988
+ const candidates = [
989
+ elements.root && elements.root.scrollWidth,
990
+ document.body && document.body.scrollWidth,
991
+ document.documentElement && document.documentElement.scrollWidth,
992
+ ];
993
+ for (const candidate of candidates) {
994
+ const parsed = boundedPositiveHeight(candidate);
995
+ if (parsed) return parsed;
996
+ }
997
+ return 1;
998
+ }
999
+
1000
+ function applyHostContainerDimensions() {
1001
+ const dimensions = state.hostContainerDimensions;
1002
+ if (!dimensions) return;
1003
+ if (dimensions.height) {
1004
+ document.documentElement.style.height = "100vh";
1005
+ } else {
1006
+ document.documentElement.style.removeProperty("height");
1007
+ }
1008
+ if (dimensions.maxHeight) {
1009
+ document.documentElement.style.maxHeight = `${boundedPositiveHeight(dimensions.maxHeight)}px`;
1010
+ } else {
1011
+ document.documentElement.style.removeProperty("max-height");
1012
+ }
1013
+ document.documentElement.style.overflowY = "auto";
1014
+ scheduleIntrinsicSizeReport(true);
1015
+ }
1016
+
1017
+ function measureIntrinsicHeight() {
1018
+ const candidates = [
1019
+ elements.root && elements.root.scrollHeight,
1020
+ document.body && document.body.scrollHeight,
1021
+ document.documentElement && document.documentElement.scrollHeight,
1022
+ ];
1023
+ let measured = 1;
1024
+ for (const candidate of candidates) {
1025
+ const parsed = boundedPositiveHeight(candidate);
1026
+ if (parsed) {
1027
+ measured = parsed;
1028
+ break;
1029
+ }
1030
+ }
1031
+ const limit = intrinsicHeightLimit();
1032
+ return limit ? Math.min(measured, limit) : measured;
1033
+ }
1034
+
1035
+ function reportIntrinsicHeight(force = false) {
1036
+ state.pendingSizeReport = false;
1037
+ state.forceSizeReport = false;
1038
+ const height = measureIntrinsicHeight();
1039
+ if (!force && height === state.lastReportedHeight) return;
1040
+ state.lastReportedHeight = height;
1041
+ if (state.standardHostReady) {
1042
+ postNotification("ui/notifications/size-changed", {
1043
+ width: measureIntrinsicWidth(),
1044
+ height,
1045
+ });
1046
+ }
1047
+ const bridge = openAiBridge();
1048
+ if (bridge && typeof bridge.notifyIntrinsicHeight === "function") {
1049
+ void Promise.resolve(bridge.notifyIntrinsicHeight(height)).catch(
1050
+ () => {},
1051
+ );
1052
+ }
1053
+ }
1054
+
1055
+ function scheduleIntrinsicSizeReport(force = false) {
1056
+ state.forceSizeReport = state.forceSizeReport || force;
1057
+ if (state.pendingSizeReport) return;
1058
+ state.pendingSizeReport = true;
1059
+ const flush = () => reportIntrinsicHeight(state.forceSizeReport);
1060
+ if (typeof window.requestAnimationFrame === "function") {
1061
+ window.requestAnimationFrame(() => flush());
1062
+ return;
1063
+ }
1064
+ window.setTimeout(flush, 0);
1065
+ }
1066
+
1067
+ function installIntrinsicSizeObserver() {
1068
+ if (
1069
+ state.resizeObserver ||
1070
+ state.mutationObserver ||
1071
+ state.resizeHandler
1072
+ )
1073
+ return;
1074
+ if (typeof ResizeObserver === "function" && elements.root) {
1075
+ state.resizeObserver = new ResizeObserver(() =>
1076
+ scheduleIntrinsicSizeReport(),
1077
+ );
1078
+ state.resizeObserver.observe(elements.root);
1079
+ } else if (typeof MutationObserver === "function" && elements.root) {
1080
+ state.mutationObserver = new MutationObserver(() =>
1081
+ scheduleIntrinsicSizeReport(),
1082
+ );
1083
+ state.mutationObserver.observe(elements.root, {
1084
+ attributes: true,
1085
+ childList: true,
1086
+ subtree: true,
1087
+ characterData: true,
1088
+ });
1089
+ }
1090
+ state.resizeHandler = () => scheduleIntrinsicSizeReport();
1091
+ window.addEventListener("resize", state.resizeHandler);
1092
+ }
1093
+
1094
+ function setFallbackVisibility(visible) {
1095
+ elements.fallbackLink.hidden = !visible;
1096
+ scheduleIntrinsicSizeReport(true);
1097
+ }
1098
+
1099
+ function requireFallback(
1100
+ message,
1101
+ tone = "warning",
1102
+ blockPicker = false,
1103
+ ) {
1104
+ state.fallbackRequired = true;
1105
+ state.uploadBlocked = state.uploadBlocked || blockPicker;
1106
+ setFallbackVisibility(true);
1107
+ if (message) setSessionStatus(message, tone);
1108
+ setPickerAvailability();
1109
+ }
1110
+
1111
+ function enqueueIngestion(task) {
1112
+ const next = state.ingestionQueue.then(task, task);
1113
+ state.ingestionQueue = next.catch(() => {});
1114
+ return next;
1115
+ }
1116
+
1117
+ function activeAbortSignal() {
1118
+ if (!state.abortController || state.abortController.signal.aborted) {
1119
+ state.abortController =
1120
+ typeof AbortController === "function"
1121
+ ? new AbortController()
1122
+ : null;
1123
+ }
1124
+ return state.abortController?.signal;
1125
+ }
1126
+
1127
+ function abortActiveWork() {
1128
+ state.abortController?.abort();
1129
+ for (const request of state.activeXhrs) request.abort();
1130
+ state.activeXhrs.clear();
1131
+ }
1132
+
1133
+ function cleanup() {
1134
+ if (state.disposed) return;
1135
+ state.disposed = true;
1136
+ abortActiveWork();
1137
+ state.resizeObserver?.disconnect();
1138
+ state.mutationObserver?.disconnect();
1139
+ if (state.resizeHandler)
1140
+ window.removeEventListener("resize", state.resizeHandler);
1141
+ if (state.hostMessageHandler)
1142
+ window.removeEventListener("message", state.hostMessageHandler);
1143
+ for (const item of state.items) revokePreviewUrl(item);
1144
+ for (const [id, pending] of state.pendingRpc) {
1145
+ window.clearTimeout(pending.timeout);
1146
+ pending.reject(new Error("resource_teardown"));
1147
+ state.pendingRpc.delete(id);
1148
+ }
1149
+ }
1150
+
1151
+ function normalizedBridgeReferenceId(reference) {
1152
+ return isRecord(reference)
1153
+ ? boundedString(
1154
+ reference.file_id || reference.fileId || reference.id,
1155
+ 300,
1156
+ )
1157
+ : "";
1158
+ }
1159
+
1160
+ function normalizedBridgeDownloadUrl(result) {
1161
+ return typeof result === "string"
1162
+ ? result
1163
+ : isRecord(result)
1164
+ ? boundedString(
1165
+ result.download_url || result.downloadUrl || result.url,
1166
+ 3000,
1167
+ )
1168
+ : "";
894
1169
  }
895
1170
 
896
1171
  function setPickerAvailability() {
897
1172
  const disabled =
898
1173
  !state.session ||
899
1174
  sessionExpired() ||
1175
+ state.uploadBlocked ||
900
1176
  state.busyBatch ||
901
1177
  state.items.length >= safeSessionLimit();
902
1178
  elements.input.disabled = disabled;
@@ -925,10 +1201,12 @@
925
1201
  }
926
1202
 
927
1203
  function initialize() {
928
- if (state.initialized) return;
1204
+ if (state.initialized || state.disposed) return;
929
1205
  state.initialized = true;
930
1206
  bindUi();
1207
+ installIntrinsicSizeObserver();
931
1208
  setPickerAvailability();
1209
+ scheduleIntrinsicSizeReport(true);
932
1210
  void initializeStandardHost();
933
1211
  const bridge = openAiBridge();
934
1212
  elements.libraryPickerButton.hidden = !(
@@ -997,27 +1275,22 @@
997
1275
  "click",
998
1276
  () => void submitDestination(),
999
1277
  );
1000
- elements.checkStatus.addEventListener(
1001
- "click",
1002
- () => void refreshSessionStatus(),
1003
- );
1004
- elements.pendingRefresh.addEventListener(
1005
- "click",
1006
- () => void refreshSessionStatus(),
1007
- );
1008
1278
  elements.copyButton.addEventListener(
1009
1279
  "click",
1010
1280
  () => void copyHandoff(),
1011
1281
  );
1012
1282
 
1013
- window.addEventListener("message", handleHostMessage);
1283
+ state.hostMessageHandler = handleHostMessage;
1284
+ window.addEventListener("message", state.hostMessageHandler);
1014
1285
  }
1015
1286
 
1016
1287
  async function initializeStandardHost() {
1017
1288
  try {
1018
1289
  const result = await rpcRequest("ui/initialize", {
1019
1290
  appInfo: { name: "ProductMaker Meta upload", version: "1.0.0" },
1020
- appCapabilities: {},
1291
+ appCapabilities: {
1292
+ availableDisplayModes: ["inline", "fullscreen"],
1293
+ },
1021
1294
  protocolVersion: APP_PROTOCOL_VERSION,
1022
1295
  });
1023
1296
  state.standardHostReady = true;
@@ -1025,11 +1298,20 @@
1025
1298
  isRecord(result) && isRecord(result.hostCapabilities)
1026
1299
  ? result.hostCapabilities
1027
1300
  : {};
1301
+ const hostContext =
1302
+ isRecord(result) && isRecord(result.hostContext)
1303
+ ? result.hostContext
1304
+ : {};
1305
+ state.hostContainerDimensions = parseHostContainerDimensions(
1306
+ hostContext.containerDimensions,
1307
+ );
1308
+ applyHostContainerDimensions();
1028
1309
  state.hostCanMessage = capabilityEnabled(capabilities.message);
1029
1310
  state.hostCanServerTools = capabilityEnabled(
1030
1311
  capabilities.serverTools,
1031
1312
  );
1032
1313
  postNotification("ui/notifications/initialized", {});
1314
+ scheduleIntrinsicSizeReport(true);
1033
1315
  } catch {
1034
1316
  state.standardHostReady = false;
1035
1317
  state.hostCanServerTools = false;
@@ -1038,6 +1320,10 @@
1038
1320
 
1039
1321
  function rpcRequest(method, params) {
1040
1322
  return new Promise((resolve, reject) => {
1323
+ if (state.disposed) {
1324
+ reject(new Error("resource_teardown"));
1325
+ return;
1326
+ }
1041
1327
  state.rpcId += 1;
1042
1328
  const id = state.rpcId;
1043
1329
  const timeout = window.setTimeout(() => {
@@ -1053,12 +1339,44 @@
1053
1339
  }
1054
1340
 
1055
1341
  function postNotification(method, params) {
1342
+ if (state.disposed) return;
1056
1343
  window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
1057
1344
  }
1058
1345
 
1059
1346
  function handleHostMessage(event) {
1060
1347
  if (event.source !== window.parent || !isRecord(event.data)) return;
1061
1348
  const message = event.data;
1349
+ if (message.method === "ui/resource-teardown") {
1350
+ cleanup();
1351
+ if (Object.prototype.hasOwnProperty.call(message, "id")) {
1352
+ window.parent.postMessage(
1353
+ { jsonrpc: "2.0", id: message.id, result: {} },
1354
+ "*",
1355
+ );
1356
+ }
1357
+ return;
1358
+ }
1359
+ if (message.method === "ui/notifications/tool-cancelled") {
1360
+ state.workCancelled = true;
1361
+ abortActiveWork();
1362
+ requireFallback(
1363
+ "La carga fue cancelada por el host. Puedes continuar desde el enlace seguro.",
1364
+ "warning",
1365
+ true,
1366
+ );
1367
+ return;
1368
+ }
1369
+ if (message.method === "ui/notifications/host-context-changed") {
1370
+ const params = isRecord(message.params) ? message.params : {};
1371
+ const hostContext = isRecord(params.hostContext)
1372
+ ? params.hostContext
1373
+ : params;
1374
+ state.hostContainerDimensions = parseHostContainerDimensions(
1375
+ hostContext.containerDimensions,
1376
+ );
1377
+ applyHostContainerDimensions();
1378
+ return;
1379
+ }
1062
1380
  if (Object.prototype.hasOwnProperty.call(message, "id")) {
1063
1381
  const pending = state.pendingRpc.get(message.id);
1064
1382
  if (!pending) return;
@@ -1090,22 +1408,33 @@
1090
1408
  if (!candidate) return;
1091
1409
  const session = parseSession(candidate);
1092
1410
  if (!session) return;
1411
+ const sessionChanged =
1412
+ !state.session || state.session.sessionId !== session.sessionId;
1413
+ if (sessionChanged) {
1414
+ state.uploadBlocked = false;
1415
+ state.fallbackRequired = false;
1416
+ state.workCancelled = false;
1417
+ if (state.abortController?.signal.aborted) {
1418
+ state.abortController = null;
1419
+ }
1420
+ }
1093
1421
  state.session = session;
1094
1422
  elements.sessionId.textContent = session.sessionId;
1095
1423
  elements.fallbackLink.href = session.uploadPageUrl;
1096
- elements.fallbackLink.hidden = false;
1424
+ updateUploadTitle();
1097
1425
  if (sessionExpired()) {
1098
- setSessionStatus(
1426
+ requireFallback(
1099
1427
  "La sesión está expirada. Abre el enlace seguro para solicitar una nueva.",
1100
1428
  "danger",
1429
+ true,
1101
1430
  );
1102
- elements.pendingRefresh.hidden = true;
1103
1431
  } else {
1432
+ setFallbackVisibility(state.fallbackRequired);
1104
1433
  setSessionStatus("Sesión lista para recibir archivos.", "neutral");
1105
- elements.pendingRefresh.hidden = false;
1106
1434
  }
1107
1435
  setPickerAvailability();
1108
1436
  void consumePendingBridgeFiles();
1437
+ scheduleIntrinsicSizeReport(true);
1109
1438
  }
1110
1439
 
1111
1440
  function parseSession(candidate) {
@@ -1139,8 +1468,26 @@
1139
1468
  void consumePendingBridgeFiles();
1140
1469
  }
1141
1470
 
1142
- async function consumePendingBridgeFiles() {
1471
+ function consumePendingBridgeFiles() {
1472
+ if (
1473
+ state.disposed ||
1474
+ state.bridgeConsumptionQueued ||
1475
+ !state.session ||
1476
+ !Array.isArray(state.toolInput.files)
1477
+ )
1478
+ return;
1479
+ state.bridgeConsumptionQueued = true;
1480
+ void enqueueIngestion(async () => {
1481
+ state.bridgeConsumptionQueued = false;
1482
+ await consumePendingBridgeFilesNow();
1483
+ }).catch(() => {
1484
+ state.bridgeConsumptionQueued = false;
1485
+ });
1486
+ }
1487
+
1488
+ async function consumePendingBridgeFilesNow() {
1143
1489
  if (
1490
+ state.disposed ||
1144
1491
  !state.session ||
1145
1492
  !Array.isArray(state.toolInput.files) ||
1146
1493
  state.consumingBridgeFiles
@@ -1149,14 +1496,12 @@
1149
1496
  state.consumingBridgeFiles = true;
1150
1497
  try {
1151
1498
  const pending = state.toolInput.files.filter((reference) => {
1152
- const id = isRecord(reference)
1153
- ? boundedString(reference.file_id, 300)
1154
- : "";
1499
+ const id = normalizedBridgeReferenceId(reference);
1155
1500
  return id && !state.consumedBridgeIds.has(id);
1156
1501
  });
1157
1502
  if (!pending.length) return;
1158
1503
  if (!hasOpenAiFunction("getFileDownloadUrl")) {
1159
- setSessionStatus(
1504
+ requireFallback(
1160
1505
  "Este host no expone el puente de archivos. Usa el selector o el enlace seguro.",
1161
1506
  "warning",
1162
1507
  );
@@ -1168,11 +1513,13 @@
1168
1513
  );
1169
1514
  const files = [];
1170
1515
  for (const reference of pending.slice(0, availableSlots)) {
1171
- const id = boundedString(reference.file_id, 300);
1172
- state.consumedBridgeIds.add(id);
1516
+ const id = normalizedBridgeReferenceId(reference);
1173
1517
  try {
1174
- files.push(await downloadBridgeReference(reference));
1518
+ const file = await downloadBridgeReference(reference);
1519
+ state.consumedBridgeIds.add(id);
1520
+ files.push(file);
1175
1521
  } catch {
1522
+ state.consumedBridgeIds.add(id);
1176
1523
  addBridgeFailure(reference);
1177
1524
  }
1178
1525
  }
@@ -1182,9 +1529,7 @@
1182
1529
  const hasUnconsumed =
1183
1530
  Array.isArray(state.toolInput.files) &&
1184
1531
  state.toolInput.files.some((reference) => {
1185
- const id = isRecord(reference)
1186
- ? boundedString(reference.file_id, 300)
1187
- : "";
1532
+ const id = normalizedBridgeReferenceId(reference);
1188
1533
  return id && !state.consumedBridgeIds.has(id);
1189
1534
  });
1190
1535
  if (
@@ -1197,82 +1542,75 @@
1197
1542
  }
1198
1543
  }
1199
1544
 
1200
- async function processLocalFiles(files) {
1201
- if (!state.session || sessionExpired() || state.busyBatch) return;
1202
- const bridge = openAiBridge();
1203
- if (
1204
- !bridge ||
1205
- typeof bridge.uploadFile !== "function" ||
1206
- typeof bridge.getFileDownloadUrl !== "function"
1207
- ) {
1208
- await processFiles(files);
1209
- return;
1210
- }
1211
- const availableSlots = Math.max(
1212
- 0,
1213
- safeSessionLimit() - state.items.length,
1214
- );
1215
- const normalized = [];
1216
- for (const file of files.slice(0, availableSlots)) {
1217
- try {
1218
- const fileId = boundedString(
1219
- await bridge.uploadFile(file, { library: false }),
1220
- 300,
1221
- );
1222
- if (!fileId) throw new Error("missing_bridge_file_id");
1223
- normalized.push(
1224
- await downloadBridgeReference({
1225
- fileId,
1226
- fileName: file.name,
1227
- mimeType: file.type,
1228
- }),
1545
+ function processLocalFiles(files) {
1546
+ return enqueueIngestion(async () => {
1547
+ if (
1548
+ state.disposed ||
1549
+ !state.session ||
1550
+ sessionExpired() ||
1551
+ state.uploadBlocked
1552
+ )
1553
+ return;
1554
+ state.workCancelled = false;
1555
+ if (files.length > safeSessionLimit() - state.items.length) {
1556
+ setBatchError(
1557
+ `Puedes subir hasta ${safeSessionLimit()} archivos por sesión.`,
1229
1558
  );
1230
- } catch {
1231
- addBridgeFailure({ fileName: file.name, mimeType: file.type });
1232
1559
  }
1233
- }
1234
- if (files.length > availableSlots) {
1235
- setBatchError(
1236
- `Puedes subir hasta ${safeSessionLimit()} archivos por sesión.`,
1237
- );
1238
- }
1239
- await processFiles(normalized);
1560
+ await processFiles(files);
1561
+ });
1240
1562
  }
1241
1563
 
1242
- async function chooseLibraryFiles() {
1243
- if (!state.session || sessionExpired() || state.busyBatch) return;
1244
- const bridge = openAiBridge();
1245
- if (
1246
- !bridge ||
1247
- typeof bridge.selectFiles !== "function" ||
1248
- typeof bridge.getFileDownloadUrl !== "function"
1249
- )
1250
- return;
1251
- try {
1252
- const result = await bridge.selectFiles();
1253
- const selected = Array.isArray(result) ? result : [];
1254
- const availableSlots = Math.max(
1255
- 0,
1256
- safeSessionLimit() - state.items.length,
1257
- );
1258
- const files = [];
1259
- for (const candidate of selected.slice(0, availableSlots)) {
1260
- if (!isRecord(candidate)) continue;
1261
- const fileId = boundedString(candidate.fileId, 300);
1262
- const fileName = boundedString(candidate.fileName, 240);
1263
- const mimeType = boundedString(candidate.mimeType, 100);
1264
- if (!fileId || !fileName || !mimeType) continue;
1265
- files.push(
1266
- await downloadBridgeReference({ fileId, fileName, mimeType }),
1564
+ function chooseLibraryFiles() {
1565
+ return enqueueIngestion(async () => {
1566
+ if (
1567
+ state.disposed ||
1568
+ !state.session ||
1569
+ sessionExpired() ||
1570
+ state.uploadBlocked
1571
+ )
1572
+ return;
1573
+ const bridge = openAiBridge();
1574
+ if (
1575
+ !bridge ||
1576
+ typeof bridge.selectFiles !== "function" ||
1577
+ typeof bridge.getFileDownloadUrl !== "function"
1578
+ )
1579
+ return;
1580
+ try {
1581
+ const result = await bridge.selectFiles();
1582
+ const selected = Array.isArray(result) ? result : [];
1583
+ const availableSlots = Math.max(
1584
+ 0,
1585
+ safeSessionLimit() - state.items.length,
1586
+ );
1587
+ const files = [];
1588
+ for (const candidate of selected.slice(0, availableSlots)) {
1589
+ if (!isRecord(candidate)) continue;
1590
+ const fileId = boundedString(candidate.fileId, 300);
1591
+ const fileName = boundedString(candidate.fileName, 240);
1592
+ const mimeType = boundedString(candidate.mimeType, 100);
1593
+ if (!fileId || !fileName || !mimeType) continue;
1594
+ try {
1595
+ files.push(
1596
+ await downloadBridgeReference({
1597
+ fileId,
1598
+ fileName,
1599
+ mimeType,
1600
+ }),
1601
+ );
1602
+ } catch {
1603
+ addBridgeFailure({ fileId, fileName, mimeType });
1604
+ }
1605
+ }
1606
+ await processFiles(files);
1607
+ } catch {
1608
+ setSessionStatus(
1609
+ "No se pudo abrir el selector del host. Usa el selector del navegador.",
1610
+ "warning",
1267
1611
  );
1268
1612
  }
1269
- await processFiles(files);
1270
- } catch {
1271
- setSessionStatus(
1272
- "No se pudo abrir el selector del host. Usa el selector del navegador.",
1273
- "warning",
1274
- );
1275
- }
1613
+ });
1276
1614
  }
1277
1615
 
1278
1616
  function isBrowserFile(value) {
@@ -1290,31 +1628,21 @@
1290
1628
  if (!bridge || typeof bridge.getFileDownloadUrl !== "function") {
1291
1629
  throw new Error("bridge_download_unavailable");
1292
1630
  }
1293
- const fileId = boundedString(
1294
- reference.file_id || reference.fileId || reference.id,
1295
- 300,
1296
- );
1631
+ const fileId = normalizedBridgeReferenceId(reference);
1297
1632
  if (!fileId) throw new Error("invalid_bridge_file");
1298
1633
  const resolved = await bridge.getFileDownloadUrl({ fileId });
1299
- const temporaryUrl =
1300
- typeof resolved === "string"
1301
- ? resolved
1302
- : isRecord(resolved)
1303
- ? boundedString(
1304
- resolved.download_url ||
1305
- resolved.downloadUrl ||
1306
- resolved.url,
1307
- 3000,
1308
- )
1309
- : "";
1634
+ const temporaryUrl = normalizedBridgeDownloadUrl(resolved);
1310
1635
  const parsedUrl = new URL(temporaryUrl);
1311
- if (!safeWebProtocol(parsedUrl)) throw new Error("unsafe_bridge_url");
1636
+ if (!safeBridgeDownloadUrl(parsedUrl))
1637
+ throw new Error("unsafe_bridge_url");
1312
1638
  const response = await fetch(parsedUrl.toString(), {
1313
1639
  method: "GET",
1314
1640
  credentials: "omit",
1641
+ redirect: "error",
1642
+ signal: activeAbortSignal(),
1315
1643
  });
1316
1644
  if (!response.ok) throw new Error("bridge_download_failed");
1317
- const blob = await response.blob();
1645
+ const blob = await readBoundedBridgeBlob(response);
1318
1646
  const preferredType = boundedString(
1319
1647
  reference.mime_type || reference.mimeType || reference.type,
1320
1648
  100,
@@ -1338,6 +1666,48 @@
1338
1666
  });
1339
1667
  }
1340
1668
 
1669
+ async function readBoundedBridgeBlob(response) {
1670
+ const declaredLength = Number(
1671
+ response.headers.get("content-length") || "",
1672
+ );
1673
+ if (
1674
+ Number.isFinite(declaredLength) &&
1675
+ declaredLength > MAX_FILE_SIZE_BYTES
1676
+ ) {
1677
+ throw new Error("bridge_file_too_large");
1678
+ }
1679
+
1680
+ if (!response.body || typeof response.body.getReader !== "function") {
1681
+ const blob = await response.blob();
1682
+ if (blob.size > MAX_FILE_SIZE_BYTES) {
1683
+ throw new Error("bridge_file_too_large");
1684
+ }
1685
+ return blob;
1686
+ }
1687
+
1688
+ const reader = response.body.getReader();
1689
+ const chunks = [];
1690
+ let total = 0;
1691
+ try {
1692
+ while (true) {
1693
+ const next = await reader.read();
1694
+ if (next.done) break;
1695
+ const chunk = next.value;
1696
+ total += chunk.byteLength;
1697
+ if (total > MAX_FILE_SIZE_BYTES) {
1698
+ await reader.cancel();
1699
+ throw new Error("bridge_file_too_large");
1700
+ }
1701
+ chunks.push(chunk);
1702
+ }
1703
+ } finally {
1704
+ reader.releaseLock?.();
1705
+ }
1706
+ return new Blob(chunks, {
1707
+ type: response.headers.get("content-type") || "",
1708
+ });
1709
+ }
1710
+
1341
1711
  function addBridgeFailure(reference) {
1342
1712
  const type = boundedString(
1343
1713
  reference.mime_type || reference.mimeType || reference.type,
@@ -1345,11 +1715,19 @@
1345
1715
  );
1346
1716
  const mediaKind = mediaKindForType(type) || "image";
1347
1717
  const item = createItem(null, mediaKind);
1718
+ item.displayName = boundedString(
1719
+ reference.file_name || reference.fileName || reference.name,
1720
+ 300,
1721
+ );
1348
1722
  item.status = "failed";
1349
1723
  item.error =
1350
1724
  "No se pudo leer el archivo temporal del host. Usa el selector o el enlace seguro.";
1351
1725
  item.failureStage = "bridge";
1352
1726
  state.items.push(item);
1727
+ requireFallback(
1728
+ "No se pudo leer el archivo temporal del host. Usa el selector o el enlace seguro.",
1729
+ "warning",
1730
+ );
1353
1731
  renderItems();
1354
1732
  emitAnalytics("ads_meta_remote_upload_error", mediaKind, {
1355
1733
  failureStage: "bridge",
@@ -1358,7 +1736,14 @@
1358
1736
  }
1359
1737
 
1360
1738
  async function processFiles(fileCandidates) {
1361
- if (!state.session || sessionExpired() || state.busyBatch) return;
1739
+ if (
1740
+ state.disposed ||
1741
+ !state.session ||
1742
+ sessionExpired() ||
1743
+ state.uploadBlocked ||
1744
+ state.busyBatch
1745
+ )
1746
+ return;
1362
1747
  const files = fileCandidates.filter(isBrowserFile);
1363
1748
  if (!files.length) return;
1364
1749
  setBatchError("");
@@ -1395,6 +1780,7 @@
1395
1780
  id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`,
1396
1781
  uploadId: createClientUuid(),
1397
1782
  file,
1783
+ displayName: file?.name || "",
1398
1784
  uploadFile: file,
1399
1785
  mediaKind,
1400
1786
  status: "pending",
@@ -1403,6 +1789,7 @@
1403
1789
  error: "",
1404
1790
  failureStage: "",
1405
1791
  stagingAssetId: "",
1792
+ previewUrl: "",
1406
1793
  };
1407
1794
  }
1408
1795
 
@@ -1435,6 +1822,18 @@
1435
1822
  failureStage: failure.stage,
1436
1823
  errorType: failure.code,
1437
1824
  });
1825
+ if (failure.code === "session_expired") {
1826
+ requireFallback(
1827
+ "La sesión expiró. Abre el enlace seguro para crear otra.",
1828
+ "danger",
1829
+ true,
1830
+ );
1831
+ } else if (failure.stage !== "validation") {
1832
+ requireFallback(
1833
+ "La carga no terminó. Puedes reintentarlo o continuar desde el enlace seguro.",
1834
+ "warning",
1835
+ );
1836
+ }
1438
1837
  }
1439
1838
  renderItems();
1440
1839
  }
@@ -1606,6 +2005,12 @@
1606
2005
  alpha: file.type !== "image/jpeg",
1607
2006
  });
1608
2007
  if (!context) throw new Error("canvas_unavailable");
2008
+ if (file.type === "image/png") {
2009
+ context.save();
2010
+ context.fillStyle = "#ffffff";
2011
+ context.fillRect(0, 0, canvas.width, canvas.height);
2012
+ context.restore();
2013
+ }
1609
2014
  context.drawImage(
1610
2015
  inspection.image,
1611
2016
  0,
@@ -1613,16 +2018,13 @@
1613
2018
  canvas.width,
1614
2019
  canvas.height,
1615
2020
  );
1616
- const blob = await new Promise((resolve, reject) => {
1617
- canvas.toBlob(
1618
- (result) =>
1619
- result
1620
- ? resolve(result)
1621
- : reject(new Error("compression_failed")),
1622
- file.type,
1623
- 0.85,
1624
- );
1625
- });
2021
+ let blob = await canvasBlob(canvas, file.type);
2022
+ if (
2023
+ file.type === "image/png" &&
2024
+ blob.size > IMAGE_COMPRESSION_MAX_BYTES
2025
+ ) {
2026
+ blob = await canvasBlob(canvas, "image/jpeg");
2027
+ }
1626
2028
  const compressed = new File([blob], file.name, {
1627
2029
  type: blob.type || file.type,
1628
2030
  lastModified: file.lastModified,
@@ -1651,6 +2053,19 @@
1651
2053
  }
1652
2054
  }
1653
2055
 
2056
+ function canvasBlob(canvas, type) {
2057
+ return new Promise((resolve, reject) => {
2058
+ canvas.toBlob(
2059
+ (result) =>
2060
+ result
2061
+ ? resolve(result)
2062
+ : reject(new Error("compression_failed")),
2063
+ type,
2064
+ 0.85,
2065
+ );
2066
+ });
2067
+ }
2068
+
1654
2069
  async function uploadWithRetries(item) {
1655
2070
  let lastError = null;
1656
2071
  for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt += 1) {
@@ -1675,6 +2090,7 @@
1675
2090
  return;
1676
2091
  } catch (error) {
1677
2092
  if (error && error.message === "session_expired") throw error;
2093
+ if (state.disposed || state.workCancelled) throw error;
1678
2094
  lastError = error;
1679
2095
  }
1680
2096
  }
@@ -1712,6 +2128,7 @@
1712
2128
  fileSize: file.size,
1713
2129
  }),
1714
2130
  credentials: "omit",
2131
+ signal: activeAbortSignal(),
1715
2132
  });
1716
2133
  if (!response.ok) {
1717
2134
  const error = new Error(
@@ -1761,6 +2178,21 @@
1761
2178
  );
1762
2179
  }
1763
2180
 
2181
+ function safeBridgeDownloadUrl(url) {
2182
+ if (
2183
+ url.protocol !== "https:" ||
2184
+ url.username ||
2185
+ url.password ||
2186
+ url.port
2187
+ )
2188
+ return false;
2189
+ const hostname = url.hostname.toLowerCase();
2190
+ return (
2191
+ hostname === "files.oaiusercontent.com" ||
2192
+ hostname.endsWith(".oaiusercontent.com")
2193
+ );
2194
+ }
2195
+
1764
2196
  function safeUploadPageUrl(url) {
1765
2197
  if (url.username || url.password) return false;
1766
2198
  if (
@@ -1779,23 +2211,32 @@
1779
2211
  function putFileWithProgress(url, file, onProgress) {
1780
2212
  return new Promise((resolve, reject) => {
1781
2213
  const request = new XMLHttpRequest();
2214
+ let settled = false;
2215
+ const finish = (callback) => {
2216
+ if (settled) return;
2217
+ settled = true;
2218
+ state.activeXhrs.delete(request);
2219
+ callback();
2220
+ };
1782
2221
  request.open("PUT", url);
1783
2222
  request.timeout = 5 * 60 * 1000;
1784
2223
  request.setRequestHeader("Content-Type", file.type);
2224
+ state.activeXhrs.add(request);
1785
2225
  request.upload.addEventListener("progress", (event) => {
1786
2226
  if (event.lengthComputable && event.total > 0) {
1787
2227
  onProgress(Math.round((event.loaded / event.total) * 100));
1788
2228
  }
1789
2229
  });
1790
2230
  request.addEventListener("load", () => {
1791
- if (request.status >= 200 && request.status < 300) resolve();
1792
- else reject(uploadTransportError("put_failed"));
2231
+ if (request.status >= 200 && request.status < 300)
2232
+ finish(resolve);
2233
+ else finish(() => reject(uploadTransportError("put_failed")));
1793
2234
  });
1794
2235
  request.addEventListener("error", () =>
1795
- reject(uploadTransportError("network_error")),
2236
+ finish(() => reject(uploadTransportError("network_error"))),
1796
2237
  );
1797
2238
  request.addEventListener("timeout", () =>
1798
- reject(uploadTransportError("upload_timeout")),
2239
+ finish(() => reject(uploadTransportError("upload_timeout"))),
1799
2240
  );
1800
2241
  request.send(file);
1801
2242
  });
@@ -1846,11 +2287,38 @@
1846
2287
 
1847
2288
  function removeItem(index) {
1848
2289
  if (state.busyBatch) return;
1849
- state.items.splice(index, 1);
2290
+ const [item] = state.items.splice(index, 1);
2291
+ if (item) revokePreviewUrl(item);
1850
2292
  renderItems();
1851
2293
  setPickerAvailability();
1852
2294
  }
1853
2295
 
2296
+ function revokePreviewUrl(item) {
2297
+ if (
2298
+ item.previewUrl &&
2299
+ typeof window.URL.revokeObjectURL === "function"
2300
+ ) {
2301
+ window.URL.revokeObjectURL(item.previewUrl);
2302
+ item.previewUrl = "";
2303
+ }
2304
+ }
2305
+
2306
+ function previewUrlFor(item) {
2307
+ if (
2308
+ !item.file ||
2309
+ item.mediaKind !== "image" ||
2310
+ !IMAGE_TYPES.has(item.file.type)
2311
+ )
2312
+ return "";
2313
+ if (
2314
+ !item.previewUrl &&
2315
+ typeof window.URL.createObjectURL === "function"
2316
+ ) {
2317
+ item.previewUrl = window.URL.createObjectURL(item.file);
2318
+ }
2319
+ return item.previewUrl;
2320
+ }
2321
+
1854
2322
  function renderItems() {
1855
2323
  const fragment = document.createDocumentFragment();
1856
2324
  state.items.forEach((item, index) => {
@@ -1858,33 +2326,46 @@
1858
2326
  card.className = "file-card";
1859
2327
  card.dataset.testid = `meta-upload-file-${index}`;
1860
2328
 
1861
- const head = document.createElement("div");
1862
- head.className = "file-head";
1863
- const meta = document.createElement("div");
1864
- meta.className = "file-meta";
1865
- const icon = document.createElement("span");
1866
- icon.className = "file-kind";
1867
- icon.setAttribute("aria-hidden", "true");
1868
- icon.textContent = item.mediaKind === "video" ? "▶" : "▧";
1869
- const text = document.createElement("div");
1870
- const label = document.createElement("p");
1871
- label.className = "file-label";
1872
- label.textContent =
1873
- item.mediaKind === "video"
1874
- ? `Video ${index + 1}`
1875
- : `Imagen ${index + 1}`;
1876
- const status = document.createElement("p");
1877
- status.className = `file-state${item.status === "failed" ? " error" : ""}`;
1878
- status.dataset.testid =
1879
- item.status === "failed"
1880
- ? `meta-upload-file-error-${index}`
1881
- : `meta-upload-file-status-${index}`;
1882
- status.textContent = itemStatusText(item);
1883
- text.append(label, status);
1884
- meta.append(icon, text);
2329
+ const preview = document.createElement("div");
2330
+ preview.className = "file-preview";
2331
+ preview.dataset.testid = `meta-upload-file-preview-${index}`;
2332
+ const previewUrl = previewUrlFor(item);
2333
+ if (previewUrl) {
2334
+ const image = document.createElement("img");
2335
+ image.src = previewUrl;
2336
+ image.alt =
2337
+ item.file?.name || item.displayName || "Uploaded image";
2338
+ image.dataset.testid = `meta-upload-preview-${index}`;
2339
+ preview.append(image);
2340
+ } else if (item.mediaKind === "video") {
2341
+ const video = document.createElement("span");
2342
+ video.className = "video-preview";
2343
+ video.setAttribute("aria-hidden", "true");
2344
+ video.textContent = "▶";
2345
+ preview.append(video);
2346
+ } else {
2347
+ const placeholder = document.createElement("span");
2348
+ placeholder.className = "file-kind";
2349
+ placeholder.setAttribute("aria-hidden", "true");
2350
+ placeholder.textContent = "▧";
2351
+ preview.append(placeholder);
2352
+ }
2353
+
2354
+ if (item.status === "uploading" || item.status === "uploaded") {
2355
+ const progress = document.createElement("div");
2356
+ progress.className = "progress";
2357
+ progress.setAttribute("role", "progressbar");
2358
+ progress.setAttribute("aria-valuemin", "0");
2359
+ progress.setAttribute("aria-valuemax", "100");
2360
+ progress.setAttribute("aria-valuenow", String(item.progress));
2361
+ progress.dataset.testid = `meta-upload-progress-${index}`;
2362
+ const bar = document.createElement("div");
2363
+ bar.className = "progress-bar";
2364
+ bar.style.width = `${item.progress}%`;
2365
+ progress.append(bar);
2366
+ preview.append(progress);
2367
+ }
1885
2368
 
1886
- const actions = document.createElement("div");
1887
- actions.className = "file-actions";
1888
2369
  if (
1889
2370
  item.status === "failed" &&
1890
2371
  item.failureStage !== "validation" &&
@@ -1892,42 +2373,50 @@
1892
2373
  ) {
1893
2374
  const retry = document.createElement("button");
1894
2375
  retry.type = "button";
1895
- retry.className = "icon-button";
2376
+ retry.className = "overlay-button retry-overlay";
1896
2377
  retry.dataset.testid = `meta-upload-retry-${index}`;
1897
- retry.textContent = "Reintentar";
2378
+ retry.setAttribute("aria-label", `Retry ${item.file.name}`);
2379
+ retry.textContent = "Retry";
1898
2380
  retry.addEventListener("click", () => void retryItem(index));
1899
- actions.append(retry);
2381
+ preview.append(retry);
1900
2382
  }
2383
+
1901
2384
  const remove = document.createElement("button");
1902
2385
  remove.type = "button";
1903
- remove.className = "icon-button";
2386
+ remove.className = "overlay-button remove-overlay";
1904
2387
  remove.dataset.testid = `meta-upload-remove-${index}`;
1905
- remove.textContent = "Quitar";
2388
+ remove.setAttribute(
2389
+ "aria-label",
2390
+ `Remove ${item.file?.name || item.displayName || `file ${index + 1}`}`,
2391
+ );
2392
+ remove.textContent = "×";
1906
2393
  remove.disabled = ["uploading", "validating", "uploaded"].includes(
1907
2394
  item.status,
1908
2395
  );
1909
2396
  remove.addEventListener("click", () => removeItem(index));
1910
- actions.append(remove);
1911
- head.append(meta, actions);
1912
- card.append(head);
2397
+ preview.append(remove);
2398
+ card.append(preview);
1913
2399
 
1914
- if (item.status === "uploading" || item.status === "uploaded") {
1915
- const progress = document.createElement("div");
1916
- progress.className = "progress";
1917
- progress.setAttribute("role", "progressbar");
1918
- progress.setAttribute("aria-valuemin", "0");
1919
- progress.setAttribute("aria-valuemax", "100");
1920
- progress.setAttribute("aria-valuenow", String(item.progress));
1921
- progress.dataset.testid = `meta-upload-progress-${index}`;
1922
- const bar = document.createElement("div");
1923
- bar.className = "progress-bar";
1924
- bar.style.width = `${item.progress}%`;
1925
- progress.append(bar);
1926
- card.append(progress);
1927
- }
2400
+ const head = document.createElement("div");
2401
+ head.className = "file-head";
2402
+ const label = document.createElement("p");
2403
+ label.className = "file-label";
2404
+ label.textContent =
2405
+ item.file?.name || item.displayName || `File ${index + 1}`;
2406
+ const status = document.createElement("p");
2407
+ status.className = `file-state${item.status === "failed" ? " error" : ""}`;
2408
+ status.dataset.testid =
2409
+ item.status === "failed"
2410
+ ? `meta-upload-file-error-${index}`
2411
+ : `meta-upload-file-status-${index}`;
2412
+ status.textContent = itemStatusText(item);
2413
+ head.append(label, status);
2414
+ card.append(head);
1928
2415
  fragment.append(card);
1929
2416
  });
1930
2417
  elements.fileList.replaceChildren(fragment);
2418
+ updateUploadTitle();
2419
+ scheduleIntrinsicSizeReport(true);
1931
2420
  }
1932
2421
 
1933
2422
  function itemStatusText(item) {
@@ -1940,8 +2429,6 @@
1940
2429
 
1941
2430
  async function refreshSessionStatus() {
1942
2431
  if (!state.session) return;
1943
- elements.checkStatus.disabled = true;
1944
- elements.pendingRefresh.disabled = true;
1945
2432
  setSessionStatus("Comprobando el staging privado…", "neutral");
1946
2433
  try {
1947
2434
  const result = await callReadOnlyTool("check_meta_media_upload", {
@@ -1950,24 +2437,33 @@
1950
2437
  const status = parseStatusResult(result);
1951
2438
  state.assets = status.assets;
1952
2439
  if (!status.assets.length) {
1953
- setSessionStatus(
1954
- status.legacyKindAmbiguous
1955
- ? "El tipo del asset legacy es ambiguo. Vuelve a comprobar o usa el enlace seguro."
1956
- : "La sesión sigue pendiente. Termina la carga o vuelve a comprobar.",
1957
- "warning",
1958
- );
2440
+ if (status.legacyKindAmbiguous) {
2441
+ requireFallback(
2442
+ "El tipo del asset legacy es ambiguo. Vuelve a comprobar o usa el enlace seguro.",
2443
+ "warning",
2444
+ );
2445
+ } else {
2446
+ requireFallback(
2447
+ "La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
2448
+ "warning",
2449
+ );
2450
+ }
1959
2451
  elements.destinationSection.hidden = true;
1960
- elements.pendingRefresh.hidden = false;
1961
2452
  return;
1962
2453
  }
1963
- elements.pendingRefresh.hidden = true;
1964
2454
 
1965
- const locallyUploaded = state.items.filter(
1966
- (item) => item.status === "uploaded",
1967
- ).length;
1968
2455
  const partial =
1969
2456
  status.status === "pending" ||
1970
- status.assets.length < locallyUploaded;
2457
+ (state.items.length > 0 &&
2458
+ status.assets.length < state.items.length);
2459
+ if (partial) {
2460
+ requireFallback(
2461
+ "Carga parcial verificada. Puedes continuar desde el enlace seguro si algún archivo no terminó.",
2462
+ "warning",
2463
+ );
2464
+ } else {
2465
+ setFallbackVisibility(state.fallbackRequired);
2466
+ }
1971
2467
  setSessionStatus(
1972
2468
  partial
1973
2469
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
@@ -1986,18 +2482,12 @@
1986
2482
  (error && /expired|expirad/i.test(String(error.message)));
1987
2483
  const toolsUnavailable =
1988
2484
  error && error.message === "host_tools_unavailable";
1989
- setSessionStatus(
1990
- expired
1991
- ? "La sesión está expirada. Abre el enlace seguro para solicitar una nueva."
1992
- : toolsUnavailable
1993
- ? "host_tools_unavailable: este host no permite comprobar el staging desde la App. Usa el enlace seguro."
1994
- : "No se pudo comprobar la sesión. Conserva el enlace y vuelve a intentarlo.",
1995
- "danger",
1996
- );
1997
- elements.pendingRefresh.hidden = expired;
1998
- } finally {
1999
- elements.checkStatus.disabled = false;
2000
- elements.pendingRefresh.disabled = false;
2485
+ const message = expired
2486
+ ? "La sesión está expirada. Abre el enlace seguro para solicitar una nueva."
2487
+ : toolsUnavailable
2488
+ ? "host_tools_unavailable: este host no permite comprobar el staging desde la App. Usa el enlace seguro."
2489
+ : "No se pudo comprobar la sesión. Conserva el enlace y vuelve a intentarlo.";
2490
+ requireFallback(message, "danger", expired);
2001
2491
  }
2002
2492
  }
2003
2493
 
@@ -2012,7 +2502,15 @@
2012
2502
  const bridge = openAiBridge();
2013
2503
  let result;
2014
2504
  if (bridge && typeof bridge.callTool === "function") {
2015
- result = await bridge.callTool(name, args);
2505
+ try {
2506
+ result = await bridge.callTool(name, args);
2507
+ } catch (error) {
2508
+ if (!state.hostCanServerTools) throw error;
2509
+ result = await rpcRequest("tools/call", {
2510
+ name,
2511
+ arguments: args,
2512
+ });
2513
+ }
2016
2514
  } else if (state.hostCanServerTools) {
2017
2515
  result = await rpcRequest("tools/call", { name, arguments: args });
2018
2516
  } else {
@@ -2125,18 +2623,19 @@
2125
2623
  fillSelect(
2126
2624
  elements.portfolio,
2127
2625
  state.destinations.portfolios,
2128
- "Selecciona un portfolio",
2626
+ "Portfolio",
2129
2627
  (entry) => entry.name,
2130
2628
  );
2131
2629
  fillSelect(
2132
2630
  elements.page,
2133
2631
  state.destinations.pages,
2134
- "Selecciona una página",
2632
+ "Facebook Page",
2135
2633
  (entry) => entry.name,
2136
2634
  );
2137
2635
  renderAdAccounts();
2138
2636
  elements.destinationSection.hidden = false;
2139
2637
  updateDestinationSubmit();
2638
+ scheduleIntrinsicSizeReport(true);
2140
2639
  }
2141
2640
 
2142
2641
  function renderAdAccounts() {
@@ -2149,9 +2648,7 @@
2149
2648
  fillSelect(
2150
2649
  elements.adAccount,
2151
2650
  accounts,
2152
- portfolioId
2153
- ? "Selecciona una cuenta"
2154
- : "Elige primero un portfolio",
2651
+ portfolioId ? "Ad account" : "Ad account",
2155
2652
  (entry) =>
2156
2653
  `${entry.name}${entry.currency ? ` · ${entry.currency}` : ""}`,
2157
2654
  );
@@ -2236,6 +2733,7 @@
2236
2733
  "Copia el handoff exacto y pégalo en el chat.",
2237
2734
  "warning",
2238
2735
  );
2736
+ scheduleIntrinsicSizeReport(true);
2239
2737
  }
2240
2738
 
2241
2739
  async function copyHandoff() {