@widgetic/creator 0.3.64 → 0.3.66

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.
@@ -27,7 +27,12 @@
27
27
  UploadsApi,
28
28
  ResponseError
29
29
  } from '@widgetic/api-sdk';
30
- import type { Middleware } from '@widgetic/api-sdk';
30
+ import type {
31
+ Middleware,
32
+ UpdateComposition,
33
+ GenerateCodeRequest,
34
+ AgentModelsGet200ResponseModelsInner
35
+ } from '@widgetic/api-sdk';
31
36
  import { parseVersionPreviewError } from './utils/versionPreviewError.js';
32
37
 
33
38
  /**
@@ -70,6 +75,8 @@
70
75
  import { uploadChatFileToWidgeticApi } from './utils/widgeticChatUpload.js';
71
76
  import { persistWidgetThumbnail } from './utils/persistWidgetThumbnail.js';
72
77
  import { nativeScrollbarPadding } from './utils/nativeScrollbarPadding.js';
78
+ import { pinScrollToBottom } from './utils/pinScrollToBottom.js';
79
+ import { installFreezeWatchdog, measureSync } from './utils/freezeWatchdog.js';
73
80
  import { streamOperationUpdates } from './utils/operationStream.js';
74
81
  import { generateEmbedCode, EMBED_FORMATS, EMBED_FORMAT_LABELS, type EmbedFormat } from './utils/embedCode.js';
75
82
 
@@ -111,7 +118,9 @@
111
118
  /** Opt-in Dexie L1 — host (site / standalone) passes true. */
112
119
  export let enableLocalCache: CreatorAppProps['enableLocalCache'] = false;
113
120
  export let onPublishComplete: CreatorAppProps['onPublishComplete'] = undefined;
114
- export let onBack: CreatorAppProps['onBack'] = undefined;
121
+ // Passed by the site host but never read here — export const keeps the
122
+ // init-set contract without the unused-reactive warning.
123
+ export const onBack: CreatorAppProps['onBack'] = undefined;
115
124
  export let onWidgetRenamed: CreatorAppProps['onWidgetRenamed'] = undefined;
116
125
 
117
126
  // Opt-in Dexie owner — reactive to prop changes without $effect (would force runes mode).
@@ -119,7 +128,8 @@
119
128
 
120
129
  function getRealTokenExpUnix(token: string): number | null {
121
130
  const decoded = decodeJWT(token);
122
- return decoded?.exp ?? null;
131
+ const exp = decoded?.exp;
132
+ return typeof exp === 'number' && Number.isFinite(exp) ? exp : null;
123
133
  }
124
134
 
125
135
  let didForceCreateNewWidget = false;
@@ -483,7 +493,10 @@
483
493
 
484
494
  // Lightweight static imports from canvas (excludes Canvas.svelte — 10K+ lines)
485
495
  import { Tooltip } from '@widgetic/canvas/components';
486
- import type { WidgetConversionPayload } from '@widgetic/canvas/components';
496
+ // WidgetConversionPayload lives on the canvas root export (re-exported from
497
+ // ./canvas); the components barrel only carries the input controls. Type-only
498
+ // import, so the 10K-line Canvas.svelte still never loads.
499
+ import type { WidgetConversionPayload } from '@widgetic/canvas';
487
500
 
488
501
  // Chat module for code generation conversation UI
489
502
  import { Chat, ContextType } from '@widgetic/chat';
@@ -518,8 +531,40 @@
518
531
  const canvasModule = await canvasModulePromise;
519
532
  WidgeticCanvas = canvasModule.Canvas;
520
533
  }
534
+ installLongTaskFreezeReporter();
535
+ installFreezeWatchdog();
521
536
  });
522
537
 
538
+ /**
539
+ * Freeze attribution: while a code generation is tracked, any main-thread
540
+ * block ≥ LONG_TASK_FREEZE_MS is reported with its most likely culprit (the
541
+ * last status line + open panels). This gives a concrete answer to "why does
542
+ * the site freeze during generation" instead of a silent stuck progress bar.
543
+ */
544
+ const LONG_TASK_FREEZE_MS = 250;
545
+ let freezeObserver: PerformanceObserver | null = null;
546
+ function installLongTaskFreezeReporter(): void {
547
+ if (typeof window === 'undefined' || !('PerformanceObserver' in window)) return;
548
+ if (freezeObserver) return;
549
+ try {
550
+ freezeObserver = new PerformanceObserver((list) => {
551
+ if (!Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id])) return;
552
+ for (const entry of list.getEntries()) {
553
+ if (entry.duration < LONG_TASK_FREEZE_MS) continue;
554
+ console.warn(
555
+ `[FreezeReport] Main thread blocked ${Math.round(entry.duration)}ms during generation.`
556
+ + ` Status: "${generateCodeStatus ?? '—'}".`
557
+ + ` Open panels: ${openWidgetPanelIds.length}.`
558
+ + ` Start: ${Math.round(entry.startTime)}ms after nav.`,
559
+ );
560
+ }
561
+ });
562
+ freezeObserver.observe({ entryTypes: ['longtask'] });
563
+ } catch (err) {
564
+ console.warn('[FreezeReport] longtask observer unavailable', err);
565
+ }
566
+ }
567
+
523
568
  // Trigger WidgetDetails load as soon as a panel is opened or requested
524
569
  $: if (openWidgetPanelIds.length > 0 && !WidgetDetails) {
525
570
  void loadWidgetDetailsModule();
@@ -601,10 +646,31 @@
601
646
  repositoryId?: string | null;
602
647
  repository_id?: string | null;
603
648
  repoId?: string | null;
649
+ repo_id?: string | null;
604
650
  name?: string;
605
651
  [extra: string]: unknown;
606
652
  }
607
653
 
654
+ /**
655
+ * View any API payload as the app's open WidgetSummary shape. The SDK's
656
+ * generated models (Widget, CreateWidget201Response, …) lack the index
657
+ * signature callers rely on for snake_case/extra fields, so direct
658
+ * assignments fail structurally; this documents the deliberate widening.
659
+ */
660
+ function asWidgetSummary(payload: unknown): WidgetSummary {
661
+ return (payload ?? {}) as WidgetSummary;
662
+ }
663
+
664
+ /**
665
+ * Reactivity marker for template {@const}s: a Svelte 4 {@const} only re-runs
666
+ * when a value referenced in its expression changes, so the panel-revision
667
+ * counter is passed explicitly. The bare comma idiom this replaces
668
+ * (`(revision, value)`) is a TS unused-expression error.
669
+ */
670
+ function rev<T>(revision: number, value: T, ..._alsoTracked: unknown[]): T {
671
+ return value;
672
+ }
673
+
608
674
  // LocalStorage keys for persisting selection across refresh
609
675
  const SELECTED_WIDGET_STORAGE_KEY = 'widgetCreator_selectedWidgetId';
610
676
  const SELECTED_CANVAS_STORAGE_KEY = 'widgetCreator_selectedCanvasId';
@@ -797,6 +863,9 @@
797
863
  let codeGenerationPrompt = DEFAULT_PROMPT;
798
864
  let isGeneratingCode = false;
799
865
  let generatingWidgetIds: Record<string, boolean> = {};
866
+ function isAnyWidgetGenerating(): boolean {
867
+ return isGeneratingCode || Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id]);
868
+ }
800
869
  const widgetAbortControllers = new Map<string, AbortController>();
801
870
  let currentGenerationOperationId: string | null = null;
802
871
  let generateCodeStatus: string | null = null;
@@ -831,7 +900,7 @@
831
900
  // Chat component reference and context (focused panel)
832
901
  let chatMethods: {
833
902
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
834
- addAssistantMessage: (content: string, skipPersist?: boolean) => void;
903
+ addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: string[]) => void;
835
904
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
836
905
  getConversationId: () => string | null;
837
906
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -967,6 +1036,7 @@
967
1036
  fileSize?: number;
968
1037
  }>,
969
1038
  sourceWidgetId?: string,
1039
+ sendOptions?: { skipIntentGate?: boolean },
970
1040
  ) {
971
1041
  const widgetId = sourceWidgetId ?? selectedWidgetId;
972
1042
  if (!widgetId) return;
@@ -1089,6 +1159,7 @@
1089
1159
  attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
1090
1160
  promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
1091
1161
  persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
1162
+ skipIntentGate: sendOptions?.skipIntentGate === true,
1092
1163
  });
1093
1164
  }
1094
1165
 
@@ -1453,10 +1524,12 @@
1453
1524
  : null) || userMessages[userMessages.length - 1] || null;
1454
1525
  const existingUrls = new Set(
1455
1526
  (targetUser?.attachments || [])
1456
- .map((a: { url?: string }) => a.url)
1457
- .filter((url: string | undefined): url is string => !!url),
1527
+ .map((a: { url?: unknown }) => a.url)
1528
+ .filter((url): url is string => typeof url === 'string' && url.length > 0),
1529
+ );
1530
+ const existingHasRemoteImage = [...existingUrls].some(
1531
+ (url) => typeof url === 'string' && /^https?:\/\//i.test(url)
1458
1532
  );
1459
- const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
1460
1533
  // Chat already persisted CDN attachments for this turn — do not merge data URLs/canvas shots.
1461
1534
  if (targetUser && existingHasRemoteImage) {
1462
1535
  if (convId !== visibleConversationId) {
@@ -1578,6 +1651,9 @@
1578
1651
  iframe: HTMLIFrameElement,
1579
1652
  timeoutMs = 6000,
1580
1653
  ): Promise<{ data: string | null; error: string | null }> {
1654
+ if (isAnyWidgetGenerating()) {
1655
+ return Promise.resolve({ data: null, error: 'generation in progress' });
1656
+ }
1581
1657
  return new Promise((resolve) => {
1582
1658
  if (!iframe.contentWindow) {
1583
1659
  resolve({ data: null, error: 'no-content-window' });
@@ -1609,6 +1685,14 @@
1609
1685
  * Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
1610
1686
  */
1611
1687
  async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
1688
+ // Loading a widget in a hidden iframe costs a full Svelte + Tailwind CDN
1689
+ // startup on the main thread this page shares with it, and the capture that
1690
+ // follows is refused during a generation anyway (`requestIframeScreenshot`),
1691
+ // so the whole path is skipped rather than paying the load for nothing.
1692
+ if (isAnyWidgetGenerating()) {
1693
+ console.log('[Chat→Screenshot] Skip hidden Worker capture — codegen in progress', widgetId?.substring(0, 8));
1694
+ return null;
1695
+ }
1612
1696
  const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
1613
1697
  console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
1614
1698
  widgetId: widgetId.substring(0, 8),
@@ -1657,25 +1741,29 @@
1657
1741
 
1658
1742
  /** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
1659
1743
  async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
1744
+ if (isAnyWidgetGenerating()) return;
1660
1745
  const capturedCanvasId = getEffectiveCanvasId();
1661
1746
  const capturedCanvas = canvasRef;
1662
1747
  const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
1663
1748
  // Frame/intent identity owns this capture, not focused-panel HEAD hydration
1664
1749
  // or another widget's global generation operation.
1665
1750
  const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
1666
- && !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId]
1751
+ && !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !isAnyWidgetGenerating()
1667
1752
  && capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
1668
1753
  && capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
1669
1754
  && resolveLiveWidgetShapeIframe(widgetId) === iframe;
1670
1755
  if (!isCurrent()) return;
1671
1756
  try {
1672
- const screenshot = await handleChatScreenshot(widgetId, iframe);
1757
+ const screenshot = await handleChatScreenshot(widgetId, iframe, true);
1673
1758
  if (!screenshot || !isCurrent()) return;
1674
1759
  // Persist before Fabric/autosave side effects can resync the live overlay.
1675
1760
  await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
1676
1761
  if (!isCurrent()) return;
1677
1762
  capturedCanvas?.updateWidgetImage(widgetId, screenshot);
1678
- const canvasJson = capturedCanvas?.saveCanvas?.();
1763
+ const canvasJson = measureSync(
1764
+ 'canvas-serialize:shape-refresh-thumbnail',
1765
+ () => capturedCanvas?.saveCanvas?.() ?? null,
1766
+ );
1679
1767
  if (canvasJson) void handleCanvasAutoSave(canvasJson);
1680
1768
  } catch (error) {
1681
1769
  console.warn('[WidgetShape] Refresh thumbnail capture failed', widgetId, error);
@@ -1683,13 +1771,34 @@
1683
1771
  }
1684
1772
 
1685
1773
  /** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
1686
- async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null): Promise<string | null> {
1774
+ async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null, quiet = false): Promise<string | null> {
1687
1775
  const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
1688
1776
  if (!targetId) {
1689
1777
  console.warn('[Chat→Screenshot] No widget id');
1690
1778
  return null;
1691
1779
  }
1780
+ // Never run background / refresh screenshot captures while code generation is running —
1781
+ // html2canvas inside any same-origin iframe freezes the browser main thread.
1782
+ if ((quiet || requiredIframe != null) && isAnyWidgetGenerating()) {
1783
+ console.log('[Chat→Screenshot] Skip capture — generation in progress');
1784
+ return null;
1785
+ }
1786
+ // Same single-flight as handleRefreshScreenshot: both paths run html2canvas
1787
+ // inside preview iframes that share the page main thread. Overlapping
1788
+ // captures during generation were a major freeze source.
1789
+ if (screenshotCaptureInFlight.has(targetId)) {
1790
+ console.log('[Chat→Screenshot] Capture already in flight — skipping', targetId.substring(0, 8));
1791
+ return null;
1792
+ }
1793
+ screenshotCaptureInFlight.add(targetId);
1794
+ try {
1795
+ return await performChatScreenshot(targetId, requiredIframe, quiet);
1796
+ } finally {
1797
+ screenshotCaptureInFlight.delete(targetId);
1798
+ }
1799
+ }
1692
1800
 
1801
+ async function performChatScreenshot(targetId: string, requiredIframe: HTMLIFrameElement | null, quiet = false): Promise<string | null> {
1693
1802
  const liveIframe = requiredIframe ?? resolveLiveWidgetShapeIframe(targetId);
1694
1803
  if (requiredIframe && (requiredIframe !== resolveLiveWidgetShapeIframe(targetId) || !requiredIframe.isConnected)) return null;
1695
1804
  const liveSrc = liveIframe
@@ -1712,6 +1821,8 @@
1712
1821
 
1713
1822
  // Explicit refresh must not persist a different version or a stale Fabric image.
1714
1823
  if (requiredIframe) return null;
1824
+ // Quiet thumbnail sync must NEVER construct a temporary hidden iframe and compile worker preview.
1825
+ if (quiet) return null;
1715
1826
 
1716
1827
  // 2) Hidden Worker /preview (same capture script; wait for load)
1717
1828
  const fromWorker = await captureViaHiddenWorkerPreview(targetId);
@@ -1737,7 +1848,6 @@
1737
1848
  });
1738
1849
  return null;
1739
1850
  }
1740
-
1741
1851
  function normalizeConversationRecords(json: any): any[] {
1742
1852
  const raw = json?.data ?? json?.conversations ?? json;
1743
1853
  const list = Array.isArray(raw)
@@ -2416,8 +2526,14 @@
2416
2526
 
2417
2527
  /** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
2418
2528
  const CHAT_DEFAULT_HEIGHT = 400;
2419
- const CHAT_MIN_HEIGHT = 400;
2529
+ const CHAT_MIN_HEIGHT = 260;
2420
2530
  const CHAT_MAX_HEIGHT = 2000;
2531
+ /**
2532
+ * Floor applied to the chat box while it auto-fills the create step. A floor
2533
+ * taller than the available area makes the surrounding scroll container
2534
+ * scroll, which puts the chat input below the fold on open.
2535
+ */
2536
+ const CHAT_AUTO_MIN_HEIGHT = 0;
2421
2537
 
2422
2538
  function getChatContainerHeightForPanel(widgetId: string): number | null {
2423
2539
  const stored = getPanelState(widgetId).chatContainerHeight;
@@ -2502,7 +2618,7 @@
2502
2618
  if (!schema) return schema;
2503
2619
  try {
2504
2620
  const clone = structuredClone(schema);
2505
- const groups = Array.isArray(clone) ? clone : clone.componentGroups;
2621
+ const groups = Array.isArray(clone) ? clone : (clone as { componentGroups?: unknown }).componentGroups;
2506
2622
  if (!Array.isArray(groups) || groups.length === 0) return schema;
2507
2623
  const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
2508
2624
  (group.components || []).flatMap((component) => component.properties || [])
@@ -3594,7 +3710,7 @@
3594
3710
  !compositionRecordProjectId(selectedComposition as Record<string, unknown>);
3595
3711
  const res = await compositionsClient.updateComposition({
3596
3712
  id: selectedComposition.id,
3597
- updateCompositionRequest: {
3713
+ updateComposition: {
3598
3714
  content: buildCompositionContentPayload(),
3599
3715
  ...(needsProjectBackfill ? projectBackfill : {}),
3600
3716
  }
@@ -3686,7 +3802,7 @@
3686
3802
  try {
3687
3803
  await compositionsClient.updateComposition({
3688
3804
  id: compositionId,
3689
- updateCompositionRequest: { name: trimmed }
3805
+ updateComposition: { name: trimmed }
3690
3806
  });
3691
3807
  showToast('success', 'Composition renamed');
3692
3808
  await loadCompositions();
@@ -3747,7 +3863,9 @@
3747
3863
  };
3748
3864
  await compositionsClient.updateComposition({
3749
3865
  id: selectedComposition.id,
3750
- updateCompositionRequest: { embedSettings }
3866
+ // The gateway accepts embedSettings (compositionSchemas update schema);
3867
+ // the generated UpdateComposition model predates that field.
3868
+ updateComposition: { embedSettings } as unknown as UpdateComposition
3751
3869
  });
3752
3870
  selectedComposition = { ...selectedComposition, embedSettings };
3753
3871
  embedDimensionsChanged = false;
@@ -3943,11 +4061,11 @@
3943
4061
  // SDK returns { success, models } directly (not nested under data)
3944
4062
  if (response.success && Array.isArray(response.models)) {
3945
4063
  // Transform API response to match our interface
3946
- availableModels = response.models.map((m: { id: string; displayName: string; provider: string; description: string; supportsVision?: boolean }) => ({
3947
- id: m.id,
3948
- name: m.displayName,
3949
- provider: m.provider.charAt(0).toUpperCase() + m.provider.slice(1), // Capitalize provider
3950
- description: m.description,
4064
+ availableModels = response.models.map((m: AgentModelsGet200ResponseModelsInner) => ({
4065
+ id: m.id ?? '',
4066
+ name: m.displayName ?? '',
4067
+ provider: (m.provider ?? '').charAt(0).toUpperCase() + (m.provider ?? '').slice(1), // Capitalize provider
4068
+ description: m.description ?? '',
3951
4069
  supportsVision: m.supportsVision === true,
3952
4070
  }));
3953
4071
 
@@ -4687,7 +4805,12 @@
4687
4805
 
4688
4806
  // Step 3: Check operation status
4689
4807
  try {
4690
- const op = await operationsClient.operationsIdGet({ id: operationId });
4808
+ const op = (await operationsClient.operationsIdGet({ id: operationId })) as {
4809
+ status?: string;
4810
+ error?: string;
4811
+ progress?: number;
4812
+ metadata?: Record<string, unknown>;
4813
+ };
4691
4814
  const status = (op?.status || '').toLowerCase();
4692
4815
 
4693
4816
  if (status === 'completed') {
@@ -5635,22 +5758,29 @@
5635
5758
  const res = await widgetsClient.getWidgets({ canvasId: canvasId ?? undefined });
5636
5759
 
5637
5760
  // normalize error shape from apiRequest
5638
- if (res && (res.error || res.message)) {
5761
+ const envelope = res as unknown as {
5762
+ error?: string;
5763
+ message?: string;
5764
+ data?: unknown;
5765
+ };
5766
+ if (envelope && (envelope.error || envelope.message)) {
5639
5767
  // console.error('Widget Creator: loadWidgetsForCanvas: response not ok:', res);
5640
5768
  widgets = [];
5641
5769
  selectedWidgetId = null;
5642
5770
  isLoadingWidgets = false;
5643
- widgetsError = res.message || res.error || 'Failed to load widgets';
5771
+ widgetsError = envelope.message || envelope.error || 'Failed to load widgets';
5644
5772
  throw new Error(widgetsError);
5645
5773
  }
5646
5774
 
5647
- // if the response is ok, parse the data
5775
+ // if the response is ok, parse the data (bare list, {data: list} or {data: {data: list}})
5776
+ const envelopeData = envelope?.data;
5777
+ const envelopeNestedData = (envelopeData as { data?: unknown } | undefined)?.data;
5648
5778
  const fetched = Array.isArray(res)
5649
- ? (res as WidgetSummary[])
5650
- : Array.isArray(res?.data?.data)
5651
- ? (res.data.data as WidgetSummary[])
5652
- : Array.isArray(res?.data)
5653
- ? (res.data as WidgetSummary[])
5779
+ ? (res as unknown as WidgetSummary[])
5780
+ : Array.isArray(envelopeNestedData)
5781
+ ? (envelopeNestedData as unknown as WidgetSummary[])
5782
+ : Array.isArray(envelopeData)
5783
+ ? (envelopeData as unknown as WidgetSummary[])
5654
5784
  : [];
5655
5785
 
5656
5786
  const previousById = new Map(
@@ -5747,9 +5877,13 @@
5747
5877
  }
5748
5878
 
5749
5879
  function getWidgetCreatedAtMs(widget: WidgetSummary): number {
5750
- const raw = widget.createdAt ?? widget.created_at;
5751
- if (!raw) return 0;
5752
- const ms = new Date(raw).getTime();
5880
+ const rawCreated = widget.createdAt ?? widget.created_at;
5881
+ if (!rawCreated) return 0;
5882
+ const ms = new Date(
5883
+ typeof rawCreated === 'string' || typeof rawCreated === 'number' || rawCreated instanceof Date
5884
+ ? rawCreated
5885
+ : String(rawCreated)
5886
+ ).getTime();
5753
5887
  return Number.isFinite(ms) ? ms : 0;
5754
5888
  }
5755
5889
 
@@ -6155,10 +6289,10 @@
6155
6289
  async function resolvePrimaryWidgetIdForCanvas(targetCanvasId: string): Promise<string | null> {
6156
6290
  if (!canvasesClient || !targetCanvasId?.trim()) return null;
6157
6291
  try {
6158
- const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
6159
- const record =
6160
- (canvasRecord as { data?: Record<string, unknown> })?.data ??
6161
- (canvasRecord as Record<string, unknown>);
6292
+ const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
6293
+ const record =
6294
+ (canvasRecord as { data?: Record<string, unknown> })?.data ??
6295
+ (canvasRecord as unknown as Record<string, unknown>);
6162
6296
  const raw =
6163
6297
  (record as { primaryWidgetId?: unknown })?.primaryWidgetId ??
6164
6298
  (record as { primary_widget_id?: unknown })?.primary_widget_id ??
@@ -6640,7 +6774,12 @@
6640
6774
  if (!widgetId || !agentClient) return null;
6641
6775
  try {
6642
6776
  const json = await agentClient.agentWidgetBuilderRepoHeadWidgetIdGet({ widgetId });
6643
- const data = json?.data ?? json;
6777
+ const repoHeadEnvelope = json as unknown as {
6778
+ data?: { repoHeadCommitSha?: string | null; repo_head_commit_sha?: string | null };
6779
+ repoHeadCommitSha?: string | null;
6780
+ repo_head_commit_sha?: string | null;
6781
+ };
6782
+ const data = repoHeadEnvelope?.data ?? repoHeadEnvelope;
6644
6783
  const sha = data?.repoHeadCommitSha ?? data?.repo_head_commit_sha ?? null;
6645
6784
  return typeof sha === 'string' && sha.length > 0 ? sha : null;
6646
6785
  } catch (err) {
@@ -6958,13 +7097,14 @@
6958
7097
  }
6959
7098
  }
6960
7099
  );
6961
- if (!response || response.error || response.message) {
7100
+ const createEnvelope = response as unknown as { error?: string; message?: string };
7101
+ if (!response || createEnvelope.error || createEnvelope.message) {
6962
7102
  console.error('Widget create failed:', response);
6963
- authStatus = `Create widget failed: ${response?.message || response?.error || 'Unknown error'}`;
7103
+ authStatus = `Create widget failed: ${createEnvelope.message || createEnvelope.error || 'Unknown error'}`;
6964
7104
  return;
6965
7105
  }
6966
7106
  // Ensure we have an id
6967
- const newWidget = response.data || response; // handle envelope or direct
7107
+ const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response); // handle envelope or direct
6968
7108
  if (newWidget?.id) {
6969
7109
  widgets = [{ ...newWidget }, ...widgets];
6970
7110
  selectWidget(newWidget);
@@ -7619,12 +7759,12 @@
7619
7759
  canvasShapePreviewSyncTimers.clear();
7620
7760
  }
7621
7761
  function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
7622
- if (!widgetId) return;
7762
+ if (!widgetId || isAnyWidgetGenerating()) return;
7623
7763
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
7624
7764
  if (existing) clearTimeout(existing);
7625
7765
  const timer = setTimeout(() => {
7626
7766
  canvasShapePreviewSyncTimers.delete(widgetId);
7627
- if (generatingWidgetIds[widgetId]) {
7767
+ if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
7628
7768
  // console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
7629
7769
  return;
7630
7770
  }
@@ -7639,10 +7779,38 @@
7639
7779
  canvasShapePreviewSyncTimers.set(widgetId, timer);
7640
7780
  }
7641
7781
 
7782
+ /**
7783
+ * Serializing the canvas stringifies every shape's embedded image, so it is
7784
+ * megabytes of synchronous work on a busy canvas. The thumbnail paths run it
7785
+ * immediately after a capture, which is exactly where the freeze reports land,
7786
+ * so each call is measured and named.
7787
+ */
7788
+ function serializeCanvasJson(reason: string): string | null {
7789
+ return measureSync(`canvas-serialize:${reason}`, () => canvasRef?.saveCanvas?.() ?? null);
7790
+ }
7791
+
7642
7792
  async function handleRefreshScreenshot(widgetId: string, options: { quiet?: boolean } = {}) {
7643
7793
  const quiet = !!options.quiet;
7644
7794
  // console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
7645
7795
 
7796
+ // Never run quiet background screenshot captures while ANY code generation is running —
7797
+ // html2canvas execution inside any preview iframe blocks the single shared main thread.
7798
+ if (quiet && isAnyWidgetGenerating()) {
7799
+ console.log('[handleRefreshScreenshot] Skip quiet capture — codegen in progress', widgetId?.substring(0, 8));
7800
+ return;
7801
+ }
7802
+
7803
+ // Single-flight per widget: overlapping captures stack html2canvas runs in
7804
+ // the preview iframe — same-origin iframes share this page's main thread,
7805
+ // and stacked captures blocked the whole site during generation (no
7806
+ // [FreezeReport] because the long task is attributed to the iframe).
7807
+ if (screenshotCaptureInFlight.has(widgetId)) {
7808
+ console.log('[handleRefreshScreenshot] Capture already in flight — skipping', widgetId?.substring(0, 8));
7809
+ return;
7810
+ }
7811
+ screenshotCaptureInFlight.add(widgetId);
7812
+
7813
+ try {
7646
7814
  if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
7647
7815
  // console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
7648
7816
  return;
@@ -7673,10 +7841,10 @@
7673
7841
  if (!shapeThumbnailCaptureInFlight.has(widgetId)) {
7674
7842
  shapeThumbnailCaptureInFlight.add(widgetId);
7675
7843
  try {
7676
- const freshCapture = await handleChatScreenshot(widgetId);
7844
+ const freshCapture = await handleChatScreenshot(widgetId, null, quiet);
7677
7845
  if (freshCapture) {
7678
7846
  canvasRef?.updateWidgetImage(widgetId, freshCapture);
7679
- const freshJson = canvasRef?.saveCanvas?.();
7847
+ const freshJson = serializeCanvasJson('quiet-shape-thumbnail');
7680
7848
  if (freshJson) void handleCanvasAutoSave(freshJson);
7681
7849
  void persistWidgetListThumbnail(widgetId, freshCapture);
7682
7850
  } else {
@@ -7713,8 +7881,7 @@
7713
7881
  showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
7714
7882
  }
7715
7883
 
7716
- try {
7717
- const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
7884
+ const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
7718
7885
  const timeout = setTimeout(() => {
7719
7886
  window.removeEventListener('message', handler);
7720
7887
  resolve({ data: null, error: 'timeout' });
@@ -7736,7 +7903,7 @@
7736
7903
  if (result.data && canvasRef) {
7737
7904
  canvasRef.updateWidgetImage(widgetId, result.data);
7738
7905
  setTimeout(() => {
7739
- const json = canvasRef?.saveCanvas();
7906
+ const json = serializeCanvasJson('screenshot-refresh');
7740
7907
  if (json) handleCanvasAutoSave(json);
7741
7908
  }, 500);
7742
7909
  void persistWidgetListThumbnail(widgetId, result.data);
@@ -7765,6 +7932,8 @@
7765
7932
  if (!quiet) {
7766
7933
  showToast('error', 'Screenshot capture failed', { duration: 3000 });
7767
7934
  }
7935
+ } finally {
7936
+ screenshotCaptureInFlight.delete(widgetId);
7768
7937
  }
7769
7938
  }
7770
7939
 
@@ -7846,6 +8015,8 @@ Requirements:
7846
8015
  attachmentIds?: string[];
7847
8016
  /** Image URLs / data URLs captured before chat clearDraft. */
7848
8017
  promptImagesOverride?: string[];
8018
+ /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8019
+ skipIntentGate?: boolean;
7849
8020
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
7850
8021
  persistFileAttachments?: Array<{
7851
8022
  url?: string;
@@ -7935,6 +8106,7 @@ Requirements:
7935
8106
  attachmentIds: options.attachmentIds,
7936
8107
  promptImagesOverride: options.promptImagesOverride,
7937
8108
  persistFileAttachments: options.persistFileAttachments,
8109
+ skipIntentGate: options.skipIntentGate,
7938
8110
  });
7939
8111
  }
7940
8112
 
@@ -7956,6 +8128,8 @@ Requirements:
7956
8128
  forceRecreateRepo?: boolean;
7957
8129
  attachmentIds?: string[];
7958
8130
  promptImagesOverride?: string[];
8131
+ /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8132
+ skipIntentGate?: boolean;
7959
8133
  persistFileAttachments?: Array<{
7960
8134
  url?: string;
7961
8135
  file?: File;
@@ -7971,6 +8145,7 @@ Requirements:
7971
8145
 
7972
8146
  const panelChat = getChatMethodsForWidget(generationWidgetId);
7973
8147
  const panelSnap = getPanelState(generationWidgetId);
8148
+ const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
7974
8149
 
7975
8150
  // Per-widget abort: only abort if the SAME widget already has a running generation
7976
8151
  const existingController = widgetAbortControllers.get(generationWidgetId);
@@ -8191,17 +8366,67 @@ Requirements:
8191
8366
  modelId: coderModelForRequest || undefined,
8192
8367
  plannerModelId: plannerModelIdForRequest,
8193
8368
  widgetId: generationWidgetId,
8194
- conversationId: activeConversationId ?? undefined,
8195
- images: imagesToSend,
8369
+ conversationId: activeConversationId ?? undefined, images: imagesToSend,
8196
8370
  attachmentIds: attachmentIdsToSend,
8197
8371
  runtimeLogs: runtimeLogsForRequest,
8198
8372
  compileFixOnly: options.compileFixOnly === true,
8199
- }
8373
+ skipIntentGate: options.skipIntentGate === true,
8374
+ } as GenerateCodeRequest
8200
8375
  });
8201
8376
  // console.log('[WidgetCreator] Code generation request sent for widget', generationWidgetId.substring(0, 8), 'with model:', coderModelForRequest, 'planner:', plannerModelIdForRequest || 'none', 'conversationId:', activeConversationId || 'none', 'images:', imagesToSend?.length || 0, 'attachmentIds:', attachmentIdsToSend?.length || 0, 'runtimeLogs:', runtimeLogsForRequest?.length ?? 0);
8202
8377
  // console.log('Widget Creator: Code generation response:', response);
8203
8378
 
8204
- if (response && response.error === 'TOKEN_EXPIRED') {
8379
+ // --- Intent gate: the server answered instead of generating ---
8380
+ // A question got a meta answer, an ambiguous ask got a clarification
8381
+ // card, or a publish/settings ask was redirected. None of these spent
8382
+ // a generation slot. Anything unexpected here falls through to the
8383
+ // normal path (operation tracking), never blocks a real build.
8384
+ const gatedData = (response as any)?.data;
8385
+ if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
8386
+ const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
8387
+ const gatedReply: string = gatedData.reply ?? '';
8388
+ const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8389
+
8390
+ clearActiveOperationId();
8391
+ patchCodegenUiState(generationWidgetId, {
8392
+ lastGenerationFailed: false,
8393
+ lastGenerationError: null,
8394
+ generateCodeStatus:
8395
+ gatedAction === 'answer'
8396
+ ? 'Answered from widget details — no generation needed.'
8397
+ : gatedAction === 'clarify'
8398
+ ? 'Need a bit more detail before generating.'
8399
+ : 'No generation needed — use the publish or settings control.',
8400
+ });
8401
+
8402
+ const chatMethods = getChatMethodsForWidget(generationWidgetId);
8403
+ if (gatedAction === 'clarify' && suggestions.length > 0 && chatMethods?.addAssistantMessage) {
8404
+ // Suggestions travel as message data, not flattened text — the chat
8405
+ // renders them as clickable chips that send the next user message.
8406
+ chatMethods.addAssistantMessage(gatedReply, false, suggestions);
8407
+ } else if (chatMethods?.addAssistantMessage) {
8408
+ chatMethods.addAssistantMessage(gatedReply);
8409
+ }
8410
+
8411
+ showToast(
8412
+ gatedAction === 'answer' ? 'info' : 'warning',
8413
+ gatedAction === 'answer' ? 'Answered without generating' : 'No generation started',
8414
+ {
8415
+ description: gatedReply.slice(0, 140),
8416
+ duration: 8000,
8417
+ },
8418
+ );
8419
+ generatingWidgetIds = (() => {
8420
+ const { [generationWidgetId]: _gated, ...rest } = generatingWidgetIds;
8421
+ return rest;
8422
+ })();
8423
+ isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
8424
+ setCrossTabGenerating(false, generationWidgetId);
8425
+ widgetAbortControllers.delete(generationWidgetId);
8426
+ return;
8427
+ }
8428
+
8429
+ if (response && (response as { error?: string }).error === 'TOKEN_EXPIRED') {
8205
8430
  patchCodegenUiState(generationWidgetId, {
8206
8431
  generateCodeStatus: '❌ Authentication expired. Please login again.',
8207
8432
  lastGenerationFailed: true,
@@ -8214,7 +8439,8 @@ Requirements:
8214
8439
  return;
8215
8440
  }
8216
8441
 
8217
- const generateErrorMessage = response && (response.error || response.message);
8442
+ const generateEnvelope = response as unknown as { error?: string; message?: string };
8443
+ const generateErrorMessage = generateEnvelope && (generateEnvelope.error || generateEnvelope.message);
8218
8444
  if (generateErrorMessage) {
8219
8445
  const normalizedMessage = normalizeErrorMessage(generateErrorMessage);
8220
8446
  patchCodegenUiState(generationWidgetId, {
@@ -8230,8 +8456,10 @@ Requirements:
8230
8456
  return;
8231
8457
  }
8232
8458
 
8233
- const responseData = response?.data || response;
8234
- const operationId = responseData?.operationId;
8459
+ const generateData = ((response as { data?: unknown }).data ?? response) as {
8460
+ operationId?: string;
8461
+ };
8462
+ const operationId = generateData?.operationId;
8235
8463
 
8236
8464
  // The Worker serves rebuilt code at the same preview URL, so the panel
8237
8465
  // must know when the current generation started — hasLiveWorkerPreview()
@@ -8314,6 +8542,16 @@ Requirements:
8314
8542
 
8315
8543
  const completedCommitSha = getOperationCommitId(op.metadata);
8316
8544
  const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
8545
+ const completedCached = getOperationCached(op.metadata);
8546
+ const completedCachedFiles = getOperationCachedFiles(op.metadata);
8547
+ const cachedChatSummary = completedCached
8548
+ ? `Already done — the widget is in the state you asked for.` +
8549
+ (completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : '') +
8550
+ '\nThat change was made earlier in this conversation, so no new generation was needed' +
8551
+ (completedCachedFiles.length > 0
8552
+ ? ':\n' + completedCachedFiles.map((p) => `- ${p}`).join('\n')
8553
+ : '.')
8554
+ : null;
8317
8555
  const generationPanelRef = getWidgetDetailsRef(generationWidgetId);
8318
8556
  const isFocusedPanel = generationWidgetId === focusedPanelWidgetId;
8319
8557
  const completedWidgetName = getWidgetById(generationWidgetId)?.name || 'widget';
@@ -8351,18 +8589,25 @@ Requirements:
8351
8589
  // Refresh version history after every generation so the new commit shows
8352
8590
  // up in the version menu immediately. Publishing is deliberately not
8353
8591
  // automatic — the user previews and presses Publish themselves.
8592
+ // Deferred past the preview remount: running this synchronously against
8593
+ // `dynamic_worker_preview_ready` stacked three heavy re-renders on the
8594
+ // main thread in the same 800ms window — the "frozen at 80%" report.
8354
8595
  if (generationWidgetId) {
8355
8596
  const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
8356
8597
  shapeHistoryResolvedByWidgetId = restResolved;
8357
- void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
8358
- syncWidgetShapeLivePreviews();
8359
- if (isFocusedPanel) panelDisplayRevision += 1;
8360
- });
8598
+ setTimeout(() => {
8599
+ void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
8600
+ syncWidgetShapeLivePreviews();
8601
+ if (isFocusedPanel) panelDisplayRevision += 1;
8602
+ });
8603
+ }, 1200);
8361
8604
  }
8362
8605
 
8363
8606
  // Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
8607
+ // Deferred — same window as the schema/history reloads above.
8364
8608
  if (generationWidgetId) {
8365
- void (async () => {
8609
+ setTimeout(() => {
8610
+ void (async () => {
8366
8611
  try {
8367
8612
  const fetchedHead = await fetchRepoHeadCommitSha(generationWidgetId);
8368
8613
  // Prefer the codegen commit from the operation. GitLab HEAD can still
@@ -8404,11 +8649,15 @@ Requirements:
8404
8649
  } catch (err) {
8405
8650
  console.warn('[Version] Failed to reconcile HEAD after codegen:', err);
8406
8651
  }
8407
- })();
8652
+ })();
8653
+ }, 1000);
8408
8654
  }
8409
8655
  if (generationWidgetId) {
8410
8656
  // console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
8411
- void loadWidgetSchemas(generationWidgetId, { force: true });
8657
+ // Deferred with the publish-history reload — see comment above.
8658
+ setTimeout(() => {
8659
+ void loadWidgetSchemas(generationWidgetId, { force: true });
8660
+ }, 1600);
8412
8661
  }
8413
8662
 
8414
8663
  const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
@@ -8427,11 +8676,13 @@ Requirements:
8427
8676
  });
8428
8677
 
8429
8678
  const commitShort = completedCommitSha ? completedCommitSha.substring(0, 7) : '';
8430
- panelChat?.updateLatestCodegenStatusMessage?.(
8431
- completedPreviewBuildErr
8432
- ? `Code saved${commitShort ? ` (${commitShort})` : ''} — building preview…`
8433
- : `Code saved${commitShort ? ` (${commitShort})` : ''} — loading preview…`,
8434
- );
8679
+ if (!completedCached) {
8680
+ panelChat?.updateLatestCodegenStatusMessage?.(
8681
+ completedPreviewBuildErr
8682
+ ? `Code saved${commitShort ? ` (${commitShort})` : ''} — building preview…`
8683
+ : `Code saved${commitShort ? ` (${commitShort})` : ''} — loading preview…`,
8684
+ );
8685
+ }
8435
8686
 
8436
8687
  // console.log('Widget Creator: Code generation operation completed for widget', generationWidgetId.substring(0, 8), ':', op);
8437
8688
 
@@ -8467,13 +8718,17 @@ Requirements:
8467
8718
  // Final chat message regardless of panel focus — without this, a
8468
8719
  // closed/unfocused panel never receives the terminal update and the
8469
8720
  // chat stays on the last progress line (e.g. stuck at 80%).
8721
+ // A cache-served completion gets the "already done" affordance:
8722
+ // it names the earlier change (and files) instead of claiming a
8723
+ // generation just happened.
8470
8724
  panelChat?.addAssistantMessage(
8471
- completedPreviewBuildErr
8472
- ? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
8473
- : `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
8725
+ cachedChatSummary ??
8726
+ (completedPreviewBuildErr
8727
+ ? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
8728
+ : `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`),
8474
8729
  true
8475
8730
  );
8476
- if (!completedPreviewBuildErr) {
8731
+ if (!completedPreviewBuildErr && !completedCached) {
8477
8732
  panelChat?.updateLatestCodegenStatusMessage?.(
8478
8733
  `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
8479
8734
  );
@@ -8506,6 +8761,9 @@ Requirements:
8506
8761
  await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
8507
8762
  }
8508
8763
  activePanelRef?.setPreviewBuildError(null);
8764
+ // TODO(tomorrow): Dupa o generare noua, sincronizarea versiunii in dropdown si istoric se desincronizeaza.
8765
+ // Trebuie armonizata selectia automata a noii versiuni draft (vN) create in istoric,
8766
+ // actualizat trigger-ul pe canvas si in chat la noul numar de versiune si pastrata coerenta intre preview si dropdown.
8509
8767
  // Show Worker preview on the canvas shape (not the previous Live CDN artifact).
8510
8768
  shapePreviewSourceByWidgetId = {
8511
8769
  ...shapePreviewSourceByWidgetId,
@@ -8531,10 +8789,13 @@ Requirements:
8531
8789
  panelChat?.markWidgetHeadAtLatestCommit?.();
8532
8790
  }, 2500);
8533
8791
  setTimeout(() => {
8534
- void (async () => {
8535
- await handleRefreshScreenshot(generationWidgetId, { quiet: true });
8536
- // List thumb only from live iframe capture — never Fabric shape icon.
8537
- })();
8792
+ // Only capture screenshot after the very first generation or at publish
8793
+ // Subsequent generations skip this heavy operation to keep the main thread smooth.
8794
+ if (!hadCodeBeforeGeneration) {
8795
+ void (async () => {
8796
+ await handleRefreshScreenshot(generationWidgetId, { quiet: true });
8797
+ })();
8798
+ }
8538
8799
  }, 3000);
8539
8800
  // Do NOT auto-publish — user previews first, then clicks Publish / Set live.
8540
8801
  } else {
@@ -8878,11 +9139,17 @@ Requirements:
8878
9139
  publishingWidgetId = null;
8879
9140
  setCrossTabPublishing(false, capturedWidgetId);
8880
9141
 
8881
- const version = publishData?.data?.version || publishData?.version || 1;
8882
- const artifactUrl = publishData?.data?.artifactUrl || publishData?.artifactUrl || null;
9142
+ const publishEnvelope = publishData as unknown as {
9143
+ data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
9144
+ version?: number;
9145
+ artifactUrl?: string | null;
9146
+ commitSha?: string | null;
9147
+ };
9148
+ const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
9149
+ const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
8883
9150
  const commitSha =
8884
- publishData?.data?.commitSha
8885
- ?? publishData?.commitSha
9151
+ publishEnvelope?.data?.commitSha
9152
+ ?? publishEnvelope?.commitSha
8886
9153
  ?? commitShaForPublish
8887
9154
  ?? null;
8888
9155
  applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha);
@@ -8930,9 +9197,9 @@ Requirements:
8930
9197
 
8931
9198
 
8932
9199
  // Token expiration check interval
8933
- let tokenCheckInterval: number;
9200
+ let tokenCheckInterval: ReturnType<typeof setInterval>;
8934
9201
  let tokenCheckIntervalTime: number = 10; // seconds — check frequently so logout is prompt
8935
- let tokenDisplayInterval: number;
9202
+ let tokenDisplayInterval: ReturnType<typeof setInterval>;
8936
9203
 
8937
9204
  // Reactive variables for real-time token status
8938
9205
  let tokenTimeLeft: number = 0;
@@ -8944,6 +9211,7 @@ Requirements:
8944
9211
  widgetPublishStatus,
8945
9212
  codeGenerationStatus,
8946
9213
  agentHarnessOutput,
9214
+ dynamicWorkerPreviewStatus,
8947
9215
  setAuthToken as setWsAuthToken,
8948
9216
  setWebsocketApiUrl
8949
9217
  } from './stores/websocketStore.js';
@@ -9014,6 +9282,9 @@ Requirements:
9014
9282
  // Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
9015
9283
  // component scope so `onDestroy` can remove the same reference.
9016
9284
  window.addEventListener('beforeunload', handleBeforeUnload);
9285
+ // Keep a component-scope binding so onDestroy can remove the listener —
9286
+ // the arrow itself is scoped to this onMount callback.
9287
+ handleBeforeUnloadRef = handleBeforeUnload;
9017
9288
 
9018
9289
  // Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
9019
9290
  // AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
@@ -9095,6 +9366,8 @@ Requirements:
9095
9366
  };
9096
9367
  });
9097
9368
 
9369
+ let handleBeforeUnloadRef: (() => void) | null = null;
9370
+
9098
9371
  // Clean up interval on component destroy
9099
9372
  onDestroy(() => {
9100
9373
  clearCanvasLoadFailsafe();
@@ -9107,7 +9380,7 @@ Requirements:
9107
9380
  }
9108
9381
  // Cross-tab cleanup: remove flags so other tabs don't stay blocked
9109
9382
  if (typeof window !== 'undefined') {
9110
- window.removeEventListener('beforeunload', handleBeforeUnload);
9383
+ window.removeEventListener('beforeunload', handleBeforeUnloadRef);
9111
9384
  window.removeEventListener('storage', handleCrossTabStorageEvent);
9112
9385
  window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
9113
9386
  window.removeEventListener('click', handleCanvasDropdownOutsideClick);
@@ -9298,6 +9571,19 @@ Requirements:
9298
9571
  return sanitizePreviewBuildError(raw);
9299
9572
  }
9300
9573
 
9574
+ /** True when the operation completed from the semantic cache (no new generation ran). */
9575
+ function getOperationCached(metadata?: Record<string, unknown>): boolean {
9576
+ return metadata?.cached === true;
9577
+ }
9578
+
9579
+ /** Changed-file paths reported by a semantic-cache-served operation. */
9580
+ function getOperationCachedFiles(metadata?: Record<string, unknown>): string[] {
9581
+ if (!metadata) return [];
9582
+ const raw = metadata.generatedFiles;
9583
+ if (!Array.isArray(raw)) return [];
9584
+ return raw.filter((p): p is string => typeof p === 'string');
9585
+ }
9586
+
9301
9587
  function getChatMethodsForWidget(widgetId: string | null | undefined) {
9302
9588
  if (!widgetId) return chatMethods ?? null;
9303
9589
  const instanceKey = panelInstanceKeyByWidgetId[widgetId] ?? widgetId;
@@ -9330,6 +9616,9 @@ Requirements:
9330
9616
 
9331
9617
  function pushCodegenProgressToUi(op: OperationStatus, widgetId: string | null | undefined) {
9332
9618
  const line = formatCodegenProgressStatus(op);
9619
+ // Feed the freeze watchdog the latest status so a [FreezeReport] names the
9620
+ // stage the UI was showing when the main thread stopped responding.
9621
+ (window as any).__widgeticFreezeWatchdog?.setStatus?.(line);
9333
9622
  // SSE progress frames can arrive many times per second while the LLM
9334
9623
  // streams. Each one re-renders every open panel — coalesce them.
9335
9624
  if (widgetId) {
@@ -9684,8 +9973,7 @@ Requirements:
9684
9973
  codeGenerationPrompt: storedPrompt,
9685
9974
  promptImages: [],
9686
9975
  lastCommitId: hydratedHead,
9687
- lastPublishedVersion: publishedVersion,
9688
- selectedWidgetJsPath: widget?.jsPath ?? null,
9976
+ lastPublishedVersion: publishedVersion, selectedWidgetJsPath: (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null,
9689
9977
  currentRepositoryId: normalizeRepositoryId(widget),
9690
9978
  widgetDetailsStep: resolveCreatorWorkflowStep(publishedVersion !== null ? 'edit' : 'create'),
9691
9979
  generateCodeStatus: null,
@@ -9920,10 +10208,30 @@ Requirements:
9920
10208
 
9921
10209
  function getPanelEditPreviewUrl(widgetId: string): string | null {
9922
10210
  const ps = getPanelDisplayState(widgetId);
10211
+ const source = shapePreviewSourceByWidgetId[widgetId];
10212
+ if (source && source.startsWith('v:')) {
10213
+ const version = parseInt(source.slice(2), 10);
10214
+ if (Number.isFinite(version)) {
10215
+ const record = getShapePublishHistory(widgetId).find(
10216
+ (row) => Number(row.version) === version,
10217
+ );
10218
+ const isPublished = !!(record?.publishedAt ?? record?.published_at);
10219
+ const artifact =
10220
+ record?.artifactUrl ||
10221
+ record?.artifact_url ||
10222
+ (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10223
+ if (isPublished && artifact) {
10224
+ const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10225
+ if (url) return url;
10226
+ }
10227
+ const previewState = resolveWidgetShapeVersionPreviewState(widgetId);
10228
+ if (previewState?.url) return previewState.url;
10229
+ }
10230
+ }
9923
10231
  const widget = getWidgetById(widgetId);
9924
10232
  const version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widget);
9925
10233
  if (version == null) return null;
9926
- const jsPath = ps.selectedWidgetJsPath ?? widget?.jsPath ?? null;
10234
+ const jsPath = ps.selectedWidgetJsPath ?? (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null;
9927
10235
  return resolvePublishedWidgetUrl(widgetId, version, jsPath);
9928
10236
  }
9929
10237
 
@@ -9947,6 +10255,8 @@ Requirements:
9947
10255
  // Historical previews are never interchangeable with the shared HEAD preview.
9948
10256
  let shapeVersionPreviewByWidgetId: Record<string, {
9949
10257
  versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
10258
+ /** Commit the selected version was built from — drives the chat's Head-at-commit badge. */
10259
+ commitSha?: string | null;
9950
10260
  errorDetails?: Awaited<ReturnType<typeof parseVersionPreviewError>> | null;
9951
10261
  }> = {};
9952
10262
  let shapeVersionPreviewRequestId = 0;
@@ -10022,6 +10332,15 @@ Requirements:
10022
10332
  return fromPanel?.length ? fromPanel : [];
10023
10333
  }
10024
10334
 
10335
+ /** Newest unpublished version — Worker `/preview` (repo HEAD) shows this version's code. */
10336
+ function getNewestDraftVersionId(widgetId: string): number | null {
10337
+ const draftVersions = getShapePublishHistory(widgetId)
10338
+ .filter((row) => !(row.publishedAt ?? row.published_at))
10339
+ .map((row) => Number(row.version))
10340
+ .filter((v) => Number.isFinite(v));
10341
+ return draftVersions.length > 0 ? Math.max(...draftVersions) : null;
10342
+ }
10343
+
10025
10344
  function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
10026
10345
  if (!widgetId) return null;
10027
10346
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
@@ -10049,6 +10368,11 @@ Requirements:
10049
10368
  const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10050
10369
  if (url) return url;
10051
10370
  }
10371
+ // The newest draft IS repo HEAD — Worker `/preview/{widgetId}` already serves this code!
10372
+ const newestDraft = getNewestDraftVersionId(widgetId);
10373
+ if (!isPublished && newestDraft != null && version === newestDraft && workerUrl) {
10374
+ return workerUrl;
10375
+ }
10052
10376
  return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
10053
10377
  }
10054
10378
  return null;
@@ -10074,6 +10398,13 @@ Requirements:
10074
10398
  if (!Number.isFinite(version)) return null;
10075
10399
  return `v${version}`;
10076
10400
  }
10401
+ // 'preview' = Worker HEAD = newest generated (draft) version. Showing the
10402
+ // Live version here hid every fresh generation behind the old artifact.
10403
+ if (source === 'preview') {
10404
+ const newestDraft = getNewestDraftVersionId(widgetId);
10405
+ if (newestDraft != null) return `v${newestDraft}`;
10406
+ return null;
10407
+ }
10077
10408
  return liveVersion != null ? `v${liveVersion}` : null;
10078
10409
  }
10079
10410
 
@@ -10160,6 +10491,13 @@ Requirements:
10160
10491
  function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
10161
10492
  const source = shapePreviewSourceByWidgetId[widgetId];
10162
10493
  if (source?.startsWith('v:')) return source;
10494
+ if (source === 'preview') {
10495
+ // Worker HEAD preview = the newest draft — keep the dropdown selection
10496
+ // on it, otherwise the picker kept pointing at the stale Live version.
10497
+ const newestDraft = getNewestDraftVersionId(widgetId);
10498
+ if (newestDraft != null) return `v:${newestDraft}`;
10499
+ return null;
10500
+ }
10163
10501
  const ps = getPanelDisplayState(widgetId);
10164
10502
  const liveVersion =
10165
10503
  ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
@@ -10191,9 +10529,12 @@ Requirements:
10191
10529
  // Published artifacts need no build. Drafts must never substitute HEAD.
10192
10530
  if (versionId.startsWith('v:')) {
10193
10531
  const version = Number(versionId.slice(2));
10194
- if (resolveWidgetShapeLivePreviewUrl(widgetId)) {
10532
+ const directUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10533
+ if (directUrl) {
10195
10534
  const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10196
10535
  console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
10536
+ const detailsRef = getWidgetDetailsRef(widgetId);
10537
+ detailsRef?.showPublishedArtifact?.(directUrl);
10197
10538
  } else {
10198
10539
  void ensureWidgetShapeVersionPreview(widgetId, version);
10199
10540
  }
@@ -10238,12 +10579,17 @@ Requirements:
10238
10579
  }
10239
10580
  shapeVersionPreviewByWidgetId = {
10240
10581
  ...shapeVersionPreviewByWidgetId,
10241
- [widgetId]: { versionId, requestId, loading: false, error: null, url: previewUrl },
10582
+ [widgetId]: {
10583
+ versionId, requestId, loading: false, error: null, url: previewUrl,
10584
+ commitSha: normalizeCommitSha(commitSha),
10585
+ },
10242
10586
  };
10243
10587
  syncWidgetShapeLivePreviews();
10244
10588
  panelDisplayRevision += 1;
10245
10589
  shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: null };
10246
10590
  console.log('[VersionPreview] Selection', { widgetId, version, commitSha });
10591
+ const detailsRef = getWidgetDetailsRef(widgetId);
10592
+ detailsRef?.showPublishedArtifact?.(previewUrl);
10247
10593
  } catch (error: unknown) {
10248
10594
  if (
10249
10595
  shapePreviewSourceByWidgetId[widgetId] !== versionId
@@ -10275,6 +10621,14 @@ Requirements:
10275
10621
  if (source.startsWith('v:')) {
10276
10622
  const parsed = parseInt(source.slice(2), 10);
10277
10623
  version = Number.isFinite(parsed) ? parsed : null;
10624
+ // A draft version has no publish-history row to read a commit from, so
10625
+ // without the sha the version preview reported we fell back to repo HEAD
10626
+ // and the badge stayed on the newest turn instead of moving to the
10627
+ // selected version's own turn.
10628
+ const previewState = shapeVersionPreviewByWidgetId[widgetId];
10629
+ if (previewState?.versionId === source && previewState.commitSha) {
10630
+ return normalizeCommitSha(previewState.commitSha);
10631
+ }
10278
10632
  } else {
10279
10633
  version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
10280
10634
  }
@@ -10293,28 +10647,84 @@ Requirements:
10293
10647
  return normalizeCommitSha(ps.lastCommitId);
10294
10648
  }
10295
10649
 
10650
+ /**
10651
+ * Head commit per open panel, for the chat's Head-at-commit badge.
10652
+ *
10653
+ * The resolver reads the selected version and its preview state out of records
10654
+ * the compiler cannot follow through the call, so calling it straight from the
10655
+ * template never re-ran on a version switch: the chat kept the previous
10656
+ * commit and the badge stayed on the newest turn. Naming the records here is
10657
+ * what makes the switch reach the chat.
10658
+ */
10659
+ let headCommitShaByWidgetId: Record<string, string | null> = {};
10660
+ /**
10661
+ * Short commit sha → "Version N" per panel, so a chat turn reads as the version
10662
+ * it produced. A hash is meaningless to a non-developer, so it stays behind
10663
+ * debug mode and this label is what the turn shows.
10664
+ */
10665
+ let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10666
+ $: {
10667
+ void shapePreviewSourceByWidgetId;
10668
+ void shapeVersionPreviewByWidgetId;
10669
+ void panelDisplayRevision;
10670
+ const nextHeads: Record<string, string | null> = {};
10671
+ const nextLabels: Record<string, Record<string, string>> = {};
10672
+ for (const widgetId of openWidgetPanelIds) {
10673
+ nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
10674
+ nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
10675
+ }
10676
+ headCommitShaByWidgetId = nextHeads;
10677
+ versionLabelsByWidgetId = nextLabels;
10678
+ }
10679
+
10680
+ /** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
10681
+ function versionLabelKey(sha: string | null | undefined): string | null {
10682
+ const normalized = normalizeCommitSha(sha);
10683
+ return normalized ? normalized.slice(0, 7) : null;
10684
+ }
10685
+
10686
+ function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
10687
+ const labels: Record<string, string> = {};
10688
+ const ps = getPanelDisplayState(widgetId);
10689
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10690
+ for (const record of history || []) {
10691
+ const key = versionLabelKey(record?.commitSha ?? record?.commit_sha);
10692
+ if (key && record?.version != null) labels[key] = `Version ${record.version}`;
10693
+ }
10694
+ // The version the user is previewing right now may have been fetched rather
10695
+ // than read from history, and it is the one whose label matters most.
10696
+ const previewState = shapeVersionPreviewByWidgetId[widgetId];
10697
+ const previewKey = versionLabelKey(previewState?.commitSha);
10698
+ if (previewKey && previewState?.versionId?.startsWith('v:')) {
10699
+ labels[previewKey] = `Version ${previewState.versionId.slice(2)}`;
10700
+ }
10701
+ return labels;
10702
+ }
10703
+
10296
10704
  function syncWidgetShapeLivePreviews(): void {
10297
- try {
10298
- canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
10299
- widgetShapeLivePreviewsRef?.sync?.();
10300
- // Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
10301
- const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
10302
- const liveWidgetIds = new Set<string>();
10303
- for (const rect of rects) {
10304
- if (!rect?.widgetId) continue;
10305
- if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
10306
- liveWidgetIds.add(rect.widgetId);
10307
- canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
10308
- }
10309
- const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
10310
- for (const widgetId of linkedIds) {
10311
- if (!liveWidgetIds.has(widgetId)) {
10312
- canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
10705
+ measureSync('sync-widget-shape-previews', () => {
10706
+ try {
10707
+ canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
10708
+ widgetShapeLivePreviewsRef?.sync?.();
10709
+ // Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
10710
+ const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
10711
+ const liveWidgetIds = new Set<string>();
10712
+ for (const rect of rects) {
10713
+ if (!rect?.widgetId) continue;
10714
+ if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
10715
+ liveWidgetIds.add(rect.widgetId);
10716
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
10313
10717
  }
10718
+ const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
10719
+ for (const widgetId of linkedIds) {
10720
+ if (!liveWidgetIds.has(widgetId)) {
10721
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
10722
+ }
10723
+ }
10724
+ } catch (err) {
10725
+ console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
10314
10726
  }
10315
- } catch (err) {
10316
- console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
10317
- }
10727
+ });
10318
10728
  }
10319
10729
 
10320
10730
  function getPanelEmbedPreviewUrl(widgetId: string): string | null {
@@ -11729,6 +12139,17 @@ $: if ($agentHarnessOutput?.timestamp) {
11729
12139
  }
11730
12140
  }
11731
12141
 
12142
+ // Worker preview ready via WebSocket event — reload canvas shape preview immediately
12143
+ $: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previewUrl) {
12144
+ const wsReadyWidgetId = $dynamicWorkerPreviewStatus.widgetId;
12145
+ shapePreviewSourceByWidgetId = {
12146
+ ...shapePreviewSourceByWidgetId,
12147
+ [wsReadyWidgetId]: 'preview',
12148
+ };
12149
+ syncWidgetShapeLivePreviews();
12150
+ widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12151
+ }
12152
+
11732
12153
  // ===================== Publish History =====================
11733
12154
 
11734
12155
  /**
@@ -11749,13 +12170,12 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
11749
12170
  });
11750
12171
  return;
11751
12172
  }
11752
- try {
11753
- const historyRes = await widgetsClient.getWidgetPublishHistory({ widgetId });
11754
- const history = Array.isArray(historyRes)
11755
- ? historyRes
11756
- : (Array.isArray((historyRes as { data?: unknown })?.data)
11757
- ? (historyRes as { data: unknown[] }).data
11758
- : []);
12173
+ try { const historyRes = await widgetsClient.getWidgetPublishHistory({ widgetId });
12174
+ const historyEnvelope = historyRes as unknown as { data?: unknown };
12175
+ // SDK types the envelope ({ success, data }); a response-transform middleware
12176
+ // may unwrap it, so a bare array stays tolerated.
12177
+ const historyData = Array.isArray(historyRes) ? historyRes : historyEnvelope?.data;
12178
+ const history = (Array.isArray(historyData) ? historyData : []) as PublishHistoryRecord[];
11759
12179
  patchPanelSnapshot(widgetId, {
11760
12180
  publishHistory: history,
11761
12181
  publishHistoryResolved: true,
@@ -11893,7 +12313,9 @@ function togglePublishHistory(triggerEl?: HTMLElement | null) {
11893
12313
  const cluster = triggerEl.closest('.wc-version-history-ct') as HTMLElement | null;
11894
12314
  const rect = (cluster ?? triggerEl).getBoundingClientRect();
11895
12315
  liveHistoryPanelTop = Math.round(rect.bottom + 4);
11896
- liveHistoryPanelRight = Math.round(window.innerWidth - rect.right);
12316
+ // Rows inset their action button 12px from the panel edge (panel p-1 4px + row px-2 8px).
12317
+ // Shift right by 12px so the per-row action buttons line up under the header action button.
12318
+ liveHistoryPanelRight = Math.round(window.innerWidth - rect.right) - 12;
11897
12319
  }
11898
12320
  loadPublishHistory();
11899
12321
  }
@@ -12053,8 +12475,12 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12053
12475
  // console.log('Signup response:', signupResponse);
12054
12476
 
12055
12477
  // show error signup status
12056
- if (!signupResponse || !signupResponse.user) {
12057
- authStatus = signupResponse?.message ?? 'Signup failed.';
12478
+ // signupUser is typed Promise<void>; it resolves to undefined on the
12479
+ // success path, so a falsy response IS the success signal. Error details
12480
+ // arrive via the thrown ResponseError caught below.
12481
+ const signupEnvelope = signupResponse as unknown as { user?: unknown; message?: string } | undefined;
12482
+ if (!signupEnvelope || !signupEnvelope.user) {
12483
+ authStatus = signupEnvelope?.message ?? 'Signup failed.';
12058
12484
  return;
12059
12485
  }
12060
12486
 
@@ -12076,7 +12502,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12076
12502
  // console.log('Login response:', loginResponse);
12077
12503
 
12078
12504
  // show success login status
12079
- const payload = loginResponse?.data;
12505
+ // The generated loginUser return type is mis-derived (a billing endpoint's
12506
+ // shape); the real /auth/login payload is a session envelope. Cast honestly.
12507
+ const payload = (loginResponse as unknown as { data?: { accessToken?: string; user?: Record<string, unknown> } })?.data;
12080
12508
  if (payload?.accessToken) {
12081
12509
  currentUserToken = payload.accessToken;
12082
12510
 
@@ -12142,7 +12570,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12142
12570
  // console.log('WidgetCreator.updateTokenStatus: Decoded token:', decoded);
12143
12571
 
12144
12572
  // if the decoded token has an expiration field, update the token status
12145
- if (decoded && decoded.exp) {
12573
+ if (decoded && typeof decoded.exp === 'number') {
12146
12574
  const currentTime = Math.floor(Date.now() / 1000);
12147
12575
  const expUnix = decoded.exp;
12148
12576
  tokenTimeLeft = expUnix - currentTime;
@@ -12316,17 +12744,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12316
12744
  // console.log('Available fields:', Object.keys(decodedToken));
12317
12745
 
12318
12746
  // Extract user info from Supabase JWT structure
12747
+ // user_metadata is an untyped JWT claim blob — index it explicitly so the
12748
+ // accesses below are checked reads, not property probes on `unknown`.
12749
+ const userMetadata = (decodedToken.user_metadata ?? {}) as Record<string, unknown>;
12319
12750
  user = {
12320
12751
  id: decodedToken.sub || decodedToken.user_id || decodedToken.id || 'token-user',
12321
- email: decodedToken.email || decodedToken.user_metadata?.email || 'user@token-auth.com',
12752
+ email: userMetadata.email || 'user@token-auth.com',
12322
12753
  name:
12323
- decodedToken.user_metadata?.name ||
12324
- decodedToken.user_metadata?.full_name ||
12754
+ userMetadata.name ||
12755
+ userMetadata.full_name ||
12325
12756
  decodedToken.name ||
12326
12757
  decodedToken.full_name ||
12327
12758
  'Token User',
12328
12759
  role: decodedToken.role || 'user',
12329
- avatar: decodedToken.user_metadata?.avatar_url || decodedToken.user_metadata?.picture
12760
+ avatar: userMetadata.avatar_url || userMetadata.picture
12330
12761
  };
12331
12762
 
12332
12763
  // Show more detailed status with actual extracted info
@@ -12385,6 +12816,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12385
12816
  */
12386
12817
  const shapeThumbnailCaptureInFlight = new Set<string>();
12387
12818
 
12819
+ /**
12820
+ * Whole-function single-flight guard for handleRefreshScreenshot.
12821
+ * Stacked html2canvas captures run inside the preview iframe, which shares
12822
+ * the page main thread — a capture storm froze the entire site with no
12823
+ * parent-page [FreezeReport] (long tasks attributed to the iframe).
12824
+ */
12825
+ const screenshotCaptureInFlight = new Set<string>();
12826
+
12388
12827
  /**
12389
12828
  * Called when a Widget Image shape is clicked on the canvas.
12390
12829
  * Selects the linked widget and opens WidgetDetails.
@@ -12463,7 +12902,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12463
12902
  notifyHostWidgetName(widget);
12464
12903
  }
12465
12904
  await openWidgetDetailsPanel(widgetId);
12466
- scheduleCanvasShapePreviewSync(widgetId);
12467
12905
  }
12468
12906
 
12469
12907
  /**
@@ -12566,10 +13004,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12566
13004
  }
12567
13005
 
12568
13006
  const sourceHasCode =
12569
- !!normalizeRepositoryId(newWidget)
13007
+ !!normalizeRepositoryId(asWidgetSummary(newWidget))
12570
13008
  || !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
12571
13009
  ?? (newWidget as { js_path?: string | null }).js_path)
12572
- || resolvePublishedVersionFromWidget(newWidget) !== null
13010
+ || resolvePublishedVersionFromWidget(asWidgetSummary(newWidget)) !== null
12573
13011
  || !!sourceCommit;
12574
13012
 
12575
13013
  if (!canvasPrimaryWidgetId) {
@@ -12577,7 +13015,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12577
13015
  }
12578
13016
 
12579
13017
  // Duplicate stays a sibling on the canvas — do not make it the primary widget.
12580
- await selectWidget(newWidget, { skipPlaceShape: true });
13018
+ await selectWidget(asWidgetSummary(newWidget), { skipPlaceShape: true });
12581
13019
  notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
12582
13020
 
12583
13021
  showToast('success', 'Widget duplicated', {
@@ -12607,11 +13045,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12607
13045
  try {
12608
13046
  await handleWidgetDataChanged(newWidget.id);
12609
13047
  const freshWidget = getWidgetById(newWidget.id);
12610
- let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(newWidget);
13048
+ let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(asWidgetSummary(newWidget));
12611
13049
 
12612
13050
  if (!repoId) {
12613
13051
  // console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
12614
- repoId = await ensureWidgetRepository(freshWidget || newWidget, { silent: true });
13052
+ repoId = await ensureWidgetRepository(freshWidget || asWidgetSummary(newWidget), { silent: true });
12615
13053
  if (repoId) {
12616
13054
  widgets = widgets.map((w) => w.id === newWidget.id ? { ...w, repositoryId: repoId } : w);
12617
13055
  // console.log('[WidgetCreator] Repository created for duplicate:', repoId);
@@ -13072,14 +13510,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13072
13510
  }
13073
13511
 
13074
13512
  let objectCount = 0;
13075
- try {
13076
- const parsed = JSON.parse(json);
13077
- // saveCanvas() wraps as { version, savedAt, viewport, canvas: { objects: [...] } }
13078
- const objects = parsed?.canvas?.objects ?? parsed?.objects ?? [];
13079
- objectCount = Array.isArray(objects) ? objects.length : 0;
13080
- } catch {
13081
- // ignore parse errors for debug
13082
- }
13513
+ // Canvas JSON carries the shapes' embedded images, so it reaches megabytes on
13514
+ // a busy canvas and every parse of it blocks the main thread. Parse once and
13515
+ // reuse it for the API payload below.
13516
+ let parsedCanvas: any = null;
13517
+ measureSync('canvas-autosave:parse', () => {
13518
+ try {
13519
+ parsedCanvas = JSON.parse(json);
13520
+ // saveCanvas() wraps as { version, savedAt, viewport, canvas: { objects: [...] } }
13521
+ const objects = parsedCanvas?.canvas?.objects ?? parsedCanvas?.objects ?? [];
13522
+ objectCount = Array.isArray(objects) ? objects.length : 0;
13523
+ } catch {
13524
+ // ignore parse errors for debug
13525
+ }
13526
+ });
13083
13527
 
13084
13528
  if (isUnmountSave) {
13085
13529
  // console.log(`[CanvasDebug] Unmount save: objectCount=${objectCount}, lastSavedCount=${lastSavedCanvasObjectCount}`);
@@ -13112,11 +13556,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13112
13556
 
13113
13557
  // Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
13114
13558
  saveCanvasState(canvasId, json);
13115
- // Keep the legacy localStorage copy in sync for downgrade safety.
13116
- try {
13117
- localStorage.setItem(getCanvasStorageKey(), json);
13118
- } catch (err) {
13119
- console.warn('[WidgetCreator] Failed to save canvas to localStorage:', err);
13559
+ // Keep the legacy localStorage copy in sync for downgrade safety. `setItem` is
13560
+ // a synchronous disk write of the whole canvas, so during a generation it is
13561
+ // deferred to the end: the durable IndexedDB copy above is already written,
13562
+ // and the mirror exists only for a downgrade.
13563
+ if (!isAnyWidgetGenerating() || force || isUnmountSave) {
13564
+ measureSync('canvas-autosave:localStorage-mirror', () => {
13565
+ try {
13566
+ localStorage.setItem(getCanvasStorageKey(), json);
13567
+ } catch (err) {
13568
+ console.warn('[WidgetCreator] Failed to save canvas to localStorage:', err);
13569
+ }
13570
+ });
13120
13571
  }
13121
13572
 
13122
13573
  // Save to API via SDK with sequence race protection
@@ -13126,7 +13577,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13126
13577
  await canvasesClient.updateCanvas({
13127
13578
  id: canvasId,
13128
13579
  updateCanvasRequest: {
13129
- canvasContent: JSON.parse(json)
13580
+ canvasContent: parsedCanvas ?? JSON.parse(json)
13130
13581
  }
13131
13582
  });
13132
13583
  if (currentSeq < lastResolvedAutosaveSequence) {
@@ -13336,13 +13787,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13336
13787
  { headers: { 'Idempotency-Key': idempotencyKey } }
13337
13788
  );
13338
13789
 
13339
- if (!response || response.error || response.message) {
13790
+ const createEnvelope = response as unknown as { error?: string; message?: string };
13791
+ if (!response || createEnvelope.error || createEnvelope.message) {
13340
13792
  console.error('[WidgetCreator] Widget create on first send failed:', response);
13341
13793
  showToast('error', 'Failed to create widget from design');
13342
13794
  return null;
13343
13795
  }
13344
13796
 
13345
- const newWidget = response.data || response;
13797
+ const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response);
13346
13798
  if (!newWidget?.id) {
13347
13799
  showToast('warning', 'Widget created but no ID returned');
13348
13800
  return null;
@@ -13613,7 +14065,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13613
14065
  }
13614
14066
 
13615
14067
  async function ensureWidgetRepository(
13616
- widget: WidgetSummary,
14068
+ widget: WidgetSummary | null | undefined,
13617
14069
  options: { forceRecreate?: boolean; silent?: boolean } = {}
13618
14070
  ): Promise<string | null> {
13619
14071
  const { forceRecreate = false, silent = false } = options;
@@ -13738,8 +14190,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13738
14190
  });
13739
14191
  // console.log('Widget Creator: Repository creation response:', response);
13740
14192
 
13741
- const repoData = response?.data || response;
13742
- const repoId = repoData?.id || repoData?.repoId || repoData?.repo_id || null;
14193
+ const repoData = ((response as unknown as { data?: Record<string, unknown> }).data ?? response) as Record<string, unknown>;
14194
+ const repoId = (repoData?.id || repoData?.repoId || repoData?.repo_id || null) as string | null;
13743
14195
 
13744
14196
  if (!repoId) {
13745
14197
  showToast('error', 'Repository creation failed', {
@@ -13826,7 +14278,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13826
14278
  const response = await widgetsClient.deleteWidget(
13827
14279
  {
13828
14280
  id: adminDeleteWidget.id,
13829
- reason: 'admin_delete'
14281
+ deleteWidgetRequest: { reason: 'admin_delete' }
13830
14282
  },
13831
14283
  {
13832
14284
  headers: {
@@ -13835,10 +14287,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13835
14287
  }
13836
14288
  );
13837
14289
 
13838
- if (response?.error || response?.message) {
14290
+ // deleteWidget resolves void; only a thrown ResponseError (caught below)
14291
+ // carries a failure. The defensive envelope check is kept for the response-
14292
+ // transform middleware, but must go through an honest cast.
14293
+ const deleteEnvelope = response as unknown as { error?: string; message?: string } | undefined;
14294
+ if (deleteEnvelope && (deleteEnvelope.error || deleteEnvelope.message)) {
13839
14295
  console.error('Admin widget deletion failed:', response);
13840
14296
  showToast('error', 'Delete failed', {
13841
- description: response?.message || response?.error || 'Unknown error'
14297
+ description: deleteEnvelope?.message || deleteEnvelope?.error || 'Unknown error'
13842
14298
  });
13843
14299
  adminDeleteLoading = false;
13844
14300
  return;
@@ -14152,6 +14608,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14152
14608
  onTranslateEnd={(widgetId) => {
14153
14609
  canvasRef?.commitWidgetShapeTranslate?.(widgetId);
14154
14610
  }}
14611
+ onOverlayDragChange={(active) => {
14612
+ canvasRef?.setWidgetShapeOverlayDragActive?.(active);
14613
+ }}
14155
14614
  onSelectShape={(widgetId) => {
14156
14615
  canvasRef?.selectWidgetShapeById?.(widgetId);
14157
14616
  canvasSelectedWidgetShapeId = widgetId;
@@ -14161,6 +14620,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14161
14620
  void openWidgetDetailsPanel(widgetId);
14162
14621
  }}
14163
14622
  onRefresh={handleWidgetShapeRefresh}
14623
+ isGenerating={isAnyWidgetGenerating()}
14164
14624
  />
14165
14625
 
14166
14626
  <!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
@@ -14564,8 +15024,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14564
15024
  if (isPanelFocused && previewWidgetId === panelWidgetId) {
14565
15025
  chatMethods?.clearStalePreviewBuildFailures?.();
14566
15026
  }
14567
- // Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
14568
- scheduleCanvasShapePreviewSync(previewWidgetId);
14569
15027
  if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
14570
15028
  applyPanelCompositionToPreview(previewWidgetId);
14571
15029
  }
@@ -14800,10 +15258,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14800
15258
  {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
14801
15259
  {@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
14802
15260
  ? parseInt(shapeSource.slice(2), 10)
14803
- // A 'preview' source (Worker HEAD, unpublished) is not a published
14804
- // version — falling back to panelPublishedVersion produced `vnull`
14805
- // right after generation. The newest draft IS this HEAD's version.
14806
- : (newestDraft ? newestDraft.version : panelPublishedVersion)}
15261
+ : shapeSource === 'preview'
15262
+ // A 'preview' source (Worker HEAD, unpublished) is not a published
15263
+ // version — falling back to panelPublishedVersion produced `vnull`
15264
+ // right after generation AND showed the stale Live version when a
15265
+ // draft existed. The newest draft IS this HEAD's version.
15266
+ ? (newestDraft ? newestDraft.version : panelPublishedVersion)
15267
+ : panelPublishedVersion}
14807
15268
  {@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
14808
15269
  <!--
14809
15270
  A version is a Draft until it is published. The history is one
@@ -14841,13 +15302,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14841
15302
  }}
14842
15303
  disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
14843
15304
  title={getPublishDisabledReasonForPanel(panelWidgetId) ?? 'Publish the latest generated code as the Live version'}
14844
- class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
15305
+ class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
14845
15306
  >
14846
15307
  {isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
14847
15308
  </button>
14848
15309
  {:else if viewingIsLive}
14849
15310
  <span
14850
- class="wc-version-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 opacity-80"
15311
+ class="wc-version-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 opacity-80"
14851
15312
  title="Live — generate new code to Publish a new version"
14852
15313
  >Live</span>
14853
15314
  {:else if viewingIsDraft}
@@ -14863,7 +15324,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14863
15324
  }}
14864
15325
  disabled={!canPublishDraftVersionForPanel(panelWidgetId, viewingVersion) || getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled}
14865
15326
  title="Build this draft version and make it the Live version"
14866
- class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
15327
+ class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
14867
15328
  >
14868
15329
  {isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
14869
15330
  </button>
@@ -14880,7 +15341,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14880
15341
  }}
14881
15342
  disabled={isSettingLiveVersion === Number(viewingVersion)}
14882
15343
  title="Publish this version so visitors see it on your website"
14883
- class="wc-version-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
15344
+ class="wc-version-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
14884
15345
  >
14885
15346
  {isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
14886
15347
  </button>
@@ -14893,15 +15354,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14893
15354
  aria-label="Close version history"
14894
15355
  onclick={() => { isHistoryOpen = false; }}
14895
15356
  ></button>
15357
+ {@const hasChatScroll = ps.publishHistory.length > 5}
14896
15358
  <div
14897
- class="wc-version-history-panel flex flex-col gap-1 rounded-md border border-emerald-200 bg-white shadow-lg p-1.5 z-[9999] w-max"
14898
- style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px; max-width: min(22rem, calc(100vw - 16px));"
15359
+ class="wc-version-history-panel rounded-md border border-emerald-200 bg-white shadow-lg p-1 z-[9999] w-max"
15360
+ style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px;"
14899
15361
  >
14900
15362
  {#if isLoadingHistory}
14901
15363
  <p class="text-xs text-emerald-500 animate-pulse text-center py-2">Loading history...</p>
14902
15364
  {:else if ps.publishHistory.length === 0}
14903
15365
  <p class="text-xs text-emerald-400 text-center py-2">No history records yet. Generate or publish to start tracking.</p>
14904
15366
  {:else}
15367
+ <div
15368
+ class="wc-version-history-scroll flex flex-col gap-1 overflow-x-hidden {hasChatScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
15369
+ style="{hasChatScroll ? 'max-height: 185px; margin-right: -4px; padding-right: 4px;' : 'max-height: none;'} scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
15370
+ >
14905
15371
  {#each ps.publishHistory as record (record.id)}
14906
15372
  {@const rowVersion = Number(record.version)}
14907
15373
  {@const rowIsDraft = !(record.publishedAt ?? record.published_at)}
@@ -14922,6 +15388,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14922
15388
  publishDisabledReason={publishGate.disabled ? publishGate.reason : null}
14923
15389
  setLiveDisabled={isSettingLiveVersion === record.version}
14924
15390
  onselect={() => {
15391
+ isHistoryOpen = false;
14925
15392
  handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
14926
15393
  }}
14927
15394
  onpublish={() => {
@@ -14933,6 +15400,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14933
15400
  }}
14934
15401
  />
14935
15402
  {/each}
15403
+ </div>
14936
15404
  {/if}
14937
15405
  </div>
14938
15406
  </Portal>
@@ -15021,6 +15489,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15021
15489
  class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
15022
15490
  style="padding: 0;"
15023
15491
  use:nativeScrollbarPadding={0}
15492
+ use:pinScrollToBottom
15024
15493
  onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
15025
15494
  >
15026
15495
  {#key panelInstanceKey}
@@ -15028,11 +15497,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15028
15497
  class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
15029
15498
  data-widget-chat-height={panelWidgetId}
15030
15499
  style={chatUserHeight == null
15031
- ? `flex: 1 1 auto; min-height: ${CHAT_MIN_HEIGHT}px;`
15500
+ ? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
15032
15501
  : `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
15033
15502
  >
15034
15503
  <Chat class="code-generation-chat w-full h-full rounded-none border-0"
15035
- style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
15504
+ style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
15036
15505
  resizeExpandsContainer={true}
15037
15506
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
15038
15507
  showLogs={debugMode}
@@ -15059,7 +15528,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15059
15528
  autoCodeDetection: false,
15060
15529
  }
15061
15530
  }}
15062
- widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
15531
+ widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15532
+ versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15063
15533
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15064
15534
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
15065
15535
  onUploadChatFile={handleChatUploadFile}
@@ -15271,7 +15741,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15271
15741
  {/if}
15272
15742
 
15273
15743
  <!-- ─── Schema loading / error / PropsEditor ─── -->
15274
- {@const panelSchemasLoading = (panelDisplayRevision, ps.compositionSchemasLoading)}
15744
+ {@const panelSchemasLoading = rev(panelDisplayRevision, ps.compositionSchemasLoading)}
15275
15745
  {@const panelSchemaLoadLabel = getPanelSchemaLoadLabel(panelWidgetId)}
15276
15746
  {#if panelSchemasLoading}
15277
15747
  <div class="composition-editor-loading flex flex-col items-center justify-center py-8">
@@ -15576,7 +16046,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15576
16046
  {/if}
15577
16047
 
15578
16048
  <!-- Delete Widget Shape Confirmation Dialog -->
15579
- <Dialog.Root class="confirm-delete-dialog-root" bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
16049
+ <!-- Dialog.Root (bits-ui AlertDialogRoot) exposes no class prop — styling hooks
16050
+ live on Content/Header below. -->
16051
+ <Dialog.Root bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
15580
16052
  <Dialog.Content class="confirm-delete-dialog-content max-w-md bg-white z-9999">
15581
16053
  <Dialog.Header class="confirm-delete-dialog-header">
15582
16054
  <Dialog.Title>Delete Widget Shape</Dialog.Title>
@@ -15618,7 +16090,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15618
16090
  </Dialog.Root>
15619
16091
 
15620
16092
  <!-- Delete Canvas Confirmation Dialog -->
15621
- <Dialog.Root class="confirm-delete-canvas-dialog-root" bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
16093
+ <Dialog.Root bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
15622
16094
  <Dialog.Content class="confirm-delete-canvas-dialog-content max-w-sm bg-white z-9999">
15623
16095
  <Dialog.Header class="confirm-delete-canvas-dialog-header">
15624
16096
  <Dialog.Title>Delete Canvas</Dialog.Title>
@@ -15678,7 +16150,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15678
16150
 
15679
16151
  <!-- Confirm Generation Dialog (shown on convert-to-widget before auto-generating) -->
15680
16152
  <Dialog.Root
15681
- class="confirm-generate-dialog-root"
15682
16153
  open={confirmGenerateDialogOpen}
15683
16154
  onOpenChange={(open) => {
15684
16155
  confirmGenerateDialogOpen = open;
@@ -15821,21 +16292,24 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15821
16292
  outline: none;
15822
16293
  box-shadow: none;
15823
16294
  }
15824
- .wc-version-history-panel {
16295
+ .wc-version-history-scroll {
15825
16296
  scrollbar-width: thin;
15826
16297
  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
15827
16298
  }
15828
- .wc-version-history-panel::-webkit-scrollbar {
15829
- width: 8px;
16299
+ .wc-version-history-scroll::-webkit-scrollbar {
16300
+ width: 4px;
15830
16301
  background: transparent;
15831
16302
  }
15832
- .wc-version-history-panel::-webkit-scrollbar-track {
16303
+ .wc-version-history-scroll::-webkit-scrollbar-track {
15833
16304
  background: transparent;
15834
16305
  }
15835
- .wc-version-history-panel::-webkit-scrollbar-thumb {
16306
+ .wc-version-history-scroll::-webkit-scrollbar-thumb {
15836
16307
  background: rgba(0, 0, 0, 0.2);
15837
16308
  border-radius: 4px;
15838
16309
  }
16310
+ .wc-version-history-scroll::-webkit-scrollbar-thumb:hover {
16311
+ background: rgba(0, 0, 0, 0.35);
16312
+ }
15839
16313
  .generation-scroll-ct {
15840
16314
  scrollbar-width: thin;
15841
16315
  scrollbar-color: rgba(0, 0, 0, 0.22) transparent;