@widgetic/creator 0.3.70 → 0.3.71

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.
@@ -1194,7 +1194,12 @@
1194
1194
  // console.log('[Chat] Codegen failure bubble:', content.slice(0, 160));
1195
1195
  }
1196
1196
 
1197
- /** Retry failed codegen without duplicating the user bubble in chat. */
1197
+ /** A retry of a prompt that already produced a version opens a new message. A failed retry stays on the same bubble. */
1198
+ function messageTextHasCommit(content: string | null | undefined): boolean {
1199
+ return /(?:Commit:?\s+|Code saved\s*\()[a-f0-9]{7,40}/i.test(String(content || ''));
1200
+ }
1201
+
1202
+ /** Retry failed codegen without duplicating the user bubble. A successful prompt forks a new message so each version can show "You are here". */
1198
1203
  async function handleChatRetryMessage(content: string, sourceWidgetId?: string) {
1199
1204
  const widgetId = sourceWidgetId ?? selectedWidgetId;
1200
1205
  if (!widgetId) return;
@@ -1212,44 +1217,63 @@
1212
1217
  const draftUploadIds = panelChat?.getAttachmentUploadIds?.() ?? [];
1213
1218
  const draftImageUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
1214
1219
 
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.
1220
+ // A prompt that already has a commit must not reuse that bubble: Version N and
1221
+ // Version N+1 would share one message, and "You are here" could not mark the older one.
1222
+ // The standing sketch is attached on the server for the model. It is not copied onto the chat row.
1223
+ let forkUserMessage = false;
1217
1224
  let retryFileAttachments: Array<{ url: string; file_type: string; file_name: string | null; file_size: number | null }> = [];
1218
1225
  if (draftUploadIds.length === 0 && draftImageUrls.length === 0) {
1219
1226
  try {
1220
1227
  const panelState = getPanelState(widgetId);
1221
1228
  const convId = panelChat?.getConversationId?.() ?? panelState.currentChatConversationId ?? null;
1222
1229
  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');
1230
+ const convMessages = await handleLoadMessages(convId, widgetId) || [];
1231
+ const needle = content.trim();
1232
+ let matchIndex = -1;
1233
+ for (let i = convMessages.length - 1; i >= 0; i--) {
1234
+ const message = convMessages[i];
1235
+ const type = message.messageType || message.message_type;
1236
+ const text = String(message.messageContent || message.message_content || '').trim();
1237
+ if (type === 'user' && text === needle) {
1238
+ matchIndex = i;
1239
+ break;
1240
+ }
1241
+ }
1242
+ if (matchIndex >= 0) {
1243
+ forkUserMessage = convMessages.slice(matchIndex + 1).some((message) =>
1244
+ (message.messageType || message.message_type) === 'assistant'
1245
+ && messageTextHasCommit(message.messageContent || message.message_content),
1246
+ );
1247
+ if (!forkUserMessage) {
1248
+ retryFileAttachments = (convMessages[matchIndex].attachments || [])
1249
+ .filter((attachment) => attachment.url && /^https?:\/\//i.test(attachment.url))
1250
+ .filter((attachment) => !/design-screenshot/i.test(attachment.file_name || attachment.fileName || ''))
1251
+ .map((attachment) => ({
1252
+ url: attachment.url,
1253
+ file_type: attachment.file_type || attachment.fileType || 'application/octet-stream',
1254
+ file_name: attachment.file_name || attachment.fileName || null,
1255
+ file_size: attachment.file_size || attachment.fileSize || null,
1256
+ }));
1257
+ }
1238
1258
  }
1239
1259
  }
1240
1260
  } catch (retryAttErr) {
1241
- console.warn('[Chat→Retry] Could not load previous attachments for retry:', retryAttErr);
1261
+ console.warn('[Chat→Retry] Could not load the prompt turn for retry:', retryAttErr);
1242
1262
  }
1243
1263
  }
1264
+ if (forkUserMessage) {
1265
+ panelChat?.addUserMessage?.(content);
1266
+ }
1244
1267
 
1245
1268
  await generateCode({
1246
1269
  prompt: content,
1247
1270
  fromChat: true,
1248
1271
  widgetId,
1249
1272
  forceRecreateRepo: true,
1250
- attachmentIds: draftUploadIds.length > 0 ? draftUploadIds : undefined,
1251
- promptImagesOverride: draftImageUrls.length > 0 ? draftImageUrls : undefined,
1252
- persistFileAttachments: retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
1273
+ forkUserMessage,
1274
+ attachmentIds: forkUserMessage ? undefined : (draftUploadIds.length > 0 ? draftUploadIds : undefined),
1275
+ promptImagesOverride: forkUserMessage ? [] : (draftImageUrls.length > 0 ? draftImageUrls : undefined),
1276
+ persistFileAttachments: !forkUserMessage && retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
1253
1277
  });
1254
1278
  }
1255
1279
 
@@ -1532,6 +1556,7 @@
1532
1556
  file_size?: number | null;
1533
1557
  display_order?: number;
1534
1558
  }>,
1559
+ forkUserMessage = false,
1535
1560
  ): Promise<void> {
1536
1561
  if (!conversationsClient || !messagesClient) return;
1537
1562
  try {
@@ -1552,6 +1577,8 @@
1552
1577
  convId = conv?.id;
1553
1578
  }
1554
1579
  if (!convId) return;
1580
+ // The new bubble was already saved by addUserMessage. Do not patch the previous turn with the sketch.
1581
+ if (forkUserMessage) return;
1555
1582
 
1556
1583
  const persistImages = uniquePersistImageUrls([
1557
1584
  ...(imageUrls || []),
@@ -8288,6 +8315,8 @@ Requirements:
8288
8315
  skipIntentGate?: boolean;
8289
8316
  /** The tapped chip's structured intent, declared so the server measures the chip conversion without matching copy. */
8290
8317
  chipIntent?: ClarificationSuggestion['intent'];
8318
+ /** Successful retry: a new user row already exists. Do not attach images to the previous turn. */
8319
+ forkUserMessage?: boolean;
8291
8320
  /** Chat files (PDF, video, images) to store on the user message before codegen. */
8292
8321
  persistFileAttachments?: Array<{
8293
8322
  url?: string;
@@ -8545,8 +8574,9 @@ Requirements:
8545
8574
  }
8546
8575
 
8547
8576
  // Prepare images for the request (if any) — prefer IDs/URLs captured before clearDraft
8548
- let panelPromptImages =
8549
- options.promptImagesOverride && options.promptImagesOverride.length > 0
8577
+ let panelPromptImages = options.forkUserMessage
8578
+ ? []
8579
+ : options.promptImagesOverride && options.promptImagesOverride.length > 0
8550
8580
  ? options.promptImagesOverride
8551
8581
  : generationWidgetId === focusedPanelWidgetId
8552
8582
  ? promptImages
@@ -8556,12 +8586,14 @@ Requirements:
8556
8586
  panelPromptImages = panelPromptImages.filter(
8557
8587
  (url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
8558
8588
  );
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)) {
8589
+ const chatAttachmentUrls = options.forkUserMessage ? [] : (panelChat?.getAttachmentDataUrls?.() ?? []);
8590
+ const chatUploadIds = options.forkUserMessage
8591
+ ? []
8592
+ : [
8593
+ ...(options.attachmentIds || []),
8594
+ ...(panelChat?.getAttachmentUploadIds?.() ?? []),
8595
+ ].filter((id, index, arr) => id && arr.indexOf(id) === index);
8596
+ if (!options.forkUserMessage && chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
8565
8597
  const fromChat = chatAttachmentUrls.filter(
8566
8598
  (url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
8567
8599
  );
@@ -8633,8 +8665,9 @@ Requirements:
8633
8665
  await ensureCodegenUserMessagePersisted(
8634
8666
  generationWidgetId,
8635
8667
  prompt,
8636
- persistImageUrls,
8637
- options.persistFileAttachments,
8668
+ options.forkUserMessage ? [] : persistImageUrls,
8669
+ options.forkUserMessage ? undefined : options.persistFileAttachments,
8670
+ !!options.forkUserMessage,
8638
8671
  );
8639
8672
 
8640
8673
  // Clear unsent draft/attachments once payload is captured — prevents restore on widget return
@@ -8885,14 +8918,63 @@ Requirements:
8885
8918
  if (generationWidgetId) {
8886
8919
  const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
8887
8920
  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}`);
8921
+ const alignWidgetId = generationWidgetId;
8922
+ const alignCommitSha = completedCommitSha;
8923
+ const alignGeneratedVersion = (attempt: number) => {
8924
+ void loadPublishHistoryForPanel(alignWidgetId, { silent: true }).then(() => {
8925
+ const versionId = findVersionIdMatchingCommit(alignWidgetId, alignCommitSha);
8926
+ const matchedByCommit = !!alignCommitSha
8927
+ && !!versionId
8928
+ && getShapePublishHistory(alignWidgetId).some((row) =>
8929
+ versionId === `v:${Number(row.version)}`
8930
+ && commitsMatch(row?.commitSha ?? row?.commit_sha, alignCommitSha),
8931
+ );
8932
+ if (!matchedByCommit && alignCommitSha && attempt < 4) {
8933
+ setTimeout(() => alignGeneratedVersion(attempt + 1), 1500);
8934
+ return;
8893
8935
  }
8936
+ if (!versionId) return;
8937
+ if (restoredViewingVersionByWidgetId[alignWidgetId] != null) {
8938
+ const { [alignWidgetId]: _clearedRestore, ...restRestored } = restoredViewingVersionByWidgetId;
8939
+ restoredViewingVersionByWidgetId = restRestored;
8940
+ }
8941
+ const menuNow = dropdownVersionByWidgetId[alignWidgetId];
8942
+ const sourceNow = shapePreviewSourceByWidgetId[alignWidgetId];
8943
+ const selectedRow = getShapePublishHistory(alignWidgetId)
8944
+ .find((row) => `v:${Number(row.version)}` === menuNow);
8945
+ if (
8946
+ menuNow?.startsWith('v:')
8947
+ && menuNow === sourceNow
8948
+ && alignCommitSha
8949
+ && !commitsMatch(selectedRow?.commitSha ?? selectedRow?.commit_sha, alignCommitSha)
8950
+ ) {
8951
+ return;
8952
+ }
8953
+ selectVersionInDropdowns(alignWidgetId, versionId);
8954
+ shapePreviewSourceByWidgetId = {
8955
+ ...shapePreviewSourceByWidgetId,
8956
+ [alignWidgetId]: versionId,
8957
+ };
8958
+ syncWidgetShapeLivePreviews();
8959
+ panelDisplayRevision += 1;
8960
+ if (versionUsesSharedHeadPreview(alignWidgetId, versionId)) {
8961
+ const headUrl = resolveWidgetShapeLivePreviewUrl(alignWidgetId);
8962
+ getWidgetDetailsRef(alignWidgetId)?.showLiveWorkerPreview?.(headUrl);
8963
+ widgetShapeLivePreviewsRef?.requestReload?.(alignWidgetId);
8964
+ } else {
8965
+ const versionNumber = Number(versionId.slice(2));
8966
+ if (Number.isFinite(versionNumber)) {
8967
+ void ensureWidgetShapeVersionPreview(alignWidgetId, versionNumber);
8968
+ }
8969
+ }
8970
+ console.log('[VersionPreview] Aligned dropdown with generated commit', {
8971
+ widgetId: alignWidgetId.substring(0, 8),
8972
+ versionId,
8973
+ commit: alignCommitSha?.substring(0, 8) ?? null,
8974
+ });
8894
8975
  });
8895
- }, 1200);
8976
+ };
8977
+ setTimeout(() => alignGeneratedVersion(0), 1200);
8896
8978
  }
8897
8979
 
8898
8980
  // Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
@@ -9053,10 +9135,8 @@ Requirements:
9053
9135
  await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
9054
9136
  }
9055
9137
  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
9138
  // Show Worker preview on the canvas shape (not the previous Live CDN artifact).
9139
+ // History alignment below moves the dropdown onto the commit this build belongs to.
9060
9140
  shapePreviewSourceByWidgetId = {
9061
9141
  ...shapePreviewSourceByWidgetId,
9062
9142
  [generationWidgetId]: 'preview',
@@ -10495,6 +10575,15 @@ Requirements:
10495
10575
  function getPanelEditPreviewUrl(widgetId: string): string | null {
10496
10576
  const ps = getPanelDisplayState(widgetId);
10497
10577
  const source = shapePreviewSourceByWidgetId[widgetId];
10578
+ // Head draft and the post-codegen 'preview' source are the Worker build.
10579
+ // Falling through here used to hand the panel the last published CDN file
10580
+ // while the dropdown already said the new draft.
10581
+ if (source === 'preview' || (source && versionUsesSharedHeadPreview(widgetId, source))) {
10582
+ const hasCode = !!(ps.lastCommitId || loadCommitIdForWidget(widgetId));
10583
+ if (hasCode && source !== 'live') {
10584
+ return `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
10585
+ }
10586
+ }
10498
10587
  if (source && source.startsWith('v:')) {
10499
10588
  const version = parseInt(source.slice(2), 10);
10500
10589
  if (Number.isFinite(version)) {
@@ -10631,6 +10720,45 @@ Requirements:
10631
10720
  return draftVersions.length > 0 ? Math.max(...draftVersions) : null;
10632
10721
  }
10633
10722
 
10723
+ /** True when this draft row's commit is still the repo HEAD the shared Worker preview serves. */
10724
+ function draftCommitMatchesRepoHead(widgetId: string, record: { commitSha?: string | null; commit_sha?: string | null } | null | undefined): boolean {
10725
+ const draftCommitSha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha ?? null);
10726
+ const headCommitSha = normalizeCommitSha(
10727
+ getPanelDisplayState(widgetId).lastCommitId ?? loadCommitIdForWidget(widgetId),
10728
+ );
10729
+ return !draftCommitSha || !headCommitSha || commitsMatch(draftCommitSha, headCommitSha);
10730
+ }
10731
+
10732
+ /**
10733
+ * Shared `/preview/{widgetId}` is only the selected row when that row is the
10734
+ * newest draft and its commit is still HEAD. Every other version needs its
10735
+ * own build — otherwise the dropdown says vN while the iframe shows HEAD.
10736
+ */
10737
+ function versionUsesSharedHeadPreview(widgetId: string, versionId: string | null | undefined): boolean {
10738
+ if (!versionId || versionId === 'preview' || versionId === 'live') return versionId !== 'live';
10739
+ if (!versionId.startsWith('v:')) return false;
10740
+ const version = parseInt(versionId.slice(2), 10);
10741
+ if (!Number.isFinite(version)) return false;
10742
+ const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
10743
+ if (record?.publishedAt ?? record?.published_at) return false;
10744
+ const newestDraft = getNewestDraftVersionId(widgetId);
10745
+ if (newestDraft == null || version !== newestDraft) return false;
10746
+ if (restoredViewingVersionByWidgetId[widgetId] != null) return false;
10747
+ return draftCommitMatchesRepoHead(widgetId, record);
10748
+ }
10749
+
10750
+ function findVersionIdMatchingCommit(widgetId: string, commitSha: string | null): string | null {
10751
+ const target = normalizeCommitSha(commitSha);
10752
+ const history = getShapePublishHistory(widgetId);
10753
+ if (target) {
10754
+ const match = history.find((row) => commitsMatch(row?.commitSha ?? row?.commit_sha, target));
10755
+ const version = Number(match?.version);
10756
+ if (Number.isFinite(version)) return `v:${version}`;
10757
+ }
10758
+ const newestDraft = getNewestDraftVersionId(widgetId);
10759
+ return newestDraft != null ? `v:${newestDraft}` : null;
10760
+ }
10761
+
10634
10762
  function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
10635
10763
  if (!widgetId) return null;
10636
10764
  const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
@@ -10665,11 +10793,15 @@ Requirements:
10665
10793
  // `/preview/{widgetId}` artifact then serves the restored code under the
10666
10794
  // newest version's number — ask for a commit-specific build instead.
10667
10795
  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) {
10796
+ const draftIsRepoHead = draftCommitMatchesRepoHead(widgetId, record);
10797
+ if (
10798
+ !isPublished
10799
+ && newestDraft != null
10800
+ && version === newestDraft
10801
+ && draftIsRepoHead
10802
+ && workerUrl
10803
+ && restoredViewingVersionByWidgetId[widgetId] == null
10804
+ ) {
10673
10805
  return workerUrl;
10674
10806
  }
10675
10807
  return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
@@ -10852,10 +10984,18 @@ Requirements:
10852
10984
  const isPublished = !!(record?.publishedAt ?? record?.published_at);
10853
10985
  const newestDraft = getNewestDraftVersionId(widgetId);
10854
10986
  const workerUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
10855
- const viewingHeadDraft = !isPublished && newestDraft != null && version === newestDraft && workerUrl && !restoredViewingVersionByWidgetId[widgetId];
10856
- if (viewingHeadDraft) {
10987
+ const viewingHeadDraft = !isPublished
10988
+ && newestDraft != null
10989
+ && version === newestDraft
10990
+ && draftCommitMatchesRepoHead(widgetId, record)
10991
+ && !!workerUrl
10992
+ && restoredViewingVersionByWidgetId[widgetId] == null;
10993
+ if (viewingHeadDraft && workerUrl) {
10857
10994
  const detailsRef = getWidgetDetailsRef(widgetId);
10858
- detailsRef?.showPublishedArtifact?.(workerUrl);
10995
+ // Same Worker URL as the previous draft: the shape iframe does not
10996
+ // remount unless the cache-bust token changes.
10997
+ detailsRef?.showLiveWorkerPreview?.(workerUrl);
10998
+ widgetShapeLivePreviewsRef?.requestReload?.(widgetId);
10859
10999
  } else {
10860
11000
  void ensureWidgetShapeVersionPreview(widgetId, version);
10861
11001
  }
@@ -10992,6 +11132,7 @@ Requirements:
10992
11132
  */
10993
11133
  let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
10994
11134
  let versionPromptLabelsByWidgetId: Record<string, Record<string, string>> = {};
11135
+ let versionTimelineByWidgetId: Record<string, Array<{ version: number; sha: string; prompt: string }>> = {};
10995
11136
  let viewingVersionByWidgetId: Record<string, number | null> = {};
10996
11137
  $: {
10997
11138
  void shapePreviewSourceByWidgetId;
@@ -11002,16 +11143,19 @@ Requirements:
11002
11143
  const nextHeads: Record<string, string | null> = {};
11003
11144
  const nextLabels: Record<string, Record<string, string>> = {};
11004
11145
  const nextPrompts: Record<string, Record<string, string>> = {};
11146
+ const nextTimeline: Record<string, Array<{ version: number; sha: string; prompt: string }>> = {};
11005
11147
  const nextViewing: Record<string, number | null> = {};
11006
11148
  for (const widgetId of openWidgetPanelIds) {
11007
11149
  nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
11008
11150
  nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
11009
11151
  nextPrompts[widgetId] = buildVersionPromptLabelsForPanel(widgetId);
11152
+ nextTimeline[widgetId] = buildVersionTimelineForPanel(widgetId);
11010
11153
  nextViewing[widgetId] = viewingVersionNumberForPanel(widgetId);
11011
11154
  }
11012
11155
  headCommitShaByWidgetId = nextHeads;
11013
11156
  versionLabelsByWidgetId = nextLabels;
11014
11157
  versionPromptLabelsByWidgetId = nextPrompts;
11158
+ versionTimelineByWidgetId = nextTimeline;
11015
11159
  viewingVersionByWidgetId = nextViewing;
11016
11160
  }
11017
11161
 
@@ -11049,6 +11193,20 @@ Requirements:
11049
11193
  return text || null;
11050
11194
  }
11051
11195
 
11196
+ function buildVersionTimelineForPanel(widgetId: string): Array<{ version: number; sha: string; prompt: string }> {
11197
+ const ps = getPanelDisplayState(widgetId);
11198
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
11199
+ const timeline: Array<{ version: number; sha: string; prompt: string }> = [];
11200
+ for (const record of history || []) {
11201
+ if (record?.version == null) continue;
11202
+ const sha = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
11203
+ const prompt = versionPromptKey(record?.prompt);
11204
+ if (!sha || !prompt) continue;
11205
+ timeline.push({ version: Number(record.version), sha, prompt });
11206
+ }
11207
+ return timeline;
11208
+ }
11209
+
11052
11210
  /** User prompt → "Version N", for turns whose commit line was never saved. */
11053
11211
  function buildVersionPromptLabelsForPanel(widgetId: string): Record<string, string> {
11054
11212
  const labels: Record<string, string> = {};
@@ -12521,15 +12679,24 @@ $: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previ
12521
12679
  const wsEventKey = `${wsReadyWidgetId}:${$dynamicWorkerPreviewStatus.previewUrl}:${$dynamicWorkerPreviewStatus.timestamp ?? ''}`;
12522
12680
  if (wsEventKey !== lastWsWorkerPreviewEventKey) {
12523
12681
  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));
12682
+ const selectedVersion = dropdownVersionByWidgetId[wsReadyWidgetId]
12683
+ || shapePreviewSourceByWidgetId[wsReadyWidgetId];
12684
+ const keepHistoricalSelection = !generatingWidgetIds[wsReadyWidgetId]
12685
+ && !!selectedVersion?.startsWith('v:')
12686
+ && !versionUsesSharedHeadPreview(wsReadyWidgetId, selectedVersion);
12687
+ if (keepHistoricalSelection) {
12688
+ console.log('[CreatorApp] Historical version stays selected; shared HEAD build ignored', wsReadyWidgetId.substring(0, 8));
12530
12689
  } else {
12531
- syncWidgetShapeLivePreviews();
12532
- widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12690
+ shapePreviewSourceByWidgetId = {
12691
+ ...shapePreviewSourceByWidgetId,
12692
+ [wsReadyWidgetId]: 'preview',
12693
+ };
12694
+ if (codegenCaptureBlocked()) {
12695
+ console.log('[CreatorApp] Defer canvas Worker preview reload until codegen finishes', wsReadyWidgetId.substring(0, 8));
12696
+ } else {
12697
+ syncWidgetShapeLivePreviews();
12698
+ widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
12699
+ }
12533
12700
  }
12534
12701
  }
12535
12702
  }
@@ -15934,6 +16101,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15934
16101
  widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
15935
16102
  versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
15936
16103
  versionLabelByPrompt={versionPromptLabelsByWidgetId[panelWidgetId] || {}}
16104
+ versionTimeline={versionTimelineByWidgetId[panelWidgetId] || []}
15937
16105
  viewingVersion={viewingVersionByWidgetId[panelWidgetId] ?? null}
15938
16106
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
15939
16107
  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.71",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",