@burdenoff/microfe-vibecontrols 2026.528.9 → 2026.528.11
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/dist/components/plugins/PluginIframeMount.js +66 -65
- package/dist/components/plugins/PluginIframeMount.js.map +1 -1
- package/dist/generated/wspace-operations.js +1533 -1512
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js +15 -13
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +10 -10
- package/dist/pages/SessionDetailsPage.js +460 -404
- package/dist/pages/SessionDetailsPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +1 -1
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/services/iframeToken.js +5 -1
- package/dist/services/iframeToken.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,86 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { appendIframeTokenFragment as e } from "../../services/iframeToken.js";
|
|
2
|
+
import { useEffect as t, useRef as n, useState as r } from "react";
|
|
3
|
+
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
4
|
+
import { PluginIframe as s } from "@burdenoff/fe-libs/shared/plugins";
|
|
4
5
|
//#region src/components/plugins/PluginIframeMount.tsx
|
|
5
|
-
var
|
|
6
|
-
function
|
|
7
|
-
let { contribution:
|
|
6
|
+
var c = 6e4, l = 3e4;
|
|
7
|
+
function u(u) {
|
|
8
|
+
let { contribution: d, tunnel: f, restPaths: p, context: m, agentId: h, profile: g, theme: _, locale: v, featureFlags: y, hostVersion: b, loadingNode: x, errorNode: S, style: C } = u, w = p.join("|"), [T, E] = r({
|
|
8
9
|
token: null,
|
|
9
10
|
error: null
|
|
10
|
-
}),
|
|
11
|
+
}), D = n(0), O = n(null), k = async (e) => {
|
|
11
12
|
try {
|
|
12
|
-
let t = await
|
|
13
|
-
if (e !==
|
|
13
|
+
let t = await f.mintIframeToken({ pathPrefixes: p });
|
|
14
|
+
if (e !== D.current) return;
|
|
14
15
|
let n = Date.parse(t.expiresAt);
|
|
15
16
|
if (!Number.isFinite(n)) {
|
|
16
|
-
|
|
17
|
+
E({
|
|
17
18
|
token: null,
|
|
18
19
|
error: "Invalid token expiry from agent"
|
|
19
20
|
});
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
E({
|
|
23
24
|
token: t,
|
|
24
25
|
error: null
|
|
25
26
|
});
|
|
26
27
|
} catch (t) {
|
|
27
|
-
if (e !==
|
|
28
|
-
|
|
28
|
+
if (e !== D.current) return;
|
|
29
|
+
E({
|
|
29
30
|
token: null,
|
|
30
31
|
error: t instanceof Error ? t.message : String(t)
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
let e =
|
|
37
|
-
return
|
|
38
|
-
|
|
35
|
+
if (t(() => {
|
|
36
|
+
D.current += 1;
|
|
37
|
+
let e = D.current;
|
|
38
|
+
return k(e), () => {
|
|
39
|
+
D.current += 1, O.current !== null && (clearTimeout(O.current), O.current = null);
|
|
39
40
|
};
|
|
40
41
|
}, [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
]),
|
|
45
|
-
let e =
|
|
42
|
+
f.tunnelUrl,
|
|
43
|
+
f.profile,
|
|
44
|
+
w
|
|
45
|
+
]), t(() => {
|
|
46
|
+
let e = T.token;
|
|
46
47
|
if (!e) return;
|
|
47
48
|
let t = Date.parse(e.expiresAt);
|
|
48
49
|
if (!Number.isFinite(t)) return;
|
|
49
|
-
let n = Math.max(5e3, t - Date.now() -
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
if (r !==
|
|
50
|
+
let n = Math.max(5e3, t - Date.now() - c), r = D.current;
|
|
51
|
+
return O.current = setTimeout(() => {
|
|
52
|
+
f.mintIframeToken({ pathPrefixes: p }).then((e) => {
|
|
53
|
+
if (r !== D.current) return;
|
|
53
54
|
let t = Date.parse(e.expiresAt);
|
|
54
|
-
Number.isFinite(t) &&
|
|
55
|
+
Number.isFinite(t) && E({
|
|
55
56
|
token: e,
|
|
56
57
|
error: null
|
|
57
58
|
});
|
|
58
59
|
}).catch(() => {
|
|
59
|
-
r ===
|
|
60
|
-
|
|
61
|
-
},
|
|
60
|
+
r === D.current && (O.current !== null && clearTimeout(O.current), O.current = setTimeout(() => {
|
|
61
|
+
k(r);
|
|
62
|
+
}, l));
|
|
62
63
|
});
|
|
63
64
|
}, n), () => {
|
|
64
|
-
|
|
65
|
+
O.current !== null && (clearTimeout(O.current), O.current = null);
|
|
65
66
|
};
|
|
66
67
|
}, [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
]),
|
|
71
|
-
let e = () => void
|
|
72
|
-
return
|
|
73
|
-
message:
|
|
68
|
+
T.token,
|
|
69
|
+
w,
|
|
70
|
+
f
|
|
71
|
+
]), T.error && !T.token) {
|
|
72
|
+
let e = () => void k(D.current);
|
|
73
|
+
return S ? /* @__PURE__ */ a(i, { children: S({
|
|
74
|
+
message: T.error,
|
|
74
75
|
retry: e
|
|
75
|
-
}) }) : /* @__PURE__ */
|
|
76
|
+
}) }) : /* @__PURE__ */ o("div", {
|
|
76
77
|
className: "p-4 text-sm text-status-danger-text",
|
|
77
78
|
children: [
|
|
78
79
|
"Failed to load ",
|
|
79
|
-
|
|
80
|
+
d.contribution.title,
|
|
80
81
|
": ",
|
|
81
|
-
|
|
82
|
+
T.error,
|
|
82
83
|
" ",
|
|
83
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ a("button", {
|
|
84
85
|
type: "button",
|
|
85
86
|
onClick: e,
|
|
86
87
|
className: "ml-2 underline text-text-secondary hover:text-text-primary",
|
|
@@ -89,37 +90,37 @@ function l(l) {
|
|
|
89
90
|
]
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
|
-
if (!
|
|
93
|
+
if (!T.token) return /* @__PURE__ */ a(i, { children: x ?? /* @__PURE__ */ o("div", {
|
|
93
94
|
className: "p-4 text-text-secondary",
|
|
94
95
|
children: [
|
|
95
96
|
"Loading ",
|
|
96
|
-
|
|
97
|
+
d.contribution.title,
|
|
97
98
|
"…"
|
|
98
99
|
]
|
|
99
100
|
}) });
|
|
100
|
-
let
|
|
101
|
+
let A = (() => {
|
|
101
102
|
try {
|
|
102
|
-
return new URL(
|
|
103
|
+
return new URL(f.tunnelUrl).origin;
|
|
103
104
|
} catch {
|
|
104
|
-
return
|
|
105
|
+
return f.tunnelUrl;
|
|
105
106
|
}
|
|
106
107
|
})();
|
|
107
|
-
return /* @__PURE__ */
|
|
108
|
-
src: `${
|
|
109
|
-
origin:
|
|
110
|
-
mountPoint:
|
|
111
|
-
context:
|
|
112
|
-
agentId:
|
|
113
|
-
profile:
|
|
114
|
-
tunnelUrl:
|
|
115
|
-
iframeToken:
|
|
116
|
-
capabilities:
|
|
117
|
-
theme:
|
|
118
|
-
locale:
|
|
119
|
-
featureFlags:
|
|
120
|
-
hostVersion:
|
|
121
|
-
title:
|
|
122
|
-
style:
|
|
108
|
+
return /* @__PURE__ */ a(s, {
|
|
109
|
+
src: e(`${f.tunnelUrl.replace(/\/$/, "")}${d.url}`, T.token.token),
|
|
110
|
+
origin: A,
|
|
111
|
+
mountPoint: d.contribution.mountPoint,
|
|
112
|
+
context: m,
|
|
113
|
+
agentId: h,
|
|
114
|
+
profile: g,
|
|
115
|
+
tunnelUrl: f.tunnelUrl,
|
|
116
|
+
iframeToken: T.token.token,
|
|
117
|
+
capabilities: d.capabilities,
|
|
118
|
+
theme: _,
|
|
119
|
+
locale: v,
|
|
120
|
+
featureFlags: y,
|
|
121
|
+
hostVersion: b,
|
|
122
|
+
title: d.contribution.title,
|
|
123
|
+
style: C ?? {
|
|
123
124
|
width: "100%",
|
|
124
125
|
height: "100%"
|
|
125
126
|
},
|
|
@@ -129,6 +130,6 @@ function l(l) {
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
//#endregion
|
|
132
|
-
export {
|
|
133
|
+
export { u as PluginIframeMount };
|
|
133
134
|
|
|
134
135
|
//# sourceMappingURL=PluginIframeMount.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginIframeMount.js","names":[],"sources":["../../../src/components/plugins/PluginIframeMount.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type CSSProperties } from 'react';\n\nimport {\n PluginIframe,\n type DiscoveredContribution,\n type MountTunnel,\n} from '@burdenoff/fe-libs/shared/plugins';\n\nimport { type IframeTokenResponse } from '@/services/iframeToken';\n\nconst TOKEN_REFRESH_LEEWAY_MS = 60_000;\nconst TOKEN_REFRESH_RETRY_MS = 30_000;\n\nexport interface PluginIframeMountProps {\n contribution: DiscoveredContribution;\n tunnel: MountTunnel;\n restPaths: string[];\n /** Per-page context the plugin reads via `host:init`. */\n context: Record<string, unknown>;\n /** Canonical agent id surfaced in `host:init`. */\n agentId: string;\n /** Profile that owns the iframe token (must match `tunnel.profile`). */\n profile: string;\n theme: { colorMode: 'light' | 'dark'; tokens: Record<string, string> };\n locale: string;\n featureFlags: Record<string, boolean>;\n /** Real version string. Don't reuse an entity id here. */\n hostVersion: string;\n /** Render-prop override for the loading state. */\n loadingNode?: React.ReactNode;\n /** Render-prop override for the error state. Receives the error message + a retry callback. */\n errorNode?: (error: { message: string; retry: () => void }) => React.ReactNode;\n style?: CSSProperties;\n}\n\ninterface TokenState {\n token: IframeTokenResponse | null;\n error: string | null;\n}\n\n/**\n * Shared iframe-mount helper used by the Vibe + Agent detail pages\n * and the standalone `/plugin/:pluginKey` route. Owns the per-\n * contribution scoped-token lifecycle (initial mint + refresh ~60s\n * before expiry + retry-on-failure) and renders the iframe via the\n * fe-libs `<PluginIframe>` primitive.\n *\n * Behaviour:\n * - Initial mint failure surfaces an error state with a retry\n * button (no infinite \"Loading…\" spinner).\n * - Refresh failure keeps the existing token in place + retries\n * after a short delay; the iframe doesn't unmount.\n * - Component-level unmount cancels all in-flight + scheduled work\n * via the alive ref.\n */\nexport function PluginIframeMount(props: PluginIframeMountProps) {\n const {\n contribution,\n tunnel,\n restPaths,\n context,\n agentId,\n profile,\n theme,\n locale,\n featureFlags,\n hostVersion,\n loadingNode,\n errorNode,\n style,\n } = props;\n const restPathsKey = restPaths.join('|');\n const [tokenState, setTokenState] = useState<TokenState>({ token: null, error: null });\n // Monotonic generation counter — incremented on every effect re-run.\n // In-flight mint promises capture their generation when they started;\n // a stale promise that resolves after props change won't overwrite\n // tokenState because its captured generation no longer matches.\n // The old `aliveRef = true` pattern flipped back on each re-run,\n // letting a stale resolve from the previous run pass the guard.\n const generationRef = useRef(0);\n const refreshHandleRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const mintOnce = async (gen: number): Promise<void> => {\n try {\n const next = await tunnel.mintIframeToken({ pathPrefixes: restPaths });\n if (gen !== generationRef.current) return;\n const ts = Date.parse(next.expiresAt);\n if (!Number.isFinite(ts)) {\n setTokenState({ token: null, error: 'Invalid token expiry from agent' });\n return;\n }\n setTokenState({ token: next, error: null });\n } catch (err) {\n if (gen !== generationRef.current) return;\n setTokenState({ token: null, error: err instanceof Error ? err.message : String(err) });\n }\n };\n\n // Initial mint + refresh scheduling lifecycle.\n useEffect(() => {\n generationRef.current += 1;\n const gen = generationRef.current;\n void mintOnce(gen);\n return () => {\n // Bumping the generation invalidates any in-flight promise that\n // started under this run; their resolve handler sees the new\n // generation and bails.\n generationRef.current += 1;\n if (refreshHandleRef.current !== null) {\n clearTimeout(refreshHandleRef.current);\n refreshHandleRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [tunnel.tunnelUrl, tunnel.profile, restPathsKey]);\n\n // Schedule a refresh ~60s before expiry. Refresh failure does NOT\n // null the existing token — keeping it lets the iframe stay\n // mounted while we retry, instead of silently unmounting.\n useEffect(() => {\n const current = tokenState.token;\n if (!current) return undefined;\n const expiresAtMs = Date.parse(current.expiresAt);\n if (!Number.isFinite(expiresAtMs)) return undefined;\n const delay = Math.max(5_000, expiresAtMs - Date.now() - TOKEN_REFRESH_LEEWAY_MS);\n const gen = generationRef.current;\n refreshHandleRef.current = setTimeout(() => {\n void tunnel\n .mintIframeToken({ pathPrefixes: restPaths })\n .then((next) => {\n if (gen !== generationRef.current) return;\n const ts = Date.parse(next.expiresAt);\n if (!Number.isFinite(ts)) return;\n setTokenState({ token: next, error: null });\n })\n .catch(() => {\n // Keep the existing token in place + retry in a short window.\n if (gen !== generationRef.current) return;\n if (refreshHandleRef.current !== null) clearTimeout(refreshHandleRef.current);\n refreshHandleRef.current = setTimeout(() => {\n void mintOnce(gen);\n }, TOKEN_REFRESH_RETRY_MS);\n });\n }, delay);\n return () => {\n if (refreshHandleRef.current !== null) {\n clearTimeout(refreshHandleRef.current);\n refreshHandleRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [tokenState.token, restPathsKey, tunnel]);\n\n if (tokenState.error && !tokenState.token) {\n // Retry uses the current generation so subsequent state changes\n // (tab switch / agent change) cancel a stale retry click.\n const retry = () => void mintOnce(generationRef.current);\n if (errorNode) {\n return <>{errorNode({ message: tokenState.error, retry })}</>;\n }\n return (\n <div className=\"p-4 text-sm text-status-danger-text\">\n Failed to load {contribution.contribution.title}: {tokenState.error}{' '}\n <button\n type=\"button\"\n onClick={retry}\n className=\"ml-2 underline text-text-secondary hover:text-text-primary\"\n >\n Retry\n </button>\n </div>\n );\n }\n\n if (!tokenState.token) {\n return (\n <>\n {loadingNode ?? (\n <div className=\"p-4 text-text-secondary\">Loading {contribution.contribution.title}…</div>\n )}\n </>\n );\n }\n\n const origin = (() => {\n try {\n return new URL(tunnel.tunnelUrl).origin;\n } catch {\n return tunnel.tunnelUrl;\n }\n })();\n const iframeSrc = `${tunnel.tunnelUrl.replace(/\\/$/, '')}${contribution.url}?vt=${encodeURIComponent(tokenState.token.token)}`;\n\n return (\n <PluginIframe\n src={iframeSrc}\n origin={origin}\n mountPoint={contribution.contribution.mountPoint}\n context={context}\n agentId={agentId}\n profile={profile}\n tunnelUrl={tunnel.tunnelUrl}\n iframeToken={tokenState.token.token}\n capabilities={contribution.capabilities}\n theme={theme}\n locale={locale}\n featureFlags={featureFlags}\n hostVersion={hostVersion}\n title={contribution.contribution.title}\n style={style ?? { width: '100%', height: '100%' }}\n onRpcRequest={async () => {\n throw Object.assign(new Error('Host RPC not yet implemented'), {\n code: 'method_not_found',\n });\n }}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PluginIframeMount.js","names":[],"sources":["../../../src/components/plugins/PluginIframeMount.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type CSSProperties } from 'react';\n\nimport {\n PluginIframe,\n type DiscoveredContribution,\n type MountTunnel,\n} from '@burdenoff/fe-libs/shared/plugins';\n\nimport { appendIframeTokenFragment, type IframeTokenResponse } from '@/services/iframeToken';\n\nconst TOKEN_REFRESH_LEEWAY_MS = 60_000;\nconst TOKEN_REFRESH_RETRY_MS = 30_000;\n\nexport interface PluginIframeMountProps {\n contribution: DiscoveredContribution;\n tunnel: MountTunnel;\n restPaths: string[];\n /** Per-page context the plugin reads via `host:init`. */\n context: Record<string, unknown>;\n /** Canonical agent id surfaced in `host:init`. */\n agentId: string;\n /** Profile that owns the iframe token (must match `tunnel.profile`). */\n profile: string;\n theme: { colorMode: 'light' | 'dark'; tokens: Record<string, string> };\n locale: string;\n featureFlags: Record<string, boolean>;\n /** Real version string. Don't reuse an entity id here. */\n hostVersion: string;\n /** Render-prop override for the loading state. */\n loadingNode?: React.ReactNode;\n /** Render-prop override for the error state. Receives the error message + a retry callback. */\n errorNode?: (error: { message: string; retry: () => void }) => React.ReactNode;\n style?: CSSProperties;\n}\n\ninterface TokenState {\n token: IframeTokenResponse | null;\n error: string | null;\n}\n\n/**\n * Shared iframe-mount helper used by the Vibe + Agent detail pages\n * and the standalone `/plugin/:pluginKey` route. Owns the per-\n * contribution scoped-token lifecycle (initial mint + refresh ~60s\n * before expiry + retry-on-failure) and renders the iframe via the\n * fe-libs `<PluginIframe>` primitive.\n *\n * Behaviour:\n * - Initial mint failure surfaces an error state with a retry\n * button (no infinite \"Loading…\" spinner).\n * - Refresh failure keeps the existing token in place + retries\n * after a short delay; the iframe doesn't unmount.\n * - Component-level unmount cancels all in-flight + scheduled work\n * via the alive ref.\n */\nexport function PluginIframeMount(props: PluginIframeMountProps) {\n const {\n contribution,\n tunnel,\n restPaths,\n context,\n agentId,\n profile,\n theme,\n locale,\n featureFlags,\n hostVersion,\n loadingNode,\n errorNode,\n style,\n } = props;\n const restPathsKey = restPaths.join('|');\n const [tokenState, setTokenState] = useState<TokenState>({ token: null, error: null });\n // Monotonic generation counter — incremented on every effect re-run.\n // In-flight mint promises capture their generation when they started;\n // a stale promise that resolves after props change won't overwrite\n // tokenState because its captured generation no longer matches.\n // The old `aliveRef = true` pattern flipped back on each re-run,\n // letting a stale resolve from the previous run pass the guard.\n const generationRef = useRef(0);\n const refreshHandleRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const mintOnce = async (gen: number): Promise<void> => {\n try {\n const next = await tunnel.mintIframeToken({ pathPrefixes: restPaths });\n if (gen !== generationRef.current) return;\n const ts = Date.parse(next.expiresAt);\n if (!Number.isFinite(ts)) {\n setTokenState({ token: null, error: 'Invalid token expiry from agent' });\n return;\n }\n setTokenState({ token: next, error: null });\n } catch (err) {\n if (gen !== generationRef.current) return;\n setTokenState({ token: null, error: err instanceof Error ? err.message : String(err) });\n }\n };\n\n // Initial mint + refresh scheduling lifecycle.\n useEffect(() => {\n generationRef.current += 1;\n const gen = generationRef.current;\n void mintOnce(gen);\n return () => {\n // Bumping the generation invalidates any in-flight promise that\n // started under this run; their resolve handler sees the new\n // generation and bails.\n generationRef.current += 1;\n if (refreshHandleRef.current !== null) {\n clearTimeout(refreshHandleRef.current);\n refreshHandleRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [tunnel.tunnelUrl, tunnel.profile, restPathsKey]);\n\n // Schedule a refresh ~60s before expiry. Refresh failure does NOT\n // null the existing token — keeping it lets the iframe stay\n // mounted while we retry, instead of silently unmounting.\n useEffect(() => {\n const current = tokenState.token;\n if (!current) return undefined;\n const expiresAtMs = Date.parse(current.expiresAt);\n if (!Number.isFinite(expiresAtMs)) return undefined;\n const delay = Math.max(5_000, expiresAtMs - Date.now() - TOKEN_REFRESH_LEEWAY_MS);\n const gen = generationRef.current;\n refreshHandleRef.current = setTimeout(() => {\n void tunnel\n .mintIframeToken({ pathPrefixes: restPaths })\n .then((next) => {\n if (gen !== generationRef.current) return;\n const ts = Date.parse(next.expiresAt);\n if (!Number.isFinite(ts)) return;\n setTokenState({ token: next, error: null });\n })\n .catch(() => {\n // Keep the existing token in place + retry in a short window.\n if (gen !== generationRef.current) return;\n if (refreshHandleRef.current !== null) clearTimeout(refreshHandleRef.current);\n refreshHandleRef.current = setTimeout(() => {\n void mintOnce(gen);\n }, TOKEN_REFRESH_RETRY_MS);\n });\n }, delay);\n return () => {\n if (refreshHandleRef.current !== null) {\n clearTimeout(refreshHandleRef.current);\n refreshHandleRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [tokenState.token, restPathsKey, tunnel]);\n\n if (tokenState.error && !tokenState.token) {\n // Retry uses the current generation so subsequent state changes\n // (tab switch / agent change) cancel a stale retry click.\n const retry = () => void mintOnce(generationRef.current);\n if (errorNode) {\n return <>{errorNode({ message: tokenState.error, retry })}</>;\n }\n return (\n <div className=\"p-4 text-sm text-status-danger-text\">\n Failed to load {contribution.contribution.title}: {tokenState.error}{' '}\n <button\n type=\"button\"\n onClick={retry}\n className=\"ml-2 underline text-text-secondary hover:text-text-primary\"\n >\n Retry\n </button>\n </div>\n );\n }\n\n if (!tokenState.token) {\n return (\n <>\n {loadingNode ?? (\n <div className=\"p-4 text-text-secondary\">Loading {contribution.contribution.title}…</div>\n )}\n </>\n );\n }\n\n const origin = (() => {\n try {\n return new URL(tunnel.tunnelUrl).origin;\n } catch {\n return tunnel.tunnelUrl;\n }\n })();\n // F5 — token rides the URL fragment, not the query string. The agent's\n // bootstrap shim at /ui/<plugin> reads `#vt=` client-side and trades it\n // for a path-scoped HttpOnly cookie via POST /api/agent/ui-exchange.\n const iframeSrc = appendIframeTokenFragment(\n `${tunnel.tunnelUrl.replace(/\\/$/, '')}${contribution.url}`,\n tokenState.token.token\n );\n\n return (\n <PluginIframe\n src={iframeSrc}\n origin={origin}\n mountPoint={contribution.contribution.mountPoint}\n context={context}\n agentId={agentId}\n profile={profile}\n tunnelUrl={tunnel.tunnelUrl}\n iframeToken={tokenState.token.token}\n capabilities={contribution.capabilities}\n theme={theme}\n locale={locale}\n featureFlags={featureFlags}\n hostVersion={hostVersion}\n title={contribution.contribution.title}\n style={style ?? { width: '100%', height: '100%' }}\n onRpcRequest={async () => {\n throw Object.assign(new Error('Host RPC not yet implemented'), {\n code: 'method_not_found',\n });\n }}\n />\n );\n}\n"],"mappings":"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"}
|