@opengeni/react 2.0.1 → 2.0.2-canary.38789554071001
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/artifacts.js +4 -2
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-RZH5JVFZ.js → browser-viewer-M7BIVFTW.js} +3 -3
- package/dist/{chat-interactive-block-UCACTZIE.js → chat-interactive-block-BO2IWXVK.js} +3 -3
- package/dist/{chunk-NIIXN66Z.js → chunk-5XEXTWEM.js} +2 -2
- package/dist/{chunk-4JUSFDTA.js → chunk-6JFN3OWG.js} +28 -225
- package/dist/chunk-6JFN3OWG.js.map +1 -0
- package/dist/{chunk-BSIADJ4U.js → chunk-6SNDCUPB.js} +2 -2
- package/dist/{chunk-RRYCD3QD.js → chunk-AMVCNAGC.js} +992 -924
- package/dist/chunk-AMVCNAGC.js.map +1 -0
- package/dist/{chunk-ERLG2X3U.js → chunk-CORUP6JB.js} +3 -2
- package/dist/chunk-CORUP6JB.js.map +1 -0
- package/dist/{chunk-6JUSXDF4.js → chunk-HBXL6HWG.js} +2 -2
- package/dist/{chunk-HQN5AZFY.js → chunk-HGKELH6Z.js} +12 -3
- package/dist/{chunk-HQN5AZFY.js.map → chunk-HGKELH6Z.js.map} +1 -1
- package/dist/{chunk-QWA4BHC6.js → chunk-ISN726VG.js} +2 -2
- package/dist/{chunk-QQ5UGGGE.js → chunk-KZENYIA2.js} +16 -2
- package/dist/{chunk-QQ5UGGGE.js.map → chunk-KZENYIA2.js.map} +1 -1
- package/dist/{chunk-25YZS7TK.js → chunk-O5GPRG76.js} +4 -4
- package/dist/{chunk-UPP6YD2R.js → chunk-PWCBMEV2.js} +4 -4
- package/dist/{chunk-X6KAA43Z.js → chunk-Q26UAIXI.js} +2 -2
- package/dist/{chunk-U7A22SY4.js → chunk-TIHSXNBC.js} +6 -2
- package/dist/{chunk-U7A22SY4.js.map → chunk-TIHSXNBC.js.map} +1 -1
- package/dist/{chunk-2U54CHR5.js → chunk-VIUEOHXY.js} +3 -2
- package/dist/chunk-VIUEOHXY.js.map +1 -0
- package/dist/components/artifacts/artifact-sandbox.d.ts +3 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +15 -51
- package/dist/components/artifacts/site-view.d.ts +4 -1
- package/dist/components/linkified-text.d.ts +18 -0
- package/dist/composer-messages.js +2 -2
- package/dist/composer.js +4 -4
- package/dist/connect.js +2 -2
- package/dist/index.js +12 -12
- package/dist/interaction.js +2 -2
- package/dist/{latest-question-6WZQV23L.js → latest-question-CHE7E753.js} +3 -3
- package/dist/lib/format.d.ts +7 -0
- package/dist/machines.js +2 -2
- package/dist/{platform-activity-row-UQDQDX4C.js → platform-activity-row-5OCKOQWZ.js} +2 -2
- package/dist/session-ui.js +10 -10
- package/dist/session.js +4 -4
- package/dist/sites.js +1 -1
- package/dist/timeline/types.d.ts +1 -1
- package/dist/timeline-model.js +3 -3
- package/package.json +3 -3
- package/src/components/artifacts/artifact-sandbox.tsx +4 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +48 -321
- package/src/components/artifacts/site-view.tsx +5 -0
- package/src/components/human-input-form.tsx +19 -13
- package/src/components/linkified-text.tsx +79 -0
- package/src/components/message-timeline.tsx +4 -3
- package/src/hooks/use-machines.ts +4 -0
- package/src/lib/format.ts +25 -1
- package/src/timeline/platform-activity-presentation.ts +1 -0
- package/src/timeline/projection.ts +18 -1
- package/src/timeline/types.ts +1 -0
- package/dist/chunk-2U54CHR5.js.map +0 -1
- package/dist/chunk-4JUSFDTA.js.map +0 -1
- package/dist/chunk-ERLG2X3U.js.map +0 -1
- package/dist/chunk-RRYCD3QD.js.map +0 -1
- /package/dist/{browser-viewer-RZH5JVFZ.js.map → browser-viewer-M7BIVFTW.js.map} +0 -0
- /package/dist/{chat-interactive-block-UCACTZIE.js.map → chat-interactive-block-BO2IWXVK.js.map} +0 -0
- /package/dist/{chunk-NIIXN66Z.js.map → chunk-5XEXTWEM.js.map} +0 -0
- /package/dist/{chunk-BSIADJ4U.js.map → chunk-6SNDCUPB.js.map} +0 -0
- /package/dist/{chunk-6JUSXDF4.js.map → chunk-HBXL6HWG.js.map} +0 -0
- /package/dist/{chunk-QWA4BHC6.js.map → chunk-ISN726VG.js.map} +0 -0
- /package/dist/{chunk-25YZS7TK.js.map → chunk-O5GPRG76.js.map} +0 -0
- /package/dist/{chunk-UPP6YD2R.js.map → chunk-PWCBMEV2.js.map} +0 -0
- /package/dist/{chunk-X6KAA43Z.js.map → chunk-Q26UAIXI.js.map} +0 -0
- /package/dist/{latest-question-6WZQV23L.js.map → latest-question-CHE7E753.js.map} +0 -0
- /package/dist/{platform-activity-row-UQDQDX4C.js.map → platform-activity-row-5OCKOQWZ.js.map} +0 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SparklesIcon,
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
|
10
|
+
import type { SiteViewer } from "@opengeni/sdk";
|
|
10
11
|
|
|
11
12
|
import { cn } from "../../lib/cn";
|
|
12
13
|
import { ArtifactBadge, ArtifactButton, useArtifactLabels } from "./artifact-chrome";
|
|
@@ -30,6 +31,8 @@ export type ArtifactSandboxProps = {
|
|
|
30
31
|
editDisabled?: boolean | undefined;
|
|
31
32
|
onEdit?: (() => void) | undefined;
|
|
32
33
|
toolBridge?: PublishedHtmlArtifactToolBridge | undefined;
|
|
34
|
+
/** Who is viewing, answered to the Site's `site.viewer` over the bridge. */
|
|
35
|
+
viewer?: SiteViewer | undefined;
|
|
33
36
|
connectedToolCount?: number | undefined;
|
|
34
37
|
sourceFileCount?: number | undefined;
|
|
35
38
|
fill?: boolean | undefined;
|
|
@@ -174,6 +177,7 @@ export function ArtifactSandbox(props: ArtifactSandboxProps) {
|
|
|
174
177
|
{...(props.theme ? { theme: props.theme } : {})}
|
|
175
178
|
{...(!focused && props.height ? { style: { height: props.height } } : {})}
|
|
176
179
|
{...(props.toolBridge ? { toolBridge: props.toolBridge } : {})}
|
|
180
|
+
{...(props.viewer ? { viewer: props.viewer } : {})}
|
|
177
181
|
className={cn(
|
|
178
182
|
"block h-[clamp(30rem,62vh,48rem)] w-full border-0 bg-white",
|
|
179
183
|
props.fill && "h-0 min-h-0 flex-1",
|
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
isOpenGeniSiteBridgeRequestMessage,
|
|
9
|
-
sanitizeOpenGeniSiteToolCallRequest,
|
|
10
|
-
sanitizeOpenGeniSiteToolInvokeRequest,
|
|
11
|
-
type OpenGeniSiteBridgeReadyMessage,
|
|
12
|
-
type OpenGeniSiteBridgeRequestMessage,
|
|
13
|
-
isSiteHttpRequest,
|
|
14
|
-
serveSiteHttp,
|
|
15
|
-
type SiteHttpRequest,
|
|
16
|
-
type OpenGeniSiteBridgeResponseMessage,
|
|
17
|
-
type OpenGeniSiteToolCatalog,
|
|
18
|
-
} from "@opengeni/sdk/site";
|
|
19
|
-
import type {
|
|
20
|
-
ToolGatewayCallRequest,
|
|
21
|
-
ToolGatewayCallResponse,
|
|
22
|
-
ToolGatewayDeclarationsResponse,
|
|
23
|
-
ToolGatewayResolveRequest,
|
|
24
|
-
ToolGatewayResolvedTool,
|
|
25
|
-
ToolGatewayInvokeRequest,
|
|
26
|
-
ToolGatewayInvokeResponse,
|
|
27
|
-
} from "@opengeni/sdk";
|
|
2
|
+
SiteBridgeHost,
|
|
3
|
+
injectSiteBridgeBootstrap,
|
|
4
|
+
type SiteBridgeToolHandlers,
|
|
5
|
+
type SiteBridgeViewerHandlers,
|
|
6
|
+
} from "@opengeni/sdk/site-bridge-host";
|
|
7
|
+
import type { OpenGeniSiteSignInResult, OpenGeniSiteViewer } from "@opengeni/sdk/site";
|
|
28
8
|
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
|
29
9
|
import { resolveSiteClientScript } from "@opengeni/sdk/site-document";
|
|
30
10
|
|
|
11
|
+
export {
|
|
12
|
+
SiteBridgeDocumentLease,
|
|
13
|
+
SiteBridgeRequestRegistry,
|
|
14
|
+
handleSiteBridgeRequest,
|
|
15
|
+
openGeniSiteBridgePortFromBootstrap,
|
|
16
|
+
siteBridgeError,
|
|
17
|
+
siteBridgeReadyMessage,
|
|
18
|
+
} from "@opengeni/sdk/site-bridge-host";
|
|
19
|
+
|
|
31
20
|
export const PUBLISHED_HTML_ARTIFACT_IFRAME_SANDBOX = [
|
|
32
21
|
"allow-downloads",
|
|
33
22
|
"allow-forms",
|
|
@@ -48,68 +37,24 @@ export type PublishedHtmlArtifactFrameProps = {
|
|
|
48
37
|
className?: string;
|
|
49
38
|
style?: CSSProperties;
|
|
50
39
|
toolBridge?: PublishedHtmlArtifactToolBridge;
|
|
40
|
+
/**
|
|
41
|
+
* The signed-in member viewing the Site, answered to `site.viewer`. When
|
|
42
|
+
* omitted the frame does not advertise viewers and the SDK resolves `null`.
|
|
43
|
+
*/
|
|
44
|
+
viewer?: OpenGeniSiteViewer;
|
|
45
|
+
/**
|
|
46
|
+
* Answer `site.auth.signIn()`. The console viewer is already signed in, so
|
|
47
|
+
* the default resolves `{ started: false }`.
|
|
48
|
+
*/
|
|
49
|
+
onSignIn?: () => Promise<OpenGeniSiteSignInResult> | OpenGeniSiteSignInResult;
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
request: ToolGatewayResolveRequest,
|
|
56
|
-
options: { signal: AbortSignal },
|
|
57
|
-
) => Promise<ToolGatewayResolvedTool>;
|
|
58
|
-
invoke?: (
|
|
59
|
-
request: ToolGatewayInvokeRequest,
|
|
60
|
-
options: { signal: AbortSignal },
|
|
61
|
-
) => Promise<ToolGatewayInvokeResponse>;
|
|
62
|
-
fetch?: (request: SiteHttpRequest, signal: AbortSignal) => Promise<Response>;
|
|
63
|
-
/** Advertise Site Data: `fetch` forwards `published-artifacts/current` routes to the pinned Site. */
|
|
64
|
-
siteData?: boolean;
|
|
65
|
-
catalog: (options: { signal: AbortSignal }) => Promise<OpenGeniSiteToolCatalog>;
|
|
66
|
-
call: (
|
|
67
|
-
request: ToolGatewayCallRequest,
|
|
68
|
-
options: { signal: AbortSignal },
|
|
69
|
-
) => Promise<ToolGatewayCallResponse>;
|
|
70
|
-
declarations?: (options: { signal: AbortSignal }) => Promise<ToolGatewayDeclarationsResponse>;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/** Capabilities the host advertises when a Site connects; Sites never ask for more. */
|
|
74
|
-
export function siteBridgeReadyMessage(
|
|
75
|
-
bridge: PublishedHtmlArtifactToolBridge | undefined,
|
|
76
|
-
): OpenGeniSiteBridgeReadyMessage {
|
|
77
|
-
return {
|
|
78
|
-
type: OPENGENI_SITE_BRIDGE_READY,
|
|
79
|
-
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
80
|
-
...(bridge?.resolve && bridge?.invoke ? { targetTools: 1 as const } : {}),
|
|
81
|
-
...(bridge?.fetch && bridge.siteData ? { siteData: 1 as const } : {}),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const SITE_BRIDGE_BOOTSTRAP_SCRIPT = `<script>(()=>{const key=${JSON.stringify(
|
|
86
|
-
OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL,
|
|
87
|
-
)};const parentWindow=window.parent;if(parentWindow===window)return;window.addEventListener("message",event=>{const data=event.data;if(event.source!==parentWindow||!data||data.type!==${JSON.stringify(
|
|
88
|
-
OPENGENI_SITE_BRIDGE_READY,
|
|
89
|
-
)}||data.version!==${OPENGENI_SITE_BRIDGE_VERSION}||event.ports.length!==1)return;const port=event.ports[0];const previous=window[key]?.port;if(previous&&previous!==port)previous.close();Object.defineProperty(window,key,{configurable:true,value:{port}});port.start();});})();</script>`;
|
|
52
|
+
/** Host handlers for Site tools and the Site HTTP channel (see `@opengeni/sdk/site-bridge-host`). */
|
|
53
|
+
export type PublishedHtmlArtifactToolBridge = SiteBridgeToolHandlers;
|
|
90
54
|
|
|
91
55
|
export function publishedHtmlArtifactDocument(html: string, toolBridgeEnabled: boolean): string {
|
|
92
56
|
if (!toolBridgeEnabled) return html;
|
|
93
|
-
|
|
94
|
-
const doctypeEnd = leadingDoctypeEnd(html);
|
|
95
|
-
if (doctypeEnd >= 0) {
|
|
96
|
-
return `${html.slice(0, doctypeEnd + 1)}${SITE_BRIDGE_BOOTSTRAP_SCRIPT}${html.slice(doctypeEnd + 1)}`;
|
|
97
|
-
}
|
|
98
|
-
return `${SITE_BRIDGE_BOOTSTRAP_SCRIPT}${html}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function leadingDoctypeEnd(html: string): number {
|
|
102
|
-
const lower = html.toLowerCase();
|
|
103
|
-
let cursor = html.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
104
|
-
while (cursor < html.length) {
|
|
105
|
-
while (/\s/.test(html[cursor] ?? "")) cursor += 1;
|
|
106
|
-
if (!html.startsWith("<!--", cursor)) break;
|
|
107
|
-
const commentEnd = html.indexOf("-->", cursor + 4);
|
|
108
|
-
if (commentEnd < 0) return -1;
|
|
109
|
-
cursor = commentEnd + 3;
|
|
110
|
-
}
|
|
111
|
-
if (!lower.startsWith("<!doctype", cursor)) return -1;
|
|
112
|
-
return html.indexOf(">", cursor + 9);
|
|
57
|
+
return injectSiteBridgeBootstrap(resolveSiteClientScript(html));
|
|
113
58
|
}
|
|
114
59
|
|
|
115
60
|
/**
|
|
@@ -143,94 +88,29 @@ export function PublishedHtmlArtifactFrame(props: PublishedHtmlArtifactFrameProp
|
|
|
143
88
|
);
|
|
144
89
|
}, [props.theme]);
|
|
145
90
|
const bridgeRef = useRef(props.toolBridge);
|
|
91
|
+
const viewerRef = useRef(siteViewerHandlers(props.viewer, props.onSignIn));
|
|
146
92
|
const onFrameLoadRef = useRef<() => void>(() => undefined);
|
|
147
93
|
const frameLoadPendingRef = useRef(false);
|
|
148
|
-
|
|
94
|
+
// The bridge (and its bootstrap script) exists only when the host offers
|
|
95
|
+
// something over it: tools/Site HTTP, or the viewer.
|
|
96
|
+
const toolBridgeEnabled = props.toolBridge !== undefined || props.viewer !== undefined;
|
|
149
97
|
useEffect(() => {
|
|
150
98
|
bridgeRef.current = props.toolBridge;
|
|
151
99
|
}, [props.toolBridge]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
viewerRef.current = siteViewerHandlers(props.viewer, props.onSignIn);
|
|
102
|
+
}, [props.viewer, props.onSignIn]);
|
|
152
103
|
useEffect(() => {
|
|
153
104
|
if (!toolBridgeEnabled || typeof window === "undefined") return;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
requests.addPort(port);
|
|
159
|
-
port.addEventListener("message", (portEvent: MessageEvent<unknown>) => {
|
|
160
|
-
const message = portEvent.data;
|
|
161
|
-
if (isOpenGeniSiteBridgeCancelMessage(message)) {
|
|
162
|
-
requests.cancel(port, message.requestId);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (isSiteHttpRequest(message) && portEvent.ports.length === 1) {
|
|
166
|
-
const controller = requests.start(port, message.requestId);
|
|
167
|
-
if (!controller) {
|
|
168
|
-
portEvent.ports[0]!.close();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
void serveSiteHttp(
|
|
172
|
-
message,
|
|
173
|
-
portEvent.ports[0]!,
|
|
174
|
-
async (request, signal) => {
|
|
175
|
-
const fetch = bridgeRef.current?.fetch;
|
|
176
|
-
if (!fetch) throw new Error("This Site host does not support the session SDK");
|
|
177
|
-
return fetch(request, signal);
|
|
178
|
-
},
|
|
179
|
-
controller.signal,
|
|
180
|
-
).finally(() => requests.complete(port, message.requestId, controller));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (!isOpenGeniSiteBridgeRequestMessage(message)) return;
|
|
184
|
-
const controller = requests.start(port, message.requestId);
|
|
185
|
-
if (!controller) {
|
|
186
|
-
port.postMessage({
|
|
187
|
-
type: OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
188
|
-
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
189
|
-
requestId: message.requestId,
|
|
190
|
-
ok: false,
|
|
191
|
-
error: {
|
|
192
|
-
code: "duplicate_request_id",
|
|
193
|
-
message: "A Site tool request with this id is already running",
|
|
194
|
-
retryable: false,
|
|
195
|
-
},
|
|
196
|
-
} satisfies OpenGeniSiteBridgeResponseMessage);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
void handleSiteBridgeRequest(bridgeRef.current, message, controller.signal)
|
|
200
|
-
.then((value) => {
|
|
201
|
-
if (!controller.signal.aborted) {
|
|
202
|
-
port.postMessage({
|
|
203
|
-
type: OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
204
|
-
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
205
|
-
requestId: message.requestId,
|
|
206
|
-
ok: true,
|
|
207
|
-
value,
|
|
208
|
-
} satisfies OpenGeniSiteBridgeResponseMessage);
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
.catch((error) => {
|
|
212
|
-
if (!controller.signal.aborted) {
|
|
213
|
-
port.postMessage({
|
|
214
|
-
type: OPENGENI_SITE_BRIDGE_RESPONSE,
|
|
215
|
-
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
216
|
-
requestId: message.requestId,
|
|
217
|
-
ok: false,
|
|
218
|
-
error: siteBridgeError(error, message.method === "call"),
|
|
219
|
-
} satisfies OpenGeniSiteBridgeResponseMessage);
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
.finally(() => requests.complete(port, message.requestId, controller));
|
|
223
|
-
});
|
|
224
|
-
port.addEventListener("messageerror", () => requests.closePort(port));
|
|
225
|
-
port.start();
|
|
226
|
-
port.postMessage(siteBridgeReadyMessage(bridgeRef.current));
|
|
227
|
-
};
|
|
228
|
-
const documentLease = new SiteBridgeDocumentLease(attachToolPort, () => requests.closeAll());
|
|
105
|
+
const host = new SiteBridgeHost({
|
|
106
|
+
tools: () => bridgeRef.current,
|
|
107
|
+
viewer: () => viewerRef.current,
|
|
108
|
+
});
|
|
229
109
|
const onFrameLoad = () => {
|
|
230
110
|
frameLoadPendingRef.current = false;
|
|
231
111
|
const frameWindow = frameRef.current?.contentWindow ?? null;
|
|
232
112
|
if (!frameWindow) return;
|
|
233
|
-
|
|
113
|
+
host.load(frameWindow);
|
|
234
114
|
};
|
|
235
115
|
onFrameLoadRef.current = onFrameLoad;
|
|
236
116
|
if (frameLoadPendingRef.current) {
|
|
@@ -239,7 +119,7 @@ export function PublishedHtmlArtifactFrame(props: PublishedHtmlArtifactFrameProp
|
|
|
239
119
|
}
|
|
240
120
|
return () => {
|
|
241
121
|
onFrameLoadRef.current = () => undefined;
|
|
242
|
-
|
|
122
|
+
host.close();
|
|
243
123
|
};
|
|
244
124
|
}, [props.html, toolBridgeEnabled]);
|
|
245
125
|
return (
|
|
@@ -263,166 +143,13 @@ export function PublishedHtmlArtifactFrame(props: PublishedHtmlArtifactFrameProp
|
|
|
263
143
|
);
|
|
264
144
|
}
|
|
265
145
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// not another client connecting, revokes all of that document's ports.
|
|
272
|
-
if (!this.controllersByPort.has(port)) this.controllersByPort.set(port, new Map());
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
start(port: MessagePort, requestId: string): AbortController | null {
|
|
276
|
-
const controllers = this.controllersByPort.get(port);
|
|
277
|
-
if (!controllers || controllers.has(requestId)) return null;
|
|
278
|
-
const controller = new AbortController();
|
|
279
|
-
controllers.set(requestId, controller);
|
|
280
|
-
return controller;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
cancel(port: MessagePort, requestId: string): void {
|
|
284
|
-
const controllers = this.controllersByPort.get(port);
|
|
285
|
-
const controller = controllers?.get(requestId);
|
|
286
|
-
if (!controller) return;
|
|
287
|
-
controller.abort(new Error("Site tool request cancelled"));
|
|
288
|
-
controllers!.delete(requestId);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
complete(port: MessagePort, requestId: string, controller: AbortController): void {
|
|
292
|
-
const controllers = this.controllersByPort.get(port);
|
|
293
|
-
if (controllers?.get(requestId) === controller) controllers.delete(requestId);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
closePort(port: MessagePort): void {
|
|
297
|
-
const controllers = this.controllersByPort.get(port);
|
|
298
|
-
if (!controllers) return;
|
|
299
|
-
for (const controller of controllers.values()) {
|
|
300
|
-
controller.abort(new Error("Site tool bridge port closed"));
|
|
301
|
-
}
|
|
302
|
-
controllers.clear();
|
|
303
|
-
this.controllersByPort.delete(port);
|
|
304
|
-
port.close();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
closeAll(): void {
|
|
308
|
-
for (const port of [...this.controllersByPort.keys()]) this.closePort(port);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export class SiteBridgeDocumentLease {
|
|
313
|
-
private loaded = false;
|
|
314
|
-
private bootstrapPort: MessagePort | null = null;
|
|
315
|
-
|
|
316
|
-
constructor(
|
|
317
|
-
private readonly attachToolPort: (data: unknown, ports: readonly MessagePort[]) => void,
|
|
318
|
-
private readonly closeActivePorts: () => void,
|
|
319
|
-
private readonly createMessageChannel: () => MessageChannel = () => new MessageChannel(),
|
|
320
|
-
) {}
|
|
321
|
-
|
|
322
|
-
load(frameWindow: Pick<Window, "postMessage">): boolean {
|
|
323
|
-
if (this.loaded) {
|
|
324
|
-
this.close();
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
this.loaded = true;
|
|
328
|
-
const channel = this.createMessageChannel();
|
|
329
|
-
this.bootstrapPort = channel.port1;
|
|
330
|
-
channel.port1.addEventListener("message", (event: MessageEvent<unknown>) => {
|
|
331
|
-
this.attachToolPort(event.data, event.ports);
|
|
332
|
-
});
|
|
333
|
-
channel.port1.start();
|
|
334
|
-
frameWindow.postMessage(
|
|
335
|
-
{
|
|
336
|
-
type: OPENGENI_SITE_BRIDGE_READY,
|
|
337
|
-
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
338
|
-
},
|
|
339
|
-
"*",
|
|
340
|
-
[channel.port2],
|
|
341
|
-
);
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
close(): void {
|
|
346
|
-
this.bootstrapPort?.close();
|
|
347
|
-
this.bootstrapPort = null;
|
|
348
|
-
this.closeActivePorts();
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function openGeniSiteBridgePortFromBootstrap(
|
|
353
|
-
data: unknown,
|
|
354
|
-
ports: readonly MessagePort[],
|
|
355
|
-
): MessagePort | null {
|
|
356
|
-
if (!isOpenGeniSiteBridgeConnectMessage(data)) return null;
|
|
357
|
-
return ports.length === 1 ? ports[0]! : null;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export async function handleSiteBridgeRequest(
|
|
361
|
-
bridge: PublishedHtmlArtifactToolBridge | undefined,
|
|
362
|
-
message: OpenGeniSiteBridgeRequestMessage,
|
|
363
|
-
signal: AbortSignal,
|
|
364
|
-
): Promise<
|
|
365
|
-
| OpenGeniSiteToolCatalog
|
|
366
|
-
| ToolGatewayCallResponse
|
|
367
|
-
| ToolGatewayDeclarationsResponse
|
|
368
|
-
| ToolGatewayResolvedTool
|
|
369
|
-
| ToolGatewayInvokeResponse
|
|
370
|
-
> {
|
|
371
|
-
if (!bridge) throw new Error("Site tool bridge is unavailable");
|
|
372
|
-
if (message.method === "resolve") {
|
|
373
|
-
if (!bridge.resolve) throw new Error("Targeted Site tools are unavailable");
|
|
374
|
-
return await bridge.resolve({ target: message.payload.target }, { signal });
|
|
375
|
-
}
|
|
376
|
-
if (message.method === "invoke") {
|
|
377
|
-
if (!bridge.invoke) throw new Error("Targeted Site tools are unavailable");
|
|
378
|
-
return await bridge.invoke(sanitizeOpenGeniSiteToolInvokeRequest(message.payload), { signal });
|
|
379
|
-
}
|
|
380
|
-
if (message.method === "catalog") return await bridge.catalog({ signal });
|
|
381
|
-
if (message.method === "declarations") {
|
|
382
|
-
if (!bridge.declarations) throw new Error("Site tool declarations are unavailable");
|
|
383
|
-
return await bridge.declarations({ signal });
|
|
384
|
-
}
|
|
385
|
-
return await bridge.call(sanitizeOpenGeniSiteToolCallRequest(message.payload!), { signal });
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export function siteBridgeError(
|
|
389
|
-
error: unknown,
|
|
390
|
-
legacyCall = false,
|
|
391
|
-
): {
|
|
392
|
-
code: string;
|
|
393
|
-
message: string;
|
|
394
|
-
retryable: boolean;
|
|
395
|
-
outcomeUnknown: boolean;
|
|
396
|
-
} {
|
|
397
|
-
const candidate = error as {
|
|
398
|
-
code?: unknown;
|
|
399
|
-
message?: unknown;
|
|
400
|
-
retryable?: unknown;
|
|
401
|
-
outcomeUnknown?: unknown;
|
|
402
|
-
details?: { code?: unknown };
|
|
403
|
-
};
|
|
404
|
-
const nestedCode = candidate?.details?.code;
|
|
405
|
-
const knownPreexecution = candidate?.retryable === true && candidate?.outcomeUnknown === false;
|
|
406
|
-
const stale =
|
|
407
|
-
knownPreexecution && (nestedCode === "tool_definition_stale" || nestedCode === "catalog_stale");
|
|
408
|
-
// Historical saved bundles inspect code alone. A stale-named transport error
|
|
409
|
-
// without known pre-execution proof must remain an error, not invite replay.
|
|
410
|
-
const unsafeControl =
|
|
411
|
-
legacyCall &&
|
|
412
|
-
!knownPreexecution &&
|
|
413
|
-
(candidate?.code === "catalog_stale" || candidate?.code === "tool_definition_stale");
|
|
414
|
-
const message =
|
|
415
|
-
typeof candidate?.message === "string" ? candidate.message : "Site tool request failed";
|
|
146
|
+
function siteViewerHandlers(
|
|
147
|
+
viewer: OpenGeniSiteViewer | undefined,
|
|
148
|
+
onSignIn: PublishedHtmlArtifactFrameProps["onSignIn"],
|
|
149
|
+
): SiteBridgeViewerHandlers | undefined {
|
|
150
|
+
if (!viewer) return undefined;
|
|
416
151
|
return {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
: unsafeControl
|
|
420
|
-
? "site_tool_call_failed"
|
|
421
|
-
: typeof candidate?.code === "string"
|
|
422
|
-
? candidate.code
|
|
423
|
-
: "site_tool_call_failed",
|
|
424
|
-
message: unsafeControl ? `[${candidate.code}] ${message}` : message,
|
|
425
|
-
retryable: candidate?.retryable === true,
|
|
426
|
-
outcomeUnknown: candidate?.outcomeUnknown === true,
|
|
152
|
+
viewer: () => viewer,
|
|
153
|
+
signIn: async () => (onSignIn ? await onSignIn() : { started: false }),
|
|
427
154
|
};
|
|
428
155
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { SiteViewer } from "@opengeni/sdk";
|
|
2
3
|
|
|
3
4
|
import { loadSiteSnapshot } from "../../sites-ui";
|
|
4
5
|
import {
|
|
@@ -18,6 +19,8 @@ export type SiteViewProps = {
|
|
|
18
19
|
siteId: string;
|
|
19
20
|
/** Host-owned Site API access. Omit it and the Site renders without workspace tools. */
|
|
20
21
|
toolBridge?: SiteToolBridgeFactory | undefined;
|
|
22
|
+
/** Who is viewing, answered to the Site's `site.viewer`. */
|
|
23
|
+
viewer?: SiteViewer | undefined;
|
|
21
24
|
theme?: "light" | "dark" | undefined;
|
|
22
25
|
/** Shown instead of the archived notice; the console links to its full page. */
|
|
23
26
|
archivedMessage?: string | undefined;
|
|
@@ -39,6 +42,7 @@ export function SiteView({
|
|
|
39
42
|
workspaceId,
|
|
40
43
|
siteId,
|
|
41
44
|
toolBridge,
|
|
45
|
+
viewer,
|
|
42
46
|
theme,
|
|
43
47
|
archivedMessage,
|
|
44
48
|
showTitle = true,
|
|
@@ -107,6 +111,7 @@ export function SiteView({
|
|
|
107
111
|
showTitle={showTitle}
|
|
108
112
|
versionLabel={`v${snapshot.detail.artifact.currentVersion?.revision}`}
|
|
109
113
|
toolBridge={bridge}
|
|
114
|
+
viewer={viewer}
|
|
110
115
|
connectedToolCount={content.requestedTools.length}
|
|
111
116
|
theme={theme}
|
|
112
117
|
fill
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type HumanInputFormMessages,
|
|
32
32
|
} from "../human-input-model";
|
|
33
33
|
import { SecretQuestionField, type SecretFieldHandle } from "./secret-question-field";
|
|
34
|
+
import { linkifyText } from "./linkified-text";
|
|
34
35
|
|
|
35
36
|
export {
|
|
36
37
|
answersFromDrafts,
|
|
@@ -503,8 +504,11 @@ function HumanInputRequestForm({
|
|
|
503
504
|
</span>
|
|
504
505
|
<div className="min-w-0 flex-1">
|
|
505
506
|
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
506
|
-
<h2
|
|
507
|
-
{
|
|
507
|
+
<h2
|
|
508
|
+
id={titleId}
|
|
509
|
+
className="min-w-0 text-og-md font-semibold text-og-fg [overflow-wrap:anywhere]"
|
|
510
|
+
>
|
|
511
|
+
{linkifyText(resolvedTitle)}
|
|
508
512
|
{singleQuestion?.required && !request.allowSkip && !decision ? (
|
|
509
513
|
<span aria-hidden className="ml-1 text-og-status-failed">
|
|
510
514
|
*
|
|
@@ -523,7 +527,9 @@ function HumanInputRequestForm({
|
|
|
523
527
|
) : null}
|
|
524
528
|
</div>
|
|
525
529
|
{resolvedDescription ? (
|
|
526
|
-
<div className="mt-0.5 text-og-sm text-og-fg-muted">
|
|
530
|
+
<div className="mt-0.5 text-og-sm text-og-fg-muted [overflow-wrap:anywhere]">
|
|
531
|
+
{linkifyText(resolvedDescription)}
|
|
532
|
+
</div>
|
|
527
533
|
) : null}
|
|
528
534
|
{request.expiresAt ? (
|
|
529
535
|
<p className="mt-1 text-og-xs text-og-fg-subtle">
|
|
@@ -557,9 +563,9 @@ function HumanInputRequestForm({
|
|
|
557
563
|
{decision.question.helpText ||
|
|
558
564
|
decision.accept.description ||
|
|
559
565
|
decision.decline.description ? (
|
|
560
|
-
<div className="shrink-0 space-y-1 px-4 pt-3 text-og-sm text-og-fg-muted">
|
|
566
|
+
<div className="shrink-0 space-y-1 px-4 pt-3 text-og-sm text-og-fg-muted [overflow-wrap:anywhere]">
|
|
561
567
|
{decision.question.label && decision.question.helpText ? (
|
|
562
|
-
<p>{decision.question.helpText}</p>
|
|
568
|
+
<p>{linkifyText(decision.question.helpText)}</p>
|
|
563
569
|
) : null}
|
|
564
570
|
{[decision.accept, decision.decline].map((option) =>
|
|
565
571
|
option.description ? (
|
|
@@ -866,13 +872,13 @@ function QuestionControls({
|
|
|
866
872
|
<label
|
|
867
873
|
id={labelId}
|
|
868
874
|
htmlFor={question.kind === "text" ? fieldId : undefined}
|
|
869
|
-
className="text-og-sm font-medium text-og-fg"
|
|
875
|
+
className="min-w-0 text-og-sm font-medium text-og-fg [overflow-wrap:anywhere]"
|
|
870
876
|
>
|
|
871
877
|
{questionNumber === null ? null : (
|
|
872
878
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{questionNumber}.</span>
|
|
873
879
|
)}
|
|
874
880
|
{questionNumber === null ? null : " "}
|
|
875
|
-
{visibleLabel}
|
|
881
|
+
{linkifyText(visibleLabel)}
|
|
876
882
|
{question.required && !allowSkip ? (
|
|
877
883
|
<span aria-hidden className="ml-1 text-og-status-failed">
|
|
878
884
|
*
|
|
@@ -885,13 +891,13 @@ function QuestionControls({
|
|
|
885
891
|
</label>
|
|
886
892
|
</div>
|
|
887
893
|
{question.label ? (
|
|
888
|
-
<p id={promptId} className="text-og-sm text-og-fg-muted">
|
|
889
|
-
{question.prompt}
|
|
894
|
+
<p id={promptId} className="text-og-sm text-og-fg-muted [overflow-wrap:anywhere]">
|
|
895
|
+
{linkifyText(question.prompt)}
|
|
890
896
|
</p>
|
|
891
897
|
) : null}
|
|
892
898
|
{question.helpText ? (
|
|
893
|
-
<p id={helpId} className="text-og-xs text-og-fg-subtle">
|
|
894
|
-
{question.helpText}
|
|
899
|
+
<p id={helpId} className="text-og-xs text-og-fg-subtle [overflow-wrap:anywhere]">
|
|
900
|
+
{linkifyText(question.helpText)}
|
|
895
901
|
</p>
|
|
896
902
|
) : null}
|
|
897
903
|
{hint ? (
|
|
@@ -906,8 +912,8 @@ function QuestionControls({
|
|
|
906
912
|
<p className="text-og-xs text-og-fg-subtle">{messages.optional}</p>
|
|
907
913
|
) : null}
|
|
908
914
|
{question.helpText && question.label ? (
|
|
909
|
-
<p id={helpId} className="mb-1.5 text-og-xs text-og-fg-subtle">
|
|
910
|
-
{question.helpText}
|
|
915
|
+
<p id={helpId} className="mb-1.5 text-og-xs text-og-fg-subtle [overflow-wrap:anywhere]">
|
|
916
|
+
{linkifyText(question.helpText)}
|
|
911
917
|
</p>
|
|
912
918
|
) : null}
|
|
913
919
|
{hint ? (
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Fragment, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
// Bare http(s) URLs in plain agent-written text (question prompts, help text).
|
|
4
|
+
const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
|
|
5
|
+
// Sentence punctuation that follows a URL is not part of it.
|
|
6
|
+
const TRAILING_PUNCTUATION = /[.,;:!?]+$/;
|
|
7
|
+
|
|
8
|
+
export const LINKIFIED_LINK_CLASS = "font-medium og-link [overflow-wrap:anywhere]";
|
|
9
|
+
|
|
10
|
+
type Segment = { kind: "text"; value: string } | { kind: "link"; value: string };
|
|
11
|
+
|
|
12
|
+
function trimUrl(raw: string): string {
|
|
13
|
+
let url = raw.replace(TRAILING_PUNCTUATION, "");
|
|
14
|
+
// Drop a closing paren that wraps the URL rather than belonging to it.
|
|
15
|
+
while (url.endsWith(")") && countOf(url, "(") < countOf(url, ")")) {
|
|
16
|
+
url = url.slice(0, -1).replace(TRAILING_PUNCTUATION, "");
|
|
17
|
+
}
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function countOf(value: string, char: string): number {
|
|
22
|
+
let count = 0;
|
|
23
|
+
for (const current of value) if (current === char) count += 1;
|
|
24
|
+
return count;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isValidUrl(value: string): boolean {
|
|
28
|
+
try {
|
|
29
|
+
return /^https?:$/u.test(new URL(value).protocol);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Split plain text into text and bare http(s) URL segments. */
|
|
36
|
+
export function linkifySegments(text: string): Segment[] {
|
|
37
|
+
const segments: Segment[] = [];
|
|
38
|
+
let cursor = 0;
|
|
39
|
+
for (const match of text.matchAll(URL_PATTERN)) {
|
|
40
|
+
const start = match.index ?? 0;
|
|
41
|
+
const url = trimUrl(match[0]);
|
|
42
|
+
if (!isValidUrl(url)) continue;
|
|
43
|
+
if (start > cursor) segments.push({ kind: "text", value: text.slice(cursor, start) });
|
|
44
|
+
segments.push({ kind: "link", value: url });
|
|
45
|
+
cursor = start + url.length;
|
|
46
|
+
}
|
|
47
|
+
if (cursor < text.length) segments.push({ kind: "text", value: text.slice(cursor) });
|
|
48
|
+
return segments;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render plain text with bare http(s) URLs as links that open in a new tab and
|
|
53
|
+
* wrap anywhere, so a long sign-in or approval URL stays clickable and inside
|
|
54
|
+
* its card. Non-string nodes pass through unchanged.
|
|
55
|
+
*/
|
|
56
|
+
export function linkifyText(node: ReactNode): ReactNode {
|
|
57
|
+
if (typeof node !== "string") return node;
|
|
58
|
+
const segments = linkifySegments(node);
|
|
59
|
+
if (!segments.some((segment) => segment.kind === "link")) return node;
|
|
60
|
+
// Segments are contiguous, so each one's text offset is a stable unique key.
|
|
61
|
+
let offset = 0;
|
|
62
|
+
return segments.map((segment) => {
|
|
63
|
+
const key = offset;
|
|
64
|
+
offset += segment.value.length;
|
|
65
|
+
return segment.kind === "link" ? (
|
|
66
|
+
<a
|
|
67
|
+
key={key}
|
|
68
|
+
className={LINKIFIED_LINK_CLASS}
|
|
69
|
+
href={segment.value}
|
|
70
|
+
target="_blank"
|
|
71
|
+
rel="noreferrer noopener"
|
|
72
|
+
>
|
|
73
|
+
{segment.value}
|
|
74
|
+
</a>
|
|
75
|
+
) : (
|
|
76
|
+
<Fragment key={key}>{segment.value}</Fragment>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
type OlderHistoryLoader,
|
|
93
93
|
} from "../older-history";
|
|
94
94
|
import { Markdown, type MarkdownProps } from "./markdown";
|
|
95
|
+
import { linkifyText } from "./linkified-text";
|
|
95
96
|
import { OpenGeniLinkProvider, type OpenGeniLinkResolver } from "./open-geni-links";
|
|
96
97
|
import {
|
|
97
98
|
TimelineBeforeLayout,
|
|
@@ -4476,7 +4477,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
4476
4477
|
item.questions.map((question, index) => (
|
|
4477
4478
|
<div key={question.id}>
|
|
4478
4479
|
{question.label ? (
|
|
4479
|
-
<p className="text-og-sm font-semibold text-og-fg">
|
|
4480
|
+
<p className="text-og-sm font-semibold text-og-fg [overflow-wrap:anywhere]">
|
|
4480
4481
|
{multipleQuestions ? (
|
|
4481
4482
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
|
|
4482
4483
|
) : null}
|
|
@@ -4486,7 +4487,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
4486
4487
|
) : null}
|
|
4487
4488
|
<p
|
|
4488
4489
|
className={cn(
|
|
4489
|
-
"text-og-md leading-6 text-og-fg",
|
|
4490
|
+
"text-og-md leading-6 text-og-fg [overflow-wrap:anywhere]",
|
|
4490
4491
|
question.label && "mt-0.5 text-og-sm text-og-fg-muted",
|
|
4491
4492
|
)}
|
|
4492
4493
|
>
|
|
@@ -4494,7 +4495,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
4494
4495
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
|
|
4495
4496
|
) : null}
|
|
4496
4497
|
{!question.label && multipleQuestions ? " " : null}
|
|
4497
|
-
{question.prompt}
|
|
4498
|
+
{linkifyText(question.prompt)}
|
|
4498
4499
|
</p>
|
|
4499
4500
|
</div>
|
|
4500
4501
|
))
|