@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
package/corpus/README.md
CHANGED
|
@@ -7,6 +7,7 @@ import { useT } from "@agent-native/core/client/i18n";
|
|
|
7
7
|
import { type ReviewThread } from "@agent-native/core/client/review";
|
|
8
8
|
import type { ReviewComment } from "@agent-native/core/review";
|
|
9
9
|
import { injectDocumentMarkup } from "@agent-native/core/shared";
|
|
10
|
+
import { isLoopbackPreviewAllowed } from "@shared/builder-preview-url";
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_CANVAS_MAX_ZOOM,
|
|
12
13
|
DEFAULT_CANVAS_MIN_ZOOM,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
import { toast } from "sonner";
|
|
40
41
|
|
|
41
42
|
import { Button } from "@/components/ui/button";
|
|
43
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
42
44
|
// NOTE: This wires up the NEW shared visual-editor DrawOverlay + comment-pin
|
|
43
45
|
// components from `@/components/visual-editor`. The legacy iframe-only
|
|
44
46
|
// DrawOverlay at `./DrawOverlay.tsx` is intentionally NOT used here — both
|
|
@@ -150,8 +152,15 @@ function isAllowedFusionOrigin(
|
|
|
150
152
|
} catch {
|
|
151
153
|
return false;
|
|
152
154
|
}
|
|
153
|
-
// Only allow secure (https) Builder origins.
|
|
154
|
-
|
|
155
|
+
// Only allow secure (https) Builder origins. Loopback is a development-only
|
|
156
|
+
// exception: without it every bridge message from a local proxy is dropped.
|
|
157
|
+
if (protocol !== "https:") {
|
|
158
|
+
const loopbackDev =
|
|
159
|
+
isLoopbackPreviewAllowed() &&
|
|
160
|
+
(host === "localhost" || host === "127.0.0.1" || host === "[::1]");
|
|
161
|
+
if (!loopbackDev) return false;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
155
164
|
// Exact match against the configured fusion URL's origin.
|
|
156
165
|
if (fusionUrl) {
|
|
157
166
|
try {
|
|
@@ -1401,27 +1410,27 @@ export function DesignCanvas({
|
|
|
1401
1410
|
);
|
|
1402
1411
|
const onExternalContentSnapshotRef = useRef(onExternalContentSnapshot);
|
|
1403
1412
|
const isEmbeddedFrame = Boolean(embeddedFrame);
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
// original inline HTML).
|
|
1408
|
-
// 2. Otherwise fall back to the content-based URL detection (handles the case
|
|
1409
|
-
// where the branch URL has been written into the design file content, or
|
|
1410
|
-
// where the localhost URL is the file content).
|
|
1413
|
+
// The screen's own URL wins: it carries the route path and may address the
|
|
1414
|
+
// container through a same-origin proxy. `fusionUrl` only covers fusion
|
|
1415
|
+
// screens whose content is still the original inline HTML.
|
|
1411
1416
|
const rawExternalPreviewUrl = useMemo(() => {
|
|
1417
|
+
const contentUrl = getExternalPreviewUrl(renderedContent);
|
|
1418
|
+
if (contentUrl) return contentUrl;
|
|
1412
1419
|
if (sourceType === "fusion" && fusionUrl) {
|
|
1413
1420
|
try {
|
|
1414
1421
|
const url = new URL(fusionUrl);
|
|
1415
1422
|
url.hash = "";
|
|
1416
1423
|
return url.toString();
|
|
1417
1424
|
} catch {
|
|
1418
|
-
//
|
|
1425
|
+
// coercion-ok: an unparseable URL has no frame to render; the screen
|
|
1426
|
+
// falls back to its own content, as it did before any fusion linkage.
|
|
1427
|
+
return null;
|
|
1419
1428
|
}
|
|
1420
1429
|
}
|
|
1421
|
-
return
|
|
1430
|
+
return null;
|
|
1422
1431
|
}, [fusionUrl, renderedContent, sourceType]);
|
|
1423
1432
|
const runtimeLayerSnapshotEnabled =
|
|
1424
|
-
sourceType === "localhost" &&
|
|
1433
|
+
(sourceType === "localhost" || sourceType === "fusion") &&
|
|
1425
1434
|
Boolean(rawExternalPreviewUrl) &&
|
|
1426
1435
|
Boolean(onRuntimeLayerSnapshot);
|
|
1427
1436
|
const activeExternalSnapshotHtml =
|
|
@@ -1606,6 +1615,117 @@ export function DesignCanvas({
|
|
|
1606
1615
|
runtimeReplacementContentRef.current = runtimeReplacementContent;
|
|
1607
1616
|
runtimeReplacementKeyRef.current = runtimeReplacementKey;
|
|
1608
1617
|
|
|
1618
|
+
// A framed container has no dev-server bridge to register the editor chrome
|
|
1619
|
+
// with, so it goes over postMessage to the bootstrap its proxy injects.
|
|
1620
|
+
const containerPreview = useMemo(() => {
|
|
1621
|
+
// Not the localhost live-edit path: there the bridge server injects the
|
|
1622
|
+
// bridge into its own proxied document, so nothing is posted from here.
|
|
1623
|
+
if (usesLiveEditInjectedBridge) return false;
|
|
1624
|
+
if (!externalPreviewUrl || typeof window === "undefined") return false;
|
|
1625
|
+
try {
|
|
1626
|
+
return (
|
|
1627
|
+
new URL(externalPreviewUrl, window.location.href).origin !==
|
|
1628
|
+
window.location.origin
|
|
1629
|
+
);
|
|
1630
|
+
// coercion-ok: an unparseable URL frames nothing to bridge into.
|
|
1631
|
+
} catch {
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
}, [externalPreviewUrl, usesLiveEditInjectedBridge]);
|
|
1635
|
+
const installedBridgeKeyRef = useRef<string | null>(null);
|
|
1636
|
+
const sendBridgeToContainer = useCallback(() => {
|
|
1637
|
+
if (!containerPreview || !includeLiveEditEditorChrome) {
|
|
1638
|
+
console.log("[design:bridge] install skipped", {
|
|
1639
|
+
containerPreview,
|
|
1640
|
+
includeLiveEditEditorChrome,
|
|
1641
|
+
});
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const target = iframeRef.current?.contentWindow;
|
|
1645
|
+
if (!target || !externalPreviewUrl) {
|
|
1646
|
+
console.log("[design:bridge] install skipped", {
|
|
1647
|
+
hasTarget: Boolean(target),
|
|
1648
|
+
externalPreviewUrl,
|
|
1649
|
+
});
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if (installedBridgeKeyRef.current === liveEditBridgeKey) return;
|
|
1653
|
+
let origin: string;
|
|
1654
|
+
try {
|
|
1655
|
+
origin = new URL(externalPreviewUrl, window.location.href).origin;
|
|
1656
|
+
// coercion-ok: without a parseable origin there is nowhere safe to post.
|
|
1657
|
+
} catch {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
try {
|
|
1661
|
+
// Never "*": the bridge carries editor internals, and the container is
|
|
1662
|
+
// the only window that should receive it.
|
|
1663
|
+
target.postMessage(
|
|
1664
|
+
{
|
|
1665
|
+
type: "agentNative.installBridge",
|
|
1666
|
+
key: liveEditBridgeKey,
|
|
1667
|
+
script: liveEditBridgeScript,
|
|
1668
|
+
},
|
|
1669
|
+
origin,
|
|
1670
|
+
);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
// Leave the key unmarked: `bridgeReady` drives the real install, and
|
|
1673
|
+
// marking a failed attempt disables the bridge for the document's life.
|
|
1674
|
+
console.error("[design:bridge] install post threw", origin, error);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
console.log(
|
|
1678
|
+
"[design:bridge] install posted from",
|
|
1679
|
+
window.location.origin,
|
|
1680
|
+
"to",
|
|
1681
|
+
origin,
|
|
1682
|
+
);
|
|
1683
|
+
installedBridgeKeyRef.current = liveEditBridgeKey;
|
|
1684
|
+
}, [
|
|
1685
|
+
containerPreview,
|
|
1686
|
+
externalPreviewUrl,
|
|
1687
|
+
includeLiveEditEditorChrome,
|
|
1688
|
+
liveEditBridgeKey,
|
|
1689
|
+
liveEditBridgeScript,
|
|
1690
|
+
]);
|
|
1691
|
+
// The bootstrap announces itself on every document load, which is also how a
|
|
1692
|
+
// reload or in-frame navigation asks for the bridge again.
|
|
1693
|
+
useEffect(() => {
|
|
1694
|
+
if (!containerPreview) return;
|
|
1695
|
+
function onBootstrapMessage(event: MessageEvent) {
|
|
1696
|
+
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
1697
|
+
// A failed install must not read as an installed one, or the retry below
|
|
1698
|
+
// never fires and the canvas silently loses selection and inline editing.
|
|
1699
|
+
if (event.data?.type === "agentNative.bridgeFailed") {
|
|
1700
|
+
installedBridgeKeyRef.current = null;
|
|
1701
|
+
console.error(
|
|
1702
|
+
"[design] editor bridge failed to install in the preview:",
|
|
1703
|
+
event.data?.message,
|
|
1704
|
+
);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (event.data?.type === "agentNative.bridgeRejected") {
|
|
1708
|
+
installedBridgeKeyRef.current = null;
|
|
1709
|
+
console.error(
|
|
1710
|
+
`[design] the preview container refused the editor bridge from ${window.location.origin}. ` +
|
|
1711
|
+
"Containers install it only for https://design.agent-native.com, so selection " +
|
|
1712
|
+
"and inline editing are dead anywhere else.",
|
|
1713
|
+
);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (event.data?.type === "agentNative.bridgeInstalled") {
|
|
1717
|
+
console.log("[design:bridge] installed", event.data?.key);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
if (event.data?.type !== "agentNative.bridgeReady") return;
|
|
1721
|
+
console.log("[design:bridge] container reported ready");
|
|
1722
|
+
installedBridgeKeyRef.current = null;
|
|
1723
|
+
sendBridgeToContainer();
|
|
1724
|
+
}
|
|
1725
|
+
window.addEventListener("message", onBootstrapMessage);
|
|
1726
|
+
return () => window.removeEventListener("message", onBootstrapMessage);
|
|
1727
|
+
}, [containerPreview, sendBridgeToContainer]);
|
|
1728
|
+
|
|
1609
1729
|
useEffect(() => {
|
|
1610
1730
|
onExternalContentSnapshotRef.current = onExternalContentSnapshot;
|
|
1611
1731
|
}, [onExternalContentSnapshot]);
|
|
@@ -2336,6 +2456,23 @@ export function DesignCanvas({
|
|
|
2336
2456
|
// long tasks. Memoizing on the (already-stable) `srcdoc` reference
|
|
2337
2457
|
// makes unrelated re-renders skip this entirely.
|
|
2338
2458
|
const srcdocHash = useMemo(() => contentHash(srcdoc ?? ""), [srcdoc]);
|
|
2459
|
+
/**
|
|
2460
|
+
* A container we framed ourselves is cross-origin, so its messages match
|
|
2461
|
+
* neither `parentOrigin` nor the localhost bridge origin. Without its origin
|
|
2462
|
+
* here every selection, hover and layer message from the editor bridge is
|
|
2463
|
+
* dropped as untrusted while the bootstrap handshake still succeeds.
|
|
2464
|
+
*/
|
|
2465
|
+
const canvasBridgeAllowedOrigins = useMemo(
|
|
2466
|
+
() =>
|
|
2467
|
+
[
|
|
2468
|
+
sourceType === "localhost" && bridgeUrl
|
|
2469
|
+
? originFromUrl(bridgeUrl)
|
|
2470
|
+
: null,
|
|
2471
|
+
externalPreviewUrl ? originFromUrl(externalPreviewUrl) : null,
|
|
2472
|
+
].filter((origin): origin is string => Boolean(origin)),
|
|
2473
|
+
[bridgeUrl, externalPreviewUrl, sourceType],
|
|
2474
|
+
);
|
|
2475
|
+
|
|
2339
2476
|
const iframeDocumentIdentity = externalPreviewUrl
|
|
2340
2477
|
? `src:${externalPreviewUrl}`
|
|
2341
2478
|
: waitingForLiveEditBridge
|
|
@@ -2345,6 +2482,12 @@ export function DesignCanvas({
|
|
|
2345
2482
|
previousIframeDocumentIdentityRef.current = iframeDocumentIdentity;
|
|
2346
2483
|
bridgeReadyRef.current = false;
|
|
2347
2484
|
}
|
|
2485
|
+
// Only a URL-backed frame boots: srcdoc paints synchronously, so gating it on
|
|
2486
|
+
// an onLoad that already fired would strand a spinner over finished content.
|
|
2487
|
+
const [previewFrameLoaded, setPreviewFrameLoaded] = useState(false);
|
|
2488
|
+
useEffect(() => {
|
|
2489
|
+
setPreviewFrameLoaded(false);
|
|
2490
|
+
}, [iframeDocumentIdentity]);
|
|
2348
2491
|
// No snapshot is ever painted over the live frame, not even for the few
|
|
2349
2492
|
// frames of a document swap. Covering the real iframe with a frozen copy is
|
|
2350
2493
|
// the same false-success shape as rendering the snapshot outright: when the
|
|
@@ -2354,6 +2497,12 @@ export function DesignCanvas({
|
|
|
2354
2497
|
usesLiveEditEditorBridge &&
|
|
2355
2498
|
Boolean(externalPreviewUrl) &&
|
|
2356
2499
|
readyIframeDocumentIdentity !== iframeDocumentIdentity;
|
|
2500
|
+
// A proxied container paints its own app immediately, so without this the
|
|
2501
|
+
// canvas looks ready while hover, selection and layers are still dead.
|
|
2502
|
+
const sameOriginBridgePending =
|
|
2503
|
+
containerPreview &&
|
|
2504
|
+
includeLiveEditEditorChrome &&
|
|
2505
|
+
readyIframeDocumentIdentity !== iframeDocumentIdentity;
|
|
2357
2506
|
|
|
2358
2507
|
// Listen for messages from the iframe
|
|
2359
2508
|
useEffect(() => {
|
|
@@ -2370,12 +2519,7 @@ export function DesignCanvas({
|
|
|
2370
2519
|
origin: e.origin,
|
|
2371
2520
|
iframeWindow: runtimeVerificationWindow,
|
|
2372
2521
|
parentOrigin: window.location.origin,
|
|
2373
|
-
allowedOrigins:
|
|
2374
|
-
sourceType === "localhost" && bridgeUrl
|
|
2375
|
-
? [originFromUrl(bridgeUrl)].filter((origin): origin is string =>
|
|
2376
|
-
Boolean(origin),
|
|
2377
|
-
)
|
|
2378
|
-
: [],
|
|
2522
|
+
allowedOrigins: canvasBridgeAllowedOrigins,
|
|
2379
2523
|
});
|
|
2380
2524
|
if (trustedRuntimeVerificationFrame) {
|
|
2381
2525
|
if (e.data?.type !== "agent-native:runtime-layer-snapshot") return;
|
|
@@ -2402,16 +2546,11 @@ export function DesignCanvas({
|
|
|
2402
2546
|
e.data?.type === "agent-native:editor-chrome-ready"
|
|
2403
2547
|
? lateLiveEditReadyRecoveryRef.current
|
|
2404
2548
|
: null;
|
|
2405
|
-
//
|
|
2406
|
-
//
|
|
2407
|
-
//
|
|
2408
|
-
// arbitrary cross-origin frame), AND we validate the message origin
|
|
2409
|
-
// against a Builder-host allowlist (the configured fusionUrl origin or the
|
|
2410
|
-
// *.builder.io family) before relaxing the origin check. If the origin is
|
|
2411
|
-
// not on the allowlist we keep the strict check so a hostile frame that
|
|
2412
|
-
// somehow shares our window reference still can't be trusted.
|
|
2549
|
+
// A cross-origin fusion frame can never satisfy `origin === parentOrigin`,
|
|
2550
|
+
// so trust there rests on window identity plus a Builder-host allowlist.
|
|
2551
|
+
// A proxied one is same-origin and takes the strict path unchanged.
|
|
2413
2552
|
const trustedCurrentFrame =
|
|
2414
|
-
sourceType === "fusion"
|
|
2553
|
+
sourceType === "fusion" && e.origin !== window.location.origin
|
|
2415
2554
|
? iframeWindow !== null &&
|
|
2416
2555
|
e.source === iframeWindow &&
|
|
2417
2556
|
isAllowedFusionOrigin(e.origin, fusionUrl)
|
|
@@ -2420,12 +2559,7 @@ export function DesignCanvas({
|
|
|
2420
2559
|
origin: e.origin,
|
|
2421
2560
|
iframeWindow,
|
|
2422
2561
|
parentOrigin: window.location.origin,
|
|
2423
|
-
allowedOrigins:
|
|
2424
|
-
sourceType === "localhost" && bridgeUrl
|
|
2425
|
-
? [originFromUrl(bridgeUrl)].filter(
|
|
2426
|
-
(origin): origin is string => Boolean(origin),
|
|
2427
|
-
)
|
|
2428
|
-
: [],
|
|
2562
|
+
allowedOrigins: canvasBridgeAllowedOrigins,
|
|
2429
2563
|
});
|
|
2430
2564
|
const trustedLateLiveEditReady =
|
|
2431
2565
|
sourceType === "localhost" &&
|
|
@@ -2436,9 +2570,7 @@ export function DesignCanvas({
|
|
|
2436
2570
|
origin: e.origin,
|
|
2437
2571
|
iframeWindow: lateReadyRecovery.source,
|
|
2438
2572
|
parentOrigin: window.location.origin,
|
|
2439
|
-
allowedOrigins:
|
|
2440
|
-
(origin): origin is string => Boolean(origin),
|
|
2441
|
-
),
|
|
2573
|
+
allowedOrigins: canvasBridgeAllowedOrigins,
|
|
2442
2574
|
});
|
|
2443
2575
|
const trusted = trustedCurrentFrame || trustedLateLiveEditReady;
|
|
2444
2576
|
if (!trusted) {
|
|
@@ -4445,6 +4577,8 @@ export function DesignCanvas({
|
|
|
4445
4577
|
})}
|
|
4446
4578
|
data-design-preview-iframe
|
|
4447
4579
|
onLoad={(event) => {
|
|
4580
|
+
setPreviewFrameLoaded(true);
|
|
4581
|
+
sendBridgeToContainer();
|
|
4448
4582
|
// The bridge logs into the IFRAME console and cannot read
|
|
4449
4583
|
// import.meta.env, so dev has to switch it on from out here.
|
|
4450
4584
|
if (!import.meta.env?.DEV) return;
|
|
@@ -4478,6 +4612,14 @@ export function DesignCanvas({
|
|
|
4478
4612
|
title={t("designEditor.designPreview")}
|
|
4479
4613
|
/>
|
|
4480
4614
|
)}
|
|
4615
|
+
{externalPreviewUrl && !previewFrameLoaded ? (
|
|
4616
|
+
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center gap-2 bg-background px-2 text-muted-foreground">
|
|
4617
|
+
<Spinner className="size-4 shrink-0" />
|
|
4618
|
+
<span className="truncate !text-[11px] font-medium">
|
|
4619
|
+
{t("multiScreenCanvas.preparingLiveEditor")}
|
|
4620
|
+
</span>
|
|
4621
|
+
</div>
|
|
4622
|
+
) : null}
|
|
4481
4623
|
{runtimeVerificationUrl ? (
|
|
4482
4624
|
<iframe
|
|
4483
4625
|
key={`${runtimeVerificationUrl}::${runtimeVerificationRequest?.requestId ?? 0}`}
|
|
@@ -4573,6 +4715,7 @@ export function DesignCanvas({
|
|
|
4573
4715
|
) : null}
|
|
4574
4716
|
{waitingForEditableExternalSnapshot ||
|
|
4575
4717
|
waitingForLiveEditBridge ||
|
|
4718
|
+
sameOriginBridgePending ||
|
|
4576
4719
|
liveEditDocumentPending ? (
|
|
4577
4720
|
<div className="pointer-events-auto absolute inset-0 z-10 flex items-center justify-center bg-background/85 px-4 text-center text-sm text-muted-foreground">
|
|
4578
4721
|
{waitingForLiveEditBridge &&
|
|
@@ -4609,7 +4752,7 @@ export function DesignCanvas({
|
|
|
4609
4752
|
{"Retry" /* i18n-ignore local dev bridge retry button */}
|
|
4610
4753
|
</Button>
|
|
4611
4754
|
</div>
|
|
4612
|
-
) : waitingForLiveEditBridge ? (
|
|
4755
|
+
) : waitingForLiveEditBridge || sameOriginBridgePending ? (
|
|
4613
4756
|
<div className="max-w-[28rem] rounded-md border bg-card px-4 py-3 shadow-sm">
|
|
4614
4757
|
{
|
|
4615
4758
|
"Preparing live editor..." /* i18n-ignore transient localhost live-edit bridge loading state */
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type PenNode,
|
|
46
46
|
type PenPath,
|
|
47
47
|
} from "@shared/pen-path";
|
|
48
|
+
import { isRunningAppSourceType } from "@shared/source-mode";
|
|
48
49
|
import {
|
|
49
50
|
IconCopy,
|
|
50
51
|
IconDots,
|
|
@@ -7998,6 +7999,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
7998
7999
|
? canvasFrames.find((entry) => entry.screen.id === singleSelectedFrame.id)
|
|
7999
8000
|
?.screen
|
|
8000
8001
|
: undefined;
|
|
8002
|
+
const singleSelectedFrameIsRunningApp = singleSelectedFrameScreen
|
|
8003
|
+
? isRunningAppSourceType(
|
|
8004
|
+
getResolvedMetadata(singleSelectedFrameScreen).source,
|
|
8005
|
+
)
|
|
8006
|
+
: false;
|
|
8001
8007
|
// Overview element selection (a Layers-panel row or an in-canvas click
|
|
8002
8008
|
// resolving to a specific node) also suppresses the frame box: the parent
|
|
8003
8009
|
// still carries the screen in `selectedIds` (other UI — z-order, "topmost
|
|
@@ -8382,8 +8388,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
8382
8388
|
onStartRotate={(event) =>
|
|
8383
8389
|
beginRotate(singleSelectedFrame.id, event)
|
|
8384
8390
|
}
|
|
8385
|
-
onStartDrag={
|
|
8386
|
-
|
|
8391
|
+
onStartDrag={
|
|
8392
|
+
// A running app's frame is dragged by its label. Blanketing its
|
|
8393
|
+
// content with a drag surface would make the app unclickable the
|
|
8394
|
+
// moment it is selected, which is most of the time.
|
|
8395
|
+
singleSelectedFrameIsRunningApp
|
|
8396
|
+
? undefined
|
|
8397
|
+
: (event) => beginFrameDrag(singleSelectedFrame.id, event)
|
|
8387
8398
|
}
|
|
8388
8399
|
/>
|
|
8389
8400
|
) : null}
|
|
@@ -9720,9 +9731,13 @@ const Screen = memo(function Screen({
|
|
|
9720
9731
|
!isSelected &&
|
|
9721
9732
|
!groupSelected &&
|
|
9722
9733
|
!suppressFrameChromeForChild;
|
|
9734
|
+
// A running app's `content` is its URL, so it never "has child layers" — but
|
|
9735
|
+
// its live DOM does, and that DOM is the only thing there is to select.
|
|
9723
9736
|
const screenContentInteractive =
|
|
9724
9737
|
Boolean(screenContent) &&
|
|
9725
|
-
(isSelected ||
|
|
9738
|
+
(isSelected ||
|
|
9739
|
+
isRunningAppSourceType(metadata.source) ||
|
|
9740
|
+
hasScreenChildLayers(screen.content)) &&
|
|
9726
9741
|
!locked &&
|
|
9727
9742
|
!penActive &&
|
|
9728
9743
|
!creationToolActive &&
|
|
@@ -20,6 +20,7 @@ import { useLocation } from "react-router";
|
|
|
20
20
|
|
|
21
21
|
import { useNavigationState } from "@/hooks/use-navigation-state";
|
|
22
22
|
import { DESIGN_CHAT_STORAGE_KEY } from "@/lib/agent-chat";
|
|
23
|
+
import { isBuilderHostEmbed } from "@/lib/builder-host-origin";
|
|
23
24
|
import {
|
|
24
25
|
designEditorRoute,
|
|
25
26
|
isDesignEditorRoute,
|
|
@@ -59,7 +60,9 @@ export function Layout({ children }: LayoutProps) {
|
|
|
59
60
|
const t = useT();
|
|
60
61
|
const { session } = useSession();
|
|
61
62
|
const hasSession = Boolean(session?.email);
|
|
62
|
-
|
|
63
|
+
// The shell canvas is embedded without a session, so this cannot be the token
|
|
64
|
+
// check alone or it renders Design's own nav inside Builder.
|
|
65
|
+
const embedded = isBuilderHostEmbed() || isEmbedAuthActive();
|
|
63
66
|
useNavigationState(hasSession);
|
|
64
67
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
65
68
|
const openMobileSidebar = useCallback(() => setMobileSidebarOpen(true), []);
|
|
@@ -12,6 +12,7 @@ export interface NavigationState {
|
|
|
12
12
|
designSystemId?: string;
|
|
13
13
|
templateId?: string;
|
|
14
14
|
editorView?: "single" | "overview";
|
|
15
|
+
mode?: "edit" | "annotate" | "interact";
|
|
15
16
|
inspectorTab?: "design" | "comments" | "tweaks" | "code" | "extensions";
|
|
16
17
|
inspector?: "design" | "comments" | "tweaks" | "code" | "extensions";
|
|
17
18
|
leftPanel?:
|
|
@@ -152,6 +153,14 @@ function normalizeLeftPanel(
|
|
|
152
153
|
: undefined;
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
function normalizeEditorMode(
|
|
157
|
+
value: unknown,
|
|
158
|
+
): "edit" | "annotate" | "interact" | undefined {
|
|
159
|
+
return value === "edit" || value === "annotate" || value === "interact"
|
|
160
|
+
? value
|
|
161
|
+
: undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
function normalizeDesignTool(value: unknown): string | undefined {
|
|
156
165
|
return typeof value === "string" &&
|
|
157
166
|
DESIGN_EDITOR_TOOLS.includes(value as (typeof DESIGN_EDITOR_TOOLS)[number])
|
|
@@ -166,7 +175,7 @@ export function editorPathFromCommand(cmd: NavigationState): string | null {
|
|
|
166
175
|
const params = new URLSearchParams();
|
|
167
176
|
const editorView = normalizeEditorView(cmd.editorView);
|
|
168
177
|
if (editorView) params.set("view", editorView);
|
|
169
|
-
if (editorView === "single") params.set("mode", "interact");
|
|
178
|
+
if (editorView === "single") params.set("mode", cmd.mode ?? "interact");
|
|
170
179
|
const inspectorTab = normalizeInspectorTab(cmd.inspectorTab ?? cmd.inspector);
|
|
171
180
|
if (inspectorTab) params.set("inspector", inspectorTab);
|
|
172
181
|
const leftPanel = normalizeLeftPanel(cmd.leftPanel ?? cmd.panel);
|
|
@@ -202,7 +211,7 @@ export function editorCommandFromNavigate(
|
|
|
202
211
|
path,
|
|
203
212
|
};
|
|
204
213
|
if (editorView) command.editorView = editorView;
|
|
205
|
-
if (editorView === "single") command.mode = "interact";
|
|
214
|
+
if (editorView === "single") command.mode = cmd.mode ?? "interact";
|
|
206
215
|
if (inspectorTab) command.inspectorTab = inspectorTab;
|
|
207
216
|
if (leftPanel) command.leftPanel = leftPanel;
|
|
208
217
|
if (cmd.fileId) command.fileId = cmd.fileId;
|
|
@@ -244,6 +253,8 @@ export function useNavigationState(enabled = true) {
|
|
|
244
253
|
state.designId = params.id;
|
|
245
254
|
const editorView = normalizeEditorView(searchParams.get("view"));
|
|
246
255
|
if (editorView) state.editorView = editorView;
|
|
256
|
+
const mode = normalizeEditorMode(searchParams.get("mode"));
|
|
257
|
+
if (mode) state.mode = mode;
|
|
247
258
|
const inspectorTab = normalizeInspectorTab(
|
|
248
259
|
searchParams.get("inspector"),
|
|
249
260
|
);
|
|
@@ -1177,6 +1177,7 @@ const enUS = {
|
|
|
1177
1177
|
multiScreenCanvas: {
|
|
1178
1178
|
addBreakpointToAllScreens:
|
|
1179
1179
|
"Add {{label}} breakpoint ({{width}}px) to all screens",
|
|
1180
|
+
preparingLiveEditor: "Preparing the live editor…",
|
|
1180
1181
|
duplicate: "Duplicate",
|
|
1181
1182
|
fork: "Fork",
|
|
1182
1183
|
fullView: "Full view",
|
|
@@ -8054,6 +8055,7 @@ const designModeFeatureOverrides = {
|
|
|
8054
8055
|
},
|
|
8055
8056
|
},
|
|
8056
8057
|
multiScreenCanvas: {
|
|
8058
|
+
preparingLiveEditor: "正在準備即時編輯器…",
|
|
8057
8059
|
addBreakpointToAllScreens:
|
|
8058
8060
|
"為所有畫面新增 {{label}} 中斷點({{width}}px)",
|
|
8059
8061
|
duplicate: "複製",
|
|
@@ -8226,6 +8228,7 @@ const designModeFeatureOverrides = {
|
|
|
8226
8228
|
},
|
|
8227
8229
|
},
|
|
8228
8230
|
multiScreenCanvas: {
|
|
8231
|
+
preparingLiveEditor: "正在准备实时编辑器…",
|
|
8229
8232
|
addBreakpointToAllScreens: "为所有画面添加 {{label}} 断点({{width}}px)",
|
|
8230
8233
|
duplicate: "复制",
|
|
8231
8234
|
fork: "分支",
|
|
@@ -8398,6 +8401,7 @@ const designModeFeatureOverrides = {
|
|
|
8398
8401
|
},
|
|
8399
8402
|
},
|
|
8400
8403
|
multiScreenCanvas: {
|
|
8404
|
+
preparingLiveEditor: "Preparando el editor en vivo…",
|
|
8401
8405
|
addBreakpointToAllScreens:
|
|
8402
8406
|
"Añadir punto de ruptura {{label}} ({{width}}px) a todas las pantallas",
|
|
8403
8407
|
duplicate: "Duplicar",
|
|
@@ -8592,6 +8596,7 @@ const designModeFeatureOverrides = {
|
|
|
8592
8596
|
},
|
|
8593
8597
|
},
|
|
8594
8598
|
multiScreenCanvas: {
|
|
8599
|
+
preparingLiveEditor: "Préparation de l'éditeur en direct…",
|
|
8595
8600
|
addBreakpointToAllScreens:
|
|
8596
8601
|
"Ajouter le point de rupture {{label}} ({{width}}px) à tous les écrans",
|
|
8597
8602
|
duplicate: "Dupliquer",
|
|
@@ -8783,6 +8788,7 @@ const designModeFeatureOverrides = {
|
|
|
8783
8788
|
},
|
|
8784
8789
|
},
|
|
8785
8790
|
multiScreenCanvas: {
|
|
8791
|
+
preparingLiveEditor: "Live-Editor wird vorbereitet…",
|
|
8786
8792
|
addBreakpointToAllScreens:
|
|
8787
8793
|
"{{label}}-Breakpoint ({{width}}px) zu allen Screens hinzufügen",
|
|
8788
8794
|
duplicate: "Duplizieren",
|
|
@@ -8979,6 +8985,7 @@ const designModeFeatureOverrides = {
|
|
|
8979
8985
|
},
|
|
8980
8986
|
},
|
|
8981
8987
|
multiScreenCanvas: {
|
|
8988
|
+
preparingLiveEditor: "ライブエディターを準備しています…",
|
|
8982
8989
|
addBreakpointToAllScreens:
|
|
8983
8990
|
"すべての画面に {{label}} ブレークポイント({{width}}px)を追加",
|
|
8984
8991
|
duplicate: "複製",
|
|
@@ -9170,6 +9177,7 @@ const designModeFeatureOverrides = {
|
|
|
9170
9177
|
},
|
|
9171
9178
|
},
|
|
9172
9179
|
multiScreenCanvas: {
|
|
9180
|
+
preparingLiveEditor: "실시간 편집기를 준비하는 중…",
|
|
9173
9181
|
addBreakpointToAllScreens:
|
|
9174
9182
|
"모든 화면에 {{label}} 중단점({{width}}px) 추가",
|
|
9175
9183
|
duplicate: "복제",
|
|
@@ -9358,6 +9366,7 @@ const designModeFeatureOverrides = {
|
|
|
9358
9366
|
},
|
|
9359
9367
|
},
|
|
9360
9368
|
multiScreenCanvas: {
|
|
9369
|
+
preparingLiveEditor: "Preparando o editor ao vivo…",
|
|
9361
9370
|
addBreakpointToAllScreens:
|
|
9362
9371
|
"Adicionar ponto de quebra {{label}} ({{width}}px) a todas as telas",
|
|
9363
9372
|
duplicate: "Duplicar",
|
|
@@ -9546,6 +9555,7 @@ const designModeFeatureOverrides = {
|
|
|
9546
9555
|
},
|
|
9547
9556
|
},
|
|
9548
9557
|
multiScreenCanvas: {
|
|
9558
|
+
preparingLiveEditor: "लाइव एडिटर तैयार किया जा रहा है…",
|
|
9549
9559
|
addBreakpointToAllScreens:
|
|
9550
9560
|
"सभी स्क्रीन में {{label}} ब्रेकपॉइंट ({{width}}px) जोड़ें",
|
|
9551
9561
|
duplicate: "डुप्लिकेट",
|
|
@@ -9734,6 +9744,7 @@ const designModeFeatureOverrides = {
|
|
|
9734
9744
|
},
|
|
9735
9745
|
},
|
|
9736
9746
|
multiScreenCanvas: {
|
|
9747
|
+
preparingLiveEditor: "جارٍ تحضير المحرر المباشر…",
|
|
9737
9748
|
addBreakpointToAllScreens:
|
|
9738
9749
|
"إضافة نقطة توقف {{label}} ({{width}}px) إلى جميع الشاشات",
|
|
9739
9750
|
duplicate: "تكرار",
|
|
@@ -5,6 +5,13 @@ import {
|
|
|
5
5
|
type AgentChatMessage,
|
|
6
6
|
type SendToAgentChatAndConfirmResult,
|
|
7
7
|
} from "@agent-native/core/client/agent-chat";
|
|
8
|
+
import { sendToBuilderChat } from "@agent-native/core/client/host";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getVerifiedBuilderHostOrigin,
|
|
12
|
+
isBuilderHostEmbed,
|
|
13
|
+
} from "./builder-host-origin";
|
|
14
|
+
import { isEmbedChromeRequested } from "./embed-chrome";
|
|
8
15
|
|
|
9
16
|
export const DESIGN_CHAT_STORAGE_KEY = "design";
|
|
10
17
|
|
|
@@ -39,6 +46,12 @@ export function sendToDesignAgentChatAndConfirm(
|
|
|
39
46
|
export interface DesignSourceHandoffResult {
|
|
40
47
|
target: "host" | "local";
|
|
41
48
|
delivered: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The host prefilled its composer and the user has not sent it yet. Callers
|
|
51
|
+
* must not treat this as applied: discarding pending edits here loses work
|
|
52
|
+
* the agent was never asked to do.
|
|
53
|
+
*/
|
|
54
|
+
staged?: boolean;
|
|
42
55
|
reason?: string;
|
|
43
56
|
tabId?: string;
|
|
44
57
|
}
|
|
@@ -58,6 +71,23 @@ export async function sendDesignSourceHandoffAndConfirm(
|
|
|
58
71
|
opts: AgentChatMessage,
|
|
59
72
|
options?: { timeoutMs?: number },
|
|
60
73
|
): Promise<DesignSourceHandoffResult> {
|
|
74
|
+
// `embedChrome` alone is a display preference any embedder can ask for, so the
|
|
75
|
+
// shell route is what decides who receives source-edit context. `submit` is
|
|
76
|
+
// the host's call there, and its composer filling is the delivery evidence.
|
|
77
|
+
if (isEmbedChromeRequested() && isBuilderHostEmbed()) {
|
|
78
|
+
const posted = sendToBuilderChat({
|
|
79
|
+
message: opts.message,
|
|
80
|
+
context: opts.context,
|
|
81
|
+
submit: false,
|
|
82
|
+
targetOrigin: getVerifiedBuilderHostOrigin() ?? undefined,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
target: "host",
|
|
86
|
+
delivered: posted,
|
|
87
|
+
...(posted ? { staged: true } : { reason: "host-post-failed" }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
const hostDelivery = sendMcpAppHostMessage({
|
|
62
92
|
message: opts.message,
|
|
63
93
|
context: opts.context,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand selection and pending visual edits to the embedding host's chat instead
|
|
3
|
+
* of the editor's own agent.
|
|
4
|
+
*
|
|
5
|
+
* `submit: false` throughout: the host prefills its composer so the user can
|
|
6
|
+
* say what they actually want before the turn is spent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sendToBuilderChat } from "@agent-native/core/client/host";
|
|
10
|
+
|
|
11
|
+
/** The host's chip syntax is attribute-quoted, so a stray `"` truncates it. */
|
|
12
|
+
function chipAttribute(value: string, max: number): string {
|
|
13
|
+
return value.replace(/\s+/g, " ").replace(/"/g, "'").trim().slice(0, max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function builderSelectionChip(args: {
|
|
17
|
+
label: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
}): string {
|
|
20
|
+
return `<chip text="${chipAttribute(args.label, 80)}" detail="${chipAttribute(
|
|
21
|
+
args.detail,
|
|
22
|
+
240,
|
|
23
|
+
)}" />`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sendBuilderSelectionContext(args: {
|
|
27
|
+
label: string;
|
|
28
|
+
detail: string;
|
|
29
|
+
}): boolean {
|
|
30
|
+
return sendToBuilderChat({
|
|
31
|
+
message: `${builderSelectionChip(args)} `,
|
|
32
|
+
submit: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -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
|
+
}
|