@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +27 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/server/builder-design-systems.ts +299 -82
- package/corpus/core/src/server/email-template.ts +33 -0
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/templates/analytics/_gitignore +2 -0
- package/corpus/templates/assets/_gitignore +2 -0
- package/corpus/templates/calendar/_gitignore +2 -0
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/clips/AGENTS.md +6 -1
- package/corpus/templates/clips/_gitignore +2 -0
- package/corpus/templates/clips/actions/complete-transactional-email-summary.ts +56 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +258 -76
- package/corpus/templates/clips/actions/lib/direct-video.ts +96 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +53 -2
- package/corpus/templates/clips/actions/lib/loom-video.ts +2 -62
- package/corpus/templates/clips/actions/lib/video-download-limits.ts +62 -0
- package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +249 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +65 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +55 -0
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +28 -126
- package/corpus/templates/clips/app/global.css +23 -0
- package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +98 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +20 -0
- package/corpus/templates/clips/app/routes/_app.tsx +2 -0
- package/corpus/templates/clips/app/routes/import.tsx +396 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
- package/corpus/templates/clips/app/routes/record.tsx +13 -71
- package/corpus/templates/clips/changelog/2026-07-25-two-clip-emails-now-include-a-concise-summary-generated-secu.md +6 -0
- package/corpus/templates/clips/data/clips-transactional-emails/config.json +3 -0
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +1034 -0
- package/corpus/templates/clips/server/lib/transactional-email-store.ts +865 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +270 -0
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/view-event.post.ts +59 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/AGENTS.md +8 -0
- package/corpus/templates/content/_gitignore +2 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +28 -3
- package/corpus/templates/content/actions/_property-utils.ts +32 -2
- package/corpus/templates/content/actions/configure-document-property.ts +12 -2
- package/corpus/templates/content/actions/delete-document-property.ts +13 -3
- package/corpus/templates/content/actions/duplicate-document-property.ts +13 -3
- package/corpus/templates/content/actions/list-content-spaces.ts +5 -2
- package/corpus/templates/content/actions/list-document-properties.ts +12 -3
- package/corpus/templates/content/actions/move-database-item.ts +74 -55
- package/corpus/templates/content/actions/reorder-document-property.ts +20 -4
- package/corpus/templates/content/actions/set-document-property.ts +10 -1
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +7 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +31 -21
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +30 -3
- package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +8 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +72 -20
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +79 -10
- package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +8 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +6 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +246 -26
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +491 -197
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +3 -3
- package/corpus/templates/content/app/components/sidebar/sidebar-reorder.tsx +409 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +117 -3
- package/corpus/templates/content/app/hooks/use-content-spaces.ts +1 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +62 -18
- package/corpus/templates/content/app/hooks/use-documents.ts +13 -5
- package/corpus/templates/content/app/i18n-data.ts +261 -3
- package/corpus/templates/content/app/routes/_app.page.$id.tsx +9 -2
- package/corpus/templates/content/changelog/2026-07-24-database-page-properties-keep-their-context.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-sidebar-references-can-now-be-personally-reordered.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-07-24-database-property-context-switch-diagnosis.md +249 -0
- package/corpus/templates/content/docs/solutions/content-sidebar-order-shape.md +389 -0
- package/corpus/templates/content/shared/api.ts +28 -0
- package/corpus/templates/design/_gitignore +2 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -0
- package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
- package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
- package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
- package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/dispatch/_gitignore +2 -0
- package/corpus/templates/forms/_gitignore +2 -0
- package/corpus/templates/macros/_gitignore +2 -0
- package/corpus/templates/mail/_gitignore +2 -0
- package/corpus/templates/plan/_gitignore +2 -0
- package/corpus/templates/slides/_gitignore +2 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
- package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
- package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
- package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/tasks/_gitignore +2 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/DragHandle.ts +63 -9
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/builder-design-systems.d.ts +49 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +221 -69
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/email-template.d.ts +7 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +24 -0
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/_gitignore +2 -0
- package/package.json +5 -3
- package/src/server/builder-design-systems.ts +299 -82
- package/src/server/email-template.ts +33 -0
- package/src/server/index.ts +8 -0
- package/src/templates/chat/_gitignore +2 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
- 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 =
|
|
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";
|
|
@@ -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
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|