@widgetic/creator 0.3.67 → 0.3.68

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.
@@ -806,6 +806,11 @@
806
806
  // console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
807
807
  }
808
808
 
809
+ /** Restore Checkpoint must compile GitLab HEAD even inside the post-codegen window. */
810
+ export function clearCodegenPreviewFresh(): void {
811
+ codegenPreviewFreshUntil = 0;
812
+ }
813
+
809
814
  /** Timestamp of generation start — set by the parent when the operation begins. */
810
815
  let codegenStartedAt: number | null = null;
811
816
  /** Parent calls this when a generation starts for THIS widget's panel. */
@@ -2394,6 +2399,10 @@
2394
2399
  if (previewHealthCheckHandle) { clearInterval(previewHealthCheckHandle); previewHealthCheckHandle = null; }
2395
2400
  previewFallbackHandle = setTimeout(() => {
2396
2401
  if ((previewLoading || previewHydrating) && (directPreviewUrl || iframeSrc)) {
2402
+ if (previewWidgetReady && !previewHydrating) {
2403
+ previewLoading = false;
2404
+ return;
2405
+ }
2397
2406
  console.warn('[WidgetDetails] Preview fallback timeout — revealing iframe');
2398
2407
  previewLoading = false;
2399
2408
  previewError = null;
@@ -3172,6 +3181,11 @@
3172
3181
  }
3173
3182
 
3174
3183
  let lastWsPreviewSignature: string | null = null;
3184
+ let pendingWorkerPreviewFromWs: {
3185
+ workerUrl: string;
3186
+ buildTimeMs?: number | null;
3187
+ eventTimestamp?: string | null;
3188
+ } | null = null;
3175
3189
 
3176
3190
  async function applyWorkerPreviewFromEvent(
3177
3191
  workerUrl: string,
@@ -3188,6 +3202,16 @@
3188
3202
  if (signature === lastWsPreviewSignature) return;
3189
3203
  lastWsPreviewSignature = signature;
3190
3204
 
3205
+ if (isGeneratingCode) {
3206
+ pendingWorkerPreviewFromWs = { workerUrl, buildTimeMs, eventTimestamp };
3207
+ dynamicWorkerPreviewUrl = fetchUrl;
3208
+ directPreviewUrl = toWorkerPreviewDisplayUrl(fetchUrl);
3209
+ console.log('[WidgetDetails] Defer Worker iframe remount until codegen finishes');
3210
+ return;
3211
+ }
3212
+
3213
+ pendingWorkerPreviewFromWs = null;
3214
+
3191
3215
  // console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
3192
3216
  // widgetId: selectedWidgetId,
3193
3217
  // previewUrl: fetchUrl,
@@ -3215,6 +3239,13 @@
3215
3239
  );
3216
3240
  }
3217
3241
 
3242
+ $: if (!isGeneratingCode && pendingWorkerPreviewFromWs) {
3243
+ const pending = pendingWorkerPreviewFromWs;
3244
+ pendingWorkerPreviewFromWs = null;
3245
+ lastWsPreviewSignature = null;
3246
+ void applyWorkerPreviewFromEvent(pending.workerUrl, pending.buildTimeMs, pending.eventTimestamp);
3247
+ }
3248
+
3218
3249
  // Process queued actions
3219
3250
  $: if (queuedActions.length > 0) {
3220
3251
  void processQueuedActions();
@@ -3910,9 +3941,7 @@
3910
3941
 
3911
3942
  <style>
3912
3943
  .widget-details-drag-bar button:focus,
3913
- .widget-details-drag-bar button:focus-visible,
3914
- .widget-details-drag-bar a:focus,
3915
- .widget-details-drag-bar a:focus-visible {
3944
+ .widget-details-drag-bar button:focus-visible {
3916
3945
  outline: none;
3917
3946
  box-shadow: none;
3918
3947
  }
@@ -108,6 +108,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
108
108
  showPublishedArtifact?: (url: string | null) => void;
109
109
  finishCompositionHydration?: () => void;
110
110
  markCodegenPreviewFresh?: () => void;
111
+ clearCodegenPreviewFresh?: () => void;
111
112
  markCodegenStarted?: () => void;
112
113
  hasLiveWorkerPreview?: () => boolean;
113
114
  getLatestRuntimeLogs?: (maxLines?: number) => string[];
@@ -208,6 +209,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
208
209
  showPublishedArtifact: (url: string | null) => void;
209
210
  finishCompositionHydration: () => void;
210
211
  markCodegenPreviewFresh: () => void;
212
+ clearCodegenPreviewFresh: () => void;
211
213
  markCodegenStarted: () => void;
212
214
  hasLiveWorkerPreview: () => boolean;
213
215
  getLatestRuntimeLogs: (maxLines?: number) => string[];
@@ -130,17 +130,36 @@
130
130
  }: Props = $props();
131
131
 
132
132
  const DRAG_BAR_H = 30;
133
- const BAR_SHAPE_GAP = 4;
134
133
  const SHOW_TITLE_MIN_WIDTH = 160;
135
134
  /** Minimum chrome width when expanded so version + Live/Publish controls fit. */
136
135
  const MIN_EXPANDED_CHROME_WIDTH = 220;
136
+ /** Keeps Fabric handles clickable. The strip is inside the shape and outside the iframe. */
137
137
  const IFRAME_INSET_PX = 2;
138
+ /**
139
+ * Negative so the bar covers that inset. Otherwise the strip is a dead hit-zone:
140
+ * the pointer falls through to the canvas and selects the widget underneath.
141
+ */
142
+ const BAR_SHAPE_GAP = -IFRAME_INSET_PX;
138
143
  /** ~5 version rows before the menu scrolls (matches chat history panel). */
139
144
  /** Row height ≈2.1rem (py-1.5 + text) + 0.6rem vertical padding for the menu box. */
140
145
  const VERSION_MENU_ROW_REM = 2.1;
141
146
  const VERSION_MENU_PAD_REM = 0.6;
142
147
  /** Menu shows at most 5 rows before scrolling. */
143
148
  const VERSION_MENU_MAX_ROWS = 5;
149
+ /** Fabric corner handles sit on the shape corners and must stay clickable through the bar. */
150
+ const HANDLE_NOTCH_PX = 14;
151
+
152
+ function headerNotchClip(barWidth: number, shapeWidth: number): string {
153
+ const notch = HANDLE_NOTCH_PX;
154
+ const height = DRAG_BAR_H;
155
+ const y = height - notch;
156
+ const shapeRight = Math.min(barWidth, Math.max(shapeWidth, notch * 2));
157
+ if (shapeRight >= barWidth - 1) {
158
+ return `polygon(0px 0px, ${barWidth}px 0px, ${barWidth}px ${y}px, ${barWidth - notch}px ${y}px, ${barWidth - notch}px ${height}px, ${notch}px ${height}px, ${notch}px ${y}px, 0px ${y}px)`;
159
+ }
160
+ const rightInner = Math.max(notch + 1, shapeRight - notch);
161
+ return `polygon(0px 0px, ${barWidth}px 0px, ${barWidth}px ${height}px, ${shapeRight}px ${height}px, ${shapeRight}px ${y}px, ${rightInner}px ${y}px, ${rightInner}px ${height}px, ${notch}px ${height}px, ${notch}px ${y}px, 0px ${y}px)`;
162
+ }
144
163
 
145
164
  type ShapeRect = {
146
165
  widgetId: string;
@@ -200,6 +219,7 @@
200
219
  const coalescedReloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
201
220
 
202
221
  export function requestReload(widgetId: string): void {
222
+ if (isGenerating) return;
203
223
  const now = Date.now();
204
224
  const sinceLast = now - (lastReloadAtByWidgetId[widgetId] || 0);
205
225
  if (sinceLast < RELOAD_COALESCE_MS) {
@@ -222,12 +242,38 @@
222
242
  lastReloadAtByWidgetId[widgetId] = now;
223
243
  cancelPendingRefresh(widgetId);
224
244
  const token = Math.max(now, (reloadTokens[widgetId] || 0) + 1);
225
- pendingRefreshTokens.set(widgetId, token);
245
+ // Do not set pendingRefreshTokens — that path runs html2canvas (explicit Refresh only).
226
246
  reloadTokens = { ...reloadTokens, [widgetId]: token };
227
247
  iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
228
248
  sync();
229
249
  }
230
250
 
251
+ /** Drop coalesced overlay reloads and pending captures when a generation starts. */
252
+ export function cancelReloads(widgetId?: string): void {
253
+ if (widgetId) {
254
+ const coalesced = coalescedReloadTimers.get(widgetId);
255
+ if (coalesced) {
256
+ clearTimeout(coalesced);
257
+ coalescedReloadTimers.delete(widgetId);
258
+ }
259
+ cancelPendingRefresh(widgetId);
260
+ return;
261
+ }
262
+ for (const timer of coalescedReloadTimers.values()) clearTimeout(timer);
263
+ coalescedReloadTimers.clear();
264
+ for (const id of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(id);
265
+ }
266
+
267
+ export function setSelectedVersion(widgetId: string, versionId: string | null): void {
268
+ if (!widgetId) return;
269
+ if (!versionId) {
270
+ const { [widgetId]: _dropped, ...rest } = localSelectedVersionByWidgetId;
271
+ localSelectedVersionByWidgetId = rest;
272
+ return;
273
+ }
274
+ localSelectedVersionByWidgetId = { ...localSelectedVersionByWidgetId, [widgetId]: versionId };
275
+ }
276
+
231
277
  /** Which shape chrome version menu is open. */
232
278
  let openVersionMenuWidgetId = $state<string | null>(null);
233
279
  /** Local manual version selection override per widget (ensures trigger updates immediately). */
@@ -291,17 +337,32 @@
291
337
  draggingWidgetId = widgetId;
292
338
  let lastX = event.clientX;
293
339
  let lastY = event.clientY;
340
+ let pendingDx = 0;
341
+ let pendingDy = 0;
342
+ let dragFrame = 0;
343
+
344
+ function flushDrag() {
345
+ dragFrame = 0;
346
+ const dx = pendingDx;
347
+ const dy = pendingDy;
348
+ pendingDx = 0;
349
+ pendingDy = 0;
350
+ if (dx === 0 && dy === 0) return;
351
+ onTranslate?.(widgetId, dx, dy);
352
+ }
294
353
 
295
354
  function onMove(e: PointerEvent) {
296
- const dx = e.clientX - lastX;
297
- const dy = e.clientY - lastY;
355
+ pendingDx += e.clientX - lastX;
356
+ pendingDy += e.clientY - lastY;
298
357
  lastX = e.clientX;
299
358
  lastY = e.clientY;
300
- if (dx === 0 && dy === 0) return;
301
- onTranslate?.(widgetId, dx, dy);
359
+ if (dragFrame) return;
360
+ dragFrame = requestAnimationFrame(flushDrag);
302
361
  }
303
362
 
304
363
  function endDrag() {
364
+ if (dragFrame) cancelAnimationFrame(dragFrame);
365
+ flushDrag();
305
366
  draggingWidgetId = null;
306
367
  document.removeEventListener('pointermove', onMove);
307
368
  document.removeEventListener('pointerup', endDrag);
@@ -415,11 +476,24 @@
415
476
  return (option.shortLabel || option.label || fallback).trim();
416
477
  }
417
478
 
479
+ let versionMenuBox = $state<{ widgetId: string; top: number; right: number } | null>(null);
480
+
418
481
  function toggleVersionMenu(event: MouseEvent, widgetId: string) {
419
482
  event.preventDefault();
420
483
  event.stopPropagation();
421
484
  onSelectShape?.(widgetId);
422
- openVersionMenuWidgetId = openVersionMenuWidgetId === widgetId ? null : widgetId;
485
+ if (openVersionMenuWidgetId === widgetId) {
486
+ openVersionMenuWidgetId = null;
487
+ versionMenuBox = null;
488
+ return;
489
+ }
490
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
491
+ versionMenuBox = {
492
+ widgetId,
493
+ top: rect.bottom + 2,
494
+ right: window.innerWidth - rect.right,
495
+ };
496
+ openVersionMenuWidgetId = widgetId;
423
497
  }
424
498
 
425
499
  function selectVersion(event: MouseEvent, widgetId: string, versionId: string) {
@@ -428,7 +502,6 @@
428
502
  openVersionMenuWidgetId = null;
429
503
  if (!versionId) return;
430
504
  onVersionChange?.(widgetId, versionId);
431
- // The parent logs the selected version and resolved commit once the build completes.
432
505
  }
433
506
 
434
507
  function handleSetLiveClick(event: MouseEvent, widgetId: string, versionId: string) {
@@ -586,6 +659,13 @@
586
659
  {@const bodyWidth = overlay.localWidth ?? overlay.width}
587
660
  {@const bodyHeight = overlay.localHeight ?? overlay.height}
588
661
  {@const bodyMatrix = overlay.matrix ?? [1, 0, 0, 1, overlay.left, overlay.top]}
662
+ {@const frameScaleX = Math.hypot(bodyMatrix[0], bodyMatrix[1]) || 1}
663
+ {@const frameScaleY = Math.hypot(bodyMatrix[2], bodyMatrix[3]) || 1}
664
+ {@const frameRotation = Math.atan2(bodyMatrix[1], bodyMatrix[0])}
665
+ {@const frameCos = Math.cos(frameRotation)}
666
+ {@const frameSin = Math.sin(frameRotation)}
667
+ {@const frameWidth = Math.max(1, bodyWidth * frameScaleX)}
668
+ {@const frameHeight = Math.max(1, bodyHeight * frameScaleY)}
589
669
  {@const showExpandedChrome = isSelected || overlay.width >= SHOW_TITLE_MIN_WIDTH}
590
670
  {@const chromeWidth = showExpandedChrome
591
671
  ? Math.max(overlay.width, MIN_EXPANDED_CHROME_WIDTH)
@@ -621,10 +701,16 @@
621
701
  <div
622
702
  class="widget-shape-drag-bar pointer-events-auto absolute left-0 right-0 top-0 z-10 flex items-center gap-1 rounded-t-md border border-b-0 px-1.5 shadow-sm select-none [&_button:focus]:outline-none [&_button:focus]:shadow-none [&_button:focus-visible]:outline-none [&_button:focus-visible]:shadow-none
623
703
  {isPrimary ? 'bg-red-100' : 'bg-white'} {isSelected ? 'border-blue-400' : 'border-gray-300'}"
624
- style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px;"
704
+ style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px; clip-path: {headerNotchClip(chromeWidth, overlay.width)};"
625
705
  title="Drag widget"
626
706
  onpointerdown={(e) => startShapeDrag(e, overlay.widgetId)}
627
707
  >
708
+ {#if isPrimary}
709
+ <div
710
+ class="widget-shape-primary-stroke pointer-events-none absolute z-20 bg-[#DC2626]"
711
+ style="left: 0; width: {overlay.width}px; top: {DRAG_BAR_H - IFRAME_INSET_PX - 1}px; height: 2px; clip-path: inset(0 {HANDLE_NOTCH_PX}px 0 {HANDLE_NOTCH_PX}px);"
712
+ ></div>
713
+ {/if}
628
714
  <span class="widget-shape-drag-grip shrink-0 text-xs leading-none text-gray-400" aria-hidden="true">⠿</span>
629
715
 
630
716
  {#if showExpandedChrome}
@@ -733,42 +819,6 @@
733
819
  </HoverTooltip>
734
820
  {/if}
735
821
  {/if}
736
- {#if versionMenuOpen}
737
- {@const hasScroll = versionOptions.length > 5}
738
- <div
739
- class="widget-shape-version-menu absolute top-full z-30 mt-0.5 w-max rounded-md border border-emerald-200 bg-white p-1 shadow-lg"
740
- style="right: -12px;"
741
- role="listbox"
742
- >
743
- <div
744
- class="widget-shape-version-scroll flex flex-col gap-1 overflow-x-hidden {hasScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
745
- style="{hasScroll ? 'max-height: 185px; margin-right: -4px; padding-right: 4px;' : 'max-height: none;'} scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
746
- >
747
- {#each versionOptions as option (option.id)}
748
- {@const optionVersion = Number(option.id.slice(2))}
749
- {@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
750
- {@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
751
- || !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
752
- || (selectedVersion?.id === option.id && selectedPreviewBlocked)}
753
- {@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
754
- <VersionDropdownRow
755
- versionLabel={option.shortLabel || option.label}
756
- dateLabel={option.dateLabel}
757
- isLive={option.isLive === true}
758
- isDraft={isDraftVersion(option)}
759
- selected={selectedVersion?.id === option.id}
760
- busy={optionBusy}
761
- publishDisabled={optionPublishDisabled}
762
- publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
763
- setLiveDisabled={settingLiveVersion === optionVersion}
764
- onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
765
- onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
766
- onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
767
- />
768
- {/each}
769
- </div>
770
- </div>
771
- {/if}
772
822
  </div>
773
823
  {:else if versionTriggerText && versionTriggerText !== '—'}
774
824
  <span
@@ -829,7 +879,7 @@
829
879
  <div
830
880
  class="widget-shape-live-preview-frame pointer-events-none fixed left-0 top-0 origin-top-left overflow-visible"
831
881
  data-widget-id={overlay.widgetId}
832
- style="height: {bodyHeight}px; width: {bodyWidth}px; transform: matrix({bodyMatrix.join(',')}); z-index: {isSelected ? 60 : 1};"
882
+ style="height: {frameHeight}px; width: {frameWidth}px; transform: matrix({frameCos}, {frameSin}, {-frameSin}, {frameCos}, {bodyMatrix[4]}, {bodyMatrix[5]}); z-index: {isSelected ? 60 : 1};"
833
883
  >
834
884
  <!-- A new reload gets its own window so old load/screenshot replies cannot win. -->
835
885
  {#if overlay.src}
@@ -880,6 +930,43 @@
880
930
  ></div>
881
931
  {/if}
882
932
  </div>
933
+ {#if versionMenuOpen && versionMenuBox?.widgetId === overlay.widgetId}
934
+ {@const hasScroll = versionOptions.length > 5}
935
+ <div
936
+ class="widget-shape-version-menu pointer-events-auto fixed z-[400] w-max rounded-md border border-emerald-200 bg-white p-1 shadow-lg"
937
+ data-version-menu-for={overlay.widgetId}
938
+ style="top: {versionMenuBox.top}px; right: {versionMenuBox.right}px;"
939
+ role="listbox"
940
+ >
941
+ <div
942
+ class="widget-shape-version-scroll flex flex-col gap-1 overflow-x-hidden {hasScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
943
+ style="{hasScroll ? 'max-height: 185px; margin-right: -4px; padding-right: 4px;' : 'max-height: none;'} scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
944
+ >
945
+ {#each versionOptions as option (option.id)}
946
+ {@const optionVersion = Number(option.id.slice(2))}
947
+ {@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
948
+ {@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
949
+ || !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
950
+ || (selectedVersion?.id === option.id && selectedPreviewBlocked)}
951
+ {@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
952
+ <VersionDropdownRow
953
+ versionLabel={option.shortLabel || option.label}
954
+ dateLabel={option.dateLabel}
955
+ isLive={option.isLive === true}
956
+ isDraft={isDraftVersion(option)}
957
+ selected={selectedVersion?.id === option.id}
958
+ busy={optionBusy}
959
+ publishDisabled={optionPublishDisabled}
960
+ publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
961
+ setLiveDisabled={settingLiveVersion === optionVersion}
962
+ onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
963
+ onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
964
+ onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
965
+ />
966
+ {/each}
967
+ </div>
968
+ </div>
969
+ {/if}
883
970
  </div>
884
971
  {/each}
885
972
  </div>
@@ -95,6 +95,8 @@ interface Props {
95
95
  }
96
96
  declare const WidgetShapeLivePreviews: import("svelte").Component<Props, {
97
97
  requestReload: (widgetId: string) => void;
98
+ cancelReloads: (widgetId?: string) => void;
99
+ setSelectedVersion: (widgetId: string, versionId: string | null) => void;
98
100
  sync: () => void;
99
101
  }, "">;
100
102
  type WidgetShapeLivePreviews = ReturnType<typeof WidgetShapeLivePreviews>;
@@ -24,7 +24,7 @@ export interface CodeGenerationStatus {
24
24
  error: string | null;
25
25
  timestamp: string | null;
26
26
  }
27
- /** Agent harness repair lines pushed during codegen (vmConsole:aiderOutput). */
27
+ /** Agent harness repair lines pushed during codegen (codegen:output). */
28
28
  export interface AgentHarnessOutput {
29
29
  output: string;
30
30
  timestamp: string;
@@ -222,7 +222,7 @@ function setupSocketListeners(sock) {
222
222
  // =========================================================================
223
223
  // Dynamic Worker Preview
224
224
  // =========================================================================
225
- sock.on('vmConsole:aiderOutput', (data) => {
225
+ sock.on('codegen:output', (data) => {
226
226
  const output = typeof data?.output === 'string' ? data.output.trim() : '';
227
227
  if (!output)
228
228
  return;
@@ -29,7 +29,7 @@ let reported = false;
29
29
  let lastStatus = '';
30
30
  let breadcrumbs = [];
31
31
  const PING_MS = 500;
32
- const STALL_MS = 4000;
32
+ const STALL_MS = 1500;
33
33
 
34
34
  function describeBreadcrumbs() {
35
35
  if (breadcrumbs.length === 0) return 'no instrumented task over 200ms — the block is somewhere not yet measured';
@@ -45,12 +45,16 @@ setInterval(() => {
45
45
  if (!freezeStartedAt) freezeStartedAt = now - STALL_MS;
46
46
  if (!reported) {
47
47
  reported = true;
48
- console.warn('[FreezeReport] Main thread unresponsive ' + Math.round(now - freezeStartedAt) +
49
- 'ms. Last status: "' + (lastStatus || '—') + '"');
48
+ const msg = '[FreezeReport] Main thread unresponsive ' + Math.round(now - freezeStartedAt) +
49
+ 'ms. Last status: "' + (lastStatus || '—') + '". Select the top page in the console frame picker (not the widget iframe).';
50
+ console.warn(msg);
51
+ postMessage({ type: 'freeze-report', phase: 'start', message: msg });
50
52
  }
51
53
  } else if (freezeStartedAt) {
52
- console.warn('[FreezeReport] Main thread recovered after ' + Math.round(now - freezeStartedAt) +
53
- 'ms. Last status: "' + (lastStatus || '—') + '". Blocking work: ' + describeBreadcrumbs());
54
+ const msg = '[FreezeReport] Main thread recovered after ' + Math.round(now - freezeStartedAt) +
55
+ 'ms. Last status: "' + (lastStatus || '—') + '". Blocking work: ' + describeBreadcrumbs();
56
+ console.warn(msg);
57
+ postMessage({ type: 'freeze-report', phase: 'recovery', message: msg });
54
58
  freezeStartedAt = null;
55
59
  reported = false;
56
60
  }
@@ -128,11 +132,38 @@ export function installFreezeWatchdog() {
128
132
  URL.revokeObjectURL(url);
129
133
  let lastStatus = '';
130
134
  let pendingBreadcrumbs = [];
135
+ let lastLongTaskSentAt = performance.now();
136
+ function longTasksSince(since) {
137
+ if (typeof performance === 'undefined' || typeof performance.getEntriesByType !== 'function')
138
+ return [];
139
+ const found = [];
140
+ for (const entry of performance.getEntriesByType('longtask')) {
141
+ const endedAt = entry.startTime + entry.duration;
142
+ if (endedAt < since || entry.duration < BREADCRUMB_MIN_MS)
143
+ continue;
144
+ const attribution = entry.attribution?.[0];
145
+ const source = attribution?.containerSrc || attribution?.containerName || attribution?.containerType;
146
+ found.push({ label: 'longtask', duration: entry.duration, source });
147
+ }
148
+ return found;
149
+ }
131
150
  worker.onmessage = (event) => {
132
- if (event.data?.type === 'ping') {
133
- worker.postMessage({ type: 'pong', status: lastStatus, breadcrumbs: pendingBreadcrumbs });
151
+ const data = event.data;
152
+ if (data?.type === 'ping') {
153
+ const breadcrumbs = pendingBreadcrumbs.concat(longTasksSince(lastLongTaskSentAt));
154
+ lastLongTaskSentAt = performance.now();
155
+ worker.postMessage({ type: 'pong', status: lastStatus, breadcrumbs });
134
156
  pendingBreadcrumbs = [];
135
157
  }
158
+ else if (data?.type === 'freeze-report' && data.message) {
159
+ console.warn(data.message);
160
+ try {
161
+ window.top?.console.warn(data.message);
162
+ }
163
+ catch {
164
+ /* cross-origin embed */
165
+ }
166
+ }
136
167
  };
137
168
  let stopLongTaskObserver = null;
138
169
  const watchdog = {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.67",
3
+ "version": "0.3.68",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",
7
7
  "@widgetic/api-sdk": "^1.0.10",
8
8
  "@widgetic/cache-layer": "^0.1.2",
9
9
  "@widgetic/canvas": "^0.5.4",
10
- "@widgetic/chat": "^0.1.4",
10
+ "@widgetic/chat": "^0.1.13",
11
11
  "@widgetic/design-system": "^0.5.8",
12
12
  "@widgetic/editor": "^4.0.1",
13
13
  "@widgetic/file-browser": "^0.5.17",
@@ -43,9 +43,8 @@
43
43
  "dev": "npm run stop-dev && npm run dev-sv",
44
44
  "predev-sv": "node scripts/suppress-postcss-from-warning.js",
45
45
  "dev-sv": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") VITE_BUILD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo local) NODE_ENV=development vite dev --port 5174",
46
- "lps": "volta run npm link --save @widgetic/design-system && volta run npm link --save @widgetic/api-sdk && volta run npm link --save @widgetic/canvas",
46
+ "lps": "volta run npm link --save @widgetic/design-system && volta run npm link --save @widgetic/api-sdk && volta run npm link --save @widgetic/canvas && volta run npm link --save @widgetic/chat",
47
47
  "llinks": "ls -la node_modules/@widgetic",
48
- "ldt": "volta run npm link --save @widgetic/dev-tools",
49
48
  "clean-npm-cache": "npm cache clean --force",
50
49
  "clean-svelte-cache": "rm -rf .svelte-kit && npx svelte-kit sync",
51
50
  "clean-vite-cache": "rm -rf node_modules/.vite",
@@ -72,7 +71,7 @@
72
71
  "@widgetic/api-sdk": "^1.0.10",
73
72
  "@widgetic/cache-layer": "^0.1.2",
74
73
  "@widgetic/canvas": "^0.5.4",
75
- "@widgetic/chat": "^0.1.4",
74
+ "@widgetic/chat": "^0.1.13",
76
75
  "@widgetic/design-system": "^0.5.8",
77
76
  "@widgetic/dev-tools": "^0.2.0",
78
77
  "@widgetic/editor": "^4.0.1",