@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.
Files changed (139) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +17 -0
  3. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +27 -3
  4. package/corpus/templates/analytics/app/components/AnalyticsReviewArtifactPreview.tsx +8 -2
  5. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -10
  6. package/corpus/templates/analytics/app/root.tsx +2 -4
  7. package/corpus/templates/analytics/app/routes/_index.tsx +1 -36
  8. package/corpus/templates/analytics/app/routes/home.tsx +1 -1
  9. package/corpus/templates/analytics/changelog/2026-09-25-human-review-previews-show-same-organization-dashboard-chart.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-09-25-human-review-skips-saved-bigquery-queries-in-previews.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  12. package/corpus/templates/analytics/server/lib/dashboards-store.ts +14 -0
  13. package/corpus/templates/analytics/server/plugins/auth.ts +0 -3
  14. package/corpus/templates/assets/CHANGELOG.md +16 -0
  15. package/corpus/templates/assets/app/root.tsx +2 -7
  16. package/corpus/templates/assets/app/routes/_index.tsx +1 -36
  17. package/corpus/templates/assets/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  18. package/corpus/templates/assets/server/plugins/auth.ts +0 -3
  19. package/corpus/templates/brain/app/root.tsx +2 -13
  20. package/corpus/templates/brain/app/routes/_index.tsx +1 -35
  21. package/corpus/templates/brain/app/routes/home.tsx +1 -1
  22. package/corpus/templates/calendar/CHANGELOG.md +15 -0
  23. package/corpus/templates/calendar/app/root.tsx +2 -10
  24. package/corpus/templates/calendar/app/routes/_index.tsx +1 -34
  25. package/corpus/templates/calendar/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  26. package/corpus/templates/calendar/server/plugins/auth.ts +0 -3
  27. package/corpus/templates/chat/AGENTS.md +3 -3
  28. package/corpus/templates/chat/app/root.tsx +3 -15
  29. package/corpus/templates/chat/app/routes/_index.tsx +1 -37
  30. package/corpus/templates/chat/server/plugins/auth.ts +0 -3
  31. package/corpus/templates/clips/CHANGELOG.md +10 -0
  32. package/corpus/templates/clips/app/root.tsx +2 -10
  33. package/corpus/templates/clips/app/routes/_index.tsx +1 -33
  34. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -3
  35. package/corpus/templates/clips/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  36. package/corpus/templates/clips/server/plugins/auth.ts +0 -3
  37. package/corpus/templates/content/app/root.tsx +2 -3
  38. package/corpus/templates/content/app/routes/_index.tsx +1 -33
  39. package/corpus/templates/content/server/plugins/auth.ts +0 -3
  40. package/corpus/templates/crm/app/root.tsx +3 -16
  41. package/corpus/templates/crm/app/routes/_index.tsx +1 -36
  42. package/corpus/templates/crm/app/routes/home.tsx +1 -1
  43. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +30 -17
  44. package/corpus/templates/design/CHANGELOG.md +31 -0
  45. package/corpus/templates/design/actions/connect-localhost.ts +98 -37
  46. package/corpus/templates/design/actions/get-design.schema.ts +10 -0
  47. package/corpus/templates/design/actions/get-design.ts +25 -6
  48. package/corpus/templates/design/actions/publish-visual-edit-pending.ts +4 -59
  49. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +108 -49
  50. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +63 -21
  51. package/corpus/templates/design/app/components/design/types.ts +2 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +98 -9
  53. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-insert-timeout.ts +61 -6
  54. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +6 -5
  55. package/corpus/templates/design/app/pages/design-editor/commands/publish-visual-edit-pending.ts +21 -3
  56. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +4 -7
  57. package/corpus/templates/design/app/root.tsx +2 -20
  58. package/corpus/templates/design/app/routes/_index.tsx +1 -34
  59. package/corpus/templates/design/changelog/2026-09-25-fix-live-visual-edit-recovery-handoff-and-interaction-workfl.md +6 -0
  60. package/corpus/templates/design/changelog/2026-09-25-human-review-previews-show-the-actual-design-when-you-have-o.md +6 -0
  61. package/corpus/templates/design/changelog/2026-09-25-live-screens-recover-safely-from-interrupted-cross-screen-mo.md +6 -0
  62. package/corpus/templates/design/changelog/2026-09-26-canvas-focus-stays-on-toolbar-controls-when-a-live-preview-f.md +6 -0
  63. package/corpus/templates/design/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  64. package/corpus/templates/design/server/plugins/auth.ts +0 -3
  65. package/corpus/templates/dispatch/app/root.tsx +2 -11
  66. package/corpus/templates/dispatch/app/routes/_index.tsx +1 -34
  67. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +0 -3
  68. package/corpus/templates/factory/app/root.tsx +3 -15
  69. package/corpus/templates/factory/app/routes/_index.tsx +1 -34
  70. package/corpus/templates/factory/app/routes/home.tsx +1 -1
  71. package/corpus/templates/forms/AGENTS.md +1 -1
  72. package/corpus/templates/forms/app/root.tsx +2 -4
  73. package/corpus/templates/forms/app/routes/_index.tsx +1 -34
  74. package/corpus/templates/forms/server/plugins/auth.ts +0 -3
  75. package/corpus/templates/mail/CHANGELOG.md +8 -0
  76. package/corpus/templates/mail/app/root.tsx +2 -6
  77. package/corpus/templates/mail/app/routes/_index.tsx +1 -36
  78. package/corpus/templates/mail/app/routes/home.tsx +1 -1
  79. package/corpus/templates/mail/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  80. package/corpus/templates/mail/server/plugins/auth.ts +0 -3
  81. package/corpus/templates/plan/app/root.tsx +7 -13
  82. package/corpus/templates/plan/app/routes/_index.tsx +1 -35
  83. package/corpus/templates/plan/app/routes/home.tsx +1 -1
  84. package/corpus/templates/plan/server/plugins/auth.ts +0 -3
  85. package/corpus/templates/slides/CHANGELOG.md +21 -0
  86. package/corpus/templates/slides/actions/get-deck.ts +49 -12
  87. package/corpus/templates/slides/app/root.tsx +1 -3
  88. package/corpus/templates/slides/app/routes/_index.tsx +1 -36
  89. package/corpus/templates/slides/app/routes/home.tsx +1 -1
  90. package/corpus/templates/slides/changelog/2026-09-25-human-review-previews-show-same-organization-slides-for-orga.md +6 -0
  91. package/corpus/templates/slides/changelog/2026-09-26-the-app-entry-now-opens-the-shared-sign-in-and-sign-up-scree.md +6 -0
  92. package/corpus/templates/slides/server/plugins/auth.ts +0 -3
  93. package/corpus/templates/tasks/AGENTS.md +1 -1
  94. package/corpus/templates/tasks/README.md +1 -1
  95. package/corpus/templates/tasks/app/root.tsx +2 -11
  96. package/corpus/templates/tasks/app/routes/_index.tsx +1 -37
  97. package/corpus/templates/tasks/app/routes/home.tsx +1 -1
  98. package/dist/cli/design-connect.d.ts +1 -0
  99. package/dist/cli/design-connect.js +94 -32
  100. package/dist/client/auth/AuthPage.d.ts +1 -17
  101. package/dist/client/auth/AuthPage.js +7 -49
  102. package/dist/client/observability/OutputPreview.js +8 -2
  103. package/dist/client/sign-in-landing.d.ts +1 -0
  104. package/dist/client/sign-in-landing.js +8 -0
  105. package/dist/collab/awareness.d.ts +2 -2
  106. package/dist/deploy/workspace-deploy.js +32 -8
  107. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  108. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  109. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  110. package/dist/server/auth-marketing.d.ts +0 -7
  111. package/dist/server/auth-marketing.js +0 -36
  112. package/dist/server/auth.d.ts +1 -10
  113. package/dist/server/index.d.ts +1 -1
  114. package/dist/server/index.js +1 -1
  115. package/dist/server/onboarding-html.d.ts +1 -12
  116. package/dist/server/onboarding-html.js +4 -817
  117. package/dist/shared/ssr-auth-redirect.d.ts +3 -3
  118. package/dist/shared/ssr-auth-redirect.js +4 -4
  119. package/dist/templates/chat/AGENTS.md +3 -3
  120. package/dist/templates/chat/app/root.tsx +3 -15
  121. package/dist/templates/chat/app/routes/_index.tsx +1 -37
  122. package/dist/templates/chat/server/plugins/auth.ts +0 -3
  123. package/dist/templates/default/AGENTS.md +2 -4
  124. package/dist/templates/default/DEVELOPING.md +3 -5
  125. package/dist/templates/default/app/root.tsx +2 -9
  126. package/dist/templates/default/app/routes/_index.tsx +8 -15
  127. package/package.json +5 -4
  128. package/src/templates/chat/AGENTS.md +3 -3
  129. package/src/templates/chat/app/root.tsx +3 -15
  130. package/src/templates/chat/app/routes/_index.tsx +1 -37
  131. package/src/templates/chat/server/plugins/auth.ts +0 -3
  132. package/src/templates/default/AGENTS.md +2 -4
  133. package/src/templates/default/DEVELOPING.md +3 -5
  134. package/src/templates/default/app/root.tsx +2 -9
  135. package/src/templates/default/app/routes/_index.tsx +8 -15
  136. package/dist/server/auth-marketing-locales.d.ts +0 -9
  137. package/dist/server/auth-marketing-locales.js +0 -1322
  138. package/dist/templates/chat/public/auth-marketing/chat.webp +0 -0
  139. 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 access = await resolveAccess("design", id);
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 { getRequestContext } from "@agent-native/core/server/request-context";
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
- let access: Awaited<ReturnType<typeof assertAccess>>;
92
- if (isVisualEditBrowserRequest(ctx, designId)) {
93
- const browserAccess = await resolveAccess("design", designId);
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 lastRuntimeStructureDeleteCancelRequestIdRef = useRef<string | null>(
1926
- null,
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
- if (!request?.waitForInsertTransaction) return;
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
- lastRuntimeStructureDeleteCancelRequestIdRef.current = null;
6941
+ lastRuntimeStructureDeleteCancelRequestRef.current = null;
6919
6942
  return;
6920
6943
  }
6921
6944
  if (readyIframeDocumentIdentity !== iframeDocumentIdentity) {
6922
- lastRuntimeStructureDeleteCancelRequestIdRef.current = null;
6945
+ lastRuntimeStructureDeleteCancelRequestRef.current = null;
6923
6946
  return;
6924
6947
  }
6948
+ const retryCount = request.cancellationRetryCount ?? 0;
6925
6949
  if (
6926
- lastRuntimeStructureDeleteCancelRequestIdRef.current === request.requestId
6950
+ lastRuntimeStructureDeleteCancelRequestRef.current?.requestId ===
6951
+ request.requestId &&
6952
+ lastRuntimeStructureDeleteCancelRequestRef.current.retryCount ===
6953
+ retryCount
6927
6954
  ) {
6928
6955
  return;
6929
6956
  }
6930
- lastRuntimeStructureDeleteCancelRequestIdRef.current = request.requestId;
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
- onRuntimeStructureDeleteRejected?.({
6944
- screenId,
6945
- requestId: request.requestId,
6946
- transactionId: request.transactionId,
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
- const surface = scrollContainerRef.current;
7668
- if (!surface || document.activeElement === surface) return;
7669
- // A picker drag ending over the canvas must not take focus from the open
7670
- // picker: losing it ends the inspector gesture and drops a styled text range.
7671
- if (
7672
- textEditingStateRef.current.active ||
7673
- document.activeElement?.closest("[data-radix-popper-content-wrapper]")
7674
- ) {
7675
- return;
7676
- }
7677
- const focusedElement = document.activeElement;
7678
- if (focusedElement instanceof HTMLIFrameElement) {
7679
- try {
7680
- const frameDocument = focusedElement.contentDocument;
7681
- if (
7682
- !frameDocument ||
7683
- frameDocument.activeElement?.closest(EDITABLE_FOCUS_SELECTOR)
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
- // Taking focus for keyboard panning must never outrank a field the user
7693
- // was just handed: a composer that opens under the cursor would otherwise
7694
- // be focused on mount and silently unfocused by the same pointer motion.
7695
- if (focusedElement?.closest(EDITABLE_FOCUS_SELECTOR)) return;
7696
- surface.focus({ preventScroll: true });
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={focusScrollSurface}
8418
- onMouseEnter={focusScrollSurface}
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={focusScrollSurface}
8436
- onMouseEnter={focusScrollSurface}
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={focusScrollSurface}
8470
- onMouseEnter={focusScrollSurface}
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
- const shouldPreviewSourceDelete =
3607
- !payload.duplicate &&
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 {
@@ -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,