@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
package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ApplyFileContentUpdateArgs {
|
|
|
37
37
|
refreshPreview?: boolean;
|
|
38
38
|
skipPreview?: boolean;
|
|
39
39
|
forcePreviewFullDocument?: boolean;
|
|
40
|
+
immediateSave?: boolean;
|
|
40
41
|
persist?: boolean;
|
|
41
42
|
recordHistory?: boolean;
|
|
42
43
|
historyBeforeContent?: string;
|
|
@@ -126,6 +127,7 @@ export function runApplyFileContentUpdate(
|
|
|
126
127
|
refreshPreview?: boolean;
|
|
127
128
|
skipPreview?: boolean;
|
|
128
129
|
forcePreviewFullDocument?: boolean;
|
|
130
|
+
immediateSave?: boolean;
|
|
129
131
|
persist?: boolean;
|
|
130
132
|
recordHistory?: boolean;
|
|
131
133
|
historyBeforeContent?: string;
|
|
@@ -109,6 +109,8 @@ export interface DeleteFilesArgs {
|
|
|
109
109
|
fileCreationUndoStackRef: RefObject<FileCreationHistoryEntry[]>;
|
|
110
110
|
fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
111
111
|
fileHistoryMutationPendingRef: RefObject<boolean>;
|
|
112
|
+
captureHistoryCheckpoint?: () => Promise<string>;
|
|
113
|
+
clearPendingHistory?: () => void;
|
|
112
114
|
files: DesignFile[];
|
|
113
115
|
geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
114
116
|
geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
@@ -132,7 +134,7 @@ export interface DeleteFilesArgs {
|
|
|
132
134
|
) => void;
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
export function runDeleteFiles(
|
|
137
|
+
export async function runDeleteFiles(
|
|
136
138
|
{
|
|
137
139
|
activeFile,
|
|
138
140
|
canvasFrameGeometryById,
|
|
@@ -149,6 +151,8 @@ export function runDeleteFiles(
|
|
|
149
151
|
fileCreationUndoStackRef,
|
|
150
152
|
fileDeletionUndoStackRef,
|
|
151
153
|
fileHistoryMutationPendingRef,
|
|
154
|
+
captureHistoryCheckpoint,
|
|
155
|
+
clearPendingHistory,
|
|
152
156
|
files,
|
|
153
157
|
geometryRedoStackRef,
|
|
154
158
|
geometryUndoStackRef,
|
|
@@ -186,8 +190,11 @@ export function runDeleteFiles(
|
|
|
186
190
|
failedFiles: DesignFile[],
|
|
187
191
|
) => void;
|
|
188
192
|
},
|
|
189
|
-
) {
|
|
193
|
+
): Promise<void> {
|
|
190
194
|
if (!filesToDelete.length) return;
|
|
195
|
+
if (options?.recordDeletionHistory && fileHistoryMutationPendingRef.current) {
|
|
196
|
+
throw new Error(t("common.genericError"));
|
|
197
|
+
}
|
|
191
198
|
const deleteIds = new Set(filesToDelete.map((file) => file.id));
|
|
192
199
|
const nextActiveFile = files.find((file) => !deleteIds.has(file.id));
|
|
193
200
|
const nextGeometry = cloneCanvasFrameGeometry(canvasFrameGeometryById);
|
|
@@ -201,10 +208,24 @@ export function runDeleteFiles(
|
|
|
201
208
|
})),
|
|
202
209
|
}
|
|
203
210
|
: null;
|
|
211
|
+
let historyCheckpointId: string | undefined;
|
|
204
212
|
if (deletionHistoryEntry) {
|
|
205
|
-
clearRedoStacks();
|
|
206
213
|
fileHistoryMutationPendingRef.current = true;
|
|
207
214
|
syncUndoRedoState();
|
|
215
|
+
if (captureHistoryCheckpoint) {
|
|
216
|
+
try {
|
|
217
|
+
historyCheckpointId = await captureHistoryCheckpoint();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
fileHistoryMutationPendingRef.current = false;
|
|
220
|
+
clearPendingHistory?.();
|
|
221
|
+
syncUndoRedoState();
|
|
222
|
+
toast.error(
|
|
223
|
+
error instanceof Error ? error.message : t("common.genericError"),
|
|
224
|
+
);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
clearRedoStacks();
|
|
208
229
|
}
|
|
209
230
|
filesToDelete.forEach((file) => {
|
|
210
231
|
delete nextGeometry[file.id];
|
|
@@ -371,61 +392,62 @@ export function runDeleteFiles(
|
|
|
371
392
|
setSelectedElement(null);
|
|
372
393
|
setSelectedLayerIdsState([]);
|
|
373
394
|
|
|
374
|
-
|
|
395
|
+
const results = await Promise.allSettled(
|
|
375
396
|
filesToDelete.map((file) =>
|
|
376
397
|
deleteFileMutation.mutateAsync({
|
|
377
398
|
id: file.id,
|
|
378
399
|
allowLockedLayers: true,
|
|
400
|
+
...(historyCheckpointId ? { historyCheckpointId } : {}),
|
|
379
401
|
} as any),
|
|
380
402
|
),
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
);
|
|
388
|
-
});
|
|
389
|
-
const deletedIds = new Set(deletedFiles.map((file) => file.id));
|
|
390
|
-
const failedFiles = filesToDelete.filter(
|
|
391
|
-
(file) => !deletedIds.has(file.id),
|
|
403
|
+
);
|
|
404
|
+
const deletedFiles = filesToDelete.filter((_, index) => {
|
|
405
|
+
const result = results[index];
|
|
406
|
+
return (
|
|
407
|
+
result?.status === "fulfilled" &&
|
|
408
|
+
(result.value as { deleted?: boolean } | undefined)?.deleted !== false
|
|
392
409
|
);
|
|
410
|
+
});
|
|
411
|
+
const deletedIds = new Set(deletedFiles.map((file) => file.id));
|
|
412
|
+
const failedFiles = filesToDelete.filter((file) => !deletedIds.has(file.id));
|
|
393
413
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
414
|
+
if (deletionHistoryEntry && deletedFiles.length > 0) {
|
|
415
|
+
fileDeletionUndoStackRef.current = [
|
|
416
|
+
...fileDeletionUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
417
|
+
filterFileDeletionHistoryEntry(deletionHistoryEntry, deletedIds),
|
|
418
|
+
];
|
|
419
|
+
historyOrderRef.current = [
|
|
420
|
+
...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
421
|
+
"file-deleted",
|
|
422
|
+
];
|
|
423
|
+
}
|
|
404
424
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
425
|
+
const rejected = results.find(
|
|
426
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
427
|
+
);
|
|
428
|
+
if (failedFiles.length > 0) {
|
|
429
|
+
void queryClient.invalidateQueries({
|
|
430
|
+
queryKey: ["action", "get-design"],
|
|
431
|
+
});
|
|
432
|
+
if (rejected) {
|
|
433
|
+
toast.error(
|
|
434
|
+
rejected.reason instanceof Error
|
|
435
|
+
? rejected.reason.message
|
|
436
|
+
: t("common.genericError"),
|
|
437
|
+
);
|
|
419
438
|
}
|
|
439
|
+
// A partial/failed batch is not a safe boundary for a queued undo/redo.
|
|
440
|
+
// The surviving user intent must be reissued explicitly after refresh.
|
|
441
|
+
if (deletionHistoryEntry) clearPendingHistory?.();
|
|
442
|
+
}
|
|
420
443
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
444
|
+
if (deletionHistoryEntry) {
|
|
445
|
+
fileHistoryMutationPendingRef.current = false;
|
|
446
|
+
for (const fileId of deletedIds)
|
|
447
|
+
latestClipboardMutationContentRef.current.delete(fileId);
|
|
448
|
+
}
|
|
449
|
+
options?.onMutationSettled?.(deletedFiles, failedFiles);
|
|
450
|
+
syncUndoRedoState();
|
|
429
451
|
|
|
430
452
|
// File-backed screen deletion is not a geometry-only edit. The screen rows
|
|
431
453
|
// are hard-deleted, so suppress MultiScreenCanvas' local frame-history
|
|
@@ -153,7 +153,9 @@ export interface RedoArgs {
|
|
|
153
153
|
fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
154
154
|
fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
155
155
|
fileHistoryMutationPendingRef: RefObject<boolean>;
|
|
156
|
+
clearPendingHistory?: () => void;
|
|
156
157
|
files: DesignFile[];
|
|
158
|
+
filesRef?: RefObject<DesignFile[]>;
|
|
157
159
|
focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
|
|
158
160
|
geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
159
161
|
geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
@@ -335,7 +337,9 @@ export function runRedo({
|
|
|
335
337
|
fileDeletionRedoStackRef,
|
|
336
338
|
fileDeletionUndoStackRef,
|
|
337
339
|
fileHistoryMutationPendingRef,
|
|
340
|
+
clearPendingHistory,
|
|
338
341
|
files,
|
|
342
|
+
filesRef,
|
|
339
343
|
focusCreatedScreen,
|
|
340
344
|
geometryRedoStackRef,
|
|
341
345
|
geometryUndoStackRef,
|
|
@@ -399,8 +403,9 @@ export function runRedo({
|
|
|
399
403
|
selection: GeometryHistorySelection | undefined,
|
|
400
404
|
replaySources: Record<string, string> = {},
|
|
401
405
|
) => {
|
|
406
|
+
const currentFiles = filesRef?.current ?? files;
|
|
402
407
|
const actualSources = Object.fromEntries(
|
|
403
|
-
|
|
408
|
+
currentFiles.map((file) => [
|
|
404
409
|
file.id,
|
|
405
410
|
replaySources[file.id] ?? getScreenContent(file.id),
|
|
406
411
|
]),
|
|
@@ -1226,6 +1231,7 @@ export function runRedo({
|
|
|
1226
1231
|
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
1227
1232
|
"file-created",
|
|
1228
1233
|
];
|
|
1234
|
+
clearPendingHistory?.();
|
|
1229
1235
|
fileHistoryMutationPendingRef.current = false;
|
|
1230
1236
|
syncUndoRedoState();
|
|
1231
1237
|
toast.error(
|
|
@@ -1280,6 +1286,7 @@ export function runRedo({
|
|
|
1280
1286
|
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
1281
1287
|
"file-deleted",
|
|
1282
1288
|
];
|
|
1289
|
+
clearPendingHistory?.();
|
|
1283
1290
|
}
|
|
1284
1291
|
fileHistoryMutationPendingRef.current = false;
|
|
1285
1292
|
syncUndoRedoState();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { sourceContentHash } from "@shared/source-workspace";
|
|
2
3
|
import type { QueryClient } from "@tanstack/react-query";
|
|
3
4
|
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
4
5
|
import { toast } from "sonner";
|
|
@@ -7,7 +8,11 @@ import type { DesignSaveOutboxEntry } from "@/lib/design-save-outbox";
|
|
|
7
8
|
import { updateFileResultPersistedContent } from "@/lib/design-save-outbox";
|
|
8
9
|
import type { PatchProofState } from "@/pages/design-editor/command-types";
|
|
9
10
|
import type { FileContentSaveRequest } from "@/pages/design-editor/editor-state";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
advanceLatestUnloadSaveBase,
|
|
13
|
+
prepareFileContentSaveKeepalive,
|
|
14
|
+
shouldClearLatestUnloadSave,
|
|
15
|
+
} from "@/pages/design-editor/editor-state";
|
|
11
16
|
import {
|
|
12
17
|
classifyDesignSaveFailure,
|
|
13
18
|
designSaveErrorMessage,
|
|
@@ -43,6 +48,80 @@ export interface SaveFileContentArgs {
|
|
|
43
48
|
warnChangesWillRetry: () => void;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
type FileContentSaveKeepaliveAttempt =
|
|
52
|
+
| { accepted: true; completion: Promise<unknown> }
|
|
53
|
+
| { accepted: false; completion: null };
|
|
54
|
+
|
|
55
|
+
export interface SaveFileContentKeepaliveArgs {
|
|
56
|
+
acknowledgeOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<void>;
|
|
57
|
+
createFileSaveOutboxEntry: (
|
|
58
|
+
pending: FileContentSaveRequest,
|
|
59
|
+
) => DesignSaveOutboxEntry | null;
|
|
60
|
+
journalOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<boolean>;
|
|
61
|
+
latestFileSaveForUnloadRef: RefObject<Record<string, FileContentSaveRequest>>;
|
|
62
|
+
sendKeepalive: (
|
|
63
|
+
payload: Record<string, unknown>,
|
|
64
|
+
) => FileContentSaveKeepaliveAttempt;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function runFileContentSaveKeepalive(
|
|
68
|
+
{
|
|
69
|
+
acknowledgeOutboxEntry,
|
|
70
|
+
createFileSaveOutboxEntry,
|
|
71
|
+
journalOutboxEntry,
|
|
72
|
+
latestFileSaveForUnloadRef,
|
|
73
|
+
sendKeepalive,
|
|
74
|
+
}: SaveFileContentKeepaliveArgs,
|
|
75
|
+
pending: FileContentSaveRequest,
|
|
76
|
+
) {
|
|
77
|
+
// Keep the folded oldest-base entry durable while the direct request uses
|
|
78
|
+
// this edit's own base. If the predecessor never lands, replay must start
|
|
79
|
+
// from the oldest known version rather than the successor's base.
|
|
80
|
+
const durableEntry = createFileSaveOutboxEntry(pending);
|
|
81
|
+
const keepaliveEntry = createFileSaveOutboxEntry(
|
|
82
|
+
prepareFileContentSaveKeepalive(pending),
|
|
83
|
+
);
|
|
84
|
+
if (!durableEntry || !keepaliveEntry) return;
|
|
85
|
+
const journalPromise = journalOutboxEntry(durableEntry);
|
|
86
|
+
const attempt = sendKeepalive(keepaliveEntry.payload);
|
|
87
|
+
if (!attempt.accepted) {
|
|
88
|
+
void journalPromise.catch(() => {});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
void attempt.completion
|
|
92
|
+
.then(async (result: unknown) => {
|
|
93
|
+
await journalPromise;
|
|
94
|
+
const persistedContentMatches = updateFileResultPersistedContent(
|
|
95
|
+
result,
|
|
96
|
+
pending.content,
|
|
97
|
+
);
|
|
98
|
+
if (!persistedContentMatches) return;
|
|
99
|
+
const resultInfo = result as { versionHash?: string } | undefined;
|
|
100
|
+
const latest = latestFileSaveForUnloadRef.current[pending.id];
|
|
101
|
+
if (shouldClearLatestUnloadSave(latest, pending)) {
|
|
102
|
+
await acknowledgeOutboxEntry(durableEntry);
|
|
103
|
+
delete latestFileSaveForUnloadRef.current[pending.id];
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
advanceLatestUnloadSaveBase(
|
|
108
|
+
latest,
|
|
109
|
+
pending,
|
|
110
|
+
resultInfo?.versionHash ?? sourceContentHash(pending.content),
|
|
111
|
+
)
|
|
112
|
+
) {
|
|
113
|
+
const advancedOutboxEntry = latest
|
|
114
|
+
? createFileSaveOutboxEntry(latest)
|
|
115
|
+
: null;
|
|
116
|
+
if (advancedOutboxEntry) {
|
|
117
|
+
await journalOutboxEntry(advancedOutboxEntry);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await acknowledgeOutboxEntry(durableEntry);
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
|
|
46
125
|
export function runSaveFileContent(
|
|
47
126
|
{
|
|
48
127
|
acknowledgeOutboxEntry,
|
|
@@ -120,6 +199,30 @@ export function runSaveFileContent(
|
|
|
120
199
|
pending.content,
|
|
121
200
|
t("common.genericError"),
|
|
122
201
|
);
|
|
202
|
+
const latest = latestFileSaveForUnloadRef.current[pending.id];
|
|
203
|
+
if (
|
|
204
|
+
persistedContentMatches &&
|
|
205
|
+
advanceLatestUnloadSaveBase(
|
|
206
|
+
latest,
|
|
207
|
+
pending,
|
|
208
|
+
resultInfo?.versionHash ?? sourceContentHash(pending.content),
|
|
209
|
+
)
|
|
210
|
+
) {
|
|
211
|
+
const advancedOutboxEntry = latest
|
|
212
|
+
? createFileSaveOutboxEntry(latest)
|
|
213
|
+
: null;
|
|
214
|
+
if (advancedOutboxEntry)
|
|
215
|
+
await journalOutboxEntry(advancedOutboxEntry);
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
persistedContentMatches &&
|
|
219
|
+
pending.identityMigrationSourceContent !== undefined &&
|
|
220
|
+
latestFileSaveForUnloadRef.current[pending.id] === pending
|
|
221
|
+
) {
|
|
222
|
+
// Identity repair has landed. Retire its raw-base marker so the next
|
|
223
|
+
// user edit publishes against the canonical bytes it already sees.
|
|
224
|
+
markPendingLocalFileContent(pending.id, pending.content);
|
|
225
|
+
}
|
|
123
226
|
if (persistedContentMatches && outboxEntry) {
|
|
124
227
|
await acknowledgeOutboxEntry(outboxEntry);
|
|
125
228
|
} else if (!persistedContentMatches) {
|
|
@@ -143,7 +246,6 @@ export function runSaveFileContent(
|
|
|
143
246
|
shouldClearLatestUnloadSave(
|
|
144
247
|
latestFileSaveForUnloadRef.current[pending.id],
|
|
145
248
|
pending,
|
|
146
|
-
!persistedContentMatches,
|
|
147
249
|
)
|
|
148
250
|
) {
|
|
149
251
|
delete latestFileSaveForUnloadRef.current[pending.id];
|
|
@@ -180,6 +282,9 @@ export function runSaveFileContent(
|
|
|
180
282
|
if (failureKind === "conflict") {
|
|
181
283
|
// Roll back our optimistic bytes before the refetch can race ahead.
|
|
182
284
|
rollbackPendingLocalFileContent(pending.id, pending.content);
|
|
285
|
+
if (latestFileSaveForUnloadRef.current[pending.id] === pending) {
|
|
286
|
+
delete latestFileSaveForUnloadRef.current[pending.id];
|
|
287
|
+
}
|
|
183
288
|
}
|
|
184
289
|
void queryClient.invalidateQueries({
|
|
185
290
|
queryKey: ["action", "get-design"],
|
|
@@ -410,6 +410,13 @@ export interface UndoArgs {
|
|
|
410
410
|
createFileMutation: ReturnType<
|
|
411
411
|
typeof useActionMutation<undefined, undefined, "create-file">
|
|
412
412
|
>;
|
|
413
|
+
optimisticallyInsertCreatedFile?: (args: {
|
|
414
|
+
fileId: string;
|
|
415
|
+
filename: string;
|
|
416
|
+
fileType: DesignFile["fileType"];
|
|
417
|
+
content: string;
|
|
418
|
+
result?: Record<string, unknown> | null;
|
|
419
|
+
}) => void;
|
|
413
420
|
deleteFileMutation: ReturnType<
|
|
414
421
|
typeof useActionMutation<undefined, undefined, "delete-file">
|
|
415
422
|
>;
|
|
@@ -419,7 +426,9 @@ export interface UndoArgs {
|
|
|
419
426
|
fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
420
427
|
fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
421
428
|
fileHistoryMutationPendingRef: RefObject<boolean>;
|
|
429
|
+
clearPendingHistory?: () => void;
|
|
422
430
|
files: DesignFile[];
|
|
431
|
+
filesRef?: RefObject<DesignFile[]>;
|
|
423
432
|
geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
424
433
|
geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
425
434
|
getFreshActiveContent: () => string;
|
|
@@ -547,6 +556,7 @@ export function runUndo({
|
|
|
547
556
|
contentUndoSelectionStackRef,
|
|
548
557
|
contentUndoStackRef,
|
|
549
558
|
createFileMutation,
|
|
559
|
+
optimisticallyInsertCreatedFile,
|
|
550
560
|
deleteFileMutation,
|
|
551
561
|
designDataJsonRef,
|
|
552
562
|
fileCreationRedoStackRef,
|
|
@@ -554,7 +564,9 @@ export function runUndo({
|
|
|
554
564
|
fileDeletionRedoStackRef,
|
|
555
565
|
fileDeletionUndoStackRef,
|
|
556
566
|
fileHistoryMutationPendingRef,
|
|
567
|
+
clearPendingHistory,
|
|
557
568
|
files,
|
|
569
|
+
filesRef,
|
|
558
570
|
geometryRedoStackRef,
|
|
559
571
|
geometryUndoStackRef,
|
|
560
572
|
getFreshActiveContent,
|
|
@@ -609,8 +621,9 @@ export function runUndo({
|
|
|
609
621
|
selection: GeometryHistorySelection | undefined,
|
|
610
622
|
replaySources: Record<string, string> = {},
|
|
611
623
|
) => {
|
|
624
|
+
const currentFiles = filesRef?.current ?? files;
|
|
612
625
|
const actualSources = Object.fromEntries(
|
|
613
|
-
|
|
626
|
+
currentFiles.map((file) => [
|
|
614
627
|
file.id,
|
|
615
628
|
replaySources[file.id] ?? getScreenContent(file.id),
|
|
616
629
|
]),
|
|
@@ -1478,6 +1491,17 @@ export function runUndo({
|
|
|
1478
1491
|
...file,
|
|
1479
1492
|
content: preparedFiles[index]?.content ?? file.content,
|
|
1480
1493
|
}));
|
|
1494
|
+
entry.files.forEach((file, index) => {
|
|
1495
|
+
const recreatedFile = recreatedEntry.files[index];
|
|
1496
|
+
const prepared = preparedFiles[index];
|
|
1497
|
+
if (!recreatedFile || !prepared) return;
|
|
1498
|
+
optimisticallyInsertCreatedFile?.({
|
|
1499
|
+
fileId: recreatedFile.id,
|
|
1500
|
+
filename: file.filename,
|
|
1501
|
+
fileType: file.fileType,
|
|
1502
|
+
content: prepared.content,
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1481
1505
|
const metadataRestore = restoreMetadataAndGeometry(
|
|
1482
1506
|
restoreEntry,
|
|
1483
1507
|
recreatedEntry,
|
|
@@ -1548,6 +1572,7 @@ export function runUndo({
|
|
|
1548
1572
|
setSelectedLayerIdsState(recreatedEntry.files.map((file) => file.id));
|
|
1549
1573
|
}
|
|
1550
1574
|
} catch (error) {
|
|
1575
|
+
clearPendingHistory?.();
|
|
1551
1576
|
const cleanupResults = await Promise.allSettled(
|
|
1552
1577
|
recreatedIds.map((fileId) =>
|
|
1553
1578
|
deleteFileMutation.mutateAsync({
|
|
@@ -599,6 +599,12 @@ export function restorePendingFileContent<
|
|
|
599
599
|
|
|
600
600
|
export interface FileContentSaveRequest {
|
|
601
601
|
identityMigrationSourceContent?: string;
|
|
602
|
+
/**
|
|
603
|
+
* CAS base for the durable latest-content replay. Normal saves keep their
|
|
604
|
+
* per-edit base so the serialized chain remains strict; the outbox needs the
|
|
605
|
+
* oldest unacknowledged base so a pagehide can replay the full snapshot.
|
|
606
|
+
*/
|
|
607
|
+
unloadExpectedVersionHash?: string;
|
|
602
608
|
id: string;
|
|
603
609
|
content: string;
|
|
604
610
|
syncCollab: boolean;
|
|
@@ -610,6 +616,19 @@ export interface FileContentSaveRequest {
|
|
|
610
616
|
expectedVersionHash: string;
|
|
611
617
|
}
|
|
612
618
|
|
|
619
|
+
/**
|
|
620
|
+
* A pagehide request must retain its own CAS base. The durable outbox may fold
|
|
621
|
+
* later edits onto the oldest base, but a direct keepalive can race that
|
|
622
|
+
* predecessor and must remain replayable in operation order.
|
|
623
|
+
*/
|
|
624
|
+
export function prepareFileContentSaveKeepalive(
|
|
625
|
+
pending: FileContentSaveRequest,
|
|
626
|
+
): FileContentSaveRequest {
|
|
627
|
+
return pending.unloadExpectedVersionHash === undefined
|
|
628
|
+
? pending
|
|
629
|
+
: { ...pending, unloadExpectedVersionHash: undefined };
|
|
630
|
+
}
|
|
631
|
+
|
|
613
632
|
export function coalescePendingFileContentSave(
|
|
614
633
|
next: FileContentSaveRequest,
|
|
615
634
|
pending: FileContentSaveRequest | undefined,
|
|
@@ -637,10 +656,8 @@ type FileContentSaveRequestsById = Readonly<
|
|
|
637
656
|
export function shouldClearLatestUnloadSave(
|
|
638
657
|
latest: FileContentSaveRequest | undefined,
|
|
639
658
|
completed: FileContentSaveRequest,
|
|
640
|
-
skippedStaleMirror = false,
|
|
641
659
|
): boolean {
|
|
642
660
|
return Boolean(
|
|
643
|
-
!skippedStaleMirror &&
|
|
644
661
|
latest &&
|
|
645
662
|
latest.id === completed.id &&
|
|
646
663
|
latest.content === completed.content &&
|
|
@@ -650,6 +667,50 @@ export function shouldClearLatestUnloadSave(
|
|
|
650
667
|
);
|
|
651
668
|
}
|
|
652
669
|
|
|
670
|
+
/**
|
|
671
|
+
* A completed predecessor advances a newer unload replay from its old CAS
|
|
672
|
+
* base. Mutate the request in place so debounce slots and save chains keep the
|
|
673
|
+
* same request object, then let the caller re-journal its updated payload.
|
|
674
|
+
*/
|
|
675
|
+
export function advanceLatestUnloadSaveBase(
|
|
676
|
+
latest: FileContentSaveRequest | undefined,
|
|
677
|
+
completed: FileContentSaveRequest,
|
|
678
|
+
persistedVersionHash: string,
|
|
679
|
+
): boolean {
|
|
680
|
+
const completedBase =
|
|
681
|
+
completed.unloadExpectedVersionHash ?? completed.expectedVersionHash;
|
|
682
|
+
const latestBase =
|
|
683
|
+
latest?.unloadExpectedVersionHash ?? latest?.expectedVersionHash;
|
|
684
|
+
if (
|
|
685
|
+
!latest ||
|
|
686
|
+
latest === completed ||
|
|
687
|
+
latest.id !== completed.id ||
|
|
688
|
+
latest.operationSource !== completed.operationSource ||
|
|
689
|
+
latest.operationRevision <= completed.operationRevision ||
|
|
690
|
+
latestBase !== completedBase
|
|
691
|
+
) {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
latest.unloadExpectedVersionHash = persistedVersionHash;
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function shouldClearLatestUnloadSaveForOutboxEntry(
|
|
699
|
+
latest: FileContentSaveRequest | undefined,
|
|
700
|
+
entry: {
|
|
701
|
+
resourceId: string;
|
|
702
|
+
operationSource: string;
|
|
703
|
+
operationRevision: number;
|
|
704
|
+
},
|
|
705
|
+
): boolean {
|
|
706
|
+
return Boolean(
|
|
707
|
+
latest &&
|
|
708
|
+
latest.id === entry.resourceId &&
|
|
709
|
+
latest.operationSource === entry.operationSource &&
|
|
710
|
+
latest.operationRevision === entry.operationRevision,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
653
714
|
/**
|
|
654
715
|
* Flushes debounce slots through the ordinary serialized mutation path during
|
|
655
716
|
* React cleanup (route/design changes). Real document unload is handled by the
|
|
@@ -421,6 +421,19 @@ function isRenderedBlockDisplay(display: string | null | undefined): boolean {
|
|
|
421
421
|
return display === "block" || display === "list-item";
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
function hasRenderedGridPlacement(
|
|
425
|
+
computedStyles: Record<string, string> | undefined,
|
|
426
|
+
): boolean {
|
|
427
|
+
return [computedStyles?.gridColumn, computedStyles?.gridRow].some((value) => {
|
|
428
|
+
const normalized = value?.trim().replace(/\s+/g, " ").toLowerCase();
|
|
429
|
+
if (!normalized) return false;
|
|
430
|
+
return normalized.split("/").some((line) => {
|
|
431
|
+
const trimmed = line.trim();
|
|
432
|
+
return trimmed !== "auto" && !/^span(?:\s|$)/.test(trimmed);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
424
437
|
export interface ResolveElementNudgeIntentArgs {
|
|
425
438
|
content: string;
|
|
426
439
|
source?: CodeLayerSource;
|
|
@@ -477,14 +490,45 @@ export function resolveElementNudgeIntent(
|
|
|
477
490
|
// round-trip has happened yet and `parentDisplay` is simply absent. An
|
|
478
491
|
// element that really is inline or a flex child is handled above — the parser
|
|
479
492
|
// sees those, and a rendered value always wins over this default.
|
|
480
|
-
const rendered =
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
//
|
|
484
|
-
|
|
493
|
+
const rendered =
|
|
494
|
+
args.selectedElement.parentDisplay ??
|
|
495
|
+
args.selectedElement.parentLayout?.display;
|
|
496
|
+
// A rendered grid needs its track count to map an arrow onto the next visual
|
|
497
|
+
// cell, and `display: grid` alone does not carry it. Reuse the computed track
|
|
498
|
+
// templates already reported by the bridge instead of guessing a flex row.
|
|
499
|
+
const renderedGrid =
|
|
485
500
|
parsedContainer.kind === "none" &&
|
|
486
501
|
!escapesFlow(position) &&
|
|
487
502
|
(rendered === "grid" || rendered === "inline-grid")
|
|
503
|
+
? describeFlowContainer({
|
|
504
|
+
style: {
|
|
505
|
+
display: rendered,
|
|
506
|
+
...(args.selectedElement.parentLayout?.gridTemplateColumns
|
|
507
|
+
? {
|
|
508
|
+
"grid-template-columns":
|
|
509
|
+
args.selectedElement.parentLayout.gridTemplateColumns,
|
|
510
|
+
}
|
|
511
|
+
: {}),
|
|
512
|
+
...(args.selectedElement.parentLayout?.gridTemplateRows
|
|
513
|
+
? {
|
|
514
|
+
"grid-template-rows":
|
|
515
|
+
args.selectedElement.parentLayout.gridTemplateRows,
|
|
516
|
+
}
|
|
517
|
+
: {}),
|
|
518
|
+
...(args.selectedElement.parentLayout?.gridAutoFlow
|
|
519
|
+
? {
|
|
520
|
+
"grid-auto-flow":
|
|
521
|
+
args.selectedElement.parentLayout.gridAutoFlow,
|
|
522
|
+
}
|
|
523
|
+
: {}),
|
|
524
|
+
},
|
|
525
|
+
})
|
|
526
|
+
: NO_FLOW_CONTAINER;
|
|
527
|
+
if (
|
|
528
|
+
parsedContainer.kind === "none" &&
|
|
529
|
+
!escapesFlow(position) &&
|
|
530
|
+
(rendered === "grid" || rendered === "inline-grid") &&
|
|
531
|
+
(renderedGrid.kind !== "grid" || renderedGrid.lineLength === null)
|
|
488
532
|
) {
|
|
489
533
|
return { kind: "none" };
|
|
490
534
|
}
|
|
@@ -496,29 +540,31 @@ export function resolveElementNudgeIntent(
|
|
|
496
540
|
if (
|
|
497
541
|
parsedContainer.kind === "none" &&
|
|
498
542
|
!escapesFlow(position) &&
|
|
499
|
-
|
|
543
|
+
(rendered === "flex" || rendered === "inline-flex") &&
|
|
500
544
|
!renderedFlexDirection
|
|
501
545
|
) {
|
|
502
546
|
return { kind: "none" };
|
|
503
547
|
}
|
|
504
548
|
const container: FlowContainerInfo =
|
|
505
549
|
parsedContainer.kind === "none" && !escapesFlow(position)
|
|
506
|
-
?
|
|
507
|
-
?
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
: "
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
550
|
+
? renderedGrid.kind === "grid"
|
|
551
|
+
? renderedGrid
|
|
552
|
+
: isRenderedFlowDisplay(rendered)
|
|
553
|
+
? {
|
|
554
|
+
...NO_FLOW_CONTAINER,
|
|
555
|
+
kind: "flex",
|
|
556
|
+
axis: renderedFlexDirection?.startsWith("column")
|
|
557
|
+
? "vertical"
|
|
558
|
+
: "horizontal",
|
|
559
|
+
reversed: renderedFlexDirection?.endsWith("-reverse") ?? false,
|
|
560
|
+
}
|
|
561
|
+
: // `parent` null means the node is a projection root: it has no flow to
|
|
562
|
+
// reorder within, and the bridge reports `parentDisplay: undefined`
|
|
563
|
+
// for it exactly as it does for a not-yet-measured selection.
|
|
564
|
+
isRenderedBlockDisplay(rendered) ||
|
|
565
|
+
(rendered === undefined && parent !== null)
|
|
566
|
+
? BLOCK_FLOW_CONTAINER
|
|
567
|
+
: parsedContainer
|
|
522
568
|
: parsedContainer;
|
|
523
569
|
// Flex/grid paint children by `order` and explicit grid placement, not DOM
|
|
524
570
|
// position, so moving the node would write a source change that produces no
|
|
@@ -552,6 +598,12 @@ export function resolveElementNudgeIntent(
|
|
|
552
598
|
) {
|
|
553
599
|
return { kind: "none" };
|
|
554
600
|
}
|
|
601
|
+
if (
|
|
602
|
+
container.kind === "grid" &&
|
|
603
|
+
hasRenderedGridPlacement(args.selectedElement.computedStyles)
|
|
604
|
+
) {
|
|
605
|
+
return { kind: "none" };
|
|
606
|
+
}
|
|
555
607
|
|
|
556
608
|
const intent = resolveNudgeIntent({
|
|
557
609
|
direction: args.direction,
|