@agent-native/core 0.161.8 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +180 -37
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -3
- package/corpus/templates/design/app/components/layout/Layout.tsx +4 -1
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +13 -2
- package/corpus/templates/design/app/i18n-data.ts +11 -0
- package/corpus/templates/design/app/lib/agent-chat.ts +30 -0
- package/corpus/templates/design/app/lib/builder-host-chat.ts +34 -0
- package/corpus/templates/design/app/lib/builder-host-origin.ts +31 -0
- package/corpus/templates/design/app/lib/embed-chrome.ts +70 -0
- package/corpus/templates/design/app/lib/shell-design.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +89 -0
- package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +85 -12
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +43 -19
- package/corpus/templates/design/app/pages/design-editor/screen-command-utils.ts +9 -2
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +13 -0
- package/corpus/templates/design/app/root.tsx +23 -1
- package/corpus/templates/design/server/lib/fusion-screens.ts +17 -1
- package/corpus/templates/design/server/plugins/builder-host-embed-headers.ts +37 -0
- package/corpus/templates/design/server/routes/[...page].get.ts +1 -0
- package/corpus/templates/design/shared/builder-preview-url.ts +113 -0
- package/corpus/templates/design/shared/full-app.ts +19 -0
- package/corpus/templates/design/shared/shell-screens.ts +139 -0
- package/corpus/templates/design/shared/source-mode.ts +10 -0
- package/dist/client/RuntimeConfigNotice.js +3 -0
- package/dist/client/api-surface.d.ts +19 -0
- package/dist/client/api-surface.js +32 -0
- package/dist/client/application-state.js +4 -0
- package/dist/client/builder-frame.d.ts +6 -0
- package/dist/client/builder-frame.js +1 -1
- package/dist/client/client-status-requests.js +5 -0
- package/dist/client/host/index.d.ts +1 -0
- package/dist/client/host/index.js +1 -0
- package/dist/client/use-action.d.ts +1 -1
- package/dist/client/use-action.js +17 -0
- package/dist/client/use-session.js +5 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +6 -6
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- /package/corpus/templates/design/app/routes/{visual-edit.$id.tsx → visual-edit_.$id.tsx} +0 -0
|
@@ -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
|
|
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
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
(
|
|
1190
|
-
|
|
1191
|
-
|
|
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 {
|
|
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 =
|
|
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>
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
import { getDb, schema } from "../db/index.js";
|
|
33
33
|
import { mutateDesignData } from "./design-data-mutation.js";
|
|
34
34
|
|
|
35
|
+
const PATH_BASE_PLACEHOLDER = "http://fusion-screen-base.invalid";
|
|
36
|
+
|
|
35
37
|
/** Default iframe viewport, mirroring add-localhost-screens' defaults. */
|
|
36
38
|
export const DEFAULT_FUSION_SCREEN_WIDTH = 1280;
|
|
37
39
|
export const DEFAULT_FUSION_SCREEN_HEIGHT = 900;
|
|
@@ -129,9 +131,23 @@ export async function upsertFusionScreens(args: {
|
|
|
129
131
|
|
|
130
132
|
const results: FusionScreenResult[] = [];
|
|
131
133
|
|
|
134
|
+
// The base may carry a path prefix (the builder-host preview proxy), so route
|
|
135
|
+
// paths join onto it rather than resolve against it: `new URL("/", base)`
|
|
136
|
+
// would drop the prefix and point at the origin root.
|
|
137
|
+
const originRelativeBase = previewUrl.startsWith("/");
|
|
138
|
+
const baseWithSlash = previewUrl.endsWith("/")
|
|
139
|
+
? previewUrl
|
|
140
|
+
: `${previewUrl}/`;
|
|
141
|
+
const resolutionBase = originRelativeBase
|
|
142
|
+
? new URL(baseWithSlash, PATH_BASE_PLACEHOLDER).toString()
|
|
143
|
+
: baseWithSlash;
|
|
144
|
+
|
|
132
145
|
for (let index = 0; index < paths.length; index += 1) {
|
|
133
146
|
const path = paths[index]!;
|
|
134
|
-
const
|
|
147
|
+
const screenUrl = new URL(path.replace(/^\/+/, ""), resolutionBase);
|
|
148
|
+
const url = originRelativeBase
|
|
149
|
+
? `${screenUrl.pathname}${screenUrl.search}`
|
|
150
|
+
: screenUrl.toString();
|
|
135
151
|
const preferredFilename = `fusion-${slugForPath(path)}.html`;
|
|
136
152
|
const existing = existingByFilename.get(preferredFilename);
|
|
137
153
|
const filename = existing?.filename ?? uniqueFilename(path, usedFilenames);
|