@widgetic/creator 0.3.65 → 0.3.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,12 @@
27
27
  UploadsApi,
28
28
  ResponseError
29
29
  } from '@widgetic/api-sdk';
30
- import type { Middleware } from '@widgetic/api-sdk';
30
+ import type {
31
+ Middleware,
32
+ UpdateComposition,
33
+ GenerateCodeRequest,
34
+ AgentModelsGet200ResponseModelsInner
35
+ } from '@widgetic/api-sdk';
31
36
  import { parseVersionPreviewError } from './utils/versionPreviewError.js';
32
37
 
33
38
  /**
@@ -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';
@@ -635,10 +646,31 @@
635
646
  repositoryId?: string | null;
636
647
  repository_id?: string | null;
637
648
  repoId?: string | null;
649
+ repo_id?: string | null;
638
650
  name?: string;
639
651
  [extra: string]: unknown;
640
652
  }
641
653
 
654
+ /**
655
+ * View any API payload as the app's open WidgetSummary shape. The SDK's
656
+ * generated models (Widget, CreateWidget201Response, …) lack the index
657
+ * signature callers rely on for snake_case/extra fields, so direct
658
+ * assignments fail structurally; this documents the deliberate widening.
659
+ */
660
+ function asWidgetSummary(payload: unknown): WidgetSummary {
661
+ return (payload ?? {}) as WidgetSummary;
662
+ }
663
+
664
+ /**
665
+ * Reactivity marker for template {@const}s: a Svelte 4 {@const} only re-runs
666
+ * when a value referenced in its expression changes, so the panel-revision
667
+ * counter is passed explicitly. The bare comma idiom this replaces
668
+ * (`(revision, value)`) is a TS unused-expression error.
669
+ */
670
+ function rev<T>(revision: number, value: T, ..._alsoTracked: unknown[]): T {
671
+ return value;
672
+ }
673
+
642
674
  // LocalStorage keys for persisting selection across refresh
643
675
  const SELECTED_WIDGET_STORAGE_KEY = 'widgetCreator_selectedWidgetId';
644
676
  const SELECTED_CANVAS_STORAGE_KEY = 'widgetCreator_selectedCanvasId';
@@ -868,7 +900,7 @@
868
900
  // Chat component reference and context (focused panel)
869
901
  let chatMethods: {
870
902
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
871
- addAssistantMessage: (content: string, skipPersist?: boolean) => void;
903
+ addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: string[]) => void;
872
904
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
873
905
  getConversationId: () => string | null;
874
906
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -1004,6 +1036,7 @@
1004
1036
  fileSize?: number;
1005
1037
  }>,
1006
1038
  sourceWidgetId?: string,
1039
+ sendOptions?: { skipIntentGate?: boolean },
1007
1040
  ) {
1008
1041
  const widgetId = sourceWidgetId ?? selectedWidgetId;
1009
1042
  if (!widgetId) return;
@@ -1126,6 +1159,7 @@
1126
1159
  attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
1127
1160
  promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
1128
1161
  persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
1162
+ skipIntentGate: sendOptions?.skipIntentGate === true,
1129
1163
  });
1130
1164
  }
1131
1165
 
@@ -1490,10 +1524,12 @@
1490
1524
  : null) || userMessages[userMessages.length - 1] || null;
1491
1525
  const existingUrls = new Set(
1492
1526
  (targetUser?.attachments || [])
1493
- .map((a: { url?: string }) => a.url)
1494
- .filter((url: string | undefined): url is string => !!url),
1527
+ .map((a: { url?: unknown }) => a.url)
1528
+ .filter((url): url is string => typeof url === 'string' && url.length > 0),
1529
+ );
1530
+ const existingHasRemoteImage = [...existingUrls].some(
1531
+ (url) => typeof url === 'string' && /^https?:\/\//i.test(url)
1495
1532
  );
1496
- const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
1497
1533
  // Chat already persisted CDN attachments for this turn — do not merge data URLs/canvas shots.
1498
1534
  if (targetUser && existingHasRemoteImage) {
1499
1535
  if (convId !== visibleConversationId) {
@@ -1649,6 +1685,14 @@
1649
1685
  * Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
1650
1686
  */
1651
1687
  async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
1688
+ // Loading a widget in a hidden iframe costs a full Svelte + Tailwind CDN
1689
+ // startup on the main thread this page shares with it, and the capture that
1690
+ // follows is refused during a generation anyway (`requestIframeScreenshot`),
1691
+ // so the whole path is skipped rather than paying the load for nothing.
1692
+ if (isAnyWidgetGenerating()) {
1693
+ console.log('[Chat→Screenshot] Skip hidden Worker capture — codegen in progress', widgetId?.substring(0, 8));
1694
+ return null;
1695
+ }
1652
1696
  const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
1653
1697
  console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
1654
1698
  widgetId: widgetId.substring(0, 8),
@@ -1716,7 +1760,10 @@
1716
1760
  await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
1717
1761
  if (!isCurrent()) return;
1718
1762
  capturedCanvas?.updateWidgetImage(widgetId, screenshot);
1719
- const canvasJson = capturedCanvas?.saveCanvas?.();
1763
+ const canvasJson = measureSync(
1764
+ 'canvas-serialize:shape-refresh-thumbnail',
1765
+ () => capturedCanvas?.saveCanvas?.() ?? null,
1766
+ );
1720
1767
  if (canvasJson) void handleCanvasAutoSave(canvasJson);
1721
1768
  } catch (error) {
1722
1769
  console.warn('[WidgetShape] Refresh thumbnail capture failed', widgetId, error);
@@ -2479,8 +2526,14 @@
2479
2526
 
2480
2527
  /** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
2481
2528
  const CHAT_DEFAULT_HEIGHT = 400;
2482
- const CHAT_MIN_HEIGHT = 400;
2529
+ const CHAT_MIN_HEIGHT = 260;
2483
2530
  const CHAT_MAX_HEIGHT = 2000;
2531
+ /**
2532
+ * Floor applied to the chat box while it auto-fills the create step. A floor
2533
+ * taller than the available area makes the surrounding scroll container
2534
+ * scroll, which puts the chat input below the fold on open.
2535
+ */
2536
+ const CHAT_AUTO_MIN_HEIGHT = 0;
2484
2537
 
2485
2538
  function getChatContainerHeightForPanel(widgetId: string): number | null {
2486
2539
  const stored = getPanelState(widgetId).chatContainerHeight;
@@ -2565,7 +2618,7 @@
2565
2618
  if (!schema) return schema;
2566
2619
  try {
2567
2620
  const clone = structuredClone(schema);
2568
- const groups = Array.isArray(clone) ? clone : clone.componentGroups;
2621
+ const groups = Array.isArray(clone) ? clone : (clone as { componentGroups?: unknown }).componentGroups;
2569
2622
  if (!Array.isArray(groups) || groups.length === 0) return schema;
2570
2623
  const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
2571
2624
  (group.components || []).flatMap((component) => component.properties || [])
@@ -3657,7 +3710,7 @@
3657
3710
  !compositionRecordProjectId(selectedComposition as Record<string, unknown>);
3658
3711
  const res = await compositionsClient.updateComposition({
3659
3712
  id: selectedComposition.id,
3660
- updateCompositionRequest: {
3713
+ updateComposition: {
3661
3714
  content: buildCompositionContentPayload(),
3662
3715
  ...(needsProjectBackfill ? projectBackfill : {}),
3663
3716
  }
@@ -3749,7 +3802,7 @@
3749
3802
  try {
3750
3803
  await compositionsClient.updateComposition({
3751
3804
  id: compositionId,
3752
- updateCompositionRequest: { name: trimmed }
3805
+ updateComposition: { name: trimmed }
3753
3806
  });
3754
3807
  showToast('success', 'Composition renamed');
3755
3808
  await loadCompositions();
@@ -3810,7 +3863,9 @@
3810
3863
  };
3811
3864
  await compositionsClient.updateComposition({
3812
3865
  id: selectedComposition.id,
3813
- updateCompositionRequest: { embedSettings }
3866
+ // The gateway accepts embedSettings (compositionSchemas update schema);
3867
+ // the generated UpdateComposition model predates that field.
3868
+ updateComposition: { embedSettings } as unknown as UpdateComposition
3814
3869
  });
3815
3870
  selectedComposition = { ...selectedComposition, embedSettings };
3816
3871
  embedDimensionsChanged = false;
@@ -4006,11 +4061,11 @@
4006
4061
  // SDK returns { success, models } directly (not nested under data)
4007
4062
  if (response.success && Array.isArray(response.models)) {
4008
4063
  // 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,
4064
+ availableModels = response.models.map((m: AgentModelsGet200ResponseModelsInner) => ({
4065
+ id: m.id ?? '',
4066
+ name: m.displayName ?? '',
4067
+ provider: (m.provider ?? '').charAt(0).toUpperCase() + (m.provider ?? '').slice(1), // Capitalize provider
4068
+ description: m.description ?? '',
4014
4069
  supportsVision: m.supportsVision === true,
4015
4070
  }));
4016
4071
 
@@ -4750,7 +4805,12 @@
4750
4805
 
4751
4806
  // Step 3: Check operation status
4752
4807
  try {
4753
- const op = await operationsClient.operationsIdGet({ id: operationId });
4808
+ const op = (await operationsClient.operationsIdGet({ id: operationId })) as {
4809
+ status?: string;
4810
+ error?: string;
4811
+ progress?: number;
4812
+ metadata?: Record<string, unknown>;
4813
+ };
4754
4814
  const status = (op?.status || '').toLowerCase();
4755
4815
 
4756
4816
  if (status === 'completed') {
@@ -5698,22 +5758,29 @@
5698
5758
  const res = await widgetsClient.getWidgets({ canvasId: canvasId ?? undefined });
5699
5759
 
5700
5760
  // normalize error shape from apiRequest
5701
- if (res && (res.error || res.message)) {
5761
+ const envelope = res as unknown as {
5762
+ error?: string;
5763
+ message?: string;
5764
+ data?: unknown;
5765
+ };
5766
+ if (envelope && (envelope.error || envelope.message)) {
5702
5767
  // console.error('Widget Creator: loadWidgetsForCanvas: response not ok:', res);
5703
5768
  widgets = [];
5704
5769
  selectedWidgetId = null;
5705
5770
  isLoadingWidgets = false;
5706
- widgetsError = res.message || res.error || 'Failed to load widgets';
5771
+ widgetsError = envelope.message || envelope.error || 'Failed to load widgets';
5707
5772
  throw new Error(widgetsError);
5708
5773
  }
5709
5774
 
5710
- // if the response is ok, parse the data
5775
+ // if the response is ok, parse the data (bare list, {data: list} or {data: {data: list}})
5776
+ const envelopeData = envelope?.data;
5777
+ const envelopeNestedData = (envelopeData as { data?: unknown } | undefined)?.data;
5711
5778
  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[])
5779
+ ? (res as unknown as WidgetSummary[])
5780
+ : Array.isArray(envelopeNestedData)
5781
+ ? (envelopeNestedData as unknown as WidgetSummary[])
5782
+ : Array.isArray(envelopeData)
5783
+ ? (envelopeData as unknown as WidgetSummary[])
5717
5784
  : [];
5718
5785
 
5719
5786
  const previousById = new Map(
@@ -5810,9 +5877,13 @@
5810
5877
  }
5811
5878
 
5812
5879
  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();
5880
+ const rawCreated = widget.createdAt ?? widget.created_at;
5881
+ if (!rawCreated) return 0;
5882
+ const ms = new Date(
5883
+ typeof rawCreated === 'string' || typeof rawCreated === 'number' || rawCreated instanceof Date
5884
+ ? rawCreated
5885
+ : String(rawCreated)
5886
+ ).getTime();
5816
5887
  return Number.isFinite(ms) ? ms : 0;
5817
5888
  }
5818
5889
 
@@ -6218,10 +6289,10 @@
6218
6289
  async function resolvePrimaryWidgetIdForCanvas(targetCanvasId: string): Promise<string | null> {
6219
6290
  if (!canvasesClient || !targetCanvasId?.trim()) return null;
6220
6291
  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>);
6292
+ const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
6293
+ const record =
6294
+ (canvasRecord as { data?: Record<string, unknown> })?.data ??
6295
+ (canvasRecord as unknown as Record<string, unknown>);
6225
6296
  const raw =
6226
6297
  (record as { primaryWidgetId?: unknown })?.primaryWidgetId ??
6227
6298
  (record as { primary_widget_id?: unknown })?.primary_widget_id ??
@@ -6703,7 +6774,12 @@
6703
6774
  if (!widgetId || !agentClient) return null;
6704
6775
  try {
6705
6776
  const json = await agentClient.agentWidgetBuilderRepoHeadWidgetIdGet({ widgetId });
6706
- const data = json?.data ?? json;
6777
+ const repoHeadEnvelope = json as unknown as {
6778
+ data?: { repoHeadCommitSha?: string | null; repo_head_commit_sha?: string | null };
6779
+ repoHeadCommitSha?: string | null;
6780
+ repo_head_commit_sha?: string | null;
6781
+ };
6782
+ const data = repoHeadEnvelope?.data ?? repoHeadEnvelope;
6707
6783
  const sha = data?.repoHeadCommitSha ?? data?.repo_head_commit_sha ?? null;
6708
6784
  return typeof sha === 'string' && sha.length > 0 ? sha : null;
6709
6785
  } catch (err) {
@@ -7021,13 +7097,14 @@
7021
7097
  }
7022
7098
  }
7023
7099
  );
7024
- if (!response || response.error || response.message) {
7100
+ const createEnvelope = response as unknown as { error?: string; message?: string };
7101
+ if (!response || createEnvelope.error || createEnvelope.message) {
7025
7102
  console.error('Widget create failed:', response);
7026
- authStatus = `Create widget failed: ${response?.message || response?.error || 'Unknown error'}`;
7103
+ authStatus = `Create widget failed: ${createEnvelope.message || createEnvelope.error || 'Unknown error'}`;
7027
7104
  return;
7028
7105
  }
7029
7106
  // Ensure we have an id
7030
- const newWidget = response.data || response; // handle envelope or direct
7107
+ const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response); // handle envelope or direct
7031
7108
  if (newWidget?.id) {
7032
7109
  widgets = [{ ...newWidget }, ...widgets];
7033
7110
  selectWidget(newWidget);
@@ -7702,6 +7779,16 @@
7702
7779
  canvasShapePreviewSyncTimers.set(widgetId, timer);
7703
7780
  }
7704
7781
 
7782
+ /**
7783
+ * Serializing the canvas stringifies every shape's embedded image, so it is
7784
+ * megabytes of synchronous work on a busy canvas. The thumbnail paths run it
7785
+ * immediately after a capture, which is exactly where the freeze reports land,
7786
+ * so each call is measured and named.
7787
+ */
7788
+ function serializeCanvasJson(reason: string): string | null {
7789
+ return measureSync(`canvas-serialize:${reason}`, () => canvasRef?.saveCanvas?.() ?? null);
7790
+ }
7791
+
7705
7792
  async function handleRefreshScreenshot(widgetId: string, options: { quiet?: boolean } = {}) {
7706
7793
  const quiet = !!options.quiet;
7707
7794
  // console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
@@ -7757,7 +7844,7 @@
7757
7844
  const freshCapture = await handleChatScreenshot(widgetId, null, quiet);
7758
7845
  if (freshCapture) {
7759
7846
  canvasRef?.updateWidgetImage(widgetId, freshCapture);
7760
- const freshJson = canvasRef?.saveCanvas?.();
7847
+ const freshJson = serializeCanvasJson('quiet-shape-thumbnail');
7761
7848
  if (freshJson) void handleCanvasAutoSave(freshJson);
7762
7849
  void persistWidgetListThumbnail(widgetId, freshCapture);
7763
7850
  } else {
@@ -7816,7 +7903,7 @@
7816
7903
  if (result.data && canvasRef) {
7817
7904
  canvasRef.updateWidgetImage(widgetId, result.data);
7818
7905
  setTimeout(() => {
7819
- const json = canvasRef?.saveCanvas();
7906
+ const json = serializeCanvasJson('screenshot-refresh');
7820
7907
  if (json) handleCanvasAutoSave(json);
7821
7908
  }, 500);
7822
7909
  void persistWidgetListThumbnail(widgetId, result.data);
@@ -7928,6 +8015,8 @@ Requirements:
7928
8015
  attachmentIds?: string[];
7929
8016
  /** Image URLs / data URLs captured before chat clearDraft. */
7930
8017
  promptImagesOverride?: string[];
8018
+ /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8019
+ skipIntentGate?: boolean;
7931
8020
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
7932
8021
  persistFileAttachments?: Array<{
7933
8022
  url?: string;
@@ -8017,6 +8106,7 @@ Requirements:
8017
8106
  attachmentIds: options.attachmentIds,
8018
8107
  promptImagesOverride: options.promptImagesOverride,
8019
8108
  persistFileAttachments: options.persistFileAttachments,
8109
+ skipIntentGate: options.skipIntentGate,
8020
8110
  });
8021
8111
  }
8022
8112
 
@@ -8038,6 +8128,8 @@ Requirements:
8038
8128
  forceRecreateRepo?: boolean;
8039
8129
  attachmentIds?: string[];
8040
8130
  promptImagesOverride?: string[];
8131
+ /** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
8132
+ skipIntentGate?: boolean;
8041
8133
  persistFileAttachments?: Array<{
8042
8134
  url?: string;
8043
8135
  file?: File;
@@ -8274,17 +8366,67 @@ Requirements:
8274
8366
  modelId: coderModelForRequest || undefined,
8275
8367
  plannerModelId: plannerModelIdForRequest,
8276
8368
  widgetId: generationWidgetId,
8277
- conversationId: activeConversationId ?? undefined,
8278
- images: imagesToSend,
8369
+ conversationId: activeConversationId ?? undefined, images: imagesToSend,
8279
8370
  attachmentIds: attachmentIdsToSend,
8280
8371
  runtimeLogs: runtimeLogsForRequest,
8281
8372
  compileFixOnly: options.compileFixOnly === true,
8282
- }
8373
+ skipIntentGate: options.skipIntentGate === true,
8374
+ } as GenerateCodeRequest
8283
8375
  });
8284
8376
  // console.log('[WidgetCreator] Code generation request sent for widget', generationWidgetId.substring(0, 8), 'with model:', coderModelForRequest, 'planner:', plannerModelIdForRequest || 'none', 'conversationId:', activeConversationId || 'none', 'images:', imagesToSend?.length || 0, 'attachmentIds:', attachmentIdsToSend?.length || 0, 'runtimeLogs:', runtimeLogsForRequest?.length ?? 0);
8285
8377
  // console.log('Widget Creator: Code generation response:', response);
8286
8378
 
8287
- if (response && response.error === 'TOKEN_EXPIRED') {
8379
+ // --- Intent gate: the server answered instead of generating ---
8380
+ // A question got a meta answer, an ambiguous ask got a clarification
8381
+ // card, or a publish/settings ask was redirected. None of these spent
8382
+ // a generation slot. Anything unexpected here falls through to the
8383
+ // normal path (operation tracking), never blocks a real build.
8384
+ const gatedData = (response as any)?.data;
8385
+ if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
8386
+ const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
8387
+ const gatedReply: string = gatedData.reply ?? '';
8388
+ const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
8389
+
8390
+ clearActiveOperationId();
8391
+ patchCodegenUiState(generationWidgetId, {
8392
+ lastGenerationFailed: false,
8393
+ lastGenerationError: null,
8394
+ generateCodeStatus:
8395
+ gatedAction === 'answer'
8396
+ ? 'Answered from widget details — no generation needed.'
8397
+ : gatedAction === 'clarify'
8398
+ ? 'Need a bit more detail before generating.'
8399
+ : 'No generation needed — use the publish or settings control.',
8400
+ });
8401
+
8402
+ const chatMethods = getChatMethodsForWidget(generationWidgetId);
8403
+ if (gatedAction === 'clarify' && suggestions.length > 0 && chatMethods?.addAssistantMessage) {
8404
+ // Suggestions travel as message data, not flattened text — the chat
8405
+ // renders them as clickable chips that send the next user message.
8406
+ chatMethods.addAssistantMessage(gatedReply, false, suggestions);
8407
+ } else if (chatMethods?.addAssistantMessage) {
8408
+ chatMethods.addAssistantMessage(gatedReply);
8409
+ }
8410
+
8411
+ showToast(
8412
+ gatedAction === 'answer' ? 'info' : 'warning',
8413
+ gatedAction === 'answer' ? 'Answered without generating' : 'No generation started',
8414
+ {
8415
+ description: gatedReply.slice(0, 140),
8416
+ duration: 8000,
8417
+ },
8418
+ );
8419
+ generatingWidgetIds = (() => {
8420
+ const { [generationWidgetId]: _gated, ...rest } = generatingWidgetIds;
8421
+ return rest;
8422
+ })();
8423
+ isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
8424
+ setCrossTabGenerating(false, generationWidgetId);
8425
+ widgetAbortControllers.delete(generationWidgetId);
8426
+ return;
8427
+ }
8428
+
8429
+ if (response && (response as { error?: string }).error === 'TOKEN_EXPIRED') {
8288
8430
  patchCodegenUiState(generationWidgetId, {
8289
8431
  generateCodeStatus: '❌ Authentication expired. Please login again.',
8290
8432
  lastGenerationFailed: true,
@@ -8297,7 +8439,8 @@ Requirements:
8297
8439
  return;
8298
8440
  }
8299
8441
 
8300
- const generateErrorMessage = response && (response.error || response.message);
8442
+ const generateEnvelope = response as unknown as { error?: string; message?: string };
8443
+ const generateErrorMessage = generateEnvelope && (generateEnvelope.error || generateEnvelope.message);
8301
8444
  if (generateErrorMessage) {
8302
8445
  const normalizedMessage = normalizeErrorMessage(generateErrorMessage);
8303
8446
  patchCodegenUiState(generationWidgetId, {
@@ -8313,8 +8456,10 @@ Requirements:
8313
8456
  return;
8314
8457
  }
8315
8458
 
8316
- const responseData = response?.data || response;
8317
- const operationId = responseData?.operationId;
8459
+ const generateData = ((response as { data?: unknown }).data ?? response) as {
8460
+ operationId?: string;
8461
+ };
8462
+ const operationId = generateData?.operationId;
8318
8463
 
8319
8464
  // The Worker serves rebuilt code at the same preview URL, so the panel
8320
8465
  // must know when the current generation started — hasLiveWorkerPreview()
@@ -8397,6 +8542,16 @@ Requirements:
8397
8542
 
8398
8543
  const completedCommitSha = getOperationCommitId(op.metadata);
8399
8544
  const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
8545
+ const completedCached = getOperationCached(op.metadata);
8546
+ const completedCachedFiles = getOperationCachedFiles(op.metadata);
8547
+ const cachedChatSummary = completedCached
8548
+ ? `Already done — the widget is in the state you asked for.` +
8549
+ (completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : '') +
8550
+ '\nThat change was made earlier in this conversation, so no new generation was needed' +
8551
+ (completedCachedFiles.length > 0
8552
+ ? ':\n' + completedCachedFiles.map((p) => `- ${p}`).join('\n')
8553
+ : '.')
8554
+ : null;
8400
8555
  const generationPanelRef = getWidgetDetailsRef(generationWidgetId);
8401
8556
  const isFocusedPanel = generationWidgetId === focusedPanelWidgetId;
8402
8557
  const completedWidgetName = getWidgetById(generationWidgetId)?.name || 'widget';
@@ -8521,11 +8676,13 @@ Requirements:
8521
8676
  });
8522
8677
 
8523
8678
  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
- );
8679
+ if (!completedCached) {
8680
+ panelChat?.updateLatestCodegenStatusMessage?.(
8681
+ completedPreviewBuildErr
8682
+ ? `Code saved${commitShort ? ` (${commitShort})` : ''} — building preview…`
8683
+ : `Code saved${commitShort ? ` (${commitShort})` : ''} — loading preview…`,
8684
+ );
8685
+ }
8529
8686
 
8530
8687
  // console.log('Widget Creator: Code generation operation completed for widget', generationWidgetId.substring(0, 8), ':', op);
8531
8688
 
@@ -8561,13 +8718,17 @@ Requirements:
8561
8718
  // Final chat message regardless of panel focus — without this, a
8562
8719
  // closed/unfocused panel never receives the terminal update and the
8563
8720
  // chat stays on the last progress line (e.g. stuck at 80%).
8721
+ // A cache-served completion gets the "already done" affordance:
8722
+ // it names the earlier change (and files) instead of claiming a
8723
+ // generation just happened.
8564
8724
  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…`,
8725
+ cachedChatSummary ??
8726
+ (completedPreviewBuildErr
8727
+ ? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
8728
+ : `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`),
8568
8729
  true
8569
8730
  );
8570
- if (!completedPreviewBuildErr) {
8731
+ if (!completedPreviewBuildErr && !completedCached) {
8571
8732
  panelChat?.updateLatestCodegenStatusMessage?.(
8572
8733
  `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
8573
8734
  );
@@ -8978,11 +9139,17 @@ Requirements:
8978
9139
  publishingWidgetId = null;
8979
9140
  setCrossTabPublishing(false, capturedWidgetId);
8980
9141
 
8981
- const version = publishData?.data?.version || publishData?.version || 1;
8982
- const artifactUrl = publishData?.data?.artifactUrl || publishData?.artifactUrl || null;
9142
+ const publishEnvelope = publishData as unknown as {
9143
+ data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
9144
+ version?: number;
9145
+ artifactUrl?: string | null;
9146
+ commitSha?: string | null;
9147
+ };
9148
+ const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
9149
+ const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
8983
9150
  const commitSha =
8984
- publishData?.data?.commitSha
8985
- ?? publishData?.commitSha
9151
+ publishEnvelope?.data?.commitSha
9152
+ ?? publishEnvelope?.commitSha
8986
9153
  ?? commitShaForPublish
8987
9154
  ?? null;
8988
9155
  applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha);
@@ -9030,9 +9197,9 @@ Requirements:
9030
9197
 
9031
9198
 
9032
9199
  // Token expiration check interval
9033
- let tokenCheckInterval: number;
9200
+ let tokenCheckInterval: ReturnType<typeof setInterval>;
9034
9201
  let tokenCheckIntervalTime: number = 10; // seconds — check frequently so logout is prompt
9035
- let tokenDisplayInterval: number;
9202
+ let tokenDisplayInterval: ReturnType<typeof setInterval>;
9036
9203
 
9037
9204
  // Reactive variables for real-time token status
9038
9205
  let tokenTimeLeft: number = 0;
@@ -9115,6 +9282,9 @@ Requirements:
9115
9282
  // Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
9116
9283
  // component scope so `onDestroy` can remove the same reference.
9117
9284
  window.addEventListener('beforeunload', handleBeforeUnload);
9285
+ // Keep a component-scope binding so onDestroy can remove the listener —
9286
+ // the arrow itself is scoped to this onMount callback.
9287
+ handleBeforeUnloadRef = handleBeforeUnload;
9118
9288
 
9119
9289
  // Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
9120
9290
  // AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
@@ -9196,6 +9366,8 @@ Requirements:
9196
9366
  };
9197
9367
  });
9198
9368
 
9369
+ let handleBeforeUnloadRef: (() => void) | null = null;
9370
+
9199
9371
  // Clean up interval on component destroy
9200
9372
  onDestroy(() => {
9201
9373
  clearCanvasLoadFailsafe();
@@ -9208,7 +9380,7 @@ Requirements:
9208
9380
  }
9209
9381
  // Cross-tab cleanup: remove flags so other tabs don't stay blocked
9210
9382
  if (typeof window !== 'undefined') {
9211
- window.removeEventListener('beforeunload', handleBeforeUnload);
9383
+ window.removeEventListener('beforeunload', handleBeforeUnloadRef);
9212
9384
  window.removeEventListener('storage', handleCrossTabStorageEvent);
9213
9385
  window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
9214
9386
  window.removeEventListener('click', handleCanvasDropdownOutsideClick);
@@ -9399,6 +9571,19 @@ Requirements:
9399
9571
  return sanitizePreviewBuildError(raw);
9400
9572
  }
9401
9573
 
9574
+ /** True when the operation completed from the semantic cache (no new generation ran). */
9575
+ function getOperationCached(metadata?: Record<string, unknown>): boolean {
9576
+ return metadata?.cached === true;
9577
+ }
9578
+
9579
+ /** Changed-file paths reported by a semantic-cache-served operation. */
9580
+ function getOperationCachedFiles(metadata?: Record<string, unknown>): string[] {
9581
+ if (!metadata) return [];
9582
+ const raw = metadata.generatedFiles;
9583
+ if (!Array.isArray(raw)) return [];
9584
+ return raw.filter((p): p is string => typeof p === 'string');
9585
+ }
9586
+
9402
9587
  function getChatMethodsForWidget(widgetId: string | null | undefined) {
9403
9588
  if (!widgetId) return chatMethods ?? null;
9404
9589
  const instanceKey = panelInstanceKeyByWidgetId[widgetId] ?? widgetId;
@@ -9788,8 +9973,7 @@ Requirements:
9788
9973
  codeGenerationPrompt: storedPrompt,
9789
9974
  promptImages: [],
9790
9975
  lastCommitId: hydratedHead,
9791
- lastPublishedVersion: publishedVersion,
9792
- selectedWidgetJsPath: widget?.jsPath ?? null,
9976
+ lastPublishedVersion: publishedVersion, selectedWidgetJsPath: (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null,
9793
9977
  currentRepositoryId: normalizeRepositoryId(widget),
9794
9978
  widgetDetailsStep: resolveCreatorWorkflowStep(publishedVersion !== null ? 'edit' : 'create'),
9795
9979
  generateCodeStatus: null,
@@ -10047,7 +10231,7 @@ Requirements:
10047
10231
  const widget = getWidgetById(widgetId);
10048
10232
  const version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widget);
10049
10233
  if (version == null) return null;
10050
- const jsPath = ps.selectedWidgetJsPath ?? widget?.jsPath ?? null;
10234
+ const jsPath = ps.selectedWidgetJsPath ?? (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null;
10051
10235
  return resolvePublishedWidgetUrl(widgetId, version, jsPath);
10052
10236
  }
10053
10237
 
@@ -10071,6 +10255,8 @@ Requirements:
10071
10255
  // Historical previews are never interchangeable with the shared HEAD preview.
10072
10256
  let shapeVersionPreviewByWidgetId: Record<string, {
10073
10257
  versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
10258
+ /** Commit the selected version was built from — drives the chat's Head-at-commit badge. */
10259
+ commitSha?: string | null;
10074
10260
  errorDetails?: Awaited<ReturnType<typeof parseVersionPreviewError>> | null;
10075
10261
  }> = {};
10076
10262
  let shapeVersionPreviewRequestId = 0;
@@ -10393,7 +10579,10 @@ Requirements:
10393
10579
  }
10394
10580
  shapeVersionPreviewByWidgetId = {
10395
10581
  ...shapeVersionPreviewByWidgetId,
10396
- [widgetId]: { versionId, requestId, loading: false, error: null, url: previewUrl },
10582
+ [widgetId]: {
10583
+ versionId, requestId, loading: false, error: null, url: previewUrl,
10584
+ commitSha: normalizeCommitSha(commitSha),
10585
+ },
10397
10586
  };
10398
10587
  syncWidgetShapeLivePreviews();
10399
10588
  panelDisplayRevision += 1;
@@ -10432,6 +10621,14 @@ Requirements:
10432
10621
  if (source.startsWith('v:')) {
10433
10622
  const parsed = parseInt(source.slice(2), 10);
10434
10623
  version = Number.isFinite(parsed) ? parsed : null;
10624
+ // A draft version has no publish-history row to read a commit from, so
10625
+ // without the sha the version preview reported we fell back to repo HEAD
10626
+ // and the badge stayed on the newest turn instead of moving to the
10627
+ // selected version's own turn.
10628
+ const previewState = shapeVersionPreviewByWidgetId[widgetId];
10629
+ if (previewState?.versionId === source && previewState.commitSha) {
10630
+ return normalizeCommitSha(previewState.commitSha);
10631
+ }
10435
10632
  } else {
10436
10633
  version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
10437
10634
  }
@@ -10450,28 +10647,84 @@ Requirements:
10450
10647
  return normalizeCommitSha(ps.lastCommitId);
10451
10648
  }
10452
10649
 
10650
+ /**
10651
+ * Head commit per open panel, for the chat's Head-at-commit badge.
10652
+ *
10653
+ * The resolver reads the selected version and its preview state out of records
10654
+ * the compiler cannot follow through the call, so calling it straight from the
10655
+ * template never re-ran on a version switch: the chat kept the previous
10656
+ * commit and the badge stayed on the newest turn. Naming the records here is
10657
+ * what makes the switch reach the chat.
10658
+ */
10659
+ let headCommitShaByWidgetId: Record<string, string | null> = {};
10660
+ /**
10661
+ * Short commit sha → "Version N" per panel, so a chat turn reads as the version
10662
+ * it produced. A hash is meaningless to a non-developer, so it stays behind
10663
+ * debug mode and this label is what the turn shows.
10664
+ */
10665
+ let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10666
+ $: {
10667
+ void shapePreviewSourceByWidgetId;
10668
+ void shapeVersionPreviewByWidgetId;
10669
+ void panelDisplayRevision;
10670
+ const nextHeads: Record<string, string | null> = {};
10671
+ const nextLabels: Record<string, Record<string, string>> = {};
10672
+ for (const widgetId of openWidgetPanelIds) {
10673
+ nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
10674
+ nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
10675
+ }
10676
+ headCommitShaByWidgetId = nextHeads;
10677
+ versionLabelsByWidgetId = nextLabels;
10678
+ }
10679
+
10680
+ /** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
10681
+ function versionLabelKey(sha: string | null | undefined): string | null {
10682
+ const normalized = normalizeCommitSha(sha);
10683
+ return normalized ? normalized.slice(0, 7) : null;
10684
+ }
10685
+
10686
+ function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
10687
+ const labels: Record<string, string> = {};
10688
+ const ps = getPanelDisplayState(widgetId);
10689
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
10690
+ for (const record of history || []) {
10691
+ const key = versionLabelKey(record?.commitSha ?? record?.commit_sha);
10692
+ if (key && record?.version != null) labels[key] = `Version ${record.version}`;
10693
+ }
10694
+ // The version the user is previewing right now may have been fetched rather
10695
+ // than read from history, and it is the one whose label matters most.
10696
+ const previewState = shapeVersionPreviewByWidgetId[widgetId];
10697
+ const previewKey = versionLabelKey(previewState?.commitSha);
10698
+ if (previewKey && previewState?.versionId?.startsWith('v:')) {
10699
+ labels[previewKey] = `Version ${previewState.versionId.slice(2)}`;
10700
+ }
10701
+ return labels;
10702
+ }
10703
+
10453
10704
  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);
10705
+ measureSync('sync-widget-shape-previews', () => {
10706
+ try {
10707
+ canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
10708
+ widgetShapeLivePreviewsRef?.sync?.();
10709
+ // Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
10710
+ const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
10711
+ const liveWidgetIds = new Set<string>();
10712
+ for (const rect of rects) {
10713
+ if (!rect?.widgetId) continue;
10714
+ if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
10715
+ liveWidgetIds.add(rect.widgetId);
10716
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
10717
+ }
10718
+ const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
10719
+ for (const widgetId of linkedIds) {
10720
+ if (!liveWidgetIds.has(widgetId)) {
10721
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
10722
+ }
10470
10723
  }
10724
+ } catch (err) {
10725
+ console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
10471
10726
  }
10472
- } catch (err) {
10473
- console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
10474
- }
10727
+ });
10475
10728
  }
10476
10729
 
10477
10730
  function getPanelEmbedPreviewUrl(widgetId: string): string | null {
@@ -11917,13 +12170,12 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
11917
12170
  });
11918
12171
  return;
11919
12172
  }
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
- : []);
12173
+ try { const historyRes = await widgetsClient.getWidgetPublishHistory({ widgetId });
12174
+ const historyEnvelope = historyRes as unknown as { data?: unknown };
12175
+ // SDK types the envelope ({ success, data }); a response-transform middleware
12176
+ // may unwrap it, so a bare array stays tolerated.
12177
+ const historyData = Array.isArray(historyRes) ? historyRes : historyEnvelope?.data;
12178
+ const history = (Array.isArray(historyData) ? historyData : []) as PublishHistoryRecord[];
11927
12179
  patchPanelSnapshot(widgetId, {
11928
12180
  publishHistory: history,
11929
12181
  publishHistoryResolved: true,
@@ -12223,8 +12475,12 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12223
12475
  // console.log('Signup response:', signupResponse);
12224
12476
 
12225
12477
  // show error signup status
12226
- if (!signupResponse || !signupResponse.user) {
12227
- authStatus = signupResponse?.message ?? 'Signup failed.';
12478
+ // signupUser is typed Promise<void>; it resolves to undefined on the
12479
+ // success path, so a falsy response IS the success signal. Error details
12480
+ // arrive via the thrown ResponseError caught below.
12481
+ const signupEnvelope = signupResponse as unknown as { user?: unknown; message?: string } | undefined;
12482
+ if (!signupEnvelope || !signupEnvelope.user) {
12483
+ authStatus = signupEnvelope?.message ?? 'Signup failed.';
12228
12484
  return;
12229
12485
  }
12230
12486
 
@@ -12246,7 +12502,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12246
12502
  // console.log('Login response:', loginResponse);
12247
12503
 
12248
12504
  // show success login status
12249
- const payload = loginResponse?.data;
12505
+ // The generated loginUser return type is mis-derived (a billing endpoint's
12506
+ // shape); the real /auth/login payload is a session envelope. Cast honestly.
12507
+ const payload = (loginResponse as unknown as { data?: { accessToken?: string; user?: Record<string, unknown> } })?.data;
12250
12508
  if (payload?.accessToken) {
12251
12509
  currentUserToken = payload.accessToken;
12252
12510
 
@@ -12312,7 +12570,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12312
12570
  // console.log('WidgetCreator.updateTokenStatus: Decoded token:', decoded);
12313
12571
 
12314
12572
  // if the decoded token has an expiration field, update the token status
12315
- if (decoded && decoded.exp) {
12573
+ if (decoded && typeof decoded.exp === 'number') {
12316
12574
  const currentTime = Math.floor(Date.now() / 1000);
12317
12575
  const expUnix = decoded.exp;
12318
12576
  tokenTimeLeft = expUnix - currentTime;
@@ -12486,17 +12744,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12486
12744
  // console.log('Available fields:', Object.keys(decodedToken));
12487
12745
 
12488
12746
  // Extract user info from Supabase JWT structure
12747
+ // user_metadata is an untyped JWT claim blob — index it explicitly so the
12748
+ // accesses below are checked reads, not property probes on `unknown`.
12749
+ const userMetadata = (decodedToken.user_metadata ?? {}) as Record<string, unknown>;
12489
12750
  user = {
12490
12751
  id: decodedToken.sub || decodedToken.user_id || decodedToken.id || 'token-user',
12491
- email: decodedToken.email || decodedToken.user_metadata?.email || 'user@token-auth.com',
12752
+ email: userMetadata.email || 'user@token-auth.com',
12492
12753
  name:
12493
- decodedToken.user_metadata?.name ||
12494
- decodedToken.user_metadata?.full_name ||
12754
+ userMetadata.name ||
12755
+ userMetadata.full_name ||
12495
12756
  decodedToken.name ||
12496
12757
  decodedToken.full_name ||
12497
12758
  'Token User',
12498
12759
  role: decodedToken.role || 'user',
12499
- avatar: decodedToken.user_metadata?.avatar_url || decodedToken.user_metadata?.picture
12760
+ avatar: userMetadata.avatar_url || userMetadata.picture
12500
12761
  };
12501
12762
 
12502
12763
  // Show more detailed status with actual extracted info
@@ -12743,10 +13004,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12743
13004
  }
12744
13005
 
12745
13006
  const sourceHasCode =
12746
- !!normalizeRepositoryId(newWidget)
13007
+ !!normalizeRepositoryId(asWidgetSummary(newWidget))
12747
13008
  || !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
12748
13009
  ?? (newWidget as { js_path?: string | null }).js_path)
12749
- || resolvePublishedVersionFromWidget(newWidget) !== null
13010
+ || resolvePublishedVersionFromWidget(asWidgetSummary(newWidget)) !== null
12750
13011
  || !!sourceCommit;
12751
13012
 
12752
13013
  if (!canvasPrimaryWidgetId) {
@@ -12754,7 +13015,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12754
13015
  }
12755
13016
 
12756
13017
  // Duplicate stays a sibling on the canvas — do not make it the primary widget.
12757
- await selectWidget(newWidget, { skipPlaceShape: true });
13018
+ await selectWidget(asWidgetSummary(newWidget), { skipPlaceShape: true });
12758
13019
  notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
12759
13020
 
12760
13021
  showToast('success', 'Widget duplicated', {
@@ -12784,11 +13045,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12784
13045
  try {
12785
13046
  await handleWidgetDataChanged(newWidget.id);
12786
13047
  const freshWidget = getWidgetById(newWidget.id);
12787
- let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(newWidget);
13048
+ let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(asWidgetSummary(newWidget));
12788
13049
 
12789
13050
  if (!repoId) {
12790
13051
  // console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
12791
- repoId = await ensureWidgetRepository(freshWidget || newWidget, { silent: true });
13052
+ repoId = await ensureWidgetRepository(freshWidget || asWidgetSummary(newWidget), { silent: true });
12792
13053
  if (repoId) {
12793
13054
  widgets = widgets.map((w) => w.id === newWidget.id ? { ...w, repositoryId: repoId } : w);
12794
13055
  // console.log('[WidgetCreator] Repository created for duplicate:', repoId);
@@ -13249,14 +13510,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13249
13510
  }
13250
13511
 
13251
13512
  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
- }
13513
+ // Canvas JSON carries the shapes' embedded images, so it reaches megabytes on
13514
+ // a busy canvas and every parse of it blocks the main thread. Parse once and
13515
+ // reuse it for the API payload below.
13516
+ let parsedCanvas: any = null;
13517
+ measureSync('canvas-autosave:parse', () => {
13518
+ try {
13519
+ parsedCanvas = JSON.parse(json);
13520
+ // saveCanvas() wraps as { version, savedAt, viewport, canvas: { objects: [...] } }
13521
+ const objects = parsedCanvas?.canvas?.objects ?? parsedCanvas?.objects ?? [];
13522
+ objectCount = Array.isArray(objects) ? objects.length : 0;
13523
+ } catch {
13524
+ // ignore parse errors for debug
13525
+ }
13526
+ });
13260
13527
 
13261
13528
  if (isUnmountSave) {
13262
13529
  // console.log(`[CanvasDebug] Unmount save: objectCount=${objectCount}, lastSavedCount=${lastSavedCanvasObjectCount}`);
@@ -13289,11 +13556,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13289
13556
 
13290
13557
  // Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
13291
13558
  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);
13559
+ // Keep the legacy localStorage copy in sync for downgrade safety. `setItem` is
13560
+ // a synchronous disk write of the whole canvas, so during a generation it is
13561
+ // deferred to the end: the durable IndexedDB copy above is already written,
13562
+ // and the mirror exists only for a downgrade.
13563
+ if (!isAnyWidgetGenerating() || force || isUnmountSave) {
13564
+ measureSync('canvas-autosave:localStorage-mirror', () => {
13565
+ try {
13566
+ localStorage.setItem(getCanvasStorageKey(), json);
13567
+ } catch (err) {
13568
+ console.warn('[WidgetCreator] Failed to save canvas to localStorage:', err);
13569
+ }
13570
+ });
13297
13571
  }
13298
13572
 
13299
13573
  // Save to API via SDK with sequence race protection
@@ -13303,7 +13577,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13303
13577
  await canvasesClient.updateCanvas({
13304
13578
  id: canvasId,
13305
13579
  updateCanvasRequest: {
13306
- canvasContent: JSON.parse(json)
13580
+ canvasContent: parsedCanvas ?? JSON.parse(json)
13307
13581
  }
13308
13582
  });
13309
13583
  if (currentSeq < lastResolvedAutosaveSequence) {
@@ -13513,13 +13787,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13513
13787
  { headers: { 'Idempotency-Key': idempotencyKey } }
13514
13788
  );
13515
13789
 
13516
- if (!response || response.error || response.message) {
13790
+ const createEnvelope = response as unknown as { error?: string; message?: string };
13791
+ if (!response || createEnvelope.error || createEnvelope.message) {
13517
13792
  console.error('[WidgetCreator] Widget create on first send failed:', response);
13518
13793
  showToast('error', 'Failed to create widget from design');
13519
13794
  return null;
13520
13795
  }
13521
13796
 
13522
- const newWidget = response.data || response;
13797
+ const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response);
13523
13798
  if (!newWidget?.id) {
13524
13799
  showToast('warning', 'Widget created but no ID returned');
13525
13800
  return null;
@@ -13790,7 +14065,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13790
14065
  }
13791
14066
 
13792
14067
  async function ensureWidgetRepository(
13793
- widget: WidgetSummary,
14068
+ widget: WidgetSummary | null | undefined,
13794
14069
  options: { forceRecreate?: boolean; silent?: boolean } = {}
13795
14070
  ): Promise<string | null> {
13796
14071
  const { forceRecreate = false, silent = false } = options;
@@ -13915,8 +14190,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13915
14190
  });
13916
14191
  // console.log('Widget Creator: Repository creation response:', response);
13917
14192
 
13918
- const repoData = response?.data || response;
13919
- const repoId = repoData?.id || repoData?.repoId || repoData?.repo_id || null;
14193
+ const repoData = ((response as unknown as { data?: Record<string, unknown> }).data ?? response) as Record<string, unknown>;
14194
+ const repoId = (repoData?.id || repoData?.repoId || repoData?.repo_id || null) as string | null;
13920
14195
 
13921
14196
  if (!repoId) {
13922
14197
  showToast('error', 'Repository creation failed', {
@@ -14003,7 +14278,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14003
14278
  const response = await widgetsClient.deleteWidget(
14004
14279
  {
14005
14280
  id: adminDeleteWidget.id,
14006
- reason: 'admin_delete'
14281
+ deleteWidgetRequest: { reason: 'admin_delete' }
14007
14282
  },
14008
14283
  {
14009
14284
  headers: {
@@ -14012,10 +14287,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14012
14287
  }
14013
14288
  );
14014
14289
 
14015
- if (response?.error || response?.message) {
14290
+ // deleteWidget resolves void; only a thrown ResponseError (caught below)
14291
+ // carries a failure. The defensive envelope check is kept for the response-
14292
+ // transform middleware, but must go through an honest cast.
14293
+ const deleteEnvelope = response as unknown as { error?: string; message?: string } | undefined;
14294
+ if (deleteEnvelope && (deleteEnvelope.error || deleteEnvelope.message)) {
14016
14295
  console.error('Admin widget deletion failed:', response);
14017
14296
  showToast('error', 'Delete failed', {
14018
- description: response?.message || response?.error || 'Unknown error'
14297
+ description: deleteEnvelope?.message || deleteEnvelope?.error || 'Unknown error'
14019
14298
  });
14020
14299
  adminDeleteLoading = false;
14021
14300
  return;
@@ -15218,11 +15497,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15218
15497
  class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
15219
15498
  data-widget-chat-height={panelWidgetId}
15220
15499
  style={chatUserHeight == null
15221
- ? `flex: 1 1 auto; min-height: ${CHAT_MIN_HEIGHT}px;`
15500
+ ? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
15222
15501
  : `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
15223
15502
  >
15224
15503
  <Chat class="code-generation-chat w-full h-full rounded-none border-0"
15225
- style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
15504
+ style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
15226
15505
  resizeExpandsContainer={true}
15227
15506
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
15228
15507
  showLogs={debugMode}
@@ -15249,7 +15528,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15249
15528
  autoCodeDetection: false,
15250
15529
  }
15251
15530
  }}
15252
- widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
15531
+ widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15532
+ versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15253
15533
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15254
15534
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
15255
15535
  onUploadChatFile={handleChatUploadFile}
@@ -15461,7 +15741,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15461
15741
  {/if}
15462
15742
 
15463
15743
  <!-- ─── Schema loading / error / PropsEditor ─── -->
15464
- {@const panelSchemasLoading = (panelDisplayRevision, ps.compositionSchemasLoading)}
15744
+ {@const panelSchemasLoading = rev(panelDisplayRevision, ps.compositionSchemasLoading)}
15465
15745
  {@const panelSchemaLoadLabel = getPanelSchemaLoadLabel(panelWidgetId)}
15466
15746
  {#if panelSchemasLoading}
15467
15747
  <div class="composition-editor-loading flex flex-col items-center justify-center py-8">
@@ -15766,7 +16046,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15766
16046
  {/if}
15767
16047
 
15768
16048
  <!-- Delete Widget Shape Confirmation Dialog -->
15769
- <Dialog.Root class="confirm-delete-dialog-root" bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
16049
+ <!-- Dialog.Root (bits-ui AlertDialogRoot) exposes no class prop — styling hooks
16050
+ live on Content/Header below. -->
16051
+ <Dialog.Root bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
15770
16052
  <Dialog.Content class="confirm-delete-dialog-content max-w-md bg-white z-9999">
15771
16053
  <Dialog.Header class="confirm-delete-dialog-header">
15772
16054
  <Dialog.Title>Delete Widget Shape</Dialog.Title>
@@ -15808,7 +16090,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15808
16090
  </Dialog.Root>
15809
16091
 
15810
16092
  <!-- Delete Canvas Confirmation Dialog -->
15811
- <Dialog.Root class="confirm-delete-canvas-dialog-root" bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
16093
+ <Dialog.Root bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
15812
16094
  <Dialog.Content class="confirm-delete-canvas-dialog-content max-w-sm bg-white z-9999">
15813
16095
  <Dialog.Header class="confirm-delete-canvas-dialog-header">
15814
16096
  <Dialog.Title>Delete Canvas</Dialog.Title>
@@ -15868,7 +16150,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15868
16150
 
15869
16151
  <!-- Confirm Generation Dialog (shown on convert-to-widget before auto-generating) -->
15870
16152
  <Dialog.Root
15871
- class="confirm-generate-dialog-root"
15872
16153
  open={confirmGenerateDialogOpen}
15873
16154
  onOpenChange={(open) => {
15874
16155
  confirmGenerateDialogOpen = open;