@widgetic/creator 0.3.71 → 0.3.73

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 +882 -78
  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;
@@ -891,6 +1010,16 @@
891
1010
  let currentGenerationOperationId: string | null = null;
892
1011
  let generateCodeStatus: string | null = null;
893
1012
  let lastGenerationFailed = false; // Track if last generation failed for retry UI
1013
+ let publishPlanMessageByWidgetId: Record<string, string> = {};
1014
+
1015
+ function isPlanLimitMessage(message: string | null | undefined): boolean {
1016
+ if (!message) return false;
1017
+ return /credit balance|free trial|not available in your plan|not available on the free plan|upgrade your plan|upgrade to a solo/i.test(message);
1018
+ }
1019
+
1020
+ function openUpgradePlans() {
1021
+ window.location.assign('/workspaces/personal/settings/billing');
1022
+ }
894
1023
  let lastGenerationError: string | null = null; // Store the error message for display
895
1024
  let lastGenerationPrompt: string | null = null; // Last prompt used, for retrying failed generations
896
1025
  let pendingAutoGenerateWidgetId: string | null = null; // Widget created from canvas that needs auto code generation
@@ -922,6 +1051,7 @@
922
1051
  let chatMethods: {
923
1052
  sendMessage: (content: string, attachments?: File[]) => Promise<void>;
924
1053
  addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: ClarificationSuggestion[]) => void;
1054
+ addDesignControlsMessage?: (content: string, designControls: DesignControlRequest) => void;
925
1055
  addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
926
1056
  getConversationId: () => string | null;
927
1057
  addImageAttachment?: (dataUrl: string, fileName?: string) => void;
@@ -960,6 +1090,45 @@
960
1090
  // Populated via Chat's onConversationChange callback and passed to generateCode()
961
1091
  // so the backend can load multi-turn LLM history (Aider-style incremental edits).
962
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
+ }
963
1132
 
964
1133
  /** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
965
1134
  function promptImpliesVisionReference(prompt: string): boolean {
@@ -1133,6 +1302,40 @@
1133
1302
  }
1134
1303
  });
1135
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
+
1136
1339
  const effectivePromptImages =
1137
1340
  panelPromptImages.length > 0
1138
1341
  ? panelPromptImages
@@ -2116,6 +2319,12 @@
2116
2319
  // Suggestion chips from a persisted clarification card — restored
2117
2320
  // on reload exactly as they were served.
2118
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,
2119
2328
  isCommit: m.is_commit || m.isCommit || false,
2120
2329
  // What the turn shipped, in the commit's own words — persisted beside
2121
2330
  // is_commit by the codegen pipeline and rendered on the turn's commit
@@ -2295,6 +2504,10 @@
2295
2504
  }>,
2296
2505
  suggestions?: ClarificationSuggestion[],
2297
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,
2298
2511
  ): Promise<any> {
2299
2512
  try {
2300
2513
  // Draft convert panels hold a client-only conversation until the widget
@@ -2332,6 +2545,9 @@
2332
2545
  // Clarification-card suggestions ride the same write as the reply
2333
2546
  // they belong to, so a reload re-renders the card with its chips.
2334
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 } : {}),
2335
2551
  };
2336
2552
  const persistMessage = async (payload: typeof createPayload) => {
2337
2553
  const response = await messagesClient.createMessageRaw({
@@ -2549,6 +2765,10 @@
2549
2765
  // API returns camelCase (via responseTransformMiddleware), fallback to snake_case
2550
2766
  commitSha?: string | null;
2551
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;
2552
2772
  artifactUrl?: string;
2553
2773
  artifact_url?: string;
2554
2774
  /**
@@ -2573,6 +2793,8 @@
2573
2793
  let isHistoryOpen = false;
2574
2794
  let isLoadingHistory = false;
2575
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;
2576
2798
  let liveHistoryPanelTop = 0;
2577
2799
  let liveHistoryPanelRight = 0;
2578
2800
  /** True once publish history fetch completed for the current widget (or widget was never published). */
@@ -2677,8 +2899,8 @@
2677
2899
  // COMPOSITION EDITOR STATE
2678
2900
  // =============================================================================
2679
2901
  let compositionEditorOpen = false;
2680
- let compositionDesignSchema: any = null;
2681
- let compositionContentSchema: any = null;
2902
+ let compositionDesignSchema: DesignSchemaView | null = null;
2903
+ let compositionContentSchema: ContentSchemaView | null = null;
2682
2904
  let compositionSchemasLoading = false;
2683
2905
  let compositionSchemasError: string | null = null;
2684
2906
 
@@ -2712,6 +2934,9 @@
2712
2934
  }
2713
2935
 
2714
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;
2715
2940
  const measured = document
2716
2941
  .querySelector(`[data-widget-chat-height="${widgetId}"]`)
2717
2942
  ?.getBoundingClientRect().height;
@@ -2719,8 +2944,8 @@
2719
2944
  measured && measured > 0
2720
2945
  ? measured
2721
2946
  : (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
2722
- const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
2723
- 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;
2724
2949
  patchPanelSnapshot(widgetId, { chatContainerHeight: next });
2725
2950
  }
2726
2951
 
@@ -2763,19 +2988,77 @@
2763
2988
  });
2764
2989
  }
2765
2990
 
2766
- 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 {
2767
3048
  if (!designRes) return null;
2768
3049
  const payload = (designRes as { data?: unknown })?.data ?? designRes;
2769
3050
  if (!payload || typeof payload !== 'object') return null;
2770
- if (Array.isArray(payload)) return payload;
3051
+ if (Array.isArray(payload)) return payload as DesignSchemaView;
2771
3052
  const record = payload as Record<string, unknown>;
2772
3053
  const groups = record.componentGroups ?? record.component_groups;
2773
3054
  if (Array.isArray(groups)) {
2774
3055
  if (groups.length > 1) {
2775
- 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 || []);
2776
3059
  return [{ name: (groups[0] as { name?: string })?.name || 'Design', components: mergedComponents }];
2777
3060
  }
2778
- return groups;
3061
+ return groups as DesignSchemaView;
2779
3062
  }
2780
3063
  // Legacy VM default schema: { theme: { colors: ... } } — not editable via PropsEditor
2781
3064
  if (record.theme && !groups) return null;
@@ -2826,10 +3109,10 @@
2826
3109
  }
2827
3110
  }
2828
3111
 
2829
- function normalizeContentSchemaFromApiResponse(contentRes: unknown): any {
3112
+ function normalizeContentSchemaFromApiResponse(contentRes: unknown): ContentSchemaView | null {
2830
3113
  if (!contentRes) return null;
2831
3114
  const payload = (contentRes as { data?: unknown })?.data ?? contentRes;
2832
- return payload && typeof payload === 'object' ? payload : null;
3115
+ return payload && typeof payload === 'object' ? (payload as ContentSchemaView) : null;
2833
3116
  }
2834
3117
 
2835
3118
  const SCHEMA_LOAD_TIMEOUT_MS = 20000;
@@ -2839,8 +3122,8 @@
2839
3122
  const schemaLoadWaitQueue: string[] = [];
2840
3123
  /** Per-widget schema cache — survives focus switches and snapshot merges. */
2841
3124
  const widgetSchemaCache = new Map<string, {
2842
- design: any;
2843
- content: any;
3125
+ design: DesignSchemaView | null;
3126
+ content: ContentSchemaView | null;
2844
3127
  error: string | null;
2845
3128
  }>();
2846
3129
 
@@ -3065,8 +3348,8 @@
3065
3348
  compositionSchemasError = null;
3066
3349
  }
3067
3350
 
3068
- let designSchema: any = null;
3069
- let contentSchema: any = null;
3351
+ let designSchema: DesignSchemaView | null = null;
3352
+ let contentSchema: ContentSchemaView | null = null;
3070
3353
  let schemaError: string | null = null;
3071
3354
 
3072
3355
  try {
@@ -3089,7 +3372,7 @@
3089
3372
 
3090
3373
  designSchema = ensureSubtitleFontInDesignSchema(
3091
3374
  normalizeDesignSchemaFromApiResponse(designRes)
3092
- );
3375
+ ) as DesignSchemaView | null;
3093
3376
  contentSchema = normalizeContentSchemaFromApiResponse(contentRes);
3094
3377
 
3095
3378
  if (!designSchema && !contentSchema) {
@@ -3452,6 +3735,206 @@
3452
3735
  return out;
3453
3736
  }
3454
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
+
3455
3938
  /** Handle property change from PropsEditor — send to widget preview iframe */
3456
3939
  function handleEditorPropertyChange(event: CustomEvent<{ schema: string; propertyId: string; value: unknown; opacity?: number; itemId?: string }>) {
3457
3940
  const { schema, propertyId, value, itemId } = event.detail;
@@ -3496,10 +3979,12 @@
3496
3979
  /** Handle content items reorder/add/delete from PropsEditor */
3497
3980
  function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
3498
3981
  const incoming = Array.isArray(event.detail.items) ? event.detail.items : [];
3499
- const schemaItems = compositionContentSchema?.contentItems
3500
- ?? compositionContentSchema?.data?.contentItems
3501
- ?? [];
3502
- liveContentItems = mergeContentItemsWithSchema(incoming, schemaItems);
3982
+ const schemaItems = compositionContentSchema?.contentItems
3983
+ ?? compositionContentSchema?.data?.contentItems;
3984
+ liveContentItems = mergeContentItemsWithSchema(
3985
+ incoming,
3986
+ Array.isArray(schemaItems) ? schemaItems : []
3987
+ );
3503
3988
  // console.log('[PropsEditor→Preview] Content items changed:', liveContentItems.length, 'items');
3504
3989
  getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
3505
3990
  type: 'widgetic:update',
@@ -4711,8 +5196,7 @@
4711
5196
  return !cid || cid === targetCanvasId;
4712
5197
  }) || fromList[0] || null;
4713
5198
  if (primaryId) {
4714
- // console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
4715
- await persistPrimaryWidgetId(primaryId);
5199
+ console.warn('[CreatorApp] Canvas has no saved primary — showing', primaryId.slice(0, 8), 'without replacing the saved one');
4716
5200
  }
4717
5201
  }
4718
5202
  if (primaryId) {
@@ -8618,11 +9102,22 @@ Requirements:
8618
9102
  promptImages = imagesToSend;
8619
9103
  savePromptImagesToSession();
8620
9104
  }
8621
- } else if (promptImpliesVisionReference(prompt)) {
8622
- console.warn(
8623
- '[WidgetCreator] Codegen proceeding WITHOUT image although prompt references one — widget',
8624
- generationWidgetId,
8625
- );
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
+ }
8626
9121
  }
8627
9122
  let coderModelForRequest = selectedModelId || defaultModelId;
8628
9123
  if (imagesToSend?.length || attachmentIdsToSend?.length) {
@@ -8649,7 +9144,7 @@ Requirements:
8649
9144
  duration: 8000
8650
9145
  });
8651
9146
  }
8652
- } else if (promptImpliesVisionReference(prompt)) {
9147
+ } else if (promptImpliesVisionReference(prompt) && !options.forkUserMessage) {
8653
9148
  showToast('warning', 'Generare fără imagine', {
8654
9149
  description:
8655
9150
  'Request-ul merge fără vision (DeepSeek etc.). Atașează PNG/JPEG în chat înainte de Send.',
@@ -8681,7 +9176,9 @@ Requirements:
8681
9176
  ? undefined
8682
9177
  : resolvePlannerModelIdForGeneration(prompt, coderModelForRequest);
8683
9178
  const runtimeLogsForRequest = collectRuntimeLogsForCodegen(generationWidgetId);
8684
- 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({
8685
9182
  generateCodeRequest: {
8686
9183
  repositoryId: repoId,
8687
9184
  prompt,
@@ -8694,8 +9191,15 @@ Requirements:
8694
9191
  compileFixOnly: options.compileFixOnly === true,
8695
9192
  skipIntentGate: options.skipIntentGate === true,
8696
9193
  chipIntent: options.chipIntent,
9194
+ brandKit: brandKitForRequest(),
8697
9195
  } as GenerateCodeRequest
8698
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
+ }
8699
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);
8700
9204
  // console.log('Widget Creator: Code generation response:', response);
8701
9205
 
@@ -8749,7 +9253,8 @@ Requirements:
8749
9253
  return;
8750
9254
  }
8751
9255
 
8752
- 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') {
8753
9258
  patchCodegenUiState(generationWidgetId, {
8754
9259
  generateCodeStatus: '❌ Authentication expired. Please login again.',
8755
9260
  lastGenerationFailed: true,
@@ -8779,9 +9284,9 @@ Requirements:
8779
9284
  return;
8780
9285
  }
8781
9286
 
8782
- const generateData = ((response as { data?: unknown }).data ?? response) as {
9287
+ const generateData = (response?.data ?? response) as {
8783
9288
  operationId?: string;
8784
- };
9289
+ } | null;
8785
9290
  const operationId = generateData?.operationId;
8786
9291
 
8787
9292
  // The Worker serves rebuilt code at the same preview URL, so the panel
@@ -8864,6 +9369,7 @@ Requirements:
8864
9369
  clearActiveOperationId();
8865
9370
 
8866
9371
  const completedCommitSha = getOperationCommitId(op.metadata);
9372
+ const completedCommitTitle = getOperationCommitTitle(op.metadata);
8867
9373
  const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
8868
9374
  const completedCached = getOperationCached(op.metadata);
8869
9375
  const completedCachedFiles = getOperationCachedFiles(op.metadata);
@@ -8884,6 +9390,11 @@ Requirements:
8884
9390
 
8885
9391
  if (completedCommitSha && generationWidgetId) {
8886
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);
8887
9398
  }
8888
9399
 
8889
9400
  // Mark pending publish when this widget already has a live version.
@@ -8922,6 +9433,10 @@ Requirements:
8922
9433
  const alignCommitSha = completedCommitSha;
8923
9434
  const alignGeneratedVersion = (attempt: number) => {
8924
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);
8925
9440
  const versionId = findVersionIdMatchingCommit(alignWidgetId, alignCommitSha);
8926
9441
  const matchedByCommit = !!alignCommitSha
8927
9442
  && !!versionId
@@ -9108,6 +9623,17 @@ Requirements:
9108
9623
  );
9109
9624
  }
9110
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
+
9111
9637
  setTimeout(async () => {
9112
9638
  if (!getWidgetDetailsRef(generationWidgetId)) return;
9113
9639
  const activePanelRef = getWidgetDetailsRef(generationWidgetId);
@@ -9154,6 +9680,7 @@ Requirements:
9154
9680
  patchCodegenUiState(generationWidgetId, {
9155
9681
  generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
9156
9682
  });
9683
+ void refreshTrialCounter();
9157
9684
  setTimeout(async () => {
9158
9685
  await panelChat?.reloadConversationMessages?.();
9159
9686
  panelChat?.markWidgetHeadAtLatestCommit?.();
@@ -9203,6 +9730,26 @@ Requirements:
9203
9730
  return;
9204
9731
  }
9205
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
+
9206
9753
  // Handle rate limiting (429) — too many API requests in the time window
9207
9754
  if (httpStatus === 429) {
9208
9755
  console.warn('Widget Creator: Rate limited (429) during code generation');
@@ -9293,7 +9840,8 @@ Requirements:
9293
9840
  widgetId: string,
9294
9841
  version: number,
9295
9842
  artifactUrl?: string | null,
9296
- commitSha?: string | null
9843
+ commitSha?: string | null,
9844
+ commitTitle?: string | null
9297
9845
  ): void {
9298
9846
  const jsPath = resolvePublishedJsPath(widgetId, version, artifactUrl);
9299
9847
  const prevSnap = getPanelState(widgetId);
@@ -9317,13 +9865,36 @@ Requirements:
9317
9865
  : w
9318
9866
  );
9319
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);
9320
9881
  const optimisticHistoryEntry = normalizedCommit
9321
9882
  ? {
9322
9883
  version,
9323
9884
  commitSha: normalizedCommit,
9324
9885
  commit_sha: normalizedCommit,
9886
+ commitTitle: titleForEntry,
9887
+ commit_title: titleForEntry,
9325
9888
  artifactUrl: artifactUrl ?? jsPath,
9326
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,
9327
9898
  }
9328
9899
  : null;
9329
9900
  const mergedHistory = optimisticHistoryEntry
@@ -9379,6 +9950,44 @@ Requirements:
9379
9950
  void loadPublishHistoryForPanel(widgetId, { silent: true });
9380
9951
  }
9381
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
+
9382
9991
  /**
9383
9992
  * Publish the current widget: saves first, then triggers the production build job.
9384
9993
  * Flow: Save → fetch latest component → POST /canvases/{id}/components/{id}/publish
@@ -9506,10 +10115,11 @@ Requirements:
9506
10115
  setCrossTabPublishing(false, capturedWidgetId);
9507
10116
 
9508
10117
  const publishEnvelope = publishData as unknown as {
9509
- data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
10118
+ data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null; commitTitle?: string | null };
9510
10119
  version?: number;
9511
10120
  artifactUrl?: string | null;
9512
10121
  commitSha?: string | null;
10122
+ commitTitle?: string | null;
9513
10123
  };
9514
10124
  const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
9515
10125
  const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
@@ -9518,7 +10128,13 @@ Requirements:
9518
10128
  ?? publishEnvelope?.commitSha
9519
10129
  ?? commitShaForPublish
9520
10130
  ?? null;
9521
- 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);
9522
10138
 
9523
10139
  if (!publishOpts.silent) {
9524
10140
  showToast('success', 'Widget published!', {
@@ -9537,10 +10153,8 @@ Requirements:
9537
10153
  publishedPanelRef?.invalidatePublishedArtifactCache?.();
9538
10154
  await publishedPanelRef?.refreshPreview({ withRetry: true });
9539
10155
 
9540
- if (!publishOpts.silent) {
9541
- isHistoryOpen = true;
9542
- }
9543
10156
  await loadPublishHistory();
10157
+ void refreshTrialCounter();
9544
10158
 
9545
10159
  } catch (err: any) {
9546
10160
  console.error('[Publish] Error publishing widget:', err);
@@ -9548,10 +10162,18 @@ Requirements:
9548
10162
  isPublishing = false;
9549
10163
  publishingWidgetId = null;
9550
10164
  setCrossTabPublishing(false, capturedWidgetId);
9551
- showToast('error', 'Publish failed', {
9552
- description: normalizeErrorMessage(err) || 'An unexpected error occurred. Please try again.',
9553
- duration: 8000
9554
- });
10165
+ const publishErrorText = normalizeErrorMessage(err) || 'An unexpected error occurred. Please try again.';
10166
+ if (isPlanLimitMessage(publishErrorText) && capturedWidgetId) {
10167
+ publishPlanMessageByWidgetId = {
10168
+ ...publishPlanMessageByWidgetId,
10169
+ [capturedWidgetId]: publishErrorText,
10170
+ };
10171
+ } else {
10172
+ showToast('error', 'Publish failed', {
10173
+ description: publishErrorText,
10174
+ duration: 8000
10175
+ });
10176
+ }
9555
10177
  }
9556
10178
  // isPublishing stays true until widget_publish_status WebSocket event arrives (success or failure)
9557
10179
  }
@@ -9930,6 +10552,13 @@ Requirements:
9930
10552
  return typeof raw === 'string' && /^[a-f0-9]{7,40}$/i.test(raw) ? raw : null;
9931
10553
  }
9932
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
+
9933
10562
  function getOperationPreviewBuildError(metadata?: Record<string, unknown>): string | null {
9934
10563
  if (!metadata) return null;
9935
10564
  const raw = metadata.previewBuildError ?? metadata.preview_build_error;
@@ -9937,6 +10566,17 @@ Requirements:
9937
10566
  return sanitizePreviewBuildError(raw);
9938
10567
  }
9939
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
+
9940
10580
  /** True when the operation completed from the semantic cache (no new generation ran). */
9941
10581
  function getOperationCached(metadata?: Record<string, unknown>): boolean {
9942
10582
  return metadata?.cached === true;
@@ -10185,8 +10825,8 @@ Requirements:
10185
10825
  liveDesignValues: Record<string, unknown>;
10186
10826
  liveContentValues: Record<string, unknown>;
10187
10827
  liveContentItems: any[];
10188
- compositionDesignSchema: any;
10189
- compositionContentSchema: any;
10828
+ compositionDesignSchema: DesignSchemaView | null;
10829
+ compositionContentSchema: ContentSchemaView | null;
10190
10830
  compositions: any[];
10191
10831
  selectedComposition: any;
10192
10832
  compositionDirty: boolean;
@@ -10809,8 +11449,10 @@ Requirements:
10809
11449
  return null;
10810
11450
  }
10811
11451
 
10812
- // After codegen (unpublished), prefer Worker preview over the Live CDN artifact.
10813
- 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') {
10814
11456
  if (workerUrl) return workerUrl;
10815
11457
  }
10816
11458
 
@@ -10838,7 +11480,9 @@ Requirements:
10838
11480
  if (newestDraft != null) return `v${newestDraft}`;
10839
11481
  return null;
10840
11482
  }
10841
- 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;
10842
11486
  }
10843
11487
 
10844
11488
  function formatPublishHistoryDate(record: {
@@ -10939,7 +11583,9 @@ Requirements:
10939
11583
  const ps = getPanelDisplayState(widgetId);
10940
11584
  const liveVersion =
10941
11585
  ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
10942
- 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;
10943
11589
  }
10944
11590
 
10945
11591
  function selectVersionInDropdowns(widgetId: string, versionId: string): void {
@@ -11669,7 +12315,7 @@ Requirements:
11669
12315
  ? getWidgetDetailsRef(selectedWidgetId)
11670
12316
  : null;
11671
12317
 
11672
- function getWidgetDetailsRef(widgetId?: string | null): any {
12318
+ function getWidgetDetailsRef(widgetId?: string | null): WidgetDetailsInstance | null {
11673
12319
  const id = widgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
11674
12320
  if (!id) return null;
11675
12321
  const instanceKey = panelInstanceKeyByWidgetId[id] ?? id;
@@ -12592,7 +13238,9 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
12592
13238
  applyPublishSuccessToWidgetState(
12593
13239
  publishWidgetId,
12594
13240
  publishedVersion,
12595
- publishResult.artifactUrl ?? null
13241
+ publishResult.artifactUrl ?? null,
13242
+ publishResult.commitSha ?? null,
13243
+ publishResult.commitTitle ?? null
12596
13244
  );
12597
13245
  const publishedName = getWidgetById(publishWidgetId)?.name || 'Widget';
12598
13246
  showToast('success', `"${publishedName}" published!`, {
@@ -12810,7 +13458,7 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
12810
13458
  widgetId,
12811
13459
  version
12812
13460
  });
12813
- 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;
12814
13462
  const liveData = nested?.data ?? nested ?? {};
12815
13463
  const history = getPanelDisplayState(widgetId).publishHistory;
12816
13464
  const record = history.find((row) => row.version === version);
@@ -12820,7 +13468,12 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
12820
13468
  const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
12821
13469
  || (liveData as { artifact_url?: string }).artifact_url
12822
13470
  || record?.artifactUrl || record?.artifact_url || null;
12823
- 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);
12824
13477
  shapePreviewSourceByWidgetId = {
12825
13478
  ...shapePreviewSourceByWidgetId,
12826
13479
  [widgetId]: `v:${version}`,
@@ -12847,6 +13500,63 @@ async function setLiveVersionForWidget(widgetId: string, version: number) {
12847
13500
  }
12848
13501
  }
12849
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
+
12850
13560
  async function setLiveVersion(version: number) {
12851
13561
  if (!selectedWidgetId) return;
12852
13562
  await setLiveVersionForWidget(selectedWidgetId, version);
@@ -12962,8 +13672,12 @@ $: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId)
12962
13672
  }
12963
13673
  if (isGeneratingCode || confirmGenerateDialogOpen) return;
12964
13674
  const widgetForDialog = getWidgetById(capturedWidgetId);
13675
+ const panelSnap = getPanelState(capturedWidgetId);
12965
13676
  const hasRepo = !!normalizeRepositoryId(widgetForDialog);
12966
- 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;
12967
13681
  const hasPrompt = codeGenerationPrompt !== DEFAULT_PROMPT || promptImages.length > 0;
12968
13682
  const hasPreview = widgetDetails?.hasValidPreview() ?? false;
12969
13683
  if (hasRepo && hasNoCode && !hasPreview && hasPrompt) {
@@ -15014,6 +15728,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15014
15728
 
15015
15729
  <!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
15016
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}
15017
15743
  {#if debugModeAvailable}
15018
15744
  <HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
15019
15745
  <button
@@ -15103,6 +15829,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15103
15829
  canvasComponentReady = false;
15104
15830
  }}
15105
15831
  onConvertToWidget={handleConvertToWidget}
15832
+ onRecognizeStroke={handleRecognizeStroke}
15106
15833
  onWidgetImageClick={handleWidgetImageClick}
15107
15834
  onCanvasDirty={() => emitCreatorSaveState(true)}
15108
15835
  onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
@@ -15118,8 +15845,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15118
15845
  if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
15119
15846
  // console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
15120
15847
  return;
15121
- }
15122
- const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
15848
+ } const active = canvasRef?.getSelectedObject?.();
15123
15849
  if (active?._isWidgetImage && active?._widgetId) {
15124
15850
  return;
15125
15851
  }
@@ -15511,7 +16237,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15511
16237
  {publishWidgetStatus}
15512
16238
  publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
15513
16239
  lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
15514
- widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath ?? null}
16240
+ widgetJsPath={ps.selectedWidgetJsPath ?? (typeof panelWidget?.jsPath === 'string' ? panelWidget.jsPath : null)}
15515
16241
  {isGeneratingInOtherTab}
15516
16242
  {isPublishingInOtherTab}
15517
16243
  {isSettingLiveVersion}
@@ -15913,7 +16639,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15913
16639
  <Portal target="wc-live-history-portal-{panelWidgetId}">
15914
16640
  <button
15915
16641
  type="button"
15916
- 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"
15917
16643
  aria-label="Close version history"
15918
16644
  onclick={() => { isHistoryOpen = false; }}
15919
16645
  ></button>
@@ -15937,12 +16663,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
15937
16663
  {@const rowIsViewing = rowVersion === Number(viewingVersion)}
15938
16664
  {@const rowIsLive = !rowIsDraft && rowVersion === Number(panelPublishedVersion)}
15939
16665
  {@const rowDateLabel = formatPublishHistoryDate(record)}
15940
- {@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)}
15941
- <VersionDropdownRow
15942
- versionLabel={'v' + rowVersion}
15943
- commitSha={record.commitSha ?? record.commit_sha ?? null}
15944
- showCommit={debugMode}
15945
- 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}
15946
16671
  isLive={rowIsLive}
15947
16672
  isDraft={rowIsDraft}
15948
16673
  selected={rowIsViewing}
@@ -16008,10 +16733,47 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16008
16733
  {/if}
16009
16734
  </div>
16010
16735
  </div>
16736
+ {#if publishPlanMessageByWidgetId[panelWidgetId]}
16737
+ <div class="publish-plan-notice p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
16738
+ <p class="text-xs text-red-700 whitespace-pre-wrap">{publishPlanMessageByWidgetId[panelWidgetId]}</p>
16739
+ <div class="flex gap-2 mt-3 justify-center">
16740
+ <Button
16741
+ onclick={openUpgradePlans}
16742
+ variant="default"
16743
+ size="sm"
16744
+ class="bg-red-600 hover:bg-red-700 text-white rounded-lg"
16745
+ >
16746
+ Upgrade
16747
+ </Button>
16748
+ <Button
16749
+ onclick={() => {
16750
+ const next = { ...publishPlanMessageByWidgetId };
16751
+ delete next[panelWidgetId];
16752
+ publishPlanMessageByWidgetId = next;
16753
+ }}
16754
+ variant="outline"
16755
+ size="sm"
16756
+ class="border-gray-300 text-gray-600 hover:bg-gray-50 rounded-lg"
16757
+ >
16758
+ Dismiss
16759
+ </Button>
16760
+ </div>
16761
+ </div>
16762
+ {/if}
16011
16763
  {#if ps.lastGenerationFailed}
16012
16764
  <div class="p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
16013
16765
  <p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
16014
16766
  <div class="flex gap-2 mt-3 justify-center">
16767
+ {#if isPlanLimitMessage(ps.lastGenerationError)}
16768
+ <Button
16769
+ onclick={openUpgradePlans}
16770
+ variant="default"
16771
+ size="sm"
16772
+ class="bg-red-600 hover:bg-red-700 text-white rounded-lg"
16773
+ >
16774
+ Upgrade
16775
+ </Button>
16776
+ {:else}
16015
16777
  <Button
16016
16778
  onclick={() => {
16017
16779
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
@@ -16026,6 +16788,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16026
16788
  >
16027
16789
  Retry
16028
16790
  </Button>
16791
+ {/if}
16029
16792
  <Button
16030
16793
  onclick={() => {
16031
16794
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
@@ -16070,9 +16833,50 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16070
16833
  ? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
16071
16834
  : `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
16072
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}
16073
16875
  <Chat class="code-generation-chat w-full h-full rounded-none border-0"
16876
+ actionBarExtras={generationOptionChecks}
16074
16877
  style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
16075
16878
  resizeExpandsContainer={true}
16879
+ renderDesignControls={renderDesignControls as any}
16076
16880
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
16077
16881
  showLogs={debugMode}
16078
16882
  showHeader={false}
@@ -16094,6 +16898,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16094
16898
  // Debug-only surfaces: assistant "Updated N files: ..." summaries
16095
16899
  // and heuristic code-block auto-detection. Hidden in production.
16096
16900
  showGenerationSummaries: debugMode,
16901
+ markComplete: false,
16097
16902
  // Keep false — debug autoCodeDetection mislabels "Code generation failed: …" as JavaScript.
16098
16903
  autoCodeDetection: false,
16099
16904
  }
@@ -16111,10 +16916,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
16111
16916
  onCompletionToggle={(detail) => handleChatCompletionToggle(panelWidgetId, detail)}
16112
16917
  onLoadConversations={handleLoadConversations}
16113
16918
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
16114
- onCreateConversation={handleCreateConversation}
16115
- onSaveMessage={(conversationId, content, messageType, attachments, suggestions) =>
16116
- handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId)
16117
- }
16919
+ onCreateConversation={handleCreateConversation} onSaveMessage={(conversationId, content, messageType, attachments, suggestions, designControls) =>
16920
+ handleSaveMessage(conversationId, content, messageType, attachments, suggestions, panelWidgetId, designControls)
16921
+ }
16118
16922
  onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
16119
16923
  handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
16120
16924
  }