@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/README.md
CHANGED
|
@@ -212,6 +212,16 @@ function ClipsExtensionAuthBridge() {
|
|
|
212
212
|
const targetExtensionId = extensionId;
|
|
213
213
|
|
|
214
214
|
let cancelled = false;
|
|
215
|
+
let removeBridgeListener: (() => void) | null = null;
|
|
216
|
+
|
|
217
|
+
const completeExtensionSignIn = () => {
|
|
218
|
+
if (cancelled) return;
|
|
219
|
+
const cleaned = new URL(window.location.href);
|
|
220
|
+
cleaned.searchParams.delete("clipsExtensionAuth");
|
|
221
|
+
cleaned.searchParams.delete("clipsExtensionId");
|
|
222
|
+
window.history.replaceState(window.history.state, "", cleaned);
|
|
223
|
+
setShowAuthSuccess(true);
|
|
224
|
+
};
|
|
215
225
|
|
|
216
226
|
async function sendSessionToExtension() {
|
|
217
227
|
const runtime = (
|
|
@@ -219,7 +229,6 @@ function ClipsExtensionAuthBridge() {
|
|
|
219
229
|
chrome?: { runtime?: ExternalChromeRuntime };
|
|
220
230
|
}
|
|
221
231
|
).chrome?.runtime;
|
|
222
|
-
if (!runtime?.sendMessage) return;
|
|
223
232
|
|
|
224
233
|
const response = await fetch(appPath("/_agent-native/auth/session"), {
|
|
225
234
|
credentials: "include",
|
|
@@ -233,21 +242,60 @@ function ClipsExtensionAuthBridge() {
|
|
|
233
242
|
return;
|
|
234
243
|
}
|
|
235
244
|
|
|
245
|
+
const message = {
|
|
246
|
+
source: "clips-auth-bridge",
|
|
247
|
+
kind: "session",
|
|
248
|
+
token: session.token,
|
|
249
|
+
email: session.email,
|
|
250
|
+
clipsBaseUrl: window.location.origin,
|
|
251
|
+
} as const;
|
|
252
|
+
const sendViaPageBridge = () => {
|
|
253
|
+
const onMessage = (event: MessageEvent) => {
|
|
254
|
+
if (
|
|
255
|
+
event.source !== window ||
|
|
256
|
+
event.origin !== window.location.origin
|
|
257
|
+
) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const data = event.data as
|
|
261
|
+
| { source?: unknown; kind?: unknown; ok?: unknown }
|
|
262
|
+
| undefined;
|
|
263
|
+
if (
|
|
264
|
+
data?.source !== "clips-auth-bridge" ||
|
|
265
|
+
data.kind !== "session-result"
|
|
266
|
+
) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
removeBridgeListener?.();
|
|
270
|
+
removeBridgeListener = null;
|
|
271
|
+
if (data.ok === true) completeExtensionSignIn();
|
|
272
|
+
};
|
|
273
|
+
removeBridgeListener = () =>
|
|
274
|
+
window.removeEventListener("message", onMessage);
|
|
275
|
+
window.addEventListener("message", onMessage);
|
|
276
|
+
window.postMessage(message, window.location.origin);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (!runtime?.sendMessage) {
|
|
280
|
+
sendViaPageBridge();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
236
284
|
runtime.sendMessage(
|
|
237
285
|
targetExtensionId,
|
|
238
286
|
{
|
|
239
287
|
type: "CLIPS_AUTH_SESSION",
|
|
240
|
-
token:
|
|
241
|
-
email:
|
|
242
|
-
clipsBaseUrl:
|
|
288
|
+
token: message.token,
|
|
289
|
+
email: message.email,
|
|
290
|
+
clipsBaseUrl: message.clipsBaseUrl,
|
|
243
291
|
},
|
|
244
292
|
(extensionResponse) => {
|
|
245
|
-
if (cancelled
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
293
|
+
if (cancelled) return;
|
|
294
|
+
if (!runtime.lastError && extensionResponse?.ok) {
|
|
295
|
+
completeExtensionSignIn();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
sendViaPageBridge();
|
|
251
299
|
},
|
|
252
300
|
);
|
|
253
301
|
}
|
|
@@ -255,6 +303,7 @@ function ClipsExtensionAuthBridge() {
|
|
|
255
303
|
void sendSessionToExtension();
|
|
256
304
|
return () => {
|
|
257
305
|
cancelled = true;
|
|
306
|
+
removeBridgeListener?.();
|
|
258
307
|
};
|
|
259
308
|
}, [location.search]);
|
|
260
309
|
|
|
@@ -77,6 +77,13 @@ type PopupStartMessage = {
|
|
|
77
77
|
settings?: Partial<ExtensionSettings>;
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
type AuthSessionMessage = {
|
|
81
|
+
type: "CLIPS_AUTH_SESSION";
|
|
82
|
+
token?: string;
|
|
83
|
+
email?: string;
|
|
84
|
+
clipsBaseUrl?: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
80
87
|
type ExternalMessage =
|
|
81
88
|
| {
|
|
82
89
|
type: "CLIPS_CAPTURE_START";
|
|
@@ -93,12 +100,7 @@ type ExternalMessage =
|
|
|
93
100
|
type: "CLIPS_CAPTURE_CANCEL";
|
|
94
101
|
sessionId?: string;
|
|
95
102
|
}
|
|
96
|
-
|
|
|
97
|
-
type: "CLIPS_AUTH_SESSION";
|
|
98
|
-
token?: string;
|
|
99
|
-
email?: string;
|
|
100
|
-
clipsBaseUrl?: string;
|
|
101
|
-
};
|
|
103
|
+
| AuthSessionMessage;
|
|
102
104
|
|
|
103
105
|
type ChromeTab = {
|
|
104
106
|
id?: number;
|
|
@@ -2237,6 +2239,35 @@ async function handlePopupSignIn(message: {
|
|
|
2237
2239
|
return { ok: true };
|
|
2238
2240
|
}
|
|
2239
2241
|
|
|
2242
|
+
async function handleAuthSession(
|
|
2243
|
+
message: AuthSessionMessage,
|
|
2244
|
+
senderUrl?: string,
|
|
2245
|
+
) {
|
|
2246
|
+
const settings = await readSettings();
|
|
2247
|
+
const clipsBaseUrl = normalizeBaseUrl(
|
|
2248
|
+
message.clipsBaseUrl ?? settings.clipsBaseUrl,
|
|
2249
|
+
);
|
|
2250
|
+
const senderOrigin = originOf(senderUrl);
|
|
2251
|
+
if (
|
|
2252
|
+
!senderOrigin ||
|
|
2253
|
+
senderOrigin !== originOf(clipsBaseUrl) ||
|
|
2254
|
+
senderOrigin !== originOf(settings.clipsBaseUrl)
|
|
2255
|
+
) {
|
|
2256
|
+
return { ok: false, error: "Auth message came from the wrong origin." };
|
|
2257
|
+
}
|
|
2258
|
+
if (typeof message.token !== "string" || !message.token.trim()) {
|
|
2259
|
+
return { ok: false, error: "Missing auth token." };
|
|
2260
|
+
}
|
|
2261
|
+
await storageSet({ ...settings, clipsBaseUrl });
|
|
2262
|
+
await saveAuthSession({
|
|
2263
|
+
token: message.token,
|
|
2264
|
+
email: typeof message.email === "string" ? message.email : undefined,
|
|
2265
|
+
clipsBaseUrl,
|
|
2266
|
+
savedAt: nowIso(),
|
|
2267
|
+
});
|
|
2268
|
+
return { ok: true };
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2240
2271
|
function summarize(snapshot: {
|
|
2241
2272
|
endedAt: string;
|
|
2242
2273
|
consoleLogs: ConsoleLog[];
|
|
@@ -2734,25 +2765,7 @@ async function handleExternalMessage(
|
|
|
2734
2765
|
}
|
|
2735
2766
|
|
|
2736
2767
|
if (message.type === "CLIPS_AUTH_SESSION") {
|
|
2737
|
-
|
|
2738
|
-
const clipsBaseUrl = normalizeBaseUrl(
|
|
2739
|
-
message.clipsBaseUrl ?? settings.clipsBaseUrl,
|
|
2740
|
-
);
|
|
2741
|
-
const senderOrigin = originOf(sender?.url);
|
|
2742
|
-
if (!senderOrigin || senderOrigin !== originOf(clipsBaseUrl)) {
|
|
2743
|
-
return { ok: false, error: "Auth message came from the wrong origin." };
|
|
2744
|
-
}
|
|
2745
|
-
if (typeof message.token !== "string" || !message.token.trim()) {
|
|
2746
|
-
return { ok: false, error: "Missing auth token." };
|
|
2747
|
-
}
|
|
2748
|
-
await storageSet({ ...settings, clipsBaseUrl });
|
|
2749
|
-
await saveAuthSession({
|
|
2750
|
-
token: message.token,
|
|
2751
|
-
email: typeof message.email === "string" ? message.email : undefined,
|
|
2752
|
-
clipsBaseUrl,
|
|
2753
|
-
savedAt: nowIso(),
|
|
2754
|
-
});
|
|
2755
|
-
return { ok: true };
|
|
2768
|
+
return handleAuthSession(message, sender?.url);
|
|
2756
2769
|
}
|
|
2757
2770
|
|
|
2758
2771
|
if (message.type === "CLIPS_CAPTURE_START") {
|
|
@@ -2858,6 +2871,13 @@ async function dispatchRuntimeMessage(
|
|
|
2858
2871
|
): Promise<unknown> {
|
|
2859
2872
|
const type = (message as { type?: unknown }).type;
|
|
2860
2873
|
|
|
2874
|
+
if (type === "CLIPS_AUTH_SESSION") {
|
|
2875
|
+
return handleAuthSession(
|
|
2876
|
+
message as AuthSessionMessage,
|
|
2877
|
+
sender?.tab?.url ?? sender?.url,
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2861
2881
|
if (type === "CLIPS_DIAGNOSTIC_INTERACTION") {
|
|
2862
2882
|
const tabId = sender?.tab?.id;
|
|
2863
2883
|
const sessionId =
|
|
@@ -141,6 +141,49 @@
|
|
|
141
141
|
}
|
|
142
142
|
sendDiagnosticNavigation(data.url);
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
window.addEventListener("message", (event) => {
|
|
146
|
+
if (event.source !== window || event.origin !== window.location.origin) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const data = event.data as
|
|
150
|
+
| {
|
|
151
|
+
source?: unknown;
|
|
152
|
+
kind?: unknown;
|
|
153
|
+
token?: unknown;
|
|
154
|
+
email?: unknown;
|
|
155
|
+
clipsBaseUrl?: unknown;
|
|
156
|
+
}
|
|
157
|
+
| undefined;
|
|
158
|
+
if (
|
|
159
|
+
data?.source !== "clips-auth-bridge" ||
|
|
160
|
+
data.kind !== "session" ||
|
|
161
|
+
typeof data.token !== "string" ||
|
|
162
|
+
typeof data.clipsBaseUrl !== "string"
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
chrome.runtime.sendMessage(
|
|
167
|
+
{
|
|
168
|
+
type: "CLIPS_AUTH_SESSION",
|
|
169
|
+
token: data.token,
|
|
170
|
+
...(typeof data.email === "string" ? { email: data.email } : {}),
|
|
171
|
+
clipsBaseUrl: data.clipsBaseUrl,
|
|
172
|
+
},
|
|
173
|
+
(response?: { ok?: boolean; error?: string }) => {
|
|
174
|
+
void chrome.runtime.lastError;
|
|
175
|
+
window.postMessage(
|
|
176
|
+
{
|
|
177
|
+
source: "clips-auth-bridge",
|
|
178
|
+
kind: "session-result",
|
|
179
|
+
ok: response?.ok === true,
|
|
180
|
+
...(response?.error ? { error: response.error } : {}),
|
|
181
|
+
},
|
|
182
|
+
window.location.origin,
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
});
|
|
144
187
|
window.postMessage(
|
|
145
188
|
{ source: "clips-diagnostic-history", kind: "request-token" },
|
|
146
189
|
"*",
|
|
@@ -43,6 +43,8 @@ mod util;
|
|
|
43
43
|
mod whisper_model;
|
|
44
44
|
mod whisper_speech;
|
|
45
45
|
|
|
46
|
+
use std::time::Duration;
|
|
47
|
+
|
|
46
48
|
use tauri::{Emitter, Manager};
|
|
47
49
|
|
|
48
50
|
use clips::{position_popover, toggle_popover};
|
|
@@ -62,6 +64,12 @@ use util::{
|
|
|
62
64
|
// should replace with their real icon.
|
|
63
65
|
pub(crate) const TRAY_PNG: &[u8] = include_bytes!("../icons/tray.png");
|
|
64
66
|
|
|
67
|
+
const POPOVER_BLUR_GUARD: Duration = Duration::from_millis(1500);
|
|
68
|
+
|
|
69
|
+
fn popover_blur_delay(elapsed: Duration) -> Option<Duration> {
|
|
70
|
+
(elapsed < POPOVER_BLUR_GUARD).then(|| POPOVER_BLUR_GUARD - elapsed)
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
fn present_popover(app: &tauri::AppHandle) {
|
|
66
74
|
if let Some(window) = app.get_webview_window("popover") {
|
|
67
75
|
set_capture_excluded(&window);
|
|
@@ -480,7 +488,7 @@ pub fn run() {
|
|
|
480
488
|
}
|
|
481
489
|
|
|
482
490
|
// Hide the popover on blur so it feels like a real menu-bar popover.
|
|
483
|
-
// The
|
|
491
|
+
// The 1.5s guard is the important bit — during the tray-click
|
|
484
492
|
// itself macOS briefly steals focus from the popover, which would
|
|
485
493
|
// fire Focused(false) and hide the window we literally just showed.
|
|
486
494
|
if let Some(window) = app.get_webview_window("popover") {
|
|
@@ -511,11 +519,37 @@ pub fn run() {
|
|
|
511
519
|
let shown_at = app_handle
|
|
512
520
|
.try_state::<PopoverShownAt>()
|
|
513
521
|
.and_then(|s| s.0.lock().ok().and_then(|g| *g));
|
|
514
|
-
let
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
522
|
+
let elapsed = shown_at.map(|t| t.elapsed()).unwrap_or(Duration::MAX);
|
|
523
|
+
dlog!(
|
|
524
|
+
"[clips-tray] popover blur, elapsed_ms={}",
|
|
525
|
+
elapsed.as_millis()
|
|
526
|
+
);
|
|
527
|
+
if let Some(delay) = popover_blur_delay(elapsed) {
|
|
528
|
+
let Some(shown_at) = shown_at else {
|
|
529
|
+
return;
|
|
530
|
+
};
|
|
531
|
+
let delayed_handle = handle.clone();
|
|
532
|
+
let delayed_app_handle = app_handle.clone();
|
|
533
|
+
tauri::async_runtime::spawn(async move {
|
|
534
|
+
tokio::time::sleep(delay).await;
|
|
535
|
+
let still_current = delayed_app_handle
|
|
536
|
+
.try_state::<PopoverShownAt>()
|
|
537
|
+
.and_then(|state| {
|
|
538
|
+
state.0.lock().ok().map(|guard| *guard == Some(shown_at))
|
|
539
|
+
})
|
|
540
|
+
.unwrap_or(false);
|
|
541
|
+
if !still_current
|
|
542
|
+
|| is_recording_active(&delayed_app_handle)
|
|
543
|
+
|| !config::auto_hide_popover_enabled(&delayed_app_handle)
|
|
544
|
+
|| delayed_handle.is_focused().unwrap_or(true)
|
|
545
|
+
{
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
let _ = delayed_handle.hide();
|
|
549
|
+
clips::close_bubble_if_idle(&delayed_app_handle);
|
|
550
|
+
let _ = delayed_app_handle.emit("clips:popover-visible", false);
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
519
553
|
let _ = handle.hide();
|
|
520
554
|
clips::close_bubble_if_idle(&app_handle);
|
|
521
555
|
let _ = app_handle.emit("clips:popover-visible", false);
|
|
@@ -623,3 +657,18 @@ pub fn run() {
|
|
|
623
657
|
}
|
|
624
658
|
});
|
|
625
659
|
}
|
|
660
|
+
|
|
661
|
+
#[cfg(test)]
|
|
662
|
+
mod tests {
|
|
663
|
+
use super::{popover_blur_delay, POPOVER_BLUR_GUARD};
|
|
664
|
+
use std::time::Duration;
|
|
665
|
+
|
|
666
|
+
#[test]
|
|
667
|
+
fn retries_blur_until_guard_expires() {
|
|
668
|
+
assert_eq!(
|
|
669
|
+
popover_blur_delay(Duration::from_millis(400)),
|
|
670
|
+
Some(Duration::from_millis(1100))
|
|
671
|
+
);
|
|
672
|
+
assert_eq!(popover_blur_delay(POPOVER_BLUR_GUARD), None);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
@@ -3320,6 +3320,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
3320
3320
|
alignItems: parentStyles.alignItems,
|
|
3321
3321
|
justifyContent: parentStyles.justifyContent,
|
|
3322
3322
|
gap: parentStyles.gap,
|
|
3323
|
+
gridAutoFlow: parentStyles.gridAutoFlow,
|
|
3323
3324
|
gridTemplateColumns: parentStyles.gridTemplateColumns,
|
|
3324
3325
|
gridTemplateRows: parentStyles.gridTemplateRows,
|
|
3325
3326
|
position: parentStyles.position
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../server/lib/design-data-mutation.js";
|
|
18
18
|
import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
|
|
19
19
|
import { resolveLocalhostConnectionScope } from "../server/lib/localhost-connection.js";
|
|
20
|
+
import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
|
|
20
21
|
import {
|
|
21
22
|
mergeCanvasFramePlacements,
|
|
22
23
|
parseCanvasFrameGeometryById,
|
|
@@ -938,7 +939,7 @@ export default defineAction({
|
|
|
938
939
|
)
|
|
939
940
|
? preferredExisting
|
|
940
941
|
: undefined;
|
|
941
|
-
|
|
942
|
+
let existing =
|
|
942
943
|
matchingPreferredExisting ??
|
|
943
944
|
routeCandidates.find((candidate) => {
|
|
944
945
|
const frame = existingCanvasFrames[candidate.id];
|
|
@@ -997,27 +998,51 @@ export default defineAction({
|
|
|
997
998
|
seenRouteRequestKeys.add(routeRequestKey);
|
|
998
999
|
|
|
999
1000
|
if (existing) {
|
|
1000
|
-
await
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1001
|
+
const updated = await withDesignSourceMutationTransaction(
|
|
1002
|
+
designId,
|
|
1003
|
+
async (tx) => {
|
|
1004
|
+
const [current] = await tx
|
|
1005
|
+
.select({ id: schema.designFiles.id })
|
|
1006
|
+
.from(schema.designFiles)
|
|
1007
|
+
.where(
|
|
1008
|
+
and(
|
|
1009
|
+
eq(schema.designFiles.id, existing!.id),
|
|
1010
|
+
eq(schema.designFiles.designId, designId),
|
|
1011
|
+
),
|
|
1012
|
+
)
|
|
1013
|
+
.limit(1);
|
|
1014
|
+
if (!current) return false;
|
|
1015
|
+
await tx
|
|
1016
|
+
.update(schema.designFiles)
|
|
1017
|
+
.set({ content: url, fileType: "html", updatedAt: now })
|
|
1018
|
+
.where(eq(schema.designFiles.id, existing!.id));
|
|
1019
|
+
return true;
|
|
1020
|
+
},
|
|
1021
|
+
);
|
|
1022
|
+
if (updated) {
|
|
1023
|
+
if (await hasCollabState(existing.id)) {
|
|
1024
|
+
await applyText(existing.id, url, "content", "agent");
|
|
1025
|
+
} else {
|
|
1026
|
+
await seedFromText(existing.id, url);
|
|
1027
|
+
}
|
|
1006
1028
|
} else {
|
|
1007
|
-
|
|
1029
|
+
existing = undefined;
|
|
1008
1030
|
}
|
|
1009
|
-
}
|
|
1031
|
+
}
|
|
1032
|
+
if (!existing) {
|
|
1010
1033
|
for (let attempt = 0; ; attempt += 1) {
|
|
1011
1034
|
try {
|
|
1012
|
-
await
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1035
|
+
await withDesignSourceMutationTransaction(designId, (tx) =>
|
|
1036
|
+
tx.insert(schema.designFiles).values({
|
|
1037
|
+
id: fileId,
|
|
1038
|
+
designId,
|
|
1039
|
+
filename,
|
|
1040
|
+
fileType: "html",
|
|
1041
|
+
content: url,
|
|
1042
|
+
createdAt: now,
|
|
1043
|
+
updatedAt: now,
|
|
1044
|
+
}),
|
|
1045
|
+
);
|
|
1021
1046
|
await seedFromText(fileId, url);
|
|
1022
1047
|
break;
|
|
1023
1048
|
} catch (err) {
|
|
@@ -1048,10 +1073,17 @@ export default defineAction({
|
|
|
1048
1073
|
routeUrlsMatch(winner.content, url)
|
|
1049
1074
|
) {
|
|
1050
1075
|
fileId = winner.id;
|
|
1051
|
-
await
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1076
|
+
await withDesignSourceMutationTransaction(designId, (tx) =>
|
|
1077
|
+
tx
|
|
1078
|
+
.update(schema.designFiles)
|
|
1079
|
+
.set({ content: url, fileType: "html", updatedAt: now })
|
|
1080
|
+
.where(
|
|
1081
|
+
and(
|
|
1082
|
+
eq(schema.designFiles.id, winner.id),
|
|
1083
|
+
eq(schema.designFiles.designId, designId),
|
|
1084
|
+
),
|
|
1085
|
+
),
|
|
1086
|
+
);
|
|
1055
1087
|
if (await hasCollabState(winner.id)) {
|
|
1056
1088
|
await applyText(winner.id, url, "content", "agent");
|
|
1057
1089
|
} else {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
remapTemplateFileIds,
|
|
18
18
|
templateFileDimensions,
|
|
19
19
|
} from "../server/lib/design-template-data.js";
|
|
20
|
+
import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
|
|
20
21
|
import { BOARD_FILENAME } from "../shared/board-file.js";
|
|
21
22
|
import { getDesignTemplatePreset } from "../shared/design-template-presets.js";
|
|
22
23
|
import { countLockedLayersAcrossFiles } from "../shared/locked-layers.js";
|
|
@@ -24,6 +25,10 @@ import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
|
|
|
24
25
|
import { sourceContentHash } from "../shared/source-workspace.js";
|
|
25
26
|
import getDesignSystem from "./get-design-system.js";
|
|
26
27
|
|
|
28
|
+
type DesignTransaction = Parameters<
|
|
29
|
+
Parameters<ReturnType<typeof getDb>["transaction"]>[0]
|
|
30
|
+
>[0];
|
|
31
|
+
|
|
27
32
|
interface TemplateFile {
|
|
28
33
|
id: string;
|
|
29
34
|
filename: string;
|
|
@@ -168,39 +173,6 @@ export default defineAction({
|
|
|
168
173
|
let targetExistingData: Record<string, unknown> = {};
|
|
169
174
|
if (targetDesignId) {
|
|
170
175
|
await assertAccess("design", targetDesignId, "editor");
|
|
171
|
-
// The editor creates the board row on mount, so a design with nothing
|
|
172
|
-
// drawn in it already has one file. Screens are what count as content.
|
|
173
|
-
const [existingDesign] = await db
|
|
174
|
-
.select({ data: schema.designs.data })
|
|
175
|
-
.from(schema.designs)
|
|
176
|
-
.where(eq(schema.designs.id, targetDesignId))
|
|
177
|
-
.limit(1);
|
|
178
|
-
if (typeof existingDesign?.data === "string") {
|
|
179
|
-
try {
|
|
180
|
-
const parsed = JSON.parse(existingDesign.data);
|
|
181
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
182
|
-
targetExistingData = parsed as Record<string, unknown>;
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
// coercion-ok: unreadable prior data is replaced wholesale below,
|
|
186
|
-
// which is the same outcome as the create path.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
const [existing] = await db
|
|
190
|
-
.select({ id: schema.designFiles.id })
|
|
191
|
-
.from(schema.designFiles)
|
|
192
|
-
.where(
|
|
193
|
-
and(
|
|
194
|
-
eq(schema.designFiles.designId, targetDesignId),
|
|
195
|
-
ne(schema.designFiles.filename, BOARD_FILENAME),
|
|
196
|
-
),
|
|
197
|
-
)
|
|
198
|
-
.limit(1);
|
|
199
|
-
if (existing) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
"Target design already has files. Templates only fill an empty design.",
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
176
|
}
|
|
205
177
|
const designId = targetDesignId ?? nanoid();
|
|
206
178
|
const now = new Date().toISOString();
|
|
@@ -244,8 +216,45 @@ export default defineAction({
|
|
|
244
216
|
content: annotateScreenHtmlForPersist(file.content, file.fileType),
|
|
245
217
|
}));
|
|
246
218
|
|
|
247
|
-
|
|
219
|
+
const persist = async (tx: DesignTransaction) => {
|
|
248
220
|
if (targetDesignId) {
|
|
221
|
+
// The editor creates the board row on mount, so a design with nothing
|
|
222
|
+
// drawn in it already has one file. Screens are what count as content.
|
|
223
|
+
const [existingDesign] = await tx
|
|
224
|
+
.select({ data: schema.designs.data })
|
|
225
|
+
.from(schema.designs)
|
|
226
|
+
.where(eq(schema.designs.id, targetDesignId))
|
|
227
|
+
.limit(1);
|
|
228
|
+
if (typeof existingDesign?.data === "string") {
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(existingDesign.data);
|
|
231
|
+
if (
|
|
232
|
+
parsed &&
|
|
233
|
+
typeof parsed === "object" &&
|
|
234
|
+
!Array.isArray(parsed)
|
|
235
|
+
) {
|
|
236
|
+
targetExistingData = parsed as Record<string, unknown>;
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// coercion-ok: unreadable prior data is replaced wholesale below,
|
|
240
|
+
// which is the same outcome as the create path.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const [existing] = await tx
|
|
244
|
+
.select({ id: schema.designFiles.id })
|
|
245
|
+
.from(schema.designFiles)
|
|
246
|
+
.where(
|
|
247
|
+
and(
|
|
248
|
+
eq(schema.designFiles.designId, targetDesignId),
|
|
249
|
+
ne(schema.designFiles.filename, BOARD_FILENAME),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
.limit(1);
|
|
253
|
+
if (existing) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"Target design already has files. Templates only fill an empty design.",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
249
258
|
await tx
|
|
250
259
|
.update(schema.designs)
|
|
251
260
|
.set({
|
|
@@ -285,7 +294,13 @@ export default defineAction({
|
|
|
285
294
|
updatedAt: now,
|
|
286
295
|
})),
|
|
287
296
|
);
|
|
288
|
-
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (targetDesignId) {
|
|
300
|
+
await withDesignSourceMutationTransaction(targetDesignId, persist);
|
|
301
|
+
} else {
|
|
302
|
+
await db.transaction(persist);
|
|
303
|
+
}
|
|
289
304
|
|
|
290
305
|
return {
|
|
291
306
|
id: designId,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { createDesignVersionSnapshot } from "../server/lib/design-versions.js";
|
|
5
|
+
|
|
6
|
+
export default defineAction({
|
|
7
|
+
description:
|
|
8
|
+
"Create one frontend editor history checkpoint before a grouped design mutation.",
|
|
9
|
+
agentTool: false,
|
|
10
|
+
mcpTool: false,
|
|
11
|
+
schema: z.object({
|
|
12
|
+
designId: z.string().describe("Design project ID"),
|
|
13
|
+
}),
|
|
14
|
+
run: async ({ designId }) => {
|
|
15
|
+
const version = await createDesignVersionSnapshot(designId, {
|
|
16
|
+
label: "Before editor screen delete",
|
|
17
|
+
chatContext: { surface: "editor", actionName: "delete-file" },
|
|
18
|
+
});
|
|
19
|
+
return version;
|
|
20
|
+
},
|
|
21
|
+
});
|