@agent-native/core 0.128.0 → 0.128.2

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 (132) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +27 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/server/builder-design-systems.ts +299 -82
  5. package/corpus/core/src/server/email-template.ts +33 -0
  6. package/corpus/core/src/server/index.ts +8 -0
  7. package/corpus/templates/analytics/_gitignore +2 -0
  8. package/corpus/templates/assets/_gitignore +2 -0
  9. package/corpus/templates/calendar/_gitignore +2 -0
  10. package/corpus/templates/chat/_gitignore +2 -0
  11. package/corpus/templates/clips/AGENTS.md +6 -1
  12. package/corpus/templates/clips/_gitignore +2 -0
  13. package/corpus/templates/clips/actions/complete-transactional-email-summary.ts +56 -0
  14. package/corpus/templates/clips/actions/import-loom-recording.ts +258 -76
  15. package/corpus/templates/clips/actions/lib/direct-video.ts +96 -0
  16. package/corpus/templates/clips/actions/lib/loom-import-job.ts +53 -2
  17. package/corpus/templates/clips/actions/lib/loom-video.ts +2 -62
  18. package/corpus/templates/clips/actions/lib/video-download-limits.ts +62 -0
  19. package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +249 -0
  20. package/corpus/templates/clips/app/components/library/library-grid.tsx +65 -0
  21. package/corpus/templates/clips/app/components/library/library-layout.tsx +55 -0
  22. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +28 -126
  23. package/corpus/templates/clips/app/global.css +23 -0
  24. package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +98 -0
  25. package/corpus/templates/clips/app/i18n/en-US.ts +20 -0
  26. package/corpus/templates/clips/app/routes/_app.tsx +2 -0
  27. package/corpus/templates/clips/app/routes/import.tsx +396 -0
  28. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
  29. package/corpus/templates/clips/app/routes/record.tsx +13 -71
  30. package/corpus/templates/clips/changelog/2026-07-25-two-clip-emails-now-include-a-concise-summary-generated-secu.md +6 -0
  31. package/corpus/templates/clips/data/clips-transactional-emails/config.json +3 -0
  32. package/corpus/templates/clips/server/jobs/transactional-emails.ts +1034 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-store.ts +865 -0
  34. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +270 -0
  35. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  36. package/corpus/templates/clips/server/routes/api/view-event.post.ts +59 -2
  37. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
  38. package/corpus/templates/content/AGENTS.md +8 -0
  39. package/corpus/templates/content/_gitignore +2 -0
  40. package/corpus/templates/content/actions/_content-database-personal-view.ts +28 -3
  41. package/corpus/templates/content/actions/_property-utils.ts +32 -2
  42. package/corpus/templates/content/actions/configure-document-property.ts +12 -2
  43. package/corpus/templates/content/actions/delete-document-property.ts +13 -3
  44. package/corpus/templates/content/actions/duplicate-document-property.ts +13 -3
  45. package/corpus/templates/content/actions/list-content-spaces.ts +5 -2
  46. package/corpus/templates/content/actions/list-document-properties.ts +12 -3
  47. package/corpus/templates/content/actions/move-database-item.ts +74 -55
  48. package/corpus/templates/content/actions/reorder-document-property.ts +20 -4
  49. package/corpus/templates/content/actions/set-document-property.ts +10 -1
  50. package/corpus/templates/content/actions/update-content-database-personal-view.ts +7 -0
  51. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +31 -21
  52. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +30 -3
  53. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +8 -1
  54. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +72 -20
  55. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +79 -10
  56. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +8 -1
  57. package/corpus/templates/content/app/components/editor/database/settings.tsx +6 -0
  58. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +246 -26
  59. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +491 -197
  60. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +3 -3
  61. package/corpus/templates/content/app/components/sidebar/sidebar-reorder.tsx +409 -0
  62. package/corpus/templates/content/app/hooks/use-content-database.ts +117 -3
  63. package/corpus/templates/content/app/hooks/use-content-spaces.ts +1 -0
  64. package/corpus/templates/content/app/hooks/use-document-properties.ts +62 -18
  65. package/corpus/templates/content/app/hooks/use-documents.ts +13 -5
  66. package/corpus/templates/content/app/i18n-data.ts +261 -3
  67. package/corpus/templates/content/app/routes/_app.page.$id.tsx +9 -2
  68. package/corpus/templates/content/changelog/2026-07-24-database-page-properties-keep-their-context.md +6 -0
  69. package/corpus/templates/content/changelog/2026-07-26-sidebar-references-can-now-be-personally-reordered.md +6 -0
  70. package/corpus/templates/content/docs/solutions/2026-07-24-database-property-context-switch-diagnosis.md +249 -0
  71. package/corpus/templates/content/docs/solutions/content-sidebar-order-shape.md +389 -0
  72. package/corpus/templates/content/shared/api.ts +28 -0
  73. package/corpus/templates/design/_gitignore +2 -0
  74. package/corpus/templates/design/app/i18n-data.ts +10 -0
  75. package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
  76. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
  77. package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
  78. package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
  79. package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
  80. package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
  81. package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
  82. package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
  83. package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
  84. package/corpus/templates/dispatch/_gitignore +2 -0
  85. package/corpus/templates/forms/_gitignore +2 -0
  86. package/corpus/templates/macros/_gitignore +2 -0
  87. package/corpus/templates/mail/_gitignore +2 -0
  88. package/corpus/templates/plan/_gitignore +2 -0
  89. package/corpus/templates/slides/_gitignore +2 -0
  90. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
  91. package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
  92. package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
  93. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  94. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
  95. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
  96. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
  97. package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
  98. package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
  99. package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
  100. package/corpus/templates/tasks/_gitignore +2 -0
  101. package/corpus/toolkit/CHANGELOG.md +6 -0
  102. package/corpus/toolkit/package.json +1 -1
  103. package/corpus/toolkit/src/editor/DragHandle.ts +63 -9
  104. package/dist/collab/routes.d.ts +1 -1
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/observability/routes.d.ts +3 -3
  107. package/dist/progress/routes.d.ts +1 -1
  108. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  109. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  110. package/dist/server/builder-design-systems.d.ts +49 -0
  111. package/dist/server/builder-design-systems.d.ts.map +1 -1
  112. package/dist/server/builder-design-systems.js +221 -69
  113. package/dist/server/builder-design-systems.js.map +1 -1
  114. package/dist/server/email-template.d.ts +7 -0
  115. package/dist/server/email-template.d.ts.map +1 -1
  116. package/dist/server/email-template.js +24 -0
  117. package/dist/server/email-template.js.map +1 -1
  118. package/dist/server/index.d.ts +1 -1
  119. package/dist/server/index.d.ts.map +1 -1
  120. package/dist/server/index.js +1 -1
  121. package/dist/server/index.js.map +1 -1
  122. package/dist/server/transcribe-voice.d.ts +1 -1
  123. package/dist/templates/chat/_gitignore +2 -0
  124. package/package.json +5 -3
  125. package/src/server/builder-design-systems.ts +299 -82
  126. package/src/server/email-template.ts +33 -0
  127. package/src/server/index.ts +8 -0
  128. package/src/templates/chat/_gitignore +2 -0
  129. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
  130. package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
  131. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
  132. package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +0 -1
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1625
32
32
  - toolkit files: 160
33
- - template files: 6826
33
+ - template files: 6853
@@ -1,5 +1,32 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.128.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 363d36f: Add organization logo support to the shared email template, safe sender display-name overrides, and a server-side user profile export for transactional emails.
8
+
9
+ ## 0.128.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 9140268: Update Builder design-system indexing to call the current `/design-systems/v1/index`
14
+ endpoint with a structured `sources` array (uploaded files, public repos, connected
15
+ projects) instead of the retired `generate` endpoint and its flat `uploads` payload.
16
+ File selections are now attached per source, and the incomplete-response guard only
17
+ requires a `designSystemId`. "Open in Builder" now links into the actual
18
+ project/branch (`branchUrl`) when the service returns one, falling back to the
19
+ design-system-intelligence docs URL. When `/index` returns only a `jobId`, the
20
+ Fusion branch URL is read from `GET /design-systems/v1/decode-jobs/:jobId`
21
+ (exposed as `fetchBuilderDesignSystemDecodeJobStatus`) so "Open in Builder" lands
22
+ on the branch. Large files (notably `.fig`) now stream to
23
+ storage in 16 MiB resumable chunks with retry and offset recovery instead of a
24
+ single unbounded request body. Indexing is split into `startBuilderDesignSystemUpload`
25
+ (opens signed resumable-upload slots) and `indexBuilderDesignSystem` (finalizes from
26
+ resolved sources) so browsers can stream `.fig` bytes straight to storage and never
27
+ hit the serverless request-body cap; `startBuilderDesignSystemIndex` still handles
28
+ small in-memory server-side payloads. Also exports `builderProjectBranchUrl`.
29
+
3
30
  ## 0.128.0
4
31
 
5
32
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.128.0",
3
+ "version": "0.128.2",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -164,6 +164,7 @@
164
164
  "./application-state": "./dist/application-state/index.js",
165
165
  "./settings": "./dist/settings/index.js",
166
166
  "./user-profile": "./dist/user-profile/index.js",
167
+ "./user-profile/server": "./dist/user-profile/store.js",
167
168
  "./feature-flags": "./dist/feature-flags/index.js",
168
169
  "./feature-flags/registry": "./dist/feature-flags/registry.js",
169
170
  "./feature-flags/actions/get-feature-flags": "./dist/feature-flags/actions/get-feature-flags.js",
@@ -338,6 +339,7 @@
338
339
  "@assistant-ui/react-markdown": "^0.12.6",
339
340
  "@assistant-ui/store": ">=0.2.9 <0.2.14",
340
341
  "@assistant-ui/tap": "^0.5.14",
342
+ "@builder.io/ai-utils": "^0.84.0",
341
343
  "@clack/prompts": "^1.4.0",
342
344
  "@codemirror/lang-sql": "^6.10.0",
343
345
  "@codemirror/theme-one-dark": "^6.1.3",
@@ -1,3 +1,5 @@
1
+ import type { DesignSystemSourceInput } from "@builder.io/ai-utils";
2
+
1
3
  import { withBuilderUtmTrackingParams } from "../shared/builder-link-tracking.js";
2
4
  import { FeatureNotConfiguredError } from "./credential-provider.js";
3
5
  import {
@@ -7,6 +9,12 @@ import {
7
9
 
8
10
  const DEFAULT_TIMEOUT_MS = 120_000;
9
11
 
12
+ // GCS resumable uploads require every chunk except the last to be a multiple
13
+ // of 256 KiB. 16 MiB is the recommended default and keeps very large `.fig`
14
+ // files off a single unbounded request body.
15
+ const GCS_CHUNK_SIZE = 16 * 1024 * 1024;
16
+ const MAX_CHUNK_RETRIES = 5;
17
+
10
18
  export interface BuilderDesignSystemIndexFile {
11
19
  name: string;
12
20
  data: Uint8Array;
@@ -123,16 +131,65 @@ interface UploadStartResponse {
123
131
  uploads?: Array<{ idx: number; uploadUrl: string; uploadToken: string }>;
124
132
  }
125
133
 
126
- interface GenerateResponse {
127
- projectId?: string;
128
- jobId?: string;
134
+ interface IndexResponse {
129
135
  designSystemId?: string;
136
+ jobId?: string;
137
+ projectId?: string;
138
+ branchUrl?: string;
139
+ branchName?: string;
140
+ }
141
+
142
+ export interface BuilderDesignSystemUploadAttachment {
143
+ name: string;
144
+ mimetype: string;
145
+ declaredSize: number;
146
+ }
147
+
148
+ export interface BuilderDesignSystemUploadSlot {
149
+ idx: number;
150
+ uploadUrl: string;
151
+ uploadToken: string;
152
+ }
153
+
154
+ export interface BuilderDesignSystemIndexFromSourcesOptions {
155
+ sources: DesignSystemSourceInput[];
156
+ projectName?: string;
157
+ devToolsVersion?: string;
158
+ }
159
+
160
+ export interface BuilderDesignSystemDecodeJobStatus {
161
+ jobId: string;
162
+ status: "pending" | "processing" | "complete" | "error";
163
+ framesProcessed: number;
164
+ totalFrames: number;
165
+ branchName: string | null;
166
+ branchUrl: string | null;
167
+ error: string | null;
168
+ partialFailure?: boolean;
169
+ createdAt: number;
170
+ updatedAt: number;
130
171
  }
131
172
 
132
173
  const DEFAULT_MAX_CODE_FILES = 50;
133
174
  const DEFAULT_MAX_TOTAL_CODE_BYTES = 2 * 1024 * 1024;
134
175
  const MAX_DOC_CONTENT_CHARS = 4_000;
135
176
 
177
+ export async function fetchBuilderDesignSystemDecodeJobStatus(
178
+ jobId: string,
179
+ ): Promise<BuilderDesignSystemDecodeJobStatus> {
180
+ const credentials = await resolveBuilderDesignSystemCredentials();
181
+ const url = makeBuilderDesignSystemUrl(
182
+ "decode-jobs/" + encodeURIComponent(jobId),
183
+ credentials,
184
+ );
185
+ const response = await fetchWithTimeout(url, {
186
+ method: "GET",
187
+ headers: makeBuilderHeaders(credentials),
188
+ });
189
+ await assertOk(response, "Builder design-system decode-job status failed");
190
+ return (await response.json()) as BuilderDesignSystemDecodeJobStatus;
191
+ }
192
+
136
193
  function trimTrailingSlash(value: string): string {
137
194
  return value.replace(/\/+$/, "");
138
195
  }
@@ -318,17 +375,45 @@ async function assertOk(response: Response, label: string): Promise<void> {
318
375
  );
319
376
  }
320
377
 
378
+ // GCS reports the highest committed byte in a `Range: bytes=0-<end>` header.
379
+ function committedOffsetFromRange(response: Response): number | null {
380
+ const match = response.headers.get("Range")?.match(/bytes=0-(\d+)/);
381
+ return match ? parseInt(match[1], 10) + 1 : null;
382
+ }
383
+
384
+ async function queryCommittedOffset(
385
+ sessionUrl: string,
386
+ total: number,
387
+ ): Promise<number> {
388
+ const response = await fetchWithTimeout(sessionUrl, {
389
+ method: "PUT",
390
+ headers: { "Content-Range": `bytes */${total}` },
391
+ });
392
+ if (response.status === 200 || response.status === 201) return total;
393
+ if (response.status === 308) {
394
+ return committedOffsetFromRange(response) ?? 0;
395
+ }
396
+ throw new Error(
397
+ `Builder design-system upload status query failed (${response.status}).`,
398
+ );
399
+ }
400
+
401
+ function delay(ms: number): Promise<void> {
402
+ return new Promise((resolve) => setTimeout(resolve, ms));
403
+ }
404
+
321
405
  async function uploadToResumableUrl(
322
406
  slot: { uploadUrl: string },
323
407
  file: BuilderDesignSystemIndexFile,
324
408
  ): Promise<void> {
325
409
  const mimeType = mimeTypeForFile(file);
326
410
  const bytes = file.data;
411
+ const total = bytes.byteLength;
327
412
  const start = await fetchWithTimeout(slot.uploadUrl, {
328
413
  method: "POST",
329
414
  headers: {
330
415
  "x-goog-resumable": "start",
331
- "x-goog-content-length-range": `0,${bytes.byteLength}`,
416
+ "x-goog-content-length-range": `0,${total}`,
332
417
  "Content-Type": mimeType,
333
418
  },
334
419
  });
@@ -338,15 +423,60 @@ async function uploadToResumableUrl(
338
423
  throw new Error("Builder design-system upload session returned no URL.");
339
424
  }
340
425
 
341
- const response = await fetchWithTimeout(sessionUrl, {
342
- method: "PUT",
343
- headers: {
344
- "Content-Range": `bytes 0-${bytes.byteLength - 1}/${bytes.byteLength}`,
345
- "Content-Type": mimeType,
346
- },
347
- body: makeBody(bytes, mimeType),
348
- });
349
- await assertOk(response, "Builder design-system file upload failed");
426
+ if (total === 0) {
427
+ const response = await fetchWithTimeout(sessionUrl, {
428
+ method: "PUT",
429
+ headers: { "Content-Range": "bytes */0" },
430
+ body: makeBody(bytes, mimeType),
431
+ });
432
+ await assertOk(response, "Builder design-system file upload failed");
433
+ return;
434
+ }
435
+
436
+ // A failed PUT may have still landed at GCS, so the local offset can't be
437
+ // trusted after an error — only GCS's committed-offset response is
438
+ // authoritative.
439
+ let offset = 0;
440
+ let retries = 0;
441
+ while (offset < total) {
442
+ const end = Math.min(offset + GCS_CHUNK_SIZE, total);
443
+ const isLast = end === total;
444
+ try {
445
+ const response = await fetchWithTimeout(sessionUrl, {
446
+ method: "PUT",
447
+ headers: {
448
+ "Content-Range": `bytes ${offset}-${end - 1}/${total}`,
449
+ "Content-Type": mimeType,
450
+ },
451
+ body: makeBody(bytes.subarray(offset, end), mimeType),
452
+ });
453
+ if (response.status === 200 || response.status === 201) {
454
+ offset = total;
455
+ } else if (!isLast && response.status === 308) {
456
+ const nextOffset = committedOffsetFromRange(response) ?? offset;
457
+ if (nextOffset <= offset) {
458
+ throw new Error(
459
+ `Builder design-system upload stalled at byte ${offset}.`,
460
+ );
461
+ }
462
+ offset = nextOffset;
463
+ } else {
464
+ throw new Error(
465
+ `Builder design-system file upload failed (${response.status}).`,
466
+ );
467
+ }
468
+ retries = 0;
469
+ } catch (err) {
470
+ if (++retries > MAX_CHUNK_RETRIES) throw err;
471
+ await delay(500 * retries);
472
+ try {
473
+ offset = await queryCommittedOffset(sessionUrl, total);
474
+ } catch {
475
+ // If the offset query also fails, retry from the last local offset —
476
+ // GCS's resumable PUT safely re-acknowledges bytes it already has.
477
+ }
478
+ }
479
+ }
350
480
  }
351
481
 
352
482
  function nonEmptyFiles(
@@ -368,6 +498,25 @@ export function builderDesignSystemUrl(designSystemId?: string | null): string {
368
498
  });
369
499
  }
370
500
 
501
+ export function builderProjectBranchUrl(
502
+ projectId?: string | null,
503
+ branchName?: string | null,
504
+ ): string | undefined {
505
+ const project = projectId?.trim();
506
+ const branch = branchName?.trim();
507
+ if (!project || !branch) return undefined;
508
+ const host = trimTrailingSlash(getBuilderAppHost());
509
+ const path =
510
+ "/app/projects/" +
511
+ encodeURIComponent(project) +
512
+ "/" +
513
+ encodeURIComponent(branch);
514
+ return withBuilderUtmTrackingParams(host + path, {
515
+ campaign: "product",
516
+ content: "design_system_intelligence",
517
+ });
518
+ }
519
+
371
520
  export function localBuilderDesignSystemId(
372
521
  builderDesignSystemId: string,
373
522
  ): string {
@@ -569,65 +718,58 @@ export async function hydrateBuilderDesignSystemReference(
569
718
  };
570
719
  }
571
720
 
572
- export async function startBuilderDesignSystemIndex(
573
- options: BuilderDesignSystemIndexOptions,
574
- ): Promise<BuilderDesignSystemIndexResult> {
575
- const files = nonEmptyFiles(options.files);
576
- const description = options.description?.trim();
577
- if (description) {
578
- files.unshift({
579
- name: "additional-context.txt",
580
- data: new TextEncoder().encode(description),
581
- mimeType: "text/plain",
582
- });
721
+ /**
722
+ * Opens signed resumable-upload slots for `.fig`/code/design attachments so
723
+ * the browser can stream each file's bytes straight to GCS. Large `.fig`
724
+ * files must not ride through the app server as one request body -- the
725
+ * serverless host caps request bodies well below Figma export sizes.
726
+ */
727
+ export async function startBuilderDesignSystemUpload(
728
+ attachments: BuilderDesignSystemUploadAttachment[],
729
+ ): Promise<BuilderDesignSystemUploadSlot[]> {
730
+ if (attachments.length === 0) return [];
731
+ const credentials = await resolveBuilderDesignSystemCredentials();
732
+ const uploadStart = await fetchWithTimeout(
733
+ makeBuilderDesignSystemUrl("upload/start", credentials),
734
+ {
735
+ method: "POST",
736
+ headers: {
737
+ ...makeBuilderHeaders(credentials),
738
+ "Content-Type": "application/json",
739
+ },
740
+ body: JSON.stringify({ attachments }),
741
+ },
742
+ );
743
+ await assertOk(uploadStart, "Builder design-system upload start failed");
744
+ const uploadJson = (await uploadStart.json()) as UploadStartResponse;
745
+ const slots = [...(uploadJson.uploads ?? [])].sort((a, b) => a.idx - b.idx);
746
+ if (slots.length !== attachments.length) {
747
+ throw new Error("Builder did not return upload slots for all files.");
583
748
  }
584
- if (
585
- files.length === 0 &&
586
- !options.githubRepoUrl &&
587
- !options.connectedProjectId
588
- ) {
749
+ for (let i = 0; i < slots.length; i++) {
750
+ if (slots[i].idx !== i) {
751
+ throw new Error("Builder upload slot mismatch: expected " + i + ".");
752
+ }
753
+ }
754
+ return slots;
755
+ }
756
+
757
+ /**
758
+ * Finalizes indexing from already-resolved sources (uploaded file tokens,
759
+ * public repos, connected projects). Callers that stream uploads from the
760
+ * browser pass the returned `uploadToken`s as `file` sources here.
761
+ */
762
+ export async function indexBuilderDesignSystem(
763
+ options: BuilderDesignSystemIndexFromSourcesOptions,
764
+ ): Promise<BuilderDesignSystemIndexResult> {
765
+ if (options.sources.length === 0) {
589
766
  throw new Error(
590
767
  "Provide at least one .fig/code/text file or a GitHub repository URL to index with Builder.",
591
768
  );
592
769
  }
593
-
594
770
  const credentials = await resolveBuilderDesignSystemCredentials();
595
- let uploadTokens: string[] = [];
596
- if (files.length > 0) {
597
- const uploadStart = await fetchWithTimeout(
598
- makeBuilderDesignSystemUrl("upload/start", credentials),
599
- {
600
- method: "POST",
601
- headers: {
602
- ...makeBuilderHeaders(credentials),
603
- "Content-Type": "application/json",
604
- },
605
- body: JSON.stringify({
606
- attachments: files.map((file) => ({
607
- name: file.name,
608
- mimetype: mimeTypeForFile(file),
609
- declaredSize: file.data.byteLength,
610
- })),
611
- }),
612
- },
613
- );
614
- await assertOk(uploadStart, "Builder design-system upload start failed");
615
- const uploadJson = (await uploadStart.json()) as UploadStartResponse;
616
- const slots = [...(uploadJson.uploads ?? [])].sort((a, b) => a.idx - b.idx);
617
- if (slots.length !== files.length) {
618
- throw new Error("Builder did not return upload slots for all files.");
619
- }
620
- for (let i = 0; i < slots.length; i++) {
621
- if (slots[i].idx !== i) {
622
- throw new Error(`Builder upload slot mismatch: expected ${i}.`);
623
- }
624
- await uploadToResumableUrl(slots[i], files[i]);
625
- }
626
- uploadTokens = slots.map((slot) => slot.uploadToken);
627
- }
628
-
629
- const generate = await fetchWithTimeout(
630
- makeBuilderDesignSystemUrl("generate", credentials),
771
+ const index = await fetchWithTimeout(
772
+ makeBuilderDesignSystemUrl("index", credentials),
631
773
  {
632
774
  method: "POST",
633
775
  headers: {
@@ -635,39 +777,114 @@ export async function startBuilderDesignSystemIndex(
635
777
  "Content-Type": "application/json",
636
778
  },
637
779
  body: JSON.stringify({
638
- uploads: uploadTokens,
780
+ sources: options.sources,
639
781
  ...(options.projectName?.trim()
640
- ? { projectName: options.projectName.trim() }
641
- : {}),
642
- ...(options.githubRepoUrl?.trim()
643
- ? { githubRepoUrl: options.githubRepoUrl.trim() }
782
+ ? { designSystemName: options.projectName.trim() }
644
783
  : {}),
645
- ...(options.connectedProjectId?.trim()
646
- ? { connectedProjectId: options.connectedProjectId.trim() }
647
- : {}),
648
- ...(options.selection ? { selection: options.selection } : {}),
649
784
  ...(options.devToolsVersion?.trim()
650
785
  ? { devToolsVersion: options.devToolsVersion.trim() }
651
786
  : {}),
652
787
  }),
653
788
  },
654
789
  );
655
- await assertOk(generate, "Builder design-system indexing failed");
656
- const generated = (await generate.json()) as GenerateResponse;
657
- if (!generated.projectId || !generated.jobId || !generated.designSystemId) {
790
+ await assertOk(index, "Builder design-system indexing failed");
791
+ const indexed = (await index.json()) as IndexResponse;
792
+ if (!indexed.designSystemId) {
658
793
  throw new Error(
659
794
  "Builder design-system indexing returned an incomplete response.",
660
795
  );
661
796
  }
662
797
 
798
+ const jobId = indexed.jobId ?? "";
799
+ // The `.fig` decode job creates the Fusion branch asynchronously, so
800
+ // `/index` usually can't return a branchUrl yet — the caller polls the
801
+ // decode-job status endpoint for it once the job completes.
802
+ const branchUrl = indexed.branchUrl?.trim() || null;
803
+
663
804
  return {
664
805
  ok: true,
665
806
  source: "builder",
666
- projectId: generated.projectId,
667
- jobId: generated.jobId,
668
- designSystemId: generated.designSystemId,
807
+ projectId: indexed.projectId ?? "",
808
+ jobId,
809
+ designSystemId: indexed.designSystemId,
669
810
  suggestedTitle: options.projectName?.trim() || null,
670
- builderUrl: builderDesignSystemUrl(generated.designSystemId),
811
+ builderUrl:
812
+ branchUrl ||
813
+ builderProjectBranchUrl(indexed.projectId, indexed.branchName) ||
814
+ builderDesignSystemUrl(indexed.designSystemId),
671
815
  status: "in-progress",
672
816
  };
673
817
  }
818
+
819
+ /**
820
+ * Server-side indexing for in-memory files (the agent action's small inline
821
+ * payloads). Uploads each file server->GCS in resumable chunks, then
822
+ * finalizes. Browser callers should instead stream via
823
+ * `startBuilderDesignSystemUpload` + `indexBuilderDesignSystem`.
824
+ */
825
+ export async function startBuilderDesignSystemIndex(
826
+ options: BuilderDesignSystemIndexOptions,
827
+ ): Promise<BuilderDesignSystemIndexResult> {
828
+ const files = nonEmptyFiles(options.files);
829
+ const description = options.description?.trim();
830
+ if (description) {
831
+ files.unshift({
832
+ name: "additional-context.txt",
833
+ data: new TextEncoder().encode(description),
834
+ mimeType: "text/plain",
835
+ });
836
+ }
837
+ if (
838
+ files.length === 0 &&
839
+ !options.githubRepoUrl &&
840
+ !options.connectedProjectId
841
+ ) {
842
+ throw new Error(
843
+ "Provide at least one .fig/code/text file or a GitHub repository URL to index with Builder.",
844
+ );
845
+ }
846
+
847
+ const sources: DesignSystemSourceInput[] = [];
848
+ if (files.length > 0) {
849
+ const slots = await startBuilderDesignSystemUpload(
850
+ files.map((file) => ({
851
+ name: file.name,
852
+ mimetype: mimeTypeForFile(file),
853
+ declaredSize: file.data.byteLength,
854
+ })),
855
+ );
856
+ for (let i = 0; i < slots.length; i++) {
857
+ await uploadToResumableUrl(slots[i], files[i]);
858
+ }
859
+ for (let i = 0; i < slots.length; i++) {
860
+ const name = files[i].name;
861
+ const fileSelection = options.selection?.[name];
862
+ sources.push({
863
+ kind: "file",
864
+ uploadToken: slots[i].uploadToken,
865
+ ...(fileSelection && fileSelection.length > 0
866
+ ? { selection: { [name]: fileSelection } }
867
+ : {}),
868
+ });
869
+ }
870
+ }
871
+
872
+ if (options.githubRepoUrl?.trim()) {
873
+ sources.push({
874
+ kind: "public-repo",
875
+ repoUrl: options.githubRepoUrl.trim(),
876
+ });
877
+ }
878
+ if (options.connectedProjectId?.trim()) {
879
+ sources.push({
880
+ kind: "connected-repo",
881
+ fusionProjectId: options.connectedProjectId.trim(),
882
+ });
883
+ }
884
+
885
+ return indexBuilderDesignSystem({
886
+ sources,
887
+ projectName: options.projectName,
888
+ devToolsVersion: options.devToolsVersion,
889
+ });
890
+ }
@@ -27,6 +27,12 @@ export interface EmailCta {
27
27
  url: string;
28
28
  }
29
29
 
30
+ export interface EmailLinkBlock {
31
+ intro: string;
32
+ url: string;
33
+ placement?: "before-cta" | "after-cta";
34
+ }
35
+
30
36
  export interface RenderEmailArgs {
31
37
  /** Short preview text shown by email clients next to the subject. */
32
38
  preheader?: string;
@@ -36,6 +42,8 @@ export interface RenderEmailArgs {
36
42
  paragraphs: string[];
37
43
  /** Primary call-to-action rendered as a real button. */
38
44
  cta?: EmailCta;
45
+ /** A treated, copyable URL shown before or after the CTA. */
46
+ linkBlock?: EmailLinkBlock;
39
47
  /**
40
48
  * Optional trusted HTML injected above the CTA — e.g. a template-owned video
41
49
  * thumbnail with a play badge. Injected verbatim, so only pass markup built
@@ -127,6 +135,19 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
127
135
  )
128
136
  .join("");
129
137
 
138
+ const linkBlockHtml = args.linkBlock
139
+ ? `
140
+ <p style="margin:24px 0 12px 0; font-size:15px; line-height:1.6; color:#d4d4d8;">${escapeHtml(args.linkBlock.intro)}</p>
141
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:0; border:1px solid #3f3f46; border-radius:10px; background:#0a0a0c;">
142
+ <tr>
143
+ <td style="padding:14px 16px;">
144
+ <a href="${escapeAttr(args.linkBlock.url)}" style="display:block; color:#d4d4d8; font-family:'SFMono-Regular', Consolas, 'Liberation Mono', monospace; font-size:13px; line-height:1.5; text-decoration:none; word-break:break-all; overflow-wrap:anywhere;">${escapeHtml(args.linkBlock.url)}</a>
145
+ </td>
146
+ </tr>
147
+ </table>
148
+ `
149
+ : "";
150
+
130
151
  const ctaHtml = args.cta
131
152
  ? `
132
153
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0 0 0;">
@@ -187,7 +208,9 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
187
208
  </h1>
188
209
  ${paragraphsHtml}
189
210
  ${heroHtml}
211
+ ${args.linkBlock?.placement !== "after-cta" ? linkBlockHtml : ""}
190
212
  ${ctaHtml}
213
+ ${args.linkBlock?.placement === "after-cta" ? linkBlockHtml : ""}
191
214
  ${footerHtml}
192
215
  </td>
193
216
  </tr>
@@ -205,10 +228,20 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
205
228
  textLines.push(stripTags(p));
206
229
  textLines.push("");
207
230
  }
231
+ if (args.linkBlock && args.linkBlock.placement !== "after-cta") {
232
+ textLines.push(args.linkBlock.intro);
233
+ textLines.push(args.linkBlock.url);
234
+ textLines.push("");
235
+ }
208
236
  if (args.cta) {
209
237
  textLines.push(`${args.cta.label}: ${args.cta.url}`);
210
238
  textLines.push("");
211
239
  }
240
+ if (args.linkBlock?.placement === "after-cta") {
241
+ textLines.push(args.linkBlock.intro);
242
+ textLines.push(args.linkBlock.url);
243
+ textLines.push("");
244
+ }
212
245
  if (args.footer) {
213
246
  textLines.push(args.footer);
214
247
  }
@@ -486,23 +486,31 @@ export {
486
486
  } from "./credential-provider.js";
487
487
  export {
488
488
  builderDesignSystemUrl,
489
+ builderProjectBranchUrl,
489
490
  buildBuilderDesignSystemIndexFiles,
490
491
  createBuilderDesignSystemProxyFields,
492
+ fetchBuilderDesignSystemDecodeJobStatus,
491
493
  fetchBuilderDesignSystemDocs,
492
494
  getBuilderDesignSystemsBaseUrl,
493
495
  hydrateBuilderDesignSystemReference,
496
+ indexBuilderDesignSystem,
494
497
  localBuilderDesignSystemId,
495
498
  mimeTypeForBuilderDesignSystemFilename,
496
499
  parseBuilderDesignSystemProxyReference,
497
500
  startBuilderDesignSystemIndex,
501
+ startBuilderDesignSystemUpload,
498
502
  type BuildBuilderDesignSystemIndexFilesOptions,
499
503
  type BuilderDesignSystemCodeFileInput,
504
+ type BuilderDesignSystemDecodeJobStatus,
500
505
  type BuilderDesignSystemDocsOptions,
501
506
  type BuilderDesignSystemDocument,
502
507
  type BuilderDesignSystemHydratedReference,
503
508
  type BuilderDesignSystemIndexFile,
509
+ type BuilderDesignSystemIndexFromSourcesOptions,
504
510
  type BuilderDesignSystemIndexOptions,
505
511
  type BuilderDesignSystemIndexResult,
512
+ type BuilderDesignSystemUploadAttachment,
513
+ type BuilderDesignSystemUploadSlot,
506
514
  type BuilderDesignSystemProxyFields,
507
515
  type BuilderDesignSystemProxyFieldsOptions,
508
516
  type BuilderDesignSystemProxyReference,
@@ -8,8 +8,10 @@ npm-debug.log*
8
8
  pnpm-debug.log*
9
9
 
10
10
  node_modules
11
+ build/
11
12
  dist
12
13
  dist-ssr
14
+ .output/
13
15
  *.local
14
16
 
15
17
  # Editor directories and files
@@ -10,8 +10,10 @@ npm-debug.log*
10
10
  pnpm-debug.log*
11
11
 
12
12
  node_modules
13
+ build/
13
14
  dist
14
15
  dist-ssr
16
+ .output/
15
17
  *.local
16
18
 
17
19
  # Editor directories and files
@@ -8,8 +8,10 @@ npm-debug.log*
8
8
  pnpm-debug.log*
9
9
 
10
10
  node_modules
11
+ build/
11
12
  dist
12
13
  dist-ssr
14
+ .output/
13
15
  *.local
14
16
 
15
17
  # Editor directories and files
@@ -13,6 +13,8 @@ build/
13
13
  dist
14
14
  dist-ssr
15
15
  .output/
16
+ .netlify/
17
+ .deploy-tmp/
16
18
  *.local
17
19
 
18
20
  # Editor directories and files