@agent-native/core 0.161.7 → 0.161.9

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 (65) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
  3. package/corpus/templates/content/server/plugins/feature-flags.ts +5 -0
  4. package/corpus/templates/content/shared/feature-flags.ts +15 -0
  5. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +180 -37
  6. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -3
  7. package/corpus/templates/design/app/components/layout/Layout.tsx +4 -1
  8. package/corpus/templates/design/app/hooks/use-navigation-state.ts +13 -2
  9. package/corpus/templates/design/app/i18n-data.ts +11 -0
  10. package/corpus/templates/design/app/lib/agent-chat.ts +30 -0
  11. package/corpus/templates/design/app/lib/builder-host-chat.ts +34 -0
  12. package/corpus/templates/design/app/lib/builder-host-origin.ts +31 -0
  13. package/corpus/templates/design/app/lib/embed-chrome.ts +70 -0
  14. package/corpus/templates/design/app/lib/shell-design.ts +113 -0
  15. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +89 -0
  16. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +85 -12
  17. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +43 -19
  18. package/corpus/templates/design/app/pages/design-editor/screen-command-utils.ts +9 -2
  19. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +13 -0
  20. package/corpus/templates/design/app/root.tsx +23 -1
  21. package/corpus/templates/design/server/lib/fusion-screens.ts +17 -1
  22. package/corpus/templates/design/server/plugins/builder-host-embed-headers.ts +37 -0
  23. package/corpus/templates/design/server/routes/[...page].get.ts +1 -0
  24. package/corpus/templates/design/shared/builder-preview-url.ts +113 -0
  25. package/corpus/templates/design/shared/full-app.ts +19 -0
  26. package/corpus/templates/design/shared/shell-screens.ts +139 -0
  27. package/corpus/templates/design/shared/source-mode.ts +10 -0
  28. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +9 -0
  29. package/corpus/templates/slides/actions/patch-deck.ts +8 -0
  30. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +188 -66
  31. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +102 -22
  32. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +84 -2
  33. package/corpus/templates/slides/app/context/DeckContext.tsx +94 -3
  34. package/corpus/templates/slides/app/i18n/en-US.ts +5 -0
  35. package/corpus/templates/slides/app/pages/DeckEditor.tsx +122 -8
  36. package/corpus/templates/videos/package.json +9 -0
  37. package/dist/a2a/correlation.d.ts +6 -6
  38. package/dist/a2a/correlation.js +8 -6
  39. package/dist/a2a/types.d.ts +7 -5
  40. package/dist/client/RuntimeConfigNotice.js +3 -0
  41. package/dist/client/api-surface.d.ts +19 -0
  42. package/dist/client/api-surface.js +32 -0
  43. package/dist/client/application-state.js +4 -0
  44. package/dist/client/builder-frame.d.ts +6 -0
  45. package/dist/client/builder-frame.js +1 -1
  46. package/dist/client/client-status-requests.js +5 -0
  47. package/dist/client/host/index.d.ts +1 -0
  48. package/dist/client/host/index.js +1 -0
  49. package/dist/client/use-action.d.ts +1 -1
  50. package/dist/client/use-action.js +17 -0
  51. package/dist/client/use-session.js +5 -0
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/observability/routes.d.ts +5 -5
  54. package/dist/progress/routes.d.ts +1 -1
  55. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  56. package/dist/scripts/call-agent.js +6 -5
  57. package/dist/secrets/routes.d.ts +9 -9
  58. package/dist/server/agent-chat/action-filters-a2a.d.ts +6 -1
  59. package/dist/server/agent-chat/action-filters-a2a.js +29 -2
  60. package/dist/server/agent-chat/plugin-options.d.ts +6 -0
  61. package/dist/server/agent-chat-plugin.d.ts +2 -1
  62. package/dist/server/agent-chat-plugin.js +20 -4
  63. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  64. package/package.json +1 -1
  65. /package/corpus/templates/design/app/routes/{visual-edit.$id.tsx → visual-edit_.$id.tsx} +0 -0
@@ -0,0 +1,31 @@
1
+ import { SHELL_CANVAS_PATH } from "@shared/shell-screens";
2
+
3
+ /**
4
+ * The parent origin the Builder handshake arrived from, recorded once a
5
+ * `design:init` message passes the host check.
6
+ *
7
+ * Origin sniffing cannot confirm a Builder running on localhost — every local
8
+ * dev session — so the handshake is the only signal that works in both.
9
+ */
10
+ let verifiedBuilderHostOrigin: string | null = null;
11
+
12
+ export function rememberBuilderHostOrigin(origin: string): void {
13
+ if (origin) verifiedBuilderHostOrigin = origin;
14
+ }
15
+
16
+ export function getVerifiedBuilderHostOrigin(): string | null {
17
+ return verifiedBuilderHostOrigin;
18
+ }
19
+
20
+ /**
21
+ * True on the host-driven canvas. The route is the whole signal now: there is no
22
+ * token to inspect, and this picks chrome, never access.
23
+ */
24
+ export function isBuilderHostEmbed(): boolean {
25
+ if (typeof window === "undefined") return false;
26
+ return window.location.pathname === SHELL_CANVAS_PATH;
27
+ }
28
+
29
+ export function _resetBuilderHostEmbedForTests(): void {
30
+ verifiedBuilderHostOrigin = null;
31
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Embedded hosts normally supply their own chrome, so the editor hides its
3
+ * rails. A host that frames only the canvas asks for them back with
4
+ * `?embedChrome=1`.
5
+ */
6
+
7
+ const EMBED_CHROME_QUERY_PARAM = "embedChrome";
8
+
9
+ const STORAGE_KEY_PREFIX = "agent-native:embed-chrome:";
10
+
11
+ /**
12
+ * Scoped to the design being framed: a single origin-wide key would let one
13
+ * canvas-only embed leave the flag set for the next, host-owned embed in the
14
+ * same tab, which would then render rails its URL never asked for.
15
+ */
16
+ function storageKey(win: Window): string {
17
+ const match = /\/(?:visual-edit|design)\/([^/?#]+)/.exec(
18
+ win.location.pathname,
19
+ );
20
+ return `${STORAGE_KEY_PREFIX}${match?.[1] ?? "unscoped"}`;
21
+ }
22
+
23
+ // Keyed, not a bare boolean: an SPA navigation to a different design keeps this
24
+ // module alive, and a plain cached `true` would follow the user there.
25
+ let cachedKey: string | null = null;
26
+ let requested = false;
27
+
28
+ function readFromUrl(win: Window): boolean {
29
+ try {
30
+ const value = new URL(win.location.href).searchParams.get(
31
+ EMBED_CHROME_QUERY_PARAM,
32
+ );
33
+ return value === "1" || value === "true";
34
+ // coercion-ok: an unparsable URL cannot be carrying the flag.
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Sticky once seen: the editor rewrites its own URL on the first navigation,
42
+ * which would otherwise drop the flag and strip the rails mid-session.
43
+ */
44
+ export function isEmbedChromeRequested(): boolean {
45
+ if (typeof window === "undefined") return false;
46
+ const key = storageKey(window);
47
+ if (cachedKey === key) return requested;
48
+ cachedKey = key;
49
+ if (readFromUrl(window)) {
50
+ requested = true;
51
+ try {
52
+ window.sessionStorage?.setItem(key, "1");
53
+ } catch {
54
+ // coercion-ok: sandboxed hosts refuse session storage; the module-level
55
+ // value still covers the single-page boot path.
56
+ }
57
+ return true;
58
+ }
59
+ try {
60
+ requested = window.sessionStorage?.getItem(key) === "1";
61
+ } catch {
62
+ requested = false;
63
+ }
64
+ return requested;
65
+ }
66
+
67
+ export function _resetEmbedChromeForTests(): void {
68
+ cachedKey = null;
69
+ requested = false;
70
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Turns a host `design:init` payload into the `DesignData` the editor normally
3
+ * fetches from `get-design`, so the canvas can run with no design row, no
4
+ * session and no server writes. The host owns this state; it dies with the tab.
5
+ */
6
+
7
+ import {
8
+ buildShellScreens,
9
+ type ShellScreensResult,
10
+ } from "@shared/shell-screens";
11
+
12
+ import type { DesignData, DesignFile } from "@/pages/design-editor/types";
13
+
14
+ export const SHELL_DESIGN_ID = "shell";
15
+ const SHELL_EPOCH = "1970-01-01T00:00:00.000Z";
16
+
17
+ export interface ShellDesignInput {
18
+ previewOrigin: string;
19
+ routes: Array<{ path: string; title?: string }>;
20
+ projectId?: string;
21
+ branchName?: string;
22
+ builderOrgId?: string;
23
+ contentId?: string;
24
+ }
25
+
26
+ export interface ShellDesign {
27
+ design: DesignData;
28
+ screens: ShellScreensResult["screens"];
29
+ }
30
+
31
+ /**
32
+ * `editor` because the canvas gates click-to-edit on it. Nothing it unlocks can
33
+ * reach a server: the shell never mounts a save path, so this only opens the
34
+ * in-memory affordances.
35
+ */
36
+ const SHELL_ACCESS_ROLE = "editor" as const;
37
+
38
+ /**
39
+ * Whether pending edits still describe the app the host is now pointing at. A
40
+ * new route list is not a change of app; a new origin, branch or project is.
41
+ */
42
+ export function shellContextChanged(
43
+ previous: ShellDesignInput,
44
+ next: ShellDesignInput,
45
+ ): boolean {
46
+ return (
47
+ previous.previewOrigin !== next.previewOrigin ||
48
+ previous.branchName !== next.branchName ||
49
+ previous.projectId !== next.projectId
50
+ );
51
+ }
52
+
53
+ export function buildShellDesign(input: ShellDesignInput): ShellDesign {
54
+ const { screens, placedFrames } = buildShellScreens({
55
+ previewOrigin: input.previewOrigin,
56
+ paths: input.routes.map((route) => route.path),
57
+ });
58
+
59
+ // Frame geometry is keyed by fileId, the same shape the persisted canvas uses.
60
+ const canvasFrames: Record<
61
+ string,
62
+ { x: number; y: number; width: number; height: number }
63
+ > = {};
64
+ for (const placed of placedFrames) {
65
+ const { x = 0, y = 0, width, height } = placed.frame;
66
+ canvasFrames[placed.fileId] = {
67
+ x,
68
+ y,
69
+ width: width ?? 0,
70
+ height: height ?? 0,
71
+ };
72
+ }
73
+
74
+ // Fixed, not `Date.now()`: a repeated `design:init` must rebuild an identical
75
+ // design, or the canvas treats it as a new document and remounts the frames.
76
+ const now = SHELL_EPOCH;
77
+ const files: DesignFile[] = screens.map((screen) => ({
78
+ id: screen.fileId,
79
+ filename: screen.filename,
80
+ fileType: "html",
81
+ content: screen.url,
82
+ createdAt: now,
83
+ updatedAt: now,
84
+ }));
85
+
86
+ const design: DesignData = {
87
+ id: SHELL_DESIGN_ID,
88
+ title: input.branchName ?? "Design",
89
+ updatedAt: now,
90
+ projectType: "prototype",
91
+ accessRole: SHELL_ACCESS_ROLE,
92
+ files,
93
+ data: JSON.stringify({
94
+ // Without this the editor resolves the design as `inline`, which turns off
95
+ // the runtime layer projection and leaves the layer tree permanently empty.
96
+ sourceType: "fusion",
97
+ canvasFrames,
98
+ fusionApp: {
99
+ source: "builder-host",
100
+ projectId: input.projectId ?? "",
101
+ branchName: input.branchName ?? "",
102
+ ...(input.builderOrgId ? { builderOrgId: input.builderOrgId } : {}),
103
+ ...(input.contentId ? { contentId: input.contentId } : {}),
104
+ previewUrl: input.previewOrigin,
105
+ status: "ready",
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ },
109
+ }),
110
+ };
111
+
112
+ return { design, screens };
113
+ }
@@ -1293,3 +1293,92 @@ export function parseInlineStyleAttribute(
1293
1293
  }
1294
1294
  return result;
1295
1295
  }
1296
+
1297
+ /**
1298
+ * Resolve the selected element against the projection the Layers panel is
1299
+ * actually rendering.
1300
+ *
1301
+ * A running-app screen (fusion / localhost) has TWO projections: the source one
1302
+ * built from `design_files.content` — which for these screens is the route URL,
1303
+ * not markup — and the runtime one built from the live DOM snapshot. The panel
1304
+ * renders the runtime tree (see `shouldUseRuntimeLayerProjection`), and the two
1305
+ * trees mint different node ids, so resolving selection against source returns
1306
+ * an id no rendered row carries: the selected layer never highlights and has to
1307
+ * be found by hand.
1308
+ *
1309
+ * Runtime first, then source, so an inline screen — and a live screen before
1310
+ * its first snapshot arrives — resolves exactly as it did before.
1311
+ */
1312
+ export function resolveSelectedCodeLayerNode(args: {
1313
+ selectedElement: ElementInfo | null | undefined;
1314
+ sourceProjection: CodeLayerProjection;
1315
+ runtimeProjection?: CodeLayerProjection | null;
1316
+ }): CodeLayerNode | null {
1317
+ if (!args.selectedElement) return null;
1318
+ if (args.runtimeProjection) {
1319
+ const runtimeNode = resolveCodeLayerNodeFromElementInfo(
1320
+ args.runtimeProjection,
1321
+ args.selectedElement,
1322
+ );
1323
+ if (runtimeNode) return runtimeNode;
1324
+ }
1325
+ return resolveCodeLayerNodeFromElementInfo(
1326
+ args.sourceProjection,
1327
+ args.selectedElement,
1328
+ );
1329
+ }
1330
+
1331
+ /**
1332
+ * The document a keyboard nudge should resolve its intent against.
1333
+ *
1334
+ * Returns "" when a running-app screen has no live snapshot yet, which makes
1335
+ * `resolveElementNudgeIntent` fall back to a plain translate rather than
1336
+ * projecting a route URL as if it were markup.
1337
+ */
1338
+ export function nudgeBaseContentForScreen(args: {
1339
+ isRunningApp: boolean;
1340
+ runtimeSnapshotHtml?: string | null;
1341
+ liveSnapshotHtml?: string | null;
1342
+ sourceContent: string;
1343
+ }): string {
1344
+ if (!args.isRunningApp) return args.sourceContent;
1345
+ return args.runtimeSnapshotHtml ?? args.liveSnapshotHtml ?? "";
1346
+ }
1347
+
1348
+ export interface LiveNudgeReorderHandoff {
1349
+ /** Selector the pending-edit pipeline anchors the move against. */
1350
+ anchorSelector: string;
1351
+ placement: "before" | "after";
1352
+ /** Bridge-assigned id for the anchor, when it carries one. */
1353
+ anchorSourceId?: string;
1354
+ }
1355
+
1356
+ /**
1357
+ * Translate a nudge reorder intent into the arguments a LIVE structure edit
1358
+ * needs, or null when it cannot be expressed as one.
1359
+ *
1360
+ * `resolveElementNudgeIntent` reports the anchor as a PROJECTION node id, but
1361
+ * `recordPendingLiveStructureEdit` addresses the running document by SELECTOR
1362
+ * — the projection ids never reached the live DOM. Returning null (rather than
1363
+ * guessing) makes the caller drop the keypress instead of queueing an edit
1364
+ * that would anchor against nothing.
1365
+ */
1366
+ export function liveNudgeReorderHandoff(args: {
1367
+ content: string;
1368
+ anchorNodeId: string;
1369
+ placement: "before" | "after";
1370
+ }): LiveNudgeReorderHandoff | null {
1371
+ const projection = buildCodeLayerProjection(args.content);
1372
+ const anchorNode = projection.nodes.find(
1373
+ (node) => node.id === args.anchorNodeId,
1374
+ );
1375
+ const anchorSelector = codeLayerSelectorAliases(anchorNode)[0];
1376
+ if (!anchorSelector) return null;
1377
+ const anchorSourceId =
1378
+ anchorNode?.dataAttributes["data-agent-native-node-id"]?.trim();
1379
+ return {
1380
+ anchorSelector,
1381
+ placement: args.placement,
1382
+ ...(anchorSourceId ? { anchorSourceId } : {}),
1383
+ };
1384
+ }
@@ -23,7 +23,7 @@ export const DEFAULT_NUDGE_AMOUNTS: NudgeAmounts = { small: 1, big: 10 };
23
23
  export type FlowAxis = "horizontal" | "vertical";
24
24
 
25
25
  export interface FlowContainerInfo {
26
- kind: "flex" | "grid" | "none";
26
+ kind: "flex" | "grid" | "block" | "none";
27
27
  /** The axis DOM order advances along. */
28
28
  axis: FlowAxis;
29
29
  /** Visual order runs opposite to DOM order (`*-reverse`). */
@@ -33,6 +33,23 @@ export interface FlowContainerInfo {
33
33
  lineLength: number | null;
34
34
  }
35
35
 
36
+ /**
37
+ * Normal block flow: children stack vertically in DOM order, so an arrow along
38
+ * the block axis reorders exactly like a flex column.
39
+ *
40
+ * Not inferable from the parent's own markup — a `<div>` with no styles is
41
+ * block, but so is one the stylesheet turned into a flex container. Only the
42
+ * browser knows, so this is built from the bridge's rendered `parentDisplay`
43
+ * rather than from parsed styles.
44
+ */
45
+ export const BLOCK_FLOW_CONTAINER: FlowContainerInfo = {
46
+ kind: "block",
47
+ axis: "vertical",
48
+ reversed: false,
49
+ wraps: false,
50
+ lineLength: null,
51
+ };
52
+
36
53
  export const NO_FLOW_CONTAINER: FlowContainerInfo = {
37
54
  kind: "none",
38
55
  axis: "horizontal",
@@ -385,6 +402,15 @@ function isRenderedFlowDisplay(display: string | null | undefined): boolean {
385
402
  );
386
403
  }
387
404
 
405
+ /**
406
+ * Rendered `display` values whose children stack in normal block flow, so DOM
407
+ * order is visual order. `list-item` covers `<li>`; table and flow-root boxes
408
+ * lay their children out on their own rules and are deliberately excluded.
409
+ */
410
+ function isRenderedBlockDisplay(display: string | null | undefined): boolean {
411
+ return display === "block" || display === "list-item";
412
+ }
413
+
388
414
  export interface ResolveElementNudgeIntentArgs {
389
415
  content: string;
390
416
  selectedElement: ElementInfo;
@@ -427,7 +453,60 @@ export function resolveElementNudgeIntent(
427
453
  (escapesFlow(undefined, node.classes) ? "absolute" : undefined) ??
428
454
  args.selectedElement.computedStyles?.position;
429
455
 
430
- const container = describeFlowContainer(parent);
456
+ const parsedContainer = describeFlowContainer(parent);
457
+ // The parser sees only the parent's inline styles and Tailwind utilities, so
458
+ // a stylesheet-driven layout reads as `none` and every arrow key used to fall
459
+ // through to a blind translate. Prefer what the browser actually rendered.
460
+ //
461
+ // When nothing knows the display, treat it as block: that is the CSS initial
462
+ // value for the container elements a layer tree contains, and it is also the
463
+ // case that reaches here from a layers-tree selection, where no bridge
464
+ // round-trip has happened yet and `parentDisplay` is simply absent. An
465
+ // element that really is inline or a flex child is handled above — the parser
466
+ // sees those, and a rendered value always wins over this default.
467
+ const rendered = args.selectedElement.parentDisplay;
468
+ // A rendered grid needs its column count to map an arrow onto the next visual
469
+ // cell, and `display: grid` alone does not carry it. Guessing "flex row" walks
470
+ // DOM order instead, which is a different element in any multi-column grid.
471
+ if (
472
+ parsedContainer.kind === "none" &&
473
+ !escapesFlow(position) &&
474
+ (rendered === "grid" || rendered === "inline-grid")
475
+ ) {
476
+ return { kind: "none" };
477
+ }
478
+ // The rendered axis, which markup alone cannot give: a stylesheet-driven
479
+ // `flex-direction: column` maps up/down onto DOM order, and assuming a row
480
+ // reorders on left/right instead.
481
+ const renderedFlexDirection =
482
+ args.selectedElement.parentLayout?.flexDirection;
483
+ if (
484
+ parsedContainer.kind === "none" &&
485
+ !escapesFlow(position) &&
486
+ isRenderedFlowDisplay(rendered) &&
487
+ !renderedFlexDirection
488
+ ) {
489
+ return { kind: "none" };
490
+ }
491
+ const container: FlowContainerInfo =
492
+ parsedContainer.kind === "none" && !escapesFlow(position)
493
+ ? isRenderedFlowDisplay(rendered)
494
+ ? {
495
+ ...NO_FLOW_CONTAINER,
496
+ kind: "flex",
497
+ axis: renderedFlexDirection?.startsWith("column")
498
+ ? "vertical"
499
+ : "horizontal",
500
+ reversed: renderedFlexDirection?.endsWith("-reverse") ?? false,
501
+ }
502
+ : // `parent` null means the node is a projection root: it has no flow to
503
+ // reorder within, and the bridge reports `parentDisplay: undefined`
504
+ // for it exactly as it does for a not-yet-measured selection.
505
+ isRenderedBlockDisplay(rendered) ||
506
+ (rendered === undefined && parent !== null)
507
+ ? BLOCK_FLOW_CONTAINER
508
+ : parsedContainer
509
+ : parsedContainer;
431
510
  // Flex/grid paint children by `order` and explicit grid placement, not DOM
432
511
  // position, so moving the node would write a source change that produces no
433
512
  // visible movement.
@@ -444,16 +523,10 @@ export function resolveElementNudgeIntent(
444
523
  return { kind: "none" };
445
524
  }
446
525
 
447
- // A `.row { display: flex }` parent parses as no container. Translating
448
- // would write left/top onto a flex child and detach it from its
449
- // neighbours, so do nothing rather than corrupt the layout.
450
- if (
451
- container.kind === "none" &&
452
- !escapesFlow(position) &&
453
- isRenderedFlowDisplay(args.selectedElement.parentDisplay)
454
- ) {
455
- return { kind: "none" };
456
- }
526
+ // A `.row { display: flex }` parent used to reach here as `none` and get
527
+ // suppressed. It is now promoted to a flex container above, so a reorder is
528
+ // attempted instead of the key being swallowed same protection against
529
+ // writing left/top onto a flex child, but it does the useful thing.
457
530
 
458
531
  // Rendered `order` from the bridge sees stylesheet rules that the authored
459
532
  // styles above cannot.
@@ -16,6 +16,7 @@ import {
16
16
  } from "@shared/responsive-classes";
17
17
  import {
18
18
  type ElementProvenanceUnavailableReason,
19
+ isRunningAppSourceType,
19
20
  normalizeDesignSourceType,
20
21
  type DesignSourceType,
21
22
  } from "@shared/source-mode";
@@ -824,11 +825,22 @@ export function formatPendingVisualStylePrompt(args: {
824
825
  localhostConnectionId?: string | null;
825
826
  edits: readonly PendingVisualStyleEdit[];
826
827
  liveEdits?: readonly PendingLiveNonStyleEdit[];
828
+ /**
829
+ * A coding agent has the repo and none of the Design source tools, and a
830
+ * screen's `.html` filename is the editor's own bookkeeping — naming it sends
831
+ * that agent hunting for a file the project does not contain.
832
+ */
833
+ audience?: "design-agent" | "coding-agent";
834
+ /** Screen id → the route it renders, for naming screens the way the app does. */
835
+ screenRoutes?: Readonly<Record<string, string>>;
827
836
  }): string {
837
+ const codingAgent = args.audience === "coding-agent";
838
+ const nameScreen = (screenId: string, filename: string) =>
839
+ (codingAgent ? args.screenRoutes?.[screenId] : undefined) ?? filename;
828
840
  const title = args.designTitle?.trim();
829
841
  const editPayload = args.edits.map((edit) => ({
830
842
  screenId: edit.screenId,
831
- filename: edit.filename,
843
+ screen: nameScreen(edit.screenId, edit.filename),
832
844
  screenName: edit.screenName,
833
845
  selector: edit.selector,
834
846
  sourceId: edit.sourceId ?? null,
@@ -864,7 +876,7 @@ export function formatPendingVisualStylePrompt(args: {
864
876
  return {
865
877
  kind: edit.kind,
866
878
  screenId: edit.screenId,
867
- filename: edit.filename,
879
+ screen: nameScreen(edit.screenId, edit.filename),
868
880
  screenName: edit.screenName,
869
881
  selector: edit.selector,
870
882
  sourceId: edit.sourceId ?? null,
@@ -887,7 +899,7 @@ export function formatPendingVisualStylePrompt(args: {
887
899
  return {
888
900
  kind: edit.kind,
889
901
  screenId: edit.screenId,
890
- filename: edit.filename,
902
+ screen: nameScreen(edit.screenId, edit.filename),
891
903
  screenName: edit.screenName,
892
904
  selector: edit.selector,
893
905
  sourceId: edit.sourceId ?? null,
@@ -1031,7 +1043,7 @@ export function formatPendingVisualStylePrompt(args: {
1031
1043
  return {
1032
1044
  kind: edit.kind,
1033
1045
  screenId: edit.screenId,
1034
- filename: edit.filename,
1046
+ screen: nameScreen(edit.screenId, edit.filename),
1035
1047
  screenName: edit.screenName,
1036
1048
  selector: edit.selector,
1037
1049
  sourceId: edit.sourceId ?? null,
@@ -1075,18 +1087,27 @@ export function formatPendingVisualStylePrompt(args: {
1075
1087
  };
1076
1088
  });
1077
1089
 
1090
+ const activeScreenLabel = args.activeFileId
1091
+ ? nameScreen(args.activeFileId, args.activeFilename ?? "")
1092
+ : "";
1078
1093
  return [
1079
- `Apply these pending live visual edits${title ? ` to "${title}"` : ""}.`,
1080
- args.designId ? `Design id: "${args.designId}".` : "",
1094
+ codingAgent
1095
+ ? `Apply these visual edits${title ? ` to "${title}"` : ""} by editing the app's source.`
1096
+ : `Apply these pending live visual edits${title ? ` to "${title}"` : ""}.`,
1097
+ codingAgent ? "" : args.designId ? `Design id: "${args.designId}".` : "",
1081
1098
  args.activeFileId
1082
- ? `Active screen: "${args.activeFilename ?? args.activeFileId}" (${args.activeFileId}).`
1099
+ ? codingAgent
1100
+ ? `Screen: ${activeScreenLabel || "the current route"}.`
1101
+ : `Active screen: "${args.activeFilename ?? args.activeFileId}" (${args.activeFileId}).`
1083
1102
  : "",
1084
- args.localhostConnectionId
1103
+ args.localhostConnectionId && !codingAgent
1085
1104
  ? `Active localhost connection id: "${args.localhostConnectionId}".`
1086
1105
  : "",
1087
1106
  "",
1088
- "Use the Design source tools to make the source match the current live canvas preview. Read each target screen, resolve source ids/selectors through the code-layer projection, then apply the style, text, layer-state, and structure changes with focused source edits. Preserve layout, behavior, and unrelated styling.",
1089
- hasReactSourceAnchors
1107
+ codingAgent
1108
+ ? "These were made against the running app in a visual canvas, so the selectors and node ids below are runtime-only — they do not appear in source. Locate the component that renders each element using its tag, class names and current text, then make the change in that source file. Preserve layout, behavior, and unrelated styling."
1109
+ : "Use the Design source tools to make the source match the current live canvas preview. Read each target screen, resolve source ids/selectors through the code-layer projection, then apply the style, text, layer-state, and structure changes with focused source edits. Preserve layout, behavior, and unrelated styling.",
1110
+ hasReactSourceAnchors && !codingAgent
1090
1111
  ? "React sourceAnchor fields are source provenance; runtime source ids and selectors are correlation hints only. For a single-instance leaf text, literal className/class, or flat literal style-object edit, call apply-visual-edit with source.kind=local-file plus designId, connectionId, the verified project-relative path, and target.sourceAnchor. First omit persist and inspect proposedDiff; then retry with persist=true only when the diff matches the preview. That write still requires human localhost consent and exact version-hash concurrency. Verify every file, line, column, component, and surrounding control flow before editing. Never use a generic AST reparent, group, wrapper, breakpoint, dynamic expression, repeated render, or shared component transform through this path. For semantic structure edits, follow the embedded semanticHandoff packet and use this exact guarded sequence: read-local-file, capture its versionHash, obtain human write consent, write-local-file with expectedVersionHash and requireExpectedVersionHash: true, then keep the preview pending until HMR proves the intended runtime relationship. On a version conflict, re-read and re-plan; never overwrite blindly."
1091
1112
  : "",
1092
1113
  hasRepeatedOrSharedReactScope
@@ -1147,11 +1168,15 @@ export function shouldUseRuntimeLayerProjection(args: {
1147
1168
  fallbackSourceType?: DesignSourceType;
1148
1169
  content: string;
1149
1170
  }): boolean {
1171
+ // A running app's live DOM is the ground truth; only inline screens carry
1172
+ // their own source.
1150
1173
  if (
1151
- resolveOverviewScreenSourceType(
1152
- args.screen,
1153
- args.fallbackSourceType ?? "inline",
1154
- ) !== "localhost"
1174
+ !isRunningAppSourceType(
1175
+ resolveOverviewScreenSourceType(
1176
+ args.screen,
1177
+ args.fallbackSourceType ?? "inline",
1178
+ ),
1179
+ )
1155
1180
  ) {
1156
1181
  return false;
1157
1182
  }
@@ -1185,11 +1210,10 @@ export function shouldShowPendingVisualStyleApply(args: {
1185
1210
  const allEdits = [...args.edits, ...(args.liveEdits ?? [])];
1186
1211
  return (
1187
1212
  allEdits.length > 0 &&
1188
- allEdits.every(
1189
- (edit) =>
1190
- normalizeDesignSourceType(
1191
- args.screenSourceTypes.get(edit.screenId) ?? args.fallbackSourceType,
1192
- ) === "localhost",
1213
+ allEdits.every((edit) =>
1214
+ isRunningAppSourceType(
1215
+ args.screenSourceTypes.get(edit.screenId) ?? args.fallbackSourceType,
1216
+ ),
1193
1217
  )
1194
1218
  );
1195
1219
  }
@@ -1,7 +1,11 @@
1
1
  import type { DesignEditorCommand } from "@/hooks/use-navigation-state";
2
2
 
3
3
  import { queryUniqueSelector } from "./dom-utils";
4
- import { normalizeDesignLeftPanel, normalizeDesignTool } from "./tool-state";
4
+ import {
5
+ normalizeDesignLeftPanel,
6
+ normalizeDesignMode,
7
+ normalizeDesignTool,
8
+ } from "./tool-state";
5
9
  import { type DesignFile, FOCUSED_SCREEN_ZOOM } from "./types";
6
10
 
7
11
  export function normalizeScreenTarget(value: string): string {
@@ -45,7 +49,10 @@ export function designEditorCommandFromSearchParams(
45
49
  // `single` is the URL spelling for the responsive Interact surface. There
46
50
  // is no focused editing view, so even an older URL without mode=interact
47
51
  // must enter Interact directly instead of reviving the removed Full view.
48
- const mode = editorView === "single" ? "interact" : undefined;
52
+ const mode =
53
+ editorView === "single"
54
+ ? (normalizeDesignMode(searchParams.get("mode")) ?? "interact")
55
+ : undefined;
49
56
  if (
50
57
  editorView !== "overview" &&
51
58
  editorView !== "single" &&
@@ -74,6 +74,19 @@ export function normalizeDesignTool(value: unknown): DesignTool | null {
74
74
  : null;
75
75
  }
76
76
 
77
+ const DESIGN_EDITOR_MODES = new Set<EditorMode>([
78
+ "annotate",
79
+ "edit",
80
+ "interact",
81
+ ]);
82
+
83
+ export function normalizeDesignMode(value: unknown): EditorMode | null {
84
+ return typeof value === "string" &&
85
+ DESIGN_EDITOR_MODES.has(value as EditorMode)
86
+ ? (value as EditorMode)
87
+ : null;
88
+ }
89
+
77
90
  export function isSingleScreenAnnotationTool(tool: DesignTool): boolean {
78
91
  return tool === "draw" || tool === "comment";
79
92
  }
@@ -7,6 +7,7 @@ import {
7
7
  getBrowserTabId,
8
8
  useSession,
9
9
  } from "@agent-native/core/client/hooks";
10
+ import { setAgentNativeApiDisabled } from "@agent-native/core/client/host";
10
11
  import { getLocaleInitScript, useT } from "@agent-native/core/client/i18n";
11
12
  import {
12
13
  CommandMenu,
@@ -36,6 +37,7 @@ import type { LinksFunction } from "react-router";
36
37
  import { Layout as AppLayout } from "@/components/layout/Layout";
37
38
  import { Toaster } from "@/components/ui/sonner";
38
39
  import { AppToolkitProvider } from "@/components/ui/toolkit-provider";
40
+ import { isBuilderHostEmbed } from "@/lib/builder-host-origin";
39
41
  import { requestDesignUiToggle } from "@/lib/design-ui-events";
40
42
 
41
43
  import changelog from "../CHANGELOG.md?raw";
@@ -44,6 +46,11 @@ import { isPublicDesignAppPath } from "./public-routes";
44
46
 
45
47
  import stylesheet from "./global.css?url";
46
48
 
49
+ // Builder frames this canvas with no session of its own, so every
50
+ // `/_agent-native/*` call it makes is an unauthorized one that buries real
51
+ // failures in 401 noise.
52
+ if (isBuilderHostEmbed()) setAgentNativeApiDisabled("builder shell canvas");
53
+
47
54
  configureTracking({
48
55
  llmConnectionStatus:
49
56
  typeof window === "undefined" ||
@@ -168,6 +175,21 @@ function DesignCommandMenu({
168
175
  );
169
176
  }
170
177
 
178
+ /**
179
+ * The one toaster: AppProviders renders its own by default, and a second copy
180
+ * here made every toast appear twice once the two positions stopped coinciding.
181
+ * Builder's chat covers the left column when it hosts the editor, which would
182
+ * hide any toast underneath it.
183
+ */
184
+ function DesignToaster() {
185
+ return (
186
+ <Toaster
187
+ richColors
188
+ position={isBuilderHostEmbed() ? "bottom-right" : "bottom-left"}
189
+ />
190
+ );
191
+ }
192
+
171
193
  function RootContent() {
172
194
  const location = useLocation();
173
195
  const { session } = useSession();
@@ -191,7 +213,6 @@ function RootContent() {
191
213
  return (
192
214
  <>
193
215
  {hasSession && <DbSyncSetup />}
194
- <Toaster richColors position="bottom-left" />
195
216
  {hasSession && !isPublicVisualEdit && (
196
217
  <DesignCommandMenu open={cmdkOpen} onOpenChange={setCmdkOpen} />
197
218
  )}
@@ -210,6 +231,7 @@ export default function Root() {
210
231
  queryClient={queryClient}
211
232
  isPublicPath={isPublicPath}
212
233
  i18n={{ catalog: i18nCatalog, persistPreference: !isPublicPath }}
234
+ toaster={<DesignToaster />}
213
235
  >
214
236
  <RootContent />
215
237
  </AppProviders>