@widgetic/creator 0.3.65 → 0.3.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,12 @@
27
27
  UploadsApi,
28
28
  ResponseError
29
29
  } from '@widgetic/api-sdk';
30
- import type { Middleware } from '@widgetic/api-sdk';
30
+ import type {
31
+ Middleware,
32
+ UpdateComposition,
33
+ GenerateCodeRequest,
34
+ AgentModelsGet200ResponseModelsInner
35
+ } from '@widgetic/api-sdk';
31
36
  import { parseVersionPreviewError } from './utils/versionPreviewError.js';
32
37
 
33
38
  /**
@@ -71,7 +76,7 @@
71
76
  import { persistWidgetThumbnail } from './utils/persistWidgetThumbnail.js';
72
77
  import { nativeScrollbarPadding } from './utils/nativeScrollbarPadding.js';
73
78
  import { pinScrollToBottom } from './utils/pinScrollToBottom.js';
74
- import { installFreezeWatchdog } from './utils/freezeWatchdog.js';
79
+ import { installFreezeWatchdog, measureSync } from './utils/freezeWatchdog.js';
75
80
  import { streamOperationUpdates } from './utils/operationStream.js';
76
81
  import { generateEmbedCode, EMBED_FORMATS, EMBED_FORMAT_LABELS, type EmbedFormat } from './utils/embedCode.js';
77
82
 
@@ -113,7 +118,9 @@
113
118
  /** Opt-in Dexie L1 — host (site / standalone) passes true. */
114
119
  export let enableLocalCache: CreatorAppProps['enableLocalCache'] = false;
115
120
  export let onPublishComplete: CreatorAppProps['onPublishComplete'] = undefined;
116
- export let onBack: CreatorAppProps['onBack'] = undefined;
121
+ // Passed by the site host but never read here — export const keeps the
122
+ // init-set contract without the unused-reactive warning.
123
+ export const onBack: CreatorAppProps['onBack'] = undefined;
117
124
  export let onWidgetRenamed: CreatorAppProps['onWidgetRenamed'] = undefined;
118
125
 
119
126
  // Opt-in Dexie owner — reactive to prop changes without $effect (would force runes mode).
@@ -121,7 +128,8 @@
121
128
 
122
129
  function getRealTokenExpUnix(token: string): number | null {
123
130
  const decoded = decodeJWT(token);
124
- return decoded?.exp ?? null;
131
+ const exp = decoded?.exp;
132
+ return typeof exp === 'number' && Number.isFinite(exp) ? exp : null;
125
133
  }
126
134
 
127
135
  let didForceCreateNewWidget = false;
@@ -485,7 +493,10 @@
485
493
 
486
494
  // Lightweight static imports from canvas (excludes Canvas.svelte — 10K+ lines)
487
495
  import { Tooltip } from '@widgetic/canvas/components';
488
- import type { WidgetConversionPayload } from '@widgetic/canvas/components';
496
+ // WidgetConversionPayload lives on the canvas root export (re-exported from
497
+ // ./canvas); the components barrel only carries the input controls. Type-only
498
+ // import, so the 10K-line Canvas.svelte still never loads.
499
+ import type { WidgetConversionPayload } from '@widgetic/canvas';
489
500
 
490
501
  // Chat module for code generation conversation UI
491
502
  import { Chat, ContextType } from '@widgetic/chat';
@@ -518,6 +529,8 @@
518
529
  onMount(async () => {
519
530
  if (canvasModulePromise) {
520
531
  const canvasModule = await canvasModulePromise;
532
+ // Installed before the canvas mounts so the first save already externalizes
533
+ installCanvasThumbnailStore(canvasModule);
521
534
  WidgeticCanvas = canvasModule.Canvas;
522
535
  }
523
536
  installLongTaskFreezeReporter();
@@ -579,7 +592,10 @@
579
592
  saveWidgetDraft,
580
593
  readWidgetDraft,
581
594
  deleteWidgetDraft,
582
- copyWidgetDraft
595
+ copyWidgetDraft,
596
+ saveShapeThumbnail,
597
+ readShapeThumbnail,
598
+ canPersistShapeThumbnails
583
599
  } from './utils/creatorDraftStorage';
584
600
 
585
601
  /**
@@ -635,10 +651,31 @@
635
651
  repositoryId?: string | null;
636
652
  repository_id?: string | null;
637
653
  repoId?: string | null;
654
+ repo_id?: string | null;
638
655
  name?: string;
639
656
  [extra: string]: unknown;
640
657
  }
641
658
 
659
+ /**
660
+ * View any API payload as the app's open WidgetSummary shape. The SDK's
661
+ * generated models (Widget, CreateWidget201Response, …) lack the index
662
+ * signature callers rely on for snake_case/extra fields, so direct
663
+ * assignments fail structurally; this documents the deliberate widening.
664
+ */
665
+ function asWidgetSummary(payload: unknown): WidgetSummary {
666
+ return (payload ?? {}) as WidgetSummary;
667
+ }
668
+
669
+ /**
670
+ * Reactivity marker for template {@const}s: a Svelte 4 {@const} only re-runs
671
+ * when a value referenced in its expression changes, so the panel-revision
672
+ * counter is passed explicitly. The bare comma idiom this replaces
673
+ * (`(revision, value)`) is a TS unused-expression error.
674
+ */
675
+ function rev<T>(revision: number, value: T, ..._alsoTracked: unknown[]): T {
676
+ return value;
677
+ }
678
+
642
679
  // LocalStorage keys for persisting selection across refresh
643
680
  const SELECTED_WIDGET_STORAGE_KEY = 'widgetCreator_selectedWidgetId';
644
681
  const SELECTED_CANVAS_STORAGE_KEY = 'widgetCreator_selectedCanvasId';
@@ -868,7 +905,7 @@
868
905
  // Chat component reference and context (focused panel)
869
906
  let chatMethods: {
870
907
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
871
- addAssistantMessage: (content: string, skipPersist?: boolean) => void;
908
+ addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: string[]) => void;
872
909
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
873
910
  getConversationId: () => string | null;
874
911
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -1004,6 +1041,7 @@
1004
1041
  fileSize?: number;
1005
1042
  }>,
1006
1043
  sourceWidgetId?: string,
1044
+ sendOptions?: { skipIntentGate?: boolean },
1007
1045
  ) {
1008
1046
  const widgetId = sourceWidgetId ?? selectedWidgetId;
1009
1047
  if (!widgetId) return;
@@ -1126,6 +1164,7 @@
1126
1164
  attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
1127
1165
  promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
1128
1166
  persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
1167
+ skipIntentGate: sendOptions?.skipIntentGate === true,
1129
1168
  });
1130
1169
  }
1131
1170
 
@@ -1376,6 +1415,76 @@
1376
1415
  return dataUrls.slice(0, 1);
1377
1416
  }
1378
1417
 
1418
+ /**
1419
+ * Keeps widget screenshots out of the canvas JSON.
1420
+ *
1421
+ * Fabric serialized each widget shape's screenshot inline as base64, so an
1422
+ * autosave stringified megabytes of image data and wrote it three times over —
1423
+ * the main-thread block the freeze reports kept naming. The canvas now stores a
1424
+ * reference and asks for the bytes back through this store: the local copy
1425
+ * first, then the widget's persisted thumbnail so a fresh browser still paints
1426
+ * the shape.
1427
+ */
1428
+ function installCanvasThumbnailStore(canvasModule: any): void {
1429
+ if (typeof canvasModule?.setShapeImageStore !== 'function') return;
1430
+ canvasModule.setShapeImageStore({
1431
+ save(ref: string, dataUrl: string) {
1432
+ // Refusing here tells the canvas to keep the bytes inline, so a save
1433
+ // before the owner is known cannot drop a screenshot on the floor.
1434
+ if (!canPersistShapeThumbnails()) throw new Error('No cache owner yet');
1435
+ saveShapeThumbnail(ref, dataUrl);
1436
+ },
1437
+ async load(ref: string): Promise<string | null> {
1438
+ const local = await readShapeThumbnail(ref);
1439
+ if (local) return local;
1440
+ const widgetId = ref.startsWith('widget:') ? ref.slice('widget:'.length) : null;
1441
+ if (!widgetId) return null;
1442
+ return await waitForWidgetThumbnailUrl(widgetId);
1443
+ }
1444
+ });
1445
+ }
1446
+
1447
+ /**
1448
+ * The canvas often loads before the widget list does, so a reference with no
1449
+ * local copy has nothing to resolve to yet. Give the list a few seconds to
1450
+ * arrive rather than leaving the shape blank until the next screenshot.
1451
+ */
1452
+ async function waitForWidgetThumbnailUrl(widgetId: string): Promise<string | null> {
1453
+ for (let attempt = 0; attempt < 12; attempt++) {
1454
+ const widget = getWidgetById(widgetId) as any;
1455
+ const url = widget?.thumbnailUrl || widget?.thumbnail_url;
1456
+ if (url) return url;
1457
+ // The canvas widget list is slim and often omits the thumbnail, so ask the
1458
+ // API for this one widget rather than leaving the shape blank.
1459
+ if (widget) return await fetchWidgetThumbnailUrl(widgetId);
1460
+ await new Promise((resolve) => setTimeout(resolve, 500));
1461
+ }
1462
+ return null;
1463
+ }
1464
+
1465
+ /** One-widget lookup for the persisted thumbnail, cached per widget. */
1466
+ const thumbnailUrlRequests = new Map<string, Promise<string | null>>();
1467
+
1468
+ function fetchWidgetThumbnailUrl(widgetId: string): Promise<string | null> {
1469
+ const pending = thumbnailUrlRequests.get(widgetId);
1470
+ if (pending) return pending;
1471
+ const request = (async () => {
1472
+ if (!widgetsClient) return null;
1473
+ try {
1474
+ const response = (await widgetsClient.getWidgetsBatch({
1475
+ getWidgetsBatchRequest: { widgetIds: [widgetId] }
1476
+ })) as { widgets?: any[]; data?: { widgets?: any[] } };
1477
+ const fetched = (response?.widgets ?? response?.data?.widgets ?? [])[0];
1478
+ return fetched?.thumbnailUrl ?? fetched?.thumbnail_url ?? null;
1479
+ } catch (error) {
1480
+ console.warn('[CreatorApp] thumbnail lookup failed', widgetId.slice(0, 8), error);
1481
+ return null;
1482
+ }
1483
+ })();
1484
+ thumbnailUrlRequests.set(widgetId, request);
1485
+ return request;
1486
+ }
1487
+
1379
1488
  async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string, isCurrent = () => true): Promise<void> {
1380
1489
  if (!widgetsClient || !widgetId || !imageDataUrl || !isCurrent()) return;
1381
1490
  try {
@@ -1490,10 +1599,12 @@
1490
1599
  : null) || userMessages[userMessages.length - 1] || null;
1491
1600
  const existingUrls = new Set(
1492
1601
  (targetUser?.attachments || [])
1493
- .map((a: { url?: string }) => a.url)
1494
- .filter((url: string | undefined): url is string => !!url),
1602
+ .map((a: { url?: unknown }) => a.url)
1603
+ .filter((url): url is string => typeof url === 'string' && url.length > 0),
1604
+ );
1605
+ const existingHasRemoteImage = [...existingUrls].some(
1606
+ (url) => typeof url === 'string' && /^https?:\/\//i.test(url)
1495
1607
  );
1496
- const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
1497
1608
  // Chat already persisted CDN attachments for this turn — do not merge data URLs/canvas shots.
1498
1609
  if (targetUser && existingHasRemoteImage) {
1499
1610
  if (convId !== visibleConversationId) {
@@ -1649,6 +1760,14 @@
1649
1760
  * Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
1650
1761
  */
1651
1762
  async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
1763
+ // Loading a widget in a hidden iframe costs a full Svelte + Tailwind CDN
1764
+ // startup on the main thread this page shares with it, and the capture that
1765
+ // follows is refused during a generation anyway (`requestIframeScreenshot`),
1766
+ // so the whole path is skipped rather than paying the load for nothing.
1767
+ if (isAnyWidgetGenerating()) {
1768
+ console.log('[Chat→Screenshot] Skip hidden Worker capture — codegen in progress', widgetId?.substring(0, 8));
1769
+ return null;
1770
+ }
1652
1771
  const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
1653
1772
  console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
1654
1773
  widgetId: widgetId.substring(0, 8),
@@ -1716,7 +1835,10 @@
1716
1835
  await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
1717
1836
  if (!isCurrent()) return;
1718
1837
  capturedCanvas?.updateWidgetImage(widgetId, screenshot);
1719
- const canvasJson = capturedCanvas?.saveCanvas?.();
1838
+ const canvasJson = measureSync(
1839
+ 'canvas-serialize:shape-refresh-thumbnail',
1840
+ () => capturedCanvas?.saveCanvas?.() ?? null,
1841
+ );
1720
1842
  if (canvasJson) void handleCanvasAutoSave(canvasJson);
1721
1843
  } catch (error) {
1722
1844
  console.warn('[WidgetShape] Refresh thumbnail capture failed', widgetId, error);
@@ -2479,8 +2601,14 @@
2479
2601
 
2480
2602
  /** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
2481
2603
  const CHAT_DEFAULT_HEIGHT = 400;
2482
- const CHAT_MIN_HEIGHT = 400;
2604
+ const CHAT_MIN_HEIGHT = 260;
2483
2605
  const CHAT_MAX_HEIGHT = 2000;
2606
+ /**
2607
+ * Floor applied to the chat box while it auto-fills the create step. A floor
2608
+ * taller than the available area makes the surrounding scroll container
2609
+ * scroll, which puts the chat input below the fold on open.
2610
+ */
2611
+ const CHAT_AUTO_MIN_HEIGHT = 0;
2484
2612
 
2485
2613
  function getChatContainerHeightForPanel(widgetId: string): number | null {
2486
2614
  const stored = getPanelState(widgetId).chatContainerHeight;
@@ -2565,7 +2693,7 @@
2565
2693
  if (!schema) return schema;
2566
2694
  try {
2567
2695
  const clone = structuredClone(schema);
2568
- const groups = Array.isArray(clone) ? clone : clone.componentGroups;
2696
+ const groups = Array.isArray(clone) ? clone : (clone as { componentGroups?: unknown }).componentGroups;
2569
2697
  if (!Array.isArray(groups) || groups.length === 0) return schema;
2570
2698
  const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
2571
2699
  (group.components || []).flatMap((component) => component.properties || [])
@@ -3657,7 +3785,7 @@
3657
3785
  !compositionRecordProjectId(selectedComposition as Record<string, unknown>);
3658
3786
  const res = await compositionsClient.updateComposition({
3659
3787
  id: selectedComposition.id,
3660
- updateCompositionRequest: {
3788
+ updateComposition: {
3661
3789
  content: buildCompositionContentPayload(),
3662
3790
  ...(needsProjectBackfill ? projectBackfill : {}),
3663
3791
  }
@@ -3749,7 +3877,7 @@
3749
3877
  try {
3750
3878
  await compositionsClient.updateComposition({
3751
3879
  id: compositionId,
3752
- updateCompositionRequest: { name: trimmed }
3880
+ updateComposition: { name: trimmed }
3753
3881
  });
3754
3882
  showToast('success', 'Composition renamed');
3755
3883
  await loadCompositions();
@@ -3810,7 +3938,9 @@
3810
3938
  };
3811
3939
  await compositionsClient.updateComposition({
3812
3940
  id: selectedComposition.id,
3813
- updateCompositionRequest: { embedSettings }
3941
+ // The gateway accepts embedSettings (compositionSchemas update schema);
3942
+ // the generated UpdateComposition model predates that field.
3943
+ updateComposition: { embedSettings } as unknown as UpdateComposition
3814
3944
  });
3815
3945
  selectedComposition = { ...selectedComposition, embedSettings };
3816
3946
  embedDimensionsChanged = false;
@@ -4006,11 +4136,11 @@
4006
4136
  // SDK returns { success, models } directly (not nested under data)
4007
4137
  if (response.success && Array.isArray(response.models)) {
4008
4138
  // Transform API response to match our interface
4009
- availableModels = response.models.map((m: { id: string; displayName: string; provider: string; description: string; supportsVision?: boolean }) => ({
4010
- id: m.id,
4011
- name: m.displayName,
4012
- provider: m.provider.charAt(0).toUpperCase() + m.provider.slice(1), // Capitalize provider
4013
- description: m.description,
4139
+ availableModels = response.models.map((m: AgentModelsGet200ResponseModelsInner) => ({
4140
+ id: m.id ?? '',
4141
+ name: m.displayName ?? '',
4142
+ provider: (m.provider ?? '').charAt(0).toUpperCase() + (m.provider ?? '').slice(1), // Capitalize provider
4143
+ description: m.description ?? '',
4014
4144
  supportsVision: m.supportsVision === true,
4015
4145
  }));
4016
4146
 
@@ -4750,7 +4880,12 @@
4750
4880
 
4751
4881
  // Step 3: Check operation status
4752
4882
  try {
4753
- const op = await operationsClient.operationsIdGet({ id: operationId });
4883
+ const op = (await operationsClient.operationsIdGet({ id: operationId })) as {
4884
+ status?: string;
4885
+ error?: string;
4886
+ progress?: number;
4887
+ metadata?: Record<string, unknown>;
4888
+ };
4754
4889
  const status = (op?.status || '').toLowerCase();
4755
4890
 
4756
4891
  if (status === 'completed') {
@@ -5698,22 +5833,29 @@
5698
5833
  const res = await widgetsClient.getWidgets({ canvasId: canvasId ?? undefined });
5699
5834
 
5700
5835
  // normalize error shape from apiRequest
5701
- if (res && (res.error || res.message)) {
5836
+ const envelope = res as unknown as {
5837
+ error?: string;
5838
+ message?: string;
5839
+ data?: unknown;
5840
+ };
5841
+ if (envelope && (envelope.error || envelope.message)) {
5702
5842
  // console.error('Widget Creator: loadWidgetsForCanvas: response not ok:', res);
5703
5843
  widgets = [];
5704
5844
  selectedWidgetId = null;
5705
5845
  isLoadingWidgets = false;
5706
- widgetsError = res.message || res.error || 'Failed to load widgets';
5846
+ widgetsError = envelope.message || envelope.error || 'Failed to load widgets';
5707
5847
  throw new Error(widgetsError);
5708
5848
  }
5709
5849
 
5710
- // if the response is ok, parse the data
5850
+ // if the response is ok, parse the data (bare list, {data: list} or {data: {data: list}})
5851
+ const envelopeData = envelope?.data;
5852
+ const envelopeNestedData = (envelopeData as { data?: unknown } | undefined)?.data;
5711
5853
  const fetched = Array.isArray(res)
5712
- ? (res as WidgetSummary[])
5713
- : Array.isArray(res?.data?.data)
5714
- ? (res.data.data as WidgetSummary[])
5715
- : Array.isArray(res?.data)
5716
- ? (res.data as WidgetSummary[])
5854
+ ? (res as unknown as WidgetSummary[])
5855
+ : Array.isArray(envelopeNestedData)
5856
+ ? (envelopeNestedData as unknown as WidgetSummary[])
5857
+ : Array.isArray(envelopeData)
5858
+ ? (envelopeData as unknown as WidgetSummary[])
5717
5859
  : [];
5718
5860
 
5719
5861
  const previousById = new Map(
@@ -5810,9 +5952,13 @@
5810
5952
  }
5811
5953
 
5812
5954
  function getWidgetCreatedAtMs(widget: WidgetSummary): number {
5813
- const raw = widget.createdAt ?? widget.created_at;
5814
- if (!raw) return 0;
5815
- const ms = new Date(raw).getTime();
5955
+ const rawCreated = widget.createdAt ?? widget.created_at;
5956
+ if (!rawCreated) return 0;
5957
+ const ms = new Date(
5958
+ typeof rawCreated === 'string' || typeof rawCreated === 'number' || rawCreated instanceof Date
5959
+ ? rawCreated
5960
+ : String(rawCreated)
5961
+ ).getTime();
5816
5962
  return Number.isFinite(ms) ? ms : 0;
5817
5963
  }
5818
5964
 
@@ -6218,10 +6364,10 @@
6218
6364
  async function resolvePrimaryWidgetIdForCanvas(targetCanvasId: string): Promise<string | null> {
6219
6365
  if (!canvasesClient || !targetCanvasId?.trim()) return null;
6220
6366
  try {
6221
- const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
6222
- const record =
6223
- (canvasRecord as { data?: Record<string, unknown> })?.data ??
6224
- (canvasRecord as Record<string, unknown>);
6367
+ const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
6368
+ const record =
6369
+ (canvasRecord as { data?: Record<string, unknown> })?.data ??
6370
+ (canvasRecord as unknown as Record<string, unknown>);
6225
6371
  const raw =
6226
6372
  (record as { primaryWidgetId?: unknown })?.primaryWidgetId ??
6227
6373
  (record as { primary_widget_id?: unknown })?.primary_widget_id ??
@@ -6703,7 +6849,12 @@
6703
6849
  if (!widgetId || !agentClient) return null;
6704
6850
  try {
6705
6851
  const json = await agentClient.agentWidgetBuilderRepoHeadWidgetIdGet({ widgetId });
6706
- const data = json?.data ?? json;
6852
+ const repoHeadEnvelope = json as unknown as {
6853
+ data?: { repoHeadCommitSha?: string | null; repo_head_commit_sha?: string | null };
6854
+ repoHeadCommitSha?: string | null;
6855
+ repo_head_commit_sha?: string | null;
6856
+ };
6857
+ const data = repoHeadEnvelope?.data ?? repoHeadEnvelope;
6707
6858
  const sha = data?.repoHeadCommitSha ?? data?.repo_head_commit_sha ?? null;
6708
6859
  return typeof sha === 'string' && sha.length > 0 ? sha : null;
6709
6860
  } catch (err) {
@@ -7021,13 +7172,14 @@
7021
7172
  }
7022
7173
  }
7023
7174
  );
7024
- if (!response || response.error || response.message) {
7175
+ const createEnvelope = response as unknown as { error?: string; message?: string };
7176
+ if (!response || createEnvelope.error || createEnvelope.message) {
7025
7177
  console.error('Widget create failed:', response);
7026
- authStatus = `Create widget failed: ${response?.message || response?.error || 'Unknown error'}`;
7178
+ authStatus = `Create widget failed: ${createEnvelope.message || createEnvelope.error || 'Unknown error'}`;
7027
7179
  return;
7028
7180
  }
7029
7181
  // Ensure we have an id
7030
- const newWidget = response.data || response; // handle envelope or direct
7182
+ const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response); // handle envelope or direct
7031
7183
  if (newWidget?.id) {
7032
7184
  widgets = [{ ...newWidget }, ...widgets];
7033
7185
  selectWidget(newWidget);
@@ -7702,6 +7854,16 @@
7702
7854
  canvasShapePreviewSyncTimers.set(widgetId, timer);
7703
7855
  }
7704
7856
 
7857
+ /**
7858
+ * Serializing the canvas stringifies every shape's embedded image, so it is
7859
+ * megabytes of synchronous work on a busy canvas. The thumbnail paths run it
7860
+ * immediately after a capture, which is exactly where the freeze reports land,
7861
+ * so each call is measured and named.
7862
+ */
7863
+ function serializeCanvasJson(reason: string): string | null {
7864
+ return measureSync(`canvas-serialize:${reason}`, () => canvasRef?.saveCanvas?.() ?? null);
7865
+ }
7866
+
7705
7867
  async function handleRefreshScreenshot(widgetId: string, options: { quiet?: boolean } = {}) {
7706
7868
  const quiet = !!options.quiet;
7707
7869
  // console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
@@ -7757,7 +7919,7 @@
7757
7919
  const freshCapture = await handleChatScreenshot(widgetId, null, quiet);
7758
7920
  if (freshCapture) {
7759
7921
  canvasRef?.updateWidgetImage(widgetId, freshCapture);
7760
- const freshJson = canvasRef?.saveCanvas?.();
7922
+ const freshJson = serializeCanvasJson('quiet-shape-thumbnail');
7761
7923
  if (freshJson) void handleCanvasAutoSave(freshJson);
7762
7924
  void persistWidgetListThumbnail(widgetId, freshCapture);
7763
7925
  } else {
@@ -7816,7 +7978,7 @@
7816
7978
  if (result.data && canvasRef) {
7817
7979
  canvasRef.updateWidgetImage(widgetId, result.data);
7818
7980
  setTimeout(() => {
7819
- const json = canvasRef?.saveCanvas();
7981
+ const json = serializeCanvasJson('screenshot-refresh');
7820
7982
  if (json) handleCanvasAutoSave(json);
7821
7983
  }, 500);
7822
7984
  void persistWidgetListThumbnail(widgetId, result.data);
@@ -7928,6 +8090,8 @@ Requirements:
7928
8090
  attachmentIds?: string[];
7929
8091
  /** Image URLs / data URLs captured before chat clearDraft. */
7930
8092
  promptImagesOverride?: string[];
8093
+ /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8094
+ skipIntentGate?: boolean;
7931
8095
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
7932
8096
  persistFileAttachments?: Array<{
7933
8097
  url?: string;
@@ -8017,6 +8181,7 @@ Requirements:
8017
8181
  attachmentIds: options.attachmentIds,
8018
8182
  promptImagesOverride: options.promptImagesOverride,
8019
8183
  persistFileAttachments: options.persistFileAttachments,
8184
+ skipIntentGate: options.skipIntentGate,
8020
8185
  });
8021
8186
  }
8022
8187
 
@@ -8038,6 +8203,8 @@ Requirements:
8038
8203
  forceRecreateRepo?: boolean;
8039
8204
  attachmentIds?: string[];
8040
8205
  promptImagesOverride?: string[];
8206
+ /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8207
+ skipIntentGate?: boolean;
8041
8208
  persistFileAttachments?: Array<{
8042
8209
  url?: string;
8043
8210
  file?: File;
@@ -8274,17 +8441,67 @@ Requirements:
8274
8441
  modelId: coderModelForRequest || undefined,
8275
8442
  plannerModelId: plannerModelIdForRequest,
8276
8443
  widgetId: generationWidgetId,
8277
- conversationId: activeConversationId ?? undefined,
8278
- images: imagesToSend,
8444
+ conversationId: activeConversationId ?? undefined, images: imagesToSend,
8279
8445
  attachmentIds: attachmentIdsToSend,
8280
8446
  runtimeLogs: runtimeLogsForRequest,
8281
8447
  compileFixOnly: options.compileFixOnly === true,
8282
- }
8448
+ skipIntentGate: options.skipIntentGate === true,
8449
+ } as GenerateCodeRequest
8283
8450
  });
8284
8451
  // 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);
8285
8452
  // console.log('Widget Creator: Code generation response:', response);
8286
8453
 
8287
- if (response && response.error === 'TOKEN_EXPIRED') {
8454
+ // --- Intent gate: the server answered instead of generating ---
8455
+ // A question got a meta answer, an ambiguous ask got a clarification
8456
+ // card, or a publish/settings ask was redirected. None of these spent
8457
+ // a generation slot. Anything unexpected here falls through to the
8458
+ // normal path (operation tracking), never blocks a real build.
8459
+ const gatedData = (response as any)?.data;
8460
+ if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
8461
+ const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
8462
+ const gatedReply: string = gatedData.reply ?? '';
8463
+ const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8464
+
8465
+ clearActiveOperationId();
8466
+ patchCodegenUiState(generationWidgetId, {
8467
+ lastGenerationFailed: false,
8468
+ lastGenerationError: null,
8469
+ generateCodeStatus:
8470
+ gatedAction === 'answer'
8471
+ ? 'Answered from widget details — no generation needed.'
8472
+ : gatedAction === 'clarify'
8473
+ ? 'Need a bit more detail before generating.'
8474
+ : 'No generation needed — use the publish or settings control.',
8475
+ });
8476
+
8477
+ const chatMethods = getChatMethodsForWidget(generationWidgetId);
8478
+ if (gatedAction === 'clarify' && suggestions.length > 0 && chatMethods?.addAssistantMessage) {
8479
+ // Suggestions travel as message data, not flattened text — the chat
8480
+ // renders them as clickable chips that send the next user message.
8481
+ chatMethods.addAssistantMessage(gatedReply, false, suggestions);
8482
+ } else if (chatMethods?.addAssistantMessage) {
8483
+ chatMethods.addAssistantMessage(gatedReply);
8484
+ }
8485
+
8486
+ showToast(
8487
+ gatedAction === 'answer' ? 'info' : 'warning',
8488
+ gatedAction === 'answer' ? 'Answered without generating' : 'No generation started',
8489
+ {
8490
+ description: gatedReply.slice(0, 140),
8491
+ duration: 8000,
8492
+ },
8493
+ );
8494
+ generatingWidgetIds = (() => {
8495
+ const { [generationWidgetId]: _gated, ...rest } = generatingWidgetIds;
8496
+ return rest;
8497
+ })();
8498
+ isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
8499
+ setCrossTabGenerating(false, generationWidgetId);
8500
+ widgetAbortControllers.delete(generationWidgetId);
8501
+ return;
8502
+ }
8503
+
8504
+ if (response && (response as { error?: string }).error === 'TOKEN_EXPIRED') {
8288
8505
  patchCodegenUiState(generationWidgetId, {
8289
8506
  generateCodeStatus: '❌ Authentication expired. Please login again.',
8290
8507
  lastGenerationFailed: true,
@@ -8297,7 +8514,8 @@ Requirements:
8297
8514
  return;
8298
8515
  }
8299
8516
 
8300
- const generateErrorMessage = response && (response.error || response.message);
8517
+ const generateEnvelope = response as unknown as { error?: string; message?: string };
8518
+ const generateErrorMessage = generateEnvelope && (generateEnvelope.error || generateEnvelope.message);
8301
8519
  if (generateErrorMessage) {
8302
8520
  const normalizedMessage = normalizeErrorMessage(generateErrorMessage);
8303
8521
  patchCodegenUiState(generationWidgetId, {
@@ -8313,8 +8531,10 @@ Requirements:
8313
8531
  return;
8314
8532
  }
8315
8533
 
8316
- const responseData = response?.data || response;
8317
- const operationId = responseData?.operationId;
8534
+ const generateData = ((response as { data?: unknown }).data ?? response) as {
8535
+ operationId?: string;
8536
+ };
8537
+ const operationId = generateData?.operationId;
8318
8538
 
8319
8539
  // The Worker serves rebuilt code at the same preview URL, so the panel
8320
8540
  // must know when the current generation started — hasLiveWorkerPreview()
@@ -8397,6 +8617,16 @@ Requirements:
8397
8617
 
8398
8618
  const completedCommitSha = getOperationCommitId(op.metadata);
8399
8619
  const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
8620
+ const completedCached = getOperationCached(op.metadata);
8621
+ const completedCachedFiles = getOperationCachedFiles(op.metadata);
8622
+ const cachedChatSummary = completedCached
8623
+ ? `Already done — the widget is in the state you asked for.` +
8624
+ (completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : '') +
8625
+ '\nThat change was made earlier in this conversation, so no new generation was needed' +
8626
+ (completedCachedFiles.length > 0
8627
+ ? ':\n' + completedCachedFiles.map((p) => `- ${p}`).join('\n')
8628
+ : '.')
8629
+ : null;
8400
8630
  const generationPanelRef = getWidgetDetailsRef(generationWidgetId);
8401
8631
  const isFocusedPanel = generationWidgetId === focusedPanelWidgetId;
8402
8632
  const completedWidgetName = getWidgetById(generationWidgetId)?.name || 'widget';
@@ -8521,11 +8751,13 @@ Requirements:
8521
8751
  });
8522
8752
 
8523
8753
  const commitShort = completedCommitSha ? completedCommitSha.substring(0, 7) : '';
8524
- panelChat?.updateLatestCodegenStatusMessage?.(
8525
- completedPreviewBuildErr
8526
- ? `Code saved${commitShort ? ` (${commitShort})` : ''} — building preview…`
8527
- : `Code saved${commitShort ? ` (${commitShort})` : ''} — loading preview…`,
8528
- );
8754
+ if (!completedCached) {
8755
+ panelChat?.updateLatestCodegenStatusMessage?.(
8756
+ completedPreviewBuildErr
8757
+ ? `Code saved${commitShort ? ` (${commitShort})` : ''} — building preview…`
8758
+ : `Code saved${commitShort ? ` (${commitShort})` : ''} — loading preview…`,
8759
+ );
8760
+ }
8529
8761
 
8530
8762
  // console.log('Widget Creator: Code generation operation completed for widget', generationWidgetId.substring(0, 8), ':', op);
8531
8763
 
@@ -8561,13 +8793,17 @@ Requirements:
8561
8793
  // Final chat message regardless of panel focus — without this, a
8562
8794
  // closed/unfocused panel never receives the terminal update and the
8563
8795
  // chat stays on the last progress line (e.g. stuck at 80%).
8796
+ // A cache-served completion gets the "already done" affordance:
8797
+ // it names the earlier change (and files) instead of claiming a
8798
+ // generation just happened.
8564
8799
  panelChat?.addAssistantMessage(
8565
- completedPreviewBuildErr
8566
- ? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
8567
- : `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
8800
+ cachedChatSummary ??
8801
+ (completedPreviewBuildErr
8802
+ ? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
8803
+ : `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`),
8568
8804
  true
8569
8805
  );
8570
- if (!completedPreviewBuildErr) {
8806
+ if (!completedPreviewBuildErr && !completedCached) {
8571
8807
  panelChat?.updateLatestCodegenStatusMessage?.(
8572
8808
  `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
8573
8809
  );
@@ -8978,11 +9214,17 @@ Requirements:
8978
9214
  publishingWidgetId = null;
8979
9215
  setCrossTabPublishing(false, capturedWidgetId);
8980
9216
 
8981
- const version = publishData?.data?.version || publishData?.version || 1;
8982
- const artifactUrl = publishData?.data?.artifactUrl || publishData?.artifactUrl || null;
9217
+ const publishEnvelope = publishData as unknown as {
9218
+ data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
9219
+ version?: number;
9220
+ artifactUrl?: string | null;
9221
+ commitSha?: string | null;
9222
+ };
9223
+ const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
9224
+ const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
8983
9225
  const commitSha =
8984
- publishData?.data?.commitSha
8985
- ?? publishData?.commitSha
9226
+ publishEnvelope?.data?.commitSha
9227
+ ?? publishEnvelope?.commitSha
8986
9228
  ?? commitShaForPublish
8987
9229
  ?? null;
8988
9230
  applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha);
@@ -9030,9 +9272,9 @@ Requirements:
9030
9272
 
9031
9273
 
9032
9274
  // Token expiration check interval
9033
- let tokenCheckInterval: number;
9275
+ let tokenCheckInterval: ReturnType<typeof setInterval>;
9034
9276
  let tokenCheckIntervalTime: number = 10; // seconds — check frequently so logout is prompt
9035
- let tokenDisplayInterval: number;
9277
+ let tokenDisplayInterval: ReturnType<typeof setInterval>;
9036
9278
 
9037
9279
  // Reactive variables for real-time token status
9038
9280
  let tokenTimeLeft: number = 0;
@@ -9115,6 +9357,9 @@ Requirements:
9115
9357
  // Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
9116
9358
  // component scope so `onDestroy` can remove the same reference.
9117
9359
  window.addEventListener('beforeunload', handleBeforeUnload);
9360
+ // Keep a component-scope binding so onDestroy can remove the listener —
9361
+ // the arrow itself is scoped to this onMount callback.
9362
+ handleBeforeUnloadRef = handleBeforeUnload;
9118
9363
 
9119
9364
  // Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
9120
9365
  // AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
@@ -9196,6 +9441,8 @@ Requirements:
9196
9441
  };
9197
9442
  });
9198
9443
 
9444
+ let handleBeforeUnloadRef: (() => void) | null = null;
9445
+
9199
9446
  // Clean up interval on component destroy
9200
9447
  onDestroy(() => {
9201
9448
  clearCanvasLoadFailsafe();
@@ -9208,7 +9455,7 @@ Requirements:
9208
9455
  }
9209
9456
  // Cross-tab cleanup: remove flags so other tabs don't stay blocked
9210
9457
  if (typeof window !== 'undefined') {
9211
- window.removeEventListener('beforeunload', handleBeforeUnload);
9458
+ window.removeEventListener('beforeunload', handleBeforeUnloadRef);
9212
9459
  window.removeEventListener('storage', handleCrossTabStorageEvent);
9213
9460
  window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
9214
9461
  window.removeEventListener('click', handleCanvasDropdownOutsideClick);
@@ -9399,6 +9646,19 @@ Requirements:
9399
9646
  return sanitizePreviewBuildError(raw);
9400
9647
  }
9401
9648
 
9649
+ /** True when the operation completed from the semantic cache (no new generation ran). */
9650
+ function getOperationCached(metadata?: Record<string, unknown>): boolean {
9651
+ return metadata?.cached === true;
9652
+ }
9653
+
9654
+ /** Changed-file paths reported by a semantic-cache-served operation. */
9655
+ function getOperationCachedFiles(metadata?: Record<string, unknown>): string[] {
9656
+ if (!metadata) return [];
9657
+ const raw = metadata.generatedFiles;
9658
+ if (!Array.isArray(raw)) return [];
9659
+ return raw.filter((p): p is string => typeof p === 'string');
9660
+ }
9661
+
9402
9662
  function getChatMethodsForWidget(widgetId: string | null | undefined) {
9403
9663
  if (!widgetId) return chatMethods ?? null;
9404
9664
  const instanceKey = panelInstanceKeyByWidgetId[widgetId] ?? widgetId;
@@ -9788,8 +10048,7 @@ Requirements:
9788
10048
  codeGenerationPrompt: storedPrompt,
9789
10049
  promptImages: [],
9790
10050
  lastCommitId: hydratedHead,
9791
- lastPublishedVersion: publishedVersion,
9792
- selectedWidgetJsPath: widget?.jsPath ?? null,
10051
+ lastPublishedVersion: publishedVersion, selectedWidgetJsPath: (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null,
9793
10052
  currentRepositoryId: normalizeRepositoryId(widget),
9794
10053
  widgetDetailsStep: resolveCreatorWorkflowStep(publishedVersion !== null ? 'edit' : 'create'),
9795
10054
  generateCodeStatus: null,
@@ -10047,7 +10306,7 @@ Requirements:
10047
10306
  const widget = getWidgetById(widgetId);
10048
10307
  const version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widget);
10049
10308
  if (version == null) return null;
10050
- const jsPath = ps.selectedWidgetJsPath ?? widget?.jsPath ?? null;
10309
+ const jsPath = ps.selectedWidgetJsPath ?? (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null;
10051
10310
  return resolvePublishedWidgetUrl(widgetId, version, jsPath);
10052
10311
  }
10053
10312
 
@@ -10071,6 +10330,8 @@ Requirements:
10071
10330
  // Historical previews are never interchangeable with the shared HEAD preview.
10072
10331
  let shapeVersionPreviewByWidgetId: Record<string, {
10073
10332
  versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
10333
+ /** Commit the selected version was built from — drives the chat's Head-at-commit badge. */
10334
+ commitSha?: string | null;
10074
10335
  errorDetails?: Awaited<ReturnType<typeof parseVersionPreviewError>> | null;
10075
10336
  }> = {};
10076
10337
  let shapeVersionPreviewRequestId = 0;
@@ -10393,7 +10654,10 @@ Requirements:
10393
10654
  }
10394
10655
  shapeVersionPreviewByWidgetId = {
10395
10656
  ...shapeVersionPreviewByWidgetId,
10396
- [widgetId]: { versionId, requestId, loading: false, error: null, url: previewUrl },
10657
+ [widgetId]: {
10658
+ versionId, requestId, loading: false, error: null, url: previewUrl,
10659
+ commitSha: normalizeCommitSha(commitSha),
10660
+ },
10397
10661
  };
10398
10662
  syncWidgetShapeLivePreviews();
10399
10663
  panelDisplayRevision += 1;
@@ -10432,6 +10696,14 @@ Requirements:
10432
10696
  if (source.startsWith('v:')) {
10433
10697
  const parsed = parseInt(source.slice(2), 10);
10434
10698
  version = Number.isFinite(parsed) ? parsed : null;
10699
+ // A draft version has no publish-history row to read a commit from, so
10700
+ // without the sha the version preview reported we fell back to repo HEAD
10701
+ // and the badge stayed on the newest turn instead of moving to the
10702
+ // selected version's own turn.
10703
+ const previewState = shapeVersionPreviewByWidgetId[widgetId];
10704
+ if (previewState?.versionId === source && previewState.commitSha) {
10705
+ return normalizeCommitSha(previewState.commitSha);
10706
+ }
10435
10707
  } else {
10436
10708
  version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
10437
10709
  }
@@ -10450,28 +10722,84 @@ Requirements:
10450
10722
  return normalizeCommitSha(ps.lastCommitId);
10451
10723
  }
10452
10724
 
10725
+ /**
10726
+ * Head commit per open panel, for the chat's Head-at-commit badge.
10727
+ *
10728
+ * The resolver reads the selected version and its preview state out of records
10729
+ * the compiler cannot follow through the call, so calling it straight from the
10730
+ * template never re-ran on a version switch: the chat kept the previous
10731
+ * commit and the badge stayed on the newest turn. Naming the records here is
10732
+ * what makes the switch reach the chat.
10733
+ */
10734
+ let headCommitShaByWidgetId: Record<string, string | null> = {};
10735
+ /**
10736
+ * Short commit sha → "Version N" per panel, so a chat turn reads as the version
10737
+ * it produced. A hash is meaningless to a non-developer, so it stays behind
10738
+ * debug mode and this label is what the turn shows.
10739
+ */
10740
+ let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10741
+ $: {
10742
+ void shapePreviewSourceByWidgetId;
10743
+ void shapeVersionPreviewByWidgetId;
10744
+ void panelDisplayRevision;
10745
+ const nextHeads: Record<string, string | null> = {};
10746
+ const nextLabels: Record<string, Record<string, string>> = {};
10747
+ for (const widgetId of openWidgetPanelIds) {
10748
+ nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
10749
+ nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
10750
+ }
10751
+ headCommitShaByWidgetId = nextHeads;
10752
+ versionLabelsByWidgetId = nextLabels;
10753
+ }
10754
+
10755
+ /** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
10756
+ function versionLabelKey(sha: string | null | undefined): string | null {
10757
+ const normalized = normalizeCommitSha(sha);
10758
+ return normalized ? normalized.slice(0, 7) : null;
10759
+ }
10760
+
10761
+ function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
10762
+ const labels: Record<string, string> = {};
10763
+ const ps = getPanelDisplayState(widgetId);
10764
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10765
+ 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}`;
10768
+ }
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
+ 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)}`;
10775
+ }
10776
+ return labels;
10777
+ }
10778
+
10453
10779
  function syncWidgetShapeLivePreviews(): void {
10454
- try {
10455
- canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
10456
- widgetShapeLivePreviewsRef?.sync?.();
10457
- // Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
10458
- const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
10459
- const liveWidgetIds = new Set<string>();
10460
- for (const rect of rects) {
10461
- if (!rect?.widgetId) continue;
10462
- if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
10463
- liveWidgetIds.add(rect.widgetId);
10464
- canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
10465
- }
10466
- const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
10467
- for (const widgetId of linkedIds) {
10468
- if (!liveWidgetIds.has(widgetId)) {
10469
- canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
10780
+ measureSync('sync-widget-shape-previews', () => {
10781
+ try {
10782
+ canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
10783
+ widgetShapeLivePreviewsRef?.sync?.();
10784
+ // Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
10785
+ const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
10786
+ const liveWidgetIds = new Set<string>();
10787
+ for (const rect of rects) {
10788
+ if (!rect?.widgetId) continue;
10789
+ if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
10790
+ liveWidgetIds.add(rect.widgetId);
10791
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
10470
10792
  }
10793
+ const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
10794
+ for (const widgetId of linkedIds) {
10795
+ if (!liveWidgetIds.has(widgetId)) {
10796
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
10797
+ }
10798
+ }
10799
+ } catch (err) {
10800
+ console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
10471
10801
  }
10472
- } catch (err) {
10473
- console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
10474
- }
10802
+ });
10475
10803
  }
10476
10804
 
10477
10805
  function getPanelEmbedPreviewUrl(widgetId: string): string | null {
@@ -11917,13 +12245,12 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
11917
12245
  });
11918
12246
  return;
11919
12247
  }
11920
- try {
11921
- const historyRes = await widgetsClient.getWidgetPublishHistory({ widgetId });
11922
- const history = Array.isArray(historyRes)
11923
- ? historyRes
11924
- : (Array.isArray((historyRes as { data?: unknown })?.data)
11925
- ? (historyRes as { data: unknown[] }).data
11926
- : []);
12248
+ try { const historyRes = await widgetsClient.getWidgetPublishHistory({ widgetId });
12249
+ const historyEnvelope = historyRes as unknown as { data?: unknown };
12250
+ // SDK types the envelope ({ success, data }); a response-transform middleware
12251
+ // may unwrap it, so a bare array stays tolerated.
12252
+ const historyData = Array.isArray(historyRes) ? historyRes : historyEnvelope?.data;
12253
+ const history = (Array.isArray(historyData) ? historyData : []) as PublishHistoryRecord[];
11927
12254
  patchPanelSnapshot(widgetId, {
11928
12255
  publishHistory: history,
11929
12256
  publishHistoryResolved: true,
@@ -12223,8 +12550,12 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12223
12550
  // console.log('Signup response:', signupResponse);
12224
12551
 
12225
12552
  // show error signup status
12226
- if (!signupResponse || !signupResponse.user) {
12227
- authStatus = signupResponse?.message ?? 'Signup failed.';
12553
+ // signupUser is typed Promise<void>; it resolves to undefined on the
12554
+ // success path, so a falsy response IS the success signal. Error details
12555
+ // arrive via the thrown ResponseError caught below.
12556
+ const signupEnvelope = signupResponse as unknown as { user?: unknown; message?: string } | undefined;
12557
+ if (!signupEnvelope || !signupEnvelope.user) {
12558
+ authStatus = signupEnvelope?.message ?? 'Signup failed.';
12228
12559
  return;
12229
12560
  }
12230
12561
 
@@ -12246,7 +12577,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12246
12577
  // console.log('Login response:', loginResponse);
12247
12578
 
12248
12579
  // show success login status
12249
- const payload = loginResponse?.data;
12580
+ // The generated loginUser return type is mis-derived (a billing endpoint's
12581
+ // shape); the real /auth/login payload is a session envelope. Cast honestly.
12582
+ const payload = (loginResponse as unknown as { data?: { accessToken?: string; user?: Record<string, unknown> } })?.data;
12250
12583
  if (payload?.accessToken) {
12251
12584
  currentUserToken = payload.accessToken;
12252
12585
 
@@ -12312,7 +12645,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12312
12645
  // console.log('WidgetCreator.updateTokenStatus: Decoded token:', decoded);
12313
12646
 
12314
12647
  // if the decoded token has an expiration field, update the token status
12315
- if (decoded && decoded.exp) {
12648
+ if (decoded && typeof decoded.exp === 'number') {
12316
12649
  const currentTime = Math.floor(Date.now() / 1000);
12317
12650
  const expUnix = decoded.exp;
12318
12651
  tokenTimeLeft = expUnix - currentTime;
@@ -12486,17 +12819,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12486
12819
  // console.log('Available fields:', Object.keys(decodedToken));
12487
12820
 
12488
12821
  // Extract user info from Supabase JWT structure
12822
+ // user_metadata is an untyped JWT claim blob — index it explicitly so the
12823
+ // accesses below are checked reads, not property probes on `unknown`.
12824
+ const userMetadata = (decodedToken.user_metadata ?? {}) as Record<string, unknown>;
12489
12825
  user = {
12490
12826
  id: decodedToken.sub || decodedToken.user_id || decodedToken.id || 'token-user',
12491
- email: decodedToken.email || decodedToken.user_metadata?.email || 'user@token-auth.com',
12827
+ email: userMetadata.email || 'user@token-auth.com',
12492
12828
  name:
12493
- decodedToken.user_metadata?.name ||
12494
- decodedToken.user_metadata?.full_name ||
12829
+ userMetadata.name ||
12830
+ userMetadata.full_name ||
12495
12831
  decodedToken.name ||
12496
12832
  decodedToken.full_name ||
12497
12833
  'Token User',
12498
12834
  role: decodedToken.role || 'user',
12499
- avatar: decodedToken.user_metadata?.avatar_url || decodedToken.user_metadata?.picture
12835
+ avatar: userMetadata.avatar_url || userMetadata.picture
12500
12836
  };
12501
12837
 
12502
12838
  // Show more detailed status with actual extracted info
@@ -12743,10 +13079,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12743
13079
  }
12744
13080
 
12745
13081
  const sourceHasCode =
12746
- !!normalizeRepositoryId(newWidget)
13082
+ !!normalizeRepositoryId(asWidgetSummary(newWidget))
12747
13083
  || !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
12748
13084
  ?? (newWidget as { js_path?: string | null }).js_path)
12749
- || resolvePublishedVersionFromWidget(newWidget) !== null
13085
+ || resolvePublishedVersionFromWidget(asWidgetSummary(newWidget)) !== null
12750
13086
  || !!sourceCommit;
12751
13087
 
12752
13088
  if (!canvasPrimaryWidgetId) {
@@ -12754,7 +13090,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12754
13090
  }
12755
13091
 
12756
13092
  // Duplicate stays a sibling on the canvas — do not make it the primary widget.
12757
- await selectWidget(newWidget, { skipPlaceShape: true });
13093
+ await selectWidget(asWidgetSummary(newWidget), { skipPlaceShape: true });
12758
13094
  notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
12759
13095
 
12760
13096
  showToast('success', 'Widget duplicated', {
@@ -12784,11 +13120,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12784
13120
  try {
12785
13121
  await handleWidgetDataChanged(newWidget.id);
12786
13122
  const freshWidget = getWidgetById(newWidget.id);
12787
- let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(newWidget);
13123
+ let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(asWidgetSummary(newWidget));
12788
13124
 
12789
13125
  if (!repoId) {
12790
13126
  // console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
12791
- repoId = await ensureWidgetRepository(freshWidget || newWidget, { silent: true });
13127
+ repoId = await ensureWidgetRepository(freshWidget || asWidgetSummary(newWidget), { silent: true });
12792
13128
  if (repoId) {
12793
13129
  widgets = widgets.map((w) => w.id === newWidget.id ? { ...w, repositoryId: repoId } : w);
12794
13130
  // console.log('[WidgetCreator] Repository created for duplicate:', repoId);
@@ -13249,14 +13585,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13249
13585
  }
13250
13586
 
13251
13587
  let objectCount = 0;
13252
- try {
13253
- const parsed = JSON.parse(json);
13254
- // saveCanvas() wraps as { version, savedAt, viewport, canvas: { objects: [...] } }
13255
- const objects = parsed?.canvas?.objects ?? parsed?.objects ?? [];
13256
- objectCount = Array.isArray(objects) ? objects.length : 0;
13257
- } catch {
13258
- // ignore parse errors for debug
13259
- }
13588
+ // Canvas JSON carries the shapes' embedded images, so it reaches megabytes on
13589
+ // a busy canvas and every parse of it blocks the main thread. Parse once and
13590
+ // reuse it for the API payload below.
13591
+ let parsedCanvas: any = null;
13592
+ measureSync('canvas-autosave:parse', () => {
13593
+ try {
13594
+ parsedCanvas = JSON.parse(json);
13595
+ // saveCanvas() wraps as { version, savedAt, viewport, canvas: { objects: [...] } }
13596
+ const objects = parsedCanvas?.canvas?.objects ?? parsedCanvas?.objects ?? [];
13597
+ objectCount = Array.isArray(objects) ? objects.length : 0;
13598
+ } catch {
13599
+ // ignore parse errors for debug
13600
+ }
13601
+ });
13260
13602
 
13261
13603
  if (isUnmountSave) {
13262
13604
  // console.log(`[CanvasDebug] Unmount save: objectCount=${objectCount}, lastSavedCount=${lastSavedCanvasObjectCount}`);
@@ -13289,11 +13631,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13289
13631
 
13290
13632
  // Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
13291
13633
  saveCanvasState(canvasId, json);
13292
- // Keep the legacy localStorage copy in sync for downgrade safety.
13293
- try {
13294
- localStorage.setItem(getCanvasStorageKey(), json);
13295
- } catch (err) {
13296
- console.warn('[WidgetCreator] Failed to save canvas to localStorage:', err);
13634
+ // Keep the legacy localStorage copy in sync for downgrade safety. `setItem` is
13635
+ // a synchronous disk write of the whole canvas, so during a generation it is
13636
+ // deferred to the end: the durable IndexedDB copy above is already written,
13637
+ // and the mirror exists only for a downgrade.
13638
+ if (!isAnyWidgetGenerating() || force || isUnmountSave) {
13639
+ measureSync('canvas-autosave:localStorage-mirror', () => {
13640
+ try {
13641
+ localStorage.setItem(getCanvasStorageKey(), json);
13642
+ } catch (err) {
13643
+ console.warn('[WidgetCreator] Failed to save canvas to localStorage:', err);
13644
+ }
13645
+ });
13297
13646
  }
13298
13647
 
13299
13648
  // Save to API via SDK with sequence race protection
@@ -13303,7 +13652,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13303
13652
  await canvasesClient.updateCanvas({
13304
13653
  id: canvasId,
13305
13654
  updateCanvasRequest: {
13306
- canvasContent: JSON.parse(json)
13655
+ canvasContent: parsedCanvas ?? JSON.parse(json)
13307
13656
  }
13308
13657
  });
13309
13658
  if (currentSeq < lastResolvedAutosaveSequence) {
@@ -13513,13 +13862,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13513
13862
  { headers: { 'Idempotency-Key': idempotencyKey } }
13514
13863
  );
13515
13864
 
13516
- if (!response || response.error || response.message) {
13865
+ const createEnvelope = response as unknown as { error?: string; message?: string };
13866
+ if (!response || createEnvelope.error || createEnvelope.message) {
13517
13867
  console.error('[WidgetCreator] Widget create on first send failed:', response);
13518
13868
  showToast('error', 'Failed to create widget from design');
13519
13869
  return null;
13520
13870
  }
13521
13871
 
13522
- const newWidget = response.data || response;
13872
+ const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response);
13523
13873
  if (!newWidget?.id) {
13524
13874
  showToast('warning', 'Widget created but no ID returned');
13525
13875
  return null;
@@ -13790,7 +14140,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13790
14140
  }
13791
14141
 
13792
14142
  async function ensureWidgetRepository(
13793
- widget: WidgetSummary,
14143
+ widget: WidgetSummary | null | undefined,
13794
14144
  options: { forceRecreate?: boolean; silent?: boolean } = {}
13795
14145
  ): Promise<string | null> {
13796
14146
  const { forceRecreate = false, silent = false } = options;
@@ -13915,8 +14265,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13915
14265
  });
13916
14266
  // console.log('Widget Creator: Repository creation response:', response);
13917
14267
 
13918
- const repoData = response?.data || response;
13919
- const repoId = repoData?.id || repoData?.repoId || repoData?.repo_id || null;
14268
+ const repoData = ((response as unknown as { data?: Record<string, unknown> }).data ?? response) as Record<string, unknown>;
14269
+ const repoId = (repoData?.id || repoData?.repoId || repoData?.repo_id || null) as string | null;
13920
14270
 
13921
14271
  if (!repoId) {
13922
14272
  showToast('error', 'Repository creation failed', {
@@ -14003,7 +14353,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14003
14353
  const response = await widgetsClient.deleteWidget(
14004
14354
  {
14005
14355
  id: adminDeleteWidget.id,
14006
- reason: 'admin_delete'
14356
+ deleteWidgetRequest: { reason: 'admin_delete' }
14007
14357
  },
14008
14358
  {
14009
14359
  headers: {
@@ -14012,10 +14362,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14012
14362
  }
14013
14363
  );
14014
14364
 
14015
- if (response?.error || response?.message) {
14365
+ // deleteWidget resolves void; only a thrown ResponseError (caught below)
14366
+ // carries a failure. The defensive envelope check is kept for the response-
14367
+ // transform middleware, but must go through an honest cast.
14368
+ const deleteEnvelope = response as unknown as { error?: string; message?: string } | undefined;
14369
+ if (deleteEnvelope && (deleteEnvelope.error || deleteEnvelope.message)) {
14016
14370
  console.error('Admin widget deletion failed:', response);
14017
14371
  showToast('error', 'Delete failed', {
14018
- description: response?.message || response?.error || 'Unknown error'
14372
+ description: deleteEnvelope?.message || deleteEnvelope?.error || 'Unknown error'
14019
14373
  });
14020
14374
  adminDeleteLoading = false;
14021
14375
  return;
@@ -15218,11 +15572,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15218
15572
  class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
15219
15573
  data-widget-chat-height={panelWidgetId}
15220
15574
  style={chatUserHeight == null
15221
- ? `flex: 1 1 auto; min-height: ${CHAT_MIN_HEIGHT}px;`
15575
+ ? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
15222
15576
  : `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
15223
15577
  >
15224
15578
  <Chat class="code-generation-chat w-full h-full rounded-none border-0"
15225
- style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
15579
+ style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
15226
15580
  resizeExpandsContainer={true}
15227
15581
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
15228
15582
  showLogs={debugMode}
@@ -15249,7 +15603,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15249
15603
  autoCodeDetection: false,
15250
15604
  }
15251
15605
  }}
15252
- widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
15606
+ widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15607
+ versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15253
15608
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15254
15609
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
15255
15610
  onUploadChatFile={handleChatUploadFile}
@@ -15461,7 +15816,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15461
15816
  {/if}
15462
15817
 
15463
15818
  <!-- ─── Schema loading / error / PropsEditor ─── -->
15464
- {@const panelSchemasLoading = (panelDisplayRevision, ps.compositionSchemasLoading)}
15819
+ {@const panelSchemasLoading = rev(panelDisplayRevision, ps.compositionSchemasLoading)}
15465
15820
  {@const panelSchemaLoadLabel = getPanelSchemaLoadLabel(panelWidgetId)}
15466
15821
  {#if panelSchemasLoading}
15467
15822
  <div class="composition-editor-loading flex flex-col items-center justify-center py-8">
@@ -15766,7 +16121,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15766
16121
  {/if}
15767
16122
 
15768
16123
  <!-- Delete Widget Shape Confirmation Dialog -->
15769
- <Dialog.Root class="confirm-delete-dialog-root" bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
16124
+ <!-- Dialog.Root (bits-ui AlertDialogRoot) exposes no class prop — styling hooks
16125
+ live on Content/Header below. -->
16126
+ <Dialog.Root bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
15770
16127
  <Dialog.Content class="confirm-delete-dialog-content max-w-md bg-white z-9999">
15771
16128
  <Dialog.Header class="confirm-delete-dialog-header">
15772
16129
  <Dialog.Title>Delete Widget Shape</Dialog.Title>
@@ -15808,7 +16165,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15808
16165
  </Dialog.Root>
15809
16166
 
15810
16167
  <!-- Delete Canvas Confirmation Dialog -->
15811
- <Dialog.Root class="confirm-delete-canvas-dialog-root" bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
16168
+ <Dialog.Root bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
15812
16169
  <Dialog.Content class="confirm-delete-canvas-dialog-content max-w-sm bg-white z-9999">
15813
16170
  <Dialog.Header class="confirm-delete-canvas-dialog-header">
15814
16171
  <Dialog.Title>Delete Canvas</Dialog.Title>
@@ -15868,7 +16225,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15868
16225
 
15869
16226
  <!-- Confirm Generation Dialog (shown on convert-to-widget before auto-generating) -->
15870
16227
  <Dialog.Root
15871
- class="confirm-generate-dialog-root"
15872
16228
  open={confirmGenerateDialogOpen}
15873
16229
  onOpenChange={(open) => {
15874
16230
  confirmGenerateDialogOpen = open;