@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
|
@@ -1130,6 +1130,26 @@ export function prepareClonedHtmlLayer(
|
|
|
1130
1130
|
// silently misapplying edits meant for the duplicate.
|
|
1131
1131
|
reassignClonedAuthoredIds(clone, () => uniqueLayerId("copy-id"));
|
|
1132
1132
|
reassignClonedSourceIdentity(clone, () => uniqueLayerId("copy-child"));
|
|
1133
|
+
// Runtime snapshots carry the source component identity separately from
|
|
1134
|
+
// the DOM node id. Keep the component boundary stable across a clone, but
|
|
1135
|
+
// mint its instance handle with the same fresh id used for the cloned node.
|
|
1136
|
+
// Leaving the old handle in place makes two live instances address the same
|
|
1137
|
+
// runtime component when the next snapshot is serialized.
|
|
1138
|
+
for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
|
|
1139
|
+
const runtimeInstanceId = element.getAttribute(
|
|
1140
|
+
"data-agent-native-runtime-instance-id",
|
|
1141
|
+
);
|
|
1142
|
+
if (!runtimeInstanceId) continue;
|
|
1143
|
+
const nextInstanceId =
|
|
1144
|
+
nodeIdMap.get(runtimeInstanceId) ??
|
|
1145
|
+
element.getAttribute("data-agent-native-node-id");
|
|
1146
|
+
if (nextInstanceId) {
|
|
1147
|
+
element.setAttribute(
|
|
1148
|
+
"data-agent-native-runtime-instance-id",
|
|
1149
|
+
nextInstanceId,
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1133
1153
|
const linkedClone = linkedCloneSubtree(
|
|
1134
1154
|
source,
|
|
1135
1155
|
clone,
|
|
@@ -499,6 +499,8 @@ export function elementInfoFromCodeLayerNode(node: CodeLayerNode): ElementInfo {
|
|
|
499
499
|
return {
|
|
500
500
|
tagName: node.tag,
|
|
501
501
|
id: typeof node.attributes.id === "string" ? node.attributes.id : undefined,
|
|
502
|
+
componentAnnotation:
|
|
503
|
+
node.dataAttributes["data-agent-native-component"]?.trim() || undefined,
|
|
502
504
|
sourceId: bridgeSourceIdForCodeLayerNode(node),
|
|
503
505
|
provenance: provenanceForCodeLayerNode(node),
|
|
504
506
|
selector: preferredCodeLayerSelector(node),
|
package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts
CHANGED
|
@@ -53,14 +53,19 @@ export interface ApplyDesignEditorCommandArgs {
|
|
|
53
53
|
>;
|
|
54
54
|
setMode: Dispatch<SetStateAction<EditorMode>>;
|
|
55
55
|
setPinMode: Dispatch<SetStateAction<boolean>>;
|
|
56
|
+
setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
|
|
56
57
|
setScreenZoom: Dispatch<SetStateAction<number>>;
|
|
57
58
|
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
58
59
|
setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
|
|
59
60
|
setViewMode: Dispatch<SetStateAction<"single" | "overview">>;
|
|
61
|
+
pendingOverviewScreenSelectionRef?: RefObject<string | null>;
|
|
60
62
|
setZoomForView: (
|
|
61
63
|
targetView: "single" | "overview",
|
|
62
64
|
update: SetStateAction<number>,
|
|
63
65
|
) => void;
|
|
66
|
+
/** The design payload is loaded, so overview zoom conversion has a real
|
|
67
|
+
* screen list and persisted frame scale rather than the pre-load fallback. */
|
|
68
|
+
overviewDataReady?: boolean;
|
|
64
69
|
viewModeRef: RefObject<"single" | "overview">;
|
|
65
70
|
/**
|
|
66
71
|
* Reveals a screen on the overview canvas the same way a freshly-created
|
|
@@ -87,12 +92,15 @@ export function runApplyDesignEditorCommand(
|
|
|
87
92
|
setInteractDeviceName,
|
|
88
93
|
setInteractDeviceSize,
|
|
89
94
|
setMode,
|
|
95
|
+
setOverviewSelectedScreenIds,
|
|
90
96
|
setPinMode,
|
|
91
97
|
setScreenZoom,
|
|
92
98
|
setSelectedElement,
|
|
93
99
|
setSelectedLayerIdsState,
|
|
94
100
|
setViewMode,
|
|
95
101
|
setZoomForView,
|
|
102
|
+
pendingOverviewScreenSelectionRef,
|
|
103
|
+
overviewDataReady = true,
|
|
96
104
|
viewModeRef,
|
|
97
105
|
requestCameraFit,
|
|
98
106
|
}: ApplyDesignEditorCommandArgs,
|
|
@@ -132,6 +140,8 @@ export function runApplyDesignEditorCommand(
|
|
|
132
140
|
// zero files. Otherwise the navigate is silently consumed and dropped.
|
|
133
141
|
if (target && !targetFile) return false;
|
|
134
142
|
|
|
143
|
+
const targetView = editorView ?? viewModeRef.current;
|
|
144
|
+
|
|
135
145
|
const inspectorTab =
|
|
136
146
|
command.inspectorTab === "design" ||
|
|
137
147
|
command.inspectorTab === "comments" ||
|
|
@@ -171,6 +181,12 @@ export function runApplyDesignEditorCommand(
|
|
|
171
181
|
|
|
172
182
|
if (targetFile) {
|
|
173
183
|
setActiveFileId(targetFile.id);
|
|
184
|
+
if (targetView === "overview") {
|
|
185
|
+
if (pendingOverviewScreenSelectionRef) {
|
|
186
|
+
pendingOverviewScreenSelectionRef.current = targetFile.id;
|
|
187
|
+
}
|
|
188
|
+
setOverviewSelectedScreenIds([targetFile.id]);
|
|
189
|
+
}
|
|
174
190
|
}
|
|
175
191
|
if (selectionId) {
|
|
176
192
|
setSelectedLayerIdsState([selectionId]);
|
|
@@ -180,7 +196,6 @@ export function runApplyDesignEditorCommand(
|
|
|
180
196
|
typeof command.zoom === "number" && Number.isFinite(command.zoom)
|
|
181
197
|
? clampZoom(command.zoom)
|
|
182
198
|
: null;
|
|
183
|
-
const targetView = editorView ?? viewModeRef.current;
|
|
184
199
|
// Zoom-compounding fix — see shouldDeferOverviewZoomCommand's doc
|
|
185
200
|
// comment: converting a persisted overview zoom back to canvas units
|
|
186
201
|
// needs the REAL overviewZoomScale (known only once `files` loads), so
|
|
@@ -193,7 +208,10 @@ export function runApplyDesignEditorCommand(
|
|
|
193
208
|
shouldDeferOverviewZoomCommand({
|
|
194
209
|
hasZoomCommand: commandZoom !== null,
|
|
195
210
|
targetView,
|
|
196
|
-
filesLoaded:
|
|
211
|
+
filesLoaded:
|
|
212
|
+
files.length > 0 &&
|
|
213
|
+
(targetView !== "overview" ||
|
|
214
|
+
(overviewDataReady && overviewScreens.length > 0)),
|
|
197
215
|
})
|
|
198
216
|
) {
|
|
199
217
|
return false;
|
|
@@ -216,7 +234,7 @@ export function runApplyDesignEditorCommand(
|
|
|
216
234
|
const targetScreen = targetFile
|
|
217
235
|
? overviewScreens.find((screen) => screen.id === targetFile.id)
|
|
218
236
|
: undefined;
|
|
219
|
-
if (targetScreen && requestCameraFit) {
|
|
237
|
+
if (targetScreen && requestCameraFit && commandZoom === null) {
|
|
220
238
|
const geometry = resolveFrameGeometrySync({
|
|
221
239
|
screens: overviewScreens.map((screen) => ({
|
|
222
240
|
id: screen.id,
|
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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LocalJsxSourceAnchor } from "@shared/local-jsx-visual-edit";
|
|
1
2
|
import { sourceContentHash } from "@shared/source-workspace";
|
|
2
3
|
|
|
3
4
|
import type {
|
|
@@ -10,6 +11,13 @@ export interface CreateComponentSourcePreimage {
|
|
|
10
11
|
versionHash: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export interface CreateComponentLocalSource extends LocalJsxSourceAnchor {
|
|
15
|
+
connectionId: string;
|
|
16
|
+
path: string;
|
|
17
|
+
expectedVersionHash?: string;
|
|
18
|
+
propStamps?: Array<{ name: string; value: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export interface CreateComponentRequest {
|
|
14
22
|
designId: string;
|
|
15
23
|
nodeId?: string;
|
|
@@ -17,8 +25,9 @@ export interface CreateComponentRequest {
|
|
|
17
25
|
name: string;
|
|
18
26
|
fileId?: string;
|
|
19
27
|
source?: {
|
|
20
|
-
currentContent
|
|
21
|
-
expectedVersionHash
|
|
28
|
+
currentContent?: string;
|
|
29
|
+
expectedVersionHash?: string;
|
|
30
|
+
local?: CreateComponentLocalSource;
|
|
22
31
|
};
|
|
23
32
|
}
|
|
24
33
|
|
|
@@ -49,6 +58,12 @@ export interface CreateComponentActionResult {
|
|
|
49
58
|
versionHash: string;
|
|
50
59
|
updatedAt: string;
|
|
51
60
|
}>;
|
|
61
|
+
source?: {
|
|
62
|
+
kind: "local-file";
|
|
63
|
+
connectionId: string;
|
|
64
|
+
path: string;
|
|
65
|
+
versionHash: string;
|
|
66
|
+
};
|
|
52
67
|
updatedAt?: string;
|
|
53
68
|
}
|
|
54
69
|
|
|
@@ -155,10 +170,26 @@ export function createComponentActionChange(
|
|
|
155
170
|
/** Build the command-side request and hand it to the queue-owned transaction. */
|
|
156
171
|
export async function runCreateComponent(
|
|
157
172
|
args: CreateComponentArgs,
|
|
158
|
-
request: Omit<CreateComponentRequest, "designId" | "fileId" | "source"
|
|
173
|
+
request: Omit<CreateComponentRequest, "designId" | "fileId" | "source"> & {
|
|
174
|
+
source?: { local?: CreateComponentLocalSource };
|
|
175
|
+
},
|
|
159
176
|
): Promise<CreateComponentCommandOutcome | undefined> {
|
|
160
177
|
if (!args.canEditDesign) return undefined;
|
|
161
178
|
|
|
179
|
+
if (request.source?.local) {
|
|
180
|
+
const result = await args.createComponent({
|
|
181
|
+
...request,
|
|
182
|
+
designId: args.designId,
|
|
183
|
+
fileId: args.fileId,
|
|
184
|
+
source: { local: request.source.local },
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
result,
|
|
188
|
+
historyRecorded: result.persisted === true,
|
|
189
|
+
hostSync: result.persisted === true ? "accepted" : "skipped",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
return args.mutationTransaction.enqueue({
|
|
163
194
|
fileId: args.fileId,
|
|
164
195
|
selectionBefore: args.selectionBefore,
|
|
@@ -170,6 +201,16 @@ export async function runCreateComponent(
|
|
|
170
201
|
source: {
|
|
171
202
|
currentContent: source.content,
|
|
172
203
|
expectedVersionHash: source.versionHash,
|
|
204
|
+
...(request.source?.local
|
|
205
|
+
? {
|
|
206
|
+
local: {
|
|
207
|
+
...request.source.local,
|
|
208
|
+
expectedVersionHash:
|
|
209
|
+
request.source.local.expectedVersionHash ??
|
|
210
|
+
source.versionHash,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
173
214
|
},
|
|
174
215
|
}),
|
|
175
216
|
});
|
|
@@ -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
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CanvasFrameGeometry,
|
|
4
|
+
CanvasFrameGeometryById,
|
|
5
|
+
} from "@shared/canvas-frames";
|
|
3
6
|
import type { QueryClient } from "@tanstack/react-query";
|
|
4
7
|
import type { RefObject } from "react";
|
|
5
8
|
import { toast } from "sonner";
|
|
@@ -21,6 +24,41 @@ import {
|
|
|
21
24
|
import type { FileCreationHistoryEntry } from "@/pages/design-editor/history";
|
|
22
25
|
import type { DesignFile } from "@/pages/design-editor/types";
|
|
23
26
|
|
|
27
|
+
const DUPLICATE_SCREEN_GAP = 56;
|
|
28
|
+
|
|
29
|
+
function isCompleteFrameGeometry(
|
|
30
|
+
geometry: CanvasFrameGeometry | undefined,
|
|
31
|
+
): geometry is FrameGeometry {
|
|
32
|
+
return (
|
|
33
|
+
geometry !== undefined &&
|
|
34
|
+
[geometry.x, geometry.y, geometry.width, geometry.height].every(
|
|
35
|
+
(value) => typeof value === "number" && Number.isFinite(value),
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getDuplicateScreenGeometry(
|
|
41
|
+
sourceGeometry: FrameGeometry,
|
|
42
|
+
occupiedGeometries: readonly FrameGeometry[],
|
|
43
|
+
): FrameGeometry {
|
|
44
|
+
const rowBottom = sourceGeometry.y + sourceGeometry.height;
|
|
45
|
+
const rowGeometries = occupiedGeometries.filter(
|
|
46
|
+
(geometry) =>
|
|
47
|
+
geometry.y < rowBottom && geometry.y + geometry.height > sourceGeometry.y,
|
|
48
|
+
);
|
|
49
|
+
const x = Math.max(
|
|
50
|
+
sourceGeometry.x + sourceGeometry.width + DUPLICATE_SCREEN_GAP,
|
|
51
|
+
...rowGeometries.map(
|
|
52
|
+
(geometry) => geometry.x + geometry.width + DUPLICATE_SCREEN_GAP,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const z = Math.max(
|
|
56
|
+
sourceGeometry.z ?? 0,
|
|
57
|
+
...occupiedGeometries.map((geometry) => geometry.z ?? 0),
|
|
58
|
+
);
|
|
59
|
+
return { ...sourceGeometry, x, y: sourceGeometry.y, z: z + 1 };
|
|
60
|
+
}
|
|
61
|
+
|
|
24
62
|
export interface DuplicateScreenArgs {
|
|
25
63
|
canEditDesign: boolean;
|
|
26
64
|
createFileAsync: ReturnType<
|
|
@@ -28,7 +66,14 @@ export interface DuplicateScreenArgs {
|
|
|
28
66
|
>["mutateAsync"];
|
|
29
67
|
designDataJsonRef: RefObject<Record<string, unknown>>;
|
|
30
68
|
files: DesignFile[];
|
|
31
|
-
focusCreatedScreen: (
|
|
69
|
+
focusCreatedScreen: (
|
|
70
|
+
screenId: string,
|
|
71
|
+
geometry: FrameGeometry,
|
|
72
|
+
options?: {
|
|
73
|
+
preserveCamera?: boolean;
|
|
74
|
+
suppressLineupRecenter?: boolean;
|
|
75
|
+
},
|
|
76
|
+
) => void;
|
|
32
77
|
id: string | undefined;
|
|
33
78
|
liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
|
|
34
79
|
optimisticallyInsertCreatedFile: (args: {
|
|
@@ -71,6 +116,7 @@ export function runDuplicateScreen(
|
|
|
71
116
|
screenId: string,
|
|
72
117
|
request?: {
|
|
73
118
|
canvasPosition?: { x: number; y: number };
|
|
119
|
+
preserveCamera?: boolean;
|
|
74
120
|
},
|
|
75
121
|
) {
|
|
76
122
|
if (!id || !canEditDesign) return;
|
|
@@ -86,15 +132,30 @@ export function runDuplicateScreen(
|
|
|
86
132
|
width: sourceOverviewScreen?.width ?? 1280,
|
|
87
133
|
height: sourceOverviewScreen?.height ?? 2560,
|
|
88
134
|
});
|
|
135
|
+
const persistedGeometry = getCanvasFrameGeometry(designDataJsonRef.current);
|
|
136
|
+
const sourceGeometry =
|
|
137
|
+
[liveFrameGeometryRef.current[screenId], persistedGeometry[screenId]].find(
|
|
138
|
+
isCompleteFrameGeometry,
|
|
139
|
+
) ?? fallbackGeometry;
|
|
140
|
+
const occupiedGeometries = overviewScreens
|
|
141
|
+
.filter((screen) => screen.id !== screenId)
|
|
142
|
+
.map(
|
|
143
|
+
(screen) =>
|
|
144
|
+
liveFrameGeometryRef.current[screen.id] ?? persistedGeometry[screen.id],
|
|
145
|
+
)
|
|
146
|
+
.filter(isCompleteFrameGeometry);
|
|
147
|
+
const adjacentGeometry = getDuplicateScreenGeometry(
|
|
148
|
+
sourceGeometry,
|
|
149
|
+
occupiedGeometries,
|
|
150
|
+
);
|
|
89
151
|
const createdGeometry: FrameGeometry = request?.canvasPosition
|
|
90
152
|
? {
|
|
91
|
-
...
|
|
92
|
-
...liveFrameGeometryRef.current[screenId],
|
|
153
|
+
...sourceGeometry,
|
|
93
154
|
x: request.canvasPosition.x,
|
|
94
155
|
y: request.canvasPosition.y,
|
|
156
|
+
z: adjacentGeometry.z,
|
|
95
157
|
}
|
|
96
|
-
:
|
|
97
|
-
|
|
158
|
+
: adjacentGeometry;
|
|
98
159
|
// Per-call mutate callbacks, not a promise, would silently strand every
|
|
99
160
|
// duplicate but the last: a second mutate() detaches the observer from
|
|
100
161
|
// the first mutation, so only the newest call's onSuccess ever runs.
|
|
@@ -104,7 +165,7 @@ export function runDuplicateScreen(
|
|
|
104
165
|
content,
|
|
105
166
|
fileType,
|
|
106
167
|
} as any)
|
|
107
|
-
.then((result: any) => {
|
|
168
|
+
.then(async (result: any) => {
|
|
108
169
|
const nextId = typeof result?.id === "string" ? result.id : null;
|
|
109
170
|
// Refetch only when there is no created id to insert optimistically:
|
|
110
171
|
// a whole-design refetch re-downloads every screen's HTML, which is
|
|
@@ -114,30 +175,19 @@ export function runDuplicateScreen(
|
|
|
114
175
|
queryKey: ["action", "get-design"],
|
|
115
176
|
});
|
|
116
177
|
} else {
|
|
117
|
-
optimisticallyInsertCreatedFile({
|
|
118
|
-
fileId: nextId,
|
|
119
|
-
filename,
|
|
120
|
-
fileType,
|
|
121
|
-
content,
|
|
122
|
-
result,
|
|
123
|
-
});
|
|
124
178
|
// Optimistic geometry keeps frame, selection, and camera agreeing
|
|
125
|
-
// before the refetch.
|
|
126
|
-
//
|
|
179
|
+
// before the refetch. Write it before the file enters `screens`, or
|
|
180
|
+
// the geometry-sync effect can render a fallback frame first and
|
|
181
|
+
// preserve that stale position over the requested drop point.
|
|
127
182
|
writeFrameGeometrySnapshot({
|
|
128
183
|
...getCanvasFrameGeometry(designDataJsonRef.current),
|
|
129
184
|
[nextId]: createdGeometry,
|
|
130
185
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
geometry: createdGeometry,
|
|
137
|
-
});
|
|
138
|
-
// A duplicated localhost/fusion screen stays URL-backed only if its
|
|
139
|
-
// metadata comes along, and the carry must be path-addressed or it
|
|
140
|
-
// replaces a peer's metadata for every other screen.
|
|
186
|
+
// Carry screen dimensions/height mode for every duplicate so the new
|
|
187
|
+
// frame uses the same overview scale. Runtime metadata also keeps
|
|
188
|
+
// localhost/fusion duplicates URL-backed. The carry must be
|
|
189
|
+
// path-addressed or it replaces a peer's metadata for every other
|
|
190
|
+
// screen.
|
|
141
191
|
const sourceMetadataById = getDesignDataRecord(
|
|
142
192
|
designDataJsonRef.current,
|
|
143
193
|
"screenMetadata",
|
|
@@ -147,29 +197,61 @@ export function runDuplicateScreen(
|
|
|
147
197
|
screenId,
|
|
148
198
|
);
|
|
149
199
|
const sourceType = sourceMetadata.sourceType;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
const carriesRuntimeMetadata =
|
|
201
|
+
sourceType === "localhost" || sourceType === "fusion";
|
|
202
|
+
const metadataToCopy = carriesRuntimeMetadata
|
|
203
|
+
? sourceMetadata
|
|
204
|
+
: Object.fromEntries(
|
|
205
|
+
[
|
|
206
|
+
"sourceType",
|
|
207
|
+
"width",
|
|
208
|
+
"height",
|
|
209
|
+
"heightPinned",
|
|
210
|
+
"heightMode",
|
|
211
|
+
"breakpointHeights",
|
|
212
|
+
].flatMap((key) =>
|
|
213
|
+
key in sourceMetadata ? [[key, sourceMetadata[key]]] : [],
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
const screenMetadata =
|
|
217
|
+
Object.keys(metadataToCopy).length > 0
|
|
218
|
+
? { ...metadataToCopy }
|
|
219
|
+
: undefined;
|
|
220
|
+
const sourceLocalhostScreen = carriesRuntimeMetadata
|
|
221
|
+
? getDesignDataRecord(
|
|
222
|
+
getDesignDataRecord(
|
|
223
|
+
designDataJsonRef.current,
|
|
224
|
+
"localhostScreens",
|
|
225
|
+
),
|
|
226
|
+
screenId,
|
|
227
|
+
)
|
|
228
|
+
: {};
|
|
229
|
+
const localhostScreen =
|
|
230
|
+
Object.keys(sourceLocalhostScreen).length > 0
|
|
231
|
+
? { ...sourceLocalhostScreen }
|
|
232
|
+
: undefined;
|
|
233
|
+
const dataOperations: DesignDataOperation[] = [
|
|
234
|
+
{
|
|
235
|
+
op: "set",
|
|
236
|
+
path: ["canvasFrames", nextId],
|
|
237
|
+
value: createdGeometry,
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
if (screenMetadata) {
|
|
241
|
+
dataOperations.push({
|
|
242
|
+
op: "set",
|
|
243
|
+
path: ["screenMetadata", nextId],
|
|
244
|
+
value: screenMetadata,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (localhostScreen) {
|
|
248
|
+
dataOperations.push({
|
|
249
|
+
op: "set",
|
|
250
|
+
path: ["localhostScreens", nextId],
|
|
251
|
+
value: localhostScreen,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (dataOperations.length > 0) {
|
|
173
255
|
const nextData = applyDesignDataOperations(
|
|
174
256
|
designDataJsonRef.current,
|
|
175
257
|
dataOperations,
|
|
@@ -182,12 +264,35 @@ export function runDuplicateScreen(
|
|
|
182
264
|
return { ...old, data: JSON.stringify(nextData) };
|
|
183
265
|
},
|
|
184
266
|
);
|
|
185
|
-
|
|
186
|
-
|
|
267
|
+
try {
|
|
268
|
+
await updateDesignAsync({ id, dataOperations } as any);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
await queryClient.invalidateQueries({
|
|
187
271
|
queryKey: ["action", "get-design"],
|
|
188
272
|
});
|
|
189
|
-
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
190
275
|
}
|
|
276
|
+
optimisticallyInsertCreatedFile({
|
|
277
|
+
fileId: nextId,
|
|
278
|
+
filename,
|
|
279
|
+
fileType,
|
|
280
|
+
content,
|
|
281
|
+
result,
|
|
282
|
+
});
|
|
283
|
+
focusCreatedScreen(nextId, createdGeometry, {
|
|
284
|
+
preserveCamera: request?.preserveCamera,
|
|
285
|
+
suppressLineupRecenter: request?.preserveCamera,
|
|
286
|
+
});
|
|
287
|
+
recordFileCreationHistoryEntry({
|
|
288
|
+
filename,
|
|
289
|
+
content,
|
|
290
|
+
fileType,
|
|
291
|
+
geometry: createdGeometry,
|
|
292
|
+
preserveCamera: request?.preserveCamera,
|
|
293
|
+
screenMetadata,
|
|
294
|
+
localhostScreen,
|
|
295
|
+
});
|
|
191
296
|
}
|
|
192
297
|
toast.success(t("designEditor.toasts.screenDuplicated"));
|
|
193
298
|
})
|