@widgetic/creator 0.3.63 → 0.3.64

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.
@@ -494,7 +494,10 @@
494
494
  let WidgeticCanvas: any;
495
495
  let canvasRef: any;
496
496
  /** HTML iframe overlays over Fabric WidgetShapes (W3). */
497
- let widgetShapeLivePreviewsRef: { sync?: () => void } | null = null;
497
+ let widgetShapeLivePreviewsRef: {
498
+ sync?: () => void;
499
+ requestReload?: (widgetId: string) => void;
500
+ } | null = null;
498
501
  /** Selected WidgetShape id for chrome highlight (from Fabric selection). */
499
502
  let canvasSelectedWidgetShapeId: string | null = null;
500
503
  let widgetDetailsModulePromise: Promise<any> | null = null;
@@ -8230,6 +8233,11 @@ Requirements:
8230
8233
  const responseData = response?.data || response;
8231
8234
  const operationId = responseData?.operationId;
8232
8235
 
8236
+ // The Worker serves rebuilt code at the same preview URL, so the panel
8237
+ // must know when the current generation started — hasLiveWorkerPreview()
8238
+ // only counts frames loaded after this moment as "fresh".
8239
+ getWidgetDetailsRef(generationWidgetId)?.markCodegenStarted?.();
8240
+
8233
8241
  if (!operationId) {
8234
8242
  patchCodegenUiState(generationWidgetId, {
8235
8243
  generateCodeStatus: '❌ Code generation started, but no operation ID was returned.',
@@ -8504,6 +8512,10 @@ Requirements:
8504
8512
  [generationWidgetId]: 'preview',
8505
8513
  };
8506
8514
  syncWidgetShapeLivePreviews();
8515
+ // The Worker serves the rebuilt code at the SAME /preview URL — sync()
8516
+ // only reloads when src changes, so force a cache-busted iframe reload
8517
+ // on the canvas shape overlay or it keeps showing the previous version.
8518
+ widgetShapeLivePreviewsRef?.requestReload?.(generationWidgetId);
8507
8519
  panelDisplayRevision += 1;
8508
8520
  if (!completedPreviewBuildErr) {
8509
8521
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
@@ -9318,10 +9330,43 @@ Requirements:
9318
9330
 
9319
9331
  function pushCodegenProgressToUi(op: OperationStatus, widgetId: string | null | undefined) {
9320
9332
  const line = formatCodegenProgressStatus(op);
9333
+ // SSE progress frames can arrive many times per second while the LLM
9334
+ // streams. Each one re-renders every open panel — coalesce them.
9321
9335
  if (widgetId) {
9322
- patchCodegenUiState(widgetId, { generateCodeStatus: line });
9323
- }
9324
- getChatMethodsForWidget(widgetId)?.updateLatestCodegenStatusMessage?.(line);
9336
+ scheduleCodegenStatusPush(widgetId, line);
9337
+ }
9338
+ scheduleCodegenChatStatusPush(widgetId, line);
9339
+ }
9340
+
9341
+ const CODEGEN_STATUS_THROTTLE_MS = 400;
9342
+ const codegenStatusTimers: Record<string, ReturnType<typeof setTimeout>> = {};
9343
+ const codegenStatusPendingLine: Record<string, string> = {};
9344
+ const codegenChatStatusTimers: Record<string, ReturnType<typeof setTimeout>> = {};
9345
+ const codegenChatStatusPendingLine: Record<string, string> = {};
9346
+
9347
+ /** At most one panel status write per widget per interval; trailing line always lands. */
9348
+ function scheduleCodegenStatusPush(widgetId: string, line: string): void {
9349
+ codegenStatusPendingLine[widgetId] = line;
9350
+ if (codegenStatusTimers[widgetId]) return;
9351
+ codegenStatusTimers[widgetId] = setTimeout(() => {
9352
+ delete codegenStatusTimers[widgetId];
9353
+ const pending = codegenStatusPendingLine[widgetId];
9354
+ delete codegenStatusPendingLine[widgetId];
9355
+ if (pending) patchCodegenUiState(widgetId, { generateCodeStatus: pending });
9356
+ }, CODEGEN_STATUS_THROTTLE_MS);
9357
+ }
9358
+
9359
+ /** Same throttle for the in-chat status bubble (updateLatestCodegenStatusMessage). */
9360
+ function scheduleCodegenChatStatusPush(widgetId: string | null | undefined, line: string): void {
9361
+ const key = widgetId || '_focused';
9362
+ codegenChatStatusPendingLine[key] = line;
9363
+ if (codegenChatStatusTimers[key]) return;
9364
+ codegenChatStatusTimers[key] = setTimeout(() => {
9365
+ delete codegenChatStatusTimers[key];
9366
+ const pending = codegenChatStatusPendingLine[key];
9367
+ delete codegenChatStatusPendingLine[key];
9368
+ if (pending) getChatMethodsForWidget(widgetId)?.updateLatestCodegenStatusMessage?.(pending);
9369
+ }, CODEGEN_STATUS_THROTTLE_MS);
9325
9370
  }
9326
9371
 
9327
9372
  function patchCodegenUiState(
@@ -14755,7 +14800,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14755
14800
  {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
14756
14801
  {@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
14757
14802
  ? parseInt(shapeSource.slice(2), 10)
14758
- : panelPublishedVersion}
14803
+ // A 'preview' source (Worker HEAD, unpublished) is not a published
14804
+ // version — falling back to panelPublishedVersion produced `vnull`
14805
+ // right after generation. The newest draft IS this HEAD's version.
14806
+ : (newestDraft ? newestDraft.version : panelPublishedVersion)}
14759
14807
  {@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
14760
14808
  <!--
14761
14809
  A version is a Draft until it is published. The history is one
@@ -801,7 +801,20 @@
801
801
  // console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
802
802
  }
803
803
 
804
- /** True when the iframe is already showing this widget's Worker preview (not about:blank / error). */
804
+ /** Timestamp of generation start — set by the parent when the operation begins. */
805
+ let codegenStartedAt: number | null = null;
806
+ /** Parent calls this when a generation starts for THIS widget's panel. */
807
+ export function markCodegenStarted(): void {
808
+ codegenStartedAt = Date.now();
809
+ }
810
+
811
+ /**
812
+ * True when the iframe is already showing this widget's Worker preview AND was
813
+ * reloaded after the current generation started. The Worker serves new code at
814
+ * the same `/preview/{id}` URL, so URL equality alone cannot tell a stale frame
815
+ * (pre-codegen) from a fresh one — that mismatch left the old version on screen
816
+ * after generation.
817
+ */
805
818
  export function hasLiveWorkerPreview(): boolean {
806
819
  if (!selectedWidgetId) return false;
807
820
  if (!iframeSrc || iframeSrc === 'about:blank') return false;
@@ -809,7 +822,10 @@
809
822
  const workerUrl = dynamicWorkerPreviewUrl || buildWorkerPreviewUrl(selectedWidgetId);
810
823
  const currentBase = normalizePreviewBaseUrl(iframeSrc);
811
824
  const workerBase = normalizePreviewBaseUrl(toPreviewFetchUrl(workerUrl));
812
- return !!currentBase && !!workerBase && currentBase === workerBase;
825
+ if (!currentBase || !workerBase || currentBase !== workerBase) return false;
826
+ // During/after a generation, only a frame (re)loaded after it started counts.
827
+ if (codegenStartedAt !== null && lastPreviewIframeLoadAt < codegenStartedAt) return false;
828
+ return true;
813
829
  }
814
830
 
815
831
  // Preview resize
@@ -108,6 +108,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
108
108
  showPublishedArtifact?: (url: string | null) => void;
109
109
  finishCompositionHydration?: () => void;
110
110
  markCodegenPreviewFresh?: () => void;
111
+ markCodegenStarted?: () => void;
111
112
  hasLiveWorkerPreview?: () => boolean;
112
113
  getLatestRuntimeLogs?: (maxLines?: number) => string[];
113
114
  resumeDeferredPreviewLoad?: () => Promise<void>;
@@ -204,6 +205,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
204
205
  showPublishedArtifact: (url: string | null) => void;
205
206
  finishCompositionHydration: () => void;
206
207
  markCodegenPreviewFresh: () => void;
208
+ markCodegenStarted: () => void;
207
209
  hasLiveWorkerPreview: () => boolean;
208
210
  getLatestRuntimeLogs: (maxLines?: number) => string[];
209
211
  resumeDeferredPreviewLoad: () => Promise<void>;
@@ -171,6 +171,19 @@
171
171
  }
172
172
  pendingRefreshTokens.delete(widgetId);
173
173
  }
174
+
175
+ /**
176
+ * Parent-driven forced reload for widgets whose Worker preview content changed
177
+ * at the same URL (codegen rebuild). `sync()` only reloads when `src` changes,
178
+ * so without a cache-bust token the overlay keeps showing pre-codegen content.
179
+ * Bumps the reload token → `{#key}` remounts the iframe with a fresh `_t`.
180
+ */
181
+ export function requestReload(widgetId: string): void {
182
+ const token = Math.max(Date.now(), (reloadTokens[widgetId] || 0) + 1);
183
+ reloadTokens = { ...reloadTokens, [widgetId]: token };
184
+ iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
185
+ }
186
+
174
187
  /** Which shape chrome version menu is open. */
175
188
  let openVersionMenuWidgetId = $state<string | null>(null);
176
189
  /** Hide Fabric placeholder flash — show loader until iframe fires load. */
@@ -90,6 +90,7 @@ interface Props {
90
90
  resolvePublishDisabledReason?: ((widgetId: string, version: number) => string | null) | null;
91
91
  }
92
92
  declare const WidgetShapeLivePreviews: import("svelte").Component<Props, {
93
+ requestReload: (widgetId: string) => void;
93
94
  sync: () => void;
94
95
  }, "">;
95
96
  type WidgetShapeLivePreviews = ReturnType<typeof WidgetShapeLivePreviews>;
@@ -39,6 +39,30 @@ export const codeGenerationStatus = writable({
39
39
  timestamp: null
40
40
  });
41
41
  export const agentHarnessOutput = writable(null);
42
+ /**
43
+ * Throttle for high-frequency agent harness frames. The stream used to emit one
44
+ * WS event per LLM token (plus one SSE progress frame every 10 tokens), and each
45
+ * frame re-rendered every open panel — with `structuredClone` on schemas per
46
+ * render. That saturated the browser main thread and froze the whole site during
47
+ * generation. Frames now coalesce into one store write per interval; the final
48
+ * frame always flushes so the last line is never lost.
49
+ */
50
+ const AGENT_HARNESS_FLUSH_MS = 300;
51
+ let agentHarnessPending = null;
52
+ let agentHarnessFlushTimer = null;
53
+ function flushAgentHarnessOutput() {
54
+ agentHarnessFlushTimer = null;
55
+ if (!agentHarnessPending)
56
+ return;
57
+ agentHarnessOutput.set(agentHarnessPending);
58
+ agentHarnessPending = null;
59
+ }
60
+ function pushAgentHarnessOutput(output) {
61
+ agentHarnessPending = { output, timestamp: new Date().toISOString() };
62
+ if (!agentHarnessFlushTimer) {
63
+ agentHarnessFlushTimer = setTimeout(flushAgentHarnessOutput, AGENT_HARNESS_FLUSH_MS);
64
+ }
65
+ }
42
66
  // =============================================================================
43
67
  // Socket.IO Connection
44
68
  // =============================================================================
@@ -204,10 +228,8 @@ function setupSocketListeners(sock) {
204
228
  return;
205
229
  if (showLogs)
206
230
  console.log('[WsStore] Agent harness output:', output.slice(0, 120));
207
- agentHarnessOutput.set({
208
- output,
209
- timestamp: new Date().toISOString()
210
- });
231
+ // Coalesced write — see AGENT_HARNESS_FLUSH_MS above.
232
+ pushAgentHarnessOutput(output);
211
233
  });
212
234
  sock.on('dynamic_worker_preview_ready', (data) => {
213
235
  if (showLogs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.63",
3
+ "version": "0.3.64",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",