@agent-native/core 0.182.1 → 0.182.2-nightly-20260917225811
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/templates/clips/app/root.tsx +59 -10
- package/corpus/templates/clips/changelog/2026-09-17-desktop-popover-closes-when-clicking-away.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-17-self-hosted-extension-sign-in.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +45 -25
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +43 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +55 -6
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +107 -19
- package/corpus/templates/design/.generated/bridge/hit-test.generated.ts +1 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +174 -14
- package/corpus/templates/design/actions/apply-visual-edit.ts +3 -1
- package/corpus/templates/design/actions/create-component.ts +173 -12
- package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
- package/corpus/templates/design/actions/create-design-version.ts +21 -0
- package/corpus/templates/design/actions/create-file.ts +35 -33
- package/corpus/templates/design/actions/delete-design.ts +29 -28
- package/corpus/templates/design/actions/delete-file.ts +154 -23
- package/corpus/templates/design/actions/generate-design.ts +28 -16
- package/corpus/templates/design/actions/get-component-details.ts +272 -33
- package/corpus/templates/design/actions/get-design-version.ts +9 -1
- package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
- package/corpus/templates/design/actions/index-components.ts +241 -126
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
- package/corpus/templates/design/actions/present-design-variants.ts +68 -72
- package/corpus/templates/design/actions/rename-component.ts +294 -0
- package/corpus/templates/design/actions/rename-screen.ts +10 -10
- package/corpus/templates/design/actions/update-file.ts +327 -330
- package/corpus/templates/design/actions/update-screen-source.ts +2 -43
- package/corpus/templates/design/app/components/design/EditPanel.tsx +17 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +228 -24
- package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +17 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +151 -32
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +4 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +1 -1
- package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
- package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
- package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
- package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +273 -60
- package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +9 -2
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
- package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +13 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +14 -0
- package/corpus/templates/design/app/components/design/types.ts +9 -1
- package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
- package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
- package/corpus/templates/design/app/i18n-data.ts +0 -10
- package/corpus/templates/design/app/pages/DesignEditor.tsx +669 -161
- package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +20 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +2 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +21 -3
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-component.ts +44 -3
- package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +157 -52
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +76 -1
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +152 -44
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +131 -2
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +5 -1
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
- package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +58 -8
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
- package/corpus/templates/design/app/pages/design-editor/history.ts +3 -0
- package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
- package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
- package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
- package/corpus/templates/design/changelog/2026-09-16-fix-design-nested-layer-dragging-and-screen-duplication-pari.md +6 -0
- package/corpus/templates/design/changelog/2026-09-17-design-drag-and-drop-keeps-duplicates-cross-screen-moves-and.md +6 -0
- package/corpus/templates/design/changelog/2026-09-17-design-preserves-reusable-component-identity-and-props-acros.md +6 -0
- package/corpus/templates/design/changelog/2026-09-17-open-vector-paths-no-longer-show-fills-and-interact-dimensio.md +6 -0
- package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
- package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
- package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
- package/corpus/templates/design/e2e/helpers.ts +6 -1
- package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
- package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
- package/corpus/templates/design/server/lib/design-versions.ts +208 -130
- package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
- package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
- package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
- package/corpus/templates/design/server/source-workspace.ts +412 -356
- package/corpus/templates/design/shared/component-library.ts +36 -6
- package/corpus/templates/design/shared/component-model.ts +12 -2
- package/corpus/templates/design/shared/local-jsx-visual-edit.ts +231 -5
- package/corpus/templates/design/shared/source-mode.ts +22 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/AssistantChat.d.ts +4 -1
- package/dist/client/AssistantChat.js +26 -3
- package/dist/client/chat/agentkit-protocol.js +148 -34
- package/dist/client/chat/message-components.js +7 -5
- package/dist/client/chat/tool-call-display.js +4 -3
- package/dist/client/conversation/AgentConversation.js +4 -2
- package/dist/client/tool-display.d.ts +9 -0
- package/dist/client/tool-display.js +15 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/localization/core-messages/ar-SA.js +2 -0
- package/dist/localization/core-messages/de-DE.js +2 -0
- package/dist/localization/core-messages/en-US.d.ts +2 -0
- package/dist/localization/core-messages/en-US.js +2 -0
- package/dist/localization/core-messages/es-ES.js +2 -0
- package/dist/localization/core-messages/fr-FR.js +2 -0
- package/dist/localization/core-messages/hi-IN.js +2 -0
- package/dist/localization/core-messages/ja-JP.js +2 -0
- package/dist/localization/core-messages/ko-KR.js +2 -0
- package/dist/localization/core-messages/pt-BR.js +2 -0
- package/dist/localization/core-messages/zh-CN.js +2 -0
- package/dist/localization/core-messages/zh-TW.js +2 -0
- package/dist/localization/core-messages.d.ts +2 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/package.json +3 -3
- package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { defineAction } from "@agent-native/core/action";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
applyTextToYDoc,
|
|
21
|
+
CollabBaseVersionConflictError,
|
|
22
|
+
withPreparedYDocMutation,
|
|
23
|
+
} from "@agent-native/core/collab";
|
|
24
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
20
25
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
21
26
|
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
22
27
|
import { and, eq } from "drizzle-orm";
|
|
@@ -24,10 +29,16 @@ import { z } from "zod";
|
|
|
24
29
|
|
|
25
30
|
import { getDb, schema } from "../server/db/index.js";
|
|
26
31
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
32
|
+
import {
|
|
33
|
+
SourceWorkspaceEditConflictError,
|
|
34
|
+
designSourceMutationLockKey,
|
|
35
|
+
withSourceFileWriteLock,
|
|
36
|
+
} from "../server/source-workspace.js";
|
|
27
37
|
import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
|
|
28
38
|
import { buildCodeLayerProjection } from "../shared/code-layer.js";
|
|
29
39
|
import type { CodeLayerSource } from "../shared/code-layer.js";
|
|
30
40
|
import {
|
|
41
|
+
componentIndexId,
|
|
31
42
|
detectInstances,
|
|
32
43
|
buildDefinitions,
|
|
33
44
|
type ComponentDefinition,
|
|
@@ -36,29 +47,6 @@ import {
|
|
|
36
47
|
import { hasCapability } from "../shared/design-source-capabilities.js";
|
|
37
48
|
import { designSourceTypeFromData } from "../shared/source-mode.js";
|
|
38
49
|
|
|
39
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
async function liveContent(
|
|
42
|
-
fileId: string,
|
|
43
|
-
storedContent: string,
|
|
44
|
-
): Promise<string> {
|
|
45
|
-
try {
|
|
46
|
-
if (await hasCollabState(fileId)) {
|
|
47
|
-
const live = await getText(fileId, "content");
|
|
48
|
-
if (typeof live === "string") return live;
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
// Collab reads are best-effort; SQL content is the fallback.
|
|
52
|
-
}
|
|
53
|
-
return storedContent;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function componentIndexId(designId: string, name: string): string {
|
|
57
|
-
// Stable deterministic id — same component name on the same design always
|
|
58
|
-
// maps to the same row so upsert is safe.
|
|
59
|
-
return `ci_${designId}_${name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
50
|
// ─── Action ───────────────────────────────────────────────────────────────────
|
|
63
51
|
|
|
64
52
|
export default defineAction({
|
|
@@ -118,7 +106,9 @@ export default defineAction({
|
|
|
118
106
|
};
|
|
119
107
|
}
|
|
120
108
|
|
|
121
|
-
//
|
|
109
|
+
// Resolve the lock target without holding a database lock. The source-file
|
|
110
|
+
// lock must be acquired before the transaction so index and write paths
|
|
111
|
+
// enter Yjs and SQL in the same order.
|
|
122
112
|
const conditions = [
|
|
123
113
|
accessFilter(schema.designs, schema.designShares),
|
|
124
114
|
eq(schema.designFiles.designId, designId),
|
|
@@ -126,14 +116,8 @@ export default defineAction({
|
|
|
126
116
|
? eq(schema.designFiles.id, fileId)
|
|
127
117
|
: eq(schema.designFiles.filename, "index.html"),
|
|
128
118
|
];
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.select({
|
|
132
|
-
id: schema.designFiles.id,
|
|
133
|
-
designId: schema.designFiles.designId,
|
|
134
|
-
filename: schema.designFiles.filename,
|
|
135
|
-
content: schema.designFiles.content,
|
|
136
|
-
})
|
|
119
|
+
const [candidate] = await db
|
|
120
|
+
.select({ id: schema.designFiles.id })
|
|
137
121
|
.from(schema.designFiles)
|
|
138
122
|
.innerJoin(
|
|
139
123
|
schema.designs,
|
|
@@ -141,102 +125,233 @@ export default defineAction({
|
|
|
141
125
|
)
|
|
142
126
|
.where(and(...conditions))
|
|
143
127
|
.limit(1);
|
|
128
|
+
if (!candidate) throw new Error("Design HTML file not found.");
|
|
129
|
+
|
|
130
|
+
let preparedCallbackEntered = false;
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = await withSourceFileWriteLock(candidate.id, () =>
|
|
134
|
+
// Holding the core document lock across the SQL snapshot prevents a
|
|
135
|
+
// local Yjs writer from changing the document between the row locks and
|
|
136
|
+
// the component projection. Cross-process writers are stopped by the
|
|
137
|
+
// transaction-scoped _collab_docs row lock below.
|
|
138
|
+
withPreparedYDocMutation(candidate.id, undefined, async (lease) => {
|
|
139
|
+
preparedCallbackEntered = true;
|
|
140
|
+
const [file] = await db
|
|
141
|
+
.select({
|
|
142
|
+
id: schema.designFiles.id,
|
|
143
|
+
designId: schema.designFiles.designId,
|
|
144
|
+
filename: schema.designFiles.filename,
|
|
145
|
+
content: schema.designFiles.content,
|
|
146
|
+
})
|
|
147
|
+
.from(schema.designFiles)
|
|
148
|
+
.innerJoin(
|
|
149
|
+
schema.designs,
|
|
150
|
+
eq(schema.designFiles.designId, schema.designs.id),
|
|
151
|
+
)
|
|
152
|
+
.where(and(...conditions))
|
|
153
|
+
.limit(1);
|
|
154
|
+
|
|
155
|
+
if (!file) throw new Error("Design HTML file not found.");
|
|
156
|
+
|
|
157
|
+
const transaction = getDbExec().transaction;
|
|
158
|
+
if (!transaction) {
|
|
159
|
+
throw new SourceWorkspaceEditConflictError(
|
|
160
|
+
"Could not verify a source file's live version. Re-read the design and retry.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return transaction(async (tx) => {
|
|
165
|
+
await tx.execute({
|
|
166
|
+
sql: "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))",
|
|
167
|
+
args: [designSourceMutationLockKey(designId)],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Lock only the selected source row and the live collab row. The
|
|
171
|
+
// latter makes the following text_snapshot a stable cross-process
|
|
172
|
+
// view instead of a best-effort cache read.
|
|
173
|
+
const lockedFileResult = await tx.execute({
|
|
174
|
+
sql: 'SELECT id, design_id AS "designId", filename, content FROM design_files WHERE id = ? AND design_id = ? FOR UPDATE',
|
|
175
|
+
args: [file.id, designId],
|
|
176
|
+
});
|
|
177
|
+
const lockedFileRow = lockedFileResult.rows[0] as
|
|
178
|
+
| {
|
|
179
|
+
id?: unknown;
|
|
180
|
+
designId?: unknown;
|
|
181
|
+
filename?: unknown;
|
|
182
|
+
content?: unknown;
|
|
183
|
+
}
|
|
184
|
+
| undefined;
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
!lockedFileRow ||
|
|
188
|
+
typeof lockedFileRow.id !== "string" ||
|
|
189
|
+
typeof lockedFileRow.designId !== "string" ||
|
|
190
|
+
typeof lockedFileRow.filename !== "string" ||
|
|
191
|
+
(lockedFileRow.content !== null &&
|
|
192
|
+
typeof lockedFileRow.content !== "string") ||
|
|
193
|
+
lockedFileRow.content !== file.content
|
|
194
|
+
) {
|
|
195
|
+
throw new SourceWorkspaceEditConflictError(
|
|
196
|
+
"The source file changed while its live version was being read. Re-read the design and retry.",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const lockedFile = {
|
|
200
|
+
id: lockedFileRow.id,
|
|
201
|
+
designId: lockedFileRow.designId,
|
|
202
|
+
filename: lockedFileRow.filename,
|
|
203
|
+
content: lockedFileRow.content as string | null,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
let collabResult: { rows: any[] };
|
|
207
|
+
try {
|
|
208
|
+
collabResult = (await tx.execute({
|
|
209
|
+
sql: "SELECT yjs_state, text_snapshot FROM _collab_docs WHERE doc_id = ? FOR SHARE",
|
|
210
|
+
args: [file.id],
|
|
211
|
+
})) as { rows: any[] };
|
|
212
|
+
} catch {
|
|
213
|
+
throw new SourceWorkspaceEditConflictError(
|
|
214
|
+
"Could not verify a source file's live version. Re-read the design and retry.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const collabRow = collabResult.rows[0] as
|
|
218
|
+
| { yjs_state?: unknown; text_snapshot?: unknown }
|
|
219
|
+
| undefined;
|
|
220
|
+
let html = (lockedFile.content as string | null) ?? "";
|
|
221
|
+
let needsCollabSeed = !collabRow;
|
|
222
|
+
if (collabRow) {
|
|
223
|
+
if (
|
|
224
|
+
typeof collabRow.yjs_state !== "string" ||
|
|
225
|
+
typeof collabRow.text_snapshot !== "string"
|
|
226
|
+
) {
|
|
227
|
+
throw new SourceWorkspaceEditConflictError(
|
|
228
|
+
"Could not verify a source file's live version. Re-read the design and retry.",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (collabRow.yjs_state.length > 0) {
|
|
232
|
+
html = collabRow.text_snapshot;
|
|
233
|
+
} else {
|
|
234
|
+
needsCollabSeed = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A missing or empty row is lazy collab state, not permission to
|
|
239
|
+
// index an unprotected SQL fallback. Seed the prepared Y.Doc in
|
|
240
|
+
// this transaction; if another writer wins the insert/CAS, the
|
|
241
|
+
// lease raises a typed conflict and the index write rolls back.
|
|
242
|
+
if (needsCollabSeed) {
|
|
243
|
+
applyTextToYDoc(lease.doc, "content", html, "agent");
|
|
244
|
+
try {
|
|
245
|
+
await lease.persist(tx, html);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof CollabBaseVersionConflictError) {
|
|
248
|
+
throw new SourceWorkspaceEditConflictError(
|
|
249
|
+
"The source file changed while its live version was being initialized. Re-read the design and retry.",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const codeLayerSource: CodeLayerSource = {
|
|
257
|
+
kind: "design-file",
|
|
258
|
+
designId: lockedFile.designId,
|
|
259
|
+
fileId: lockedFile.id,
|
|
260
|
+
filename: lockedFile.filename,
|
|
261
|
+
};
|
|
262
|
+
const projection = buildCodeLayerProjection(html, {
|
|
263
|
+
source: codeLayerSource,
|
|
264
|
+
});
|
|
265
|
+
const instances = detectInstances(projection.nodes);
|
|
266
|
+
const definitions = buildDefinitions(instances);
|
|
267
|
+
|
|
268
|
+
// ── Persist discovered components ────────────────────────────────────
|
|
269
|
+
// Write each distinct component name into component_index so that
|
|
270
|
+
// get-component-details can resolve metadata by node id.
|
|
271
|
+
const now = new Date().toISOString();
|
|
272
|
+
const selectorByNodeId = new Map(
|
|
273
|
+
instances.map((instance) => [instance.nodeId, instance.selector]),
|
|
274
|
+
);
|
|
275
|
+
// Derive the owner from the request user, falling back to the design's
|
|
276
|
+
// owner. Never stamp an empty-string owner (an unowned row): require a real
|
|
277
|
+
// owner before writing a new component_index row.
|
|
278
|
+
const designOwner = (access.resource as { ownerEmail?: unknown })
|
|
279
|
+
.ownerEmail;
|
|
280
|
+
const ownerEmail =
|
|
281
|
+
getRequestUserEmail() ??
|
|
282
|
+
(typeof designOwner === "string" && designOwner
|
|
283
|
+
? designOwner
|
|
284
|
+
: null);
|
|
285
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
286
|
+
|
|
287
|
+
for (const def of definitions) {
|
|
288
|
+
const id = componentIndexId(designId, def.name);
|
|
289
|
+
await tx.execute({
|
|
290
|
+
sql: `INSERT INTO component_index
|
|
291
|
+
(id, design_id, name, runtime_selectors, owner_email, created_at, updated_at)
|
|
292
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
293
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
294
|
+
runtime_selectors = excluded.runtime_selectors,
|
|
295
|
+
updated_at = excluded.updated_at`,
|
|
296
|
+
args: [
|
|
297
|
+
id,
|
|
298
|
+
designId,
|
|
299
|
+
def.name,
|
|
300
|
+
JSON.stringify(
|
|
301
|
+
def.instanceNodeIds
|
|
302
|
+
.map((nodeId) => selectorByNodeId.get(nodeId))
|
|
303
|
+
.filter((selector): selector is string =>
|
|
304
|
+
Boolean(selector),
|
|
305
|
+
),
|
|
306
|
+
),
|
|
307
|
+
ownerEmail,
|
|
308
|
+
now,
|
|
309
|
+
now,
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Annotate instances with their component_index id.
|
|
315
|
+
const indexMap = new Map(
|
|
316
|
+
definitions.map((def) => [
|
|
317
|
+
def.name,
|
|
318
|
+
componentIndexId(designId, def.name),
|
|
319
|
+
]),
|
|
320
|
+
);
|
|
321
|
+
const annotatedInstances = instances.map((inst) => ({
|
|
322
|
+
...inst,
|
|
323
|
+
componentIndexId: indexMap.get(inst.name),
|
|
324
|
+
}));
|
|
144
325
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// owner before writing a new component_index row.
|
|
171
|
-
const designOwner = (access.resource as { ownerEmail?: unknown })
|
|
172
|
-
.ownerEmail;
|
|
173
|
-
const ownerEmail =
|
|
174
|
-
getRequestUserEmail() ??
|
|
175
|
-
(typeof designOwner === "string" && designOwner ? designOwner : null);
|
|
176
|
-
if (!ownerEmail) throw new Error("no authenticated user");
|
|
177
|
-
|
|
178
|
-
for (const def of definitions) {
|
|
179
|
-
const id = componentIndexId(designId, def.name);
|
|
180
|
-
const existing = await db
|
|
181
|
-
.select({ id: schema.componentIndex.id })
|
|
182
|
-
.from(schema.componentIndex)
|
|
183
|
-
.where(eq(schema.componentIndex.id, id))
|
|
184
|
-
.limit(1);
|
|
185
|
-
|
|
186
|
-
if (existing.length === 0) {
|
|
187
|
-
await db.insert(schema.componentIndex).values({
|
|
188
|
-
id,
|
|
189
|
-
designId,
|
|
190
|
-
name: def.name,
|
|
191
|
-
runtimeSelectors: JSON.stringify(
|
|
192
|
-
def.instanceNodeIds.map(
|
|
193
|
-
(nodeId) => `[data-agent-native-node-id="${nodeId}"]`,
|
|
194
|
-
),
|
|
195
|
-
),
|
|
196
|
-
ownerEmail,
|
|
197
|
-
createdAt: now,
|
|
198
|
-
updatedAt: now,
|
|
199
|
-
});
|
|
200
|
-
} else {
|
|
201
|
-
await db
|
|
202
|
-
.update(schema.componentIndex)
|
|
203
|
-
.set({
|
|
204
|
-
runtimeSelectors: JSON.stringify(
|
|
205
|
-
def.instanceNodeIds.map(
|
|
206
|
-
(nodeId) => `[data-agent-native-node-id="${nodeId}"]`,
|
|
207
|
-
),
|
|
208
|
-
),
|
|
209
|
-
updatedAt: now,
|
|
210
|
-
})
|
|
211
|
-
.where(eq(schema.componentIndex.id, id));
|
|
326
|
+
return {
|
|
327
|
+
designId,
|
|
328
|
+
sourceType,
|
|
329
|
+
ctaRequired: false,
|
|
330
|
+
hasFullIndex,
|
|
331
|
+
components: definitions,
|
|
332
|
+
instances: annotatedInstances,
|
|
333
|
+
totalComponents: definitions.length,
|
|
334
|
+
totalInstances: instances.length,
|
|
335
|
+
note:
|
|
336
|
+
sourceType === "inline"
|
|
337
|
+
? "Showing annotated Alpine components from data-agent-native-component attributes. Connect Builder (free tier available) for full TS prop types and cva variants."
|
|
338
|
+
: undefined,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (
|
|
345
|
+
!preparedCallbackEntered &&
|
|
346
|
+
!(error instanceof SourceWorkspaceEditConflictError)
|
|
347
|
+
) {
|
|
348
|
+
throw new SourceWorkspaceEditConflictError(
|
|
349
|
+
"Could not verify a source file's live version. Re-read the design and retry.",
|
|
350
|
+
);
|
|
212
351
|
}
|
|
352
|
+
throw error;
|
|
213
353
|
}
|
|
214
354
|
|
|
215
|
-
|
|
216
|
-
const indexMap = new Map(
|
|
217
|
-
definitions.map((def) => [
|
|
218
|
-
def.name,
|
|
219
|
-
componentIndexId(designId, def.name),
|
|
220
|
-
]),
|
|
221
|
-
);
|
|
222
|
-
const annotatedInstances = instances.map((inst) => ({
|
|
223
|
-
...inst,
|
|
224
|
-
componentIndexId: indexMap.get(inst.name),
|
|
225
|
-
}));
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
designId,
|
|
229
|
-
sourceType,
|
|
230
|
-
ctaRequired: false,
|
|
231
|
-
hasFullIndex,
|
|
232
|
-
components: definitions,
|
|
233
|
-
instances: annotatedInstances,
|
|
234
|
-
totalComponents: definitions.length,
|
|
235
|
-
totalInstances: instances.length,
|
|
236
|
-
note:
|
|
237
|
-
sourceType === "inline"
|
|
238
|
-
? "Showing annotated Alpine components from data-agent-native-component attributes. Connect Builder (free tier available) for full TS prop types and cva variants."
|
|
239
|
-
: undefined,
|
|
240
|
-
};
|
|
355
|
+
return result;
|
|
241
356
|
},
|
|
242
357
|
});
|
|
@@ -42,6 +42,7 @@ import { mutateDesignData } from "../server/lib/design-data-mutation.js";
|
|
|
42
42
|
import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
|
|
43
43
|
import {
|
|
44
44
|
readLiveSourceFile,
|
|
45
|
+
withDesignSourceMutationTransaction,
|
|
45
46
|
writeInlineSourceFile,
|
|
46
47
|
} from "../server/source-workspace.js";
|
|
47
48
|
import {
|
|
@@ -226,15 +227,17 @@ export default defineAction({
|
|
|
226
227
|
});
|
|
227
228
|
} else {
|
|
228
229
|
try {
|
|
229
|
-
await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
230
|
+
await withDesignSourceMutationTransaction(designId, (tx) =>
|
|
231
|
+
tx.insert(schema.designFiles).values({
|
|
232
|
+
id: boardFileId,
|
|
233
|
+
designId,
|
|
234
|
+
filename: BOARD_FILENAME,
|
|
235
|
+
fileType: "html",
|
|
236
|
+
content: boardHtml,
|
|
237
|
+
createdAt: now,
|
|
238
|
+
updatedAt: now,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
238
241
|
} catch (error) {
|
|
239
242
|
const [concurrentBoardFile] = await db
|
|
240
243
|
.select({ id: schema.designFiles.id })
|
|
@@ -15,12 +15,12 @@ import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
|
15
15
|
import { getDb, schema } from "../server/db/index.js";
|
|
16
16
|
import { mutateDesignData } from "../server/lib/design-data-mutation.js";
|
|
17
17
|
import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
|
|
18
|
+
import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
|
|
18
19
|
import {
|
|
19
20
|
mergeCanvasFramePlacements,
|
|
20
21
|
nextFreeCanvasRowY,
|
|
21
22
|
type CanvasFramePlacement,
|
|
22
23
|
} from "../shared/canvas-frames.js";
|
|
23
|
-
import { isUniqueConstraintViolation } from "../shared/db-conflict.js";
|
|
24
24
|
import { getOverviewScreenFileIds } from "../shared/design-files.js";
|
|
25
25
|
import { assertDesignHtmlWellFormed } from "../shared/html-integrity.js";
|
|
26
26
|
import { widthToPrefix } from "../shared/responsive-classes.js";
|
|
@@ -205,8 +205,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
205
205
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
const MAX_FILENAME_INSERT_ATTEMPTS = 5;
|
|
209
|
-
|
|
210
208
|
/**
|
|
211
209
|
* True only when `rawSet` proves none of its screens has ever been removed by
|
|
212
210
|
* delete-file.ts. This action stamps every variant set it creates with
|
|
@@ -496,9 +494,16 @@ async function deleteVariantSetsByIds(
|
|
|
496
494
|
});
|
|
497
495
|
|
|
498
496
|
if (removedFileIds.length > 0) {
|
|
499
|
-
await
|
|
500
|
-
|
|
501
|
-
|
|
497
|
+
await withDesignSourceMutationTransaction(designId, async (tx) => {
|
|
498
|
+
await tx
|
|
499
|
+
.delete(schema.designFiles)
|
|
500
|
+
.where(
|
|
501
|
+
and(
|
|
502
|
+
eq(schema.designFiles.designId, designId),
|
|
503
|
+
inArray(schema.designFiles.id, removedFileIds),
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
});
|
|
502
507
|
|
|
503
508
|
// Closes the small window between the metadata prune above and the
|
|
504
509
|
// physical row delete: mirrors delete-file.ts's before/delete/after
|
|
@@ -992,46 +997,45 @@ export default defineAction({
|
|
|
992
997
|
|
|
993
998
|
const db = getDb();
|
|
994
999
|
const now = new Date().toISOString();
|
|
995
|
-
const existingFiles = await db
|
|
996
|
-
.select()
|
|
997
|
-
.from(schema.designFiles)
|
|
998
|
-
.where(eq(schema.designFiles.designId, designId));
|
|
999
|
-
const usedFilenames = new Set(existingFiles.map((file) => file.filename));
|
|
1000
|
-
const screenFileIds = getOverviewScreenFileIds(existingFiles);
|
|
1001
1000
|
const variantSetId = nanoid();
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1001
|
+
let screenFileIds: string[] = [];
|
|
1002
|
+
const screenContents = new Map<string, string>();
|
|
1003
|
+
const screens = await withDesignSourceMutationTransaction(
|
|
1004
|
+
designId,
|
|
1005
|
+
async (tx) => {
|
|
1006
|
+
const existingFiles = await tx
|
|
1007
|
+
.select()
|
|
1008
|
+
.from(schema.designFiles)
|
|
1009
|
+
.where(eq(schema.designFiles.designId, designId));
|
|
1010
|
+
const usedFilenames = new Set(
|
|
1011
|
+
existingFiles.map((file) => file.filename),
|
|
1012
|
+
);
|
|
1013
|
+
screenFileIds = getOverviewScreenFileIds(existingFiles);
|
|
1014
|
+
const createdScreens: VariantScreen[] = [];
|
|
1015
|
+
|
|
1016
|
+
for (let index = 0; index < variants.length; index += 1) {
|
|
1017
|
+
const variant = variants[index]!;
|
|
1018
|
+
const label = variant.label.trim() || optionName(index);
|
|
1019
|
+
const slug = slugify(label, `option-${index + 1}`);
|
|
1020
|
+
const filename = uniqueFilename(
|
|
1021
|
+
`variant-${slug}.html`,
|
|
1022
|
+
usedFilenames,
|
|
1023
|
+
);
|
|
1024
|
+
const fileId = nanoid();
|
|
1025
|
+
const providedContent = variant.content?.trim();
|
|
1026
|
+
const initialSize = inferVariantSize(variant, prompt);
|
|
1027
|
+
const rawContent =
|
|
1028
|
+
providedContent ||
|
|
1029
|
+
fallbackVariantContent(variant, index, prompt, initialSize);
|
|
1030
|
+
const { width, height } = providedContent
|
|
1031
|
+
? inferVariantSize({ ...variant, content: rawContent })
|
|
1032
|
+
: initialSize;
|
|
1033
|
+
// Stamp missing data-agent-native-node-id attributes before
|
|
1034
|
+
// persisting so each variant screen is fully addressable by
|
|
1035
|
+
// id-keyed editor operations as soon as it lands on the board.
|
|
1036
|
+
const content = annotateScreenHtmlForPersist(rawContent, "html");
|
|
1037
|
+
|
|
1038
|
+
await tx.insert(schema.designFiles).values({
|
|
1035
1039
|
id: fileId,
|
|
1036
1040
|
designId,
|
|
1037
1041
|
filename,
|
|
@@ -1040,34 +1044,26 @@ export default defineAction({
|
|
|
1040
1044
|
createdAt: now,
|
|
1041
1045
|
updatedAt: now,
|
|
1042
1046
|
});
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
.where(eq(schema.designFiles.designId, designId));
|
|
1055
|
-
for (const file of freshFiles) usedFilenames.add(file.filename);
|
|
1056
|
-
filename = uniqueFilename(`variant-${slug}.html`, usedFilenames);
|
|
1057
|
-
fileId = nanoid();
|
|
1047
|
+
usedFilenames.add(filename);
|
|
1048
|
+
screenContents.set(fileId, content);
|
|
1049
|
+
|
|
1050
|
+
createdScreens.push({
|
|
1051
|
+
id: fileId,
|
|
1052
|
+
variantId: variant.id,
|
|
1053
|
+
label,
|
|
1054
|
+
filename,
|
|
1055
|
+
width,
|
|
1056
|
+
height,
|
|
1057
|
+
});
|
|
1058
1058
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
width,
|
|
1068
|
-
height,
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1059
|
+
return createdScreens;
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
await Promise.all(
|
|
1063
|
+
screens.map((screen) =>
|
|
1064
|
+
seedFromText(screen.id, screenContents.get(screen.id)!),
|
|
1065
|
+
),
|
|
1066
|
+
);
|
|
1071
1067
|
|
|
1072
1068
|
// Presenting options should not silently reconfigure the design. When it
|
|
1073
1069
|
// does, the overview paints an extra preview beside EVERY primary frame
|