@agent-native/core 0.182.1 → 0.182.2-nightly-20260917210345
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 +1 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
- package/corpus/templates/design/actions/apply-visual-edit.ts +1 -0
- 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 +187 -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/bridge/editor-chrome.bridge.ts +1 -0
- 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/element-classification.ts +5 -1
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
- package/corpus/templates/design/app/components/design/types.ts +1 -0
- 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 +250 -107
- 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/delete-files.ts +69 -47
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +8 -1
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +107 -2
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
- 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-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/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 +8 -2
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/chat/agentkit-protocol.js +148 -34
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/package.json +4 -4
- package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
type PrivateBlobHandle,
|
|
17
17
|
} from "@agent-native/core/private-blob";
|
|
18
18
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
19
|
-
import { assertAccess } from "@agent-native/core/sharing";
|
|
20
|
-
import { and, asc, desc, eq, isNull } from "drizzle-orm";
|
|
19
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
20
|
+
import { and, asc, desc, eq, isNull, sql } from "drizzle-orm";
|
|
21
21
|
import { nanoid } from "nanoid";
|
|
22
22
|
|
|
23
23
|
import {
|
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
type ComponentDeletionGeometry,
|
|
26
26
|
} from "../../shared/component-archive.js";
|
|
27
27
|
import { getDb, schema } from "../db/index.js";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
affectedRowCount,
|
|
30
|
+
designSourceMutationLockKey,
|
|
31
|
+
lockDesignFilesTable,
|
|
32
|
+
withSourceFileWriteLock,
|
|
33
|
+
} from "../source-workspace.js";
|
|
29
34
|
import { buildDesignSnapshot } from "./design-snapshot.js";
|
|
30
35
|
|
|
31
36
|
const CHAT_VERSION_LOOKBACK = 100;
|
|
@@ -36,6 +41,7 @@ export interface DesignVersionChatContext {
|
|
|
36
41
|
runId?: string;
|
|
37
42
|
turnId?: string;
|
|
38
43
|
actionName?: string;
|
|
44
|
+
surface?: "editor";
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export interface DesignVersionFile {
|
|
@@ -66,7 +72,7 @@ export interface DesignVersionListEntry {
|
|
|
66
72
|
designId: string;
|
|
67
73
|
label: string | null;
|
|
68
74
|
createdAt: string | null;
|
|
69
|
-
source: "chat" | "legacy";
|
|
75
|
+
source: "chat" | "editor" | "legacy";
|
|
70
76
|
fileCount: number;
|
|
71
77
|
chatContext: DesignVersionChatContext | null;
|
|
72
78
|
editable: boolean;
|
|
@@ -136,6 +142,7 @@ function parseChatContext(
|
|
|
136
142
|
context[key] = candidate;
|
|
137
143
|
}
|
|
138
144
|
}
|
|
145
|
+
if (value.surface === "editor") context.surface = "editor";
|
|
139
146
|
return Object.keys(context).length > 0 ? context : undefined;
|
|
140
147
|
}
|
|
141
148
|
|
|
@@ -337,9 +344,9 @@ export async function readDesignVersionSnapshot(
|
|
|
337
344
|
}
|
|
338
345
|
|
|
339
346
|
function chatContextKey(
|
|
340
|
-
context: DesignVersionChatContext | undefined,
|
|
347
|
+
context: DesignVersionChatContext | null | undefined,
|
|
341
348
|
): string | null {
|
|
342
|
-
if (!context) return null;
|
|
349
|
+
if (!context || context.surface === "editor") return null;
|
|
343
350
|
const scope = context.threadId ?? "";
|
|
344
351
|
const turn = context.turnId ?? context.runId ?? "";
|
|
345
352
|
return turn ? `${scope}:${turn}` : null;
|
|
@@ -358,6 +365,18 @@ function actionChatContext(
|
|
|
358
365
|
};
|
|
359
366
|
}
|
|
360
367
|
|
|
368
|
+
function editorActionContext(
|
|
369
|
+
context: ActionRunContext,
|
|
370
|
+
): DesignVersionChatContext | null {
|
|
371
|
+
if (context.caller !== "frontend" && context.caller !== "webmcp") {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
surface: "editor",
|
|
376
|
+
...(context.actionName ? { actionName: context.actionName } : {}),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
361
380
|
function versionTime(value: string | null): number {
|
|
362
381
|
if (!value) return 0;
|
|
363
382
|
const timestamp = Date.parse(value);
|
|
@@ -372,18 +391,6 @@ function nextUpdatedAt(current: string | null, now: Date): string {
|
|
|
372
391
|
return new Date(nextMs).toISOString();
|
|
373
392
|
}
|
|
374
393
|
|
|
375
|
-
function affectedRowCount(result: unknown): number | undefined {
|
|
376
|
-
if (!result || typeof result !== "object") return undefined;
|
|
377
|
-
const candidate = result as {
|
|
378
|
-
rowsAffected?: unknown;
|
|
379
|
-
rowCount?: unknown;
|
|
380
|
-
changes?: unknown;
|
|
381
|
-
};
|
|
382
|
-
const value =
|
|
383
|
-
candidate.rowsAffected ?? candidate.rowCount ?? candidate.changes;
|
|
384
|
-
return typeof value === "number" ? value : undefined;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
394
|
function designFileRevisionWhere(file: {
|
|
388
395
|
id: string;
|
|
389
396
|
designId: string;
|
|
@@ -448,10 +455,13 @@ async function assertNoUnpersistedCollaborativeEdits(
|
|
|
448
455
|
designId: string,
|
|
449
456
|
storedFiles: ReadonlyArray<{ id: string; content: string }>,
|
|
450
457
|
designData?: string | null,
|
|
458
|
+
database?: DesignDatabase,
|
|
451
459
|
): Promise<void> {
|
|
452
460
|
let liveSnapshot;
|
|
453
461
|
try {
|
|
454
|
-
liveSnapshot =
|
|
462
|
+
liveSnapshot = database
|
|
463
|
+
? await buildDesignSnapshot(designId, designData, {}, database)
|
|
464
|
+
: await buildDesignSnapshot(designId, designData);
|
|
455
465
|
} catch {
|
|
456
466
|
throw new DesignVersionRestoreConflictError(
|
|
457
467
|
"Design history cannot verify live editing state right now. Refresh and try again.",
|
|
@@ -483,6 +493,7 @@ async function withDesignFileLocks<T>(
|
|
|
483
493
|
}
|
|
484
494
|
|
|
485
495
|
type DesignAccess = Awaited<ReturnType<typeof assertAccess>>;
|
|
496
|
+
type DesignDatabase = Pick<ReturnType<typeof getDb>, "select" | "insert">;
|
|
486
497
|
|
|
487
498
|
const designVersionLocks = new Map<string, Promise<unknown>>();
|
|
488
499
|
|
|
@@ -492,10 +503,12 @@ async function captureDesignVersion(
|
|
|
492
503
|
label: string;
|
|
493
504
|
chatContext?: DesignVersionChatContext;
|
|
494
505
|
deletionGeometry?: ComponentDeletionGeometry;
|
|
506
|
+
preferStoredFileContent?: boolean;
|
|
495
507
|
},
|
|
496
508
|
access: DesignAccess,
|
|
509
|
+
database?: DesignDatabase,
|
|
497
510
|
): Promise<{ id: string; createdAt: string; label: string }> {
|
|
498
|
-
|
|
511
|
+
let design = access.resource as {
|
|
499
512
|
data?: unknown;
|
|
500
513
|
title?: unknown;
|
|
501
514
|
description?: unknown;
|
|
@@ -503,8 +516,29 @@ async function captureDesignVersion(
|
|
|
503
516
|
designSystemId?: unknown;
|
|
504
517
|
ownerEmail?: unknown;
|
|
505
518
|
};
|
|
519
|
+
if (database) {
|
|
520
|
+
const [currentDesign] = await database
|
|
521
|
+
.select()
|
|
522
|
+
.from(schema.designs)
|
|
523
|
+
.where(
|
|
524
|
+
and(
|
|
525
|
+
eq(schema.designs.id, designId),
|
|
526
|
+
accessFilter(schema.designs, schema.designShares),
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
.limit(1);
|
|
530
|
+
if (!currentDesign) {
|
|
531
|
+
throw new Error(`Design "${designId}" not found.`);
|
|
532
|
+
}
|
|
533
|
+
design = { ...design, ...currentDesign };
|
|
534
|
+
}
|
|
506
535
|
const designData = parseDesignData(designId, design.data);
|
|
507
|
-
const
|
|
536
|
+
const snapshotOptions = options.preferStoredFileContent
|
|
537
|
+
? { preferStoredFileContent: true }
|
|
538
|
+
: undefined;
|
|
539
|
+
const liveSnapshot = database
|
|
540
|
+
? await buildDesignSnapshot(designId, designData, snapshotOptions, database)
|
|
541
|
+
: await buildDesignSnapshot(designId, designData, snapshotOptions);
|
|
508
542
|
const designTitle = typeof design.title === "string" ? design.title : "";
|
|
509
543
|
const designDescription =
|
|
510
544
|
typeof design.description === "string" || design.description === null
|
|
@@ -536,7 +570,7 @@ async function captureDesignVersion(
|
|
|
536
570
|
resolvedCssVars: liveSnapshot.resolvedCssVars,
|
|
537
571
|
deletionGeometry: options.deletionGeometry,
|
|
538
572
|
};
|
|
539
|
-
const db = getDb();
|
|
573
|
+
const db = database ?? getDb();
|
|
540
574
|
const [latest] = await db
|
|
541
575
|
.select({
|
|
542
576
|
id: schema.designVersions.id,
|
|
@@ -745,69 +779,103 @@ export async function createDesignVersionSnapshot(
|
|
|
745
779
|
* retries and multi-action turns converge on the earliest checkpoint instead
|
|
746
780
|
* of filling history with one copy per tool call.
|
|
747
781
|
*/
|
|
748
|
-
|
|
782
|
+
async function snapshotDesignBeforeAgentEditInLock(
|
|
749
783
|
designId: string,
|
|
750
|
-
context
|
|
784
|
+
context: ActionRunContext,
|
|
785
|
+
database?: DesignDatabase,
|
|
751
786
|
): Promise<{ id: string; createdAt: string; label: string } | null> {
|
|
752
|
-
if (!context) return null;
|
|
753
787
|
const chatContext = actionChatContext(context);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
return withDesignVersionLock(designId, async () => {
|
|
757
|
-
const access = await assertAccess("design", designId, "editor");
|
|
758
|
-
const key = chatContextKey(chatContext);
|
|
759
|
-
if (!key) return null;
|
|
760
|
-
|
|
761
|
-
const rows = await getDb()
|
|
762
|
-
.select({
|
|
763
|
-
id: schema.designVersions.id,
|
|
764
|
-
createdAt: schema.designVersions.createdAt,
|
|
765
|
-
label: schema.designVersions.label,
|
|
766
|
-
chatContext: schema.designVersions.chatContext,
|
|
767
|
-
})
|
|
768
|
-
.from(schema.designVersions)
|
|
769
|
-
.where(eq(schema.designVersions.designId, designId))
|
|
770
|
-
.orderBy(
|
|
771
|
-
asc(isNull(schema.designVersions.createdAt)),
|
|
772
|
-
desc(schema.designVersions.createdAt),
|
|
773
|
-
desc(schema.designVersions.id),
|
|
774
|
-
)
|
|
775
|
-
.limit(CHAT_VERSION_LOOKBACK);
|
|
776
|
-
|
|
777
|
-
let existing: { id: string; createdAt: string; label: string } | undefined;
|
|
778
|
-
for (const row of rows) {
|
|
779
|
-
let rowChatContext: DesignVersionChatContext | undefined;
|
|
780
|
-
try {
|
|
781
|
-
rowChatContext = parseStoredChatContext(row.chatContext);
|
|
782
|
-
} catch {
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
if (chatContextKey(rowChatContext) !== key) continue;
|
|
786
|
-
const candidate = {
|
|
787
|
-
id: row.id,
|
|
788
|
-
createdAt: row.createdAt ?? new Date().toISOString(),
|
|
789
|
-
label: row.label ?? "Before chat edit",
|
|
790
|
-
};
|
|
791
|
-
if (
|
|
792
|
-
!existing ||
|
|
793
|
-
versionTime(candidate.createdAt) < versionTime(existing.createdAt)
|
|
794
|
-
) {
|
|
795
|
-
existing = candidate;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (existing) return existing;
|
|
788
|
+
const editorContext = editorActionContext(context);
|
|
789
|
+
if (!chatContext && !editorContext) return null;
|
|
799
790
|
|
|
791
|
+
const access = await assertAccess("design", designId, "editor");
|
|
792
|
+
if (editorContext) {
|
|
800
793
|
return captureDesignVersion(
|
|
801
794
|
designId,
|
|
802
795
|
{
|
|
803
796
|
label: context.actionName
|
|
804
|
-
? `Before
|
|
805
|
-
: "Before
|
|
806
|
-
chatContext,
|
|
797
|
+
? `Before editor edit: ${context.actionName}`
|
|
798
|
+
: "Before editor edit",
|
|
799
|
+
chatContext: editorContext,
|
|
807
800
|
},
|
|
808
801
|
access,
|
|
802
|
+
database,
|
|
809
803
|
);
|
|
810
|
-
}
|
|
804
|
+
}
|
|
805
|
+
if (!chatContext) return null;
|
|
806
|
+
const key = chatContextKey(chatContext);
|
|
807
|
+
if (!key) return null;
|
|
808
|
+
|
|
809
|
+
const rows = await (database ?? getDb())
|
|
810
|
+
.select({
|
|
811
|
+
id: schema.designVersions.id,
|
|
812
|
+
createdAt: schema.designVersions.createdAt,
|
|
813
|
+
label: schema.designVersions.label,
|
|
814
|
+
chatContext: schema.designVersions.chatContext,
|
|
815
|
+
})
|
|
816
|
+
.from(schema.designVersions)
|
|
817
|
+
.where(eq(schema.designVersions.designId, designId))
|
|
818
|
+
.orderBy(
|
|
819
|
+
asc(isNull(schema.designVersions.createdAt)),
|
|
820
|
+
desc(schema.designVersions.createdAt),
|
|
821
|
+
desc(schema.designVersions.id),
|
|
822
|
+
)
|
|
823
|
+
.limit(CHAT_VERSION_LOOKBACK);
|
|
824
|
+
|
|
825
|
+
let existing: { id: string; createdAt: string; label: string } | undefined;
|
|
826
|
+
for (const row of rows) {
|
|
827
|
+
let rowChatContext: DesignVersionChatContext | undefined;
|
|
828
|
+
try {
|
|
829
|
+
rowChatContext = parseStoredChatContext(row.chatContext);
|
|
830
|
+
} catch {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (chatContextKey(rowChatContext) !== key) continue;
|
|
834
|
+
const candidate = {
|
|
835
|
+
id: row.id,
|
|
836
|
+
createdAt: row.createdAt ?? new Date().toISOString(),
|
|
837
|
+
label: row.label ?? "Before chat edit",
|
|
838
|
+
};
|
|
839
|
+
if (
|
|
840
|
+
!existing ||
|
|
841
|
+
versionTime(candidate.createdAt) < versionTime(existing.createdAt)
|
|
842
|
+
) {
|
|
843
|
+
existing = candidate;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (existing) return existing;
|
|
847
|
+
|
|
848
|
+
return captureDesignVersion(
|
|
849
|
+
designId,
|
|
850
|
+
{
|
|
851
|
+
label: context.actionName
|
|
852
|
+
? `Before chat edit: ${context.actionName}`
|
|
853
|
+
: "Before chat edit",
|
|
854
|
+
chatContext,
|
|
855
|
+
},
|
|
856
|
+
access,
|
|
857
|
+
database,
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export async function snapshotDesignBeforeAgentEdit(
|
|
862
|
+
designId: string,
|
|
863
|
+
context?: ActionRunContext,
|
|
864
|
+
): Promise<{ id: string; createdAt: string; label: string } | null> {
|
|
865
|
+
if (!context) return null;
|
|
866
|
+
return withDesignVersionLock(designId, () =>
|
|
867
|
+
snapshotDesignBeforeAgentEditInLock(designId, context),
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Call only while the caller owns withDesignVersionLock(designId, ...). */
|
|
872
|
+
export async function snapshotDesignBeforeAgentEditInVersionLock(
|
|
873
|
+
designId: string,
|
|
874
|
+
context?: ActionRunContext,
|
|
875
|
+
database?: DesignDatabase,
|
|
876
|
+
): Promise<{ id: string; createdAt: string; label: string } | null> {
|
|
877
|
+
if (!context) return null;
|
|
878
|
+
return snapshotDesignBeforeAgentEditInLock(designId, context, database);
|
|
811
879
|
}
|
|
812
880
|
|
|
813
881
|
export async function listDesignVersions(
|
|
@@ -853,7 +921,12 @@ export async function listDesignVersions(
|
|
|
853
921
|
designId,
|
|
854
922
|
label: row.label,
|
|
855
923
|
createdAt: row.createdAt,
|
|
856
|
-
source:
|
|
924
|
+
source:
|
|
925
|
+
chatContext?.surface === "editor"
|
|
926
|
+
? "editor"
|
|
927
|
+
: chatContext
|
|
928
|
+
? "chat"
|
|
929
|
+
: "legacy",
|
|
857
930
|
fileCount: row.fileCount ?? 0,
|
|
858
931
|
chatContext: chatContext ?? null,
|
|
859
932
|
editable: Boolean(chatContext),
|
|
@@ -899,7 +972,7 @@ export async function restoreDesignVersion(args: {
|
|
|
899
972
|
collaborationReconcilePending: string[];
|
|
900
973
|
}> {
|
|
901
974
|
return withDesignVersionLock(args.designId, async () => {
|
|
902
|
-
await assertAccess("design", args.designId, "editor");
|
|
975
|
+
const access = await assertAccess("design", args.designId, "editor");
|
|
903
976
|
const db = getDb();
|
|
904
977
|
const [version] = await db
|
|
905
978
|
.select({
|
|
@@ -950,49 +1023,54 @@ export async function restoreDesignVersion(args: {
|
|
|
950
1023
|
...target.files.flatMap((file) => (file.id ? [file.id] : [])),
|
|
951
1024
|
],
|
|
952
1025
|
async () => {
|
|
953
|
-
|
|
954
|
-
.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1026
|
+
return db.transaction(async (tx) => {
|
|
1027
|
+
await tx.execute(
|
|
1028
|
+
sql`SELECT pg_advisory_xact_lock(hashtextextended(${designSourceMutationLockKey(args.designId)}, 0::bigint))`,
|
|
1029
|
+
);
|
|
1030
|
+
await lockDesignFilesTable(tx);
|
|
1031
|
+
const lockedFiles = await tx
|
|
1032
|
+
.select()
|
|
1033
|
+
.from(schema.designFiles)
|
|
1034
|
+
.where(eq(schema.designFiles.designId, args.designId));
|
|
1035
|
+
await assertNoForeignCollaborators(
|
|
1036
|
+
[
|
|
1037
|
+
...lockedFiles.map((file) => file.id),
|
|
1038
|
+
...target.files.flatMap((file) => (file.id ? [file.id] : [])),
|
|
1039
|
+
],
|
|
1040
|
+
currentEmail,
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
const [currentDesign] = await tx
|
|
1044
|
+
.select()
|
|
1045
|
+
.from(schema.designs)
|
|
1046
|
+
.where(eq(schema.designs.id, args.designId))
|
|
1047
|
+
.limit(1);
|
|
1048
|
+
if (!currentDesign) {
|
|
1049
|
+
throw new Error(`Design "${args.designId}" not found.`);
|
|
1050
|
+
}
|
|
1051
|
+
await assertNoUnpersistedCollaborativeEdits(
|
|
1052
|
+
args.designId,
|
|
1053
|
+
lockedFiles,
|
|
1054
|
+
currentDesign.data,
|
|
1055
|
+
tx,
|
|
1056
|
+
);
|
|
1057
|
+
const before = await captureDesignVersion(
|
|
1058
|
+
args.designId,
|
|
1059
|
+
{ label: "Before restore" },
|
|
1060
|
+
access,
|
|
1061
|
+
tx,
|
|
1062
|
+
);
|
|
1063
|
+
const now = new Date();
|
|
1064
|
+
const updatedAt = nextUpdatedAt(currentDesign.updatedAt ?? null, now);
|
|
1065
|
+
const currentById = new Map(
|
|
1066
|
+
lockedFiles.map((file) => [file.id, file]),
|
|
1067
|
+
);
|
|
1068
|
+
const currentByFilename = new Map(
|
|
1069
|
+
lockedFiles.map((file) => [file.filename, file]),
|
|
1070
|
+
);
|
|
1071
|
+
const claimedIds = new Set<string>();
|
|
1072
|
+
const restoreFiles: RestoreFile[] = [];
|
|
1073
|
+
|
|
996
1074
|
for (const targetFile of target.files) {
|
|
997
1075
|
const byId = targetFile.id
|
|
998
1076
|
? currentById.get(targetFile.id)
|
|
@@ -1152,18 +1230,18 @@ export async function restoreDesignVersion(args: {
|
|
|
1152
1230
|
"Design changed while history was being restored. Refresh and try again.",
|
|
1153
1231
|
);
|
|
1154
1232
|
}
|
|
1155
|
-
});
|
|
1156
1233
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1234
|
+
return {
|
|
1235
|
+
beforeRestoreVersionId: before.id,
|
|
1236
|
+
restore: {
|
|
1237
|
+
updatedAt,
|
|
1238
|
+
files: restoreFiles,
|
|
1239
|
+
deletedFileIds: lockedFiles
|
|
1240
|
+
.filter((file) => !claimedIds.has(file.id))
|
|
1241
|
+
.map((file) => file.id),
|
|
1242
|
+
},
|
|
1243
|
+
};
|
|
1244
|
+
});
|
|
1167
1245
|
},
|
|
1168
1246
|
);
|
|
1169
1247
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
applyText,
|
|
22
22
|
seedFromText,
|
|
23
23
|
} from "@agent-native/core/collab";
|
|
24
|
-
import { eq } from "drizzle-orm";
|
|
24
|
+
import { and, eq } from "drizzle-orm";
|
|
25
25
|
import { nanoid } from "nanoid";
|
|
26
26
|
|
|
27
27
|
import {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type CanvasFramePlacement,
|
|
31
31
|
} from "../../shared/canvas-frames.js";
|
|
32
32
|
import { getDb, schema } from "../db/index.js";
|
|
33
|
+
import { withDesignSourceMutationTransaction } from "../source-workspace.js";
|
|
33
34
|
import { mutateDesignData } from "./design-data-mutation.js";
|
|
34
35
|
|
|
35
36
|
const PATH_BASE_PLACEHOLDER = "http://fusion-screen-base.invalid";
|
|
@@ -149,31 +150,56 @@ export async function upsertFusionScreens(args: {
|
|
|
149
150
|
? `${screenUrl.pathname}${screenUrl.search}`
|
|
150
151
|
: screenUrl.toString();
|
|
151
152
|
const preferredFilename = `fusion-${slugForPath(path)}.html`;
|
|
152
|
-
|
|
153
|
+
let existing = existingByFilename.get(preferredFilename);
|
|
153
154
|
const filename = existing?.filename ?? uniqueFilename(path, usedFilenames);
|
|
154
|
-
|
|
155
|
+
let fileId = existing?.id ?? nanoid();
|
|
155
156
|
const title = titleFromPath(path);
|
|
156
157
|
|
|
157
158
|
if (existing) {
|
|
158
|
-
await
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
const updated = await withDesignSourceMutationTransaction(
|
|
160
|
+
designId,
|
|
161
|
+
async (tx) => {
|
|
162
|
+
const [current] = await tx
|
|
163
|
+
.select({ id: schema.designFiles.id })
|
|
164
|
+
.from(schema.designFiles)
|
|
165
|
+
.where(
|
|
166
|
+
and(
|
|
167
|
+
eq(schema.designFiles.id, existing!.id),
|
|
168
|
+
eq(schema.designFiles.designId, designId),
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
.limit(1);
|
|
172
|
+
if (!current) return false;
|
|
173
|
+
await tx
|
|
174
|
+
.update(schema.designFiles)
|
|
175
|
+
.set({ content: url, fileType: "html", updatedAt: now })
|
|
176
|
+
.where(eq(schema.designFiles.id, existing!.id));
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
if (updated) {
|
|
181
|
+
if (await hasCollabState(existing.id)) {
|
|
182
|
+
await applyText(existing.id, url, "content", "agent");
|
|
183
|
+
} else {
|
|
184
|
+
await seedFromText(existing.id, url);
|
|
185
|
+
}
|
|
164
186
|
} else {
|
|
165
|
-
|
|
187
|
+
existing = undefined;
|
|
188
|
+
fileId = nanoid();
|
|
166
189
|
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
}
|
|
191
|
+
if (!existing) {
|
|
192
|
+
await withDesignSourceMutationTransaction(designId, (tx) =>
|
|
193
|
+
tx.insert(schema.designFiles).values({
|
|
194
|
+
id: fileId,
|
|
195
|
+
designId,
|
|
196
|
+
filename,
|
|
197
|
+
fileType: "html",
|
|
198
|
+
content: url,
|
|
199
|
+
createdAt: now,
|
|
200
|
+
updatedAt: now,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
177
203
|
await seedFromText(fileId, url);
|
|
178
204
|
}
|
|
179
205
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
seedFromText,
|
|
6
6
|
} from "@agent-native/core/collab";
|
|
7
7
|
import { assertAccess, resolveAccess } from "@agent-native/core/sharing";
|
|
8
|
-
import { and, eq } from "drizzle-orm";
|
|
8
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
9
9
|
import { nanoid } from "nanoid";
|
|
10
10
|
|
|
11
11
|
import {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../../shared/canvas-frames.js";
|
|
16
16
|
import { annotateScreenHtmlForPersist } from "../../shared/screen-annotation.js";
|
|
17
17
|
import { getDb, schema } from "../db/index.js";
|
|
18
|
+
import { designSourceMutationLockKey } from "../source-workspace.js";
|
|
18
19
|
import { mutateDesignData } from "./design-data-mutation.js";
|
|
19
20
|
|
|
20
21
|
const DEFAULT_FRAME_WIDTH = 1440;
|
|
@@ -299,6 +300,9 @@ export async function saveImportedDesignFiles(
|
|
|
299
300
|
|
|
300
301
|
try {
|
|
301
302
|
await db.transaction(async (tx) => {
|
|
303
|
+
await tx.execute(
|
|
304
|
+
sql`SELECT pg_advisory_xact_lock(hashtextextended(${designSourceMutationLockKey(designId)}, 0::bigint))`,
|
|
305
|
+
);
|
|
302
306
|
const [design] = await tx
|
|
303
307
|
.select()
|
|
304
308
|
.from(schema.designs)
|
|
@@ -267,6 +267,60 @@ export function localhostBridgeRequestError(
|
|
|
267
267
|
);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/** Fetch the read-only DOM snapshot used by live Design screens. */
|
|
271
|
+
export async function fetchLocalhostSnapshot(args: {
|
|
272
|
+
bridgeUrl: string;
|
|
273
|
+
previewToken: string | null;
|
|
274
|
+
url: string;
|
|
275
|
+
}): Promise<string> {
|
|
276
|
+
if (!args.previewToken) {
|
|
277
|
+
throw new LocalhostConnectionError(
|
|
278
|
+
"bridge-token-missing",
|
|
279
|
+
"This URL-backed screen has no preview token. Reload the frame or reconnect the localhost app before requesting a live snapshot.",
|
|
280
|
+
424,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const endpoint = new URL("/snapshot", args.bridgeUrl);
|
|
284
|
+
endpoint.searchParams.set("url", args.url);
|
|
285
|
+
let response: Response;
|
|
286
|
+
try {
|
|
287
|
+
response = await fetch(endpoint, {
|
|
288
|
+
headers: {
|
|
289
|
+
accept: "application/json",
|
|
290
|
+
"x-design-preview-token": args.previewToken,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw new LocalhostConnectionError(
|
|
295
|
+
"bridge-unreachable",
|
|
296
|
+
`Could not reach the localhost bridge for snapshot (${error instanceof Error ? error.message : String(error)}).`,
|
|
297
|
+
424,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
302
|
+
throw localhostBridgeRequestError("snapshot", response.status, errorText);
|
|
303
|
+
}
|
|
304
|
+
let payload: { html?: unknown } | null;
|
|
305
|
+
try {
|
|
306
|
+
payload = (await response.json()) as { html?: unknown } | null;
|
|
307
|
+
} catch {
|
|
308
|
+
throw new LocalhostConnectionError(
|
|
309
|
+
"bridge-request-failed",
|
|
310
|
+
"The localhost bridge returned invalid snapshot JSON.",
|
|
311
|
+
424,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (!payload || typeof payload.html !== "string") {
|
|
315
|
+
throw new LocalhostConnectionError(
|
|
316
|
+
"bridge-request-failed",
|
|
317
|
+
"The localhost bridge returned no HTML snapshot.",
|
|
318
|
+
424,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return payload.html;
|
|
322
|
+
}
|
|
323
|
+
|
|
270
324
|
/** Bridge token for callers that have no other source for one. */
|
|
271
325
|
export function requireLocalhostBridgeToken(
|
|
272
326
|
connectionId: string,
|