@agent-native/core 0.192.0-nightly-20260926081002 → 0.192.0
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/analytics/CHANGELOG.md +17 -0
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +27 -3
- package/corpus/templates/analytics/app/components/AnalyticsReviewArtifactPreview.tsx +8 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -10
- package/corpus/templates/analytics/app/root.tsx +2 -4
- package/corpus/templates/analytics/app/routes/_index.tsx +1 -36
- package/corpus/templates/analytics/app/routes/home.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-09-25-human-review-previews-show-same-organization-dashboard-chart.md +6 -0
- package/corpus/templates/analytics/changelog/2026-09-25-human-review-skips-saved-bigquery-queries-in-previews.md +6 -0
- package/corpus/templates/analytics/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +14 -0
- package/corpus/templates/analytics/server/plugins/auth.ts +0 -3
- package/corpus/templates/assets/CHANGELOG.md +16 -0
- package/corpus/templates/assets/app/root.tsx +2 -7
- package/corpus/templates/assets/app/routes/_index.tsx +1 -36
- package/corpus/templates/assets/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/assets/server/plugins/auth.ts +0 -3
- package/corpus/templates/brain/app/root.tsx +2 -13
- package/corpus/templates/brain/app/routes/_index.tsx +1 -35
- package/corpus/templates/brain/app/routes/home.tsx +1 -1
- package/corpus/templates/calendar/CHANGELOG.md +15 -0
- package/corpus/templates/calendar/app/root.tsx +2 -10
- package/corpus/templates/calendar/app/routes/_index.tsx +1 -34
- package/corpus/templates/calendar/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/calendar/server/plugins/auth.ts +0 -3
- package/corpus/templates/chat/AGENTS.md +3 -3
- package/corpus/templates/chat/app/root.tsx +3 -15
- package/corpus/templates/chat/app/routes/_index.tsx +1 -37
- package/corpus/templates/chat/server/plugins/auth.ts +0 -3
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/clips/app/root.tsx +2 -10
- package/corpus/templates/clips/app/routes/_index.tsx +1 -33
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -3
- package/corpus/templates/clips/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/clips/server/plugins/auth.ts +0 -3
- package/corpus/templates/content/app/root.tsx +2 -3
- package/corpus/templates/content/app/routes/_index.tsx +1 -33
- package/corpus/templates/content/server/plugins/auth.ts +0 -3
- package/corpus/templates/crm/app/root.tsx +3 -16
- package/corpus/templates/crm/app/routes/_index.tsx +1 -36
- package/corpus/templates/crm/app/routes/home.tsx +1 -1
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +30 -17
- package/corpus/templates/design/CHANGELOG.md +31 -0
- package/corpus/templates/design/actions/connect-localhost.ts +98 -37
- package/corpus/templates/design/actions/get-design.schema.ts +10 -0
- package/corpus/templates/design/actions/get-design.ts +25 -6
- package/corpus/templates/design/actions/publish-visual-edit-pending.ts +4 -59
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +108 -49
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +63 -21
- package/corpus/templates/design/app/components/design/types.ts +2 -0
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +98 -9
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-insert-timeout.ts +61 -6
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +6 -5
- package/corpus/templates/design/app/pages/design-editor/commands/publish-visual-edit-pending.ts +21 -3
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +4 -7
- package/corpus/templates/design/app/root.tsx +2 -20
- package/corpus/templates/design/app/routes/_index.tsx +1 -34
- package/corpus/templates/design/changelog/2026-09-25-fix-live-visual-edit-recovery-handoff-and-interaction-workfl.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-human-review-previews-show-the-actual-design-when-you-have-o.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-live-screens-recover-safely-from-interrupted-cross-screen-mo.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-canvas-focus-stays-on-toolbar-controls-when-a-live-preview-f.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/design/server/plugins/auth.ts +0 -3
- package/corpus/templates/dispatch/app/root.tsx +2 -11
- package/corpus/templates/dispatch/app/routes/_index.tsx +1 -34
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +0 -3
- package/corpus/templates/factory/app/root.tsx +3 -15
- package/corpus/templates/factory/app/routes/_index.tsx +1 -34
- package/corpus/templates/factory/app/routes/home.tsx +1 -1
- package/corpus/templates/forms/AGENTS.md +1 -1
- package/corpus/templates/forms/app/root.tsx +2 -4
- package/corpus/templates/forms/app/routes/_index.tsx +1 -34
- package/corpus/templates/forms/server/plugins/auth.ts +0 -3
- package/corpus/templates/mail/CHANGELOG.md +8 -0
- package/corpus/templates/mail/app/root.tsx +2 -6
- package/corpus/templates/mail/app/routes/_index.tsx +1 -36
- package/corpus/templates/mail/app/routes/home.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/mail/server/plugins/auth.ts +0 -3
- package/corpus/templates/plan/app/root.tsx +7 -13
- package/corpus/templates/plan/app/routes/_index.tsx +1 -35
- package/corpus/templates/plan/app/routes/home.tsx +1 -1
- package/corpus/templates/plan/server/plugins/auth.ts +0 -3
- package/corpus/templates/slides/CHANGELOG.md +21 -0
- package/corpus/templates/slides/actions/get-deck.ts +49 -12
- package/corpus/templates/slides/app/root.tsx +1 -3
- package/corpus/templates/slides/app/routes/_index.tsx +1 -36
- package/corpus/templates/slides/app/routes/home.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-09-25-human-review-previews-show-same-organization-slides-for-orga.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
- package/corpus/templates/slides/server/plugins/auth.ts +0 -3
- package/corpus/templates/tasks/AGENTS.md +1 -1
- package/corpus/templates/tasks/README.md +1 -1
- package/corpus/templates/tasks/app/root.tsx +2 -11
- package/corpus/templates/tasks/app/routes/_index.tsx +1 -37
- package/corpus/templates/tasks/app/routes/home.tsx +1 -1
- package/dist/cli/design-connect.d.ts +1 -0
- package/dist/cli/design-connect.js +94 -32
- package/dist/client/auth/AuthPage.d.ts +1 -17
- package/dist/client/auth/AuthPage.js +7 -49
- package/dist/client/observability/OutputPreview.js +8 -2
- package/dist/client/sign-in-landing.d.ts +1 -0
- package/dist/client/sign-in-landing.js +8 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/deploy/workspace-deploy.js +32 -8
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +3 -3
- package/dist/server/auth-marketing.d.ts +0 -7
- package/dist/server/auth-marketing.js +0 -36
- package/dist/server/auth.d.ts +1 -10
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.d.ts +1 -12
- package/dist/server/onboarding-html.js +4 -817
- package/dist/shared/ssr-auth-redirect.d.ts +3 -3
- package/dist/shared/ssr-auth-redirect.js +4 -4
- package/dist/templates/chat/AGENTS.md +3 -3
- package/dist/templates/chat/app/root.tsx +3 -15
- package/dist/templates/chat/app/routes/_index.tsx +1 -37
- package/dist/templates/chat/server/plugins/auth.ts +0 -3
- package/dist/templates/default/AGENTS.md +2 -4
- package/dist/templates/default/DEVELOPING.md +3 -5
- package/dist/templates/default/app/root.tsx +2 -9
- package/dist/templates/default/app/routes/_index.tsx +8 -15
- package/package.json +5 -4
- package/src/templates/chat/AGENTS.md +3 -3
- package/src/templates/chat/app/root.tsx +3 -15
- package/src/templates/chat/app/routes/_index.tsx +1 -37
- package/src/templates/chat/server/plugins/auth.ts +0 -3
- package/src/templates/default/AGENTS.md +2 -4
- package/src/templates/default/DEVELOPING.md +3 -5
- package/src/templates/default/app/root.tsx +2 -9
- package/src/templates/default/app/routes/_index.tsx +8 -15
- package/dist/server/auth-marketing-locales.d.ts +0 -9
- package/dist/server/auth-marketing-locales.js +0 -1322
- package/dist/templates/chat/public/auth-marketing/chat.webp +0 -0
- package/src/templates/chat/public/auth-marketing/chat.webp +0 -0
|
@@ -16,4 +16,14 @@ export const getDesignSchema = z.object({
|
|
|
16
16
|
: value === true || value === "true" || value === "1",
|
|
17
17
|
)
|
|
18
18
|
.describe("Set false to return file metadata without HTML contents"),
|
|
19
|
+
reviewPreview: queryBoolean
|
|
20
|
+
.optional()
|
|
21
|
+
.transform((value) =>
|
|
22
|
+
value === undefined
|
|
23
|
+
? undefined
|
|
24
|
+
: value === true || value === "true" || value === "1",
|
|
25
|
+
)
|
|
26
|
+
.describe(
|
|
27
|
+
"Human Review only: read a design in the current organization. Requires an organization owner or admin.",
|
|
28
|
+
),
|
|
19
29
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { defineAction } from "@agent-native/core/action";
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
|
+
import { currentRequestUserIsOrgAdmin } from "@agent-native/core/server";
|
|
3
|
+
import { getRequestOrgId } from "@agent-native/core/server/request-context";
|
|
2
4
|
import { loadAgentDesignSystemContext } from "@agent-native/core/shared";
|
|
3
5
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
4
6
|
import { track } from "@agent-native/core/tracking";
|
|
@@ -45,8 +47,27 @@ export default defineAction({
|
|
|
45
47
|
requiresAuth: false,
|
|
46
48
|
publicAgent: { expose: true, readOnly: true, requiresAuth: false },
|
|
47
49
|
http: { method: "GET" },
|
|
48
|
-
run: async ({ id, fileId, includeFileContent }, ctx) => {
|
|
49
|
-
const
|
|
50
|
+
run: async ({ id, fileId, includeFileContent, reviewPreview }, ctx) => {
|
|
51
|
+
const db = getDb();
|
|
52
|
+
let access;
|
|
53
|
+
if (reviewPreview) {
|
|
54
|
+
const orgId = getRequestOrgId();
|
|
55
|
+
if (!orgId || !(await currentRequestUserIsOrgAdmin(orgId))) {
|
|
56
|
+
fail(
|
|
57
|
+
"Only organization owners and admins can preview reviewed designs.",
|
|
58
|
+
{ statusCode: 403 },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const [resource] = await db
|
|
62
|
+
.select()
|
|
63
|
+
.from(schema.designs)
|
|
64
|
+
.where(and(eq(schema.designs.id, id), eq(schema.designs.orgId, orgId)))
|
|
65
|
+
.limit(1);
|
|
66
|
+
if (!resource) fail("Design not found.", { statusCode: 404 });
|
|
67
|
+
access = { role: "viewer" as const, resource };
|
|
68
|
+
} else {
|
|
69
|
+
access = await resolveAccess("design", id);
|
|
70
|
+
}
|
|
50
71
|
if (!access) {
|
|
51
72
|
const error = new Error("Design not found") as Error & {
|
|
52
73
|
statusCode: number;
|
|
@@ -56,8 +77,6 @@ export default defineAction({
|
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
const row = access.resource;
|
|
59
|
-
const db = getDb();
|
|
60
|
-
|
|
61
80
|
// Fetch associated files in a stable order. This array feeds the overview
|
|
62
81
|
// canvas's screen stack and each screen's index within its layout group, so
|
|
63
82
|
// unordered rows (Postgres returns heap order, which an UPDATE can change)
|
|
@@ -100,7 +119,7 @@ export default defineAction({
|
|
|
100
119
|
getDesignSystem,
|
|
101
120
|
);
|
|
102
121
|
|
|
103
|
-
if (shouldTrackDesignView(ctx?.userEmail, id)) {
|
|
122
|
+
if (!reviewPreview && shouldTrackDesignView(ctx?.userEmail, id)) {
|
|
104
123
|
track(
|
|
105
124
|
"design_viewed",
|
|
106
125
|
{
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
|
-
import {
|
|
3
|
-
import { assertAccess, resolveAccess } from "@agent-native/core/sharing";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
3
|
import { eq, sql } from "drizzle-orm";
|
|
5
4
|
import { z } from "zod";
|
|
6
5
|
|
|
@@ -19,41 +18,6 @@ const pendingSchema = z
|
|
|
19
18
|
})
|
|
20
19
|
.nullable();
|
|
21
20
|
|
|
22
|
-
function isVisualEditBrowserRequest(
|
|
23
|
-
ctx: Parameters<typeof isSameOriginVisualEditBrowserRequest>[0],
|
|
24
|
-
designId: string,
|
|
25
|
-
requireShareLink = false,
|
|
26
|
-
): boolean {
|
|
27
|
-
if (
|
|
28
|
-
ctx?.caller !== "frontend" ||
|
|
29
|
-
!isSameOriginVisualEditBrowserRequest(ctx)
|
|
30
|
-
) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const referer = ctx.requestHeaders?.get("referer");
|
|
35
|
-
const requestOrigin = getRequestContext()?.requestOrigin;
|
|
36
|
-
if (!referer || !requestOrigin || !URL.canParse(referer)) return false;
|
|
37
|
-
|
|
38
|
-
const refererUrl = new URL(referer);
|
|
39
|
-
if (!URL.canParse(requestOrigin)) return false;
|
|
40
|
-
if (refererUrl.origin !== new URL(requestOrigin).origin) return false;
|
|
41
|
-
if (requireShareLink && refererUrl.searchParams.get("share") !== "1") {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const segments = refererUrl.pathname.split("/").filter(Boolean);
|
|
46
|
-
const referredDesignId = segments.pop();
|
|
47
|
-
if (segments.pop() !== "visual-edit" || !referredDesignId) return false;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
return decodeURIComponent(referredDesignId) === designId;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error instanceof URIError) return false;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
21
|
export default defineAction({
|
|
58
22
|
description:
|
|
59
23
|
"Publish the current DOM-only visual-edit handoff for the Design page so an external coding agent can retrieve it. Browser-only transport; this does not write design files or app source.",
|
|
@@ -88,28 +52,9 @@ export default defineAction({
|
|
|
88
52
|
});
|
|
89
53
|
}
|
|
90
54
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const browserCanEditDesign =
|
|
95
|
-
browserAccess &&
|
|
96
|
-
["owner", "admin", "editor"].includes(browserAccess.role);
|
|
97
|
-
const browserCanShareVisualEdit = isVisualEditBrowserRequest(
|
|
98
|
-
ctx,
|
|
99
|
-
designId,
|
|
100
|
-
true,
|
|
101
|
-
);
|
|
102
|
-
if (
|
|
103
|
-
browserAccess &&
|
|
104
|
-
(browserCanEditDesign || browserCanShareVisualEdit)
|
|
105
|
-
) {
|
|
106
|
-
access = browserAccess;
|
|
107
|
-
} else {
|
|
108
|
-
access = await assertAccess("design", designId, "editor");
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
access = await assertAccess("design", designId, "editor");
|
|
112
|
-
}
|
|
55
|
+
// Same-origin metadata is CSRF defense only; the scoped page capability
|
|
56
|
+
// is what grants a signed-out visual-edit session editor access.
|
|
57
|
+
const access = await assertAccess("design", designId, "editor");
|
|
113
58
|
const design = access.resource as typeof schema.designs.$inferSelect;
|
|
114
59
|
const now = new Date().toISOString();
|
|
115
60
|
const canEditDesign = ["owner", "admin", "editor"].includes(access.role);
|
|
@@ -782,6 +782,7 @@ interface DesignCanvasProps {
|
|
|
782
782
|
transactionId?: string;
|
|
783
783
|
routePath?: string;
|
|
784
784
|
reason: string;
|
|
785
|
+
sourcePresent?: boolean;
|
|
785
786
|
}) => void;
|
|
786
787
|
onRuntimeStructureRollbackResult?: (details: {
|
|
787
788
|
requestId: string;
|
|
@@ -1922,9 +1923,10 @@ export function DesignCanvas({
|
|
|
1922
1923
|
awaitingTransaction: boolean;
|
|
1923
1924
|
} | null>(null);
|
|
1924
1925
|
const lastRuntimeStructureDeleteRequestIdRef = useRef<string | null>(null);
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1926
|
+
const lastRuntimeStructureDeleteCancelRequestRef = useRef<{
|
|
1927
|
+
requestId: string;
|
|
1928
|
+
retryCount: number;
|
|
1929
|
+
} | null>(null);
|
|
1928
1930
|
const lastRuntimeStructureTargetReloadTransactionIdRef = useRef<
|
|
1929
1931
|
string | null
|
|
1930
1932
|
>(null);
|
|
@@ -4852,6 +4854,25 @@ export function DesignCanvas({
|
|
|
4852
4854
|
});
|
|
4853
4855
|
return;
|
|
4854
4856
|
}
|
|
4857
|
+
if (e.data.type === "runtime-structure-delete-cancelled") {
|
|
4858
|
+
const requestId = String(e.data.requestId || "");
|
|
4859
|
+
if (!requestId) return;
|
|
4860
|
+
onRuntimeStructureDeleteRejected?.({
|
|
4861
|
+
screenId,
|
|
4862
|
+
requestId,
|
|
4863
|
+
transactionId:
|
|
4864
|
+
typeof e.data.transactionId === "string"
|
|
4865
|
+
? e.data.transactionId
|
|
4866
|
+
: undefined,
|
|
4867
|
+
routePath:
|
|
4868
|
+
typeof e.data.routePath === "string"
|
|
4869
|
+
? e.data.routePath
|
|
4870
|
+
: (liveRoutePathRef.current ?? undefined),
|
|
4871
|
+
reason: "cancelled",
|
|
4872
|
+
sourcePresent: e.data.sourcePresent === true,
|
|
4873
|
+
});
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4855
4876
|
if (e.data.type === "runtime-structure-rollback-result") {
|
|
4856
4877
|
const requestId = String(e.data.requestId || "");
|
|
4857
4878
|
if (!requestId) return;
|
|
@@ -6876,7 +6897,9 @@ export function DesignCanvas({
|
|
|
6876
6897
|
currentPreview.transactionId = request?.transactionId;
|
|
6877
6898
|
currentPreview.awaitingTransaction = false;
|
|
6878
6899
|
}
|
|
6879
|
-
|
|
6900
|
+
// Keep the source visible until the destination has acknowledged its
|
|
6901
|
+
// insert. A refused or disconnected target must leave the move untouched.
|
|
6902
|
+
if (!request || request.waitForInsertTransaction) return;
|
|
6880
6903
|
if (readyIframeDocumentIdentity !== iframeDocumentIdentity) {
|
|
6881
6904
|
if (currentPreview?.requestId === request.requestId) {
|
|
6882
6905
|
currentPreview.documentIdentity = null;
|
|
@@ -6915,19 +6938,26 @@ export function DesignCanvas({
|
|
|
6915
6938
|
useEffect(() => {
|
|
6916
6939
|
const request = runtimeStructureDeleteRequest;
|
|
6917
6940
|
if (!request?.cancelRequested) {
|
|
6918
|
-
|
|
6941
|
+
lastRuntimeStructureDeleteCancelRequestRef.current = null;
|
|
6919
6942
|
return;
|
|
6920
6943
|
}
|
|
6921
6944
|
if (readyIframeDocumentIdentity !== iframeDocumentIdentity) {
|
|
6922
|
-
|
|
6945
|
+
lastRuntimeStructureDeleteCancelRequestRef.current = null;
|
|
6923
6946
|
return;
|
|
6924
6947
|
}
|
|
6948
|
+
const retryCount = request.cancellationRetryCount ?? 0;
|
|
6925
6949
|
if (
|
|
6926
|
-
|
|
6950
|
+
lastRuntimeStructureDeleteCancelRequestRef.current?.requestId ===
|
|
6951
|
+
request.requestId &&
|
|
6952
|
+
lastRuntimeStructureDeleteCancelRequestRef.current.retryCount ===
|
|
6953
|
+
retryCount
|
|
6927
6954
|
) {
|
|
6928
6955
|
return;
|
|
6929
6956
|
}
|
|
6930
|
-
|
|
6957
|
+
lastRuntimeStructureDeleteCancelRequestRef.current = {
|
|
6958
|
+
requestId: request.requestId,
|
|
6959
|
+
retryCount,
|
|
6960
|
+
};
|
|
6931
6961
|
postOneShotBridgeMessage({
|
|
6932
6962
|
type: "cancel-pending-delete-element",
|
|
6933
6963
|
selector: request.selector,
|
|
@@ -6939,12 +6969,11 @@ export function DesignCanvas({
|
|
|
6939
6969
|
type: "visual-structure-ack",
|
|
6940
6970
|
requestId: request.requestId,
|
|
6941
6971
|
applied: false,
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
reason: "cancelled",
|
|
6972
|
+
cancelRuntimeStructureDelete: {
|
|
6973
|
+
transactionId: request.transactionId,
|
|
6974
|
+
selector: request.selector,
|
|
6975
|
+
selectorCandidates: request.selectorCandidates ?? [],
|
|
6976
|
+
},
|
|
6948
6977
|
});
|
|
6949
6978
|
}, [
|
|
6950
6979
|
iframeDocumentIdentity,
|
|
@@ -7663,38 +7692,67 @@ export function DesignCanvas({
|
|
|
7663
7692
|
: deviceFrame === "none"
|
|
7664
7693
|
? "100%"
|
|
7665
7694
|
: (iframeHeight ?? undefined);
|
|
7666
|
-
const focusScrollSurface = useCallback(
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7695
|
+
const focusScrollSurface = useCallback(
|
|
7696
|
+
(fromIframeLoad = false) => {
|
|
7697
|
+
const surface = scrollContainerRef.current;
|
|
7698
|
+
if (
|
|
7699
|
+
!surface ||
|
|
7700
|
+
document.activeElement === surface ||
|
|
7701
|
+
!editMode ||
|
|
7702
|
+
interactMode
|
|
7703
|
+
)
|
|
7704
|
+
return;
|
|
7705
|
+
// A picker drag ending over the canvas must not take focus from the open
|
|
7706
|
+
// picker: losing it ends the inspector gesture and drops a styled text range.
|
|
7707
|
+
if (
|
|
7708
|
+
textEditingStateRef.current.active ||
|
|
7709
|
+
document.activeElement?.closest("[data-radix-popper-content-wrapper]")
|
|
7710
|
+
) {
|
|
7711
|
+
return;
|
|
7712
|
+
}
|
|
7713
|
+
const focusedElement = document.activeElement;
|
|
7714
|
+
if (
|
|
7715
|
+
fromIframeLoad &&
|
|
7716
|
+
focusedElement !== document.body &&
|
|
7717
|
+
focusedElement !== iframeRef.current
|
|
7718
|
+
) {
|
|
7719
|
+
return;
|
|
7720
|
+
}
|
|
7721
|
+
if (focusedElement instanceof HTMLIFrameElement) {
|
|
7722
|
+
try {
|
|
7723
|
+
const frameDocument = focusedElement.contentDocument;
|
|
7724
|
+
if (!frameDocument) {
|
|
7725
|
+
if (!fromIframeLoad) return;
|
|
7726
|
+
} else if (
|
|
7727
|
+
frameDocument.activeElement?.closest(EDITABLE_FOCUS_SELECTOR)
|
|
7728
|
+
) {
|
|
7729
|
+
return;
|
|
7730
|
+
}
|
|
7731
|
+
} catch (error) {
|
|
7732
|
+
if (
|
|
7733
|
+
!(error instanceof DOMException) ||
|
|
7734
|
+
error.name !== "SecurityError"
|
|
7735
|
+
) {
|
|
7736
|
+
throw error;
|
|
7737
|
+
}
|
|
7738
|
+
// A cross-origin frame may have an app-owned focused input; preserve
|
|
7739
|
+
// its focus when the parent cannot inspect it.
|
|
7685
7740
|
return;
|
|
7686
7741
|
}
|
|
7687
|
-
} catch {
|
|
7688
|
-
// Keep focus inside a frame we cannot inspect; it may own an editor.
|
|
7689
|
-
return;
|
|
7690
7742
|
}
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7743
|
+
// Taking focus for keyboard panning must never outrank a field the user
|
|
7744
|
+
// was just handed: a composer that opens under the cursor would otherwise
|
|
7745
|
+
// be focused on mount and silently unfocused by the same pointer motion.
|
|
7746
|
+
if (focusedElement?.closest(EDITABLE_FOCUS_SELECTOR)) return;
|
|
7747
|
+
surface.focus({ preventScroll: true });
|
|
7748
|
+
},
|
|
7749
|
+
[editMode, interactMode],
|
|
7750
|
+
);
|
|
7751
|
+
const handleCanvasPointerEnter = useCallback(
|
|
7752
|
+
() => focusScrollSurface(),
|
|
7753
|
+
[focusScrollSurface],
|
|
7754
|
+
);
|
|
7755
|
+
useLayoutEffect(() => focusScrollSurface(), [focusScrollSurface]);
|
|
7698
7756
|
|
|
7699
7757
|
// Single-screen pan (Figma parity §3): middle-mouse-button drag always
|
|
7700
7758
|
// pans (mirrors MultiScreenCanvas's unconditional `e.button === 1` branch
|
|
@@ -7963,6 +8021,7 @@ export function DesignCanvas({
|
|
|
7963
8021
|
onLoad={(event) => {
|
|
7964
8022
|
if (!liveEditFrameRequiresBridge) markPreviewFrameReady();
|
|
7965
8023
|
sendBridgeToContainer();
|
|
8024
|
+
focusScrollSurface(true);
|
|
7966
8025
|
event.currentTarget.contentWindow?.postMessage(
|
|
7967
8026
|
{ type: "agent-native:editor-chrome-ready-probe" },
|
|
7968
8027
|
"*",
|
|
@@ -8414,8 +8473,8 @@ export function DesignCanvas({
|
|
|
8414
8473
|
<div
|
|
8415
8474
|
ref={scrollContainerRef}
|
|
8416
8475
|
tabIndex={-1}
|
|
8417
|
-
onPointerEnter={
|
|
8418
|
-
onMouseEnter={
|
|
8476
|
+
onPointerEnter={handleCanvasPointerEnter}
|
|
8477
|
+
onMouseEnter={handleCanvasPointerEnter}
|
|
8419
8478
|
className="relative h-full w-full overflow-clip"
|
|
8420
8479
|
>
|
|
8421
8480
|
{iframeElement}
|
|
@@ -8432,8 +8491,8 @@ export function DesignCanvas({
|
|
|
8432
8491
|
<div
|
|
8433
8492
|
ref={scrollContainerRef}
|
|
8434
8493
|
tabIndex={-1}
|
|
8435
|
-
onPointerEnter={
|
|
8436
|
-
onMouseEnter={
|
|
8494
|
+
onPointerEnter={handleCanvasPointerEnter}
|
|
8495
|
+
onMouseEnter={handleCanvasPointerEnter}
|
|
8437
8496
|
className="relative h-full w-full overflow-clip"
|
|
8438
8497
|
style={{
|
|
8439
8498
|
width: embeddedFrame.displayWidth,
|
|
@@ -8466,8 +8525,8 @@ export function DesignCanvas({
|
|
|
8466
8525
|
<div
|
|
8467
8526
|
ref={scrollContainerRef}
|
|
8468
8527
|
tabIndex={-1}
|
|
8469
|
-
onPointerEnter={
|
|
8470
|
-
onMouseEnter={
|
|
8528
|
+
onPointerEnter={handleCanvasPointerEnter}
|
|
8529
|
+
onMouseEnter={handleCanvasPointerEnter}
|
|
8471
8530
|
onMouseDown={handleScrollSurfaceMouseDown}
|
|
8472
8531
|
onClick={handleScrollSurfaceBackgroundClick}
|
|
8473
8532
|
className="relative flex-1 h-full overflow-auto"
|
|
@@ -97,6 +97,7 @@ import {
|
|
|
97
97
|
useLayoutEffect,
|
|
98
98
|
useMemo,
|
|
99
99
|
type CSSProperties,
|
|
100
|
+
type FocusEvent as ReactFocusEvent,
|
|
100
101
|
type MutableRefObject,
|
|
101
102
|
type PointerEvent as ReactPointerEvent,
|
|
102
103
|
type ReactNode,
|
|
@@ -145,10 +146,7 @@ import {
|
|
|
145
146
|
parseGradientCss,
|
|
146
147
|
type GradientStopValue,
|
|
147
148
|
} from "./inspector/GradientEditor";
|
|
148
|
-
import {
|
|
149
|
-
sendLinkedScreenPreviewCancelPendingDelete,
|
|
150
|
-
sendLinkedScreenPreviewPendingDelete,
|
|
151
|
-
} from "./multi-screen/linked-screen-preview";
|
|
149
|
+
import { sendLinkedScreenPreviewCancelPendingDelete } from "./multi-screen/linked-screen-preview";
|
|
152
150
|
import type {
|
|
153
151
|
AltHoverMeasurement,
|
|
154
152
|
AltHoverMeasurementLine,
|
|
@@ -739,6 +737,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
739
737
|
const { resolvedTheme } = useTheme();
|
|
740
738
|
const t = useT();
|
|
741
739
|
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
740
|
+
const initialCanvasFocusPendingRef = useRef(true);
|
|
741
|
+
const initialCanvasFocusAttemptedRef = useRef(false);
|
|
742
742
|
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
743
743
|
const panRef = useRef(pan);
|
|
744
744
|
const [canvasZoom, setCanvasZoom] = useState(zoom);
|
|
@@ -1783,6 +1783,62 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1783
1783
|
surface.focus({ preventScroll: true });
|
|
1784
1784
|
}, []);
|
|
1785
1785
|
|
|
1786
|
+
useEffect(() => {
|
|
1787
|
+
const markCanvasUsed = () => {
|
|
1788
|
+
initialCanvasFocusPendingRef.current = false;
|
|
1789
|
+
};
|
|
1790
|
+
document.addEventListener("pointerdown", markCanvasUsed, true);
|
|
1791
|
+
document.addEventListener("keydown", markCanvasUsed, true);
|
|
1792
|
+
return () => {
|
|
1793
|
+
document.removeEventListener("pointerdown", markCanvasUsed, true);
|
|
1794
|
+
document.removeEventListener("keydown", markCanvasUsed, true);
|
|
1795
|
+
};
|
|
1796
|
+
}, []);
|
|
1797
|
+
|
|
1798
|
+
useLayoutEffect(() => {
|
|
1799
|
+
if (
|
|
1800
|
+
initialCanvasFocusAttemptedRef.current ||
|
|
1801
|
+
!editableScreenIds?.size ||
|
|
1802
|
+
interactMode ||
|
|
1803
|
+
interactScreenId
|
|
1804
|
+
) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
initialCanvasFocusAttemptedRef.current = true;
|
|
1808
|
+
if (!initialCanvasFocusPendingRef.current) return;
|
|
1809
|
+
const surface = surfaceRef.current;
|
|
1810
|
+
const active = document.activeElement;
|
|
1811
|
+
if (
|
|
1812
|
+
!surface ||
|
|
1813
|
+
(active !== document.body && isEditableHotkeyTarget(active))
|
|
1814
|
+
) {
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
surface.focus({ preventScroll: true });
|
|
1818
|
+
}, [editableScreenIds?.size, interactMode, interactScreenId]);
|
|
1819
|
+
|
|
1820
|
+
const restoreInitialCanvasFocus = useCallback(
|
|
1821
|
+
(event: ReactFocusEvent<HTMLDivElement>) => {
|
|
1822
|
+
if (
|
|
1823
|
+
!initialCanvasFocusPendingRef.current ||
|
|
1824
|
+
interactMode ||
|
|
1825
|
+
interactScreenId
|
|
1826
|
+
) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const target = event.target;
|
|
1830
|
+
if (
|
|
1831
|
+
!(target instanceof HTMLIFrameElement) ||
|
|
1832
|
+
!surfaceRef.current?.contains(target) ||
|
|
1833
|
+
target.closest('[data-screen-interact-mode="true"]')
|
|
1834
|
+
) {
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
surfaceRef.current.focus({ preventScroll: true });
|
|
1838
|
+
},
|
|
1839
|
+
[interactMode, interactScreenId],
|
|
1840
|
+
);
|
|
1841
|
+
|
|
1786
1842
|
// Per-screen memoization of resolveScreenMetadata (PF20). resolveScreenMetadata
|
|
1787
1843
|
// string-scans up to 4000 chars of content (deriveSource/derivePreviewState)
|
|
1788
1844
|
// plus URL parsing on every call. getResolvedMetadata is invoked once per
|
|
@@ -3603,23 +3659,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
3603
3659
|
clearCrossScreenDrag();
|
|
3604
3660
|
return;
|
|
3605
3661
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
Boolean(payload.sourceDeleteRequestId) &&
|
|
3609
|
-
editableScreenIds?.has(sourceScreenId) === true &&
|
|
3610
|
-
(targetCandidate.id === boardFileId ||
|
|
3611
|
-
editableScreenIds?.has(targetCandidate.id) === true);
|
|
3612
|
-
if (payload.sourceDeleteRequestId) {
|
|
3613
|
-
if (shouldPreviewSourceDelete) {
|
|
3614
|
-
sendLinkedScreenPreviewPendingDelete(sourceScreenId, {
|
|
3615
|
-
selector: payload.selector,
|
|
3616
|
-
selectorCandidates: sourceDeleteCandidates,
|
|
3617
|
-
requestId: payload.sourceDeleteRequestId,
|
|
3618
|
-
});
|
|
3619
|
-
} else {
|
|
3620
|
-
cancelPendingSourceDelete();
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3662
|
+
// The source stays visible until the destination insert is acknowledged.
|
|
3663
|
+
// DesignCanvas starts its delete preview only after that acknowledgement.
|
|
3623
3664
|
crossScreenHostCommittedRef.current = true;
|
|
3624
3665
|
if (targetCandidate.id === boardFileId) {
|
|
3625
3666
|
// A successful prior handoff retains its identity until the paired
|
|
@@ -11617,6 +11658,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
11617
11658
|
tabIndex={-1}
|
|
11618
11659
|
className="relative h-full w-full select-none overflow-clip outline-none"
|
|
11619
11660
|
onMouseDownCapture={handleMouseDown}
|
|
11661
|
+
onFocusCapture={restoreInitialCanvasFocus}
|
|
11620
11662
|
onMouseMove={handleMouseMove}
|
|
11621
11663
|
onMouseLeave={() => setAltHoverMeasurement(null)}
|
|
11622
11664
|
onDragEnter={handleCanvasDragEnter}
|
|
@@ -112,6 +112,7 @@ export interface RuntimeStructureDeleteRequest {
|
|
|
112
112
|
rollbackSourceId?: string;
|
|
113
113
|
/** Cancel a source delete and restore its DOM before rolling back the insert. */
|
|
114
114
|
cancelRequested?: boolean;
|
|
115
|
+
cancellationRetryCount?: number;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
export interface RuntimeStructureRollbackRequest {
|
|
@@ -121,6 +122,7 @@ export interface RuntimeStructureRollbackRequest {
|
|
|
121
122
|
sourceId?: string;
|
|
122
123
|
/** A target lost across canvas unmount is already rolled back if absent. */
|
|
123
124
|
idempotent?: boolean;
|
|
125
|
+
retryCount?: number;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
export interface RuntimeLayerRenameRequest {
|
package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts
CHANGED
|
@@ -15,12 +15,12 @@ import { dndHostLog } from "@/components/design/dnd-debug";
|
|
|
15
15
|
import { isShaderWriteInFlight } from "@/components/design/inspector/GlslShaderPanel";
|
|
16
16
|
import { validateCrossScreenSourceHtmlSnapshot } from "@/components/design/multi-screen/cross-screen-drop";
|
|
17
17
|
import { getPrimaryIframeId } from "@/components/design/multi-screen/iframe-targeting";
|
|
18
|
-
import { sendLinkedScreenPreviewPendingDelete } from "@/components/design/multi-screen/linked-screen-preview";
|
|
19
18
|
import type {
|
|
20
19
|
ElementInfo,
|
|
21
20
|
PortableStyleSnapshot,
|
|
22
21
|
RuntimeStructureDeleteRequest,
|
|
23
22
|
RuntimeStructureInsertRequest,
|
|
23
|
+
RuntimeStructureRollbackRequest,
|
|
24
24
|
} from "@/components/design/types";
|
|
25
25
|
import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
|
|
26
26
|
import {
|
|
@@ -98,6 +98,103 @@ export function absolutePlacePointForDrop(args: {
|
|
|
98
98
|
return absoluteDropPoint(args.targetLocalPoint, args.targetAnchorRect);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export function releaseCrossScreenDropAdmission(
|
|
102
|
+
pendingTransactionRef: RefObject<string | null> | undefined,
|
|
103
|
+
transactionId: string | undefined,
|
|
104
|
+
): boolean {
|
|
105
|
+
if (
|
|
106
|
+
!transactionId ||
|
|
107
|
+
!pendingTransactionRef ||
|
|
108
|
+
pendingTransactionRef.current !== transactionId
|
|
109
|
+
) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
pendingTransactionRef.current = null;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveCrossScreenMoveFailureRecovery(args: {
|
|
117
|
+
reason: string;
|
|
118
|
+
transactionId: string;
|
|
119
|
+
insertRequest: (RuntimeStructureInsertRequest & { screenId: string }) | null;
|
|
120
|
+
sourceDeleteRequest:
|
|
121
|
+
| (RuntimeStructureDeleteRequest & { screenId: string })
|
|
122
|
+
| null;
|
|
123
|
+
rollbackRequestId: string;
|
|
124
|
+
pendingTransactionRef?: RefObject<string | null>;
|
|
125
|
+
}): {
|
|
126
|
+
rollbackRequest:
|
|
127
|
+
| (RuntimeStructureRollbackRequest & { screenId: string })
|
|
128
|
+
| null;
|
|
129
|
+
sourceDeleteRequest:
|
|
130
|
+
| (RuntimeStructureDeleteRequest & { screenId: string })
|
|
131
|
+
| null
|
|
132
|
+
| undefined;
|
|
133
|
+
} {
|
|
134
|
+
const insertRequest =
|
|
135
|
+
args.insertRequest?.transactionId === args.transactionId
|
|
136
|
+
? args.insertRequest
|
|
137
|
+
: null;
|
|
138
|
+
const sourceDeleteRequest =
|
|
139
|
+
args.sourceDeleteRequest?.transactionId === args.transactionId
|
|
140
|
+
? args.sourceDeleteRequest
|
|
141
|
+
: null;
|
|
142
|
+
const destinationDocumentLost =
|
|
143
|
+
args.reason === "target-canvas-unmounted" ||
|
|
144
|
+
args.reason === "target-document-replaced";
|
|
145
|
+
// A lost document discarded its transient insert; only an acknowledged
|
|
146
|
+
// insert needs the source's pending delete canceled before admission opens.
|
|
147
|
+
const needsRollback =
|
|
148
|
+
!destinationDocumentLost &&
|
|
149
|
+
(args.reason === "board-drop-timeout" ||
|
|
150
|
+
args.reason === "cross-screen-insert-timeout" ||
|
|
151
|
+
Boolean(sourceDeleteRequest?.rollbackSelector));
|
|
152
|
+
const rollbackScreenId =
|
|
153
|
+
sourceDeleteRequest?.rollbackScreenId ?? insertRequest?.screenId;
|
|
154
|
+
const sourceDeleteMayHaveApplied = Boolean(
|
|
155
|
+
sourceDeleteRequest &&
|
|
156
|
+
(sourceDeleteRequest.cancelRequested ||
|
|
157
|
+
sourceDeleteRequest.waitForInsertTransaction !== true ||
|
|
158
|
+
sourceDeleteRequest.rollbackSelector),
|
|
159
|
+
);
|
|
160
|
+
const sourceRestorationMustPrecedeRollback =
|
|
161
|
+
needsRollback && sourceDeleteMayHaveApplied;
|
|
162
|
+
const rollbackRequest =
|
|
163
|
+
needsRollback && !sourceRestorationMustPrecedeRollback && rollbackScreenId
|
|
164
|
+
? {
|
|
165
|
+
screenId: rollbackScreenId,
|
|
166
|
+
requestId: args.rollbackRequestId,
|
|
167
|
+
transactionId: args.transactionId,
|
|
168
|
+
selector: sourceDeleteRequest?.rollbackSelector ?? "",
|
|
169
|
+
sourceId: sourceDeleteRequest?.rollbackSourceId,
|
|
170
|
+
idempotent: true,
|
|
171
|
+
}
|
|
172
|
+
: null;
|
|
173
|
+
const recoveredSourceDeleteRequest = sourceDeleteRequest
|
|
174
|
+
? sourceDeleteMayHaveApplied
|
|
175
|
+
? {
|
|
176
|
+
...sourceDeleteRequest,
|
|
177
|
+
cancelRequested: true,
|
|
178
|
+
...(sourceRestorationMustPrecedeRollback
|
|
179
|
+
? {}
|
|
180
|
+
: { rollbackSelector: undefined, rollbackSourceId: undefined }),
|
|
181
|
+
}
|
|
182
|
+
: null
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
if (!rollbackRequest && !recoveredSourceDeleteRequest?.cancelRequested) {
|
|
186
|
+
releaseCrossScreenDropAdmission(
|
|
187
|
+
args.pendingTransactionRef,
|
|
188
|
+
args.transactionId,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
rollbackRequest,
|
|
194
|
+
sourceDeleteRequest: recoveredSourceDeleteRequest,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
101
198
|
export interface CrossScreenElementDropArgs {
|
|
102
199
|
getCurrentFileSnapshot?: (fileId: string) => {
|
|
103
200
|
content: string;
|
|
@@ -510,14 +607,6 @@ export function runCrossScreenElementDrop(
|
|
|
510
607
|
const deleteSelectorCandidates = Array.from(
|
|
511
608
|
new Set([sourceSelector, ...codeLayerSelectorAliases(sourceOwner.node)]),
|
|
512
609
|
).filter(Boolean);
|
|
513
|
-
// Conceal the source in the current DOM before the destination insert is
|
|
514
|
-
// posted. The React prop path below replays it after source-frame reloads.
|
|
515
|
-
sendLinkedScreenPreviewPendingDelete(sourceScreenId, {
|
|
516
|
-
selector: sourceSelector,
|
|
517
|
-
selectorCandidates: deleteSelectorCandidates,
|
|
518
|
-
requestId: deleteRequestId,
|
|
519
|
-
transactionId,
|
|
520
|
-
});
|
|
521
610
|
runtimeStructureInsertRevisionRef.current += 1;
|
|
522
611
|
setRuntimeStructureInsertRequest({
|
|
523
612
|
requestId: runtimeStructureInsertRevisionRef.current,
|