@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
@@ -0,0 +1,271 @@
1
+ import { appApiPath } from "@agent-native/core/client/api-path";
2
+
3
+ import {
4
+ type BuilderIndexResult,
5
+ readBuilderIndexResponse,
6
+ } from "./builder-index-response";
7
+
8
+ // GCS resumable uploads require every chunk except the last to be a multiple
9
+ // of 256 KiB. 16 MiB is the recommended default and keeps very large `.fig`
10
+ // files off a single request body (the serverless host caps bodies well below
11
+ // Figma export sizes).
12
+ const GCS_CHUNK_SIZE = 16 * 1024 * 1024;
13
+ const MAX_CHUNK_RETRIES = 5;
14
+
15
+ interface UploadSlot {
16
+ idx: number;
17
+ uploadUrl: string;
18
+ uploadToken: string;
19
+ }
20
+
21
+ async function readJson(res: Response): Promise<any> {
22
+ try {
23
+ return await res.json();
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ async function requestUploadSlots(files: File[]): Promise<UploadSlot[]> {
30
+ const res = await fetch(appApiPath("/api/design-system-upload-start"), {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({
34
+ attachments: files.map((file) => ({
35
+ name: file.name,
36
+ mimetype: file.type || "application/octet-stream",
37
+ declaredSize: file.size,
38
+ })),
39
+ }),
40
+ });
41
+ const json = await readJson(res);
42
+ if (!res.ok || json?.error) {
43
+ throw new Error(json?.error || `Failed to start upload (${res.status})`);
44
+ }
45
+ const slots = Array.isArray(json?.uploads)
46
+ ? ([...json.uploads] as UploadSlot[]).sort((a, b) => a.idx - b.idx)
47
+ : [];
48
+ if (slots.length !== files.length) {
49
+ throw new Error("Upload could not be started for all files.");
50
+ }
51
+ return slots;
52
+ }
53
+
54
+ async function initiateResumableSession(
55
+ uploadUrl: string,
56
+ mimetype: string,
57
+ fileSize: number,
58
+ ): Promise<string> {
59
+ const response = await fetch(uploadUrl, {
60
+ method: "POST",
61
+ headers: {
62
+ "x-goog-resumable": "start",
63
+ // The signed URL commits to the exact declared size; echo it back
64
+ // byte-for-byte or GCS rejects the session.
65
+ "x-goog-content-length-range": `0,${fileSize}`,
66
+ "Content-Type": mimetype,
67
+ },
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to initiate upload session (${response.status})`);
71
+ }
72
+ const sessionUri = response.headers.get("Location");
73
+ if (!sessionUri) {
74
+ throw new Error("Upload session URI missing from storage response");
75
+ }
76
+ return sessionUri;
77
+ }
78
+
79
+ // GCS reports the highest committed byte in a `Range: bytes=0-<end>` header.
80
+ function committedOffsetFromRange(response: Response): number | null {
81
+ const match = response.headers.get("Range")?.match(/bytes=0-(\d+)/);
82
+ return match ? parseInt(match[1], 10) + 1 : null;
83
+ }
84
+
85
+ async function queryCommittedOffset(
86
+ sessionUri: string,
87
+ total: number,
88
+ ): Promise<number> {
89
+ const response = await fetch(sessionUri, {
90
+ method: "PUT",
91
+ headers: { "Content-Range": `bytes */${total}` },
92
+ });
93
+ if (response.status === 200 || response.status === 201) return total;
94
+ if (response.status === 308) return committedOffsetFromRange(response) ?? 0;
95
+ throw new Error(`Failed to query upload status (${response.status})`);
96
+ }
97
+
98
+ function delay(ms: number, signal?: AbortSignal): Promise<void> {
99
+ return new Promise((resolve, reject) => {
100
+ if (signal?.aborted) {
101
+ return reject(new DOMException("Aborted", "AbortError"));
102
+ }
103
+ const timer = setTimeout(resolve, ms);
104
+ signal?.addEventListener(
105
+ "abort",
106
+ () => {
107
+ clearTimeout(timer);
108
+ reject(new DOMException("Aborted", "AbortError"));
109
+ },
110
+ { once: true },
111
+ );
112
+ });
113
+ }
114
+
115
+ async function streamFileToStorage(
116
+ slot: UploadSlot,
117
+ file: File,
118
+ onProgress: (uploadedBytes: number) => void,
119
+ ): Promise<void> {
120
+ const mimetype = file.type || "application/octet-stream";
121
+ const sessionUri = await initiateResumableSession(
122
+ slot.uploadUrl,
123
+ mimetype,
124
+ file.size,
125
+ );
126
+ const total = file.size;
127
+
128
+ if (total === 0) {
129
+ const response = await fetch(sessionUri, {
130
+ method: "PUT",
131
+ headers: { "Content-Range": "bytes */0" },
132
+ body: new Uint8Array(0),
133
+ });
134
+ if (response.status !== 200 && response.status !== 201) {
135
+ throw new Error(
136
+ `Failed to finalize zero-byte upload (${response.status})`,
137
+ );
138
+ }
139
+ onProgress(0);
140
+ return;
141
+ }
142
+
143
+ let offset = 0;
144
+ let retries = 0;
145
+ while (offset < total) {
146
+ const end = Math.min(offset + GCS_CHUNK_SIZE, total);
147
+ const isLast = end === total;
148
+ try {
149
+ const response = await fetch(sessionUri, {
150
+ method: "PUT",
151
+ headers: { "Content-Range": `bytes ${offset}-${end - 1}/${total}` },
152
+ body: file.slice(offset, end),
153
+ });
154
+ if (response.status === 200 || response.status === 201) {
155
+ offset = total;
156
+ } else if (!isLast && response.status === 308) {
157
+ const nextOffset = committedOffsetFromRange(response) ?? offset;
158
+ if (nextOffset <= offset) {
159
+ throw new Error(`Upload stalled at byte ${offset}`);
160
+ }
161
+ offset = nextOffset;
162
+ } else {
163
+ throw new Error(`Unexpected upload status ${response.status}`);
164
+ }
165
+ retries = 0;
166
+ onProgress(offset);
167
+ } catch (err) {
168
+ if (++retries > MAX_CHUNK_RETRIES) throw err;
169
+ await delay(500 * retries);
170
+ try {
171
+ offset = await queryCommittedOffset(sessionUri, total);
172
+ } catch {
173
+ // If the offset query also fails, retry from the last local offset —
174
+ // GCS's resumable PUT safely re-acknowledges bytes it already has.
175
+ }
176
+ onProgress(offset);
177
+ }
178
+ }
179
+ }
180
+
181
+ export interface UploadAndIndexOptions {
182
+ projectName?: string;
183
+ onProgress?: (fraction: number) => void;
184
+ }
185
+
186
+ /**
187
+ * Streams `.fig`/design files straight to storage in resumable chunks, then
188
+ * finalizes Builder DSI indexing with the resulting upload tokens. No file
189
+ * bytes pass through the app server, so arbitrarily large Figma files work.
190
+ */
191
+ export async function uploadAndIndexFigmaFiles(
192
+ files: File[],
193
+ options: UploadAndIndexOptions = {},
194
+ ): Promise<BuilderIndexResult> {
195
+ if (files.length === 0) throw new Error("No files to upload.");
196
+
197
+ const slots = await requestUploadSlots(files);
198
+
199
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0) || 1;
200
+ const uploadedPerFile = new Array<number>(files.length).fill(0);
201
+ for (let i = 0; i < files.length; i++) {
202
+ await streamFileToStorage(slots[i], files[i], (uploaded) => {
203
+ uploadedPerFile[i] = uploaded;
204
+ const done = uploadedPerFile.reduce((sum, n) => sum + n, 0);
205
+ options.onProgress?.(Math.min(done / totalBytes, 1));
206
+ });
207
+ }
208
+
209
+ const res = await fetch(appApiPath("/api/index-design-system-sources"), {
210
+ method: "POST",
211
+ headers: { "Content-Type": "application/json" },
212
+ body: JSON.stringify({
213
+ projectName: options.projectName,
214
+ uploadTokens: slots.map((slot) => slot.uploadToken),
215
+ }),
216
+ });
217
+ return readBuilderIndexResponse(res);
218
+ }
219
+
220
+ export interface DecodeJobStatus {
221
+ status: "pending" | "processing" | "complete" | "error";
222
+ branchUrl: string | null;
223
+ error: string | null;
224
+ framesProcessed: number;
225
+ totalFrames: number;
226
+ }
227
+
228
+ const DECODE_JOB_POLL_INTERVAL_MS = 5_000;
229
+ const DECODE_JOB_MAX_POLLS = 120; // ~10 min at 5s, so a stuck job can't loop forever
230
+
231
+ export interface PollDecodeJobOptions {
232
+ signal?: AbortSignal;
233
+ onUpdate?: (status: DecodeJobStatus) => void;
234
+ }
235
+
236
+ /**
237
+ * After indexing returns a jobId, the `.fig` decode job is still `pending` with
238
+ * no branchUrl. Poll until the branch appears or the job reaches a terminal
239
+ * state. A job that reports `status: "error"` resolves so the caller can read
240
+ * `status.error`; network failures, timeouts, and aborts reject.
241
+ */
242
+ export async function pollDecodeJobStatus(
243
+ jobId: string,
244
+ options: PollDecodeJobOptions = {},
245
+ ): Promise<DecodeJobStatus> {
246
+ const { signal, onUpdate } = options;
247
+ const path = appApiPath(
248
+ `/api/design-system-decode-job-status?jobId=${encodeURIComponent(jobId)}`,
249
+ );
250
+ for (let i = 0; i < DECODE_JOB_MAX_POLLS; i++) {
251
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
252
+ const res = await fetch(path, { signal });
253
+ const json = await readJson(res);
254
+ if (!res.ok || json?.error) {
255
+ throw new Error(json?.error || `Status check failed (${res.status})`);
256
+ }
257
+ const status = json as DecodeJobStatus;
258
+ onUpdate?.(status);
259
+ if (
260
+ status.branchUrl ||
261
+ status.status === "complete" ||
262
+ status.status === "error"
263
+ ) {
264
+ return status;
265
+ }
266
+ await delay(DECODE_JOB_POLL_INTERVAL_MS, signal);
267
+ }
268
+ throw new Error(
269
+ "Timed out waiting for the design system to finish decoding.",
270
+ );
271
+ }
@@ -13,7 +13,7 @@ export interface BuilderIndexResult {
13
13
  builderConnectUrl?: string;
14
14
  }
15
15
 
16
- export const MAX_BUILDER_INDEX_UPLOAD_BYTES = 200 * 1024 * 1024;
16
+ export const MAX_BUILDER_INDEX_UPLOAD_BYTES = 512 * 1024 * 1024;
17
17
 
18
18
  export function formatFileSize(bytes: number): string {
19
19
  return `${Math.round(bytes / 1024 / 1024)} MB`;
@@ -537,6 +537,7 @@ const messages = {
537
537
  builderDesignSystemId: "Design system",
538
538
  builderJobId: "Job",
539
539
  openInBuilder: "Open in Builder",
540
+ decodeFailed: "Decoding failed: {{error}}",
540
541
  websiteUrl: "Website URL",
541
542
  websitePlaceholder: "example.com or Nike",
542
543
  add: "Add",
@@ -0,0 +1,49 @@
1
+ import {
2
+ FeatureNotConfiguredError,
3
+ fetchBuilderDesignSystemDecodeJobStatus,
4
+ getSession,
5
+ } from "@agent-native/core/server";
6
+ import { defineEventHandler, getQuery, setResponseStatus } from "h3";
7
+
8
+ import { withSlidesRequestContext } from "./request-auth-context.js";
9
+
10
+ /**
11
+ * Reads a Builder design-system decode job's status. The UI polls this after
12
+ * `/api/index-design-system-sources` returns a jobId, until the `.fig` decode
13
+ * job leaves `pending` and exposes a `branchUrl` (or reports an `error`).
14
+ */
15
+ export const designSystemDecodeJobStatus = defineEventHandler(async (event) => {
16
+ const session = await getSession(event).catch(() => null);
17
+ if (!session?.email) {
18
+ setResponseStatus(event, 401);
19
+ return { error: "Unauthorized" };
20
+ }
21
+
22
+ const jobId = getQuery(event).jobId;
23
+ if (typeof jobId !== "string" || !jobId.trim()) {
24
+ setResponseStatus(event, 400);
25
+ return { error: "jobId is required." };
26
+ }
27
+
28
+ try {
29
+ return await withSlidesRequestContext(event, () =>
30
+ fetchBuilderDesignSystemDecodeJobStatus(jobId.trim()),
31
+ );
32
+ } catch (err) {
33
+ if (err instanceof FeatureNotConfiguredError) {
34
+ setResponseStatus(event, 412);
35
+ return {
36
+ error: err.message,
37
+ builderConnectUrl:
38
+ err.builderConnectUrl ?? "/_agent-native/builder/connect",
39
+ };
40
+ }
41
+ setResponseStatus(event, 502);
42
+ return {
43
+ error:
44
+ err instanceof Error
45
+ ? err.message
46
+ : "Failed to read decode job status.",
47
+ };
48
+ }
49
+ });
@@ -0,0 +1,87 @@
1
+ import {
2
+ FeatureNotConfiguredError,
3
+ getSession,
4
+ startBuilderDesignSystemUpload,
5
+ } from "@agent-native/core/server";
6
+ import { defineEventHandler, readBody, setResponseStatus } from "h3";
7
+
8
+ import { withSlidesRequestContext } from "./request-auth-context.js";
9
+
10
+ const MAX_FIG_BYTES = 512 * 1024 * 1024;
11
+
12
+ interface AttachmentInput {
13
+ name?: unknown;
14
+ mimetype?: unknown;
15
+ declaredSize?: unknown;
16
+ }
17
+
18
+ /**
19
+ * Opens signed resumable-upload slots so the browser can stream large `.fig`
20
+ * bytes straight to storage. Only small JSON metadata rides through the app
21
+ * server; the file bytes never do.
22
+ */
23
+ export const designSystemUploadStart = defineEventHandler(async (event) => {
24
+ const session = await getSession(event).catch(() => null);
25
+ if (!session?.email) {
26
+ setResponseStatus(event, 401);
27
+ return { error: "Unauthorized" };
28
+ }
29
+
30
+ const body = (await readBody(event).catch(() => null)) as {
31
+ attachments?: AttachmentInput[];
32
+ } | null;
33
+ const rawList = Array.isArray(body?.attachments) ? body.attachments : [];
34
+ if (rawList.length === 0) {
35
+ setResponseStatus(event, 400);
36
+ return { error: "No attachments provided." };
37
+ }
38
+
39
+ const attachments: {
40
+ name: string;
41
+ mimetype: string;
42
+ declaredSize: number;
43
+ }[] = [];
44
+ for (const raw of rawList) {
45
+ const name = typeof raw?.name === "string" ? raw.name.trim() : "";
46
+ const mimetype =
47
+ typeof raw?.mimetype === "string" && raw.mimetype.trim()
48
+ ? raw.mimetype.trim()
49
+ : "application/octet-stream";
50
+ const declaredSize = Number(raw?.declaredSize);
51
+ if (!name) {
52
+ setResponseStatus(event, 400);
53
+ return { error: "Attachment name is required." };
54
+ }
55
+ if (!Number.isSafeInteger(declaredSize) || declaredSize <= 0) {
56
+ setResponseStatus(event, 400);
57
+ return { error: "Attachment declaredSize must be a positive integer." };
58
+ }
59
+ if (declaredSize > MAX_FIG_BYTES) {
60
+ setResponseStatus(event, 413);
61
+ return {
62
+ error: `File too large (max ${Math.round(MAX_FIG_BYTES / 1024 / 1024)} MB).`,
63
+ };
64
+ }
65
+ attachments.push({ name, mimetype, declaredSize });
66
+ }
67
+
68
+ try {
69
+ const uploads = await withSlidesRequestContext(event, () =>
70
+ startBuilderDesignSystemUpload(attachments),
71
+ );
72
+ return { uploads };
73
+ } catch (err) {
74
+ if (err instanceof FeatureNotConfiguredError) {
75
+ setResponseStatus(event, 412);
76
+ return {
77
+ error: err.message,
78
+ builderConnectUrl:
79
+ err.builderConnectUrl ?? "/_agent-native/builder/connect",
80
+ };
81
+ }
82
+ setResponseStatus(event, 502);
83
+ return {
84
+ error: err instanceof Error ? err.message : "Failed to start upload.",
85
+ };
86
+ }
87
+ });
@@ -0,0 +1,79 @@
1
+ import {
2
+ FeatureNotConfiguredError,
3
+ getSession,
4
+ indexBuilderDesignSystem,
5
+ } from "@agent-native/core/server";
6
+ import { defineEventHandler, readBody, setResponseStatus } from "h3";
7
+
8
+ import { upsertBuilderProxyDesignSystem } from "../lib/builder-design-system-proxy.js";
9
+ import { withSlidesRequestContext } from "./request-auth-context.js";
10
+
11
+ /**
12
+ * Finalizes Builder DSI indexing from upload tokens produced by the
13
+ * browser-streamed resumable upload. The file bytes were streamed straight to
14
+ * storage; this endpoint only forwards the opaque tokens.
15
+ */
16
+ export const indexDesignSystemSources = defineEventHandler(async (event) => {
17
+ const session = await getSession(event).catch(() => null);
18
+ if (!session?.email) {
19
+ setResponseStatus(event, 401);
20
+ return { error: "Unauthorized" };
21
+ }
22
+
23
+ const body = (await readBody(event).catch(() => null)) as {
24
+ projectName?: unknown;
25
+ uploadTokens?: unknown;
26
+ } | null;
27
+ const uploadTokens = Array.isArray(body?.uploadTokens)
28
+ ? body.uploadTokens.filter(
29
+ (token): token is string =>
30
+ typeof token === "string" && token.length > 0,
31
+ )
32
+ : [];
33
+ if (uploadTokens.length === 0) {
34
+ setResponseStatus(event, 400);
35
+ return { error: "No uploaded files to index." };
36
+ }
37
+ const projectName =
38
+ typeof body?.projectName === "string"
39
+ ? body.projectName.trim() || undefined
40
+ : undefined;
41
+
42
+ const sources = uploadTokens.map((uploadToken) => ({
43
+ kind: "file" as const,
44
+ uploadToken,
45
+ }));
46
+
47
+ try {
48
+ return await withSlidesRequestContext(event, async ({ email, orgId }) => {
49
+ const result = await indexBuilderDesignSystem({ sources, projectName });
50
+ const proxy = await upsertBuilderProxyDesignSystem({
51
+ result,
52
+ ownerEmail: email ?? session.email,
53
+ orgId: orgId ?? null,
54
+ projectName,
55
+ });
56
+ return {
57
+ ...result,
58
+ ...proxy,
59
+ uploadedFileCount: uploadTokens.length,
60
+ };
61
+ });
62
+ } catch (err) {
63
+ if (err instanceof FeatureNotConfiguredError) {
64
+ setResponseStatus(event, 412);
65
+ return {
66
+ error: err.message,
67
+ builderConnectUrl:
68
+ err.builderConnectUrl ?? "/_agent-native/builder/connect",
69
+ };
70
+ }
71
+ setResponseStatus(event, 502);
72
+ return {
73
+ error:
74
+ err instanceof Error
75
+ ? err.message
76
+ : "Builder design-system indexing failed.",
77
+ };
78
+ }
79
+ });
@@ -0,0 +1 @@
1
+ export { designSystemDecodeJobStatus as default } from "../../handlers/design-system-decode-job-status";
@@ -0,0 +1 @@
1
+ export { designSystemUploadStart as default } from "../../handlers/design-system-upload-start";
@@ -0,0 +1 @@
1
+ export { indexDesignSystemSources as default } from "../../handlers/index-design-system-sources";
@@ -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
@@ -1,5 +1,11 @@
1
1
  # @agent-native/toolkit
2
2
 
3
+ ## 0.10.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 16a9d1a: Keep editor block drag previews aligned with the point where the block was grabbed, then clear incidental selection and focus after a successful drop.
8
+
3
9
  ## 0.10.9
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/toolkit",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
4
4
  "description": "Reusable app-building UI and helpers for Agent-Native apps.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -148,6 +148,8 @@ type DragSession = {
148
148
  sourceNodeSize: number;
149
149
  startX: number;
150
150
  startY: number;
151
+ pointerOffsetX: number;
152
+ pointerOffsetY: number;
151
153
  dragging: boolean;
152
154
  preview: HTMLElement | null;
153
155
  dropLine: HTMLElement | null;
@@ -194,6 +196,47 @@ let activeHoverRegistration: DragHandleRegistration | null = null;
194
196
  const clamp = (value: number, min: number, max: number) =>
195
197
  Math.min(Math.max(value, min), max);
196
198
 
199
+ export const dragPreviewTransform = ({
200
+ clientX,
201
+ clientY,
202
+ pointerOffsetX,
203
+ pointerOffsetY,
204
+ }: {
205
+ clientX: number;
206
+ clientY: number;
207
+ pointerOffsetX: number;
208
+ pointerOffsetY: number;
209
+ }) =>
210
+ `translate3d(${clientX - pointerOffsetX}px, ${clientY - pointerOffsetY}px, 0)`;
211
+
212
+ const collapseAndBlurEditorAfterDrop = (
213
+ view: EditorView,
214
+ preferredPos: number,
215
+ ) => {
216
+ const pos = clamp(preferredPos, 0, view.state.doc.content.size);
217
+ const selection = TextSelection.near(view.state.doc.resolve(pos), 1);
218
+ if (!view.state.selection.eq(selection)) {
219
+ view.dispatch(view.state.tr.setSelection(selection));
220
+ }
221
+
222
+ const nativeSelection = window.getSelection();
223
+ const selectionBelongsToEditor = [
224
+ nativeSelection?.anchorNode,
225
+ nativeSelection?.focusNode,
226
+ ].some((node) => node && (node === view.dom || view.dom.contains(node)));
227
+ if (selectionBelongsToEditor) {
228
+ nativeSelection?.removeAllRanges();
229
+ }
230
+ const activeElement = document.activeElement;
231
+ if (
232
+ activeElement instanceof HTMLElement &&
233
+ (activeElement === view.dom || view.dom.contains(activeElement))
234
+ ) {
235
+ activeElement.blur();
236
+ }
237
+ view.dom.blur();
238
+ };
239
+
197
240
  const editorArea = (registration: DragHandleRegistration) => {
198
241
  const rect = registration.view.dom.getBoundingClientRect();
199
242
  return rect.width * rect.height;
@@ -977,7 +1020,12 @@ export const DragHandle = Extension.create<DragHandleOptions>({
977
1020
  ) => {
978
1021
  if (!session.preview) return;
979
1022
 
980
- session.preview.style.transform = `translate3d(${clientX + 12}px, ${clientY + 10}px, 0)`;
1023
+ session.preview.style.transform = dragPreviewTransform({
1024
+ clientX,
1025
+ clientY,
1026
+ pointerOffsetX: session.pointerOffsetX,
1027
+ pointerOffsetY: session.pointerOffsetY,
1028
+ });
981
1029
  };
982
1030
 
983
1031
  const updateDropLine = (
@@ -1174,6 +1222,7 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1174
1222
  ) {
1175
1223
  const sourceNode = session.view.state.doc.nodeAt(sourceStart);
1176
1224
  if (sourceNode) {
1225
+ let dropCommitted = false;
1177
1226
  const sourceRegistration = registrationForView(session.view);
1178
1227
  const transferData = sourceRegistration?.getDragTransferData?.({
1179
1228
  view: session.view,
@@ -1198,7 +1247,7 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1198
1247
  false);
1199
1248
 
1200
1249
  if (handled) {
1201
- target.view.focus();
1250
+ dropCommitted = true;
1202
1251
  } else if (target.view === session.view) {
1203
1252
  const insertPos =
1204
1253
  dropPos > sourceStart ? dropPos - sourceNode.nodeSize : dropPos;
@@ -1206,10 +1255,8 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1206
1255
  .delete(sourceStart, sourceEnd)
1207
1256
  .insert(insertPos, sourceNode);
1208
1257
 
1209
- tr.setSelection(NodeSelection.create(tr.doc, insertPos));
1210
-
1211
1258
  session.view.dispatch(tr.scrollIntoView());
1212
- session.view.focus();
1259
+ dropCommitted = true;
1213
1260
  } else {
1214
1261
  try {
1215
1262
  const targetNode = target.view.state.schema.nodeFromJSON(
@@ -1225,9 +1272,6 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1225
1272
  dropPos,
1226
1273
  targetNode,
1227
1274
  );
1228
- insertTr.setSelection(
1229
- NodeSelection.create(insertTr.doc, dropPos),
1230
- );
1231
1275
  target.view.dispatch(insertTr.scrollIntoView());
1232
1276
 
1233
1277
  const deleteTr = session.view.state.tr.delete(
@@ -1235,12 +1279,19 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1235
1279
  sourceEnd,
1236
1280
  );
1237
1281
  session.view.dispatch(deleteTr);
1238
- target.view.focus();
1282
+ dropCommitted = true;
1239
1283
  } catch {
1240
1284
  // If the target schema cannot accept this node, leave the
1241
1285
  // source document untouched.
1242
1286
  }
1243
1287
  }
1288
+
1289
+ if (dropCommitted) {
1290
+ collapseAndBlurEditorAfterDrop(target.view, dropPos);
1291
+ if (target.view !== session.view) {
1292
+ collapseAndBlurEditorAfterDrop(session.view, sourceStart);
1293
+ }
1294
+ }
1244
1295
  }
1245
1296
  }
1246
1297
  } else if (commit && !session.dragging && event) {
@@ -1356,6 +1407,7 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1356
1407
  if (!sourceNode) return;
1357
1408
 
1358
1409
  e.preventDefault();
1410
+ const sourceRect = currentBlock.getBoundingClientRect();
1359
1411
  dragSession = {
1360
1412
  view: editorView,
1361
1413
  sourceBlock: currentBlock,
@@ -1363,6 +1415,8 @@ export const DragHandle = Extension.create<DragHandleOptions>({
1363
1415
  sourceNodeSize: sourceNode.nodeSize,
1364
1416
  startX: e.clientX,
1365
1417
  startY: e.clientY,
1418
+ pointerOffsetX: e.clientX - sourceRect.left,
1419
+ pointerOffsetY: e.clientY - sourceRect.top,
1366
1420
  dragging: false,
1367
1421
  preview: null,
1368
1422
  dropLine: null,