@nanobpm/nano-workforce 0.167.0 → 0.167.2
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/.github/workflows/mirror-install-dispatch.yml +54 -0
- package/CHANGELOG.md +13 -0
- package/app/contracts.ts +17 -1
- package/app/pollUserTasks.test.ts +165 -0
- package/app/pullRequestReadModel.ts +6 -1
- package/app/service.ts +52 -0
- package/e2e/feature-preflight.e2e.ts +3 -2
- package/e2e/feature-run.e2e.ts +60 -5
- package/nano.app.json +4 -0
- package/operations/acknowledgePr.test.ts +64 -19
- package/operations/acknowledgePr.ts +34 -16
- package/package.json +2 -2
- package/pages/delivery-graphs/library.mount.js +59 -2
- package/pages/delivery-graphs/mount.js +88 -2
- package/resources/processes/feature.bpmn +90 -69
- package/test/delivery-graphs-ack-or-timeout.test.ts +280 -0
- package/test/delivery-graphs-library-embed.test.ts +1 -1
- package/workers/record-feature-implementing/worker.test.ts +57 -0
- package/workers/record-feature-implementing/worker.ts +31 -0
|
@@ -15,10 +15,19 @@
|
|
|
15
15
|
// It rejects (409) a PR that is NOT terminal (still converging/waiting_review/…), so it can never
|
|
16
16
|
// pre-seed the tick-off on a live PR: were `acknowledged_at` set early, the moment the PR later settled
|
|
17
17
|
// the VIEW would drop it straight into History, skipping the operator dismiss this op exists to
|
|
18
|
-
// require.
|
|
19
|
-
//
|
|
18
|
+
// require.
|
|
19
|
+
//
|
|
20
|
+
// TERMINALITY IS READ OFF THE READ MODEL, NOT THE BASE COLUMN (issue #652). PRs are the one surface
|
|
21
|
+
// that folds terminal ON READ: since `app/abandon.ts` moved to ADR-0065 derive-only tracking, the
|
|
22
|
+
// reconciler no longer WRITES 'abandoned' onto the base `pull_requests.status` on an out-of-band
|
|
23
|
+
// terminate — the `pull_requests__tracking` VIEW folds it into `derived_status`, and the
|
|
24
|
+
// `pull_requests_read_model` VIEW exposes that as its effective `status` and lights the Dismiss
|
|
25
|
+
// affordance (`ack_open`). This op therefore consults the SAME source of truth as the affordance — the
|
|
26
|
+
// read model's folded/effective `status` — rather than re-deriving terminality against the frozen base
|
|
27
|
+
// column (which for an abandoned PR still reads its last transient, e.g. 'escalated', and would 409 a
|
|
28
|
+
// row the UI shows Dismiss on). Reading the base column here was the drift #652 fixes.
|
|
20
29
|
|
|
21
|
-
import { PR_TERMINAL_STATUSES } from "../app/pullRequestReadModel.ts";
|
|
30
|
+
import { PR_TERMINAL_STATUSES, PULL_REQUEST_READ_MODEL_NAME } from "../app/pullRequestReadModel.ts";
|
|
22
31
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
23
32
|
|
|
24
33
|
const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
|
|
@@ -32,28 +41,37 @@ export default defineOperation("acknowledgePr", async ({ body }, app) => {
|
|
|
32
41
|
const prKey = str(body.pr_key);
|
|
33
42
|
if (!prKey) return { status: 400, body: { ok: false, error: "pr_key is required" } };
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
// Read the FOLDED row from the read model VIEW — its effective `status` is
|
|
45
|
+
// `COALESCE(derived_status, status)`, so an out-of-band-terminated PR whose base `status` is frozen
|
|
46
|
+
// at a transient reads its engine-truth terminal (`abandoned`) here, exactly as the Dismiss
|
|
47
|
+
// affordance (`ack_open`) does.
|
|
48
|
+
const readModel = app.data.table<{ pr_key: string; status: string }>(
|
|
49
|
+
PULL_REQUEST_READ_MODEL_NAME,
|
|
37
50
|
"pr_key",
|
|
38
51
|
);
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
52
|
+
const view = await readModel.get(prKey);
|
|
53
|
+
if (!view) {
|
|
41
54
|
app.log.warn("acknowledge-pr: no such pull request", { prKey });
|
|
42
55
|
return { status: 404, body: { ok: false, error: "no such pull request" } };
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
// Guard: only a TERMINAL PR (a `PR_TERMINAL_STATUSES` status — the same
|
|
46
|
-
// `list_bucket`
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
// Guard: only a TERMINAL PR (a `PR_TERMINAL_STATUSES` effective status — the same terminal-folded
|
|
59
|
+
// tier the read model's `list_bucket`/`ack_open` fold to History and gate the Dismiss button on)
|
|
60
|
+
// carries the Dismiss affordance. Acknowledging a live PR would pre-seed `acknowledged_at`, so the
|
|
61
|
+
// moment it later settled the VIEW would drop it straight into History, skipping the operator
|
|
62
|
+
// tick-off this op exists to require.
|
|
63
|
+
if (!PR_TERMINAL_STATUSES.includes(view.status)) {
|
|
64
|
+
app.log.warn("acknowledge-pr rejected: PR is not terminal", { prKey, status: view.status });
|
|
51
65
|
return { status: 409, body: { ok: false, error: "pull request is not terminal" } };
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
// Stamp the dismissal. `list_bucket` (→ 'history') and `ack_open`
|
|
55
|
-
// `pull_requests_read_model` VIEW from the terminal, now-acknowledged row,
|
|
56
|
-
// here. Idempotent: re-acknowledging re-stamps and stays in History.
|
|
68
|
+
// Stamp the dismissal on the BASE `pull_requests` row. `list_bucket` (→ 'history') and `ack_open`
|
|
69
|
+
// (→ 0) are derived by the `pull_requests_read_model` VIEW from the terminal, now-acknowledged row,
|
|
70
|
+
// so we never hand-set them here. Idempotent: re-acknowledging re-stamps and stays in History.
|
|
71
|
+
const table = app.data.table<{ pr_key: string; acknowledged_at: string | null; updated_at: string }>(
|
|
72
|
+
"pull_requests",
|
|
73
|
+
"pr_key",
|
|
74
|
+
);
|
|
57
75
|
const now = new Date().toISOString();
|
|
58
76
|
await table.update(prKey, { acknowledged_at: now, updated_at: now });
|
|
59
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.167.
|
|
3
|
+
"version": "0.167.2",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@nanobpm/agentic": "^0.4.0",
|
|
64
|
-
"@nanobpm/urban": "^0.
|
|
64
|
+
"@nanobpm/urban": "^0.88.1",
|
|
65
65
|
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
// standalone — only the host element and injected endpoint config differ. The app has no browser build
|
|
14
14
|
// step, so this consumes the library doors straight off the wire.
|
|
15
15
|
|
|
16
|
-
import { DG_COMPOSE_FILL_MESSAGE } from "./mount.js";
|
|
16
|
+
import { DG_COMPOSE_FILL_ACK_MESSAGE, DG_COMPOSE_FILL_MESSAGE } from "./mount.js";
|
|
17
|
+
|
|
18
|
+
// The bounded budget within which the compose App-View must ACK a Reuse fill before we surface an honest
|
|
19
|
+
// "Couldn't reach the composer" error (issue #645). Mirrors the compose mount's own ACK_TIMEOUT_MS: long
|
|
20
|
+
// enough for the App-View relay round-trip (nano-ide #518), short enough that a dropped fill fails fast
|
|
21
|
+
// instead of leaving a misleading "✓ Loaded…" up. Overridable via config for tests.
|
|
22
|
+
const REUSE_ACK_TIMEOUT_MS = 2000;
|
|
17
23
|
|
|
18
24
|
// The read behind the list: every saved library entry, newest first. Anchored to THIS MODULE's url
|
|
19
25
|
// (import.meta.url), NOT the document base. The library App-View shell (library-embed.html /
|
|
@@ -149,6 +155,8 @@ export function mountDeliveryGraphLibrary(host, config = {}) {
|
|
|
149
155
|
|
|
150
156
|
const libraryUrl = config.libraryUrl ?? DEFAULT_LIBRARY_URL;
|
|
151
157
|
const refreshMs = typeof config.refreshMs === "number" && config.refreshMs > 0 ? config.refreshMs : DEFAULT_REFRESH_MS;
|
|
158
|
+
const ackTimeoutMs =
|
|
159
|
+
typeof config.ackTimeoutMs === "number" && config.ackTimeoutMs > 0 ? config.ackTimeoutMs : REUSE_ACK_TIMEOUT_MS;
|
|
152
160
|
const headers = (url) => ({
|
|
153
161
|
"content-type": "application/json",
|
|
154
162
|
...(config.hookSecret && isSameOrigin(url) ? { "x-hook-secret": config.hookSecret } : {}),
|
|
@@ -255,7 +263,22 @@ export function mountDeliveryGraphLibrary(host, config = {}) {
|
|
|
255
263
|
// the host bridge, posting the shared `deliveryGraph.compose.fill` message UP to the console (the
|
|
256
264
|
// INBOUND twin of the outbound `nano-navigate` DI-preview bridge). Standalone (not embedded) there is
|
|
257
265
|
// no console to route it and no compose view to fill, so we say so instead of failing silently.
|
|
266
|
+
//
|
|
267
|
+
// The success toast is ACKNOWLEDGED, never optimistic (issue #645): a "✓ Loaded…" printed synchronously
|
|
268
|
+
// after the post lied whenever the fill was dropped (no relay, wrong origin, an unmounted compose view).
|
|
269
|
+
// Now we show a NEUTRAL in-progress status, tag the fill with a correlation `token`, and resolve it only
|
|
270
|
+
// when the compose mount echoes `DG_COMPOSE_FILL_ACK_MESSAGE` for that token — "✓ Loaded…" on the ack,
|
|
271
|
+
// "Couldn't reach the composer" on a short timeout. A fresh Reuse supersedes any pending one.
|
|
258
272
|
const isEmbedded = typeof window !== "undefined" && window.parent && window.parent !== window;
|
|
273
|
+
let pendingReuse = null;
|
|
274
|
+
function clearPendingReuse() {
|
|
275
|
+
if (pendingReuse && pendingReuse.timer) clearTimeout(pendingReuse.timer);
|
|
276
|
+
pendingReuse = null;
|
|
277
|
+
}
|
|
278
|
+
function newReuseToken() {
|
|
279
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
280
|
+
return `reuse-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
281
|
+
}
|
|
259
282
|
function doReuse(id) {
|
|
260
283
|
const entry = entries.find((e) => e && e.id === id);
|
|
261
284
|
if (!entry) {
|
|
@@ -270,12 +293,42 @@ export function mountDeliveryGraphLibrary(host, config = {}) {
|
|
|
270
293
|
setStatus("Open this page inside the console to reuse a saved graph in the composer.", "err");
|
|
271
294
|
return;
|
|
272
295
|
}
|
|
296
|
+
clearPendingReuse();
|
|
297
|
+
const token = newReuseToken();
|
|
273
298
|
window.parent.postMessage(
|
|
274
|
-
{ type: DG_COMPOSE_FILL_MESSAGE, graphJson: entry.graph },
|
|
299
|
+
{ type: DG_COMPOSE_FILL_MESSAGE, graphJson: entry.graph, token },
|
|
275
300
|
window.location.origin,
|
|
276
301
|
);
|
|
302
|
+
// NEUTRAL in-progress status (no "✓") — success is only earned on the compose ack below (#645).
|
|
303
|
+
setStatus("Loading into the composer above\u2026", "");
|
|
304
|
+
const timer = setTimeout(() => {
|
|
305
|
+
if (!pendingReuse || pendingReuse.token !== token) return;
|
|
306
|
+
pendingReuse = null;
|
|
307
|
+
setStatus("Couldn't reach the composer \u2014 is the composer open above this list?", "err");
|
|
308
|
+
}, ackTimeoutMs);
|
|
309
|
+
pendingReuse = { token, timer };
|
|
310
|
+
}
|
|
311
|
+
// The compose App-View's acknowledgment that it filled (#645): a same-origin `DG_COMPOSE_FILL_ACK_MESSAGE`
|
|
312
|
+
// from the parent (the console, which relays it across from the compose sibling) whose `token` matches the
|
|
313
|
+
// pending Reuse resolves it to success. A foreign origin, a non-parent source, or a stale/mismatched token
|
|
314
|
+
// is ignored — an unrelated page (or a prior Reuse's late ack) can't forge or misattribute a success toast.
|
|
315
|
+
function onReuseAck(ev) {
|
|
316
|
+
if (!ev || typeof window === "undefined") return;
|
|
317
|
+
if (ev.origin !== window.location.origin) return;
|
|
318
|
+
if (ev.source !== window.parent) return;
|
|
319
|
+
const data = ev.data;
|
|
320
|
+
if (!data || data.type !== DG_COMPOSE_FILL_ACK_MESSAGE) return;
|
|
321
|
+
// Require a string `token` that exactly matches the pending Reuse. A null/undefined-token ack
|
|
322
|
+
// (the compose mount emits `token: null` when a fill arrives without a token) can't be correlated
|
|
323
|
+
// to this Reuse and must NOT resolve it — otherwise it reintroduces the false-positive "✓ Loaded…"
|
|
324
|
+
// toast (#645).
|
|
325
|
+
if (!pendingReuse || typeof data.token !== "string" || data.token !== pendingReuse.token) return;
|
|
326
|
+
clearPendingReuse();
|
|
277
327
|
setStatus("\u2713 Loaded into the composer above \u2014 edit, Preview or Stage it.", "ok");
|
|
278
328
|
}
|
|
329
|
+
if (isEmbedded && typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
330
|
+
window.addEventListener("message", onReuseAck);
|
|
331
|
+
}
|
|
279
332
|
|
|
280
333
|
// "Export": a purely client-side download of a saved entry's graph JSON as `<name>.deliverygraph.json`
|
|
281
334
|
// (issue #525). No backend door — the list door already carries each entry's `graph` inline (the same
|
|
@@ -368,6 +421,10 @@ export function mountDeliveryGraphLibrary(host, config = {}) {
|
|
|
368
421
|
|
|
369
422
|
return () => {
|
|
370
423
|
disposed = true;
|
|
424
|
+
clearPendingReuse();
|
|
425
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
426
|
+
window.removeEventListener("message", onReuseAck);
|
|
427
|
+
}
|
|
371
428
|
clearInterval(timer);
|
|
372
429
|
root.innerHTML = "";
|
|
373
430
|
};
|
|
@@ -50,6 +50,32 @@ const DEFAULT_IMPORT_URL = new URL("../app/api/actions/delivery-graph/library/im
|
|
|
50
50
|
// routes through the single `fillComposer()` seam below; keep new fill sources going through it.
|
|
51
51
|
export const DG_COMPOSE_FILL_MESSAGE = "nano-delivery-graph-compose-fill";
|
|
52
52
|
|
|
53
|
+
// The ACK half of the reuse-fill bridge (issue #645). An optimistic "✓ Loaded…" toast printed
|
|
54
|
+
// synchronously after the Library posts the fill can never reflect the cross-frame OUTCOME — the fill
|
|
55
|
+
// message is relayed across App-Views (nano-ide #518) and could still be dropped (no relay, wrong
|
|
56
|
+
// origin, a compose view that isn't mounted), yet the toast claimed success regardless (the #645
|
|
57
|
+
// defect). So the delivery is now ACKNOWLEDGED: when THIS compose mount actually fills from a fill
|
|
58
|
+
// message, it posts this ack back UP to the host (relayed across to the Library sibling), echoing the
|
|
59
|
+
// producer's correlation `token`. The Library shows "✓ Loaded…" ONLY on that ack and a clear error on a
|
|
60
|
+
// short timeout — the success is earned, not assumed. The `type` is exported ONCE here as the single
|
|
61
|
+
// source of truth; the Library consumer imports it rather than re-declaring a synonym.
|
|
62
|
+
export const DG_COMPOSE_FILL_ACK_MESSAGE = "nano-delivery-graph-compose-fill-ack";
|
|
63
|
+
|
|
64
|
+
// The host acknowledgment of a `nano-navigate` (issue #645). "Preview generated DI" posts nano-navigate
|
|
65
|
+
// UP to the console; nano-ide #518 forwards it to the host explorer, but the host does not synchronously
|
|
66
|
+
// confirm, so an optimistic "✓ Opening…" toast printed right after the post claimed success even when the
|
|
67
|
+
// message was dropped (standalone, no relay, a console that never navigated). The compose view now treats
|
|
68
|
+
// Preview as fire-to-host with a bounded budget: it shows a NEUTRAL in-progress status, and only earns a
|
|
69
|
+
// "✓" when the host echoes this ack for the same `target`; if no host ack is observed within the budget it
|
|
70
|
+
// surfaces "Couldn't reach the console explorer" instead. Same-origin, from the parent (the console).
|
|
71
|
+
export const NANO_NAVIGATE_ACK_MESSAGE = "nano-navigate-ack";
|
|
72
|
+
|
|
73
|
+
// The bounded budget within which a cross-frame delivery (compose-fill relay, or a nano-navigate to the
|
|
74
|
+
// host) must be acknowledged before the producer surfaces an honest "couldn't reach …" error (#645). A
|
|
75
|
+
// couple of seconds is long enough for the App-View relay round-trip yet short enough that a dropped
|
|
76
|
+
// message fails fast instead of leaving a misleading toast up. Overridable via config for tests.
|
|
77
|
+
const ACK_TIMEOUT_MS = 2000;
|
|
78
|
+
|
|
53
79
|
// A bounded timeout for every door request. Without it a hung endpoint leaves the fetch promise pending
|
|
54
80
|
// forever, so the busy() lock never clears and the UI is stranded (buttons disabled, status stuck) with
|
|
55
81
|
// no way to retry. On timeout the AbortController rejects the fetch, which surfaces as an error banner
|
|
@@ -230,6 +256,8 @@ export function mountDeliveryGraphs(host, config = {}) {
|
|
|
230
256
|
const previewUrl = config.previewUrl ?? DEFAULT_PREVIEW_URL;
|
|
231
257
|
const stageUrl = config.stageUrl ?? DEFAULT_STAGE_URL;
|
|
232
258
|
const importUrl = config.importUrl ?? DEFAULT_IMPORT_URL;
|
|
259
|
+
const ackTimeoutMs =
|
|
260
|
+
typeof config.ackTimeoutMs === "number" && config.ackTimeoutMs > 0 ? config.ackTimeoutMs : ACK_TIMEOUT_MS;
|
|
233
261
|
const headers = (url) => ({
|
|
234
262
|
"content-type": "application/json",
|
|
235
263
|
...(config.hookSecret && isSameOrigin(url) ? { "x-hook-secret": config.hookSecret } : {}),
|
|
@@ -324,7 +352,23 @@ export function mountDeliveryGraphs(host, config = {}) {
|
|
|
324
352
|
if (embedded && ev.source !== window.parent) return;
|
|
325
353
|
const data = ev.data;
|
|
326
354
|
if (!data || data.type !== DG_COMPOSE_FILL_MESSAGE || typeof data.graphJson !== "string") return;
|
|
327
|
-
fillComposer(data.graphJson, { status: "Reused a saved graph \u2014 Preview or Stage it." });
|
|
355
|
+
const filled = fillComposer(data.graphJson, { status: "Reused a saved graph \u2014 Preview or Stage it." });
|
|
356
|
+
// ACK the fill back to the producer (issue #645): the Library shows "✓ Loaded…" ONLY on this ack,
|
|
357
|
+
// never optimistically. Post UP to the host, which nano-ide #518 relays across to the Library
|
|
358
|
+
// sibling App-View; echo the correlation `token` so a stale ack from a prior Reuse can't complete a
|
|
359
|
+
// newer one. Only when actually embedded (there is a parent to relay through) and only when the fill
|
|
360
|
+
// truly happened — a blank/bad payload earns no ack.
|
|
361
|
+
if (
|
|
362
|
+
filled &&
|
|
363
|
+
embedded &&
|
|
364
|
+
window.parent &&
|
|
365
|
+
typeof window.parent.postMessage === "function"
|
|
366
|
+
) {
|
|
367
|
+
window.parent.postMessage(
|
|
368
|
+
{ type: DG_COMPOSE_FILL_ACK_MESSAGE, token: typeof data.token === "string" ? data.token : null },
|
|
369
|
+
window.location.origin,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
328
372
|
}
|
|
329
373
|
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
330
374
|
window.addEventListener("message", onFillMessage);
|
|
@@ -446,6 +490,17 @@ export function mountDeliveryGraphs(host, config = {}) {
|
|
|
446
490
|
// the path. Standalone (not embedded) there is no host explorer to drive, so we say so instead of
|
|
447
491
|
// failing silently.
|
|
448
492
|
const isEmbedded = typeof window !== "undefined" && window.parent && window.parent !== window;
|
|
493
|
+
// A single in-flight Preview→host handshake (issue #645). Posting nano-navigate is fire-to-host: the
|
|
494
|
+
// console does not synchronously confirm it navigated, so we must not claim success up front. We hold
|
|
495
|
+
// the pending target + its timeout timer here; a host ack for that target resolves it to "✓ Opened…",
|
|
496
|
+
// and the timeout resolves it to an honest "Couldn't reach the console explorer". A fresh Preview
|
|
497
|
+
// supersedes any pending one (its timer is cleared) so only the latest attempt is ever resolved.
|
|
498
|
+
const DEFINITION_PREVIEW_TARGET = "definitionPreview";
|
|
499
|
+
let pendingPreview = null;
|
|
500
|
+
function clearPendingPreview() {
|
|
501
|
+
if (pendingPreview && pendingPreview.timer) clearTimeout(pendingPreview.timer);
|
|
502
|
+
pendingPreview = null;
|
|
503
|
+
}
|
|
449
504
|
function doPreviewDi() {
|
|
450
505
|
if (lastBpmn.trim() === "") {
|
|
451
506
|
setStatus("Preview a graph first — the laid-out BPMN comes from the preview.", "err");
|
|
@@ -455,11 +510,40 @@ export function mountDeliveryGraphs(host, config = {}) {
|
|
|
455
510
|
setStatus("Open this page inside the console cockpit to preview the generated DI.", "err");
|
|
456
511
|
return;
|
|
457
512
|
}
|
|
513
|
+
clearPendingPreview();
|
|
458
514
|
window.parent.postMessage(
|
|
459
515
|
{ type: "nano-navigate", target: "definitionPreview", params: { xml: lastBpmn } },
|
|
460
516
|
window.location.origin,
|
|
461
517
|
);
|
|
462
|
-
|
|
518
|
+
// NEUTRAL in-progress status (no "✓") — the success is only earned on the host ack below (#645).
|
|
519
|
+
setStatus("Opening the generated DI in the process explorer…", "");
|
|
520
|
+
const timer = setTimeout(() => {
|
|
521
|
+
if (!pendingPreview || pendingPreview.target !== DEFINITION_PREVIEW_TARGET) return;
|
|
522
|
+
pendingPreview = null;
|
|
523
|
+
setStatus("Couldn't reach the console explorer — is this page open inside the console?", "err");
|
|
524
|
+
}, ackTimeoutMs);
|
|
525
|
+
pendingPreview = { target: DEFINITION_PREVIEW_TARGET, timer };
|
|
526
|
+
}
|
|
527
|
+
// The host's acknowledgment of a Preview nano-navigate (#645): a same-origin `nano-navigate-ack` from
|
|
528
|
+
// the parent (the console) for the target we're awaiting resolves the pending Preview to success. A
|
|
529
|
+
// foreign origin, a non-parent source, or an ack for a different/absent target is ignored — an
|
|
530
|
+
// unrelated page can't forge a success toast.
|
|
531
|
+
function onNavAck(ev) {
|
|
532
|
+
if (!ev || typeof window === "undefined") return;
|
|
533
|
+
if (ev.origin !== window.location.origin) return;
|
|
534
|
+
if (ev.source !== window.parent) return;
|
|
535
|
+
const data = ev.data;
|
|
536
|
+
if (!data || data.type !== NANO_NAVIGATE_ACK_MESSAGE) return;
|
|
537
|
+
// Require a string `target` that exactly matches the pending preview. A target-less (or
|
|
538
|
+
// mismatched) ack must NOT resolve the Preview — otherwise any same-origin parent ack could
|
|
539
|
+
// forge a "✓ Opened…" toast the target never actually rendered (#645).
|
|
540
|
+
if (!pendingPreview || typeof data.target !== "string" || data.target !== pendingPreview.target)
|
|
541
|
+
return;
|
|
542
|
+
clearPendingPreview();
|
|
543
|
+
setStatus("\u2713 Opened the generated DI in the process explorer.", "ok");
|
|
544
|
+
}
|
|
545
|
+
if (isEmbedded && typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
546
|
+
window.addEventListener("message", onNavAck);
|
|
463
547
|
}
|
|
464
548
|
outputEl.addEventListener("click", (ev) => {
|
|
465
549
|
const btn = ev.target && ev.target.closest ? ev.target.closest("[data-preview-di]") : null;
|
|
@@ -469,8 +553,10 @@ export function mountDeliveryGraphs(host, config = {}) {
|
|
|
469
553
|
});
|
|
470
554
|
|
|
471
555
|
return () => {
|
|
556
|
+
clearPendingPreview();
|
|
472
557
|
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
473
558
|
window.removeEventListener("message", onFillMessage);
|
|
559
|
+
window.removeEventListener("message", onNavAck);
|
|
474
560
|
}
|
|
475
561
|
root.innerHTML = "";
|
|
476
562
|
};
|