@agent-native/core 0.180.0 → 0.180.1-nightly-20260915205111
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/calendar/app/hooks/use-events.ts +11 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +14 -20
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +323 -109
- package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +48 -18
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +22 -20
- package/corpus/templates/design/app/components/design/design-canvas/LocalNetworkAccessPrompt.tsx +92 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +41 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +55 -3
- package/corpus/templates/design/changelog/2026-09-15-fixed-padding-indicator-tick-marks-so-they-render-at-the-sam.md +6 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/vite/sentry-source-maps.js +12 -20
- package/docs/content/template-plan-automations.mdx +30 -30
- package/docs/content/template-plan-developers.mdx +10 -10
- package/docs/content/template-plan-review-workflow.mdx +2 -2
- package/package.json +3 -3
|
@@ -107,12 +107,14 @@ import {
|
|
|
107
107
|
getEmbeddedIframeBackgroundColor,
|
|
108
108
|
} from "./design-canvas/embedded-frame";
|
|
109
109
|
import {
|
|
110
|
+
classifyBridgeRegistrationFailure,
|
|
110
111
|
getDesignCanvasIframeSandbox,
|
|
111
112
|
getSnapshotRetryDelayMs,
|
|
112
113
|
resolveLiveEditPreviewUrl,
|
|
113
114
|
sanitizeLocalhostSourceSnapshotHtml,
|
|
114
115
|
shouldFetchExternalSourceSnapshot,
|
|
115
116
|
shouldUseIframeLoadReadyFallback,
|
|
117
|
+
type BridgeRegistrationFailureKind,
|
|
116
118
|
} from "./design-canvas/external-preview";
|
|
117
119
|
import { isOsFileDragEvent } from "./design-canvas/file-drop";
|
|
118
120
|
import {
|
|
@@ -130,6 +132,7 @@ import {
|
|
|
130
132
|
type EmbeddedCanvasPanSession,
|
|
131
133
|
} from "./design-canvas/iframe-pan";
|
|
132
134
|
import { withLocalRuntimes } from "./design-canvas/local-runtime";
|
|
135
|
+
import { LocalNetworkAccessPrompt } from "./design-canvas/LocalNetworkAccessPrompt";
|
|
133
136
|
import type { MotionTrackWire } from "./design-canvas/motion-types";
|
|
134
137
|
import {
|
|
135
138
|
PENDING_TEXT_EDIT_TIMEOUT_MS,
|
|
@@ -1706,12 +1709,55 @@ export function DesignCanvas({
|
|
|
1706
1709
|
// "Preparing live editor..." with no explanation and no way to recover
|
|
1707
1710
|
// short of reloading the whole page.
|
|
1708
1711
|
const bridgeRegistrationRetryAttemptRef = useRef(0);
|
|
1712
|
+
const bridgeRegistrationRetryTimerRef = useRef<number | undefined>(undefined);
|
|
1713
|
+
// Two registration attempts can be in flight at once — the automatic
|
|
1714
|
+
// effect-driven one and a manually-triggered "Connect" click (see
|
|
1715
|
+
// attemptBridgeRegistration/handleConnectLocalNetworkAccess below) — and
|
|
1716
|
+
// they can resolve out of order. Only the most recent attempt's result may
|
|
1717
|
+
// ever be written to state, or an earlier failure resolving after a later
|
|
1718
|
+
// success would incorrectly flip the UI back to "still blocked".
|
|
1719
|
+
const bridgeRegistrationAttemptGenerationRef = useRef(0);
|
|
1709
1720
|
const [bridgeRegistrationRetryNonce, setBridgeRegistrationRetryNonce] =
|
|
1710
1721
|
useState(0);
|
|
1722
|
+
// Scoped strictly to the registration fetch() itself failing — drives
|
|
1723
|
+
// externalPreviewUrl's raw-URL fallback and the floating
|
|
1724
|
+
// LocalNetworkAccessPrompt card below. Deliberately separate from
|
|
1725
|
+
// bridgeConnectionLostError: a fetch failure and "the live document never
|
|
1726
|
+
// confirmed ready after a successful registration" (handleSuspectedBridge
|
|
1727
|
+
// Restart's destructive paths) are different failure modes — the latter
|
|
1728
|
+
// proves the bridge WAS reachable, so a permission-flavored "maybe you need
|
|
1729
|
+
// to grant local network access" message would be actively misleading
|
|
1730
|
+
// there, and unlike a fetch failure there's no still-reachable raw
|
|
1731
|
+
// dev-server document to fall back to showing (the live document itself is
|
|
1732
|
+
// what stopped responding).
|
|
1711
1733
|
const [bridgeRegistrationError, setBridgeRegistrationError] = useState<{
|
|
1712
1734
|
bridgeKey: string;
|
|
1713
1735
|
message: string;
|
|
1714
1736
|
} | null>(null);
|
|
1737
|
+
// handleSuspectedBridgeRestart's destructive terminal states (retry budget
|
|
1738
|
+
// exhausted, or /health itself confirms the process is unreachable) keep
|
|
1739
|
+
// their original full-cover blocking card and retry action, untouched by
|
|
1740
|
+
// the raw-fallback/floating-card behavior above — see
|
|
1741
|
+
// bridgeRegistrationError's comment for why these must not share state.
|
|
1742
|
+
const [bridgeConnectionLostError, setBridgeConnectionLostError] = useState<{
|
|
1743
|
+
bridgeKey: string;
|
|
1744
|
+
message: string;
|
|
1745
|
+
} | null>(null);
|
|
1746
|
+
// Distinguishes "Chrome's Local Network Access permission is blocking this
|
|
1747
|
+
// request" from "the dev server is genuinely down" — see
|
|
1748
|
+
// classifyBridgeRegistrationFailure. Drives which copy/icon the floating
|
|
1749
|
+
// LocalNetworkAccessPrompt card shows; null while unclassified or resolved.
|
|
1750
|
+
const [bridgeRegistrationFailureKind, setBridgeRegistrationFailureKind] =
|
|
1751
|
+
useState<BridgeRegistrationFailureKind | null>(null);
|
|
1752
|
+
// Dismissing the floating connect card is per-script-revision: keyed by the
|
|
1753
|
+
// liveEditBridgeKey it was shown for, so a genuinely new script/mode change
|
|
1754
|
+
// re-surfaces the card instead of leaving it permanently dismissed.
|
|
1755
|
+
const [
|
|
1756
|
+
localNetworkAccessDismissedForKey,
|
|
1757
|
+
setLocalNetworkAccessDismissedForKey,
|
|
1758
|
+
] = useState<string | null>(null);
|
|
1759
|
+
const [connectingLocalNetworkAccess, setConnectingLocalNetworkAccess] =
|
|
1760
|
+
useState(false);
|
|
1715
1761
|
// Cache of the bridgeInstanceId returned by the client's LAST successful
|
|
1716
1762
|
// /live-edit-bridge registration POST (see the registration effect below).
|
|
1717
1763
|
// Compared against a later /health probe's bridgeInstanceId to tell "the
|
|
@@ -1762,12 +1808,13 @@ export function DesignCanvas({
|
|
|
1762
1808
|
bridgeKey: string;
|
|
1763
1809
|
message: string;
|
|
1764
1810
|
} | null>(null);
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1767
|
-
|
|
1811
|
+
// Clears the escalation re-arm timer on unmount so it can't fire and
|
|
1812
|
+
// schedule a fetch after this component is gone. (A stray fetch would be a
|
|
1813
|
+
// no-op anyway — every /health probe checkpoint is generation-guarded via
|
|
1814
|
+
// bridgeRegistrationAttemptGenerationRef, see handleSuspectedBridgeRestart
|
|
1815
|
+
// — but there's no reason to let the timer fire in the first place.)
|
|
1768
1816
|
useEffect(
|
|
1769
1817
|
() => () => {
|
|
1770
|
-
isUnmountedRef.current = true;
|
|
1771
1818
|
if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
|
|
1772
1819
|
window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
|
|
1773
1820
|
liveEditSameInstanceRearmTimerRef.current = undefined;
|
|
@@ -1902,8 +1949,20 @@ export function DesignCanvas({
|
|
|
1902
1949
|
const usesLiveEditInjectedBridge =
|
|
1903
1950
|
sourceType === "localhost" &&
|
|
1904
1951
|
Boolean(bridgeUrl && previewToken && rawExternalPreviewUrl);
|
|
1952
|
+
// Hoisted above usesLiveEditEditorBridge (rather than declared next to
|
|
1953
|
+
// externalPreviewUrl/usingRawFallbackPreview below, which reuse it) because
|
|
1954
|
+
// a failed registration's raw-URL fallback document has no injected editor
|
|
1955
|
+
// bridge script and can never answer an editor command or post the ready
|
|
1956
|
+
// handshake — usesLiveEditEditorBridge must already reflect that, or the
|
|
1957
|
+
// still-"editable" chrome above it (selection, inspector, hover) lets the
|
|
1958
|
+
// user attempt edits that silently queue forever against a frame that will
|
|
1959
|
+
// never respond (see postOneShotBridgeMessage's queueing above).
|
|
1960
|
+
const bridgeRegistrationFailedForCurrentKey =
|
|
1961
|
+
bridgeRegistrationError?.bridgeKey === liveEditBridgeKey;
|
|
1905
1962
|
const usesLiveEditEditorBridge =
|
|
1906
|
-
usesLiveEditInjectedBridge &&
|
|
1963
|
+
usesLiveEditInjectedBridge &&
|
|
1964
|
+
includeLiveEditEditorChrome &&
|
|
1965
|
+
!bridgeRegistrationFailedForCurrentKey;
|
|
1907
1966
|
const effectiveRegisteredLiveEditBridgeKey =
|
|
1908
1967
|
registeredLiveEditBridgeKey ??
|
|
1909
1968
|
(hasRecentLiveEditRegistration(registrationHandoffKey)
|
|
@@ -2001,9 +2060,25 @@ export function DesignCanvas({
|
|
|
2001
2060
|
// registration succeeds, the one real proxied document mounts directly.
|
|
2002
2061
|
// A viewer with no previewToken has no bridge to wait for, so it loads the
|
|
2003
2062
|
// dev server directly rather than degrading to a snapshot.
|
|
2063
|
+
//
|
|
2064
|
+
// A FAILED registration (most commonly Chrome's Local Network Access
|
|
2065
|
+
// permission blocking the fetch — see classifyBridgeRegistrationFailure)
|
|
2066
|
+
// falls back the same way: the dev server itself is still reachable via a
|
|
2067
|
+
// plain iframe navigation (unlike fetch/XHR, navigations aren't subject to
|
|
2068
|
+
// that permission check), so showing it read-only beats hiding a working
|
|
2069
|
+
// app behind an indefinite loading state. LocalNetworkAccessPrompt offers
|
|
2070
|
+
// the way to actually enable editing from here.
|
|
2071
|
+
const usingRawFallbackPreview =
|
|
2072
|
+
usesLiveEditInjectedBridge &&
|
|
2073
|
+
!liveEditExternalPreviewUrl &&
|
|
2074
|
+
bridgeRegistrationFailedForCurrentKey;
|
|
2004
2075
|
const externalPreviewUrl =
|
|
2005
2076
|
liveEditExternalPreviewUrl ??
|
|
2006
|
-
(usesLiveEditInjectedBridge
|
|
2077
|
+
(usesLiveEditInjectedBridge
|
|
2078
|
+
? bridgeRegistrationFailedForCurrentKey
|
|
2079
|
+
? rawExternalPreviewUrl
|
|
2080
|
+
: null
|
|
2081
|
+
: rawExternalPreviewUrl);
|
|
2007
2082
|
const runtimeVerificationUrl = useMemo(() => {
|
|
2008
2083
|
if (!runtimeVerificationRequest || !externalPreviewUrl) return null;
|
|
2009
2084
|
return externalPreviewUrl;
|
|
@@ -2145,8 +2220,149 @@ export function DesignCanvas({
|
|
|
2145
2220
|
// null forever (console.warn only, no retry, no error UI), pinning
|
|
2146
2221
|
// waitingForLiveEditBridge true with no way to recover short of a full
|
|
2147
2222
|
// page reload.
|
|
2223
|
+
const scheduleBridgeRegistrationRetry = useCallback(() => {
|
|
2224
|
+
const delay = getSnapshotRetryDelayMs(
|
|
2225
|
+
bridgeRegistrationRetryAttemptRef.current,
|
|
2226
|
+
);
|
|
2227
|
+
bridgeRegistrationRetryAttemptRef.current += 1;
|
|
2228
|
+
bridgeRegistrationRetryTimerRef.current = window.setTimeout(() => {
|
|
2229
|
+
setBridgeRegistrationRetryNonce((nonce) => nonce + 1);
|
|
2230
|
+
}, delay);
|
|
2231
|
+
}, []);
|
|
2232
|
+
// Invalidates any registration attempt still in flight when THIS effect
|
|
2233
|
+
// instance unmounts — deliberately not a one-way "isUnmounted" flag: that
|
|
2234
|
+
// shape never resets, so under StrictMode's dev-only mount→cleanup→mount
|
|
2235
|
+
// replay it would stay stuck true after the first (intentionally
|
|
2236
|
+
// discarded) cleanup and permanently block every later, genuinely-live
|
|
2237
|
+
// attempt. Bumping the generation counter here instead only invalidates
|
|
2238
|
+
// the specific attempt that was in
|
|
2239
|
+
// flight at THIS cleanup; the next mount's own attempt captures a fresh
|
|
2240
|
+
// generation and is unaffected.
|
|
2241
|
+
useEffect(
|
|
2242
|
+
() => () => {
|
|
2243
|
+
bridgeRegistrationAttemptGenerationRef.current += 1;
|
|
2244
|
+
},
|
|
2245
|
+
[],
|
|
2246
|
+
);
|
|
2247
|
+
// Single source of truth for a registration attempt, shared by the
|
|
2248
|
+
// automatic effect below and the manual "Connect" button (see
|
|
2249
|
+
// handleConnectLocalNetworkAccess) — see bridgeRegistrationAttemptGeneration
|
|
2250
|
+
// Ref's comment for why a shared, generation-guarded function is required
|
|
2251
|
+
// instead of each caller firing its own independent fetch.
|
|
2252
|
+
// Returns true/false for a definite, still-applicable outcome, or null when
|
|
2253
|
+
// a newer attempt (effect-driven or manual) has already superseded this
|
|
2254
|
+
// one — callers must treat null as "nothing to do", not as a failure, or a
|
|
2255
|
+
// stale attempt could schedule a redundant retry after a later attempt
|
|
2256
|
+
// already succeeded.
|
|
2257
|
+
const attemptBridgeRegistration = useCallback(async (): Promise<
|
|
2258
|
+
boolean | null
|
|
2259
|
+
> => {
|
|
2260
|
+
if (!usesLiveEditInjectedBridge || !bridgeUrl || !previewToken) {
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
const generation = ++bridgeRegistrationAttemptGenerationRef.current;
|
|
2264
|
+
// Unmount is covered by the dedicated cleanup-only effect above, which
|
|
2265
|
+
// bumps this same counter — a manual Connect click's fetch has no effect
|
|
2266
|
+
// cleanup of its own to cancel it, but that effect's bump still
|
|
2267
|
+
// invalidates it the same way a superseded attempt is invalidated.
|
|
2268
|
+
const isCurrent = () =>
|
|
2269
|
+
bridgeRegistrationAttemptGenerationRef.current === generation;
|
|
2270
|
+
// A fresh attempt — whether auto-retry or a manual Connect click,
|
|
2271
|
+
// including one retried from the destructive bridgeConnectionLostError
|
|
2272
|
+
// card's own Retry button (see handleConnectLocalNetworkAccess) — means
|
|
2273
|
+
// we're no longer in "connection lost, needs a click" limbo. Clear it now
|
|
2274
|
+
// rather than only on success/failure, or the full-cover destructive card
|
|
2275
|
+
// stays visible (it takes priority in the overlay below) even once this
|
|
2276
|
+
// attempt resolves as an ordinary registration-fetch failure instead.
|
|
2277
|
+
setBridgeConnectionLostError(null);
|
|
2278
|
+
const endpoint = new URL("/live-edit-bridge", bridgeUrl).toString();
|
|
2279
|
+
try {
|
|
2280
|
+
const response = await fetch(endpoint, {
|
|
2281
|
+
method: "POST",
|
|
2282
|
+
headers: {
|
|
2283
|
+
"content-type": "application/json",
|
|
2284
|
+
"x-design-preview-token": previewToken,
|
|
2285
|
+
},
|
|
2286
|
+
body: JSON.stringify({
|
|
2287
|
+
script: liveEditBridgeScript,
|
|
2288
|
+
bridgeKey: liveEditBridgeKey,
|
|
2289
|
+
}),
|
|
2290
|
+
});
|
|
2291
|
+
if (!response.ok) {
|
|
2292
|
+
throw new Error(`Bridge registration failed (${response.status})`);
|
|
2293
|
+
}
|
|
2294
|
+
// coercion-ok: response.ok already confirmed the registration itself
|
|
2295
|
+
// succeeded above; bridgeInstanceId is supplementary metadata for the
|
|
2296
|
+
// restart-detection heuristic only (see classifyLiveEditHealthProbe),
|
|
2297
|
+
// and the null/missing case below is checked explicitly, not treated
|
|
2298
|
+
// as equivalent to a present value.
|
|
2299
|
+
const payload = (await response.json().catch(() => null)) as {
|
|
2300
|
+
bridgeInstanceId?: string;
|
|
2301
|
+
} | null;
|
|
2302
|
+
if (!isCurrent()) return null;
|
|
2303
|
+
if (payload && typeof payload.bridgeInstanceId === "string") {
|
|
2304
|
+
// Cache the instance id from THIS successful registration so a
|
|
2305
|
+
// later suspected-restart probe (see handleSuspectedBridgeRestart)
|
|
2306
|
+
// can tell a genuinely restarted bridge process apart from the same
|
|
2307
|
+
// process rejecting a stale key. Written only after isCurrent()
|
|
2308
|
+
// passes: an older overlapping request resolving after a newer one
|
|
2309
|
+
// must not overwrite the current attempt's instance id, or the
|
|
2310
|
+
// watchdog misdiagnoses a restart and burns its reload/retry budget.
|
|
2311
|
+
bridgeInstanceIdRef.current = payload.bridgeInstanceId;
|
|
2312
|
+
}
|
|
2313
|
+
bridgeRegistrationRetryAttemptRef.current = 0;
|
|
2314
|
+
if (registrationHandoffKey) {
|
|
2315
|
+
liveEditRegistrationHandoff.set(registrationHandoffKey, Date.now());
|
|
2316
|
+
}
|
|
2317
|
+
// liveEditRestartAttemptRef is intentionally NOT reset here: a
|
|
2318
|
+
// successful registration POST only proves the bridge accepted the
|
|
2319
|
+
// script, not that the live document actually loaded it (that's what
|
|
2320
|
+
// the ready-handshake watchdog below still has to confirm). Resetting
|
|
2321
|
+
// the restart budget on every registration success — rather than only
|
|
2322
|
+
// on a genuine agent-native:editor-chrome-ready — would let a
|
|
2323
|
+
// pathological bridge that keeps minting a new bridgeInstanceId
|
|
2324
|
+
// reregister forever, defeating MAX_LIVE_EDIT_RESTART_ATTEMPTS.
|
|
2325
|
+
setBridgeRegistrationError(null);
|
|
2326
|
+
setBridgeRegistrationFailureKind(null);
|
|
2327
|
+
setBridgeConnectionLostError(null);
|
|
2328
|
+
setConnectingLocalNetworkAccess(false);
|
|
2329
|
+
lateLiveEditReadyRecoveryRef.current = null;
|
|
2330
|
+
setRegisteredLiveEditBridgeKey(liveEditBridgeKey);
|
|
2331
|
+
return true;
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
if (!isCurrent()) return null;
|
|
2334
|
+
if (registrationHandoffKey) {
|
|
2335
|
+
liveEditRegistrationHandoff.delete(registrationHandoffKey);
|
|
2336
|
+
}
|
|
2337
|
+
console.warn("live-edit bridge registration failed", error);
|
|
2338
|
+
setRegisteredLiveEditBridgeKey(null);
|
|
2339
|
+
setBridgeRegistrationError({
|
|
2340
|
+
bridgeKey: liveEditBridgeKey,
|
|
2341
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2342
|
+
});
|
|
2343
|
+
setConnectingLocalNetworkAccess(false);
|
|
2344
|
+
void classifyBridgeRegistrationFailure().then((kind) => {
|
|
2345
|
+
if (isCurrent()) setBridgeRegistrationFailureKind(kind);
|
|
2346
|
+
});
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
}, [
|
|
2350
|
+
bridgeUrl,
|
|
2351
|
+
liveEditBridgeKey,
|
|
2352
|
+
liveEditBridgeScript,
|
|
2353
|
+
previewToken,
|
|
2354
|
+
registrationHandoffKey,
|
|
2355
|
+
usesLiveEditInjectedBridge,
|
|
2356
|
+
]);
|
|
2148
2357
|
useEffect(() => {
|
|
2149
2358
|
if (!usesLiveEditInjectedBridge || !bridgeUrl || !previewToken) {
|
|
2359
|
+
// Invalidate any attempt still in flight from before this branch was
|
|
2360
|
+
// entered (previous bridge key/mode) BEFORE clearing state below —
|
|
2361
|
+
// otherwise that stale attempt's isCurrent() check would still pass
|
|
2362
|
+
// when it resolves and could restore registeredLiveEditBridgeKey,
|
|
2363
|
+
// failure state, or handoff data after we've already left this
|
|
2364
|
+
// localhost/bridge configuration.
|
|
2365
|
+
bridgeRegistrationAttemptGenerationRef.current += 1;
|
|
2150
2366
|
bridgeRegistrationRetryAttemptRef.current = 0;
|
|
2151
2367
|
liveEditRestartAttemptRef.current = 0;
|
|
2152
2368
|
liveEditSameInstanceElapsedMsRef.current = 0;
|
|
@@ -2157,94 +2373,37 @@ export function DesignCanvas({
|
|
|
2157
2373
|
}
|
|
2158
2374
|
setRegisteredLiveEditBridgeKey(null);
|
|
2159
2375
|
setBridgeRegistrationError(null);
|
|
2376
|
+
setBridgeRegistrationFailureKind(null);
|
|
2377
|
+
setBridgeConnectionLostError(null);
|
|
2160
2378
|
setLiveEditSameInstanceStalledError(null);
|
|
2161
2379
|
lateLiveEditReadyRecoveryRef.current = null;
|
|
2162
2380
|
return;
|
|
2163
2381
|
}
|
|
2164
|
-
let cancelled = false;
|
|
2165
|
-
let retryTimer: number | undefined;
|
|
2166
2382
|
setRegisteredLiveEditBridgeKey((current) =>
|
|
2167
2383
|
current === liveEditBridgeKey ? current : null,
|
|
2168
2384
|
);
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
)
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
}, delay);
|
|
2178
|
-
};
|
|
2179
|
-
void (async () => {
|
|
2180
|
-
const endpoint = new URL("/live-edit-bridge", bridgeUrl).toString();
|
|
2181
|
-
try {
|
|
2182
|
-
const response = await fetch(endpoint, {
|
|
2183
|
-
method: "POST",
|
|
2184
|
-
headers: {
|
|
2185
|
-
"content-type": "application/json",
|
|
2186
|
-
"x-design-preview-token": previewToken,
|
|
2187
|
-
},
|
|
2188
|
-
body: JSON.stringify({
|
|
2189
|
-
script: liveEditBridgeScript,
|
|
2190
|
-
bridgeKey: liveEditBridgeKey,
|
|
2191
|
-
}),
|
|
2192
|
-
});
|
|
2193
|
-
if (!response.ok) {
|
|
2194
|
-
throw new Error(`Bridge registration failed (${response.status})`);
|
|
2195
|
-
}
|
|
2196
|
-
const payload = (await response.json().catch(() => null)) as {
|
|
2197
|
-
bridgeInstanceId?: string;
|
|
2198
|
-
} | null;
|
|
2199
|
-
if (payload && typeof payload.bridgeInstanceId === "string") {
|
|
2200
|
-
// Cache the instance id from THIS successful registration so a
|
|
2201
|
-
// later suspected-restart probe (see handleSuspectedBridgeRestart)
|
|
2202
|
-
// can tell a genuinely restarted bridge process apart from the same
|
|
2203
|
-
// process rejecting a stale key.
|
|
2204
|
-
bridgeInstanceIdRef.current = payload.bridgeInstanceId;
|
|
2205
|
-
}
|
|
2206
|
-
if (cancelled) return;
|
|
2207
|
-
bridgeRegistrationRetryAttemptRef.current = 0;
|
|
2208
|
-
if (registrationHandoffKey) {
|
|
2209
|
-
liveEditRegistrationHandoff.set(registrationHandoffKey, Date.now());
|
|
2210
|
-
}
|
|
2211
|
-
// liveEditRestartAttemptRef is intentionally NOT reset here: a
|
|
2212
|
-
// successful registration POST only proves the bridge accepted the
|
|
2213
|
-
// script, not that the live document actually loaded it (that's what
|
|
2214
|
-
// the ready-handshake watchdog below still has to confirm). Resetting
|
|
2215
|
-
// the restart budget on every registration success — rather than only
|
|
2216
|
-
// on a genuine agent-native:editor-chrome-ready — would let a
|
|
2217
|
-
// pathological bridge that keeps minting a new bridgeInstanceId
|
|
2218
|
-
// reregister forever, defeating MAX_LIVE_EDIT_RESTART_ATTEMPTS.
|
|
2219
|
-
setBridgeRegistrationError(null);
|
|
2220
|
-
lateLiveEditReadyRecoveryRef.current = null;
|
|
2221
|
-
setRegisteredLiveEditBridgeKey(liveEditBridgeKey);
|
|
2222
|
-
} catch (error) {
|
|
2223
|
-
if (!cancelled) {
|
|
2224
|
-
if (registrationHandoffKey) {
|
|
2225
|
-
liveEditRegistrationHandoff.delete(registrationHandoffKey);
|
|
2226
|
-
}
|
|
2227
|
-
console.warn("live-edit bridge registration failed", error);
|
|
2228
|
-
setRegisteredLiveEditBridgeKey(null);
|
|
2229
|
-
setBridgeRegistrationError({
|
|
2230
|
-
bridgeKey: liveEditBridgeKey,
|
|
2231
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2232
|
-
});
|
|
2233
|
-
scheduleRetry();
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
})();
|
|
2385
|
+
let cancelled = false;
|
|
2386
|
+
void attemptBridgeRegistration().then((result) => {
|
|
2387
|
+
// result === false is a definite, still-applicable failure; null means
|
|
2388
|
+
// a newer attempt already superseded this one (see
|
|
2389
|
+
// attemptBridgeRegistration's return-type comment) and must not
|
|
2390
|
+
// schedule a redundant retry.
|
|
2391
|
+
if (result === false && !cancelled) scheduleBridgeRegistrationRetry();
|
|
2392
|
+
});
|
|
2237
2393
|
return () => {
|
|
2238
2394
|
cancelled = true;
|
|
2239
|
-
if (
|
|
2395
|
+
if (bridgeRegistrationRetryTimerRef.current !== undefined) {
|
|
2396
|
+
window.clearTimeout(bridgeRegistrationRetryTimerRef.current);
|
|
2397
|
+
bridgeRegistrationRetryTimerRef.current = undefined;
|
|
2398
|
+
}
|
|
2240
2399
|
};
|
|
2241
2400
|
}, [
|
|
2401
|
+
attemptBridgeRegistration,
|
|
2242
2402
|
bridgeRegistrationRetryNonce,
|
|
2243
2403
|
bridgeUrl,
|
|
2244
2404
|
liveEditBridgeKey,
|
|
2245
|
-
liveEditBridgeScript,
|
|
2246
2405
|
previewToken,
|
|
2247
|
-
|
|
2406
|
+
scheduleBridgeRegistrationRetry,
|
|
2248
2407
|
usesLiveEditInjectedBridge,
|
|
2249
2408
|
]);
|
|
2250
2409
|
|
|
@@ -2271,12 +2430,18 @@ export function DesignCanvas({
|
|
|
2271
2430
|
lateLiveEditReadyRecoveryRef.current = null;
|
|
2272
2431
|
}, [liveEditBridgeKey]);
|
|
2273
2432
|
|
|
2274
|
-
//
|
|
2275
|
-
// user-
|
|
2276
|
-
//
|
|
2277
|
-
//
|
|
2278
|
-
//
|
|
2279
|
-
|
|
2433
|
+
// Chrome only offers its Local Network Access permission dialog for a
|
|
2434
|
+
// fetch made within an active user-gesture window, so this calls the
|
|
2435
|
+
// SAME attemptBridgeRegistration used by the automatic retry effect
|
|
2436
|
+
// directly and synchronously — not through a nonce bump, which would
|
|
2437
|
+
// defer the actual fetch() past the click's gesture window and lose the
|
|
2438
|
+
// ability to trigger Chrome's permission prompt. Also resets the backoff
|
|
2439
|
+
// and same-instance escalation state, mirroring the offline-state "Retry"
|
|
2440
|
+
// buttons elsewhere in this file, so the user-initiated attempt starts
|
|
2441
|
+
// every counter fresh and any already-scheduled auto-retry doesn't fire a
|
|
2442
|
+
// second, redundant attempt shortly after this one.
|
|
2443
|
+
const handleConnectLocalNetworkAccess = useCallback(() => {
|
|
2444
|
+
setConnectingLocalNetworkAccess(true);
|
|
2280
2445
|
bridgeRegistrationRetryAttemptRef.current = 0;
|
|
2281
2446
|
liveEditRestartAttemptRef.current = 0;
|
|
2282
2447
|
liveEditSameInstanceElapsedMsRef.current = 0;
|
|
@@ -2286,8 +2451,23 @@ export function DesignCanvas({
|
|
|
2286
2451
|
liveEditSameInstanceRearmTimerRef.current = undefined;
|
|
2287
2452
|
}
|
|
2288
2453
|
setLiveEditSameInstanceStalledError(null);
|
|
2289
|
-
|
|
2290
|
-
|
|
2454
|
+
if (bridgeRegistrationRetryTimerRef.current !== undefined) {
|
|
2455
|
+
window.clearTimeout(bridgeRegistrationRetryTimerRef.current);
|
|
2456
|
+
bridgeRegistrationRetryTimerRef.current = undefined;
|
|
2457
|
+
}
|
|
2458
|
+
// A failed manual attempt (still-refused permission, dev server still
|
|
2459
|
+
// down) must not silently stop automatic recovery — schedule the same
|
|
2460
|
+
// backoff retry the automatic path uses. null means a newer attempt
|
|
2461
|
+
// (effect-driven or another click) already superseded this one, which
|
|
2462
|
+
// already has its own outcome to handle; only a definite false schedules
|
|
2463
|
+
// here.
|
|
2464
|
+
void attemptBridgeRegistration().then((result) => {
|
|
2465
|
+
if (result === false) scheduleBridgeRegistrationRetry();
|
|
2466
|
+
});
|
|
2467
|
+
}, [attemptBridgeRegistration, scheduleBridgeRegistrationRetry]);
|
|
2468
|
+
const handleDismissLocalNetworkAccessPrompt = useCallback(() => {
|
|
2469
|
+
setLocalNetworkAccessDismissedForKey(liveEditBridgeKey);
|
|
2470
|
+
}, [liveEditBridgeKey]);
|
|
2291
2471
|
|
|
2292
2472
|
// The registered iframe's `src` is a real cross-origin navigation straight
|
|
2293
2473
|
// to the bridge's authenticated /live-edit URL, so this component can never
|
|
@@ -2303,12 +2483,29 @@ export function DesignCanvas({
|
|
|
2303
2483
|
if (!bridgeUrl || !previewToken) return;
|
|
2304
2484
|
if (liveEditRestartInFlightRef.current) return;
|
|
2305
2485
|
liveEditRestartInFlightRef.current = true;
|
|
2486
|
+
// Captured once per call, not re-read at each checkpoint below: a probe
|
|
2487
|
+
// started against an old screen/key can resolve after a NEW screen has
|
|
2488
|
+
// already registered successfully (bumping this same counter via
|
|
2489
|
+
// attemptBridgeRegistration or the unmount-cleanup effect above) — this
|
|
2490
|
+
// guards every mutating branch below against silently tearing down or
|
|
2491
|
+
// re-registering that newer, unrelated registration.
|
|
2492
|
+
// Deliberately not gated on a one-way "isUnmounted" flag: that shape
|
|
2493
|
+
// never resets, so it would stay stuck true after StrictMode's dev-only
|
|
2494
|
+
// mount→cleanup→mount replay and silently stop every later health probe
|
|
2495
|
+
// from ever resolving (see attemptBridgeRegistration's identical fix
|
|
2496
|
+
// above). The dedicated unmount-cleanup effect there bumps this same
|
|
2497
|
+
// counter on a real
|
|
2498
|
+
// unmount, which isHealthProbeCurrent() already covers correctly.
|
|
2499
|
+
const healthProbeGeneration =
|
|
2500
|
+
bridgeRegistrationAttemptGenerationRef.current;
|
|
2501
|
+
const isHealthProbeCurrent = () =>
|
|
2502
|
+
bridgeRegistrationAttemptGenerationRef.current === healthProbeGeneration;
|
|
2306
2503
|
try {
|
|
2307
2504
|
const response = await fetch(healthEndpointUrl(bridgeUrl));
|
|
2308
2505
|
const payload = (await response.json().catch(() => null)) as {
|
|
2309
2506
|
bridgeInstanceId?: string;
|
|
2310
2507
|
} | null;
|
|
2311
|
-
if (
|
|
2508
|
+
if (!isHealthProbeCurrent()) return;
|
|
2312
2509
|
const responseBridgeInstanceId =
|
|
2313
2510
|
payload && typeof payload.bridgeInstanceId === "string"
|
|
2314
2511
|
? payload.bridgeInstanceId
|
|
@@ -2333,7 +2530,7 @@ export function DesignCanvas({
|
|
|
2333
2530
|
}
|
|
2334
2531
|
: null;
|
|
2335
2532
|
setRegisteredLiveEditBridgeKey(null);
|
|
2336
|
-
|
|
2533
|
+
setBridgeConnectionLostError({
|
|
2337
2534
|
bridgeKey: liveEditBridgeKey,
|
|
2338
2535
|
message: t("designCanvas.localBridge.confirmationRetryExhausted"),
|
|
2339
2536
|
});
|
|
@@ -2351,7 +2548,7 @@ export function DesignCanvas({
|
|
|
2351
2548
|
// re-triggers the registration effect above.
|
|
2352
2549
|
bridgeInstanceIdRef.current = responseBridgeInstanceId;
|
|
2353
2550
|
setRegisteredLiveEditBridgeKey(null);
|
|
2354
|
-
|
|
2551
|
+
setBridgeConnectionLostError(null);
|
|
2355
2552
|
setLiveEditSameInstanceStalledError(null);
|
|
2356
2553
|
// A genuine restart makes any same-instance-id wait we'd accumulated
|
|
2357
2554
|
// against the OLD process meaningless — reset the escalation clock so
|
|
@@ -2396,7 +2593,7 @@ export function DesignCanvas({
|
|
|
2396
2593
|
}
|
|
2397
2594
|
liveEditSameInstanceRearmTimerRef.current = window.setTimeout(() => {
|
|
2398
2595
|
liveEditSameInstanceRearmTimerRef.current = undefined;
|
|
2399
|
-
if (
|
|
2596
|
+
if (bridgeReadyRef.current || !isHealthProbeCurrent()) return;
|
|
2400
2597
|
void handleSuspectedBridgeRestart();
|
|
2401
2598
|
}, nextDelay);
|
|
2402
2599
|
return;
|
|
@@ -2417,12 +2614,12 @@ export function DesignCanvas({
|
|
|
2417
2614
|
}
|
|
2418
2615
|
: null;
|
|
2419
2616
|
setRegisteredLiveEditBridgeKey(null);
|
|
2420
|
-
|
|
2617
|
+
setBridgeConnectionLostError({
|
|
2421
2618
|
bridgeKey: liveEditBridgeKey,
|
|
2422
2619
|
message: t("designCanvas.localBridge.connectionNotConfirmed"),
|
|
2423
2620
|
});
|
|
2424
2621
|
} catch (error) {
|
|
2425
|
-
if (
|
|
2622
|
+
if (!isHealthProbeCurrent()) return;
|
|
2426
2623
|
// /health itself is unreachable (network error / thrown before a
|
|
2427
2624
|
// response) — the dev server process is actually down, not just slow.
|
|
2428
2625
|
// This destructive path (tear down + surface the error) is justified.
|
|
@@ -2438,7 +2635,7 @@ export function DesignCanvas({
|
|
|
2438
2635
|
}
|
|
2439
2636
|
: null;
|
|
2440
2637
|
setRegisteredLiveEditBridgeKey(null);
|
|
2441
|
-
|
|
2638
|
+
setBridgeConnectionLostError({
|
|
2442
2639
|
bridgeKey: liveEditBridgeKey,
|
|
2443
2640
|
message: error instanceof Error ? error.message : String(error),
|
|
2444
2641
|
});
|
|
@@ -2449,8 +2646,8 @@ export function DesignCanvas({
|
|
|
2449
2646
|
|
|
2450
2647
|
// Manual retry for the NON-destructive same-instance-id stalled card only
|
|
2451
2648
|
// (see liveEditSameInstanceStalledError below): unlike
|
|
2452
|
-
//
|
|
2453
|
-
//
|
|
2649
|
+
// handleConnectLocalNetworkAccess, registeredLiveEditBridgeKey was never
|
|
2650
|
+
// nulled here, so calling attemptBridgeRegistration again would not
|
|
2454
2651
|
// reschedule a fresh watchdog probe (liveEditBridgeRegistered never flips
|
|
2455
2652
|
// false→true to rearm that effect). Reset the backoff and probe /health
|
|
2456
2653
|
// again directly instead.
|
|
@@ -2918,9 +3115,14 @@ export function DesignCanvas({
|
|
|
2918
3115
|
// the same false-success shape as rendering the snapshot outright: when the
|
|
2919
3116
|
// swap stalls, the canvas keeps showing a screen that looks correct and
|
|
2920
3117
|
// responds to nothing. A brief flash is the honest signal.
|
|
3118
|
+
// A raw fallback document (see usingRawFallbackPreview above) never gets an
|
|
3119
|
+
// injected editor-chrome bridge, so it can never post the ready handshake
|
|
3120
|
+
// this waits for — without excluding it here, the fallback iframe would
|
|
3121
|
+
// stay marked "pending" (and thus blocked by the overlay below) forever.
|
|
2921
3122
|
const liveEditDocumentPending =
|
|
2922
3123
|
usesLiveEditEditorBridge &&
|
|
2923
3124
|
Boolean(externalPreviewUrl) &&
|
|
3125
|
+
!usingRawFallbackPreview &&
|
|
2924
3126
|
readyIframeDocumentIdentity !== iframeDocumentIdentity;
|
|
2925
3127
|
// A proxied container paints its own app immediately, so without this the
|
|
2926
3128
|
// canvas looks ready while hover, selection and layers are still dead.
|
|
@@ -3073,7 +3275,7 @@ export function DesignCanvas({
|
|
|
3073
3275
|
Date.now(),
|
|
3074
3276
|
);
|
|
3075
3277
|
}
|
|
3076
|
-
|
|
3278
|
+
setBridgeConnectionLostError((current) =>
|
|
3077
3279
|
current?.bridgeKey === lateReadyRecovery.bridgeKey ? null : current,
|
|
3078
3280
|
);
|
|
3079
3281
|
setRegisteredLiveEditBridgeKey(lateReadyRecovery.bridgeKey);
|
|
@@ -5440,20 +5642,32 @@ export function DesignCanvas({
|
|
|
5440
5642
|
</div>
|
|
5441
5643
|
</div>
|
|
5442
5644
|
) : null}
|
|
5645
|
+
{bridgeRegistrationFailedForCurrentKey &&
|
|
5646
|
+
localNetworkAccessDismissedForKey !== liveEditBridgeKey ? (
|
|
5647
|
+
// Deliberately NOT inside the blocking overlay below: usingRawFallback
|
|
5648
|
+
// Preview means the iframe right underneath is the real running dev
|
|
5649
|
+
// server, so this stays a small, dismissible corner card rather than
|
|
5650
|
+
// hiding working content behind an indefinite "preparing" screen.
|
|
5651
|
+
<LocalNetworkAccessPrompt
|
|
5652
|
+
kind={bridgeRegistrationFailureKind ?? "maybePermissionBlocked"}
|
|
5653
|
+
connecting={connectingLocalNetworkAccess}
|
|
5654
|
+
onConnect={handleConnectLocalNetworkAccess}
|
|
5655
|
+
onDismiss={handleDismissLocalNetworkAccessPrompt}
|
|
5656
|
+
/>
|
|
5657
|
+
) : null}
|
|
5443
5658
|
{waitingForEditableExternalSnapshot ||
|
|
5444
|
-
waitingForLiveEditBridge ||
|
|
5659
|
+
(waitingForLiveEditBridge && !bridgeRegistrationFailedForCurrentKey) ||
|
|
5445
5660
|
sameOriginBridgePending ||
|
|
5446
5661
|
liveEditDocumentPending ? (
|
|
5447
5662
|
<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">
|
|
5448
|
-
{
|
|
5449
|
-
|
|
5450
|
-
//
|
|
5451
|
-
// registration
|
|
5452
|
-
//
|
|
5453
|
-
//
|
|
5454
|
-
//
|
|
5455
|
-
//
|
|
5456
|
-
// to registering a new one.
|
|
5663
|
+
{bridgeConnectionLostError?.bridgeKey === liveEditBridgeKey ? (
|
|
5664
|
+
// handleSuspectedBridgeRestart's destructive terminal state (see
|
|
5665
|
+
// bridgeConnectionLostError's declaration comment): the
|
|
5666
|
+
// registration itself succeeded, so unlike the floating
|
|
5667
|
+
// LocalNetworkAccessPrompt card above there's no raw dev-server
|
|
5668
|
+
// fallback to show underneath — the live document is what
|
|
5669
|
+
// stopped responding, not the fetch. Keeps its original
|
|
5670
|
+
// full-cover card and copy, unchanged from before this PR.
|
|
5457
5671
|
<div className="pointer-events-auto flex max-w-[28rem] flex-col items-center gap-2 rounded-md border bg-card px-4 py-3 shadow-sm">
|
|
5458
5672
|
<div className="flex items-center gap-1.5 font-medium text-foreground">
|
|
5459
5673
|
<IconPlugConnectedX className="size-4 shrink-0 text-destructive" />
|
|
@@ -5467,13 +5681,13 @@ export function DesignCanvas({
|
|
|
5467
5681
|
}
|
|
5468
5682
|
</div>
|
|
5469
5683
|
<div className="w-full truncate rounded bg-muted px-2 py-1 font-mono text-[11px] text-muted-foreground">
|
|
5470
|
-
{
|
|
5684
|
+
{bridgeConnectionLostError.message}
|
|
5471
5685
|
</div>
|
|
5472
5686
|
<Button
|
|
5473
5687
|
type="button"
|
|
5474
5688
|
variant="outline"
|
|
5475
5689
|
size="sm"
|
|
5476
|
-
onClick={
|
|
5690
|
+
onClick={handleConnectLocalNetworkAccess}
|
|
5477
5691
|
>
|
|
5478
5692
|
<IconRefresh className="size-3.5" />
|
|
5479
5693
|
{"Retry" /* i18n-ignore local dev bridge retry button */}
|