@widgetic/creator 0.3.72 → 0.3.74

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.
Files changed (33) hide show
  1. package/dist/CreatorApp.svelte +821 -73
  2. package/dist/components/BrandKitPanel.svelte +556 -0
  3. package/dist/components/BrandKitPanel.svelte.d.ts +32 -0
  4. package/dist/components/DesignQuickEdit.svelte +218 -0
  5. package/dist/components/DesignQuickEdit.svelte.d.ts +33 -0
  6. package/dist/components/FileUrlSourceControl.svelte +69 -0
  7. package/dist/components/FileUrlSourceControl.svelte.d.ts +15 -0
  8. package/dist/components/VersionDropdownRow.svelte +86 -1
  9. package/dist/components/VersionDropdownRow.svelte.d.ts +10 -0
  10. package/dist/components/WidgetDetails.svelte +19 -2
  11. package/dist/components/WidgetShapeLivePreviews.svelte +8 -3
  12. package/dist/retiredSurface.test.d.ts +1 -0
  13. package/dist/retiredSurface.test.js +113 -0
  14. package/dist/stores/websocketStore.d.ts +3 -0
  15. package/dist/stores/websocketStore.js +4 -0
  16. package/dist/utils/canvasShapeSnapClient.d.ts +27 -0
  17. package/dist/utils/canvasShapeSnapClient.js +67 -0
  18. package/dist/utils/designIntentJevClient.d.ts +32 -0
  19. package/dist/utils/designIntentJevClient.js +51 -0
  20. package/dist/utils/designIntentResolver.d.ts +107 -0
  21. package/dist/utils/designIntentResolver.js +509 -0
  22. package/dist/utils/designIntentResolver.test.d.ts +1 -0
  23. package/dist/utils/designIntentResolver.test.js +246 -0
  24. package/dist/utils/designIntentTelemetry.d.ts +176 -0
  25. package/dist/utils/designIntentTelemetry.js +474 -0
  26. package/dist/utils/designIntentTelemetry.test.d.ts +1 -0
  27. package/dist/utils/designIntentTelemetry.test.js +385 -0
  28. package/dist/utils/designQuickEditValues.d.ts +51 -0
  29. package/dist/utils/designQuickEditValues.js +77 -0
  30. package/dist/utils/designQuickEditValues.test.d.ts +1 -0
  31. package/dist/utils/designQuickEditValues.test.js +100 -0
  32. package/dist/utils/pinScrollToBottom.js +9 -2
  33. package/package.json +2 -2
@@ -25,7 +25,9 @@
25
25
  MessagesApi,
26
26
  CompositionsApi,
27
27
  UploadsApi,
28
- ResponseError
28
+ UsersApi,
29
+ ResponseError,
30
+ isRetiredSurfaceError
29
31
  } from '@widgetic/api-sdk';
30
32
  import type {
31
33
  Middleware,
@@ -33,6 +35,7 @@
33
35
  AgentModelsGet200ResponseModelsInner
34
36
  } from '@widgetic/api-sdk';
35
37
  import { parseVersionPreviewError } from './utils/versionPreviewError.js';
38
+ import BrandKitPanel from './components/BrandKitPanel.svelte';
36
39
 
37
40
  /**
38
41
  * Idempotency middleware (Phase 2 hardening): the gateway rejects
@@ -345,6 +348,9 @@
345
348
  let messagesClient: MessagesApi;
346
349
  let compositionsClient: CompositionsApi;
347
350
  let uploadsClient: UploadsApi;
351
+ let usersClient: UsersApi;
352
+ /** Credits, and days left while the account is still on the free trial. */
353
+ let trialCounter: { credits: number; days: number | null; trialEnded: boolean } | null = null;
348
354
 
349
355
  // Track which token the authenticated clients were built with
350
356
  let authenticatedClientsToken: string | null = null;
@@ -390,6 +396,9 @@
390
396
  uploadsClient = apiBasePath
391
397
  ? new UploadsApi({ basePath: apiBasePath, accessToken: token, middleware: [idempotencyKeyMiddleware] })
392
398
  : new UploadsApi({ accessToken: token, middleware: [idempotencyKeyMiddleware] });
399
+ usersClient = apiBasePath
400
+ ? new UsersApi({ basePath: apiBasePath, accessToken: token })
401
+ : new UsersApi({ accessToken: token });
393
402
 
394
403
  authenticatedClientsToken = token;
395
404
  try {
@@ -398,6 +407,41 @@
398
407
  /* websocket store may not be ready yet */
399
408
  }
400
409
  syncFileBrowserUploadConfigForCreator();
410
+ void refreshTrialCounter();
411
+ }
412
+
413
+ /** Credits left and days left, from the same billing status the publish gate uses. */
414
+ async function refreshTrialCounter() {
415
+ if (!usersClient) return;
416
+ try {
417
+ const accepted = await usersClient.usersMeBillingGetRaw();
418
+ const body = await accepted.raw.json();
419
+ const data = (body && typeof body === 'object' && 'data' in body ? body.data : body) as {
420
+ trialActive?: boolean;
421
+ creditsAvailable?: number;
422
+ creditBalance?: number;
423
+ trialDaysLeft?: number | null;
424
+ subscription?: { plan_type?: string | null; planType?: string | null } | null;
425
+ } | null;
426
+ if (!data || (data.creditsAvailable == null && data.creditBalance == null && !data.trialActive)) {
427
+ trialCounter = null;
428
+ return;
429
+ }
430
+ const credits = Number(data.creditsAvailable ?? data.creditBalance ?? 0);
431
+ const planType = String(data.subscription?.plan_type || data.subscription?.planType || '').toLowerCase();
432
+ const paidPlan = planType === 'solo' || planType === 'teams' || planType === 'team';
433
+ const days = data.trialDaysLeft == null ? null : Number(data.trialDaysLeft);
434
+ trialCounter = {
435
+ credits: Number.isFinite(credits) ? Math.max(0, Math.round(credits)) : 0,
436
+ days: paidPlan || days == null || !Number.isFinite(days) ? null : Math.max(0, Math.round(days)),
437
+ trialEnded: !paidPlan && days === 0,
438
+ };
439
+ if (typeof window !== 'undefined') {
440
+ window.dispatchEvent(new CustomEvent('widgetic:trial-counter'));
441
+ }
442
+ } catch (error) {
443
+ console.warn('[Trial] Could not load the credit counter', error);
444
+ }
401
445
  }
402
446
 
403
447
  function applySiteSessionToCreator(): boolean {
@@ -476,9 +520,35 @@
476
520
  import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
477
521
  import HoverTooltip from './components/HoverTooltip.svelte';
478
522
  import VersionDropdownRow from './components/VersionDropdownRow.svelte';
523
+ // Type-only: erased at compile time, so the component's code is still
524
+ // dynamically imported below — this line exists so the ref types below can
525
+ // derive from the component's REAL class instead of the `any` a dynamic
526
+ // `import('./components/WidgetDetails.svelte')` resolves to (its module
527
+ // type carries no instance shape, which silently re-opens the any-ref hole
528
+ // this block exists to close).
529
+ import type WidgetDetailsComponent from './components/WidgetDetails.svelte';
479
530
 
480
531
  // WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
481
- let WidgetDetails: any;
532
+ //
533
+ // The component value and its module promise are **derived types**, not
534
+ // hand-written mirrors — the same contract canvasRef and
535
+ // widgetShapeLivePreviewsRef below carry. WidgetDetails.svelte is classic
536
+ // Svelte 4, so the instance type is `InstanceType` of its component class
537
+ // (imported type-only above — see the note there for why the dynamic import
538
+ // cannot supply this type). `WithoutIndexKeys` (declared with the canvas ref
539
+ // below) strips the string index signature the base plumbing inherits,
540
+ // which the falsification probe for this very file proved lets unknown-member
541
+ // calls compile. A renamed or removed `export function` on
542
+ // WidgetDetails.svelte is now a compile error at every stale CreatorApp call
543
+ // site instead of a silent runtime `undefined`; a new export appears here
544
+ // without any edit to this file. The values stay
545
+ // `| null`/`| undefined` because the module loads lazily — the
546
+ // optionality lives at the assignment, not in the type.
547
+ type WidgetDetailsInstance = WithoutIndexKeys<InstanceType<typeof WidgetDetailsComponent>>;
548
+ // The lazily-assigned value is the COMPONENT (constructor) — what
549
+ // `<svelte:component this={…}>` consumes — while the refs and
550
+ // `getWidgetDetailsRef` hand out INSTANCES. Two shapes, two types.
551
+ let WidgetDetails: typeof WidgetDetailsComponent | undefined = undefined;
482
552
  let widgetDetailsRefs: Record<string, any> = {};
483
553
  let openWidgetPanelIds: string[] = [];
484
554
  /** Stable {#each} keys so draft → real widget does not remount Widget Details. */
@@ -495,27 +565,76 @@
495
565
  // WidgetConversionPayload lives on the canvas root export (re-exported from
496
566
  // ./canvas); the components barrel only carries the input controls. Type-only
497
567
  // import, so the 10K-line Canvas.svelte still never loads.
498
- import type { WidgetConversionPayload } from '@widgetic/canvas';
568
+ import type {
569
+ WidgetConversionPayload,
570
+ StrokeRecognitionRequest,
571
+ StrokeRecognitionResult
572
+ } from '@widgetic/canvas';
499
573
 
500
574
  // Chat module for code generation conversation UI
501
575
  import { Chat, ContextType } from '@widgetic/chat';
502
576
  import type { ClarificationSuggestion } from '@widgetic/chat';
577
+ import DesignQuickEdit from './components/DesignQuickEdit.svelte';
578
+ import { collectDesignProperties, resolveDesignIntent, toDesignControlRequest, type DesignControlRequest } from './utils/designIntentResolver';
579
+ import { isDesignCardAttached } from './utils/designQuickEditValues';
580
+ import { resolveDesignIntentViaJev } from './utils/designIntentJevClient';
581
+ import { recognizeStrokeViaJev } from './utils/canvasShapeSnapClient';
582
+ import {
583
+ recordDesignIntentTelemetry,
584
+ recordDesignIntentJevOutcome,
585
+ isJevCandidateBucket,
586
+ type DesignIntentClassification
587
+ } from './utils/designIntentTelemetry';
503
588
 
504
589
  // Eagerly initiate canvas chunk fetching at script evaluation (parallel with DOM parsing)
505
590
  const canvasModulePromise = typeof window !== 'undefined' ? import('@widgetic/canvas') : null;
506
591
 
507
- let WidgeticCanvas: any;
508
- let canvasRef: any;
509
- /** HTML iframe overlays over Fabric WidgetShapes (W3). */
510
- let widgetShapeLivePreviewsRef: {
511
- sync?: () => void;
512
- requestReload?: (widgetId: string) => void;
513
- } | null = null;
592
+ let WidgeticCanvas: typeof import('@widgetic/canvas').Canvas | null = null;
593
+ /**
594
+ * Pattern reference: `__docs/15-svelte4-component-ref-typing.md`.
595
+ *
596
+ * Drops the string-index keys a Svelte 4 component's instance type inherits
597
+ * from its prop plumbing, keeping only the class's own members. Without
598
+ * this, `canvasRef.anythingAtAll` type-checks and a mistyped method name
599
+ * slips through as a silent runtime `undefined`.
600
+ */
601
+ type WithoutIndexKeys<T> = { [K in keyof T as string extends K ? never : K]: T[K] };
602
+ /**
603
+ * The canvas component instance, **derived from the component's own type**
604
+ * (`InstanceType` of the `@widgetic/canvas` `Canvas` class), not a
605
+ * hand-written mirror. This component is classic Svelte 4 — a
606
+ * `SvelteComponentTyped` subclass whose instance members are compiled
607
+ * `export function` getters — so the instance type is `InstanceType<…>`
608
+ * (the Svelte 5 call-signature components elsewhere in this file use
609
+ * `ReturnType<…>`; same idea, different component authoring era). A new
610
+ * `export function` on Canvas.svelte appears here after a dist rebuild,
611
+ * and a renamed or removed one is a compile error at the stale call site
612
+ * instead of a silent `undefined` at runtime. The ref is still `| null`
613
+ * and the calls keep their `?.` because the canvas mounts lazily — the
614
+ * optionality lives at the binding, not in the type.
615
+ */
616
+ let canvasRef: WithoutIndexKeys<InstanceType<typeof import('@widgetic/canvas').Canvas>> | null = null;
617
+ /**
618
+ * HTML iframe overlays over Fabric WidgetShapes (W3).
619
+ *
620
+ * Typed as the component's **own instance type**, not a hand-written
621
+ * mirror: a Svelte 5 component is a call-signature type whose return is
622
+ * the exported instance surface, so `ReturnType` of the imported component
623
+ * is exactly what `bind:this` delivers. The previous literal shape
624
+ * (`sync`/`requestReload`/`cancelReloads`/`setSelectedVersion`, all
625
+ * optional) had to be edited in lockstep with the component's `export
626
+ * function` block — a new export left every call site type-unchecked
627
+ * against it, a signature change drifted silently. Derived, both
628
+ * directions are compile errors at the stale site instead. The ref is
629
+ * still `| null` and the calls keep their `?.` because the canvas mounts
630
+ * lazily — the optionality lives at the binding, not in the type.
631
+ */
632
+ let widgetShapeLivePreviewsRef: ReturnType<typeof WidgetShapeLivePreviews> | null = null;
514
633
  /** Selected WidgetShape id for chrome highlight (from Fabric selection). */
515
634
  let canvasSelectedWidgetShapeId: string | null = null;
516
- let widgetDetailsModulePromise: Promise<any> | null = null;
635
+ let widgetDetailsModulePromise: Promise<typeof WidgetDetailsComponent> | null = null;
517
636
 
518
- function loadWidgetDetailsModule(): Promise<any> {
637
+ function loadWidgetDetailsModule(): Promise<typeof WidgetDetailsComponent> {
519
638
  if (!widgetDetailsModulePromise) {
520
639
  widgetDetailsModulePromise = import('./components/WidgetDetails.svelte').then((wdModule) => {
521
640
  WidgetDetails = wdModule.default;
@@ -932,6 +1051,7 @@
932
1051
  let chatMethods: {
933
1052
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
934
1053
  addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: ClarificationSuggestion[]) => void;
1054
+ addDesignControlsMessage?: (content: string, designControls: DesignControlRequest) => void;
935
1055
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
936
1056
  getConversationId: () => string | null;
937
1057
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -970,6 +1090,45 @@
970
1090
  // Populated via Chat's onConversationChange callback and passed to generateCode()
971
1091
  // so the backend can load multi-turn LLM history (Aider-style incremental edits).
972
1092
  let currentChatConversationId: string | null = null;
1093
+ let brandKitEnabled = false;
1094
+ let widgetLanguage = 'svelte';
1095
+ let librariesEnabled = false;
1096
+ let brandPrimary = '#314F2F';
1097
+ let brandSecondary = '#7BA668';
1098
+ let brandTitle = '#0F2010';
1099
+ let brandSub = '#5C6B58';
1100
+ let brandFonts = ['Inter'];
1101
+ let brandKitLibraries: string[] = [];
1102
+ let libraryGitUrl = '';
1103
+ let libraryFileName = '';
1104
+ let libraryUploads: { name: string; content: string; encoding: string }[] = [];
1105
+ let fontUploads: { name: string; content: string; encoding: string }[] = [];
1106
+
1107
+ function brandKitForRequest() {
1108
+ const kit: {
1109
+ colors?: { primary: string; secondary: string; title: string; sub: string };
1110
+ fonts?: string[];
1111
+ libraries?: string[];
1112
+ uploads?: { name: string; content: string; encoding: string }[];
1113
+ fontFiles?: { name: string; content: string; encoding: string }[];
1114
+ } = {};
1115
+ if (brandKitEnabled) {
1116
+ kit.colors = {
1117
+ primary: brandPrimary,
1118
+ secondary: brandSecondary,
1119
+ title: brandTitle,
1120
+ sub: brandSub,
1121
+ };
1122
+ const families = brandFonts.map((family) => family.trim()).filter(Boolean).slice(0, 3);
1123
+ if (families.length > 0) kit.fonts = families;
1124
+ }
1125
+ // Preset libraries are a debug test. An empty list is an explicit ban.
1126
+ if (librariesEnabled && debugMode) kit.libraries = [...brandKitLibraries];
1127
+ if (brandKitEnabled && fontUploads.length > 0) kit.fontFiles = fontUploads.map((file) => ({ ...file }));
1128
+ if (libraryUploads.length > 0) kit.uploads = libraryUploads.map((file) => ({ ...file }));
1129
+ if (!kit.colors && !kit.fonts && !kit.libraries && !kit.uploads && !kit.fontFiles) return undefined;
1130
+ return kit;
1131
+ }
973
1132
 
974
1133
  /** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
975
1134
  function promptImpliesVisionReference(prompt: string): boolean {
@@ -1143,6 +1302,40 @@
1143
1302
  }
1144
1303
  });
1145
1304
 
1305
+ // Shadow-mode measurement: bucket every send by how it could be resolved
1306
+ // (literal/arithmetic, bounded-set semantic, free text, new property, or
1307
+ // not a design ask). Records + logs only — it never changes behavior.
1308
+ // The bucket also decides whether the Jev fallback is worth a dial.
1309
+ // Inspect from the console: `__widgeticDesignIntentTelemetry.report()`.
1310
+ // The whole classification is kept, not just the bucket: the Jev dial below is
1311
+ // attributed to the *ask shape* inside that bucket (a property was named, or
1312
+ // only a value was), which is what makes its fall-through reasons actionable.
1313
+ let designIntentClassification: DesignIntentClassification | null = null;
1314
+ try {
1315
+ designIntentClassification = recordDesignIntentTelemetry(
1316
+ content,
1317
+ getPanelDisplayState(codegenWidgetId).compositionDesignSchema
1318
+ );
1319
+ } catch {
1320
+ /* telemetry must never break a send */
1321
+ }
1322
+
1323
+ // Deterministic design-tweak fast path: explicit values and arithmetic
1324
+ // resolve against the loaded design schema in code, so an unambiguous
1325
+ // property change never pays for a 30–300s codegen turn.
1326
+ if (!attachments?.length && !sendOptions?.skipIntentGate) {
1327
+ if (tryApplyDesignIntent(codegenWidgetId, content)) return;
1328
+ // Jev takes over ONLY the bounded-set-semantic bucket — the residual
1329
+ // where the property or value needs judgment but the value space is
1330
+ // closed. Free text and new properties can only fall through (Jev cannot
1331
+ // generate) and a non-design ask is not ours at all, so none of them is
1332
+ // ever dialed.
1333
+ const dialJev = isJevCandidateBucket(designIntentClassification?.bucket);
1334
+ if (dialJev && (await tryApplyDesignIntentViaJev(codegenWidgetId, content, designIntentClassification))) {
1335
+ return;
1336
+ }
1337
+ }
1338
+
1146
1339
  const effectivePromptImages =
1147
1340
  panelPromptImages.length > 0
1148
1341
  ? panelPromptImages
@@ -2126,6 +2319,12 @@
2126
2319
  // Suggestion chips from a persisted clarification card — restored
2127
2320
  // on reload exactly as they were served.
2128
2321
  suggestions: Array.isArray(m.suggestions) && m.suggestions.length > 0 ? m.suggestions : undefined,
2322
+ // The inline design-control card this turn carried, restored on
2323
+ // reload so the reply keeps its controls. The server has already
2324
+ // normalized the payload to a renderable shape (see the messages
2325
+ // service); `compositionId` on it decides whether the card can still
2326
+ // read this composition's live values.
2327
+ designControls: m.design_controls ?? m.designControls ?? undefined,
2129
2328
  isCommit: m.is_commit || m.isCommit || false,
2130
2329
  // What the turn shipped, in the commit's own words — persisted beside
2131
2330
  // is_commit by the codegen pipeline and rendered on the turn's commit
@@ -2305,6 +2504,10 @@
2305
2504
  }>,
2306
2505
  suggestions?: ClarificationSuggestion[],
2307
2506
  persistWidgetId?: string | null,
2507
+ /** The inline design-control card this reply carries, when a directed
2508
+ * design tweak was applied with no codegen. Persisted with the reply so
2509
+ * the card survives a reload and re-attaches to its composition. */
2510
+ designControls?: DesignControlRequest | null,
2308
2511
  ): Promise<any> {
2309
2512
  try {
2310
2513
  // Draft convert panels hold a client-only conversation until the widget
@@ -2342,6 +2545,9 @@
2342
2545
  // Clarification-card suggestions ride the same write as the reply
2343
2546
  // they belong to, so a reload re-renders the card with its chips.
2344
2547
  ...(suggestions && suggestions.length > 0 ? { suggestions } : {}),
2548
+ // Same for the inline design-control card: one write, so a reload
2549
+ // re-renders the reply and its controls — not half a turn.
2550
+ ...(designControls ? { design_controls: designControls } : {}),
2345
2551
  };
2346
2552
  const persistMessage = async (payload: typeof createPayload) => {
2347
2553
  const response = await messagesClient.createMessageRaw({
@@ -2559,6 +2765,10 @@
2559
2765
  // API returns camelCase (via responseTransformMiddleware), fallback to snake_case
2560
2766
  commitSha?: string | null;
2561
2767
  commit_sha?: string | null;
2768
+ /** What the change shipped, in the pipeline's own words — the same title
2769
+ * the chat turn's commit chrome shows. Null on pre-title versions. */
2770
+ commitTitle?: string | null;
2771
+ commit_title?: string | null;
2562
2772
  artifactUrl?: string;
2563
2773
  artifact_url?: string;
2564
2774
  /**
@@ -2583,6 +2793,8 @@
2583
2793
  let isHistoryOpen = false;
2584
2794
  let isLoadingHistory = false;
2585
2795
  let isSettingLiveVersion: number | null = null; // track which version is being set live
2796
+ /** The version whose title rename is in flight, as `widgetId:version`. */
2797
+ let renamingTitleFor: string | null = null;
2586
2798
  let liveHistoryPanelTop = 0;
2587
2799
  let liveHistoryPanelRight = 0;
2588
2800
  /** True once publish history fetch completed for the current widget (or widget was never published). */
@@ -2687,8 +2899,8 @@
2687
2899
  // COMPOSITION EDITOR STATE
2688
2900
  // =============================================================================
2689
2901
  let compositionEditorOpen = false;
2690
- let compositionDesignSchema: any = null;
2691
- let compositionContentSchema: any = null;
2902
+ let compositionDesignSchema: DesignSchemaView | null = null;
2903
+ let compositionContentSchema: ContentSchemaView | null = null;
2692
2904
  let compositionSchemasLoading = false;
2693
2905
  let compositionSchemasError: string | null = null;
2694
2906
 
@@ -2722,6 +2934,9 @@
2722
2934
  }
2723
2935
 
2724
2936
  function handleChatMessagesResize(widgetId: string, deltaPx: number): void {
2937
+ // Sub-pixel footer jitter was rewriting the chat height every frame and
2938
+ // re-rendering the whole creator while code generation streamed.
2939
+ if (!Number.isFinite(deltaPx) || Math.abs(deltaPx) < 2) return;
2725
2940
  const measured = document
2726
2941
  .querySelector(`[data-widget-chat-height="${widgetId}"]`)
2727
2942
  ?.getBoundingClientRect().height;
@@ -2729,8 +2944,8 @@
2729
2944
  measured && measured > 0
2730
2945
  ? measured
2731
2946
  : (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
2732
- const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
2733
- console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
2947
+ const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, Math.round(current + deltaPx)));
2948
+ if (Math.abs(next - current) < 2) return;
2734
2949
  patchPanelSnapshot(widgetId, { chatContainerHeight: next });
2735
2950
  }
2736
2951
 
@@ -2773,19 +2988,77 @@
2773
2988
  });
2774
2989
  }
2775
2990
 
2776
- function normalizeDesignSchemaFromApiResponse(designRes: unknown): any {
2991
+ /** A component property as the design schema carries it. Field set is open —
2992
+ * consumers read known keys and every schema shape is producer-defined — but
2993
+ * the keys the editor itself reads are named. */
2994
+ interface DesignSchemaPropertyView {
2995
+ id?: string;
2996
+ name?: string;
2997
+ description?: string;
2998
+ inputController?: { type?: string } & Record<string, unknown>;
2999
+ [key: string]: unknown;
3000
+ }
3001
+
3002
+ /** One component group of the design schema (the `componentGroups` array's
3003
+ * members). Open to extra keys; the editor reads `name` and `components`. */
3004
+ interface DesignSchemaGroupView {
3005
+ name?: string;
3006
+ components?: Array<{
3007
+ properties?: DesignSchemaPropertyView[];
3008
+ [key: string]: unknown;
3009
+ }>;
3010
+ [key: string]: unknown;
3011
+ }
3012
+
3013
+ /**
3014
+ * The design schema as this file's pipeline actually carries it: after
3015
+ * `normalizeDesignSchemaFromApiResponse` it is **either a group array (the
3016
+ * merged single-group form) or null** — the envelope variants
3017
+ * (`componentGroups` on an object, legacy `theme`-only payloads) are
3018
+ * collapsed upstream of this type. The API's generated model
3019
+ * (`GetCompositionDesignSchema200Response`) types `componentGroups` only as
3020
+ * `Array<object>`, so these views encode the shape the normalizer
3021
+ * guarantees, not the shape the OpenAPI spec guesses.
3022
+ */
3023
+ type DesignSchemaView = DesignSchemaGroupView[];
3024
+
3025
+ /** One content-item entry of the content schema (the `contentItems` array's
3026
+ * members). `id` and `properties` are what the merge/preview helpers read. */
3027
+ interface ContentSchemaItemView {
3028
+ id?: string;
3029
+ properties?: unknown[];
3030
+ [key: string]: unknown;
3031
+ }
3032
+
3033
+ /**
3034
+ * The content schema as this file's pipeline carries it: the raw 200
3035
+ * payload's object form (may arrive wrapped as `{ data: … }` from the
3036
+ * response envelope), with the two keys the editor reads named —
3037
+ * `contentItems` (array form; the legacy object form is tolerated at the
3038
+ * read sites and Array-guarded before use) plus the open remainder.
3039
+ */
3040
+ interface ContentSchemaView {
3041
+ /** Response-envelope artifact of the upstream API; tolerated, never authored here. */
3042
+ data?: { contentItems?: ContentSchemaItemView[] | Record<string, unknown> };
3043
+ contentItems?: ContentSchemaItemView[] | Record<string, unknown>;
3044
+ [key: string]: unknown;
3045
+ }
3046
+
3047
+ function normalizeDesignSchemaFromApiResponse(designRes: unknown): DesignSchemaView | null {
2777
3048
  if (!designRes) return null;
2778
3049
  const payload = (designRes as { data?: unknown })?.data ?? designRes;
2779
3050
  if (!payload || typeof payload !== 'object') return null;
2780
- if (Array.isArray(payload)) return payload;
3051
+ if (Array.isArray(payload)) return payload as DesignSchemaView;
2781
3052
  const record = payload as Record<string, unknown>;
2782
3053
  const groups = record.componentGroups ?? record.component_groups;
2783
3054
  if (Array.isArray(groups)) {
2784
3055
  if (groups.length > 1) {
2785
- const mergedComponents = groups.flatMap((g: { components?: unknown[] }) => g.components || []);
3056
+ // The view-shape coercion happens here, at the normalizer — the one
3057
+ // place raw API JSON is pronounced to be the editor's schema view.
3058
+ const mergedComponents = groups.flatMap((g: { components?: DesignSchemaPropertyView[] }) => g.components || []);
2786
3059
  return [{ name: (groups[0] as { name?: string })?.name || 'Design', components: mergedComponents }];
2787
3060
  }
2788
- return groups;
3061
+ return groups as DesignSchemaView;
2789
3062
  }
2790
3063
  // Legacy VM default schema: { theme: { colors: ... } } — not editable via PropsEditor
2791
3064
  if (record.theme && !groups) return null;
@@ -2836,10 +3109,10 @@
2836
3109
  }
2837
3110
  }
2838
3111
 
2839
- function normalizeContentSchemaFromApiResponse(contentRes: unknown): any {
3112
+ function normalizeContentSchemaFromApiResponse(contentRes: unknown): ContentSchemaView | null {
2840
3113
  if (!contentRes) return null;
2841
3114
  const payload = (contentRes as { data?: unknown })?.data ?? contentRes;
2842
- return payload && typeof payload === 'object' ? payload : null;
3115
+ return payload && typeof payload === 'object' ? (payload as ContentSchemaView) : null;
2843
3116
  }
2844
3117
 
2845
3118
  const SCHEMA_LOAD_TIMEOUT_MS = 20000;
@@ -2849,8 +3122,8 @@
2849
3122
  const schemaLoadWaitQueue: string[] = [];
2850
3123
  /** Per-widget schema cache — survives focus switches and snapshot merges. */
2851
3124
  const widgetSchemaCache = new Map<string, {
2852
- design: any;
2853
- content: any;
3125
+ design: DesignSchemaView | null;
3126
+ content: ContentSchemaView | null;
2854
3127
  error: string | null;
2855
3128
  }>();
2856
3129
 
@@ -3075,8 +3348,8 @@
3075
3348
  compositionSchemasError = null;
3076
3349
  }
3077
3350
 
3078
- let designSchema: any = null;
3079
- let contentSchema: any = null;
3351
+ let designSchema: DesignSchemaView | null = null;
3352
+ let contentSchema: ContentSchemaView | null = null;
3080
3353
  let schemaError: string | null = null;
3081
3354
 
3082
3355
  try {
@@ -3099,7 +3372,7 @@
3099
3372
 
3100
3373
  designSchema = ensureSubtitleFontInDesignSchema(
3101
3374
  normalizeDesignSchemaFromApiResponse(designRes)
3102
- );
3375
+ ) as DesignSchemaView | null;
3103
3376
  contentSchema = normalizeContentSchemaFromApiResponse(contentRes);
3104
3377
 
3105
3378
  if (!designSchema && !contentSchema) {
@@ -3462,6 +3735,206 @@
3462
3735
  return out;
3463
3736
  }
3464
3737
 
3738
+ /**
3739
+ * Deterministic design-tweak fast path for the canvas chat.
3740
+ *
3741
+ * `off` — never resolve; every message goes to codegen (shipped behavior).
3742
+ * `shadow` — resolve + log, but still send to codegen (measure the flip rate).
3743
+ * `enforce` — apply the resolved change to the live preview, persist it like
3744
+ * any other design edit, render the inline controller in chat, and
3745
+ * skip codegen entirely — no LLM call.
3746
+ */
3747
+ const DESIGN_INTENT_RESOLVER = (
3748
+ (import.meta.env as unknown as Record<string, string | undefined>).VITE_DESIGN_INTENT_RESOLVER ??
3749
+ 'enforce'
3750
+ ) as 'off' | 'shadow' | 'enforce';
3751
+
3752
+ /** Apply design values to the live preview + persist, mirroring the editor path. */
3753
+ function applyDesignValuesToPreview(widgetId: string, changes: Record<string, unknown>): void {
3754
+ const ps = getPanelDisplayState(widgetId);
3755
+ const base = ps.liveDesignValues ?? {};
3756
+ const next = { ...base, ...changes };
3757
+ if (widgetId === focusedPanelWidgetId) {
3758
+ liveDesignValues = next;
3759
+ } else {
3760
+ patchPanelSnapshot(widgetId, { liveDesignValues: next });
3761
+ }
3762
+ getWidgetDetailsRef(widgetId)?.sendMessageToPreview({
3763
+ type: 'widgetic:update',
3764
+ design: flattenDesignValuesForPreview(next)
3765
+ });
3766
+ compositionDirty = true;
3767
+ if (widgetId === focusedPanelWidgetId) {
3768
+ syncFocusedPanelEditorSnapshot();
3769
+ // Design edits are committed as soon as they are made, so the next codegen
3770
+ // turn already sees the updated values.
3771
+ if (selectedComposition && selectedWidgetId === widgetId) {
3772
+ void updateCompositionValues();
3773
+ }
3774
+ }
3775
+ }
3776
+
3777
+ /**
3778
+ * Property ids present in a design schema, memoized by schema identity.
3779
+ *
3780
+ * The chat design card asks whether the property it controls still exists so
3781
+ * it can disable a control whose property a regeneration dropped instead of
3782
+ * writing a dead value. That question is asked for every open panel on every
3783
+ * display revision (i.e. on every slider tick), so re-walking the schema each
3784
+ * time is wasteful. `undefined` means there is no schema to judge by yet —
3785
+ * the card then leaves its controls live rather than disabling them.
3786
+ */
3787
+ let designPropertyIdsCache: { schema: unknown; ids: Set<string> } | null = null;
3788
+ function designPropertyIdsFor(schema: unknown): Set<string> | undefined {
3789
+ if (!schema) return undefined;
3790
+ if (designPropertyIdsCache?.schema === schema) return designPropertyIdsCache.ids;
3791
+ const ids = new Set(collectDesignProperties(schema).map((property) => property.id));
3792
+ designPropertyIdsCache = { schema, ids };
3793
+ return ids;
3794
+ }
3795
+
3796
+ /**
3797
+ * Try to satisfy a chat message as a deterministic design tweak (no LLM).
3798
+ * Returns true when the message was handled and codegen must be skipped.
3799
+ */
3800
+ function tryApplyDesignIntent(widgetId: string, message: string): boolean {
3801
+ if (DESIGN_INTENT_RESOLVER === 'off') return false;
3802
+ const ps = getPanelDisplayState(widgetId);
3803
+ const schema = ps.compositionDesignSchema;
3804
+ if (!schema) return false;
3805
+
3806
+ const liveValues = ps.liveDesignValues ?? {};
3807
+ const resolution = resolveDesignIntent(message, schema, liveValues);
3808
+ if (!resolution) return false;
3809
+
3810
+ if (DESIGN_INTENT_RESOLVER === 'shadow') {
3811
+ console.log('[DesignIntent] shadow', resolution);
3812
+ return false;
3813
+ }
3814
+
3815
+ const changes: Record<string, unknown> = {};
3816
+ for (const change of resolution.changes) changes[change.propertyId] = change.value;
3817
+ applyDesignValuesToPreview(widgetId, changes);
3818
+ // The card records the composition the change landed in, so a reload can
3819
+ // re-attach it to that composition's live values instead of whatever
3820
+ // composition happens to be selected when the conversation is reopened.
3821
+ const card = toDesignControlRequest(
3822
+ resolution,
3823
+ schema,
3824
+ { ...liveValues, ...changes },
3825
+ ps.selectedComposition?.id ?? null
3826
+ );
3827
+ getChatMethodsForWidget(widgetId)?.addDesignControlsMessage?.(resolution.summary, card);
3828
+ return true;
3829
+ }
3830
+
3831
+ /**
3832
+ * Jev-backed fallback for the deterministic resolver: when the message names
3833
+ * a design property semantically and no literal value can be parsed, ask the
3834
+ * gateway to classify the property and a bounded value. Acts only when the
3835
+ * gateway says `acted` (enforce mode + confident verdict); otherwise the
3836
+ * caller generates exactly as before. Never throws, never blocks a send.
3837
+ *
3838
+ * The send's own classification rides along so the shadow telemetry can split
3839
+ * every dial's outcome by the ask shape that produced it — a fall-through where
3840
+ * the property was named indicts the value option strings, one where it wasn't
3841
+ * indicts the property strings or the confidence floor.
3842
+ */
3843
+ async function tryApplyDesignIntentViaJev(
3844
+ widgetId: string,
3845
+ message: string,
3846
+ classification: DesignIntentClassification | null
3847
+ ): Promise<boolean> {
3848
+ const ps = getPanelDisplayState(widgetId);
3849
+ const schema = ps.compositionDesignSchema;
3850
+ if (!schema || !apiBasePath) return false;
3851
+
3852
+ const result = await resolveDesignIntentViaJev({
3853
+ apiBasePath,
3854
+ token: authenticatedClientsToken,
3855
+ message,
3856
+ designSchema: schema,
3857
+ currentValues: ps.liveDesignValues ?? {}
3858
+ });
3859
+ if (!result) {
3860
+ recordDesignIntentJevOutcome({
3861
+ source: 'client_unavailable',
3862
+ acted: false,
3863
+ reason: 'no_response',
3864
+ classification
3865
+ });
3866
+ return false;
3867
+ }
3868
+
3869
+ recordDesignIntentJevOutcome({
3870
+ source: result.source,
3871
+ acted: result.acted,
3872
+ reason: result.reason,
3873
+ classification
3874
+ });
3875
+
3876
+ if (!result.acted || !result.property) {
3877
+ if (debugMode || result.source === 'jev') {
3878
+ console.log('[DesignIntentJev] fall through:', result.reason, result.property ?? '');
3879
+ }
3880
+ return false;
3881
+ }
3882
+
3883
+ const property = collectDesignProperties(schema).find((p) => p.id === result.property);
3884
+ const label = property?.label ?? result.property;
3885
+ applyDesignValuesToPreview(widgetId, { [result.property]: result.value });
3886
+ const card: DesignControlRequest = {
3887
+ summary: `Set ${label} to ${String(result.value)}.`,
3888
+ properties: [
3889
+ {
3890
+ id: result.property,
3891
+ label,
3892
+ controllerType: property?.controllerType ?? '',
3893
+ value: result.value,
3894
+ options: property?.options,
3895
+ min: property?.min,
3896
+ max: property?.max,
3897
+ step: property?.step,
3898
+ helpText: property?.helpText
3899
+ }
3900
+ ],
3901
+ // Same attribution as the deterministic path: the card is persisted
3902
+ // with the composition it was authored against (see `toDesignControlRequest`).
3903
+ compositionId: ps.selectedComposition?.id ?? null
3904
+ };
3905
+ getChatMethodsForWidget(widgetId)?.addDesignControlsMessage?.(card.summary, card);
3906
+ return true;
3907
+ }
3908
+
3909
+ /**
3910
+ * Classify a hand-drawn canvas stroke the Canvas's own geometry engine could
3911
+ * not decide.
3912
+ *
3913
+ * The Canvas measures the stroke and gates out every shape the measurements
3914
+ * rule out, then asks here only when several shapes remain. The gateway owns
3915
+ * the JEV decision and its confidence gate; this returns its shape, or null
3916
+ * (leave the stroke exactly as drawn) when it declines, degrades, or is not
3917
+ * configured. Null is always a valid answer, so nothing here can block a draw.
3918
+ */
3919
+ async function handleRecognizeStroke(
3920
+ request: StrokeRecognitionRequest
3921
+ ): Promise<StrokeRecognitionResult | null> {
3922
+ const reply = await recognizeStrokeViaJev({
3923
+ apiBasePath,
3924
+ token: authenticatedClientsToken,
3925
+ request
3926
+ });
3927
+ if (debugMode) {
3928
+ console.log(
3929
+ '[CanvasShapeSnap]',
3930
+ `candidates: ${request.candidates.map((candidate) => candidate.shape).join(', ')}`,
3931
+ `→ ${reply?.shape ?? 'as drawn'}`,
3932
+ `(${reply?.source ?? 'client_unavailable'} / ${reply?.reason ?? 'no_response'})`
3933
+ );
3934
+ }
3935
+ return reply;
3936
+ }
3937
+
3465
3938
  /** Handle property change from PropsEditor — send to widget preview iframe */
3466
3939
  function handleEditorPropertyChange(event: CustomEvent<{ schema: string; propertyId: string; value: unknown; opacity?: number; itemId?: string }>) {
3467
3940
  const { schema, propertyId, value, itemId } = event.detail;
@@ -3506,10 +3979,12 @@
3506
3979
  /** Handle content items reorder/add/delete from PropsEditor */
3507
3980
  function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
3508
3981
  const incoming = Array.isArray(event.detail.items) ? event.detail.items : [];
3509
- const schemaItems = compositionContentSchema?.contentItems
3510
- ?? compositionContentSchema?.data?.contentItems
3511
- ?? [];
3512
- liveContentItems = mergeContentItemsWithSchema(incoming, schemaItems);
3982
+ const schemaItems = compositionContentSchema?.contentItems
3983
+ ?? compositionContentSchema?.data?.contentItems;
3984
+ liveContentItems = mergeContentItemsWithSchema(
3985
+ incoming,
3986
+ Array.isArray(schemaItems) ? schemaItems : []
3987
+ );
3513
3988
  // console.log('[PropsEditor→Preview] Content items changed:', liveContentItems.length, 'items');
3514
3989
  getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
3515
3990
  type: 'widgetic:update',
@@ -4721,8 +5196,7 @@
4721
5196
  return !cid || cid === targetCanvasId;
4722
5197
  }) || fromList[0] || null;
4723
5198
  if (primaryId) {
4724
- // console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
4725
- await persistPrimaryWidgetId(primaryId);
5199
+ console.warn('[CreatorApp] Canvas has no saved primary — showing', primaryId.slice(0, 8), 'without replacing the saved one');
4726
5200
  }
4727
5201
  }
4728
5202
  if (primaryId) {
@@ -8628,11 +9102,22 @@ Requirements:
8628
9102
  promptImages = imagesToSend;
8629
9103
  savePromptImagesToSession();
8630
9104
  }
8631
- } else if (promptImpliesVisionReference(prompt)) {
8632
- console.warn(
8633
- '[WidgetCreator] Codegen proceeding WITHOUT image although prompt references one — widget',
8634
- generationWidgetId,
8635
- );
9105
+ } else if (promptImpliesVisionReference(prompt) && !options.forkUserMessage) {
9106
+ const shapeDataUrl =
9107
+ typeof canvasRef?.getWidgetShapeDataUrl === 'function'
9108
+ ? canvasRef.getWidgetShapeDataUrl(generationWidgetId)
9109
+ : null;
9110
+ if (typeof shapeDataUrl === 'string' && shapeDataUrl.startsWith('data:image/') && shapeDataUrl.length > 100) {
9111
+ const compressedShape = await compressImageDataUrlForCodegen(shapeDataUrl);
9112
+ if (compressedShape.startsWith('data:image/') && compressedShape.length > 100) {
9113
+ imagesToSend = [compressedShape];
9114
+ }
9115
+ } else {
9116
+ console.warn(
9117
+ '[WidgetCreator] Codegen proceeding WITHOUT image although prompt references one — widget',
9118
+ generationWidgetId,
9119
+ );
9120
+ }
8636
9121
  }
8637
9122
  let coderModelForRequest = selectedModelId || defaultModelId;
8638
9123
  if (imagesToSend?.length || attachmentIdsToSend?.length) {
@@ -8691,7 +9176,9 @@ Requirements:
8691
9176
  ? undefined
8692
9177
  : resolvePlannerModelIdForGeneration(prompt, coderModelForRequest);
8693
9178
  const runtimeLogsForRequest = collectRuntimeLogsForCodegen(generationWidgetId);
8694
- const response = await agentClient.agentCodeGeneratePost({
9179
+ // The generated client types this call as void and drops the 202 body.
9180
+ // Read the raw JSON so operationId and the intent-gate payload survive.
9181
+ const accepted = await agentClient.agentCodeGeneratePostRaw({
8695
9182
  generateCodeRequest: {
8696
9183
  repositoryId: repoId,
8697
9184
  prompt,
@@ -8704,8 +9191,15 @@ Requirements:
8704
9191
  compileFixOnly: options.compileFixOnly === true,
8705
9192
  skipIntentGate: options.skipIntentGate === true,
8706
9193
  chipIntent: options.chipIntent,
9194
+ brandKit: brandKitForRequest(),
8707
9195
  } as GenerateCodeRequest
8708
9196
  });
9197
+ let response: { data?: Record<string, unknown>; error?: string; message?: string } | null = null;
9198
+ try {
9199
+ response = await accepted.raw.json();
9200
+ } catch (parseError) {
9201
+ console.warn('[WidgetCreator] Code generation response was not JSON', parseError);
9202
+ }
8709
9203
  // 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);
8710
9204
  // console.log('Widget Creator: Code generation response:', response);
8711
9205
 
@@ -8759,7 +9253,8 @@ Requirements:
8759
9253
  return;
8760
9254
  }
8761
9255
 
8762
- if (response && (response as { error?: string }).error === 'TOKEN_EXPIRED') {
9256
+ const tokenEnvelope = response as unknown as { error?: string } | undefined;
9257
+ if (tokenEnvelope?.error === 'TOKEN_EXPIRED') {
8763
9258
  patchCodegenUiState(generationWidgetId, {
8764
9259
  generateCodeStatus: '❌ Authentication expired. Please login again.',
8765
9260
  lastGenerationFailed: true,
@@ -8789,9 +9284,9 @@ Requirements:
8789
9284
  return;
8790
9285
  }
8791
9286
 
8792
- const generateData = ((response as { data?: unknown }).data ?? response) as {
9287
+ const generateData = (response?.data ?? response) as {
8793
9288
  operationId?: string;
8794
- };
9289
+ } | null;
8795
9290
  const operationId = generateData?.operationId;
8796
9291
 
8797
9292
  // The Worker serves rebuilt code at the same preview URL, so the panel
@@ -8874,6 +9369,7 @@ Requirements:
8874
9369
  clearActiveOperationId();
8875
9370
 
8876
9371
  const completedCommitSha = getOperationCommitId(op.metadata);
9372
+ const completedCommitTitle = getOperationCommitTitle(op.metadata);
8877
9373
  const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
8878
9374
  const completedCached = getOperationCached(op.metadata);
8879
9375
  const completedCachedFiles = getOperationCachedFiles(op.metadata);
@@ -8894,6 +9390,11 @@ Requirements:
8894
9390
 
8895
9391
  if (completedCommitSha && generationWidgetId) {
8896
9392
  saveCommitIdForWidget(generationWidgetId, completedCommitSha);
9393
+ // The publish path stamps its optimistic row from the publish
9394
+ // response; a generation stamps this turn's draft row from the
9395
+ // operation, so the version menu shows what the turn shipped
9396
+ // without waiting for the history refetch below.
9397
+ stampCodegenTitleIntoPanelHistory(generationWidgetId, completedCommitSha, completedCommitTitle);
8897
9398
  }
8898
9399
 
8899
9400
  // Mark pending publish when this widget already has a live version.
@@ -8932,6 +9433,10 @@ Requirements:
8932
9433
  const alignCommitSha = completedCommitSha;
8933
9434
  const alignGeneratedVersion = (attempt: number) => {
8934
9435
  void loadPublishHistoryForPanel(alignWidgetId, { silent: true }).then(() => {
9436
+ // The refetch is authoritative, but a snapshot that raced the
9437
+ // row's title (or an API row still missing it) gets the
9438
+ // operation's label — the guard inside keeps a user rename.
9439
+ stampCodegenTitleIntoPanelHistory(generationWidgetId, completedCommitSha, completedCommitTitle);
8935
9440
  const versionId = findVersionIdMatchingCommit(alignWidgetId, alignCommitSha);
8936
9441
  const matchedByCommit = !!alignCommitSha
8937
9442
  && !!versionId
@@ -9118,6 +9623,17 @@ Requirements:
9118
9623
  );
9119
9624
  }
9120
9625
 
9626
+ // Design-contract violations mean some design controls will silently
9627
+ // do nothing (the property isn't wired, or the widget never applies
9628
+ // design updates). Say which, right where the user is looking — a
9629
+ // control that looks live but is dead is the worst failure mode here.
9630
+ // Served from the operation metadata; the server also embeds it in the
9631
+ // persisted reply, so it survives a reload.
9632
+ const designContractWarning = getOperationDesignContractWarning(op.metadata);
9633
+ if (designContractWarning) {
9634
+ panelChat?.addAssistantMessage(designContractWarning, true);
9635
+ }
9636
+
9121
9637
  setTimeout(async () => {
9122
9638
  if (!getWidgetDetailsRef(generationWidgetId)) return;
9123
9639
  const activePanelRef = getWidgetDetailsRef(generationWidgetId);
@@ -9164,6 +9680,7 @@ Requirements:
9164
9680
  patchCodegenUiState(generationWidgetId, {
9165
9681
  generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
9166
9682
  });
9683
+ void refreshTrialCounter();
9167
9684
  setTimeout(async () => {
9168
9685
  await panelChat?.reloadConversationMessages?.();
9169
9686
  panelChat?.markWidgetHeadAtLatestCommit?.();
@@ -9213,6 +9730,26 @@ Requirements:
9213
9730
  return;
9214
9731
  }
9215
9732
 
9733
+ // Handle retired-surface 410 (PLANNER_REMOVED / VM_REMOVED): the server
9734
+ // says this running bundle predates a removal — no retry can fix it.
9735
+ if (isRetiredSurfaceError(error)) {
9736
+ console.warn('Widget Creator: retired surface answered 410', { code: error.code, message: error.message });
9737
+ patchCodegenUiState(generationWidgetId, {
9738
+ generateCodeStatus: '⚠️ This app needs a refresh to continue.',
9739
+ lastGenerationFailed: true,
9740
+ lastGenerationError: error.message,
9741
+ });
9742
+ showToast('warning', 'This app needs a refresh', {
9743
+ description: error.message,
9744
+ duration: 12000,
9745
+ action: {
9746
+ label: 'Reload',
9747
+ onClick: () => window.location.reload()
9748
+ }
9749
+ });
9750
+ return;
9751
+ }
9752
+
9216
9753
  // Handle rate limiting (429) — too many API requests in the time window
9217
9754
  if (httpStatus === 429) {
9218
9755
  console.warn('Widget Creator: Rate limited (429) during code generation');
@@ -9303,7 +9840,8 @@ Requirements:
9303
9840
  widgetId: string,
9304
9841
  version: number,
9305
9842
  artifactUrl?: string | null,
9306
- commitSha?: string | null
9843
+ commitSha?: string | null,
9844
+ commitTitle?: string | null
9307
9845
  ): void {
9308
9846
  const jsPath = resolvePublishedJsPath(widgetId, version, artifactUrl);
9309
9847
  const prevSnap = getPanelState(widgetId);
@@ -9327,13 +9865,36 @@ Requirements:
9327
9865
  : w
9328
9866
  );
9329
9867
 
9868
+ // The optimistic row replaces the refetch the publish flow used to wait
9869
+ // for, so it must carry every field the dropdown renders — not just the
9870
+ // sha/artifact pair. The title comes from the publish response (or the
9871
+ // WebSocket payload for cross-tab/background publishes); when the caller
9872
+ // had none, an already-known row for this version keeps its title rather
9873
+ // than being downgraded to untitled (the set-live path re-merges a row the
9874
+ // panel already displayed).
9875
+ const previousRow = prevSnap.publishHistory.find((r) => r.version === version);
9876
+ const titleFromCaller = typeof commitTitle === 'string' && commitTitle.trim() !== ''
9877
+ ? commitTitle
9878
+ : null;
9879
+ const titleForEntry = titleFromCaller
9880
+ ?? (previousRow?.commitTitle ?? previousRow?.commit_title ?? null);
9330
9881
  const optimisticHistoryEntry = normalizedCommit
9331
9882
  ? {
9332
9883
  version,
9333
9884
  commitSha: normalizedCommit,
9334
9885
  commit_sha: normalizedCommit,
9886
+ commitTitle: titleForEntry,
9887
+ commit_title: titleForEntry,
9335
9888
  artifactUrl: artifactUrl ?? jsPath,
9336
9889
  artifact_url: artifactUrl ?? jsPath,
9890
+ // Draft/Live split and timestamps: publishing this version is what
9891
+ // made it live, and a re-merge must not erase the original dates.
9892
+ publishedAt: previousRow?.publishedAt ?? previousRow?.published_at ?? new Date().toISOString(),
9893
+ published_at: previousRow?.publishedAt ?? previousRow?.published_at ?? new Date().toISOString(),
9894
+ createdAt: previousRow?.createdAt ?? previousRow?.created_at ?? new Date().toISOString(),
9895
+ created_at: previousRow?.createdAt ?? previousRow?.created_at ?? new Date().toISOString(),
9896
+ isPublished: true,
9897
+ is_published: true,
9337
9898
  }
9338
9899
  : null;
9339
9900
  const mergedHistory = optimisticHistoryEntry
@@ -9389,6 +9950,44 @@ Requirements:
9389
9950
  void loadPublishHistoryForPanel(widgetId, { silent: true });
9390
9951
  }
9391
9952
 
9953
+ /**
9954
+ * Stamp the version title a codegen turn shipped onto that turn's history row.
9955
+ *
9956
+ * The publish path does this for its optimistic entry (the title rides the
9957
+ * publish response into `applyPublishSuccessToWidgetState`); a generation
9958
+ * only refetched the history, so the fresh draft row kept whatever title the
9959
+ * refetch returned — the operation's own label never reached the panel.
9960
+ *
9961
+ * Same discipline as the publish optimistic entry: the row the server already
9962
+ * wrote keeps its title (a user rename must not be reverted), and only the
9963
+ * row this turn *created* is stamped. Best-effort and synchronous — the
9964
+ * authoritative refetch that follows corrects anything this gets wrong.
9965
+ */
9966
+ function stampCodegenTitleIntoPanelHistory(widgetId: string, commitSha: string | null, commitTitle: string | null): void {
9967
+ if (!commitSha || !commitTitle) return;
9968
+
9969
+ const snap = getPanelState(widgetId);
9970
+ const row = snap.publishHistory.find(
9971
+ (r) => (r.commitSha ?? r.commit_sha) === commitSha && !(r.publishedAt ?? r.published_at)
9972
+ );
9973
+ // The server's recordGeneration wrote this row with the same title; only a
9974
+ // stale snapshot makes the stamp a visible change. Never overwrite an
9975
+ // existing title (a user rename) with the pipeline's wording.
9976
+ const currentTitle = row?.commitTitle ?? row?.commit_title ?? null;
9977
+ if (!row || (currentTitle && currentTitle !== commitTitle)) return;
9978
+
9979
+ const history = snap.publishHistory.map((r) =>
9980
+ r === row
9981
+ ? { ...r, commitTitle: commitTitle, commit_title: commitTitle }
9982
+ : r
9983
+ );
9984
+ patchPanelSnapshot(widgetId, { publishHistory: history });
9985
+ shapePublishHistoryByWidgetId = {
9986
+ ...shapePublishHistoryByWidgetId,
9987
+ [widgetId]: [...history],
9988
+ };
9989
+ }
9990
+
9392
9991
  /**
9393
9992
  * Publish the current widget: saves first, then triggers the production build job.
9394
9993
  * Flow: Save → fetch latest component → POST /canvases/{id}/components/{id}/publish
@@ -9516,10 +10115,11 @@ Requirements:
9516
10115
  setCrossTabPublishing(false, capturedWidgetId);
9517
10116
 
9518
10117
  const publishEnvelope = publishData as unknown as {
9519
- data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
10118
+ data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null; commitTitle?: string | null };
9520
10119
  version?: number;
9521
10120
  artifactUrl?: string | null;
9522
10121
  commitSha?: string | null;
10122
+ commitTitle?: string | null;
9523
10123
  };
9524
10124
  const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
9525
10125
  const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
@@ -9528,7 +10128,13 @@ Requirements:
9528
10128
  ?? publishEnvelope?.commitSha
9529
10129
  ?? commitShaForPublish
9530
10130
  ?? null;
9531
- applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha);
10131
+ // The version row's title, returned so the optimistic history entry can
10132
+ // show the real label instead of waiting for the history refetch.
10133
+ const commitTitle =
10134
+ publishEnvelope?.data?.commitTitle
10135
+ ?? publishEnvelope?.commitTitle
10136
+ ?? null;
10137
+ applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha, commitTitle);
9532
10138
 
9533
10139
  if (!publishOpts.silent) {
9534
10140
  showToast('success', 'Widget published!', {
@@ -9547,10 +10153,8 @@ Requirements:
9547
10153
  publishedPanelRef?.invalidatePublishedArtifactCache?.();
9548
10154
  await publishedPanelRef?.refreshPreview({ withRetry: true });
9549
10155
 
9550
- if (!publishOpts.silent) {
9551
- isHistoryOpen = true;
9552
- }
9553
10156
  await loadPublishHistory();
10157
+ void refreshTrialCounter();
9554
10158
 
9555
10159
  } catch (err: any) {
9556
10160
  console.error('[Publish] Error publishing widget:', err);
@@ -9948,6 +10552,13 @@ Requirements:
9948
10552
  return typeof raw === 'string' && /^[a-f0-9]{7,40}$/i.test(raw) ? raw : null;
9949
10553
  }
9950
10554
 
10555
+ /** The version title a completed codegen operation shipped ("Update header styles"), from operation metadata. */
10556
+ function getOperationCommitTitle(metadata?: Record<string, unknown>): string | null {
10557
+ if (!metadata) return null;
10558
+ const raw = metadata.commitTitle ?? metadata.commit_title;
10559
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null;
10560
+ }
10561
+
9951
10562
  function getOperationPreviewBuildError(metadata?: Record<string, unknown>): string | null {
9952
10563
  if (!metadata) return null;
9953
10564
  const raw = metadata.previewBuildError ?? metadata.preview_build_error;
@@ -9955,6 +10566,17 @@ Requirements:
9955
10566
  return sanitizePreviewBuildError(raw);
9956
10567
  }
9957
10568
 
10569
+ /**
10570
+ * The short design-contract warning the codegen service attached to a
10571
+ * completed operation, naming the design properties that will silently do
10572
+ * nothing. Null when the contract was clean (or nothing checked it).
10573
+ */
10574
+ function getOperationDesignContractWarning(metadata?: Record<string, unknown>): string | null {
10575
+ if (!metadata) return null;
10576
+ const raw = metadata.designContractWarning ?? metadata.design_contract_warning;
10577
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null;
10578
+ }
10579
+
9958
10580
  /** True when the operation completed from the semantic cache (no new generation ran). */
9959
10581
  function getOperationCached(metadata?: Record<string, unknown>): boolean {
9960
10582
  return metadata?.cached === true;
@@ -10203,8 +10825,8 @@ Requirements:
10203
10825
  liveDesignValues: Record<string, unknown>;
10204
10826
  liveContentValues: Record<string, unknown>;
10205
10827
  liveContentItems: any[];
10206
- compositionDesignSchema: any;
10207
- compositionContentSchema: any;
10828
+ compositionDesignSchema: DesignSchemaView | null;
10829
+ compositionContentSchema: ContentSchemaView | null;
10208
10830
  compositions: any[];
10209
10831
  selectedComposition: any;
10210
10832
  compositionDirty: boolean;
@@ -10827,8 +11449,10 @@ Requirements:
10827
11449
  return null;
10828
11450
  }
10829
11451
 
10830
- // After codegen (unpublished), prefer Worker preview over the Live CDN artifact.
10831
- if (source === 'preview' || ps.pendingPublishAfterCodegen) {
11452
+ // 'preview' is the explicit post-codegen source. An unpublished commit must
11453
+ // not replace Live: on open the dropdown stays on the published version,
11454
+ // and this URL has to be that same artifact.
11455
+ if (source === 'preview') {
10832
11456
  if (workerUrl) return workerUrl;
10833
11457
  }
10834
11458
 
@@ -10856,7 +11480,9 @@ Requirements:
10856
11480
  if (newestDraft != null) return `v${newestDraft}`;
10857
11481
  return null;
10858
11482
  }
10859
- return liveVersion != null ? `v${liveVersion}` : null;
11483
+ if (liveVersion != null) return `v${liveVersion}`;
11484
+ const newestDraft = getNewestDraftVersionId(widgetId);
11485
+ return newestDraft != null ? `v${newestDraft}` : null;
10860
11486
  }
10861
11487
 
10862
11488
  function formatPublishHistoryDate(record: {
@@ -10957,7 +11583,9 @@ Requirements:
10957
11583
  const ps = getPanelDisplayState(widgetId);
10958
11584
  const liveVersion =
10959
11585
  ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
10960
- return liveVersion != null ? `v:${liveVersion}` : null;
11586
+ if (liveVersion != null) return `v:${liveVersion}`;
11587
+ const newestDraft = getNewestDraftVersionId(widgetId);
11588
+ return newestDraft != null ? `v:${newestDraft}` : null;
10961
11589
  }
10962
11590
 
10963
11591
  function selectVersionInDropdowns(widgetId: string, versionId: string): void {
@@ -11687,7 +12315,7 @@ Requirements:
11687
12315
  ? getWidgetDetailsRef(selectedWidgetId)
11688
12316
  : null;
11689
12317
 
11690
- function getWidgetDetailsRef(widgetId?: string | null): any {
12318
+ function getWidgetDetailsRef(widgetId?: string | null): WidgetDetailsInstance | null {
11691
12319
  const id = widgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
11692
12320
  if (!id) return null;
11693
12321
  const instanceKey = panelInstanceKeyByWidgetId[id] ?? id;
@@ -12610,7 +13238,9 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
12610
13238
  applyPublishSuccessToWidgetState(
12611
13239
  publishWidgetId,
12612
13240
  publishedVersion,
12613
- publishResult.artifactUrl ?? null
13241
+ publishResult.artifactUrl ?? null,
13242
+ publishResult.commitSha ?? null,
13243
+ publishResult.commitTitle ?? null
12614
13244
  );
12615
13245
  const publishedName = getWidgetById(publishWidgetId)?.name || 'Widget';
12616
13246
  showToast('success', `"${publishedName}" published!`, {
@@ -12828,7 +13458,7 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
12828
13458
  widgetId,
12829
13459
  version
12830
13460
  });
12831
- const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
13461
+ const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string; commitTitle?: string; commit_title?: string } | undefined;
12832
13462
  const liveData = nested?.data ?? nested ?? {};
12833
13463
  const history = getPanelDisplayState(widgetId).publishHistory;
12834
13464
  const record = history.find((row) => row.version === version);
@@ -12838,7 +13468,12 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
12838
13468
  const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
12839
13469
  || (liveData as { artifact_url?: string }).artifact_url
12840
13470
  || record?.artifactUrl || record?.artifact_url || null;
12841
- applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha);
13471
+ // The row's current title — set-live re-merges a row the panel already
13472
+ // showed, so the response value keeps the label without a refetch.
13473
+ const commitTitle = (liveData as { commitTitle?: string; commit_title?: string }).commitTitle
13474
+ || (liveData as { commit_title?: string }).commit_title
13475
+ || record?.commitTitle || record?.commit_title || null;
13476
+ applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha, commitTitle);
12842
13477
  shapePreviewSourceByWidgetId = {
12843
13478
  ...shapePreviewSourceByWidgetId,
12844
13479
  [widgetId]: `v:${version}`,
@@ -12865,6 +13500,63 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
12865
13500
  }
12866
13501
  }
12867
13502
 
13503
+ /**
13504
+ * Renames one version's title — the owner's correction of the wording the
13505
+ * pipeline authored. Optimistic on the row; a failure rolls the row back
13506
+ * and says so in a toast.
13507
+ */
13508
+ async function renameVersionTitleForWidget(widgetId: string, version: number, title: string) {
13509
+ if (!widgetId || !$userSession?.jwt) return;
13510
+ if (!widgetsClient) {
13511
+ showToast('error', 'Widgets API client is not configured');
13512
+ return;
13513
+ }
13514
+
13515
+ const renameKey = `${widgetId}:${version}`;
13516
+ renamingTitleFor = renameKey;
13517
+
13518
+ // Optimistic: write the trimmed title into the row immediately so the
13519
+ // dropdown does not flicker while the PATCH is in flight.
13520
+ const history = getPanelDisplayState(widgetId).publishHistory;
13521
+ const record = history.find((row) => Number(row.version) === version);
13522
+ const previousTitle = record
13523
+ ? (record.commitTitle ?? record.commit_title ?? null)
13524
+ : null;
13525
+ if (record) {
13526
+ record.commitTitle = title === '' ? null : title;
13527
+ record.commit_title = title === '' ? null : title;
13528
+ }
13529
+ panelDisplayRevision += 1;
13530
+
13531
+ try {
13532
+ const res = await widgetsClient.renameWidgetPublishHistoryTitle({
13533
+ widgetId,
13534
+ version,
13535
+ renameWidgetPublishHistoryTitleRequest: { commitTitle: title },
13536
+ });
13537
+ const stored = (res as { data?: { commitTitle?: string | null } }).data?.commitTitle;
13538
+ // The stored value is authoritative (server trims and bounds).
13539
+ if (record) {
13540
+ record.commitTitle = stored ?? null;
13541
+ record.commit_title = stored ?? null;
13542
+ }
13543
+ panelDisplayRevision += 1;
13544
+ showToast('success', stored ? 'Version renamed' : 'Version title cleared');
13545
+ } catch (err) {
13546
+ // Roll the row back; nothing else needs reloading — the write did not land.
13547
+ if (record) {
13548
+ record.commitTitle = previousTitle;
13549
+ record.commit_title = previousTitle;
13550
+ }
13551
+ panelDisplayRevision += 1;
13552
+ showToast('error', 'Could not rename the version', {
13553
+ description: err instanceof ResponseError ? 'The server refused the rename.' : 'Check your connection and try again.',
13554
+ });
13555
+ } finally {
13556
+ renamingTitleFor = null;
13557
+ }
13558
+ }
13559
+
12868
13560
  async function setLiveVersion(version: number) {
12869
13561
  if (!selectedWidgetId) return;
12870
13562
  await setLiveVersionForWidget(selectedWidgetId, version);
@@ -12980,8 +13672,12 @@ $: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId)
12980
13672
  }
12981
13673
  if (isGeneratingCode || confirmGenerateDialogOpen) return;
12982
13674
  const widgetForDialog = getWidgetById(capturedWidgetId);
13675
+ const panelSnap = getPanelState(capturedWidgetId);
12983
13676
  const hasRepo = !!normalizeRepositoryId(widgetForDialog);
12984
- const hasNoCode = !lastCommitId && !lastPublishedVersion;
13677
+ const publishedVersion = panelSnap.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widgetForDialog);
13678
+ const hasNoCode = !panelSnap.lastCommitId
13679
+ && publishedVersion == null
13680
+ && (panelSnap.publishHistory?.length ?? 0) === 0;
12985
13681
  const hasPrompt = codeGenerationPrompt !== DEFAULT_PROMPT || promptImages.length > 0;
12986
13682
  const hasPreview = widgetDetails?.hasValidPreview() ?? false;
12987
13683
  if (hasRepo && hasNoCode && !hasPreview && hasPrompt) {
@@ -15032,6 +15728,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15032
15728
 
15033
15729
  <!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
15034
15730
  <div class="wc-top-bar-right flex items-center gap-3 shrink-0">
15731
+ {#if trialCounter}
15732
+ <span
15733
+ class="wc-trial-counter shrink-0 whitespace-nowrap rounded-md border border-[#314F2F]/20 bg-[#f4f7f4] px-2 py-1 text-[11px] font-medium text-[#314F2F]"
15734
+ title={trialCounter.days == null ? 'Credits left on this account' : 'Free trial: credits left and days left'}
15735
+ >
15736
+ {trialCounter.credits} credits{trialCounter.trialEnded
15737
+ ? ' · trial ended'
15738
+ : trialCounter.days == null
15739
+ ? ''
15740
+ : ` · ${trialCounter.days} ${trialCounter.days === 1 ? 'day' : 'days'}`}
15741
+ </span>
15742
+ {/if}
15035
15743
  {#if debugModeAvailable}
15036
15744
  <HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
15037
15745
  <button
@@ -15121,6 +15829,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15121
15829
  canvasComponentReady = false;
15122
15830
  }}
15123
15831
  onConvertToWidget={handleConvertToWidget}
15832
+ onRecognizeStroke={handleRecognizeStroke}
15124
15833
  onWidgetImageClick={handleWidgetImageClick}
15125
15834
  onCanvasDirty={() => emitCreatorSaveState(true)}
15126
15835
  onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
@@ -15136,8 +15845,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15136
15845
  if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
15137
15846
  // console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
15138
15847
  return;
15139
- }
15140
- const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
15848
+ } const active = canvasRef?.getSelectedObject?.();
15141
15849
  if (active?._isWidgetImage && active?._widgetId) {
15142
15850
  return;
15143
15851
  }
@@ -15529,7 +16237,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15529
16237
  {publishWidgetStatus}
15530
16238
  publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
15531
16239
  lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
15532
- widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath ?? null}
16240
+ widgetJsPath={ps.selectedWidgetJsPath ?? (typeof panelWidget?.jsPath === 'string' ? panelWidget.jsPath : null)}
15533
16241
  {isGeneratingInOtherTab}
15534
16242
  {isPublishingInOtherTab}
15535
16243
  {isSettingLiveVersion}
@@ -15931,7 +16639,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15931
16639
  <Portal target="wc-live-history-portal-{panelWidgetId}">
15932
16640
  <button
15933
16641
  type="button"
15934
- class="wc-live-history-backdrop fixed inset-0 z-[9998] cursor-default bg-transparent"
16642
+ class="wc-live-history-backdrop fixed left-0 right-0 bottom-0 top-14 z-[9998] cursor-default bg-transparent"
15935
16643
  aria-label="Close version history"
15936
16644
  onclick={() => { isHistoryOpen = false; }}
15937
16645
  ></button>
@@ -15955,12 +16663,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15955
16663
  {@const rowIsViewing = rowVersion === Number(viewingVersion)}
15956
16664
  {@const rowIsLive = !rowIsDraft && rowVersion === Number(panelPublishedVersion)}
15957
16665
  {@const rowDateLabel = formatPublishHistoryDate(record)}
15958
- {@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)}
15959
- <VersionDropdownRow
15960
- versionLabel={'v' + rowVersion}
15961
- commitSha={record.commitSha ?? record.commit_sha ?? null}
15962
- showCommit={debugMode}
15963
- dateLabel={rowDateLabel}
16666
+ {@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)} <VersionDropdownRow
16667
+ versionLabel={'v' + rowVersion}
16668
+ commitSha={record.commitSha ?? record.commit_sha ?? null}
16669
+ showCommit={debugMode}
16670
+ dateLabel={rowDateLabel}
15964
16671
  isLive={rowIsLive}
15965
16672
  isDraft={rowIsDraft}
15966
16673
  selected={rowIsViewing}
@@ -16126,9 +16833,50 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16126
16833
  ? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
16127
16834
  : `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
16128
16835
  >
16836
+ <!-- Cast to any below: @widgetic/chat is type-checked against its own
16837
+ Svelte install, so its Snippet type is nominally distinct from this
16838
+ package's. renderDesignControls is a real snippet either way. -->
16839
+ {#snippet renderDesignControls(spec)}
16840
+ <!-- `ps` is rebuilt on every panel display revision, so an attached
16841
+ card re-reads `liveValues` and follows a drag made in the
16842
+ PropsEditor panel; the card's own edits come back through the
16843
+ same live state via `onChange`. A persisted card only gets that
16844
+ live state while its own composition is the one loaded. -->
16845
+ {@const cardAttached = isDesignCardAttached(spec, ps.selectedComposition?.id ?? null)}
16846
+ <DesignQuickEdit
16847
+ {spec}
16848
+ liveValues={cardAttached ? ps.liveDesignValues : undefined}
16849
+ knownPropertyIds={cardAttached ? designPropertyIdsFor(ps.compositionDesignSchema) : undefined}
16850
+ detached={!cardAttached}
16851
+ onChange={(id, value) => applyDesignValuesToPreview(panelWidgetId, { [id]: value })}
16852
+ />
16853
+ {/snippet}
16854
+ {#snippet generationOptionChecks()}
16855
+ <BrandKitPanel
16856
+ part="checks"
16857
+ showPresetLibraries={debugMode}
16858
+ suspendPickers={!!generatingWidgetIds[panelWidgetId]}
16859
+ bind:brandKitEnabled
16860
+ bind:librariesEnabled
16861
+ bind:libraries={brandKitLibraries}
16862
+ bind:libraryGitUrl
16863
+ bind:libraryFileName
16864
+ bind:libraryUploads
16865
+ bind:fontUploads
16866
+ bind:primary={brandPrimary}
16867
+ bind:secondary={brandSecondary}
16868
+ bind:title={brandTitle}
16869
+ bind:sub={brandSub}
16870
+ bind:fonts={brandFonts}
16871
+ showLanguage={debugMode}
16872
+ bind:language={widgetLanguage}
16873
+ />
16874
+ {/snippet}
16129
16875
  <Chat class="code-generation-chat w-full h-full rounded-none border-0"
16876
+ actionBarExtras={generationOptionChecks}
16130
16877
  style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
16131
16878
  resizeExpandsContainer={true}
16879
+ renderDesignControls={renderDesignControls as any}
16132
16880
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
16133
16881
  showLogs={debugMode}
16134
16882
  showHeader={false}
@@ -16150,6 +16898,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16150
16898
  // Debug-only surfaces: assistant "Updated N files: ..." summaries
16151
16899
  // and heuristic code-block auto-detection. Hidden in production.
16152
16900
  showGenerationSummaries: debugMode,
16901
+ markComplete: false,
16153
16902
  // Keep false — debug autoCodeDetection mislabels "Code generation failed: …" as JavaScript.
16154
16903
  autoCodeDetection: false,
16155
16904
  }
@@ -16167,10 +16916,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16167
16916
  onCompletionToggle={(detail) => handleChatCompletionToggle(panelWidgetId, detail)}
16168
16917
  onLoadConversations={handleLoadConversations}
16169
16918
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
16170
- onCreateConversation={handleCreateConversation}
16171
- onSaveMessage={(conversationId, content, messageType, attachments, suggestions) =>
16172
- handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId)
16173
- }
16919
+ onCreateConversation={handleCreateConversation} onSaveMessage={(conversationId, content, messageType, attachments, suggestions, designControls) =>
16920
+ handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId, designControls)
16921
+ }
16174
16922
  onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
16175
16923
  handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
16176
16924
  }