@widgetic/creator 0.3.62 → 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;
@@ -7087,6 +7090,13 @@
7087
7090
  signal
7088
7091
  })
7089
7092
  .then((finalOp) => {
7093
+ if (!settled && !signal?.aborted && onMessage) {
7094
+ try {
7095
+ onMessage(finalOp as OperationStatus);
7096
+ } catch (callbackError) {
7097
+ console.warn('waitForOperation terminal callback error', callbackError);
7098
+ }
7099
+ }
7090
7100
  settle('resolve', finalOp as OperationStatus);
7091
7101
  })
7092
7102
  .catch((streamError) => {
@@ -8223,6 +8233,11 @@ Requirements:
8223
8233
  const responseData = response?.data || response;
8224
8234
  const operationId = responseData?.operationId;
8225
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
+
8226
8241
  if (!operationId) {
8227
8242
  patchCodegenUiState(generationWidgetId, {
8228
8243
  generateCodeStatus: '❌ Code generation started, but no operation ID was returned.',
@@ -8497,6 +8512,10 @@ Requirements:
8497
8512
  [generationWidgetId]: 'preview',
8498
8513
  };
8499
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);
8500
8519
  panelDisplayRevision += 1;
8501
8520
  if (!completedPreviewBuildErr) {
8502
8521
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
@@ -9311,10 +9330,43 @@ Requirements:
9311
9330
 
9312
9331
  function pushCodegenProgressToUi(op: OperationStatus, widgetId: string | null | undefined) {
9313
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.
9314
9335
  if (widgetId) {
9315
- patchCodegenUiState(widgetId, { generateCodeStatus: line });
9316
- }
9317
- 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);
9318
9370
  }
9319
9371
 
9320
9372
  function patchCodegenUiState(
@@ -14748,7 +14800,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
14748
14800
  {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
14749
14801
  {@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
14750
14802
  ? parseInt(shapeSource.slice(2), 10)
14751
- : 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)}
14752
14807
  {@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
14753
14808
  <!--
14754
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)
@@ -84,12 +84,12 @@ export function streamOperationUpdates(operationId, options) {
84
84
  reader = response.body.getReader();
85
85
  const decoder = new TextDecoder();
86
86
  let buffer = '';
87
+ // Start idle timer before any operation payload arrives.
87
88
  armWatchdog();
88
89
  while (true) {
89
90
  const { done, value } = await reader.read();
90
91
  if (done)
91
92
  break;
92
- armWatchdog();
93
93
  buffer += decoder.decode(value, { stream: true });
94
94
  // SSE frames are `data: {...}\n\n`; keepalive comment frames carry
95
95
  // no data line and are skipped.
@@ -107,6 +107,10 @@ export function streamOperationUpdates(operationId, options) {
107
107
  continue;
108
108
  try {
109
109
  const op = JSON.parse(dataLine);
110
+ // Only reset idle timer on real operation payloads — SSE
111
+ // `: keepalive` comment frames must not suppress polling fallback
112
+ // when the terminal `completed` event is lost behind a proxy.
113
+ armWatchdog();
110
114
  onMessage?.(op);
111
115
  const status = String(op?.status || '').toLowerCase();
112
116
  if (TERMINAL_STATUSES.has(status)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.62",
3
+ "version": "0.3.64",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",