@widgetic/creator 0.3.70 → 0.3.72

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.
@@ -891,6 +891,16 @@
891
891
  let currentGenerationOperationId: string | null = null;
892
892
  let generateCodeStatus: string | null = null;
893
893
  let lastGenerationFailed = false; // Track if last generation failed for retry UI
894
+ let publishPlanMessageByWidgetId: Record<string, string> = {};
895
+
896
+ function isPlanLimitMessage(message: string | null | undefined): boolean {
897
+ if (!message) return false;
898
+ return /credit balance|free trial|not available in your plan|not available on the free plan|upgrade your plan|upgrade to a solo/i.test(message);
899
+ }
900
+
901
+ function openUpgradePlans() {
902
+ window.location.assign('/workspaces/personal/settings/billing');
903
+ }
894
904
  let lastGenerationError: string | null = null; // Store the error message for display
895
905
  let lastGenerationPrompt: string | null = null; // Last prompt used, for retrying failed generations
896
906
  let pendingAutoGenerateWidgetId: string | null = null; // Widget created from canvas that needs auto code generation
@@ -1194,7 +1204,12 @@
1194
1204
  // console.log('[Chat] Codegen failure bubble:', content.slice(0, 160));
1195
1205
  }
1196
1206
 
1197
- /** Retry failed codegen without duplicating the user bubble in chat. */
1207
+ /** A retry of a prompt that already produced a version opens a new message. A failed retry stays on the same bubble. */
1208
+ function messageTextHasCommit(content: string | null | undefined): boolean {
1209
+ return /(?:Commit:?\s+|Code saved\s*\()[a-f0-9]{7,40}/i.test(String(content || ''));
1210
+ }
1211
+
1212
+ /** Retry failed codegen without duplicating the user bubble. A successful prompt forks a new message so each version can show "You are here". */
1198
1213
  async function handleChatRetryMessage(content: string, sourceWidgetId?: string) {
1199
1214
  const widgetId = sourceWidgetId ?? selectedWidgetId;
1200
1215
  if (!widgetId) return;
@@ -1212,44 +1227,63 @@
1212
1227
  const draftUploadIds = panelChat?.getAttachmentUploadIds?.() ?? [];
1213
1228
  const draftImageUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
1214
1229
 
1215
- // Re-use attachments from previous user messages in the conversation
1216
- // so that Retry doesn't lose PDFs/files that were uploaded with the original prompt.
1230
+ // A prompt that already has a commit must not reuse that bubble: Version N and
1231
+ // Version N+1 would share one message, and "You are here" could not mark the older one.
1232
+ // The standing sketch is attached on the server for the model. It is not copied onto the chat row.
1233
+ let forkUserMessage = false;
1217
1234
  let retryFileAttachments: Array<{ url: string; file_type: string; file_name: string | null; file_size: number | null }> = [];
1218
1235
  if (draftUploadIds.length === 0 && draftImageUrls.length === 0) {
1219
1236
  try {
1220
1237
  const panelState = getPanelState(widgetId);
1221
1238
  const convId = panelChat?.getConversationId?.() ?? panelState.currentChatConversationId ?? null;
1222
1239
  if (convId) {
1223
- const convMessages = await handleLoadMessages(convId, widgetId);
1224
- // Find most-recent user message that has persisted attachments
1225
- const userMsgsWithAtt = (convMessages || [])
1226
- .filter((m: any) => (m.messageType || m.message_type) === 'user' && (m.attachments || []).length > 0);
1227
- if (userMsgsWithAtt.length > 0) {
1228
- const latestWithAtt = userMsgsWithAtt[userMsgsWithAtt.length - 1];
1229
- retryFileAttachments = (latestWithAtt.attachments || [])
1230
- .filter((a: any) => a.url && /^https?:\/\//i.test(a.url))
1231
- .map((a: any) => ({
1232
- url: a.url,
1233
- file_type: a.file_type || 'application/octet-stream',
1234
- file_name: a.file_name || null,
1235
- file_size: a.file_size || null,
1236
- }));
1237
- // console.log('[Chat→Retry] Re-using', retryFileAttachments.length, 'attachment(s) from previous user message for retry');
1240
+ const convMessages = await handleLoadMessages(convId, widgetId) || [];
1241
+ const needle = content.trim();
1242
+ let matchIndex = -1;
1243
+ for (let i = convMessages.length - 1; i >= 0; i--) {
1244
+ const message = convMessages[i];
1245
+ const type = message.messageType || message.message_type;
1246
+ const text = String(message.messageContent || message.message_content || '').trim();
1247
+ if (type === 'user' && text === needle) {
1248
+ matchIndex = i;
1249
+ break;
1250
+ }
1251
+ }
1252
+ if (matchIndex >= 0) {
1253
+ forkUserMessage = convMessages.slice(matchIndex + 1).some((message) =>
1254
+ (message.messageType || message.message_type) === 'assistant'
1255
+ && messageTextHasCommit(message.messageContent || message.message_content),
1256
+ );
1257
+ if (!forkUserMessage) {
1258
+ retryFileAttachments = (convMessages[matchIndex].attachments || [])
1259
+ .filter((attachment) => attachment.url && /^https?:\/\//i.test(attachment.url))
1260
+ .filter((attachment) => !/design-screenshot/i.test(attachment.file_name || attachment.fileName || ''))
1261
+ .map((attachment) => ({
1262
+ url: attachment.url,
1263
+ file_type: attachment.file_type || attachment.fileType || 'application/octet-stream',
1264
+ file_name: attachment.file_name || attachment.fileName || null,
1265
+ file_size: attachment.file_size || attachment.fileSize || null,
1266
+ }));
1267
+ }
1238
1268
  }
1239
1269
  }
1240
1270
  } catch (retryAttErr) {
1241
- console.warn('[Chat→Retry] Could not load previous attachments for retry:', retryAttErr);
1271
+ console.warn('[Chat→Retry] Could not load the prompt turn for retry:', retryAttErr);
1242
1272
  }
1243
1273
  }
1274
+ if (forkUserMessage) {
1275
+ panelChat?.addUserMessage?.(content);
1276
+ }
1244
1277
 
1245
1278
  await generateCode({
1246
1279
  prompt: content,
1247
1280
  fromChat: true,
1248
1281
  widgetId,
1249
1282
  forceRecreateRepo: true,
1250
- attachmentIds: draftUploadIds.length > 0 ? draftUploadIds : undefined,
1251
- promptImagesOverride: draftImageUrls.length > 0 ? draftImageUrls : undefined,
1252
- persistFileAttachments: retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
1283
+ forkUserMessage,
1284
+ attachmentIds: forkUserMessage ? undefined : (draftUploadIds.length > 0 ? draftUploadIds : undefined),
1285
+ promptImagesOverride: forkUserMessage ? [] : (draftImageUrls.length > 0 ? draftImageUrls : undefined),
1286
+ persistFileAttachments: !forkUserMessage && retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
1253
1287
  });
1254
1288
  }
1255
1289
 
@@ -1532,6 +1566,7 @@
1532
1566
  file_size?: number | null;
1533
1567
  display_order?: number;
1534
1568
  }>,
1569
+ forkUserMessage = false,
1535
1570
  ): Promise<void> {
1536
1571
  if (!conversationsClient || !messagesClient) return;
1537
1572
  try {
@@ -1552,6 +1587,8 @@
1552
1587
  convId = conv?.id;
1553
1588
  }
1554
1589
  if (!convId) return;
1590
+ // The new bubble was already saved by addUserMessage. Do not patch the previous turn with the sketch.
1591
+ if (forkUserMessage) return;
1555
1592
 
1556
1593
  const persistImages = uniquePersistImageUrls([
1557
1594
  ...(imageUrls || []),
@@ -8288,6 +8325,8 @@ Requirements:
8288
8325
  skipIntentGate?: boolean;
8289
8326
  /** The tapped chip's structured intent, declared so the server measures the chip conversion without matching copy. */
8290
8327
  chipIntent?: ClarificationSuggestion['intent'];
8328
+ /** Successful retry: a new user row already exists. Do not attach images to the previous turn. */
8329
+ forkUserMessage?: boolean;
8291
8330
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
8292
8331
  persistFileAttachments?: Array<{
8293
8332
  url?: string;
@@ -8545,8 +8584,9 @@ Requirements:
8545
8584
  }
8546
8585
 
8547
8586
  // Prepare images for the request (if any) — prefer IDs/URLs captured before clearDraft
8548
- let panelPromptImages =
8549
- options.promptImagesOverride && options.promptImagesOverride.length > 0
8587
+ let panelPromptImages = options.forkUserMessage
8588
+ ? []
8589
+ : options.promptImagesOverride && options.promptImagesOverride.length > 0
8550
8590
  ? options.promptImagesOverride
8551
8591
  : generationWidgetId === focusedPanelWidgetId
8552
8592
  ? promptImages
@@ -8556,12 +8596,14 @@ Requirements:
8556
8596
  panelPromptImages = panelPromptImages.filter(
8557
8597
  (url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
8558
8598
  );
8559
- const chatAttachmentUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
8560
- const chatUploadIds = [
8561
- ...(options.attachmentIds || []),
8562
- ...(panelChat?.getAttachmentUploadIds?.() ?? []),
8563
- ].filter((id, index, arr) => id && arr.indexOf(id) === index);
8564
- if (chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
8599
+ const chatAttachmentUrls = options.forkUserMessage ? [] : (panelChat?.getAttachmentDataUrls?.() ?? []);
8600
+ const chatUploadIds = options.forkUserMessage
8601
+ ? []
8602
+ : [
8603
+ ...(options.attachmentIds || []),
8604
+ ...(panelChat?.getAttachmentUploadIds?.() ?? []),
8605
+ ].filter((id, index, arr) => id && arr.indexOf(id) === index);
8606
+ if (!options.forkUserMessage && chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
8565
8607
  const fromChat = chatAttachmentUrls.filter(
8566
8608
  (url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
8567
8609
  );
@@ -8617,7 +8659,7 @@ Requirements:
8617
8659
  duration: 8000
8618
8660
  });
8619
8661
  }
8620
- } else if (promptImpliesVisionReference(prompt)) {
8662
+ } else if (promptImpliesVisionReference(prompt) && !options.forkUserMessage) {
8621
8663
  showToast('warning', 'Generare fără imagine', {
8622
8664
  description:
8623
8665
  'Request-ul merge fără vision (DeepSeek etc.). Atașează PNG/JPEG în chat înainte de Send.',
@@ -8633,8 +8675,9 @@ Requirements:
8633
8675
  await ensureCodegenUserMessagePersisted(
8634
8676
  generationWidgetId,
8635
8677
  prompt,
8636
- persistImageUrls,
8637
- options.persistFileAttachments,
8678
+ options.forkUserMessage ? [] : persistImageUrls,
8679
+ options.forkUserMessage ? undefined : options.persistFileAttachments,
8680
+ !!options.forkUserMessage,
8638
8681
  );
8639
8682
 
8640
8683
  // Clear unsent draft/attachments once payload is captured — prevents restore on widget return
@@ -8885,14 +8928,63 @@ Requirements:
8885
8928
  if (generationWidgetId) {
8886
8929
  const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
8887
8930
  shapeHistoryResolvedByWidgetId = restResolved;
8888
- setTimeout(() => {
8889
- void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
8890
- const newestDraft = getNewestDraftVersionId(generationWidgetId);
8891
- if (newestDraft != null) {
8892
- selectVersionInDropdowns(generationWidgetId, `v:${newestDraft}`);
8931
+ const alignWidgetId = generationWidgetId;
8932
+ const alignCommitSha = completedCommitSha;
8933
+ const alignGeneratedVersion = (attempt: number) => {
8934
+ void loadPublishHistoryForPanel(alignWidgetId, { silent: true }).then(() => {
8935
+ const versionId = findVersionIdMatchingCommit(alignWidgetId, alignCommitSha);
8936
+ const matchedByCommit = !!alignCommitSha
8937
+ && !!versionId
8938
+ && getShapePublishHistory(alignWidgetId).some((row) =>
8939
+ versionId === `v:${Number(row.version)}`
8940
+ && commitsMatch(row?.commitSha ?? row?.commit_sha, alignCommitSha),
8941
+ );
8942
+ if (!matchedByCommit && alignCommitSha && attempt < 4) {
8943
+ setTimeout(() => alignGeneratedVersion(attempt + 1), 1500);
8944
+ return;
8945
+ }
8946
+ if (!versionId) return;
8947
+ if (restoredViewingVersionByWidgetId[alignWidgetId] != null) {
8948
+ const { [alignWidgetId]: _clearedRestore, ...restRestored } = restoredViewingVersionByWidgetId;
8949
+ restoredViewingVersionByWidgetId = restRestored;
8950
+ }
8951
+ const menuNow = dropdownVersionByWidgetId[alignWidgetId];
8952
+ const sourceNow = shapePreviewSourceByWidgetId[alignWidgetId];
8953
+ const selectedRow = getShapePublishHistory(alignWidgetId)
8954
+ .find((row) => `v:${Number(row.version)}` === menuNow);
8955
+ if (
8956
+ menuNow?.startsWith('v:')
8957
+ && menuNow === sourceNow
8958
+ && alignCommitSha
8959
+ && !commitsMatch(selectedRow?.commitSha ?? selectedRow?.commit_sha, alignCommitSha)
8960
+ ) {
8961
+ return;
8962
+ }
8963
+ selectVersionInDropdowns(alignWidgetId, versionId);
8964
+ shapePreviewSourceByWidgetId = {
8965
+ ...shapePreviewSourceByWidgetId,
8966
+ [alignWidgetId]: versionId,
8967
+ };
8968
+ syncWidgetShapeLivePreviews();
8969
+ panelDisplayRevision += 1;
8970
+ if (versionUsesSharedHeadPreview(alignWidgetId, versionId)) {
8971
+ const headUrl = resolveWidgetShapeLivePreviewUrl(alignWidgetId);
8972
+ getWidgetDetailsRef(alignWidgetId)?.showLiveWorkerPreview?.(headUrl);
8973
+ widgetShapeLivePreviewsRef?.requestReload?.(alignWidgetId);
8974
+ } else {
8975
+ const versionNumber = Number(versionId.slice(2));
8976
+ if (Number.isFinite(versionNumber)) {
8977
+ void ensureWidgetShapeVersionPreview(alignWidgetId, versionNumber);
8978
+ }
8893
8979
  }
8980
+ console.log('[VersionPreview] Aligned dropdown with generated commit', {
8981
+ widgetId: alignWidgetId.substring(0, 8),
8982
+ versionId,
8983
+ commit: alignCommitSha?.substring(0, 8) ?? null,
8984
+ });
8894
8985
  });
8895
- }, 1200);
8986
+ };
8987
+ setTimeout(() => alignGeneratedVersion(0), 1200);
8896
8988
  }
8897
8989
 
8898
8990
  // Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
@@ -9053,10 +9145,8 @@ Requirements:
9053
9145
  await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
9054
9146
  }
9055
9147
  activePanelRef?.setPreviewBuildError(null);
9056
- // TODO(tomorrow): Dupa o generare noua, sincronizarea versiunii in dropdown si istoric se desincronizeaza.
9057
- // Trebuie armonizata selectia automata a noii versiuni draft (vN) create in istoric,
9058
- // actualizat trigger-ul pe canvas si in chat la noul numar de versiune si pastrata coerenta intre preview si dropdown.
9059
9148
  // Show Worker preview on the canvas shape (not the previous Live CDN artifact).
9149
+ // History alignment below moves the dropdown onto the commit this build belongs to.
9060
9150
  shapePreviewSourceByWidgetId = {
9061
9151
  ...shapePreviewSourceByWidgetId,
9062
9152
  [generationWidgetId]: 'preview',
@@ -9468,10 +9558,18 @@ Requirements:
9468
9558
  isPublishing = false;
9469
9559
  publishingWidgetId = null;
9470
9560
  setCrossTabPublishing(false, capturedWidgetId);
9471
- showToast('error', 'Publish failed', {
9472
- description: normalizeErrorMessage(err) || 'An unexpected error occurred. Please try again.',
9473
- duration: 8000
9474
- });
9561
+ const publishErrorText = normalizeErrorMessage(err) || 'An unexpected error occurred. Please try again.';
9562
+ if (isPlanLimitMessage(publishErrorText) && capturedWidgetId) {
9563
+ publishPlanMessageByWidgetId = {
9564
+ ...publishPlanMessageByWidgetId,
9565
+ [capturedWidgetId]: publishErrorText,
9566
+ };
9567
+ } else {
9568
+ showToast('error', 'Publish failed', {
9569
+ description: publishErrorText,
9570
+ duration: 8000
9571
+ });
9572
+ }
9475
9573
  }
9476
9574
  // isPublishing stays true until widget_publish_status WebSocket event arrives (success or failure)
9477
9575
  }
@@ -10495,6 +10593,15 @@ Requirements:
10495
10593
  function getPanelEditPreviewUrl(widgetId: string): string | null {
10496
10594
  const ps = getPanelDisplayState(widgetId);
10497
10595
  const source = shapePreviewSourceByWidgetId[widgetId];
10596
+ // Head draft and the post-codegen 'preview' source are the Worker build.
10597
+ // Falling through here used to hand the panel the last published CDN file
10598
+ // while the dropdown already said the new draft.
10599
+ if (source === 'preview' || (source && versionUsesSharedHeadPreview(widgetId, source))) {
10600
+ const hasCode = !!(ps.lastCommitId || loadCommitIdForWidget(widgetId));
10601
+ if (hasCode && source !== 'live') {
10602
+ return `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
10603
+ }
10604
+ }
10498
10605
  if (source && source.startsWith('v:')) {
10499
10606
  const version = parseInt(source.slice(2), 10);
10500
10607
  if (Number.isFinite(version)) {
@@ -10631,6 +10738,45 @@ Requirements:
10631
10738
  return draftVersions.length > 0 ? Math.max(...draftVersions) : null;
10632
10739
  }
10633
10740
 
10741
+ /** True when this draft row's commit is still the repo HEAD the shared Worker preview serves. */
10742
+ function draftCommitMatchesRepoHead(widgetId: string, record: { commitSha?: string | null; commit_sha?: string | null } | null | undefined): boolean {
10743
+ const draftCommitSha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha ?? null);
10744
+ const headCommitSha = normalizeCommitSha(
10745
+ getPanelDisplayState(widgetId).lastCommitId ?? loadCommitIdForWidget(widgetId),
10746
+ );
10747
+ return !draftCommitSha || !headCommitSha || commitsMatch(draftCommitSha, headCommitSha);
10748
+ }
10749
+
10750
+ /**
10751
+ * Shared `/preview/{widgetId}` is only the selected row when that row is the
10752
+ * newest draft and its commit is still HEAD. Every other version needs its
10753
+ * own build — otherwise the dropdown says vN while the iframe shows HEAD.
10754
+ */
10755
+ function versionUsesSharedHeadPreview(widgetId: string, versionId: string | null | undefined): boolean {
10756
+ if (!versionId || versionId === 'preview' || versionId === 'live') return versionId !== 'live';
10757
+ if (!versionId.startsWith('v:')) return false;
10758
+ const version = parseInt(versionId.slice(2), 10);
10759
+ if (!Number.isFinite(version)) return false;
10760
+ const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10761
+ if (record?.publishedAt ?? record?.published_at) return false;
10762
+ const newestDraft = getNewestDraftVersionId(widgetId);
10763
+ if (newestDraft == null || version !== newestDraft) return false;
10764
+ if (restoredViewingVersionByWidgetId[widgetId] != null) return false;
10765
+ return draftCommitMatchesRepoHead(widgetId, record);
10766
+ }
10767
+
10768
+ function findVersionIdMatchingCommit(widgetId: string, commitSha: string | null): string | null {
10769
+ const target = normalizeCommitSha(commitSha);
10770
+ const history = getShapePublishHistory(widgetId);
10771
+ if (target) {
10772
+ const match = history.find((row) => commitsMatch(row?.commitSha ?? row?.commit_sha, target));
10773
+ const version = Number(match?.version);
10774
+ if (Number.isFinite(version)) return `v:${version}`;
10775
+ }
10776
+ const newestDraft = getNewestDraftVersionId(widgetId);
10777
+ return newestDraft != null ? `v:${newestDraft}` : null;
10778
+ }
10779
+
10634
10780
  function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
10635
10781
  if (!widgetId) return null;
10636
10782
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
@@ -10665,11 +10811,15 @@ Requirements:
10665
10811
  // `/preview/{widgetId}` artifact then serves the restored code under the
10666
10812
  // newest version's number — ask for a commit-specific build instead.
10667
10813
  const newestDraft = getNewestDraftVersionId(widgetId);
10668
- const draftCommitSha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha ?? null);
10669
- const headCommitSha = normalizeCommitSha(ps.lastCommitId ?? loadCommitIdForWidget(widgetId));
10670
- const draftIsRepoHead =
10671
- !draftCommitSha || !headCommitSha || commitsMatch(draftCommitSha, headCommitSha);
10672
- if (!isPublished && newestDraft != null && version === newestDraft && draftIsRepoHead && workerUrl) {
10814
+ const draftIsRepoHead = draftCommitMatchesRepoHead(widgetId, record);
10815
+ if (
10816
+ !isPublished
10817
+ && newestDraft != null
10818
+ && version === newestDraft
10819
+ && draftIsRepoHead
10820
+ && workerUrl
10821
+ && restoredViewingVersionByWidgetId[widgetId] == null
10822
+ ) {
10673
10823
  return workerUrl;
10674
10824
  }
10675
10825
  return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
@@ -10852,10 +11002,18 @@ Requirements:
10852
11002
  const isPublished = !!(record?.publishedAt ?? record?.published_at);
10853
11003
  const newestDraft = getNewestDraftVersionId(widgetId);
10854
11004
  const workerUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10855
- const viewingHeadDraft = !isPublished && newestDraft != null && version === newestDraft && workerUrl && !restoredViewingVersionByWidgetId[widgetId];
10856
- if (viewingHeadDraft) {
11005
+ const viewingHeadDraft = !isPublished
11006
+ && newestDraft != null
11007
+ && version === newestDraft
11008
+ && draftCommitMatchesRepoHead(widgetId, record)
11009
+ && !!workerUrl
11010
+ && restoredViewingVersionByWidgetId[widgetId] == null;
11011
+ if (viewingHeadDraft && workerUrl) {
10857
11012
  const detailsRef = getWidgetDetailsRef(widgetId);
10858
- detailsRef?.showPublishedArtifact?.(workerUrl);
11013
+ // Same Worker URL as the previous draft: the shape iframe does not
11014
+ // remount unless the cache-bust token changes.
11015
+ detailsRef?.showLiveWorkerPreview?.(workerUrl);
11016
+ widgetShapeLivePreviewsRef?.requestReload?.(widgetId);
10859
11017
  } else {
10860
11018
  void ensureWidgetShapeVersionPreview(widgetId, version);
10861
11019
  }
@@ -10992,6 +11150,7 @@ Requirements:
10992
11150
  */
10993
11151
  let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10994
11152
  let versionPromptLabelsByWidgetId: Record<string, Record<string, string>> = {};
11153
+ let versionTimelineByWidgetId: Record<string, Array<{ version: number; sha: string; prompt: string }>> = {};
10995
11154
  let viewingVersionByWidgetId: Record<string, number | null> = {};
10996
11155
  $: {
10997
11156
  void shapePreviewSourceByWidgetId;
@@ -11002,16 +11161,19 @@ Requirements:
11002
11161
  const nextHeads: Record<string, string | null> = {};
11003
11162
  const nextLabels: Record<string, Record<string, string>> = {};
11004
11163
  const nextPrompts: Record<string, Record<string, string>> = {};
11164
+ const nextTimeline: Record<string, Array<{ version: number; sha: string; prompt: string }>> = {};
11005
11165
  const nextViewing: Record<string, number | null> = {};
11006
11166
  for (const widgetId of openWidgetPanelIds) {
11007
11167
  nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
11008
11168
  nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
11009
11169
  nextPrompts[widgetId] = buildVersionPromptLabelsForPanel(widgetId);
11170
+ nextTimeline[widgetId] = buildVersionTimelineForPanel(widgetId);
11010
11171
  nextViewing[widgetId] = viewingVersionNumberForPanel(widgetId);
11011
11172
  }
11012
11173
  headCommitShaByWidgetId = nextHeads;
11013
11174
  versionLabelsByWidgetId = nextLabels;
11014
11175
  versionPromptLabelsByWidgetId = nextPrompts;
11176
+ versionTimelineByWidgetId = nextTimeline;
11015
11177
  viewingVersionByWidgetId = nextViewing;
11016
11178
  }
11017
11179
 
@@ -11049,6 +11211,20 @@ Requirements:
11049
11211
  return text || null;
11050
11212
  }
11051
11213
 
11214
+ function buildVersionTimelineForPanel(widgetId: string): Array<{ version: number; sha: string; prompt: string }> {
11215
+ const ps = getPanelDisplayState(widgetId);
11216
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
11217
+ const timeline: Array<{ version: number; sha: string; prompt: string }> = [];
11218
+ for (const record of history || []) {
11219
+ if (record?.version == null) continue;
11220
+ const sha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
11221
+ const prompt = versionPromptKey(record?.prompt);
11222
+ if (!sha || !prompt) continue;
11223
+ timeline.push({ version: Number(record.version), sha, prompt });
11224
+ }
11225
+ return timeline;
11226
+ }
11227
+
11052
11228
  /** User prompt → "Version N", for turns whose commit line was never saved. */
11053
11229
  function buildVersionPromptLabelsForPanel(widgetId: string): Record<string, string> {
11054
11230
  const labels: Record<string, string> = {};
@@ -12521,15 +12697,24 @@ $: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previ
12521
12697
  const wsEventKey = `${wsReadyWidgetId}:${$dynamicWorkerPreviewStatus.previewUrl}:${$dynamicWorkerPreviewStatus.timestamp ?? ''}`;
12522
12698
  if (wsEventKey !== lastWsWorkerPreviewEventKey) {
12523
12699
  lastWsWorkerPreviewEventKey = wsEventKey;
12524
- shapePreviewSourceByWidgetId = {
12525
- ...shapePreviewSourceByWidgetId,
12526
- [wsReadyWidgetId]: 'preview',
12527
- };
12528
- if (codegenCaptureBlocked()) {
12529
- console.log('[CreatorApp] Defer canvas Worker preview reload until codegen finishes', wsReadyWidgetId.substring(0, 8));
12700
+ const selectedVersion = dropdownVersionByWidgetId[wsReadyWidgetId]
12701
+ || shapePreviewSourceByWidgetId[wsReadyWidgetId];
12702
+ const keepHistoricalSelection = !generatingWidgetIds[wsReadyWidgetId]
12703
+ && !!selectedVersion?.startsWith('v:')
12704
+ && !versionUsesSharedHeadPreview(wsReadyWidgetId, selectedVersion);
12705
+ if (keepHistoricalSelection) {
12706
+ console.log('[CreatorApp] Historical version stays selected; shared HEAD build ignored', wsReadyWidgetId.substring(0, 8));
12530
12707
  } else {
12531
- syncWidgetShapeLivePreviews();
12532
- widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12708
+ shapePreviewSourceByWidgetId = {
12709
+ ...shapePreviewSourceByWidgetId,
12710
+ [wsReadyWidgetId]: 'preview',
12711
+ };
12712
+ if (codegenCaptureBlocked()) {
12713
+ console.log('[CreatorApp] Defer canvas Worker preview reload until codegen finishes', wsReadyWidgetId.substring(0, 8));
12714
+ } else {
12715
+ syncWidgetShapeLivePreviews();
12716
+ widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12717
+ }
12533
12718
  }
12534
12719
  }
12535
12720
  }
@@ -15841,10 +16026,47 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15841
16026
  {/if}
15842
16027
  </div>
15843
16028
  </div>
16029
+ {#if publishPlanMessageByWidgetId[panelWidgetId]}
16030
+ <div class="publish-plan-notice p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
16031
+ <p class="text-xs text-red-700 whitespace-pre-wrap">{publishPlanMessageByWidgetId[panelWidgetId]}</p>
16032
+ <div class="flex gap-2 mt-3 justify-center">
16033
+ <Button
16034
+ onclick={openUpgradePlans}
16035
+ variant="default"
16036
+ size="sm"
16037
+ class="bg-red-600 hover:bg-red-700 text-white rounded-lg"
16038
+ >
16039
+ Upgrade
16040
+ </Button>
16041
+ <Button
16042
+ onclick={() => {
16043
+ const next = { ...publishPlanMessageByWidgetId };
16044
+ delete next[panelWidgetId];
16045
+ publishPlanMessageByWidgetId = next;
16046
+ }}
16047
+ variant="outline"
16048
+ size="sm"
16049
+ class="border-gray-300 text-gray-600 hover:bg-gray-50 rounded-lg"
16050
+ >
16051
+ Dismiss
16052
+ </Button>
16053
+ </div>
16054
+ </div>
16055
+ {/if}
15844
16056
  {#if ps.lastGenerationFailed}
15845
16057
  <div class="p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
15846
16058
  <p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
15847
16059
  <div class="flex gap-2 mt-3 justify-center">
16060
+ {#if isPlanLimitMessage(ps.lastGenerationError)}
16061
+ <Button
16062
+ onclick={openUpgradePlans}
16063
+ variant="default"
16064
+ size="sm"
16065
+ class="bg-red-600 hover:bg-red-700 text-white rounded-lg"
16066
+ >
16067
+ Upgrade
16068
+ </Button>
16069
+ {:else}
15848
16070
  <Button
15849
16071
  onclick={() => {
15850
16072
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
@@ -15859,6 +16081,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15859
16081
  >
15860
16082
  Retry
15861
16083
  </Button>
16084
+ {/if}
15862
16085
  <Button
15863
16086
  onclick={() => {
15864
16087
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
@@ -15934,6 +16157,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15934
16157
  widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15935
16158
  versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15936
16159
  versionLabelByPrompt={versionPromptLabelsByWidgetId[panelWidgetId] || {}}
16160
+ versionTimeline={versionTimelineByWidgetId[panelWidgetId] || []}
15937
16161
  viewingVersion={viewingVersionByWidgetId[panelWidgetId] ?? null}
15938
16162
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15939
16163
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
@@ -497,6 +497,20 @@
497
497
  void forceReloadPreviewIframe(url);
498
498
  }
499
499
 
500
+ /**
501
+ * Shared Worker HEAD (`/preview/{widgetId}`). Do not pin it: a pin blocks the
502
+ * next build event, so the panel keeps the previous generation.
503
+ */
504
+ export function showLiveWorkerPreview(url: string | null): void {
505
+ if (!url) return;
506
+ pinnedPublishedPreviewUrl = null;
507
+ lastWsPreviewSignature = null;
508
+ publishedArtifactReachable = true;
509
+ _lastEditPreviewBase = null;
510
+ _lastEmbedPreviewBase = null;
511
+ void forceReloadPreviewIframe(url);
512
+ }
513
+
500
514
  export function finishCompositionHydration(): void {
501
515
  if (compositionHydrationTimeout) {
502
516
  clearTimeout(compositionHydrationTimeout);
@@ -106,6 +106,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
106
106
  */ showCompositionSteps?: boolean;
107
107
  beginCompositionHydration?: () => void;
108
108
  showPublishedArtifact?: (url: string | null) => void;
109
+ showLiveWorkerPreview?: (url: string | null) => void;
109
110
  finishCompositionHydration?: () => void;
110
111
  markCodegenPreviewFresh?: () => void;
111
112
  clearCodegenPreviewFresh?: () => void;
@@ -207,6 +208,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
207
208
  debugModeAvailable: boolean;
208
209
  beginCompositionHydration: () => void;
209
210
  showPublishedArtifact: (url: string | null) => void;
211
+ showLiveWorkerPreview: (url: string | null) => void;
210
212
  finishCompositionHydration: () => void;
211
213
  markCodegenPreviewFresh: () => void;
212
214
  clearCodegenPreviewFresh: () => void;
@@ -205,19 +205,25 @@
205
205
  const coalescedReloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
206
206
 
207
207
  export function requestReload(widgetId: string): void {
208
- if (isGenerating) return;
209
208
  const now = Date.now();
210
209
  const sinceLast = now - (lastReloadAtByWidgetId[widgetId] || 0);
211
- if (sinceLast < RELOAD_COALESCE_MS) {
212
- if (!coalescedReloadTimers.has(widgetId)) {
213
- coalescedReloadTimers.set(
214
- widgetId,
215
- setTimeout(() => {
216
- coalescedReloadTimers.delete(widgetId);
217
- requestReload(widgetId);
218
- }, RELOAD_COALESCE_MS - sinceLast),
219
- );
220
- }
210
+ // A second call inside the window used to be dropped when a timer already
211
+ // existed, so the codegen reload (the one that matches the new commit)
212
+ // never ran and the shape kept the previous build. Replace the timer so
213
+ // the later request wins. While generating, wait and try again.
214
+ if (isGenerating || sinceLast < RELOAD_COALESCE_MS) {
215
+ const existing = coalescedReloadTimers.get(widgetId);
216
+ if (existing) clearTimeout(existing);
217
+ const wait = isGenerating
218
+ ? RELOAD_COALESCE_MS
219
+ : Math.max(0, RELOAD_COALESCE_MS - sinceLast);
220
+ coalescedReloadTimers.set(
221
+ widgetId,
222
+ setTimeout(() => {
223
+ coalescedReloadTimers.delete(widgetId);
224
+ requestReload(widgetId);
225
+ }, wait),
226
+ );
221
227
  return;
222
228
  }
223
229
  const coalesced = coalescedReloadTimers.get(widgetId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.70",
3
+ "version": "0.3.72",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",