@widgetic/creator 0.3.67 → 0.3.69

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;
@@ -535,6 +535,7 @@
535
535
  }
536
536
  installLongTaskFreezeReporter();
537
537
  installFreezeWatchdog();
538
+ console.info('[FreezeWatchdog] Freeze reports log on this page. In DevTools, pick the top frame — not a widget iframe.');
538
539
  });
539
540
 
540
541
  /**
@@ -871,6 +872,21 @@
871
872
  function isAnyWidgetGenerating(): boolean {
872
873
  return isGeneratingCode || Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id]);
873
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
+ }
874
890
  const widgetAbortControllers = new Map<string, AbortController>();
875
891
  let currentGenerationOperationId: string | null = null;
876
892
  let generateCodeStatus: string | null = null;
@@ -905,7 +921,7 @@
905
921
  // Chat component reference and context (focused panel)
906
922
  let chatMethods: {
907
923
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
908
- addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: string[]) => void;
924
+ addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: ClarificationSuggestion[]) => void;
909
925
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
910
926
  getConversationId: () => string | null;
911
927
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -1041,7 +1057,7 @@
1041
1057
  fileSize?: number;
1042
1058
  }>,
1043
1059
  sourceWidgetId?: string,
1044
- sendOptions?: { skipIntentGate?: boolean },
1060
+ sendOptions?: { skipIntentGate?: boolean; chipIntent?: ClarificationSuggestion['intent'] },
1045
1061
  ) {
1046
1062
  const widgetId = sourceWidgetId ?? selectedWidgetId;
1047
1063
  if (!widgetId) return;
@@ -1165,6 +1181,7 @@
1165
1181
  promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
1166
1182
  persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
1167
1183
  skipIntentGate: sendOptions?.skipIntentGate === true,
1184
+ chipIntent: sendOptions?.chipIntent,
1168
1185
  });
1169
1186
  }
1170
1187
 
@@ -1626,6 +1643,7 @@
1626
1643
  buildUserPromptMessageContent(prompt),
1627
1644
  'user',
1628
1645
  persistAttachments.length > 0 ? persistAttachments : undefined,
1646
+ undefined,
1629
1647
  widgetId,
1630
1648
  );
1631
1649
  const savedAtt = saved?.attachments || saved?.data?.attachments || [];
@@ -1726,32 +1744,46 @@
1726
1744
  iframe: HTMLIFrameElement,
1727
1745
  timeoutMs = 6000,
1728
1746
  ): Promise<{ data: string | null; error: string | null }> {
1729
- if (isAnyWidgetGenerating()) {
1747
+ if (codegenCaptureBlocked()) {
1730
1748
  return Promise.resolve({ data: null, error: 'generation in progress' });
1731
1749
  }
1750
+ const captureEpoch = screenshotGenerationEpoch;
1732
1751
  return new Promise((resolve) => {
1733
1752
  if (!iframe.contentWindow) {
1734
1753
  resolve({ data: null, error: 'no-content-window' });
1735
1754
  return;
1736
1755
  }
1737
- const timeout = setTimeout(() => {
1756
+ const finish = (result: { data: string | null; error: string | null }) => {
1738
1757
  window.removeEventListener('message', handler);
1739
- 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' });
1740
1767
  }, timeoutMs);
1741
1768
 
1742
1769
  function handler(event: MessageEvent) {
1743
1770
  if (event.data?.type !== 'widgetic:screenshot') return;
1744
- // Ignore other widgets and synthetic replies with no source window.
1745
1771
  if (event.source !== iframe.contentWindow) return;
1746
- clearTimeout(timeout);
1747
- window.removeEventListener('message', handler);
1748
- resolve({
1772
+ finish({
1749
1773
  data: typeof event.data.data === 'string' ? event.data.data : null,
1750
1774
  error: typeof event.data.error === 'string' ? event.data.error : null,
1751
1775
  });
1752
1776
  }
1753
1777
  window.addEventListener('message', handler);
1754
- 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
+ }
1755
1787
  });
1756
1788
  }
1757
1789
 
@@ -1764,7 +1796,7 @@
1764
1796
  // startup on the main thread this page shares with it, and the capture that
1765
1797
  // follows is refused during a generation anyway (`requestIframeScreenshot`),
1766
1798
  // so the whole path is skipped rather than paying the load for nothing.
1767
- if (isAnyWidgetGenerating()) {
1799
+ if (codegenCaptureBlocked()) {
1768
1800
  console.log('[Chat→Screenshot] Skip hidden Worker capture — codegen in progress', widgetId?.substring(0, 8));
1769
1801
  return null;
1770
1802
  }
@@ -1816,14 +1848,14 @@
1816
1848
 
1817
1849
  /** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
1818
1850
  async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
1819
- if (isAnyWidgetGenerating()) return;
1851
+ if (codegenCaptureBlocked()) return;
1820
1852
  const capturedCanvasId = getEffectiveCanvasId();
1821
1853
  const capturedCanvas = canvasRef;
1822
1854
  const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
1823
1855
  // Frame/intent identity owns this capture, not focused-panel HEAD hydration
1824
1856
  // or another widget's global generation operation.
1825
1857
  const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
1826
- && !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !isAnyWidgetGenerating()
1858
+ && !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !codegenCaptureBlocked()
1827
1859
  && capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
1828
1860
  && capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
1829
1861
  && resolveLiveWidgetShapeIframe(widgetId) === iframe;
@@ -1854,7 +1886,7 @@
1854
1886
  }
1855
1887
  // Never run background / refresh screenshot captures while code generation is running —
1856
1888
  // html2canvas inside any same-origin iframe freezes the browser main thread.
1857
- if ((quiet || requiredIframe != null) && isAnyWidgetGenerating()) {
1889
+ if (codegenCaptureBlocked()) {
1858
1890
  console.log('[Chat→Screenshot] Skip capture — generation in progress');
1859
1891
  return null;
1860
1892
  }
@@ -1882,6 +1914,10 @@
1882
1914
 
1883
1915
  // 1) Always try the visible live iframe first (published HTML from Worker includes screenshot script)
1884
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
+ }
1885
1921
  console.log('[Chat→Screenshot] Live WidgetShape iframe', {
1886
1922
  targetId: targetId.substring(0, 8),
1887
1923
  isWorker: isWorkerPreviewUrl(liveSrc),
@@ -2050,7 +2086,14 @@
2050
2086
  messageContent: m.message_content || m.messageContent || '',
2051
2087
  messageType: m.message_type || m.messageType || 'user',
2052
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,
2053
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,
2054
2097
  userId: m.user_id || m.userId || '',
2055
2098
  createdAt: new Date(m.created_at || m.createdAt),
2056
2099
  updatedAt: new Date(m.updated_at || m.updatedAt || m.created_at || m.createdAt),
@@ -2223,6 +2266,7 @@
2223
2266
  file_size?: number | null;
2224
2267
  display_order?: number;
2225
2268
  }>,
2269
+ suggestions?: ClarificationSuggestion[],
2226
2270
  persistWidgetId?: string | null,
2227
2271
  ): Promise<any> {
2228
2272
  try {
@@ -2258,25 +2302,39 @@
2258
2302
  user_id: userId,
2259
2303
  is_commit: false,
2260
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 } : {}),
2261
2308
  };
2262
- try {
2309
+ const persistMessage = async (payload: typeof createPayload) => {
2263
2310
  const response = await messagesClient.createMessageRaw({
2264
2311
  conversationId,
2265
- createMessageRequest: createPayload,
2312
+ createMessageRequest: payload,
2266
2313
  });
2267
2314
  const json = await response.raw.json();
2268
- // console.log('[Chat→Backend] Saved message to conversation:', conversationId);
2269
2315
  const saved = json?.data || json;
2270
2316
  const savedAtt = saved?.attachments;
2271
2317
  return {
2272
2318
  ...saved,
2273
- attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : apiAttachments,
2319
+ attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : (payload.attachments || []),
2274
2320
  };
2321
+ };
2322
+ try {
2323
+ return await persistMessage(createPayload);
2275
2324
  } catch (withAttachmentsError) {
2276
- console.error(
2277
- '[Chat→Backend] Save with attachments failed — not falling back to text-only:',
2278
- withAttachmentsError,
2279
- );
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);
2280
2338
  throw withAttachmentsError;
2281
2339
  }
2282
2340
  } catch (error) {
@@ -2317,34 +2375,47 @@
2317
2375
  null;
2318
2376
  // console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
2319
2377
  if (gitRollback?.success) {
2320
- // Fast path: a recorded version at this commit has an immutable cached
2321
- // artifact — reuse it instead of the forced repo rebuild + retry ladder.
2322
- let previewFromVersionCache = false;
2323
- if (targetCommit) {
2324
- lastCommitId = targetCommit;
2325
- if (panelWidgetId) {
2326
- patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
2327
- patchCodegenUiState(panelWidgetId, {});
2328
- // Message-extracted SHAs may be short (7+ chars); compare by prefix.
2329
- const targetSha = String(targetCommit).toLowerCase();
2330
- const matchedVersion = (resolveWidgetShapeVersionOptions(panelWidgetId) ?? [])
2331
- .find((option) => {
2332
- const optionSha = String(option.commitSha ?? '').toLowerCase();
2333
- return optionSha.length >= 7 && targetSha.length >= 7
2334
- && (optionSha === targetSha || optionSha.startsWith(targetSha) || targetSha.startsWith(optionSha));
2335
- });
2336
- if (matchedVersion) {
2337
- shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${panelWidgetId}:v:${matchedVersion.id.slice(2)}`]: null };
2338
- shapePreviewSourceByWidgetId = { ...shapePreviewSourceByWidgetId, [panelWidgetId]: matchedVersion.id };
2339
- handleWidgetShapeVersionChange(panelWidgetId, matchedVersion.id);
2340
- 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}`);
2341
2405
  }
2342
2406
  }
2407
+ shapePreviewSourceByWidgetId = {
2408
+ ...shapePreviewSourceByWidgetId,
2409
+ [panelWidgetId]: 'preview',
2410
+ };
2411
+ getWidgetDetailsRef(panelWidgetId)?.clearCodegenPreviewFresh?.();
2412
+ syncWidgetShapeLivePreviews();
2413
+ panelDisplayRevision += 1;
2343
2414
  }
2344
2415
  showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
2345
2416
  description: undoMode
2346
- ? `Code reverted to commit ${formatCommitShaShort(targetCommit)}. Refreshing preview…`
2347
- : `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…`,
2348
2419
  duration: 6000,
2349
2420
  });
2350
2421
  liveDesignValues = {};
@@ -2353,18 +2424,12 @@
2353
2424
  compositionDesignSchema = null;
2354
2425
  compositionContentSchema = null;
2355
2426
  pendingCreatePreviewDefaultsWidgetId = null;
2356
- _isRestoringCheckpoint = !previewFromVersionCache;
2357
- if (previewFromVersionCache) {
2358
- // Immutable artifact already serves the preview; no rebuild ladder.
2359
- syncWidgetShapeLivePreviews();
2360
- setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
2361
- return { ...data, previewFromVersionCache };
2362
- }
2427
+ _isRestoringCheckpoint = true;
2363
2428
  const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
2364
2429
  const scheduleBuildRetry = async () => {
2365
- const delays = [2500, 5000, 8000];
2430
+ const delays = [400, 2500, 5000, 8000];
2366
2431
  for (let i = 0; i < delays.length; i++) {
2367
- await new Promise((r) => setTimeout(r, delays[i]));
2432
+ if (i > 0) await new Promise((r) => setTimeout(r, delays[i]));
2368
2433
  if (i === 0) suppressNextBuildFromRepoChatMessage = true;
2369
2434
  try {
2370
2435
  const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
@@ -2378,7 +2443,9 @@
2378
2443
  [panelWidgetId]: 'preview',
2379
2444
  };
2380
2445
  syncWidgetShapeLivePreviews();
2381
- setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
2446
+ widgetShapeLivePreviewsRef?.requestReload?.(panelWidgetId);
2447
+ panelDisplayRevision += 1;
2448
+ scheduleIdleThumbnailPersist(panelWidgetId);
2382
2449
  }
2383
2450
  return;
2384
2451
  }
@@ -3938,9 +4005,7 @@
3938
4005
  };
3939
4006
  await compositionsClient.updateComposition({
3940
4007
  id: selectedComposition.id,
3941
- // The gateway accepts embedSettings (compositionSchemas update schema);
3942
- // the generated UpdateComposition model predates that field.
3943
- updateComposition: { embedSettings } as unknown as UpdateComposition
4008
+ updateComposition: { embedSettings }
3944
4009
  });
3945
4010
  selectedComposition = { ...selectedComposition, embedSettings };
3946
4011
  embedDimensionsChanged = false;
@@ -4079,6 +4144,11 @@
4079
4144
 
4080
4145
  // debugMode — current ON/OFF state. User can still toggle when the UI is available.
4081
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
+ }
4082
4152
  // console.log('[WidgetCreator] Debug from host/env', {
4083
4153
  // hostProvidedDebug,
4084
4154
  // initialDebugMode,
@@ -4263,6 +4333,11 @@
4263
4333
  function toggleDebugMode() {
4264
4334
  if (!debugModeAvailable) return;
4265
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
+ }
4266
4341
  // When turning OFF: switches to default model (from API config), mock console
4267
4342
  // When turning ON: enables model selector, real-time console
4268
4343
  const effectiveModel = debugMode ? userSelectedModelId : defaultModelId;
@@ -4461,9 +4536,8 @@
4461
4536
  const anyW = widget as { thumbnailUrl?: string | null; thumbnail_url?: string | null } | null;
4462
4537
  const hasThumb = !!(anyW?.thumbnailUrl || anyW?.thumbnail_url);
4463
4538
  if (!hasThumb) {
4464
- setTimeout(() => {
4465
- void handleRefreshScreenshot(widgetId, { quiet: true });
4466
- }, 1200);
4539
+ // A capture 1s after open runs html2canvas on the shared main thread.
4540
+ scheduleIdleThumbnailPersist(widgetId);
4467
4541
  }
4468
4542
  return true;
4469
4543
  }
@@ -5598,6 +5672,52 @@
5598
5672
  }
5599
5673
 
5600
5674
  // ─── Create new canvas ───────────────────────────────────────────────────────
5675
+ /** New-widget opens on a local canvas. The DB row is created on the first real edit. */
5676
+ let canvasRowDeferred = false;
5677
+ let canvasRowCreatePromise: Promise<string | null> | null = null;
5678
+
5679
+ async function ensureCanvasRowForFirstEdit(): Promise<string | null> {
5680
+ const existingId = typeof canvasId === 'string' ? canvasId.trim() : '';
5681
+ if (existingId) return existingId;
5682
+ if (canvasRowCreatePromise) return canvasRowCreatePromise;
5683
+ canvasRowCreatePromise = (async () => {
5684
+ if (!canvasesClient || !$userSession.user) {
5685
+ console.warn('[CreatorApp] First edit not persisted — canvas client is not ready');
5686
+ return null;
5687
+ }
5688
+ try {
5689
+ const usedNames = new Set(userCanvases.map((c) => (c.name || '').trim()).filter(Boolean));
5690
+ let canvasIndex = userCanvases.length + 1;
5691
+ let newName = `Canvas ${canvasIndex}`;
5692
+ while (usedNames.has(newName)) {
5693
+ canvasIndex += 1;
5694
+ newName = `Canvas ${canvasIndex}`;
5695
+ }
5696
+ const createdRaw = await canvasesClient.createCanvas({
5697
+ createCanvasRequest: { persistenceKey: newName }
5698
+ });
5699
+ const createdId = unwrapCreatedCanvasId(createdRaw);
5700
+ if (!createdId) {
5701
+ console.warn('[CreatorApp] First-edit canvas create response missing id');
5702
+ return null;
5703
+ }
5704
+ userCanvases = [{ id: createdId, name: newName, hasContent: true }, ...userCanvases];
5705
+ rememberCanvasId(createdId);
5706
+ activeCanvasName = newName;
5707
+ canvasRowDeferred = false;
5708
+ syncEmbeddedCanvasUrl(createdId);
5709
+ console.log('[CreatorApp] Canvas row created on first edit', createdId);
5710
+ return createdId;
5711
+ } catch (err) {
5712
+ console.error('[CreatorApp] Failed to create canvas on first edit:', err);
5713
+ return null;
5714
+ } finally {
5715
+ canvasRowCreatePromise = null;
5716
+ }
5717
+ })();
5718
+ return canvasRowCreatePromise;
5719
+ }
5720
+
5601
5721
  async function createNewCanvas(options: { skipSaveCurrent?: boolean } = {}): Promise<string | null> {
5602
5722
  if (!canvasesClient || !$userSession.user) {
5603
5723
  console.warn('[WidgetCreator] createNewCanvas skipped — missing client or user');
@@ -5702,24 +5822,10 @@
5702
5822
  selectedWidgetId = null;
5703
5823
  selectedWidget = null;
5704
5824
  beginBlankCanvasSession('site New widget');
5705
- // console.log('[CreatorApp] New widget: blank canvas only — widget record is created on Convert (F3)');
5706
- const clientsReady = await waitForAuthenticatedCanvasClient();
5707
- if (!clientsReady) {
5708
- console.error('[CreatorApp] Auth/clients not ready — blank canvas without new canvas row');
5709
- } else {
5710
- const newCanvasId = await createNewCanvas({ skipSaveCurrent: true });
5711
- if (!newCanvasId) {
5712
- showToast('error', 'Could not create a blank canvas', {
5713
- description: 'Try reloading the page to get a fresh canvas.',
5714
- duration: 8000
5715
- });
5716
- } else {
5717
- // Replace `new_widget=1` with the concrete canvas_id so a page refresh
5718
- // reloads THIS canvas (and its sketch) instead of re-running the
5719
- // new-widget bootstrap and creating yet another blank canvas row.
5720
- syncEmbeddedCanvasUrl(newCanvasId);
5721
- }
5722
- }
5825
+ rememberCanvasId(null);
5826
+ canvasRowDeferred = true;
5827
+ // No DB row yet. Autosave creates one on the first edit that has shapes.
5828
+ // An untouched new-widget refresh stays on new_widget=1 and writes nothing.
5723
5829
  // F3: New-widget flow starts with an EMPTY canvas — no widget record, no
5724
5830
  // WidgetShape, no auto-opened Widget Details. The widget record is created
5725
5831
  // only when the user sketches something and clicks "Convert to Widget".
@@ -7155,6 +7261,7 @@
7155
7261
  typeof crypto !== 'undefined' && crypto.randomUUID
7156
7262
  ? crypto.randomUUID()
7157
7263
  : `${Date.now()}-new-widget`;
7264
+ if (!canvasId) await ensureCanvasRowForFirstEdit();
7158
7265
  const response = await widgetsClient.createWidget(
7159
7266
  {
7160
7267
  newWidget: {
@@ -7740,11 +7847,18 @@
7740
7847
  const ctx = canvas.getContext('2d');
7741
7848
  if (!ctx) return dataUrl;
7742
7849
  ctx.drawImage(img, 0, 0, width, height);
7850
+ await new Promise((resolve) => setTimeout(resolve, 0));
7851
+ const encode = (quality: number) =>
7852
+ new Promise<string>((resolve) => {
7853
+ setTimeout(() => {
7854
+ resolve(canvas.toDataURL('image/jpeg', quality));
7855
+ }, 0);
7856
+ });
7743
7857
  let quality = 0.82;
7744
- let out = canvas.toDataURL('image/jpeg', quality);
7858
+ let out = await encode(quality);
7745
7859
  while (out.length > maxPayloadChars && quality > 0.45) {
7746
7860
  quality -= 0.08;
7747
- out = canvas.toDataURL('image/jpeg', quality);
7861
+ out = await encode(quality);
7748
7862
  }
7749
7863
  // console.log('[WidgetCreator] Compressed reference image for codegen', {
7750
7864
  // fromChars: dataUrl.length,
@@ -7823,6 +7937,7 @@
7823
7937
  */
7824
7938
  /** Debounced quiet sync: live Worker preview → canvas WidgetShape thumbnail. */
7825
7939
  const canvasShapePreviewSyncTimers = new Map<string, ReturnType<typeof setTimeout>>();
7940
+ const quietScreenshotTimers = new Map<string, ReturnType<typeof setTimeout>>();
7826
7941
  function cancelCanvasShapePreviewSync(widgetId?: string | null): void {
7827
7942
  if (widgetId) {
7828
7943
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
@@ -7833,13 +7948,86 @@
7833
7948
  for (const timer of canvasShapePreviewSyncTimers.values()) clearTimeout(timer);
7834
7949
  canvasShapePreviewSyncTimers.clear();
7835
7950
  }
7951
+ function cancelQuietScreenshots(widgetId?: string | null): void {
7952
+ if (widgetId) {
7953
+ const existing = quietScreenshotTimers.get(widgetId);
7954
+ if (existing) clearTimeout(existing);
7955
+ quietScreenshotTimers.delete(widgetId);
7956
+ cancelCanvasShapePreviewSync(widgetId);
7957
+ return;
7958
+ }
7959
+ for (const timer of quietScreenshotTimers.values()) clearTimeout(timer);
7960
+ quietScreenshotTimers.clear();
7961
+ cancelCanvasShapePreviewSync();
7962
+ }
7963
+ function scheduleQuietScreenshot(widgetId: string, delayMs = 2000): void {
7964
+ if (!widgetId || codegenCaptureBlocked()) return;
7965
+ const existing = quietScreenshotTimers.get(widgetId);
7966
+ if (existing) clearTimeout(existing);
7967
+ const timer = setTimeout(() => {
7968
+ quietScreenshotTimers.delete(widgetId);
7969
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) return;
7970
+ void handleRefreshScreenshot(widgetId, { quiet: true });
7971
+ }, delayMs);
7972
+ quietScreenshotTimers.set(widgetId, timer);
7973
+ }
7974
+ /**
7975
+ * Latest widget capture for the next LLM turn, without freezing a generation.
7976
+ * html2canvas shares the page main thread, so this waits until codegen has
7977
+ * been idle, then persists thumbnail_url only (no canvas JSON serialize).
7978
+ */
7979
+ const idleThumbnailTimers = new Map<string, ReturnType<typeof setTimeout>>();
7980
+ const IDLE_THUMBNAIL_DELAY_MS = 12000;
7981
+ function cancelIdleThumbnailPersist(widgetId?: string | null): void {
7982
+ if (widgetId) {
7983
+ const existing = idleThumbnailTimers.get(widgetId);
7984
+ if (existing) clearTimeout(existing);
7985
+ idleThumbnailTimers.delete(widgetId);
7986
+ return;
7987
+ }
7988
+ for (const timer of idleThumbnailTimers.values()) clearTimeout(timer);
7989
+ idleThumbnailTimers.clear();
7990
+ }
7991
+ function scheduleIdleThumbnailPersist(widgetId: string): void {
7992
+ if (!widgetId) return;
7993
+ cancelIdleThumbnailPersist(widgetId);
7994
+ const timer = setTimeout(() => {
7995
+ idleThumbnailTimers.delete(widgetId);
7996
+ const run = () => {
7997
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) {
7998
+ scheduleIdleThumbnailPersist(widgetId);
7999
+ return;
8000
+ }
8001
+ void persistIdleWidgetThumbnail(widgetId);
8002
+ };
8003
+ if (typeof requestIdleCallback === 'function') {
8004
+ requestIdleCallback(() => run(), { timeout: 8000 });
8005
+ } else {
8006
+ run();
8007
+ }
8008
+ }, IDLE_THUMBNAIL_DELAY_MS);
8009
+ idleThumbnailTimers.set(widgetId, timer);
8010
+ }
8011
+ async function persistIdleWidgetThumbnail(widgetId: string): Promise<void> {
8012
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) return;
8013
+ const iframe = resolveLiveWidgetShapeIframe(widgetId);
8014
+ if (!iframe?.contentWindow) return;
8015
+ try {
8016
+ const screenshot = await handleChatScreenshot(widgetId, iframe, true);
8017
+ if (!screenshot || codegenCaptureBlocked() || generatingWidgetIds[widgetId]) return;
8018
+ await persistWidgetListThumbnail(widgetId, screenshot);
8019
+ console.log('[Chat→Screenshot] Idle thumbnail persisted', widgetId.substring(0, 8));
8020
+ } catch (error) {
8021
+ console.warn('[Chat→Screenshot] Idle thumbnail persist failed', widgetId, error);
8022
+ }
8023
+ }
7836
8024
  function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
7837
- if (!widgetId || isAnyWidgetGenerating()) return;
8025
+ if (!widgetId || codegenCaptureBlocked()) return;
7838
8026
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
7839
8027
  if (existing) clearTimeout(existing);
7840
8028
  const timer = setTimeout(() => {
7841
8029
  canvasShapePreviewSyncTimers.delete(widgetId);
7842
- if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
8030
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) {
7843
8031
  // console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
7844
8032
  return;
7845
8033
  }
@@ -7870,8 +8058,8 @@
7870
8058
 
7871
8059
  // Never run quiet background screenshot captures while ANY code generation is running —
7872
8060
  // html2canvas execution inside any preview iframe blocks the single shared main thread.
7873
- if (quiet && isAnyWidgetGenerating()) {
7874
- console.log('[handleRefreshScreenshot] Skip quiet capture — codegen in progress', widgetId?.substring(0, 8));
8061
+ if (codegenCaptureBlocked()) {
8062
+ console.log('[handleRefreshScreenshot] Skip capture — codegen in progress', widgetId?.substring(0, 8));
7875
8063
  return;
7876
8064
  }
7877
8065
 
@@ -7972,6 +8160,12 @@
7972
8160
  });
7973
8161
  }
7974
8162
  window.addEventListener('message', handler);
8163
+ if (codegenCaptureBlocked()) {
8164
+ clearTimeout(timeout);
8165
+ window.removeEventListener('message', handler);
8166
+ resolve({ data: null, error: 'generation in progress' });
8167
+ return;
8168
+ }
7975
8169
  iframe.contentWindow!.postMessage({ type: 'widgetic:request-screenshot' }, '*');
7976
8170
  });
7977
8171
 
@@ -8092,6 +8286,8 @@ Requirements:
8092
8286
  promptImagesOverride?: string[];
8093
8287
  /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8094
8288
  skipIntentGate?: boolean;
8289
+ /** The tapped chip's structured intent, declared so the server measures the chip conversion without matching copy. */
8290
+ chipIntent?: ClarificationSuggestion['intent'];
8095
8291
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
8096
8292
  persistFileAttachments?: Array<{
8097
8293
  url?: string;
@@ -8182,6 +8378,7 @@ Requirements:
8182
8378
  promptImagesOverride: options.promptImagesOverride,
8183
8379
  persistFileAttachments: options.persistFileAttachments,
8184
8380
  skipIntentGate: options.skipIntentGate,
8381
+ chipIntent: options.chipIntent,
8185
8382
  });
8186
8383
  }
8187
8384
 
@@ -8205,6 +8402,8 @@ Requirements:
8205
8402
  promptImagesOverride?: string[];
8206
8403
  /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8207
8404
  skipIntentGate?: boolean;
8405
+ /** The tapped chip's structured intent, declared so the server measures the chip conversion without matching copy. */
8406
+ chipIntent?: ClarificationSuggestion['intent'];
8208
8407
  persistFileAttachments?: Array<{
8209
8408
  url?: string;
8210
8409
  file?: File;
@@ -8220,7 +8419,6 @@ Requirements:
8220
8419
 
8221
8420
  const panelChat = getChatMethodsForWidget(generationWidgetId);
8222
8421
  const panelSnap = getPanelState(generationWidgetId);
8223
- const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
8224
8422
 
8225
8423
  // Per-widget abort: only abort if the SAME widget already has a running generation
8226
8424
  const existingController = widgetAbortControllers.get(generationWidgetId);
@@ -8233,6 +8431,22 @@ Requirements:
8233
8431
  const signal = abortController.signal;
8234
8432
 
8235
8433
  generatingWidgetIds = { ...generatingWidgetIds, [generationWidgetId]: true };
8434
+ noteCodegenActive(true);
8435
+ screenshotGenerationEpoch += 1;
8436
+ document.querySelectorAll('iframe').forEach((frame) => {
8437
+ try {
8438
+ frame.contentWindow?.postMessage({ type: 'widgetic:abort-screenshot' }, '*');
8439
+ } catch (err) {
8440
+ console.warn('[WidgetCreator] Could not abort in-flight screenshot', err);
8441
+ }
8442
+ });
8443
+ cancelQuietScreenshots();
8444
+ cancelIdleThumbnailPersist();
8445
+ widgetShapeLivePreviewsRef?.cancelReloads?.();
8446
+ if (restoredViewingVersionByWidgetId[generationWidgetId] != null) {
8447
+ const { [generationWidgetId]: _droppedRestore, ...restRestored } = restoredViewingVersionByWidgetId;
8448
+ restoredViewingVersionByWidgetId = restRestored;
8449
+ }
8236
8450
  lastHarnessChatLine = '';
8237
8451
 
8238
8452
  // Show user prompt in chat when triggered outside Chat (confirm dialog / Generate Now).
@@ -8446,6 +8660,7 @@ Requirements:
8446
8660
  runtimeLogs: runtimeLogsForRequest,
8447
8661
  compileFixOnly: options.compileFixOnly === true,
8448
8662
  skipIntentGate: options.skipIntentGate === true,
8663
+ chipIntent: options.chipIntent,
8449
8664
  } as GenerateCodeRequest
8450
8665
  });
8451
8666
  // 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);
@@ -8460,7 +8675,7 @@ Requirements:
8460
8675
  if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
8461
8676
  const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
8462
8677
  const gatedReply: string = gatedData.reply ?? '';
8463
- const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8678
+ const suggestions: ClarificationSuggestion[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8464
8679
 
8465
8680
  clearActiveOperationId();
8466
8681
  patchCodegenUiState(generationWidgetId, {
@@ -8672,8 +8887,10 @@ Requirements:
8672
8887
  shapeHistoryResolvedByWidgetId = restResolved;
8673
8888
  setTimeout(() => {
8674
8889
  void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
8675
- syncWidgetShapeLivePreviews();
8676
- if (isFocusedPanel) panelDisplayRevision += 1;
8890
+ const newestDraft = getNewestDraftVersionId(generationWidgetId);
8891
+ if (newestDraft != null) {
8892
+ selectVersionInDropdowns(generationWidgetId, `v:${newestDraft}`);
8893
+ }
8677
8894
  });
8678
8895
  }, 1200);
8679
8896
  }
@@ -8845,11 +9062,9 @@ Requirements:
8845
9062
  [generationWidgetId]: 'preview',
8846
9063
  };
8847
9064
  syncWidgetShapeLivePreviews();
8848
- // The Worker serves the rebuilt code at the SAME /preview URL — sync()
8849
- // only reloads when src changes, so force a cache-busted iframe reload
8850
- // on the canvas shape overlay or it keeps showing the previous version.
9065
+ // WS preview-ready was deferred during codegen so the Worker
9066
+ // iframe does not boot on the shared main thread at 80%.
8851
9067
  widgetShapeLivePreviewsRef?.requestReload?.(generationWidgetId);
8852
- panelDisplayRevision += 1;
8853
9068
  if (!completedPreviewBuildErr) {
8854
9069
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
8855
9070
  pendingCreatePreviewDefaultsWidgetId = null;
@@ -8863,15 +9078,10 @@ Requirements:
8863
9078
  await panelChat?.reloadConversationMessages?.();
8864
9079
  panelChat?.markWidgetHeadAtLatestCommit?.();
8865
9080
  }, 2500);
8866
- setTimeout(() => {
8867
- // Only capture screenshot after the very first generation or at publish
8868
- // Subsequent generations skip this heavy operation to keep the main thread smooth.
8869
- if (!hadCodeBeforeGeneration) {
8870
- void (async () => {
8871
- await handleRefreshScreenshot(generationWidgetId, { quiet: true });
8872
- })();
8873
- }
8874
- }, 3000);
9081
+ scheduleIdleThumbnailPersist(generationWidgetId);
9082
+ // Do not html2canvas here. Capture during/after generation freezes
9083
+ // the tab (iframe shares the main thread). Thumb for the next prompt
9084
+ // is persisted later, only while the tab is idle.
8875
9085
  // Do NOT auto-publish — user previews first, then clicks Publish / Set live.
8876
9086
  } else {
8877
9087
  panelChat?.updateLatestCodegenStatusMessage?.(
@@ -8949,6 +9159,7 @@ Requirements:
8949
9159
  showToast('error', 'Code generation error', { description: message, duration: 6000 });
8950
9160
  pushCodegenFailureToChat(generationWidgetId, message);
8951
9161
  } finally {
9162
+ noteCodegenActive(false);
8952
9163
  const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
8953
9164
  generatingWidgetIds = restFinally;
8954
9165
  widgetAbortControllers.delete(generationWidgetId);
@@ -9107,7 +9318,7 @@ Requirements:
9107
9318
  // rather than letting them silently become version 0.
9108
9319
  const rawTargetVersion = publishOpts.targetVersion;
9109
9320
  const targetVersion =
9110
- rawTargetVersion == null || rawTargetVersion === ''
9321
+ rawTargetVersion == null || (typeof rawTargetVersion === 'string' && rawTargetVersion === '')
9111
9322
  ? null
9112
9323
  : Number.isFinite(Number(rawTargetVersion))
9113
9324
  ? Number(rawTargetVersion)
@@ -10296,8 +10507,8 @@ Requirements:
10296
10507
  record?.artifact_url ||
10297
10508
  (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10298
10509
  if (isPublished && artifact) {
10299
- const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10300
- if (url) return url;
10510
+ const isolated = resolveWidgetShapeVersionPreviewState(widgetId);
10511
+ if (isolated?.url) return isolated.url;
10301
10512
  }
10302
10513
  const previewState = resolveWidgetShapeVersionPreviewState(widgetId);
10303
10514
  if (previewState?.url) return previewState.url;
@@ -10327,6 +10538,10 @@ Requirements:
10327
10538
  * Publish history is loaded without requiring chat to be open.
10328
10539
  */
10329
10540
  let shapePreviewSourceByWidgetId: Record<string, string> = {};
10541
+ /** Dropdown label only. Changing it must not reload the shape iframe. */
10542
+ let dropdownVersionByWidgetId: Record<string, string> = {};
10543
+ /** After Restore Checkpoint, dropdowns stay on the restored version number while preview is Worker HEAD. */
10544
+ let restoredViewingVersionByWidgetId: Record<string, number> = {};
10330
10545
  // Historical previews are never interchangeable with the shared HEAD preview.
10331
10546
  let shapeVersionPreviewByWidgetId: Record<string, {
10332
10547
  versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
@@ -10440,12 +10655,21 @@ Requirements:
10440
10655
  (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10441
10656
 
10442
10657
  if (isPublished && artifact) {
10443
- const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10444
- if (url) return url;
10658
+ const isolated = resolveWidgetShapeVersionPreviewState(widgetId);
10659
+ if (isolated?.url && isolated.versionId === `v:${version}`) return isolated.url;
10660
+ // Do not return Live CDN here — Switch Version must wait for the
10661
+ // commit-specific build, otherwise the iframe stays on the last publish.
10445
10662
  }
10446
- // The newest draft IS repo HEAD — Worker `/preview/{widgetId}` already serves this code!
10663
+ // The newest draft is repo HEAD only while HEAD still points at its commit.
10664
+ // A Restore Checkpoint writes a rollback commit on top, so the shared
10665
+ // `/preview/{widgetId}` artifact then serves the restored code under the
10666
+ // newest version's number — ask for a commit-specific build instead.
10447
10667
  const newestDraft = getNewestDraftVersionId(widgetId);
10448
- if (!isPublished && newestDraft != null && version === newestDraft && workerUrl) {
10668
+ const draftCommitSha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha ?? null);
10669
+ const headCommitSha = normalizeCommitSha(ps.lastCommitId ?? loadCommitIdForWidget(widgetId));
10670
+ const draftIsRepoHead =
10671
+ !draftCommitSha || !headCommitSha || commitsMatch(draftCommitSha, headCommitSha);
10672
+ if (!isPublished && newestDraft != null && version === newestDraft && draftIsRepoHead && workerUrl) {
10449
10673
  return workerUrl;
10450
10674
  }
10451
10675
  return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
@@ -10464,6 +10688,8 @@ Requirements:
10464
10688
  }
10465
10689
 
10466
10690
  function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
10691
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10692
+ if (Number.isFinite(restored)) return `v${restored}`;
10467
10693
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
10468
10694
  const ps = getPanelDisplayState(widgetId);
10469
10695
  const liveVersion =
@@ -10526,6 +10752,7 @@ Requirements:
10526
10752
  dateLabel?: string;
10527
10753
  isLive?: boolean;
10528
10754
  isDraft?: boolean;
10755
+ commitSha?: string;
10529
10756
  }> = [];
10530
10757
  for (const record of history) {
10531
10758
  const version = Number(record.version);
@@ -10564,6 +10791,10 @@ Requirements:
10564
10791
  }
10565
10792
 
10566
10793
  function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
10794
+ const menuVersion = dropdownVersionByWidgetId[widgetId];
10795
+ if (menuVersion) return menuVersion;
10796
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10797
+ if (Number.isFinite(restored)) return `v:${restored}`;
10567
10798
  const source = shapePreviewSourceByWidgetId[widgetId];
10568
10799
  if (source?.startsWith('v:')) return source;
10569
10800
  if (source === 'preview') {
@@ -10579,8 +10810,21 @@ Requirements:
10579
10810
  return liveVersion != null ? `v:${liveVersion}` : null;
10580
10811
  }
10581
10812
 
10813
+ function selectVersionInDropdowns(widgetId: string, versionId: string): void {
10814
+ dropdownVersionByWidgetId = { ...dropdownVersionByWidgetId, [widgetId]: versionId };
10815
+ widgetShapeLivePreviewsRef?.setSelectedVersion?.(widgetId, versionId);
10816
+ panelDisplayRevision += 1;
10817
+ }
10818
+
10582
10819
  function handleWidgetShapeVersionChange(widgetId: string, versionId: string, buildPreview = true): void {
10583
10820
  if (!versionId) return;
10821
+ // Both dropdowns show the pick. Only the dropdown the user clicked reloads the preview.
10822
+ selectVersionInDropdowns(widgetId, versionId);
10823
+ if (!buildPreview) return;
10824
+ if (restoredViewingVersionByWidgetId[widgetId] != null) {
10825
+ const { [widgetId]: _dropped, ...restRestored } = restoredViewingVersionByWidgetId;
10826
+ restoredViewingVersionByWidgetId = restRestored;
10827
+ }
10584
10828
  // Re-selecting an already successful/in-flight preview must not reset its state.
10585
10829
  const currentPreview = resolveWidgetShapeVersionPreviewState(widgetId);
10586
10830
  if (shapePreviewSourceByWidgetId[widgetId] === versionId
@@ -10599,17 +10843,19 @@ Requirements:
10599
10843
  };
10600
10844
  syncWidgetShapeLivePreviews();
10601
10845
  panelDisplayRevision += 1;
10602
- // Explicit Publish builds its own target; do not race it with a preview build.
10603
- if (!buildPreview) return;
10604
- // Published artifacts need no build. Drafts must never substitute HEAD.
10846
+ // Always compile the selected commit into an isolated Worker preview.
10847
+ // Published CDN artifacts are Live only — using them on Switch made
10848
+ // draft/restore look like the last publish even when git had moved.
10605
10849
  if (versionId.startsWith('v:')) {
10606
10850
  const version = Number(versionId.slice(2));
10607
- const directUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10608
- if (directUrl) {
10609
- const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10610
- console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
10851
+ const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10852
+ const isPublished = !!(record?.publishedAt ?? record?.published_at);
10853
+ const newestDraft = getNewestDraftVersionId(widgetId);
10854
+ const workerUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10855
+ const viewingHeadDraft = !isPublished && newestDraft != null && version === newestDraft && workerUrl && !restoredViewingVersionByWidgetId[widgetId];
10856
+ if (viewingHeadDraft) {
10611
10857
  const detailsRef = getWidgetDetailsRef(widgetId);
10612
- detailsRef?.showPublishedArtifact?.(directUrl);
10858
+ detailsRef?.showPublishedArtifact?.(workerUrl);
10613
10859
  } else {
10614
10860
  void ensureWidgetShapeVersionPreview(widgetId, version);
10615
10861
  }
@@ -10688,6 +10934,13 @@ Requirements:
10688
10934
  /** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
10689
10935
  function resolveViewingCommitShaForPanel(widgetId: string): string | null {
10690
10936
  const ps = getPanelDisplayState(widgetId);
10937
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10938
+ if (Number.isFinite(restored)) {
10939
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10940
+ const record = history.find((r) => Number(r.version) === Number(restored));
10941
+ const fromRestored = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
10942
+ if (fromRestored) return fromRestored;
10943
+ }
10691
10944
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
10692
10945
  if (source === 'preview') {
10693
10946
  return normalizeCommitSha(ps.lastCommitId);
@@ -10738,18 +10991,28 @@ Requirements:
10738
10991
  * debug mode and this label is what the turn shows.
10739
10992
  */
10740
10993
  let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10994
+ let versionPromptLabelsByWidgetId: Record<string, Record<string, string>> = {};
10995
+ let viewingVersionByWidgetId: Record<string, number | null> = {};
10741
10996
  $: {
10742
10997
  void shapePreviewSourceByWidgetId;
10743
10998
  void shapeVersionPreviewByWidgetId;
10999
+ void restoredViewingVersionByWidgetId;
11000
+ void dropdownVersionByWidgetId;
10744
11001
  void panelDisplayRevision;
10745
11002
  const nextHeads: Record<string, string | null> = {};
10746
11003
  const nextLabels: Record<string, Record<string, string>> = {};
11004
+ const nextPrompts: Record<string, Record<string, string>> = {};
11005
+ const nextViewing: Record<string, number | null> = {};
10747
11006
  for (const widgetId of openWidgetPanelIds) {
10748
11007
  nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
10749
11008
  nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
11009
+ nextPrompts[widgetId] = buildVersionPromptLabelsForPanel(widgetId);
11010
+ nextViewing[widgetId] = viewingVersionNumberForPanel(widgetId);
10750
11011
  }
10751
11012
  headCommitShaByWidgetId = nextHeads;
10752
11013
  versionLabelsByWidgetId = nextLabels;
11014
+ versionPromptLabelsByWidgetId = nextPrompts;
11015
+ viewingVersionByWidgetId = nextViewing;
10753
11016
  }
10754
11017
 
10755
11018
  /** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
@@ -10758,24 +11021,55 @@ Requirements:
10758
11021
  return normalized ? normalized.slice(0, 7) : null;
10759
11022
  }
10760
11023
 
11024
+ function assignVersionLabelKeys(labels: Record<string, string>, sha: string | null | undefined, label: string): void {
11025
+ const normalized = normalizeCommitSha(sha);
11026
+ if (!normalized) return;
11027
+ labels[normalized] = label;
11028
+ labels[normalized.slice(0, 7)] = label;
11029
+ labels[normalized.slice(0, 8)] = label;
11030
+ }
11031
+
10761
11032
  function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
10762
11033
  const labels: Record<string, string> = {};
10763
11034
  const ps = getPanelDisplayState(widgetId);
10764
11035
  const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10765
11036
  for (const record of history || []) {
10766
- const key = versionLabelKey(record?.commitSha ?? record?.commit_sha);
10767
- if (key && record?.version != null) labels[key] = `Version ${record.version}`;
11037
+ if (record?.version == null) continue;
11038
+ assignVersionLabelKeys(labels, record?.commitSha ?? record?.commit_sha, `Version ${record.version}`);
10768
11039
  }
10769
- // The version the user is previewing right now may have been fetched rather
10770
- // than read from history, and it is the one whose label matters most.
10771
11040
  const previewState = shapeVersionPreviewByWidgetId[widgetId];
10772
- const previewKey = versionLabelKey(previewState?.commitSha);
10773
- if (previewKey && previewState?.versionId?.startsWith('v:')) {
10774
- labels[previewKey] = `Version ${previewState.versionId.slice(2)}`;
11041
+ if (previewState?.versionId?.startsWith('v:')) {
11042
+ assignVersionLabelKeys(labels, previewState?.commitSha, `Version ${previewState.versionId.slice(2)}`);
10775
11043
  }
10776
11044
  return labels;
10777
11045
  }
10778
11046
 
11047
+ function versionPromptKey(prompt: string | null | undefined): string | null {
11048
+ const text = String(prompt || '').replace(/\s+/g, ' ').trim();
11049
+ return text || null;
11050
+ }
11051
+
11052
+ /** User prompt → "Version N", for turns whose commit line was never saved. */
11053
+ function buildVersionPromptLabelsForPanel(widgetId: string): Record<string, string> {
11054
+ const labels: Record<string, string> = {};
11055
+ const ps = getPanelDisplayState(widgetId);
11056
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
11057
+ for (const record of history || []) {
11058
+ if (record?.version == null) continue;
11059
+ const key = versionPromptKey(record?.prompt);
11060
+ if (!key) continue;
11061
+ labels[key] = `Version ${record.version}`;
11062
+ }
11063
+ return labels;
11064
+ }
11065
+
11066
+ function viewingVersionNumberForPanel(widgetId: string): number | null {
11067
+ const selected = resolveWidgetShapeVersionSelectedId(widgetId);
11068
+ if (!selected?.startsWith('v:')) return null;
11069
+ const version = parseInt(selected.slice(2), 10);
11070
+ return Number.isFinite(version) ? version : null;
11071
+ }
11072
+
10779
11073
  function syncWidgetShapeLivePreviews(): void {
10780
11074
  measureSync('sync-widget-shape-previews', () => {
10781
11075
  try {
@@ -11010,10 +11304,13 @@ Requirements:
11010
11304
  if (embedded && deepLinkId && widgetId === deepLinkId) {
11011
11305
  embeddedDeepLinkPanelDismissed = false;
11012
11306
  }
11013
- // One details panel at a time — close any other open widget first.
11014
- for (const openId of [...openWidgetPanelIds]) {
11015
- if (openId !== widgetId) {
11016
- closeWidgetDetailsPanel(openId, { fromShapeSwitch: true });
11307
+ // At most two chats. Opening a third closes the oldest one.
11308
+ const maxOpenWidgetChats = 2;
11309
+ if (!openWidgetPanelIds.includes(widgetId)) {
11310
+ while (openWidgetPanelIds.length >= maxOpenWidgetChats) {
11311
+ const oldestId = openWidgetPanelIds[0];
11312
+ if (!oldestId) break;
11313
+ closeWidgetDetailsPanel(oldestId, { fromShapeSwitch: true });
11017
11314
  }
11018
11315
  }
11019
11316
  const isNewPanel = !openWidgetPanelIds.includes(widgetId);
@@ -12021,7 +12318,7 @@ Requirements:
12021
12318
  * from whatever HEAD happens to contain.
12022
12319
  */
12023
12320
  function canPublishDraftVersionForPanel(widgetId: string, version: number | null | undefined): boolean {
12024
- if (version == null || version === '' || !Number.isFinite(Number(version))) return false;
12321
+ if (version == null || (typeof version === 'string' && version === '') || !Number.isFinite(Number(version))) return false;
12025
12322
  if (isPublishing && publishingWidgetId === widgetId) return false;
12026
12323
  if (isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId) return false;
12027
12324
  if (generatingWidgetIds[widgetId]) return false;
@@ -12150,7 +12447,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
12150
12447
  }
12151
12448
  if (publishWidgetId) {
12152
12449
  // console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
12153
- setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
12450
+ scheduleQuietScreenshot(publishWidgetId, 2000);
12154
12451
  }
12155
12452
  if (embedded && onPublishComplete && publishWidgetId) {
12156
12453
  onPublishComplete({
@@ -12198,7 +12495,7 @@ $: if ($widgetPublishStatus.widgetId === selectedWidgetId && $widgetPublishStatu
12198
12495
  // console.log('[CrossTab/WS] Publish building status from WebSocket — disabling Publish button');
12199
12496
  }
12200
12497
 
12201
- // Agent harness repair status (DW Phase 2) — real-time via vmConsole:aiderOutput WebSocket.
12498
+ // Agent harness repair status (DW Phase 2) — real-time via codegen:output WebSocket.
12202
12499
  $: if ($agentHarnessOutput?.timestamp) {
12203
12500
  const raw = ($agentHarnessOutput.output || '').trim();
12204
12501
  const line = raw.replace(/^🔧\s*/, '').trim();
@@ -12214,15 +12511,27 @@ $: if ($agentHarnessOutput?.timestamp) {
12214
12511
  }
12215
12512
  }
12216
12513
 
12217
- // Worker preview ready via WebSocket event — reload canvas shape preview immediately
12514
+ // Worker preview ready via WebSocket event — reload canvas shape preview immediately.
12515
+ // The store keeps the last event forever, and this block writes state it also reads,
12516
+ // so without the event key it re-ran on every unrelated update: each re-run snapped the
12517
+ // version selector back to HEAD and cancelled an in-flight version preview.
12518
+ let lastWsWorkerPreviewEventKey: string | null = null;
12218
12519
  $: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previewUrl) {
12219
12520
  const wsReadyWidgetId = $dynamicWorkerPreviewStatus.widgetId;
12220
- shapePreviewSourceByWidgetId = {
12221
- ...shapePreviewSourceByWidgetId,
12222
- [wsReadyWidgetId]: 'preview',
12223
- };
12224
- syncWidgetShapeLivePreviews();
12225
- widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12521
+ const wsEventKey = `${wsReadyWidgetId}:${$dynamicWorkerPreviewStatus.previewUrl}:${$dynamicWorkerPreviewStatus.timestamp ?? ''}`;
12522
+ if (wsEventKey !== lastWsWorkerPreviewEventKey) {
12523
+ lastWsWorkerPreviewEventKey = wsEventKey;
12524
+ shapePreviewSourceByWidgetId = {
12525
+ ...shapePreviewSourceByWidgetId,
12526
+ [wsReadyWidgetId]: 'preview',
12527
+ };
12528
+ if (codegenCaptureBlocked()) {
12529
+ console.log('[CreatorApp] Defer canvas Worker preview reload until codegen finishes', wsReadyWidgetId.substring(0, 8));
12530
+ } else {
12531
+ syncWidgetShapeLivePreviews();
12532
+ widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12533
+ }
12534
+ }
12226
12535
  }
12227
12536
 
12228
12537
  // ===================== Publish History =====================
@@ -12898,6 +13207,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12898
13207
  * parent-page [FreezeReport] (long tasks attributed to the iframe).
12899
13208
  */
12900
13209
  const screenshotCaptureInFlight = new Set<string>();
13210
+ /** Bumped when a generation starts so in-flight html2canvas results are dropped. */
13211
+ let screenshotGenerationEpoch = 0;
12901
13212
 
12902
13213
  /**
12903
13214
  * Called when a Widget Image shape is clicked on the canvas.
@@ -13645,6 +13956,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13645
13956
  });
13646
13957
  }
13647
13958
 
13959
+ // A new-widget session has no row until the user actually adds something.
13960
+ // Empty autosaves (open, refresh, unmount of a blank canvas) stay local.
13961
+ if (canvasRowDeferred && !canvasId && objectCount > 0) {
13962
+ await ensureCanvasRowForFirstEdit();
13963
+ if (canvasId) saveCanvasState(canvasId, json);
13964
+ }
13965
+
13648
13966
  // Save to API via SDK with sequence race protection
13649
13967
  if (canvasId && canvasesClient) {
13650
13968
  const currentSeq = ++autosaveSequence;
@@ -13845,6 +14163,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13845
14163
  ?? `ConvertedW-${Date.now()}`;
13846
14164
 
13847
14165
  authStatus = 'Creating widget from design...';
14166
+ if (!canvasId) await ensureCanvasRowForFirstEdit();
13848
14167
  const idempotencyKey = crypto?.randomUUID?.() ?? `${Date.now()}-convert`;
13849
14168
  const response = await widgetsClient.createWidget(
13850
14169
  {
@@ -14988,7 +15307,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14988
15307
  {@const panelInstanceKey = panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId}
14989
15308
  {@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
14990
15309
  {@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
14991
- {@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
15310
+ {@const ps = rev(panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
14992
15311
  <!--
14993
15312
  Same published-version resolution the WidgetDetails instance below gets.
14994
15313
  `ps.lastPublishedVersion` alone is only hydrated from the focused panel /
@@ -14999,9 +15318,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14999
15318
  {@const panelPublishedVersion = isDraftConvertId(panelWidgetId)
15000
15319
  ? null
15001
15320
  : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
15002
- {@const panelHasUnpublishedChanges = (panelDisplayRevision, widgets.length, hasUnpublishedChangesForPanel(panelWidgetId))}
15003
- {@const panelHasCompileErrorState = (panelDisplayRevision, panelHasCompileError(panelWidgetId))}
15004
- {@const panelHasInfraPreviewError = (panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
15321
+ {@const panelHasUnpublishedChanges = rev(panelDisplayRevision, hasUnpublishedChangesForPanel(panelWidgetId), widgets.length)}
15322
+ {@const panelHasCompileErrorState = rev(panelDisplayRevision, panelHasCompileError(panelWidgetId))}
15323
+ {@const panelHasInfraPreviewError = rev(panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
15005
15324
  <svelte:component this={WidgetDetails}
15006
15325
  bind:this={widgetDetailsRefs[panelInstanceKey]}
15007
15326
  userSession={$userSession}
@@ -15330,17 +15649,19 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15330
15649
  {@const defaultVersionSource = panelPublishedVersion !== null
15331
15650
  ? `v:${panelPublishedVersion}`
15332
15651
  : (newestDraft ? `v:${newestDraft.version}` : null)}
15333
- {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
15334
- {@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
15652
+ {@const restoredVersion = restoredViewingVersionByWidgetId[panelWidgetId]}
15653
+ {@const menuVersion = dropdownVersionByWidgetId[panelWidgetId]}
15654
+ {@const shapeSource = menuVersion || shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
15655
+ {@const viewingVersion = Number.isFinite(restoredVersion)
15656
+ ? restoredVersion
15657
+ : shapeSource && shapeSource.startsWith('v:')
15335
15658
  ? parseInt(shapeSource.slice(2), 10)
15336
15659
  : shapeSource === 'preview'
15337
- // A 'preview' source (Worker HEAD, unpublished) is not a published
15338
- // version — falling back to panelPublishedVersion produced `vnull`
15339
- // right after generation AND showed the stale Live version when a
15340
- // draft existed. The newest draft IS this HEAD's version.
15341
- ? (newestDraft ? newestDraft.version : panelPublishedVersion)
15660
+ ? (getNewestDraftVersionId(panelWidgetId) ?? panelPublishedVersion)
15342
15661
  : panelPublishedVersion}
15343
- {@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
15662
+ {@const viewingIsLive = !Number.isFinite(restoredVersion)
15663
+ && shapeSource !== 'preview'
15664
+ && Number(viewingVersion) === Number(panelPublishedVersion)}
15344
15665
  <!--
15345
15666
  A version is a Draft until it is published. The history is one
15346
15667
  sequence (`v1, v2, v3…`), so "draft" is read from the record's
@@ -15541,9 +15862,16 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15541
15862
  <Button
15542
15863
  onclick={() => {
15543
15864
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
15544
- lastGenerationFailed = false;
15545
- lastGenerationError = null;
15546
- generateCodeStatus = null;
15865
+ patchPanelSnapshot(panelWidgetId, {
15866
+ lastGenerationFailed: false,
15867
+ lastGenerationError: null,
15868
+ generateCodeStatus: 'Ready',
15869
+ });
15870
+ if (isPanelFocused || focusedPanelWidgetId === panelWidgetId) {
15871
+ lastGenerationFailed = false;
15872
+ lastGenerationError = null;
15873
+ generateCodeStatus = 'Ready';
15874
+ }
15547
15875
  }}
15548
15876
  variant="outline"
15549
15877
  size="sm"
@@ -15558,8 +15886,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15558
15886
  </svelte:fragment>
15559
15887
 
15560
15888
  <svelte:fragment slot="code-generation">
15889
+ {@const chatUserHeight = rev(panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
15561
15890
  <!-- svelte-ignore a11y_no_static_element_interactions -->
15562
- {@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
15563
15891
  <div
15564
15892
  class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
15565
15893
  style="padding: 0;"
@@ -15605,6 +15933,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15605
15933
  }}
15606
15934
  widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15607
15935
  versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15936
+ versionLabelByPrompt={versionPromptLabelsByWidgetId[panelWidgetId] || {}}
15937
+ viewingVersion={viewingVersionByWidgetId[panelWidgetId] ?? null}
15608
15938
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15609
15939
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
15610
15940
  onUploadChatFile={handleChatUploadFile}
@@ -15614,8 +15944,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15614
15944
  onLoadConversations={handleLoadConversations}
15615
15945
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
15616
15946
  onCreateConversation={handleCreateConversation}
15617
- onSaveMessage={(conversationId, content, messageType, attachments) =>
15618
- handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
15947
+ onSaveMessage={(conversationId, content, messageType, attachments, suggestions) =>
15948
+ handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId)
15619
15949
  }
15620
15950
  onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
15621
15951
  handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
@@ -15906,11 +16236,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15906
16236
 
15907
16237
  <!-- Embed Dimensions -->
15908
16238
  <div class="embed-dimensions-ct flex flex-col gap-2">
15909
- <label class="text-xs font-medium text-gray-600">Dimensions</label>
16239
+ <label class="text-xs font-medium text-gray-600" for="embed-width-input">Dimensions</label>
15910
16240
  <div class="embed-dimensions-row flex items-center gap-2">
15911
16241
  <div class="embed-dim-input flex items-center gap-1">
15912
16242
  <span class="text-[10px] text-gray-400 font-medium">W</span>
15913
16243
  <input
16244
+ id="embed-width-input"
16245
+ aria-label="Embed width"
15914
16246
  type="number"
15915
16247
  min="100"
15916
16248
  max="4000"
@@ -15929,6 +16261,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15929
16261
  <div class="embed-dim-input flex items-center gap-1">
15930
16262
  <span class="text-[10px] text-gray-400 font-medium">H</span>
15931
16263
  <input
16264
+ id="embed-height-input"
16265
+ aria-label="Embed height"
15932
16266
  type="number"
15933
16267
  min="50"
15934
16268
  max="4000"
@@ -15974,6 +16308,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15974
16308
  <!-- Embed Code Block -->
15975
16309
  <div class="embed-code-ct flex flex-col gap-2">
15976
16310
  <div class="embed-code-header flex items-center justify-between">
16311
+ <!-- svelte-ignore a11y_label_has_associated_control -->
15977
16312
  <label class="text-xs font-medium text-gray-600">Embed Code ({EMBED_FORMAT_LABELS[effectiveFormat]})</label>
15978
16313
  <!-- svelte-ignore a11y_consider_explicit_label -->
15979
16314
  <button
@@ -16095,8 +16430,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16095
16430
  <p class="text-sm text-gray-600 mb-4">
16096
16431
  Permanently delete <strong>{adminDeleteWidget?.name || adminDeleteWidget?.id?.slice(0, 8)}</strong>? This action cannot be undone.
16097
16432
  </p>
16098
- <label class="block text-sm font-medium text-gray-700 mb-1">Admin API Key</label>
16433
+ <label class="block text-sm font-medium text-gray-700 mb-1" for="admin-delete-api-key">Admin API Key</label>
16099
16434
  <input
16435
+ id="admin-delete-api-key"
16100
16436
  type="password"
16101
16437
  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"
16102
16438
  placeholder="Enter admin key..."
@@ -16355,11 +16691,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16355
16691
  :global([data-dialog-overlay]) {
16356
16692
  z-index: 9998 !important;
16357
16693
  }
16358
- .generation-models-header select:focus,
16359
- .generation-models-header select:focus-visible {
16360
- outline: none;
16361
- box-shadow: none;
16362
- }
16363
16694
  .wc-version-history-toggle:focus,
16364
16695
  .wc-version-history-toggle:focus-visible,
16365
16696
  .wc-live-history-backdrop:focus,