@widgetic/creator 0.3.67 → 0.3.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -7740,11 +7814,18 @@
7740
7814
  const ctx = canvas.getContext('2d');
7741
7815
  if (!ctx) return dataUrl;
7742
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
+ });
7743
7824
  let quality = 0.82;
7744
- let out = canvas.toDataURL('image/jpeg', quality);
7825
+ let out = await encode(quality);
7745
7826
  while (out.length > maxPayloadChars && quality > 0.45) {
7746
7827
  quality -= 0.08;
7747
- out = canvas.toDataURL('image/jpeg', quality);
7828
+ out = await encode(quality);
7748
7829
  }
7749
7830
  // console.log('[WidgetCreator] Compressed reference image for codegen', {
7750
7831
  // fromChars: dataUrl.length,
@@ -7823,6 +7904,7 @@
7823
7904
  */
7824
7905
  /** Debounced quiet sync: live Worker preview → canvas WidgetShape thumbnail. */
7825
7906
  const canvasShapePreviewSyncTimers = new Map<string, ReturnType<typeof setTimeout>>();
7907
+ const quietScreenshotTimers = new Map<string, ReturnType<typeof setTimeout>>();
7826
7908
  function cancelCanvasShapePreviewSync(widgetId?: string | null): void {
7827
7909
  if (widgetId) {
7828
7910
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
@@ -7833,13 +7915,86 @@
7833
7915
  for (const timer of canvasShapePreviewSyncTimers.values()) clearTimeout(timer);
7834
7916
  canvasShapePreviewSyncTimers.clear();
7835
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
+ }
7836
7991
  function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
7837
- if (!widgetId || isAnyWidgetGenerating()) return;
7992
+ if (!widgetId || codegenCaptureBlocked()) return;
7838
7993
  const existing = canvasShapePreviewSyncTimers.get(widgetId);
7839
7994
  if (existing) clearTimeout(existing);
7840
7995
  const timer = setTimeout(() => {
7841
7996
  canvasShapePreviewSyncTimers.delete(widgetId);
7842
- if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
7997
+ if (codegenCaptureBlocked() || generatingWidgetIds[widgetId]) {
7843
7998
  // console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
7844
7999
  return;
7845
8000
  }
@@ -7870,8 +8025,8 @@
7870
8025
 
7871
8026
  // Never run quiet background screenshot captures while ANY code generation is running —
7872
8027
  // 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));
8028
+ if (codegenCaptureBlocked()) {
8029
+ console.log('[handleRefreshScreenshot] Skip capture — codegen in progress', widgetId?.substring(0, 8));
7875
8030
  return;
7876
8031
  }
7877
8032
 
@@ -7972,6 +8127,12 @@
7972
8127
  });
7973
8128
  }
7974
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
+ }
7975
8136
  iframe.contentWindow!.postMessage({ type: 'widgetic:request-screenshot' }, '*');
7976
8137
  });
7977
8138
 
@@ -8092,6 +8253,8 @@ Requirements:
8092
8253
  promptImagesOverride?: string[];
8093
8254
  /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8094
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'];
8095
8258
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
8096
8259
  persistFileAttachments?: Array<{
8097
8260
  url?: string;
@@ -8182,6 +8345,7 @@ Requirements:
8182
8345
  promptImagesOverride: options.promptImagesOverride,
8183
8346
  persistFileAttachments: options.persistFileAttachments,
8184
8347
  skipIntentGate: options.skipIntentGate,
8348
+ chipIntent: options.chipIntent,
8185
8349
  });
8186
8350
  }
8187
8351
 
@@ -8205,6 +8369,8 @@ Requirements:
8205
8369
  promptImagesOverride?: string[];
8206
8370
  /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8207
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'];
8208
8374
  persistFileAttachments?: Array<{
8209
8375
  url?: string;
8210
8376
  file?: File;
@@ -8220,7 +8386,6 @@ Requirements:
8220
8386
 
8221
8387
  const panelChat = getChatMethodsForWidget(generationWidgetId);
8222
8388
  const panelSnap = getPanelState(generationWidgetId);
8223
- const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
8224
8389
 
8225
8390
  // Per-widget abort: only abort if the SAME widget already has a running generation
8226
8391
  const existingController = widgetAbortControllers.get(generationWidgetId);
@@ -8233,6 +8398,22 @@ Requirements:
8233
8398
  const signal = abortController.signal;
8234
8399
 
8235
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
+ }
8236
8417
  lastHarnessChatLine = '';
8237
8418
 
8238
8419
  // Show user prompt in chat when triggered outside Chat (confirm dialog / Generate Now).
@@ -8446,6 +8627,7 @@ Requirements:
8446
8627
  runtimeLogs: runtimeLogsForRequest,
8447
8628
  compileFixOnly: options.compileFixOnly === true,
8448
8629
  skipIntentGate: options.skipIntentGate === true,
8630
+ chipIntent: options.chipIntent,
8449
8631
  } as GenerateCodeRequest
8450
8632
  });
8451
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);
@@ -8460,7 +8642,7 @@ Requirements:
8460
8642
  if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
8461
8643
  const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
8462
8644
  const gatedReply: string = gatedData.reply ?? '';
8463
- const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8645
+ const suggestions: ClarificationSuggestion[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8464
8646
 
8465
8647
  clearActiveOperationId();
8466
8648
  patchCodegenUiState(generationWidgetId, {
@@ -8672,8 +8854,10 @@ Requirements:
8672
8854
  shapeHistoryResolvedByWidgetId = restResolved;
8673
8855
  setTimeout(() => {
8674
8856
  void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
8675
- syncWidgetShapeLivePreviews();
8676
- if (isFocusedPanel) panelDisplayRevision += 1;
8857
+ const newestDraft = getNewestDraftVersionId(generationWidgetId);
8858
+ if (newestDraft != null) {
8859
+ selectVersionInDropdowns(generationWidgetId, `v:${newestDraft}`);
8860
+ }
8677
8861
  });
8678
8862
  }, 1200);
8679
8863
  }
@@ -8845,11 +9029,9 @@ Requirements:
8845
9029
  [generationWidgetId]: 'preview',
8846
9030
  };
8847
9031
  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.
9032
+ // WS preview-ready was deferred during codegen so the Worker
9033
+ // iframe does not boot on the shared main thread at 80%.
8851
9034
  widgetShapeLivePreviewsRef?.requestReload?.(generationWidgetId);
8852
- panelDisplayRevision += 1;
8853
9035
  if (!completedPreviewBuildErr) {
8854
9036
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
8855
9037
  pendingCreatePreviewDefaultsWidgetId = null;
@@ -8863,15 +9045,10 @@ Requirements:
8863
9045
  await panelChat?.reloadConversationMessages?.();
8864
9046
  panelChat?.markWidgetHeadAtLatestCommit?.();
8865
9047
  }, 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);
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.
8875
9052
  // Do NOT auto-publish — user previews first, then clicks Publish / Set live.
8876
9053
  } else {
8877
9054
  panelChat?.updateLatestCodegenStatusMessage?.(
@@ -8949,6 +9126,7 @@ Requirements:
8949
9126
  showToast('error', 'Code generation error', { description: message, duration: 6000 });
8950
9127
  pushCodegenFailureToChat(generationWidgetId, message);
8951
9128
  } finally {
9129
+ noteCodegenActive(false);
8952
9130
  const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
8953
9131
  generatingWidgetIds = restFinally;
8954
9132
  widgetAbortControllers.delete(generationWidgetId);
@@ -9107,7 +9285,7 @@ Requirements:
9107
9285
  // rather than letting them silently become version 0.
9108
9286
  const rawTargetVersion = publishOpts.targetVersion;
9109
9287
  const targetVersion =
9110
- rawTargetVersion == null || rawTargetVersion === ''
9288
+ rawTargetVersion == null || (typeof rawTargetVersion === 'string' && rawTargetVersion === '')
9111
9289
  ? null
9112
9290
  : Number.isFinite(Number(rawTargetVersion))
9113
9291
  ? Number(rawTargetVersion)
@@ -10296,8 +10474,8 @@ Requirements:
10296
10474
  record?.artifact_url ||
10297
10475
  (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10298
10476
  if (isPublished && artifact) {
10299
- const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10300
- if (url) return url;
10477
+ const isolated = resolveWidgetShapeVersionPreviewState(widgetId);
10478
+ if (isolated?.url) return isolated.url;
10301
10479
  }
10302
10480
  const previewState = resolveWidgetShapeVersionPreviewState(widgetId);
10303
10481
  if (previewState?.url) return previewState.url;
@@ -10327,6 +10505,10 @@ Requirements:
10327
10505
  * Publish history is loaded without requiring chat to be open.
10328
10506
  */
10329
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> = {};
10330
10512
  // Historical previews are never interchangeable with the shared HEAD preview.
10331
10513
  let shapeVersionPreviewByWidgetId: Record<string, {
10332
10514
  versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
@@ -10440,12 +10622,21 @@ Requirements:
10440
10622
  (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
10441
10623
 
10442
10624
  if (isPublished && artifact) {
10443
- const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
10444
- 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.
10445
10629
  }
10446
- // 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.
10447
10634
  const newestDraft = getNewestDraftVersionId(widgetId);
10448
- 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) {
10449
10640
  return workerUrl;
10450
10641
  }
10451
10642
  return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
@@ -10464,6 +10655,8 @@ Requirements:
10464
10655
  }
10465
10656
 
10466
10657
  function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
10658
+ const restored = restoredViewingVersionByWidgetId[widgetId];
10659
+ if (Number.isFinite(restored)) return `v${restored}`;
10467
10660
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
10468
10661
  const ps = getPanelDisplayState(widgetId);
10469
10662
  const liveVersion =
@@ -10526,6 +10719,7 @@ Requirements:
10526
10719
  dateLabel?: string;
10527
10720
  isLive?: boolean;
10528
10721
  isDraft?: boolean;
10722
+ commitSha?: string;
10529
10723
  }> = [];
10530
10724
  for (const record of history) {
10531
10725
  const version = Number(record.version);
@@ -10564,6 +10758,10 @@ Requirements:
10564
10758
  }
10565
10759
 
10566
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}`;
10567
10765
  const source = shapePreviewSourceByWidgetId[widgetId];
10568
10766
  if (source?.startsWith('v:')) return source;
10569
10767
  if (source === 'preview') {
@@ -10579,8 +10777,21 @@ Requirements:
10579
10777
  return liveVersion != null ? `v:${liveVersion}` : null;
10580
10778
  }
10581
10779
 
10780
+ function selectVersionInDropdowns(widgetId: string, versionId: string): void {
10781
+ dropdownVersionByWidgetId = { ...dropdownVersionByWidgetId, [widgetId]: versionId };
10782
+ widgetShapeLivePreviewsRef?.setSelectedVersion?.(widgetId, versionId);
10783
+ panelDisplayRevision += 1;
10784
+ }
10785
+
10582
10786
  function handleWidgetShapeVersionChange(widgetId: string, versionId: string, buildPreview = true): void {
10583
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
+ }
10584
10795
  // Re-selecting an already successful/in-flight preview must not reset its state.
10585
10796
  const currentPreview = resolveWidgetShapeVersionPreviewState(widgetId);
10586
10797
  if (shapePreviewSourceByWidgetId[widgetId] === versionId
@@ -10599,17 +10810,19 @@ Requirements:
10599
10810
  };
10600
10811
  syncWidgetShapeLivePreviews();
10601
10812
  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.
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.
10605
10816
  if (versionId.startsWith('v:')) {
10606
10817
  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' });
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) {
10611
10824
  const detailsRef = getWidgetDetailsRef(widgetId);
10612
- detailsRef?.showPublishedArtifact?.(directUrl);
10825
+ detailsRef?.showPublishedArtifact?.(workerUrl);
10613
10826
  } else {
10614
10827
  void ensureWidgetShapeVersionPreview(widgetId, version);
10615
10828
  }
@@ -10688,6 +10901,13 @@ Requirements:
10688
10901
  /** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
10689
10902
  function resolveViewingCommitShaForPanel(widgetId: string): string | null {
10690
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
+ }
10691
10911
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
10692
10912
  if (source === 'preview') {
10693
10913
  return normalizeCommitSha(ps.lastCommitId);
@@ -10738,18 +10958,28 @@ Requirements:
10738
10958
  * debug mode and this label is what the turn shows.
10739
10959
  */
10740
10960
  let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10961
+ let versionPromptLabelsByWidgetId: Record<string, Record<string, string>> = {};
10962
+ let viewingVersionByWidgetId: Record<string, number | null> = {};
10741
10963
  $: {
10742
10964
  void shapePreviewSourceByWidgetId;
10743
10965
  void shapeVersionPreviewByWidgetId;
10966
+ void restoredViewingVersionByWidgetId;
10967
+ void dropdownVersionByWidgetId;
10744
10968
  void panelDisplayRevision;
10745
10969
  const nextHeads: Record<string, string | null> = {};
10746
10970
  const nextLabels: Record<string, Record<string, string>> = {};
10971
+ const nextPrompts: Record<string, Record<string, string>> = {};
10972
+ const nextViewing: Record<string, number | null> = {};
10747
10973
  for (const widgetId of openWidgetPanelIds) {
10748
10974
  nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
10749
10975
  nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
10976
+ nextPrompts[widgetId] = buildVersionPromptLabelsForPanel(widgetId);
10977
+ nextViewing[widgetId] = viewingVersionNumberForPanel(widgetId);
10750
10978
  }
10751
10979
  headCommitShaByWidgetId = nextHeads;
10752
10980
  versionLabelsByWidgetId = nextLabels;
10981
+ versionPromptLabelsByWidgetId = nextPrompts;
10982
+ viewingVersionByWidgetId = nextViewing;
10753
10983
  }
10754
10984
 
10755
10985
  /** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
@@ -10758,24 +10988,55 @@ Requirements:
10758
10988
  return normalized ? normalized.slice(0, 7) : null;
10759
10989
  }
10760
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
+
10761
10999
  function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
10762
11000
  const labels: Record<string, string> = {};
10763
11001
  const ps = getPanelDisplayState(widgetId);
10764
11002
  const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10765
11003
  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}`;
11004
+ if (record?.version == null) continue;
11005
+ assignVersionLabelKeys(labels, record?.commitSha ?? record?.commit_sha, `Version ${record.version}`);
10768
11006
  }
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
11007
  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)}`;
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}`;
10775
11029
  }
10776
11030
  return labels;
10777
11031
  }
10778
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
+
10779
11040
  function syncWidgetShapeLivePreviews(): void {
10780
11041
  measureSync('sync-widget-shape-previews', () => {
10781
11042
  try {
@@ -11010,10 +11271,13 @@ Requirements:
11010
11271
  if (embedded && deepLinkId && widgetId === deepLinkId) {
11011
11272
  embeddedDeepLinkPanelDismissed = false;
11012
11273
  }
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 });
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 });
11017
11281
  }
11018
11282
  }
11019
11283
  const isNewPanel = !openWidgetPanelIds.includes(widgetId);
@@ -12021,7 +12285,7 @@ Requirements:
12021
12285
  * from whatever HEAD happens to contain.
12022
12286
  */
12023
12287
  function canPublishDraftVersionForPanel(widgetId: string, version: number | null | undefined): boolean {
12024
- if (version == null || version === '' || !Number.isFinite(Number(version))) return false;
12288
+ if (version == null || (typeof version === 'string' && version === '') || !Number.isFinite(Number(version))) return false;
12025
12289
  if (isPublishing && publishingWidgetId === widgetId) return false;
12026
12290
  if (isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId) return false;
12027
12291
  if (generatingWidgetIds[widgetId]) return false;
@@ -12150,7 +12414,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
12150
12414
  }
12151
12415
  if (publishWidgetId) {
12152
12416
  // console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
12153
- setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
12417
+ scheduleQuietScreenshot(publishWidgetId, 2000);
12154
12418
  }
12155
12419
  if (embedded && onPublishComplete && publishWidgetId) {
12156
12420
  onPublishComplete({
@@ -12198,7 +12462,7 @@ $: if ($widgetPublishStatus.widgetId === selectedWidgetId && $widgetPublishStatu
12198
12462
  // console.log('[CrossTab/WS] Publish building status from WebSocket — disabling Publish button');
12199
12463
  }
12200
12464
 
12201
- // 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.
12202
12466
  $: if ($agentHarnessOutput?.timestamp) {
12203
12467
  const raw = ($agentHarnessOutput.output || '').trim();
12204
12468
  const line = raw.replace(/^🔧\s*/, '').trim();
@@ -12214,15 +12478,27 @@ $: if ($agentHarnessOutput?.timestamp) {
12214
12478
  }
12215
12479
  }
12216
12480
 
12217
- // 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;
12218
12486
  $: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previewUrl) {
12219
12487
  const wsReadyWidgetId = $dynamicWorkerPreviewStatus.widgetId;
12220
- shapePreviewSourceByWidgetId = {
12221
- ...shapePreviewSourceByWidgetId,
12222
- [wsReadyWidgetId]: 'preview',
12223
- };
12224
- syncWidgetShapeLivePreviews();
12225
- 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
+ }
12226
12502
  }
12227
12503
 
12228
12504
  // ===================== Publish History =====================
@@ -12898,6 +13174,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12898
13174
  * parent-page [FreezeReport] (long tasks attributed to the iframe).
12899
13175
  */
12900
13176
  const screenshotCaptureInFlight = new Set<string>();
13177
+ /** Bumped when a generation starts so in-flight html2canvas results are dropped. */
13178
+ let screenshotGenerationEpoch = 0;
12901
13179
 
12902
13180
  /**
12903
13181
  * Called when a Widget Image shape is clicked on the canvas.
@@ -14988,7 +15266,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14988
15266
  {@const panelInstanceKey = panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId}
14989
15267
  {@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
14990
15268
  {@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
14991
- {@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
15269
+ {@const ps = rev(panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
14992
15270
  <!--
14993
15271
  Same published-version resolution the WidgetDetails instance below gets.
14994
15272
  `ps.lastPublishedVersion` alone is only hydrated from the focused panel /
@@ -14999,9 +15277,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14999
15277
  {@const panelPublishedVersion = isDraftConvertId(panelWidgetId)
15000
15278
  ? null
15001
15279
  : (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))}
15280
+ {@const panelHasUnpublishedChanges = rev(panelDisplayRevision, hasUnpublishedChangesForPanel(panelWidgetId), widgets.length)}
15281
+ {@const panelHasCompileErrorState = rev(panelDisplayRevision, panelHasCompileError(panelWidgetId))}
15282
+ {@const panelHasInfraPreviewError = rev(panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
15005
15283
  <svelte:component this={WidgetDetails}
15006
15284
  bind:this={widgetDetailsRefs[panelInstanceKey]}
15007
15285
  userSession={$userSession}
@@ -15330,17 +15608,19 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15330
15608
  {@const defaultVersionSource = panelPublishedVersion !== null
15331
15609
  ? `v:${panelPublishedVersion}`
15332
15610
  : (newestDraft ? `v:${newestDraft.version}` : null)}
15333
- {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
15334
- {@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:')
15335
15617
  ? parseInt(shapeSource.slice(2), 10)
15336
15618
  : 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)
15619
+ ? (getNewestDraftVersionId(panelWidgetId) ?? panelPublishedVersion)
15342
15620
  : panelPublishedVersion}
15343
- {@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
15621
+ {@const viewingIsLive = !Number.isFinite(restoredVersion)
15622
+ && shapeSource !== 'preview'
15623
+ && Number(viewingVersion) === Number(panelPublishedVersion)}
15344
15624
  <!--
15345
15625
  A version is a Draft until it is published. The history is one
15346
15626
  sequence (`v1, v2, v3…`), so "draft" is read from the record's
@@ -15541,9 +15821,16 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15541
15821
  <Button
15542
15822
  onclick={() => {
15543
15823
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
15544
- lastGenerationFailed = false;
15545
- lastGenerationError = null;
15546
- 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
+ }
15547
15834
  }}
15548
15835
  variant="outline"
15549
15836
  size="sm"
@@ -15558,8 +15845,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15558
15845
  </svelte:fragment>
15559
15846
 
15560
15847
  <svelte:fragment slot="code-generation">
15848
+ {@const chatUserHeight = rev(panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
15561
15849
  <!-- svelte-ignore a11y_no_static_element_interactions -->
15562
- {@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
15563
15850
  <div
15564
15851
  class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
15565
15852
  style="padding: 0;"
@@ -15605,6 +15892,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15605
15892
  }}
15606
15893
  widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15607
15894
  versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15895
+ versionLabelByPrompt={versionPromptLabelsByWidgetId[panelWidgetId] || {}}
15896
+ viewingVersion={viewingVersionByWidgetId[panelWidgetId] ?? null}
15608
15897
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15609
15898
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
15610
15899
  onUploadChatFile={handleChatUploadFile}
@@ -15614,8 +15903,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15614
15903
  onLoadConversations={handleLoadConversations}
15615
15904
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
15616
15905
  onCreateConversation={handleCreateConversation}
15617
- onSaveMessage={(conversationId, content, messageType, attachments) =>
15618
- handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
15906
+ onSaveMessage={(conversationId, content, messageType, attachments, suggestions) =>
15907
+ handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId)
15619
15908
  }
15620
15909
  onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
15621
15910
  handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
@@ -15906,11 +16195,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15906
16195
 
15907
16196
  <!-- Embed Dimensions -->
15908
16197
  <div class="embed-dimensions-ct flex flex-col gap-2">
15909
- <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>
15910
16199
  <div class="embed-dimensions-row flex items-center gap-2">
15911
16200
  <div class="embed-dim-input flex items-center gap-1">
15912
16201
  <span class="text-[10px] text-gray-400 font-medium">W</span>
15913
16202
  <input
16203
+ id="embed-width-input"
16204
+ aria-label="Embed width"
15914
16205
  type="number"
15915
16206
  min="100"
15916
16207
  max="4000"
@@ -15929,6 +16220,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15929
16220
  <div class="embed-dim-input flex items-center gap-1">
15930
16221
  <span class="text-[10px] text-gray-400 font-medium">H</span>
15931
16222
  <input
16223
+ id="embed-height-input"
16224
+ aria-label="Embed height"
15932
16225
  type="number"
15933
16226
  min="50"
15934
16227
  max="4000"
@@ -15974,6 +16267,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15974
16267
  <!-- Embed Code Block -->
15975
16268
  <div class="embed-code-ct flex flex-col gap-2">
15976
16269
  <div class="embed-code-header flex items-center justify-between">
16270
+ <!-- svelte-ignore a11y_label_has_associated_control -->
15977
16271
  <label class="text-xs font-medium text-gray-600">Embed Code ({EMBED_FORMAT_LABELS[effectiveFormat]})</label>
15978
16272
  <!-- svelte-ignore a11y_consider_explicit_label -->
15979
16273
  <button
@@ -16095,8 +16389,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16095
16389
  <p class="text-sm text-gray-600 mb-4">
16096
16390
  Permanently delete <strong>{adminDeleteWidget?.name || adminDeleteWidget?.id?.slice(0, 8)}</strong>? This action cannot be undone.
16097
16391
  </p>
16098
- <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>
16099
16393
  <input
16394
+ id="admin-delete-api-key"
16100
16395
  type="password"
16101
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"
16102
16397
  placeholder="Enter admin key..."
@@ -16355,11 +16650,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16355
16650
  :global([data-dialog-overlay]) {
16356
16651
  z-index: 9998 !important;
16357
16652
  }
16358
- .generation-models-header select:focus,
16359
- .generation-models-header select:focus-visible {
16360
- outline: none;
16361
- box-shadow: none;
16362
- }
16363
16653
  .wc-version-history-toggle:focus,
16364
16654
  .wc-version-history-toggle:focus-visible,
16365
16655
  .wc-live-history-backdrop:focus,