@widgetic/creator 0.3.66 → 0.3.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,6 @@
29
29
  } from '@widgetic/api-sdk';
30
30
  import type {
31
31
  Middleware,
32
- UpdateComposition,
33
32
  GenerateCodeRequest,
34
33
  AgentModelsGet200ResponseModelsInner
35
34
  } from '@widgetic/api-sdk';
@@ -500,6 +499,7 @@
500
499
 
501
500
  // Chat module for code generation conversation UI
502
501
  import { Chat, ContextType } from '@widgetic/chat';
502
+ import type { ClarificationSuggestion } from '@widgetic/chat';
503
503
 
504
504
  // Eagerly initiate canvas chunk fetching at script evaluation (parallel with DOM parsing)
505
505
  const canvasModulePromise = typeof window !== 'undefined' ? import('@widgetic/canvas') : null;
@@ -529,10 +529,13 @@
529
529
  onMount(async () => {
530
530
  if (canvasModulePromise) {
531
531
  const canvasModule = await canvasModulePromise;
532
+ // Installed before the canvas mounts so the first save already externalizes
533
+ installCanvasThumbnailStore(canvasModule);
532
534
  WidgeticCanvas = canvasModule.Canvas;
533
535
  }
534
536
  installLongTaskFreezeReporter();
535
537
  installFreezeWatchdog();
538
+ console.info('[FreezeWatchdog] Freeze reports log on this page. In DevTools, pick the top frame — not a widget iframe.');
536
539
  });
537
540
 
538
541
  /**
@@ -590,7 +593,10 @@
590
593
  saveWidgetDraft,
591
594
  readWidgetDraft,
592
595
  deleteWidgetDraft,
593
- copyWidgetDraft
596
+ copyWidgetDraft,
597
+ saveShapeThumbnail,
598
+ readShapeThumbnail,
599
+ canPersistShapeThumbnails
594
600
  } from './utils/creatorDraftStorage';
595
601
 
596
602
  /**
@@ -866,6 +872,21 @@
866
872
  function isAnyWidgetGenerating(): boolean {
867
873
  return isGeneratingCode || Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id]);
868
874
  }
875
+
876
+ /** Synchronous hold so a screenshot cannot postMessage after codegen has started. */
877
+ function noteCodegenActive(active: boolean): void {
878
+ if (typeof window === 'undefined') return;
879
+ const holder = window as Window & { __widgeticCodegenCount?: number };
880
+ holder.__widgeticCodegenCount = Math.max(0, (holder.__widgeticCodegenCount || 0) + (active ? 1 : -1));
881
+ }
882
+
883
+ function codegenCaptureBlocked(): boolean {
884
+ if (typeof window !== 'undefined' && ((window as Window & { __widgeticCodegenCount?: number }).__widgeticCodegenCount || 0) > 0) {
885
+ return true;
886
+ }
887
+ if (isAnyWidgetGenerating()) return true;
888
+ return /generating code|saving to repository/i.test(generateCodeStatus || '');
889
+ }
869
890
  const widgetAbortControllers = new Map<string, AbortController>();
870
891
  let currentGenerationOperationId: string | null = null;
871
892
  let generateCodeStatus: string | null = null;
@@ -900,7 +921,7 @@
900
921
  // Chat component reference and context (focused panel)
901
922
  let chatMethods: {
902
923
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
903
- addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: string[]) => void;
924
+ addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: ClarificationSuggestion[]) => void;
904
925
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
905
926
  getConversationId: () => string | null;
906
927
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -1036,7 +1057,7 @@
1036
1057
  fileSize?: number;
1037
1058
  }>,
1038
1059
  sourceWidgetId?: string,
1039
- sendOptions?: { skipIntentGate?: boolean },
1060
+ sendOptions?: { skipIntentGate?: boolean; chipIntent?: ClarificationSuggestion['intent'] },
1040
1061
  ) {
1041
1062
  const widgetId = sourceWidgetId ?? selectedWidgetId;
1042
1063
  if (!widgetId) return;
@@ -1160,6 +1181,7 @@
1160
1181
  promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
1161
1182
  persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
1162
1183
  skipIntentGate: sendOptions?.skipIntentGate === true,
1184
+ chipIntent: sendOptions?.chipIntent,
1163
1185
  });
1164
1186
  }
1165
1187
 
@@ -1410,6 +1432,76 @@
1410
1432
  return dataUrls.slice(0, 1);
1411
1433
  }
1412
1434
 
1435
+ /**
1436
+ * Keeps widget screenshots out of the canvas JSON.
1437
+ *
1438
+ * Fabric serialized each widget shape's screenshot inline as base64, so an
1439
+ * autosave stringified megabytes of image data and wrote it three times over —
1440
+ * the main-thread block the freeze reports kept naming. The canvas now stores a
1441
+ * reference and asks for the bytes back through this store: the local copy
1442
+ * first, then the widget's persisted thumbnail so a fresh browser still paints
1443
+ * the shape.
1444
+ */
1445
+ function installCanvasThumbnailStore(canvasModule: any): void {
1446
+ if (typeof canvasModule?.setShapeImageStore !== 'function') return;
1447
+ canvasModule.setShapeImageStore({
1448
+ save(ref: string, dataUrl: string) {
1449
+ // Refusing here tells the canvas to keep the bytes inline, so a save
1450
+ // before the owner is known cannot drop a screenshot on the floor.
1451
+ if (!canPersistShapeThumbnails()) throw new Error('No cache owner yet');
1452
+ saveShapeThumbnail(ref, dataUrl);
1453
+ },
1454
+ async load(ref: string): Promise<string | null> {
1455
+ const local = await readShapeThumbnail(ref);
1456
+ if (local) return local;
1457
+ const widgetId = ref.startsWith('widget:') ? ref.slice('widget:'.length) : null;
1458
+ if (!widgetId) return null;
1459
+ return await waitForWidgetThumbnailUrl(widgetId);
1460
+ }
1461
+ });
1462
+ }
1463
+
1464
+ /**
1465
+ * The canvas often loads before the widget list does, so a reference with no
1466
+ * local copy has nothing to resolve to yet. Give the list a few seconds to
1467
+ * arrive rather than leaving the shape blank until the next screenshot.
1468
+ */
1469
+ async function waitForWidgetThumbnailUrl(widgetId: string): Promise<string | null> {
1470
+ for (let attempt = 0; attempt < 12; attempt++) {
1471
+ const widget = getWidgetById(widgetId) as any;
1472
+ const url = widget?.thumbnailUrl || widget?.thumbnail_url;
1473
+ if (url) return url;
1474
+ // The canvas widget list is slim and often omits the thumbnail, so ask the
1475
+ // API for this one widget rather than leaving the shape blank.
1476
+ if (widget) return await fetchWidgetThumbnailUrl(widgetId);
1477
+ await new Promise((resolve) => setTimeout(resolve, 500));
1478
+ }
1479
+ return null;
1480
+ }
1481
+
1482
+ /** One-widget lookup for the persisted thumbnail, cached per widget. */
1483
+ const thumbnailUrlRequests = new Map<string, Promise<string | null>>();
1484
+
1485
+ function fetchWidgetThumbnailUrl(widgetId: string): Promise<string | null> {
1486
+ const pending = thumbnailUrlRequests.get(widgetId);
1487
+ if (pending) return pending;
1488
+ const request = (async () => {
1489
+ if (!widgetsClient) return null;
1490
+ try {
1491
+ const response = (await widgetsClient.getWidgetsBatch({
1492
+ getWidgetsBatchRequest: { widgetIds: [widgetId] }
1493
+ })) as { widgets?: any[]; data?: { widgets?: any[] } };
1494
+ const fetched = (response?.widgets ?? response?.data?.widgets ?? [])[0];
1495
+ return fetched?.thumbnailUrl ?? fetched?.thumbnail_url ?? null;
1496
+ } catch (error) {
1497
+ console.warn('[CreatorApp] thumbnail lookup failed', widgetId.slice(0, 8), error);
1498
+ return null;
1499
+ }
1500
+ })();
1501
+ thumbnailUrlRequests.set(widgetId, request);
1502
+ return request;
1503
+ }
1504
+
1413
1505
  async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string, isCurrent = () => true): Promise<void> {
1414
1506
  if (!widgetsClient || !widgetId || !imageDataUrl || !isCurrent()) return;
1415
1507
  try {
@@ -1551,6 +1643,7 @@
1551
1643
  buildUserPromptMessageContent(prompt),
1552
1644
  'user',
1553
1645
  persistAttachments.length > 0 ? persistAttachments : undefined,
1646
+ undefined,
1554
1647
  widgetId,
1555
1648
  );
1556
1649
  const savedAtt = saved?.attachments || saved?.data?.attachments || [];
@@ -1651,32 +1744,46 @@
1651
1744
  iframe: HTMLIFrameElement,
1652
1745
  timeoutMs = 6000,
1653
1746
  ): Promise<{ data: string | null; error: string | null }> {
1654
- if (isAnyWidgetGenerating()) {
1747
+ if (codegenCaptureBlocked()) {
1655
1748
  return Promise.resolve({ data: null, error: 'generation in progress' });
1656
1749
  }
1750
+ const captureEpoch = screenshotGenerationEpoch;
1657
1751
  return new Promise((resolve) => {
1658
1752
  if (!iframe.contentWindow) {
1659
1753
  resolve({ data: null, error: 'no-content-window' });
1660
1754
  return;
1661
1755
  }
1662
- const timeout = setTimeout(() => {
1756
+ const finish = (result: { data: string | null; error: string | null }) => {
1663
1757
  window.removeEventListener('message', handler);
1664
- resolve({ data: null, error: 'timeout' });
1758
+ clearTimeout(timeout);
1759
+ if (captureEpoch !== screenshotGenerationEpoch || isAnyWidgetGenerating()) {
1760
+ resolve({ data: null, error: 'generation in progress' });
1761
+ return;
1762
+ }
1763
+ resolve(result);
1764
+ };
1765
+ const timeout = setTimeout(() => {
1766
+ finish({ data: null, error: 'timeout' });
1665
1767
  }, timeoutMs);
1666
1768
 
1667
1769
  function handler(event: MessageEvent) {
1668
1770
  if (event.data?.type !== 'widgetic:screenshot') return;
1669
- // Ignore other widgets and synthetic replies with no source window.
1670
1771
  if (event.source !== iframe.contentWindow) return;
1671
- clearTimeout(timeout);
1672
- window.removeEventListener('message', handler);
1673
- resolve({
1772
+ finish({
1674
1773
  data: typeof event.data.data === 'string' ? event.data.data : null,
1675
1774
  error: typeof event.data.error === 'string' ? event.data.error : null,
1676
1775
  });
1677
1776
  }
1678
1777
  window.addEventListener('message', handler);
1679
- iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
1778
+ try {
1779
+ if (codegenCaptureBlocked()) {
1780
+ finish({ data: null, error: 'generation in progress' });
1781
+ return;
1782
+ }
1783
+ iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
1784
+ } catch (err) {
1785
+ finish({ data: null, error: err instanceof Error ? err.message : 'postMessage failed' });
1786
+ }
1680
1787
  });
1681
1788
  }
1682
1789
 
@@ -1689,7 +1796,7 @@
1689
1796
  // startup on the main thread this page shares with it, and the capture that
1690
1797
  // follows is refused during a generation anyway (`requestIframeScreenshot`),
1691
1798
  // so the whole path is skipped rather than paying the load for nothing.
1692
- if (isAnyWidgetGenerating()) {
1799
+ if (codegenCaptureBlocked()) {
1693
1800
  console.log('[Chat→Screenshot] Skip hidden Worker capture — codegen in progress', widgetId?.substring(0, 8));
1694
1801
  return null;
1695
1802
  }
@@ -1741,14 +1848,14 @@
1741
1848
 
1742
1849
  /** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
1743
1850
  async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
1744
- if (isAnyWidgetGenerating()) return;
1851
+ if (codegenCaptureBlocked()) return;
1745
1852
  const capturedCanvasId = getEffectiveCanvasId();
1746
1853
  const capturedCanvas = canvasRef;
1747
1854
  const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
1748
1855
  // Frame/intent identity owns this capture, not focused-panel HEAD hydration
1749
1856
  // or another widget's global generation operation.
1750
1857
  const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
1751
- && !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !isAnyWidgetGenerating()
1858
+ && !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !codegenCaptureBlocked()
1752
1859
  && capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
1753
1860
  && capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
1754
1861
  && resolveLiveWidgetShapeIframe(widgetId) === iframe;
@@ -1779,7 +1886,7 @@
1779
1886
  }
1780
1887
  // Never run background / refresh screenshot captures while code generation is running —
1781
1888
  // html2canvas inside any same-origin iframe freezes the browser main thread.
1782
- if ((quiet || requiredIframe != null) && isAnyWidgetGenerating()) {
1889
+ if (codegenCaptureBlocked()) {
1783
1890
  console.log('[Chat→Screenshot] Skip capture — generation in progress');
1784
1891
  return null;
1785
1892
  }
@@ -1807,6 +1914,10 @@
1807
1914
 
1808
1915
  // 1) Always try the visible live iframe first (published HTML from Worker includes screenshot script)
1809
1916
  if (liveIframe?.contentWindow) {
1917
+ if (codegenCaptureBlocked()) {
1918
+ console.log('[Chat→Screenshot] Skip live capture — codegen in progress', targetId.substring(0, 8));
1919
+ return null;
1920
+ }
1810
1921
  console.log('[Chat→Screenshot] Live WidgetShape iframe', {
1811
1922
  targetId: targetId.substring(0, 8),
1812
1923
  isWorker: isWorkerPreviewUrl(liveSrc),
@@ -1975,7 +2086,14 @@
1975
2086
  messageContent: m.message_content || m.messageContent || '',
1976
2087
  messageType: m.message_type || m.messageType || 'user',
1977
2088
  attachments,
2089
+ // Suggestion chips from a persisted clarification card — restored
2090
+ // on reload exactly as they were served.
2091
+ suggestions: Array.isArray(m.suggestions) && m.suggestions.length > 0 ? m.suggestions : undefined,
1978
2092
  isCommit: m.is_commit || m.isCommit || false,
2093
+ // What the turn shipped, in the commit's own words — persisted beside
2094
+ // is_commit by the codegen pipeline and rendered on the turn's commit
2095
+ // chrome (the surface that survives a reload).
2096
+ commitTitle: m.commit_title || m.commitTitle || undefined,
1979
2097
  userId: m.user_id || m.userId || '',
1980
2098
  createdAt: new Date(m.created_at || m.createdAt),
1981
2099
  updatedAt: new Date(m.updated_at || m.updatedAt || m.created_at || m.createdAt),
@@ -2148,6 +2266,7 @@
2148
2266
  file_size?: number | null;
2149
2267
  display_order?: number;
2150
2268
  }>,
2269
+ suggestions?: ClarificationSuggestion[],
2151
2270
  persistWidgetId?: string | null,
2152
2271
  ): Promise<any> {
2153
2272
  try {
@@ -2183,25 +2302,39 @@
2183
2302
  user_id: userId,
2184
2303
  is_commit: false,
2185
2304
  attachments: apiAttachments.length > 0 ? apiAttachments : undefined,
2305
+ // Clarification-card suggestions ride the same write as the reply
2306
+ // they belong to, so a reload re-renders the card with its chips.
2307
+ ...(suggestions && suggestions.length > 0 ? { suggestions } : {}),
2186
2308
  };
2187
- try {
2309
+ const persistMessage = async (payload: typeof createPayload) => {
2188
2310
  const response = await messagesClient.createMessageRaw({
2189
2311
  conversationId,
2190
- createMessageRequest: createPayload,
2312
+ createMessageRequest: payload,
2191
2313
  });
2192
2314
  const json = await response.raw.json();
2193
- // console.log('[Chat→Backend] Saved message to conversation:', conversationId);
2194
2315
  const saved = json?.data || json;
2195
2316
  const savedAtt = saved?.attachments;
2196
2317
  return {
2197
2318
  ...saved,
2198
- attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : apiAttachments,
2319
+ attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : (payload.attachments || []),
2199
2320
  };
2321
+ };
2322
+ try {
2323
+ return await persistMessage(createPayload);
2200
2324
  } catch (withAttachmentsError) {
2201
- console.error(
2202
- '[Chat→Backend] Save with attachments failed — not falling back to text-only:',
2203
- withAttachmentsError,
2204
- );
2325
+ if (apiAttachments.length > 0) {
2326
+ console.warn(
2327
+ '[Chat-Backend] Save with attachments failed — retrying text-only so the turn is not lost:',
2328
+ withAttachmentsError,
2329
+ );
2330
+ try {
2331
+ return await persistMessage({ ...createPayload, attachments: undefined });
2332
+ } catch (textOnlyError) {
2333
+ console.error('[Chat-Backend] Text-only message save also failed:', textOnlyError);
2334
+ throw textOnlyError;
2335
+ }
2336
+ }
2337
+ console.error('[Chat-Backend] Message save failed:', withAttachmentsError);
2205
2338
  throw withAttachmentsError;
2206
2339
  }
2207
2340
  } catch (error) {
@@ -2242,34 +2375,47 @@
2242
2375
  null;
2243
2376
  // console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
2244
2377
  if (gitRollback?.success) {
2245
- // Fast path: a recorded version at this commit has an immutable cached
2246
- // artifact — reuse it instead of the forced repo rebuild + retry ladder.
2247
- let previewFromVersionCache = false;
2248
- if (targetCommit) {
2249
- lastCommitId = targetCommit;
2250
- if (panelWidgetId) {
2251
- patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
2252
- patchCodegenUiState(panelWidgetId, {});
2253
- // Message-extracted SHAs may be short (7+ chars); compare by prefix.
2254
- const targetSha = String(targetCommit).toLowerCase();
2255
- const matchedVersion = (resolveWidgetShapeVersionOptions(panelWidgetId) ?? [])
2256
- .find((option) => {
2257
- const optionSha = String(option.commitSha ?? '').toLowerCase();
2258
- return optionSha.length >= 7 && targetSha.length >= 7
2259
- && (optionSha === targetSha || optionSha.startsWith(targetSha) || targetSha.startsWith(optionSha));
2260
- });
2261
- if (matchedVersion) {
2262
- shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${panelWidgetId}:v:${matchedVersion.id.slice(2)}`]: null };
2263
- shapePreviewSourceByWidgetId = { ...shapePreviewSourceByWidgetId, [panelWidgetId]: matchedVersion.id };
2264
- handleWidgetShapeVersionChange(panelWidgetId, matchedVersion.id);
2265
- previewFromVersionCache = true;
2378
+ // Restore changes Git HEAD (often a new rollback commit). Never reuse a
2379
+ // published CDN artifact — that is Switch Version, and it hid the rebuild.
2380
+ const rollbackHeadSha =
2381
+ gitRollback.commitId ?? gitRollback.commit_id ?? targetCommit ?? null;
2382
+ const historicalSha = targetCommit || rollbackHeadSha;
2383
+ if (panelWidgetId) {
2384
+ if (rollbackHeadSha) {
2385
+ lastCommitId = rollbackHeadSha;
2386
+ saveCommitIdForWidget(panelWidgetId, rollbackHeadSha);
2387
+ patchPanelSnapshot(panelWidgetId, { lastCommitId: rollbackHeadSha });
2388
+ patchCodegenUiState(panelWidgetId, { lastCommitId: rollbackHeadSha });
2389
+ }
2390
+ const targetSha = String(historicalSha || '').toLowerCase();
2391
+ const matchedVersion = (resolveWidgetShapeVersionOptions(panelWidgetId) ?? [])
2392
+ .find((option) => {
2393
+ const optionSha = String(option.commitSha ?? '').toLowerCase();
2394
+ return optionSha.length >= 7 && targetSha.length >= 7
2395
+ && (optionSha === targetSha || optionSha.startsWith(targetSha) || targetSha.startsWith(optionSha));
2396
+ });
2397
+ if (matchedVersion?.id?.startsWith('v:')) {
2398
+ const restoredVersion = parseInt(matchedVersion.id.slice(2), 10);
2399
+ if (Number.isFinite(restoredVersion)) {
2400
+ restoredViewingVersionByWidgetId = {
2401
+ ...restoredViewingVersionByWidgetId,
2402
+ [panelWidgetId]: restoredVersion,
2403
+ };
2404
+ widgetShapeLivePreviewsRef?.setSelectedVersion?.(panelWidgetId, `v:${restoredVersion}`);
2266
2405
  }
2267
2406
  }
2407
+ shapePreviewSourceByWidgetId = {
2408
+ ...shapePreviewSourceByWidgetId,
2409
+ [panelWidgetId]: 'preview',
2410
+ };
2411
+ getWidgetDetailsRef(panelWidgetId)?.clearCodegenPreviewFresh?.();
2412
+ syncWidgetShapeLivePreviews();
2413
+ panelDisplayRevision += 1;
2268
2414
  }
2269
2415
  showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
2270
2416
  description: undoMode
2271
- ? `Code reverted to commit ${formatCommitShaShort(targetCommit)}. Refreshing preview…`
2272
- : `Widget code at commit ${formatCommitShaShort(targetCommit)}. Refreshing preview…`,
2417
+ ? `Code reverted to commit ${formatCommitShaShort(historicalSha)}. Refreshing preview…`
2418
+ : `Widget code at commit ${formatCommitShaShort(historicalSha)}. Refreshing preview…`,
2273
2419
  duration: 6000,
2274
2420
  });
2275
2421
  liveDesignValues = {};
@@ -2278,18 +2424,12 @@
2278
2424
  compositionDesignSchema = null;
2279
2425
  compositionContentSchema = null;
2280
2426
  pendingCreatePreviewDefaultsWidgetId = null;
2281
- _isRestoringCheckpoint = !previewFromVersionCache;
2282
- if (previewFromVersionCache) {
2283
- // Immutable artifact already serves the preview; no rebuild ladder.
2284
- syncWidgetShapeLivePreviews();
2285
- setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
2286
- return { ...data, previewFromVersionCache };
2287
- }
2427
+ _isRestoringCheckpoint = true;
2288
2428
  const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
2289
2429
  const scheduleBuildRetry = async () => {
2290
- const delays = [2500, 5000, 8000];
2430
+ const delays = [400, 2500, 5000, 8000];
2291
2431
  for (let i = 0; i < delays.length; i++) {
2292
- await new Promise((r) => setTimeout(r, delays[i]));
2432
+ if (i > 0) await new Promise((r) => setTimeout(r, delays[i]));
2293
2433
  if (i === 0) suppressNextBuildFromRepoChatMessage = true;
2294
2434
  try {
2295
2435
  const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
@@ -2303,7 +2443,9 @@
2303
2443
  [panelWidgetId]: 'preview',
2304
2444
  };
2305
2445
  syncWidgetShapeLivePreviews();
2306
- setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
2446
+ widgetShapeLivePreviewsRef?.requestReload?.(panelWidgetId);
2447
+ panelDisplayRevision += 1;
2448
+ scheduleIdleThumbnailPersist(panelWidgetId);
2307
2449
  }
2308
2450
  return;
2309
2451
  }
@@ -3863,9 +4005,7 @@
3863
4005
  };
3864
4006
  await compositionsClient.updateComposition({
3865
4007
  id: selectedComposition.id,
3866
- // The gateway accepts embedSettings (compositionSchemas update schema);
3867
- // the generated UpdateComposition model predates that field.
3868
- updateComposition: { embedSettings } as unknown as UpdateComposition
4008
+ updateComposition: { embedSettings }
3869
4009
  });
3870
4010
  selectedComposition = { ...selectedComposition, embedSettings };
3871
4011
  embedDimensionsChanged = false;
@@ -4004,6 +4144,11 @@
4004
4144
 
4005
4145
  // debugMode — current ON/OFF state. User can still toggle when the UI is available.
4006
4146
  let debugMode = debugModeAvailable;
4147
+ if (typeof localStorage !== 'undefined' && debugModeAvailable) {
4148
+ const storedDebugMode = localStorage.getItem('widgetic:site-debug-on');
4149
+ if (storedDebugMode === '1' || storedDebugMode === 'true') debugMode = true;
4150
+ if (storedDebugMode === '0' || storedDebugMode === 'false') debugMode = false;
4151
+ }
4007
4152
  // console.log('[WidgetCreator] Debug from host/env', {
4008
4153
  // hostProvidedDebug,
4009
4154
  // initialDebugMode,
@@ -4188,6 +4333,11 @@
4188
4333
  function toggleDebugMode() {
4189
4334
  if (!debugModeAvailable) return;
4190
4335
  debugMode = !debugMode;
4336
+ try {
4337
+ localStorage.setItem('widgetic:site-debug-on', debugMode ? '1' : '0');
4338
+ } catch (err) {
4339
+ console.warn('[WidgetCreator] Could not persist debug mode', err);
4340
+ }
4191
4341
  // When turning OFF: switches to default model (from API config), mock console
4192
4342
  // When turning ON: enables model selector, real-time console
4193
4343
  const effectiveModel = debugMode ? userSelectedModelId : defaultModelId;
@@ -4386,9 +4536,8 @@
4386
4536
  const anyW = widget as { thumbnailUrl?: string | null; thumbnail_url?: string | null } | null;
4387
4537
  const hasThumb = !!(anyW?.thumbnailUrl || anyW?.thumbnail_url);
4388
4538
  if (!hasThumb) {
4389
- setTimeout(() => {
4390
- void handleRefreshScreenshot(widgetId, { quiet: true });
4391
- }, 1200);
4539
+ // A capture 1s after open runs html2canvas on the shared main thread.
4540
+ scheduleIdleThumbnailPersist(widgetId);
4392
4541
  }
4393
4542
  return true;
4394
4543
  }
@@ -7665,11 +7814,18 @@
7665
7814
  const ctx = canvas.getContext('2d');
7666
7815
  if (!ctx) return dataUrl;
7667
7816
  ctx.drawImage(img, 0, 0, width, height);
7817
+ await new Promise((resolve) => setTimeout(resolve, 0));
7818
+ const encode = (quality: number) =>
7819
+ new Promise<string>((resolve) => {
7820
+ setTimeout(() => {
7821
+ resolve(canvas.toDataURL('image/jpeg', quality));
7822
+ }, 0);
7823
+ });
7668
7824
  let quality = 0.82;
7669
- let out = canvas.toDataURL('image/jpeg', quality);
7825
+ let out = await encode(quality);
7670
7826
  while (out.length > maxPayloadChars && quality > 0.45) {
7671
7827
  quality -= 0.08;
7672
- out = canvas.toDataURL('image/jpeg', quality);
7828
+ out = await encode(quality);
7673
7829
  }
7674
7830
  // console.log('[WidgetCreator] Compressed reference image for codegen', {
7675
7831
  // fromChars: dataUrl.length,
@@ -7748,6 +7904,7 @@
7748
7904
  */
7749
7905
  /** Debounced quiet sync: live Worker preview → canvas WidgetShape thumbnail. */
7750
7906
  const canvasShapePreviewSyncTimers = new Map<string, ReturnType<typeof setTimeout>>();
7907
+ const quietScreenshotTimers = new Map<string, ReturnType<typeof setTimeout>>();
7751
7908
  function cancelCanvasShapePreviewSync(widgetId?: string | null): void {
7752
7909
  if (widgetId) {
7753
7910
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
@@ -7758,13 +7915,86 @@
7758
7915
  for (const timer of canvasShapePreviewSyncTimers.values()) clearTimeout(timer);
7759
7916
  canvasShapePreviewSyncTimers.clear();
7760
7917
  }
7918
+ function cancelQuietScreenshots(widgetId?: string | null): void {
7919
+ if (widgetId) {
7920
+ const existing = quietScreenshotTimers.get(widgetId);
7921
+ if (existing) clearTimeout(existing);
7922
+ quietScreenshotTimers.delete(widgetId);
7923
+ cancelCanvasShapePreviewSync(widgetId);
7924
+ return;
7925
+ }
7926
+ for (const timer of quietScreenshotTimers.values()) clearTimeout(timer);
7927
+ quietScreenshotTimers.clear();
7928
+ cancelCanvasShapePreviewSync();
7929
+ }
7930
+ function scheduleQuietScreenshot(widgetId: string, delayMs = 2000): void {
7931
+ if (!widgetId || codegenCaptureBlocked()) return;
7932
+ const existing = quietScreenshotTimers.get(widgetId);
7933
+ if (existing) clearTimeout(existing);
7934
+ const timer = setTimeout(() => {
7935
+ quietScreenshotTimers.delete(widgetId);
7936
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) return;
7937
+ void handleRefreshScreenshot(widgetId, { quiet: true });
7938
+ }, delayMs);
7939
+ quietScreenshotTimers.set(widgetId, timer);
7940
+ }
7941
+ /**
7942
+ * Latest widget capture for the next LLM turn, without freezing a generation.
7943
+ * html2canvas shares the page main thread, so this waits until codegen has
7944
+ * been idle, then persists thumbnail_url only (no canvas JSON serialize).
7945
+ */
7946
+ const idleThumbnailTimers = new Map<string, ReturnType<typeof setTimeout>>();
7947
+ const IDLE_THUMBNAIL_DELAY_MS = 12000;
7948
+ function cancelIdleThumbnailPersist(widgetId?: string | null): void {
7949
+ if (widgetId) {
7950
+ const existing = idleThumbnailTimers.get(widgetId);
7951
+ if (existing) clearTimeout(existing);
7952
+ idleThumbnailTimers.delete(widgetId);
7953
+ return;
7954
+ }
7955
+ for (const timer of idleThumbnailTimers.values()) clearTimeout(timer);
7956
+ idleThumbnailTimers.clear();
7957
+ }
7958
+ function scheduleIdleThumbnailPersist(widgetId: string): void {
7959
+ if (!widgetId) return;
7960
+ cancelIdleThumbnailPersist(widgetId);
7961
+ const timer = setTimeout(() => {
7962
+ idleThumbnailTimers.delete(widgetId);
7963
+ const run = () => {
7964
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) {
7965
+ scheduleIdleThumbnailPersist(widgetId);
7966
+ return;
7967
+ }
7968
+ void persistIdleWidgetThumbnail(widgetId);
7969
+ };
7970
+ if (typeof requestIdleCallback === 'function') {
7971
+ requestIdleCallback(() => run(), { timeout: 8000 });
7972
+ } else {
7973
+ run();
7974
+ }
7975
+ }, IDLE_THUMBNAIL_DELAY_MS);
7976
+ idleThumbnailTimers.set(widgetId, timer);
7977
+ }
7978
+ async function persistIdleWidgetThumbnail(widgetId: string): Promise<void> {
7979
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) return;
7980
+ const iframe = resolveLiveWidgetShapeIframe(widgetId);
7981
+ if (!iframe?.contentWindow) return;
7982
+ try {
7983
+ const screenshot = await handleChatScreenshot(widgetId, iframe, true);
7984
+ if (!screenshot || codegenCaptureBlocked() || generatingWidgetIds[widgetId]) return;
7985
+ await persistWidgetListThumbnail(widgetId, screenshot);
7986
+ console.log('[Chat→Screenshot] Idle thumbnail persisted', widgetId.substring(0, 8));
7987
+ } catch (error) {
7988
+ console.warn('[Chat→Screenshot] Idle thumbnail persist failed', widgetId, error);
7989
+ }
7990
+ }
7761
7991
  function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
7762
- if (!widgetId || isAnyWidgetGenerating()) return;
7992
+ if (!widgetId || codegenCaptureBlocked()) return;
7763
7993
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
7764
7994
  if (existing) clearTimeout(existing);
7765
7995
  const timer = setTimeout(() => {
7766
7996
  canvasShapePreviewSyncTimers.delete(widgetId);
7767
- if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
7997
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) {
7768
7998
  // console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
7769
7999
  return;
7770
8000
  }
@@ -7795,8 +8025,8 @@
7795
8025
 
7796
8026
  // Never run quiet background screenshot captures while ANY code generation is running —
7797
8027
  // 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));
8028
+ if (codegenCaptureBlocked()) {
8029
+ console.log('[handleRefreshScreenshot] Skip capture — codegen in progress', widgetId?.substring(0, 8));
7800
8030
  return;
7801
8031
  }
7802
8032
 
@@ -7897,6 +8127,12 @@
7897
8127
  });
7898
8128
  }
7899
8129
  window.addEventListener('message', handler);
8130
+ if (codegenCaptureBlocked()) {
8131
+ clearTimeout(timeout);
8132
+ window.removeEventListener('message', handler);
8133
+ resolve({ data: null, error: 'generation in progress' });
8134
+ return;
8135
+ }
7900
8136
  iframe.contentWindow!.postMessage({ type: 'widgetic:request-screenshot' }, '*');
7901
8137
  });
7902
8138
 
@@ -8017,6 +8253,8 @@ Requirements:
8017
8253
  promptImagesOverride?: string[];
8018
8254
  /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8019
8255
  skipIntentGate?: boolean;
8256
+ /** The tapped chip's structured intent, declared so the server measures the chip conversion without matching copy. */
8257
+ chipIntent?: ClarificationSuggestion['intent'];
8020
8258
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
8021
8259
  persistFileAttachments?: Array<{
8022
8260
  url?: string;
@@ -8107,6 +8345,7 @@ Requirements:
8107
8345
  promptImagesOverride: options.promptImagesOverride,
8108
8346
  persistFileAttachments: options.persistFileAttachments,
8109
8347
  skipIntentGate: options.skipIntentGate,
8348
+ chipIntent: options.chipIntent,
8110
8349
  });
8111
8350
  }
8112
8351
 
@@ -8130,6 +8369,8 @@ Requirements:
8130
8369
  promptImagesOverride?: string[];
8131
8370
  /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8132
8371
  skipIntentGate?: boolean;
8372
+ /** The tapped chip's structured intent, declared so the server measures the chip conversion without matching copy. */
8373
+ chipIntent?: ClarificationSuggestion['intent'];
8133
8374
  persistFileAttachments?: Array<{
8134
8375
  url?: string;
8135
8376
  file?: File;
@@ -8145,7 +8386,6 @@ Requirements:
8145
8386
 
8146
8387
  const panelChat = getChatMethodsForWidget(generationWidgetId);
8147
8388
  const panelSnap = getPanelState(generationWidgetId);
8148
- const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
8149
8389
 
8150
8390
  // Per-widget abort: only abort if the SAME widget already has a running generation
8151
8391
  const existingController = widgetAbortControllers.get(generationWidgetId);
@@ -8158,6 +8398,22 @@ Requirements:
8158
8398
  const signal = abortController.signal;
8159
8399
 
8160
8400
  generatingWidgetIds = { ...generatingWidgetIds, [generationWidgetId]: true };
8401
+ noteCodegenActive(true);
8402
+ screenshotGenerationEpoch += 1;
8403
+ document.querySelectorAll('iframe').forEach((frame) => {
8404
+ try {
8405
+ frame.contentWindow?.postMessage({ type: 'widgetic:abort-screenshot' }, '*');
8406
+ } catch (err) {
8407
+ console.warn('[WidgetCreator] Could not abort in-flight screenshot', err);
8408
+ }
8409
+ });
8410
+ cancelQuietScreenshots();
8411
+ cancelIdleThumbnailPersist();
8412
+ widgetShapeLivePreviewsRef?.cancelReloads?.();
8413
+ if (restoredViewingVersionByWidgetId[generationWidgetId] != null) {
8414
+ const { [generationWidgetId]: _droppedRestore, ...restRestored } = restoredViewingVersionByWidgetId;
8415
+ restoredViewingVersionByWidgetId = restRestored;
8416
+ }
8161
8417
  lastHarnessChatLine = '';
8162
8418
 
8163
8419
  // Show user prompt in chat when triggered outside Chat (confirm dialog / Generate Now).
@@ -8371,6 +8627,7 @@ Requirements:
8371
8627
  runtimeLogs: runtimeLogsForRequest,
8372
8628
  compileFixOnly: options.compileFixOnly === true,
8373
8629
  skipIntentGate: options.skipIntentGate === true,
8630
+ chipIntent: options.chipIntent,
8374
8631
  } as GenerateCodeRequest
8375
8632
  });
8376
8633
  // 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);
@@ -8385,7 +8642,7 @@ Requirements:
8385
8642
  if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
8386
8643
  const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
8387
8644
  const gatedReply: string = gatedData.reply ?? '';
8388
- const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8645
+ const suggestions: ClarificationSuggestion[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8389
8646
 
8390
8647
  clearActiveOperationId();
8391
8648
  patchCodegenUiState(generationWidgetId, {
@@ -8597,8 +8854,10 @@ Requirements:
8597
8854
  shapeHistoryResolvedByWidgetId = restResolved;
8598
8855
  setTimeout(() => {
8599
8856
  void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
8600
- syncWidgetShapeLivePreviews();
8601
- if (isFocusedPanel) panelDisplayRevision += 1;
8857
+ const newestDraft = getNewestDraftVersionId(generationWidgetId);
8858
+ if (newestDraft != null) {
8859
+ selectVersionInDropdowns(generationWidgetId, `v:${newestDraft}`);
8860
+ }
8602
8861
  });
8603
8862
  }, 1200);
8604
8863
  }
@@ -8770,11 +9029,9 @@ Requirements:
8770
9029
  [generationWidgetId]: 'preview',
8771
9030
  };
8772
9031
  syncWidgetShapeLivePreviews();
8773
- // The Worker serves the rebuilt code at the SAME /preview URL — sync()
8774
- // only reloads when src changes, so force a cache-busted iframe reload
8775
- // on the canvas shape overlay or it keeps showing the previous version.
9032
+ // WS preview-ready was deferred during codegen so the Worker
9033
+ // iframe does not boot on the shared main thread at 80%.
8776
9034
  widgetShapeLivePreviewsRef?.requestReload?.(generationWidgetId);
8777
- panelDisplayRevision += 1;
8778
9035
  if (!completedPreviewBuildErr) {
8779
9036
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
8780
9037
  pendingCreatePreviewDefaultsWidgetId = null;
@@ -8788,15 +9045,10 @@ Requirements:
8788
9045
  await panelChat?.reloadConversationMessages?.();
8789
9046
  panelChat?.markWidgetHeadAtLatestCommit?.();
8790
9047
  }, 2500);
8791
- setTimeout(() => {
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
- }
8799
- }, 3000);
9048
+ scheduleIdleThumbnailPersist(generationWidgetId);
9049
+ // Do not html2canvas here. Capture during/after generation freezes
9050
+ // the tab (iframe shares the main thread). Thumb for the next prompt
9051
+ // is persisted later, only while the tab is idle.
8800
9052
  // Do NOT auto-publish — user previews first, then clicks Publish / Set live.
8801
9053
  } else {
8802
9054
  panelChat?.updateLatestCodegenStatusMessage?.(
@@ -8874,6 +9126,7 @@ Requirements:
8874
9126
  showToast('error', 'Code generation error', { description: message, duration: 6000 });
8875
9127
  pushCodegenFailureToChat(generationWidgetId, message);
8876
9128
  } finally {
9129
+ noteCodegenActive(false);
8877
9130
  const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
8878
9131
  generatingWidgetIds = restFinally;
8879
9132
  widgetAbortControllers.delete(generationWidgetId);
@@ -9032,7 +9285,7 @@ Requirements:
9032
9285
  // rather than letting them silently become version 0.
9033
9286
  const rawTargetVersion = publishOpts.targetVersion;
9034
9287
  const targetVersion =
9035
- rawTargetVersion == null || rawTargetVersion === ''
9288
+ rawTargetVersion == null || (typeof rawTargetVersion === 'string' && rawTargetVersion === '')
9036
9289
  ? null
9037
9290
  : Number.isFinite(Number(rawTargetVersion))
9038
9291
  ? Number(rawTargetVersion)
@@ -10221,8 +10474,8 @@ Requirements:
10221
10474
  record?.artifact_url ||
10222
10475
  (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10223
10476
  if (isPublished && artifact) {
10224
- const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10225
- if (url) return url;
10477
+ const isolated = resolveWidgetShapeVersionPreviewState(widgetId);
10478
+ if (isolated?.url) return isolated.url;
10226
10479
  }
10227
10480
  const previewState = resolveWidgetShapeVersionPreviewState(widgetId);
10228
10481
  if (previewState?.url) return previewState.url;
@@ -10252,6 +10505,10 @@ Requirements:
10252
10505
  * Publish history is loaded without requiring chat to be open.
10253
10506
  */
10254
10507
  let shapePreviewSourceByWidgetId: Record<string, string> = {};
10508
+ /** Dropdown label only. Changing it must not reload the shape iframe. */
10509
+ let dropdownVersionByWidgetId: Record<string, string> = {};
10510
+ /** After Restore Checkpoint, dropdowns stay on the restored version number while preview is Worker HEAD. */
10511
+ let restoredViewingVersionByWidgetId: Record<string, number> = {};
10255
10512
  // Historical previews are never interchangeable with the shared HEAD preview.
10256
10513
  let shapeVersionPreviewByWidgetId: Record<string, {
10257
10514
  versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
@@ -10365,12 +10622,21 @@ Requirements:
10365
10622
  (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10366
10623
 
10367
10624
  if (isPublished && artifact) {
10368
- const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10369
- if (url) return url;
10625
+ const isolated = resolveWidgetShapeVersionPreviewState(widgetId);
10626
+ if (isolated?.url && isolated.versionId === `v:${version}`) return isolated.url;
10627
+ // Do not return Live CDN here — Switch Version must wait for the
10628
+ // commit-specific build, otherwise the iframe stays on the last publish.
10370
10629
  }
10371
- // The newest draft IS repo HEAD — Worker `/preview/{widgetId}` already serves this code!
10630
+ // The newest draft is repo HEAD only while HEAD still points at its commit.
10631
+ // A Restore Checkpoint writes a rollback commit on top, so the shared
10632
+ // `/preview/{widgetId}` artifact then serves the restored code under the
10633
+ // newest version's number — ask for a commit-specific build instead.
10372
10634
  const newestDraft = getNewestDraftVersionId(widgetId);
10373
- if (!isPublished && newestDraft != null && version === newestDraft && workerUrl) {
10635
+ const draftCommitSha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha ?? null);
10636
+ const headCommitSha = normalizeCommitSha(ps.lastCommitId ?? loadCommitIdForWidget(widgetId));
10637
+ const draftIsRepoHead =
10638
+ !draftCommitSha || !headCommitSha || commitsMatch(draftCommitSha, headCommitSha);
10639
+ if (!isPublished && newestDraft != null && version === newestDraft && draftIsRepoHead && workerUrl) {
10374
10640
  return workerUrl;
10375
10641
  }
10376
10642
  return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
@@ -10389,6 +10655,8 @@ Requirements:
10389
10655
  }
10390
10656
 
10391
10657
  function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
10658
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10659
+ if (Number.isFinite(restored)) return `v${restored}`;
10392
10660
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
10393
10661
  const ps = getPanelDisplayState(widgetId);
10394
10662
  const liveVersion =
@@ -10451,6 +10719,7 @@ Requirements:
10451
10719
  dateLabel?: string;
10452
10720
  isLive?: boolean;
10453
10721
  isDraft?: boolean;
10722
+ commitSha?: string;
10454
10723
  }> = [];
10455
10724
  for (const record of history) {
10456
10725
  const version = Number(record.version);
@@ -10489,6 +10758,10 @@ Requirements:
10489
10758
  }
10490
10759
 
10491
10760
  function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
10761
+ const menuVersion = dropdownVersionByWidgetId[widgetId];
10762
+ if (menuVersion) return menuVersion;
10763
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10764
+ if (Number.isFinite(restored)) return `v:${restored}`;
10492
10765
  const source = shapePreviewSourceByWidgetId[widgetId];
10493
10766
  if (source?.startsWith('v:')) return source;
10494
10767
  if (source === 'preview') {
@@ -10504,8 +10777,21 @@ Requirements:
10504
10777
  return liveVersion != null ? `v:${liveVersion}` : null;
10505
10778
  }
10506
10779
 
10780
+ function selectVersionInDropdowns(widgetId: string, versionId: string): void {
10781
+ dropdownVersionByWidgetId = { ...dropdownVersionByWidgetId, [widgetId]: versionId };
10782
+ widgetShapeLivePreviewsRef?.setSelectedVersion?.(widgetId, versionId);
10783
+ panelDisplayRevision += 1;
10784
+ }
10785
+
10507
10786
  function handleWidgetShapeVersionChange(widgetId: string, versionId: string, buildPreview = true): void {
10508
10787
  if (!versionId) return;
10788
+ // Both dropdowns show the pick. Only the dropdown the user clicked reloads the preview.
10789
+ selectVersionInDropdowns(widgetId, versionId);
10790
+ if (!buildPreview) return;
10791
+ if (restoredViewingVersionByWidgetId[widgetId] != null) {
10792
+ const { [widgetId]: _dropped, ...restRestored } = restoredViewingVersionByWidgetId;
10793
+ restoredViewingVersionByWidgetId = restRestored;
10794
+ }
10509
10795
  // Re-selecting an already successful/in-flight preview must not reset its state.
10510
10796
  const currentPreview = resolveWidgetShapeVersionPreviewState(widgetId);
10511
10797
  if (shapePreviewSourceByWidgetId[widgetId] === versionId
@@ -10524,17 +10810,19 @@ Requirements:
10524
10810
  };
10525
10811
  syncWidgetShapeLivePreviews();
10526
10812
  panelDisplayRevision += 1;
10527
- // Explicit Publish builds its own target; do not race it with a preview build.
10528
- if (!buildPreview) return;
10529
- // Published artifacts need no build. Drafts must never substitute HEAD.
10813
+ // Always compile the selected commit into an isolated Worker preview.
10814
+ // Published CDN artifacts are Live only — using them on Switch made
10815
+ // draft/restore look like the last publish even when git had moved.
10530
10816
  if (versionId.startsWith('v:')) {
10531
10817
  const version = Number(versionId.slice(2));
10532
- const directUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10533
- if (directUrl) {
10534
- const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10535
- console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
10818
+ const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10819
+ const isPublished = !!(record?.publishedAt ?? record?.published_at);
10820
+ const newestDraft = getNewestDraftVersionId(widgetId);
10821
+ const workerUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10822
+ const viewingHeadDraft = !isPublished && newestDraft != null && version === newestDraft && workerUrl && !restoredViewingVersionByWidgetId[widgetId];
10823
+ if (viewingHeadDraft) {
10536
10824
  const detailsRef = getWidgetDetailsRef(widgetId);
10537
- detailsRef?.showPublishedArtifact?.(directUrl);
10825
+ detailsRef?.showPublishedArtifact?.(workerUrl);
10538
10826
  } else {
10539
10827
  void ensureWidgetShapeVersionPreview(widgetId, version);
10540
10828
  }
@@ -10613,6 +10901,13 @@ Requirements:
10613
10901
  /** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
10614
10902
  function resolveViewingCommitShaForPanel(widgetId: string): string | null {
10615
10903
  const ps = getPanelDisplayState(widgetId);
10904
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10905
+ if (Number.isFinite(restored)) {
10906
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10907
+ const record = history.find((r) => Number(r.version) === Number(restored));
10908
+ const fromRestored = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
10909
+ if (fromRestored) return fromRestored;
10910
+ }
10616
10911
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
10617
10912
  if (source === 'preview') {
10618
10913
  return normalizeCommitSha(ps.lastCommitId);
@@ -10663,18 +10958,28 @@ Requirements:
10663
10958
  * debug mode and this label is what the turn shows.
10664
10959
  */
10665
10960
  let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10961
+ let versionPromptLabelsByWidgetId: Record<string, Record<string, string>> = {};
10962
+ let viewingVersionByWidgetId: Record<string, number | null> = {};
10666
10963
  $: {
10667
10964
  void shapePreviewSourceByWidgetId;
10668
10965
  void shapeVersionPreviewByWidgetId;
10966
+ void restoredViewingVersionByWidgetId;
10967
+ void dropdownVersionByWidgetId;
10669
10968
  void panelDisplayRevision;
10670
10969
  const nextHeads: Record<string, string | null> = {};
10671
10970
  const nextLabels: Record<string, Record<string, string>> = {};
10971
+ const nextPrompts: Record<string, Record<string, string>> = {};
10972
+ const nextViewing: Record<string, number | null> = {};
10672
10973
  for (const widgetId of openWidgetPanelIds) {
10673
10974
  nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
10674
10975
  nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
10976
+ nextPrompts[widgetId] = buildVersionPromptLabelsForPanel(widgetId);
10977
+ nextViewing[widgetId] = viewingVersionNumberForPanel(widgetId);
10675
10978
  }
10676
10979
  headCommitShaByWidgetId = nextHeads;
10677
10980
  versionLabelsByWidgetId = nextLabels;
10981
+ versionPromptLabelsByWidgetId = nextPrompts;
10982
+ viewingVersionByWidgetId = nextViewing;
10678
10983
  }
10679
10984
 
10680
10985
  /** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
@@ -10683,24 +10988,55 @@ Requirements:
10683
10988
  return normalized ? normalized.slice(0, 7) : null;
10684
10989
  }
10685
10990
 
10991
+ function assignVersionLabelKeys(labels: Record<string, string>, sha: string | null | undefined, label: string): void {
10992
+ const normalized = normalizeCommitSha(sha);
10993
+ if (!normalized) return;
10994
+ labels[normalized] = label;
10995
+ labels[normalized.slice(0, 7)] = label;
10996
+ labels[normalized.slice(0, 8)] = label;
10997
+ }
10998
+
10686
10999
  function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
10687
11000
  const labels: Record<string, string> = {};
10688
11001
  const ps = getPanelDisplayState(widgetId);
10689
11002
  const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10690
11003
  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}`;
11004
+ if (record?.version == null) continue;
11005
+ assignVersionLabelKeys(labels, record?.commitSha ?? record?.commit_sha, `Version ${record.version}`);
10693
11006
  }
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
11007
  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)}`;
11008
+ if (previewState?.versionId?.startsWith('v:')) {
11009
+ assignVersionLabelKeys(labels, previewState?.commitSha, `Version ${previewState.versionId.slice(2)}`);
11010
+ }
11011
+ return labels;
11012
+ }
11013
+
11014
+ function versionPromptKey(prompt: string | null | undefined): string | null {
11015
+ const text = String(prompt || '').replace(/\s+/g, ' ').trim();
11016
+ return text || null;
11017
+ }
11018
+
11019
+ /** User prompt → "Version N", for turns whose commit line was never saved. */
11020
+ function buildVersionPromptLabelsForPanel(widgetId: string): Record<string, string> {
11021
+ const labels: Record<string, string> = {};
11022
+ const ps = getPanelDisplayState(widgetId);
11023
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
11024
+ for (const record of history || []) {
11025
+ if (record?.version == null) continue;
11026
+ const key = versionPromptKey(record?.prompt);
11027
+ if (!key) continue;
11028
+ labels[key] = `Version ${record.version}`;
10700
11029
  }
10701
11030
  return labels;
10702
11031
  }
10703
11032
 
11033
+ function viewingVersionNumberForPanel(widgetId: string): number | null {
11034
+ const selected = resolveWidgetShapeVersionSelectedId(widgetId);
11035
+ if (!selected?.startsWith('v:')) return null;
11036
+ const version = parseInt(selected.slice(2), 10);
11037
+ return Number.isFinite(version) ? version : null;
11038
+ }
11039
+
10704
11040
  function syncWidgetShapeLivePreviews(): void {
10705
11041
  measureSync('sync-widget-shape-previews', () => {
10706
11042
  try {
@@ -10935,10 +11271,13 @@ Requirements:
10935
11271
  if (embedded && deepLinkId && widgetId === deepLinkId) {
10936
11272
  embeddedDeepLinkPanelDismissed = false;
10937
11273
  }
10938
- // One details panel at a time — close any other open widget first.
10939
- for (const openId of [...openWidgetPanelIds]) {
10940
- if (openId !== widgetId) {
10941
- closeWidgetDetailsPanel(openId, { fromShapeSwitch: true });
11274
+ // At most two chats. Opening a third closes the oldest one.
11275
+ const maxOpenWidgetChats = 2;
11276
+ if (!openWidgetPanelIds.includes(widgetId)) {
11277
+ while (openWidgetPanelIds.length >= maxOpenWidgetChats) {
11278
+ const oldestId = openWidgetPanelIds[0];
11279
+ if (!oldestId) break;
11280
+ closeWidgetDetailsPanel(oldestId, { fromShapeSwitch: true });
10942
11281
  }
10943
11282
  }
10944
11283
  const isNewPanel = !openWidgetPanelIds.includes(widgetId);
@@ -11946,7 +12285,7 @@ Requirements:
11946
12285
  * from whatever HEAD happens to contain.
11947
12286
  */
11948
12287
  function canPublishDraftVersionForPanel(widgetId: string, version: number | null | undefined): boolean {
11949
- if (version == null || version === '' || !Number.isFinite(Number(version))) return false;
12288
+ if (version == null || (typeof version === 'string' && version === '') || !Number.isFinite(Number(version))) return false;
11950
12289
  if (isPublishing && publishingWidgetId === widgetId) return false;
11951
12290
  if (isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId) return false;
11952
12291
  if (generatingWidgetIds[widgetId]) return false;
@@ -12075,7 +12414,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
12075
12414
  }
12076
12415
  if (publishWidgetId) {
12077
12416
  // console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
12078
- setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
12417
+ scheduleQuietScreenshot(publishWidgetId, 2000);
12079
12418
  }
12080
12419
  if (embedded && onPublishComplete && publishWidgetId) {
12081
12420
  onPublishComplete({
@@ -12123,7 +12462,7 @@ $: if ($widgetPublishStatus.widgetId === selectedWidgetId && $widgetPublishStatu
12123
12462
  // console.log('[CrossTab/WS] Publish building status from WebSocket — disabling Publish button');
12124
12463
  }
12125
12464
 
12126
- // Agent harness repair status (DW Phase 2) — real-time via vmConsole:aiderOutput WebSocket.
12465
+ // Agent harness repair status (DW Phase 2) — real-time via codegen:output WebSocket.
12127
12466
  $: if ($agentHarnessOutput?.timestamp) {
12128
12467
  const raw = ($agentHarnessOutput.output || '').trim();
12129
12468
  const line = raw.replace(/^🔧\s*/, '').trim();
@@ -12139,15 +12478,27 @@ $: if ($agentHarnessOutput?.timestamp) {
12139
12478
  }
12140
12479
  }
12141
12480
 
12142
- // Worker preview ready via WebSocket event — reload canvas shape preview immediately
12481
+ // Worker preview ready via WebSocket event — reload canvas shape preview immediately.
12482
+ // The store keeps the last event forever, and this block writes state it also reads,
12483
+ // so without the event key it re-ran on every unrelated update: each re-run snapped the
12484
+ // version selector back to HEAD and cancelled an in-flight version preview.
12485
+ let lastWsWorkerPreviewEventKey: string | null = null;
12143
12486
  $: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previewUrl) {
12144
12487
  const wsReadyWidgetId = $dynamicWorkerPreviewStatus.widgetId;
12145
- shapePreviewSourceByWidgetId = {
12146
- ...shapePreviewSourceByWidgetId,
12147
- [wsReadyWidgetId]: 'preview',
12148
- };
12149
- syncWidgetShapeLivePreviews();
12150
- widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12488
+ const wsEventKey = `${wsReadyWidgetId}:${$dynamicWorkerPreviewStatus.previewUrl}:${$dynamicWorkerPreviewStatus.timestamp ?? ''}`;
12489
+ if (wsEventKey !== lastWsWorkerPreviewEventKey) {
12490
+ lastWsWorkerPreviewEventKey = wsEventKey;
12491
+ shapePreviewSourceByWidgetId = {
12492
+ ...shapePreviewSourceByWidgetId,
12493
+ [wsReadyWidgetId]: 'preview',
12494
+ };
12495
+ if (codegenCaptureBlocked()) {
12496
+ console.log('[CreatorApp] Defer canvas Worker preview reload until codegen finishes', wsReadyWidgetId.substring(0, 8));
12497
+ } else {
12498
+ syncWidgetShapeLivePreviews();
12499
+ widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12500
+ }
12501
+ }
12151
12502
  }
12152
12503
 
12153
12504
  // ===================== Publish History =====================
@@ -12823,6 +13174,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12823
13174
  * parent-page [FreezeReport] (long tasks attributed to the iframe).
12824
13175
  */
12825
13176
  const screenshotCaptureInFlight = new Set<string>();
13177
+ /** Bumped when a generation starts so in-flight html2canvas results are dropped. */
13178
+ let screenshotGenerationEpoch = 0;
12826
13179
 
12827
13180
  /**
12828
13181
  * Called when a Widget Image shape is clicked on the canvas.
@@ -14913,7 +15266,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14913
15266
  {@const panelInstanceKey = panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId}
14914
15267
  {@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
14915
15268
  {@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
14916
- {@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
15269
+ {@const ps = rev(panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
14917
15270
  <!--
14918
15271
  Same published-version resolution the WidgetDetails instance below gets.
14919
15272
  `ps.lastPublishedVersion` alone is only hydrated from the focused panel /
@@ -14924,9 +15277,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14924
15277
  {@const panelPublishedVersion = isDraftConvertId(panelWidgetId)
14925
15278
  ? null
14926
15279
  : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
14927
- {@const panelHasUnpublishedChanges = (panelDisplayRevision, widgets.length, hasUnpublishedChangesForPanel(panelWidgetId))}
14928
- {@const panelHasCompileErrorState = (panelDisplayRevision, panelHasCompileError(panelWidgetId))}
14929
- {@const panelHasInfraPreviewError = (panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
15280
+ {@const panelHasUnpublishedChanges = rev(panelDisplayRevision, hasUnpublishedChangesForPanel(panelWidgetId), widgets.length)}
15281
+ {@const panelHasCompileErrorState = rev(panelDisplayRevision, panelHasCompileError(panelWidgetId))}
15282
+ {@const panelHasInfraPreviewError = rev(panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
14930
15283
  <svelte:component this={WidgetDetails}
14931
15284
  bind:this={widgetDetailsRefs[panelInstanceKey]}
14932
15285
  userSession={$userSession}
@@ -15255,17 +15608,19 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15255
15608
  {@const defaultVersionSource = panelPublishedVersion !== null
15256
15609
  ? `v:${panelPublishedVersion}`
15257
15610
  : (newestDraft ? `v:${newestDraft.version}` : null)}
15258
- {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
15259
- {@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
15611
+ {@const restoredVersion = restoredViewingVersionByWidgetId[panelWidgetId]}
15612
+ {@const menuVersion = dropdownVersionByWidgetId[panelWidgetId]}
15613
+ {@const shapeSource = menuVersion || shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
15614
+ {@const viewingVersion = Number.isFinite(restoredVersion)
15615
+ ? restoredVersion
15616
+ : shapeSource && shapeSource.startsWith('v:')
15260
15617
  ? parseInt(shapeSource.slice(2), 10)
15261
15618
  : 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)
15619
+ ? (getNewestDraftVersionId(panelWidgetId) ?? panelPublishedVersion)
15267
15620
  : panelPublishedVersion}
15268
- {@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
15621
+ {@const viewingIsLive = !Number.isFinite(restoredVersion)
15622
+ && shapeSource !== 'preview'
15623
+ && Number(viewingVersion) === Number(panelPublishedVersion)}
15269
15624
  <!--
15270
15625
  A version is a Draft until it is published. The history is one
15271
15626
  sequence (`v1, v2, v3…`), so "draft" is read from the record's
@@ -15466,9 +15821,16 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15466
15821
  <Button
15467
15822
  onclick={() => {
15468
15823
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
15469
- lastGenerationFailed = false;
15470
- lastGenerationError = null;
15471
- generateCodeStatus = null;
15824
+ patchPanelSnapshot(panelWidgetId, {
15825
+ lastGenerationFailed: false,
15826
+ lastGenerationError: null,
15827
+ generateCodeStatus: 'Ready',
15828
+ });
15829
+ if (isPanelFocused || focusedPanelWidgetId === panelWidgetId) {
15830
+ lastGenerationFailed = false;
15831
+ lastGenerationError = null;
15832
+ generateCodeStatus = 'Ready';
15833
+ }
15472
15834
  }}
15473
15835
  variant="outline"
15474
15836
  size="sm"
@@ -15483,8 +15845,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15483
15845
  </svelte:fragment>
15484
15846
 
15485
15847
  <svelte:fragment slot="code-generation">
15848
+ {@const chatUserHeight = rev(panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
15486
15849
  <!-- svelte-ignore a11y_no_static_element_interactions -->
15487
- {@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
15488
15850
  <div
15489
15851
  class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
15490
15852
  style="padding: 0;"
@@ -15530,6 +15892,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15530
15892
  }}
15531
15893
  widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15532
15894
  versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15895
+ versionLabelByPrompt={versionPromptLabelsByWidgetId[panelWidgetId] || {}}
15896
+ viewingVersion={viewingVersionByWidgetId[panelWidgetId] ?? null}
15533
15897
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15534
15898
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
15535
15899
  onUploadChatFile={handleChatUploadFile}
@@ -15539,8 +15903,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15539
15903
  onLoadConversations={handleLoadConversations}
15540
15904
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
15541
15905
  onCreateConversation={handleCreateConversation}
15542
- onSaveMessage={(conversationId, content, messageType, attachments) =>
15543
- handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
15906
+ onSaveMessage={(conversationId, content, messageType, attachments, suggestions) =>
15907
+ handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId)
15544
15908
  }
15545
15909
  onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
15546
15910
  handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
@@ -15831,11 +16195,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15831
16195
 
15832
16196
  <!-- Embed Dimensions -->
15833
16197
  <div class="embed-dimensions-ct flex flex-col gap-2">
15834
- <label class="text-xs font-medium text-gray-600">Dimensions</label>
16198
+ <label class="text-xs font-medium text-gray-600" for="embed-width-input">Dimensions</label>
15835
16199
  <div class="embed-dimensions-row flex items-center gap-2">
15836
16200
  <div class="embed-dim-input flex items-center gap-1">
15837
16201
  <span class="text-[10px] text-gray-400 font-medium">W</span>
15838
16202
  <input
16203
+ id="embed-width-input"
16204
+ aria-label="Embed width"
15839
16205
  type="number"
15840
16206
  min="100"
15841
16207
  max="4000"
@@ -15854,6 +16220,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15854
16220
  <div class="embed-dim-input flex items-center gap-1">
15855
16221
  <span class="text-[10px] text-gray-400 font-medium">H</span>
15856
16222
  <input
16223
+ id="embed-height-input"
16224
+ aria-label="Embed height"
15857
16225
  type="number"
15858
16226
  min="50"
15859
16227
  max="4000"
@@ -15899,6 +16267,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15899
16267
  <!-- Embed Code Block -->
15900
16268
  <div class="embed-code-ct flex flex-col gap-2">
15901
16269
  <div class="embed-code-header flex items-center justify-between">
16270
+ <!-- svelte-ignore a11y_label_has_associated_control -->
15902
16271
  <label class="text-xs font-medium text-gray-600">Embed Code ({EMBED_FORMAT_LABELS[effectiveFormat]})</label>
15903
16272
  <!-- svelte-ignore a11y_consider_explicit_label -->
15904
16273
  <button
@@ -16020,8 +16389,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16020
16389
  <p class="text-sm text-gray-600 mb-4">
16021
16390
  Permanently delete <strong>{adminDeleteWidget?.name || adminDeleteWidget?.id?.slice(0, 8)}</strong>? This action cannot be undone.
16022
16391
  </p>
16023
- <label class="block text-sm font-medium text-gray-700 mb-1">Admin API Key</label>
16392
+ <label class="block text-sm font-medium text-gray-700 mb-1" for="admin-delete-api-key">Admin API Key</label>
16024
16393
  <input
16394
+ id="admin-delete-api-key"
16025
16395
  type="password"
16026
16396
  class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-red-500 focus:border-red-500 outline-none"
16027
16397
  placeholder="Enter admin key..."
@@ -16280,11 +16650,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16280
16650
  :global([data-dialog-overlay]) {
16281
16651
  z-index: 9998 !important;
16282
16652
  }
16283
- .generation-models-header select:focus,
16284
- .generation-models-header select:focus-visible {
16285
- outline: none;
16286
- box-shadow: none;
16287
- }
16288
16653
  .wc-version-history-toggle:focus,
16289
16654
  .wc-version-history-toggle:focus-visible,
16290
16655
  .wc-live-history-backdrop:focus,