@agent-native/core 0.84.24 → 0.84.27

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 (82) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +12 -8
  5. package/corpus/core/src/agent/run-manager.ts +58 -6
  6. package/corpus/core/src/agent/types.ts +9 -2
  7. package/corpus/core/src/client/AssistantChat.tsx +51 -3
  8. package/corpus/core/src/client/FeedbackButton.tsx +2 -2
  9. package/corpus/core/src/client/components/PresenceBar.tsx +7 -2
  10. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +23 -9
  11. package/corpus/core/src/client/sharing/ShareButton.tsx +22 -3
  12. package/corpus/core/src/client/sharing/ShareDialog.tsx +13 -1
  13. package/corpus/core/src/client/sse-event-processor.ts +83 -26
  14. package/corpus/core/src/client/tool-display.ts +1 -1
  15. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +3 -3
  16. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +4 -2
  17. package/corpus/templates/clips/app/components/player/share-dialog.tsx +9 -0
  18. package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -0
  19. package/corpus/templates/clips/app/i18n/de-DE.ts +3 -0
  20. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  21. package/corpus/templates/clips/app/i18n/es-ES.ts +3 -0
  22. package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -0
  23. package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -0
  24. package/corpus/templates/clips/app/i18n/ja-JP.ts +3 -0
  25. package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -0
  26. package/corpus/templates/clips/app/i18n/pt-BR.ts +3 -0
  27. package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -0
  28. package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -0
  29. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-can-now-copy-a-ready-to-send-prompt-that-tells-.md +6 -0
  30. package/corpus/templates/design/actions/insert-asset.ts +14 -2
  31. package/corpus/templates/design/actions/insert-design-native-asset.ts +16 -4
  32. package/corpus/templates/design/actions/insert-figma-library-asset.ts +14 -2
  33. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -0
  34. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +54 -10
  35. package/corpus/templates/design/app/components/design/EditPanel.tsx +23 -16
  36. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -2
  37. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +9 -5
  38. package/corpus/templates/design/app/components/layout/Header.tsx +44 -1
  39. package/corpus/templates/design/app/pages/DesignEditor.tsx +267 -105
  40. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +15 -1
  41. package/corpus/templates/design/app/pages/Index.tsx +20 -9
  42. package/corpus/templates/design/app/routes/settings.tsx +1 -1
  43. package/corpus/templates/design/changelog/2026-07-01-design-editor-polish-makes-zoom-assets-sharing-and-agent-handoffs-clearer.md +6 -0
  44. package/corpus/templates/design/changelog/2026-07-01-design-qa-fixes-make-exports-permissions-and-tools-clearer.md +6 -0
  45. package/dist/agent/production-agent.d.ts.map +1 -1
  46. package/dist/agent/production-agent.js +12 -9
  47. package/dist/agent/production-agent.js.map +1 -1
  48. package/dist/agent/run-manager.d.ts.map +1 -1
  49. package/dist/agent/run-manager.js +56 -4
  50. package/dist/agent/run-manager.js.map +1 -1
  51. package/dist/agent/types.d.ts +3 -0
  52. package/dist/agent/types.d.ts.map +1 -1
  53. package/dist/agent/types.js.map +1 -1
  54. package/dist/client/AssistantChat.d.ts.map +1 -1
  55. package/dist/client/AssistantChat.js +52 -3
  56. package/dist/client/AssistantChat.js.map +1 -1
  57. package/dist/client/FeedbackButton.js +2 -2
  58. package/dist/client/FeedbackButton.js.map +1 -1
  59. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  60. package/dist/client/components/PresenceBar.js +6 -2
  61. package/dist/client/components/PresenceBar.js.map +1 -1
  62. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  63. package/dist/client/composer/ComposerPlusMenu.js +15 -7
  64. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  65. package/dist/client/sharing/ShareButton.js +15 -2
  66. package/dist/client/sharing/ShareButton.js.map +1 -1
  67. package/dist/client/sharing/ShareDialog.js +16 -1
  68. package/dist/client/sharing/ShareDialog.js.map +1 -1
  69. package/dist/client/sse-event-processor.d.ts.map +1 -1
  70. package/dist/client/sse-event-processor.js +64 -21
  71. package/dist/client/sse-event-processor.js.map +1 -1
  72. package/dist/client/tool-display.js +1 -1
  73. package/dist/client/tool-display.js.map +1 -1
  74. package/dist/collab/awareness.d.ts +2 -2
  75. package/dist/collab/awareness.d.ts.map +1 -1
  76. package/dist/notifications/routes.d.ts +3 -3
  77. package/dist/observability/routes.d.ts +5 -5
  78. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/dist/sharing/actions/set-resource-visibility.js +3 -3
  81. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  82. package/package.json +1 -1
@@ -71,12 +71,9 @@ import { normalizeDesignSourceType } from "@shared/source-mode";
71
71
  import {
72
72
  IconArrowLeft,
73
73
  IconArrowUpRight,
74
- IconArrowsMaximize,
75
74
  IconPencil,
76
75
  IconMessage,
77
76
  IconBrush,
78
- IconZoomIn,
79
- IconZoomOut,
80
77
  IconDeviceDesktop,
81
78
  IconDeviceTablet,
82
79
  IconDeviceMobile,
@@ -118,6 +115,7 @@ import {
118
115
  IconCircleCheck,
119
116
  IconTerminal2,
120
117
  IconLink,
118
+ IconLock,
121
119
  } from "@tabler/icons-react";
122
120
  import { useQueryClient } from "@tanstack/react-query";
123
121
  import {
@@ -306,6 +304,8 @@ const BOARD_SURFACE_SIZE = 131_072;
306
304
  const LOCALHOST_WRITE_EXTENSIONS = new Set([".html", ".htm", ".css"]);
307
305
  const NO_LOCALHOST_WRITE_CONTENT_MESSAGE =
308
306
  "No content to write. Open the screen first." /* i18n-ignore */;
307
+ const TWEAK_CONTROLS_EDIT_ACCESS_MESSAGE =
308
+ "You need edit access to add tweak controls." /* i18n-ignore */;
309
309
  const FOCUSED_SCREEN_ZOOM = 100;
310
310
  export const DEFAULT_OVERVIEW_ZOOM = 60;
311
311
  const KEEPALIVE_FILE_SAVE_MAX_BYTES = 60_000;
@@ -857,6 +857,16 @@ function removeEditorChromeOverlays(root: ParentNode): void {
857
857
  .forEach((element) => element.remove());
858
858
  }
859
859
 
860
+ function sanitizeSerializedXmlForSvg(value: string): string {
861
+ // SVG opened as XML only knows the five predefined entities. HTML serializers
862
+ // can leave named entities or bare ampersands in foreignObject content; escape
863
+ // those so the downloaded SVG parses cleanly in browsers and editors.
864
+ return value.replace(
865
+ /&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)/g,
866
+ "&",
867
+ );
868
+ }
869
+
860
870
  /**
861
871
  * Resolve the document-space rect of the currently selected element inside the
862
872
  * preview iframe so image exports (PNG/SVG) can crop to just that frame instead
@@ -3741,10 +3751,17 @@ function DesignCollaboratorsMenu({
3741
3751
  return (
3742
3752
  <DropdownMenuItem
3743
3753
  key={user.email}
3744
- onSelect={() => {
3745
- if (!collaborator.isCurrent) onAvatarClick?.(user);
3754
+ onSelect={(event) => {
3755
+ if (collaborator.isCurrent) {
3756
+ event.preventDefault();
3757
+ return;
3758
+ }
3759
+ onAvatarClick?.(user);
3746
3760
  }}
3747
- className="gap-2"
3761
+ className={cn(
3762
+ "gap-2",
3763
+ collaborator.isCurrent && "cursor-default",
3764
+ )}
3748
3765
  >
3749
3766
  <DesignCollaboratorAvatar collaborator={collaborator} />
3750
3767
  <span className="min-w-0 flex-1">
@@ -3755,9 +3772,17 @@ function DesignCollaboratorsMenu({
3755
3772
  {user.email}
3756
3773
  </span>
3757
3774
  </span>
3758
- {isFollowing ? (
3775
+ {collaborator.isCurrent ? (
3776
+ <span className="text-xs text-muted-foreground">
3777
+ {"You" /* i18n-ignore collaborator row */}
3778
+ </span>
3779
+ ) : isFollowing ? (
3759
3780
  <IconCheck className="size-3.5 text-[var(--design-editor-accent-color)]" />
3760
- ) : null}
3781
+ ) : (
3782
+ <span className="text-xs text-muted-foreground">
3783
+ {"Follow" /* i18n-ignore collaborator row */}
3784
+ </span>
3785
+ )}
3761
3786
  </DropdownMenuItem>
3762
3787
  );
3763
3788
  })}
@@ -3766,6 +3791,26 @@ function DesignCollaboratorsMenu({
3766
3791
  );
3767
3792
  }
3768
3793
 
3794
+ function ReadOnlyEditorPanel({
3795
+ title,
3796
+ description,
3797
+ }: {
3798
+ title: string;
3799
+ description: string;
3800
+ }) {
3801
+ return (
3802
+ <div className="flex min-h-0 flex-1 flex-col items-center justify-center px-5 text-center">
3803
+ <div className="mb-3 flex size-10 items-center justify-center rounded-lg bg-muted text-muted-foreground">
3804
+ <IconLock className="size-5" />
3805
+ </div>
3806
+ <p className="text-sm font-medium text-foreground">{title}</p>
3807
+ <p className="mt-1 max-w-56 text-xs leading-5 text-muted-foreground">
3808
+ {description}
3809
+ </p>
3810
+ </div>
3811
+ );
3812
+ }
3813
+
3769
3814
  function externalPreviewUrlForContent(content: string): string | null {
3770
3815
  const trimmed = content.trim();
3771
3816
  if (!/^https?:\/\//i.test(trimmed)) return null;
@@ -5821,12 +5866,17 @@ export default function DesignEditor() {
5821
5866
  const next = titleDraft.trim();
5822
5867
  if (!next || next === design?.title) return;
5823
5868
 
5869
+ const designQueryKey = ["action", "get-design", { id }];
5870
+ const previousDesign = queryClient.getQueryData(designQueryKey);
5871
+ const previousListDesignsQueries = queryClient.getQueriesData({
5872
+ queryKey: ["action", "list-designs"],
5873
+ });
5824
5874
  queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
5825
5875
  if (!old || typeof old !== "object") return old;
5826
5876
  return { ...old, title: next };
5827
5877
  });
5828
- queryClient.setQueryData(
5829
- ["action", "list-designs", undefined],
5878
+ queryClient.setQueriesData(
5879
+ { queryKey: ["action", "list-designs"] },
5830
5880
  (old: any) => {
5831
5881
  if (!old) return old;
5832
5882
  return {
@@ -5840,7 +5890,12 @@ export default function DesignEditor() {
5840
5890
 
5841
5891
  updateDesignMutation.mutate({ id, title: next } as any, {
5842
5892
  onError: () => {
5893
+ queryClient.setQueryData(designQueryKey, previousDesign);
5894
+ for (const [queryKey, data] of previousListDesignsQueries) {
5895
+ queryClient.setQueryData(queryKey, data);
5896
+ }
5843
5897
  queryClient.invalidateQueries({ queryKey: ["action", "get-design"] });
5898
+ queryClient.invalidateQueries({ queryKey: ["action", "list-designs"] });
5844
5899
  },
5845
5900
  });
5846
5901
  }, [
@@ -8976,8 +9031,11 @@ export default function DesignEditor() {
8976
9031
  files: UploadedFile[],
8977
9032
  options: PromptComposerSubmitOptions,
8978
9033
  ) => {
8979
- if (!canEditDesign || !design || generating || pendingGenerationActive)
9034
+ if (!canEditDesign) {
9035
+ toast.error(TWEAK_CONTROLS_EDIT_ACCESS_MESSAGE);
8980
9036
  return;
9037
+ }
9038
+ if (!design) return;
8981
9039
  const trimmed = prompt.trim();
8982
9040
  if (!trimmed) return;
8983
9041
  const fileContext = formatUploadedFileContext(files);
@@ -9010,6 +9068,7 @@ export default function DesignEditor() {
9010
9068
  context,
9011
9069
  submit: true,
9012
9070
  openSidebar: true,
9071
+ newTab: true,
9013
9072
  model: options.model,
9014
9073
  engine: options.engine,
9015
9074
  effort: options.effort,
@@ -9021,10 +9080,8 @@ export default function DesignEditor() {
9021
9080
  activeFile,
9022
9081
  canEditDesign,
9023
9082
  design,
9024
- generating,
9025
9083
  handleTweakPromptOpenChange,
9026
9084
  id,
9027
- pendingGenerationActive,
9028
9085
  tweakSelections,
9029
9086
  tweaks,
9030
9087
  ],
@@ -9214,6 +9271,45 @@ export default function DesignEditor() {
9214
9271
  });
9215
9272
  }, [activeFile, design?.title, id, selectedCodeLayerNode, selectedElement]);
9216
9273
 
9274
+ const handleAssetInserted = useCallback(
9275
+ (selection: {
9276
+ fileId?: string;
9277
+ nodeId?: string;
9278
+ selector?: string;
9279
+ title?: string;
9280
+ }) => {
9281
+ if (selection.fileId) {
9282
+ setActiveFileId(selection.fileId);
9283
+ if (viewModeRef.current === "overview") {
9284
+ setOverviewSelectedScreenIds([selection.fileId]);
9285
+ }
9286
+ }
9287
+ if (selection.nodeId) {
9288
+ setSelectedLayerIdsState([selection.nodeId]);
9289
+ }
9290
+ if (selection.selector || selection.nodeId) {
9291
+ setSelectedElement({
9292
+ tagName: "section",
9293
+ sourceId: selection.nodeId,
9294
+ selector:
9295
+ selection.selector ??
9296
+ `[data-agent-native-node-id="${selection.nodeId}"]`,
9297
+ classes: [],
9298
+ computedStyles: {},
9299
+ boundingRect: { x: 0, y: 0, width: 0, height: 0 },
9300
+ textContent: selection.title,
9301
+ isFlexChild: false,
9302
+ isFlexContainer: false,
9303
+ });
9304
+ }
9305
+ setHoveredElement(null);
9306
+ setHoveredElementScreenId(null);
9307
+ setActiveTool("move");
9308
+ setMode("edit");
9309
+ },
9310
+ [],
9311
+ );
9312
+
9217
9313
  const designExtensionContext = useMemo<DesignExtensionSlotContext>(
9218
9314
  () => ({
9219
9315
  designId: id ?? "",
@@ -9250,6 +9346,7 @@ export default function DesignEditor() {
9250
9346
  updatedAt,
9251
9347
  });
9252
9348
  },
9349
+ onAssetInserted: handleAssetInserted,
9253
9350
  }),
9254
9351
  [
9255
9352
  activeContent,
@@ -9262,6 +9359,7 @@ export default function DesignEditor() {
9262
9359
  clearShaderFillPreview,
9263
9360
  design?.title,
9264
9361
  files,
9362
+ handleAssetInserted,
9265
9363
  id,
9266
9364
  mode,
9267
9365
  overviewSelectedScreenIds,
@@ -13326,22 +13424,44 @@ export default function DesignEditor() {
13326
13424
  );
13327
13425
  // When an element is selected, crop the export to just that frame.
13328
13426
  const cropRect = resolveExportCropRect(doc, selectedElement);
13329
- const canvas = await html2canvas(doc.documentElement, {
13330
- width,
13331
- height,
13332
- windowWidth: width,
13333
- windowHeight: height,
13334
- scale: exportScale,
13335
- useCORS: true,
13336
- foreignObjectRendering: true,
13337
- backgroundColor: null,
13338
- onclone: (clonedDocument) => {
13339
- // Sanitize colors first: it aligns source/clone elements by index,
13340
- // so remove the editor-chrome overlays only afterwards.
13341
- sanitizeHtml2CanvasClone(doc, clonedDocument);
13342
- removeEditorChromeOverlays(clonedDocument);
13343
- },
13344
- });
13427
+ let canvas: HTMLCanvasElement;
13428
+ try {
13429
+ canvas = await html2canvas(doc.documentElement, {
13430
+ width,
13431
+ height,
13432
+ windowWidth: width,
13433
+ windowHeight: height,
13434
+ scale: exportScale,
13435
+ useCORS: true,
13436
+ foreignObjectRendering: true,
13437
+ backgroundColor: null,
13438
+ onclone: (clonedDocument) => {
13439
+ // Sanitize colors first: it aligns source/clone elements by index,
13440
+ // so remove the editor-chrome overlays only afterwards.
13441
+ sanitizeHtml2CanvasClone(doc, clonedDocument);
13442
+ removeEditorChromeOverlays(clonedDocument);
13443
+ },
13444
+ });
13445
+ } catch (primaryError) {
13446
+ console.warn(
13447
+ "PNG export failed with foreignObjectRendering; retrying canvas renderer:",
13448
+ primaryError,
13449
+ );
13450
+ canvas = await html2canvas(doc.documentElement, {
13451
+ width,
13452
+ height,
13453
+ windowWidth: width,
13454
+ windowHeight: height,
13455
+ scale: exportScale,
13456
+ useCORS: true,
13457
+ foreignObjectRendering: false,
13458
+ backgroundColor: null,
13459
+ onclone: (clonedDocument) => {
13460
+ sanitizeHtml2CanvasClone(doc, clonedDocument);
13461
+ removeEditorChromeOverlays(clonedDocument);
13462
+ },
13463
+ });
13464
+ }
13345
13465
  // Render the whole page first, then crop, so ancestor backgrounds show
13346
13466
  // through the selected frame exactly as they do on screen.
13347
13467
  const outputCanvas = cropRect
@@ -13465,7 +13585,9 @@ export default function DesignEditor() {
13465
13585
  body.style.minHeight = `${height}px`;
13466
13586
  }
13467
13587
 
13468
- const serializedHtml = new XMLSerializer().serializeToString(clone);
13588
+ const serializedHtml = sanitizeSerializedXmlForSvg(
13589
+ new XMLSerializer().serializeToString(clone),
13590
+ );
13469
13591
  const safeTitle =
13470
13592
  design?.title
13471
13593
  ?.replace(/&/g, "&amp;")
@@ -13661,7 +13783,7 @@ ${serializedHtml}
13661
13783
  type="button"
13662
13784
  onClick={selectedShareExportOption.onDownload}
13663
13785
  disabled={selectedShareExportOption.disabled}
13664
- className="h-8 gap-1.5 rounded-md px-3 text-[12px]"
13786
+ className="h-8 gap-1.5 rounded-md bg-[var(--design-editor-accent-color)] px-3 text-[12px] text-[var(--design-editor-accent-contrast-color)] shadow-none hover:bg-[var(--design-editor-accent-hover-color)] hover:text-[var(--design-editor-accent-contrast-color)] disabled:bg-muted disabled:text-muted-foreground"
13665
13787
  >
13666
13788
  <IconDownload className="size-3.5" />
13667
13789
  {"Download" /* i18n-ignore share export action */}
@@ -15495,6 +15617,20 @@ ${serializedHtml}
15495
15617
  );
15496
15618
 
15497
15619
  const zoomLabel = `${Math.round(zoom)}%`;
15620
+ const [zoomMenuOpen, setZoomMenuOpen] = useState(false);
15621
+ const [zoomInputValue, setZoomInputValue] = useState(zoomLabel);
15622
+ useEffect(() => {
15623
+ if (!zoomMenuOpen) setZoomInputValue(zoomLabel);
15624
+ }, [zoomLabel, zoomMenuOpen]);
15625
+ const commitZoomInput = useCallback(() => {
15626
+ const next = Number(zoomInputValue.replace("%", "").trim());
15627
+ if (!Number.isFinite(next)) {
15628
+ setZoomInputValue(zoomLabel);
15629
+ return;
15630
+ }
15631
+ setZoom(Math.max(10, Math.min(500, next)));
15632
+ setZoomMenuOpen(false);
15633
+ }, [setZoom, zoomInputValue, zoomLabel]);
15498
15634
 
15499
15635
  const handleTokensApplied = useCallback(
15500
15636
  (resolvedCssVars: Record<string, string>) => {
@@ -15834,8 +15970,8 @@ ${serializedHtml}
15834
15970
  </span>
15835
15971
  );
15836
15972
 
15837
- const zoomControl = (
15838
- <DropdownMenu>
15973
+ const renderZoomControl = () => (
15974
+ <DropdownMenu open={zoomMenuOpen} onOpenChange={setZoomMenuOpen}>
15839
15975
  <Tooltip>
15840
15976
  <TooltipTrigger asChild>
15841
15977
  <DropdownMenuTrigger asChild>
@@ -15851,29 +15987,66 @@ ${serializedHtml}
15851
15987
  </TooltipTrigger>
15852
15988
  <TooltipContent>{t("designEditor.zoom")}</TooltipContent>
15853
15989
  </Tooltip>
15854
- <DropdownMenuContent align="end" className="w-40">
15855
- <DropdownMenuItem onClick={handleZoomOut}>
15856
- <IconZoomOut className="mr-2 h-4 w-4" />
15857
- {t("designEditor.zoomOut")}
15990
+ <DropdownMenuContent
15991
+ align="end"
15992
+ className="w-72 overflow-hidden rounded-xl border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] p-0 shadow-2xl"
15993
+ >
15994
+ <div className="p-3">
15995
+ <Input
15996
+ autoFocus
15997
+ value={zoomInputValue}
15998
+ onChange={(event) => setZoomInputValue(event.target.value)}
15999
+ onFocus={(event) => event.currentTarget.select()}
16000
+ onKeyDown={(event) => {
16001
+ event.stopPropagation();
16002
+ if (event.key === "Enter") {
16003
+ event.preventDefault();
16004
+ commitZoomInput();
16005
+ } else if (event.key === "Escape") {
16006
+ event.preventDefault();
16007
+ setZoomInputValue(zoomLabel);
16008
+ setZoomMenuOpen(false);
16009
+ }
16010
+ }}
16011
+ className="h-10 rounded-md border-[var(--design-editor-accent-color)] bg-[var(--design-editor-control-bg)] px-3 text-base font-medium tabular-nums text-foreground shadow-none focus-visible:ring-2 focus-visible:ring-[var(--design-editor-accent-color)]"
16012
+ aria-label={"Zoom percentage" /* i18n-ignore zoom field */}
16013
+ />
16014
+ </div>
16015
+ <DropdownMenuSeparator />
16016
+ <DropdownMenuItem
16017
+ onClick={handleZoomIn}
16018
+ className="h-12 px-12 text-[15px]"
16019
+ >
16020
+ <span className="flex-1">{"Zoom in" /* i18n-ignore */}</span>
16021
+ <DropdownMenuShortcut>⌘+</DropdownMenuShortcut>
15858
16022
  </DropdownMenuItem>
15859
- <DropdownMenuItem onClick={handleZoomIn}>
15860
- <IconZoomIn className="mr-2 h-4 w-4" />
15861
- {t("designEditor.zoomIn")}
16023
+ <DropdownMenuItem
16024
+ onClick={handleZoomOut}
16025
+ className="h-12 px-12 text-[15px]"
16026
+ >
16027
+ <span className="flex-1">{"Zoom out" /* i18n-ignore */}</span>
16028
+ <DropdownMenuShortcut>⌘−</DropdownMenuShortcut>
15862
16029
  </DropdownMenuItem>
15863
- <DropdownMenuItem onClick={handleZoomToFit}>
15864
- <IconArrowsMaximize className="mr-2 h-4 w-4" />
15865
- {"Fit to screen" /* i18n-ignore zoom option */}
16030
+ <DropdownMenuItem
16031
+ onClick={handleZoomToFit}
16032
+ className="h-12 px-12 text-[15px]"
16033
+ >
16034
+ <span className="flex-1">{"Zoom to fit" /* i18n-ignore */}</span>
15866
16035
  <DropdownMenuShortcut>⇧1</DropdownMenuShortcut>
15867
16036
  </DropdownMenuItem>
15868
- <DropdownMenuSeparator />
15869
- {ZOOM_PRESETS.map((preset) => (
16037
+ {[50, 100, 200].map((preset) => (
15870
16038
  <DropdownMenuItem
15871
16039
  key={preset}
15872
16040
  onClick={() => setZoom(preset)}
15873
- className="justify-between"
16041
+ className="h-12 px-12 text-[15px]"
15874
16042
  >
15875
- <span>{preset}%</span>
15876
- {Math.round(zoom) === preset && <IconCheck className="h-4 w-4" />}
16043
+ <span className="flex-1">
16044
+ {"Zoom to " /* i18n-ignore */}
16045
+ {preset}%
16046
+ </span>
16047
+ {preset === 100 ? (
16048
+ <DropdownMenuShortcut>⌘0</DropdownMenuShortcut>
16049
+ ) : null}
15877
16050
  </DropdownMenuItem>
15878
16051
  ))}
15879
16052
  </DropdownMenuContent>
@@ -16390,52 +16563,7 @@ ${serializedHtml}
16390
16563
  </DropdownMenuContent>
16391
16564
  </DropdownMenu>
16392
16565
 
16393
- {/* Zoom — collapsed into a single menu. */}
16394
- <DropdownMenu>
16395
- <Tooltip>
16396
- <TooltipTrigger asChild>
16397
- <DropdownMenuTrigger asChild>
16398
- <Button
16399
- variant="ghost"
16400
- size="sm"
16401
- className="h-7 gap-1 px-2 text-xs tabular-nums text-muted-foreground cursor-pointer"
16402
- >
16403
- {zoomLabel}
16404
- <IconChevronDown className="w-3 h-3 opacity-60" />
16405
- </Button>
16406
- </DropdownMenuTrigger>
16407
- </TooltipTrigger>
16408
- <TooltipContent>{t("designEditor.zoom")}</TooltipContent>
16409
- </Tooltip>
16410
- <DropdownMenuContent align="end" className="w-40">
16411
- <DropdownMenuItem onClick={handleZoomOut}>
16412
- <IconZoomOut className="mr-2 h-4 w-4" />
16413
- {t("designEditor.zoomOut")}
16414
- </DropdownMenuItem>
16415
- <DropdownMenuItem onClick={handleZoomIn}>
16416
- <IconZoomIn className="mr-2 h-4 w-4" />
16417
- {t("designEditor.zoomIn")}
16418
- </DropdownMenuItem>
16419
- <DropdownMenuItem onClick={handleZoomToFit}>
16420
- <IconArrowsMaximize className="mr-2 h-4 w-4" />
16421
- {"Fit to screen" /* i18n-ignore zoom option */}
16422
- <DropdownMenuShortcut>⇧1</DropdownMenuShortcut>
16423
- </DropdownMenuItem>
16424
- <DropdownMenuSeparator />
16425
- {ZOOM_PRESETS.map((preset) => (
16426
- <DropdownMenuItem
16427
- key={preset}
16428
- onClick={() => setZoom(preset)}
16429
- className="justify-between"
16430
- >
16431
- <span>{preset}%</span>
16432
- {Math.round(zoom) === preset && (
16433
- <IconCheck className="h-4 w-4" />
16434
- )}
16435
- </DropdownMenuItem>
16436
- ))}
16437
- </DropdownMenuContent>
16438
- </DropdownMenu>
16566
+ {renderZoomControl()}
16439
16567
 
16440
16568
  <div className="mx-1 h-5 w-px bg-border" />
16441
16569
  </>
@@ -16565,7 +16693,16 @@ ${serializedHtml}
16565
16693
  showScopeBadge={false}
16566
16694
  browserTabId={browserTabId}
16567
16695
  />
16568
- ) : null}
16696
+ ) : (
16697
+ <ReadOnlyEditorPanel
16698
+ title={
16699
+ "Agent chat requires editor access" /* i18n-ignore */
16700
+ }
16701
+ description={
16702
+ "Ask an owner for edit access before using the agent to change this design." /* i18n-ignore */
16703
+ }
16704
+ />
16705
+ )}
16569
16706
  </div>
16570
16707
  <div
16571
16708
  className={cn(
@@ -16578,7 +16715,16 @@ ${serializedHtml}
16578
16715
  {t("designEditor.leftRail.assets")}
16579
16716
  </h3>
16580
16717
  </div>
16581
- <AssetLibraryPanel context={designExtensionContext} />
16718
+ {canEditDesign ? (
16719
+ <AssetLibraryPanel context={designExtensionContext} />
16720
+ ) : (
16721
+ <ReadOnlyEditorPanel
16722
+ title={"Assets require editor access" /* i18n-ignore */}
16723
+ description={
16724
+ "Ask an owner for edit access before inserting assets into this design." /* i18n-ignore */
16725
+ }
16726
+ />
16727
+ )}
16582
16728
  </div>
16583
16729
  <div
16584
16730
  className={cn(
@@ -16586,11 +16732,20 @@ ${serializedHtml}
16586
16732
  activeLeftPanel === "tools" ? "flex" : "hidden",
16587
16733
  )}
16588
16734
  >
16589
- <DesignExtensionsPanel
16590
- context={designExtensionContext}
16591
- hideAssetLibrary
16592
- title={t("designEditor.leftRail.tools")}
16593
- />
16735
+ {canEditDesign ? (
16736
+ <DesignExtensionsPanel
16737
+ context={designExtensionContext}
16738
+ hideAssetLibrary
16739
+ title={t("designEditor.leftRail.tools")}
16740
+ />
16741
+ ) : (
16742
+ <ReadOnlyEditorPanel
16743
+ title={"Tools require editor access" /* i18n-ignore */}
16744
+ description={
16745
+ "Ask an owner for edit access before running tools or creating extensions for this design." /* i18n-ignore */
16746
+ }
16747
+ />
16748
+ )}
16594
16749
  </div>
16595
16750
  <div
16596
16751
  className={cn(
@@ -16598,7 +16753,7 @@ ${serializedHtml}
16598
16753
  activeLeftPanel === "tokens" ? "flex" : "hidden",
16599
16754
  )}
16600
16755
  >
16601
- {id ? (
16756
+ {id && canEditDesign ? (
16602
16757
  <>
16603
16758
  <div className="flex min-h-8 shrink-0 items-center border-b border-border/60 px-3">
16604
16759
  <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
@@ -16612,7 +16767,14 @@ ${serializedHtml}
16612
16767
  />
16613
16768
  </div>
16614
16769
  </>
16615
- ) : null}
16770
+ ) : (
16771
+ <ReadOnlyEditorPanel
16772
+ title={"Tokens require editor access" /* i18n-ignore */}
16773
+ description={
16774
+ "Ask an owner for edit access before importing, creating, or applying tokens." /* i18n-ignore */
16775
+ }
16776
+ />
16777
+ )}
16616
16778
  </div>
16617
16779
  </div>
16618
16780
  <div
@@ -17475,7 +17637,7 @@ ${serializedHtml}
17475
17637
  selectedElements={selectedInspectorElements}
17476
17638
  pageStyles={pageStyles}
17477
17639
  zoom={zoom}
17478
- headerTrailing={zoomControl}
17640
+ headerTrailing={renderZoomControl()}
17479
17641
  width={rightSidebarWidth}
17480
17642
  activeTab={activeInspectorTab}
17481
17643
  onActiveTabChange={setActiveInspectorTab}
@@ -17735,7 +17897,7 @@ ${serializedHtml}
17735
17897
  title={t("designEditor.tweaksPromptTitle")}
17736
17898
  placeholder={t("designEditor.tweaksPlaceholder")}
17737
17899
  onSubmit={handleTweakPromptSubmit}
17738
- loading={generating || pendingGenerationActive}
17900
+ loading={false}
17739
17901
  anchorRef={tweakPromptAnchorRef}
17740
17902
  />
17741
17903
 
@@ -59,6 +59,20 @@ interface BuilderIndexResult {
59
59
  instructions?: string;
60
60
  }
61
61
 
62
+ async function readJsonResponse(res: Response): Promise<any> {
63
+ const text = await res.text();
64
+ if (!text.trim()) return {};
65
+ try {
66
+ return JSON.parse(text);
67
+ } catch {
68
+ return {
69
+ error: res.ok
70
+ ? "The server returned an invalid response."
71
+ : text.slice(0, 240),
72
+ };
73
+ }
74
+ }
75
+
62
76
  export default function DesignSystemSetup() {
63
77
  const t = useT();
64
78
  const navigate = useNavigate();
@@ -127,7 +141,7 @@ export default function DesignSystemSetup() {
127
141
  body,
128
142
  },
129
143
  );
130
- const json = await res.json();
144
+ const json = await readJsonResponse(res);
131
145
  if (!res.ok || json?.error) {
132
146
  throw new Error(json?.error || `Upload failed (${res.status})`);
133
147
  }
@@ -189,6 +189,13 @@ export default function Index() {
189
189
  });
190
190
  }, [filtered]);
191
191
 
192
+ const handleSearchChange = useCallback((query: string) => {
193
+ setSearch(query);
194
+ setSelectedDesignIds((current) =>
195
+ current.size === 0 ? current : new Set(),
196
+ );
197
+ }, []);
198
+
192
199
  const clearSelection = useCallback(() => {
193
200
  setSelectedDesignIds(new Set());
194
201
  }, []);
@@ -359,14 +366,18 @@ export default function Index() {
359
366
  setRenameId(null);
360
367
  if (!next) return;
361
368
 
362
- queryClient.setQueryData(
363
- ["action", "list-designs", { includePreview: "true" }],
364
- (old: any) => ({
365
- count: old?.count ?? 0,
366
- designs: (old?.designs ?? []).map((d: Design) =>
367
- d.id === id ? { ...d, title: next } : d,
368
- ),
369
- }),
369
+ queryClient.setQueriesData(
370
+ { queryKey: ["action", "list-designs"] },
371
+ (old: any) => {
372
+ if (!old || typeof old !== "object") return old;
373
+ return {
374
+ ...old,
375
+ count: old.count ?? (old.designs ?? []).length,
376
+ designs: (old.designs ?? []).map((d: Design) =>
377
+ d.id === id ? { ...d, title: next } : d,
378
+ ),
379
+ };
380
+ },
370
381
  );
371
382
 
372
383
  updateMutation.mutate({ id, title: next } as any, {
@@ -397,7 +408,7 @@ export default function Index() {
397
408
  <IconSearch className="absolute start-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/70" />
398
409
  <Input
399
410
  value={search}
400
- onChange={(e) => setSearch(e.target.value)}
411
+ onChange={(e) => handleSearchChange(e.target.value)}
401
412
  placeholder={t("home.searchPlaceholder")}
402
413
  className="ps-8 h-8 w-48 bg-accent/50 border-border text-sm text-foreground/90 placeholder:text-muted-foreground/70"
403
414
  />
@@ -66,7 +66,7 @@ export default function SettingsRoute() {
66
66
  <CardContent>
67
67
  <button
68
68
  type="button"
69
- className="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
69
+ className="inline-flex h-9 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
70
70
  onClick={() => openAgentSettings()}
71
71
  >
72
72
  {t("settings.openAgentSettings")}