@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.
Files changed (70) hide show
  1. package/dist/artifacts.js +4 -2
  2. package/dist/artifacts.js.map +1 -1
  3. package/dist/{browser-viewer-RZH5JVFZ.js → browser-viewer-M7BIVFTW.js} +3 -3
  4. package/dist/{chat-interactive-block-UCACTZIE.js → chat-interactive-block-BO2IWXVK.js} +3 -3
  5. package/dist/{chunk-NIIXN66Z.js → chunk-5XEXTWEM.js} +2 -2
  6. package/dist/{chunk-4JUSFDTA.js → chunk-6JFN3OWG.js} +28 -225
  7. package/dist/chunk-6JFN3OWG.js.map +1 -0
  8. package/dist/{chunk-BSIADJ4U.js → chunk-6SNDCUPB.js} +2 -2
  9. package/dist/{chunk-RRYCD3QD.js → chunk-AMVCNAGC.js} +992 -924
  10. package/dist/chunk-AMVCNAGC.js.map +1 -0
  11. package/dist/{chunk-ERLG2X3U.js → chunk-CORUP6JB.js} +3 -2
  12. package/dist/chunk-CORUP6JB.js.map +1 -0
  13. package/dist/{chunk-6JUSXDF4.js → chunk-HBXL6HWG.js} +2 -2
  14. package/dist/{chunk-HQN5AZFY.js → chunk-HGKELH6Z.js} +12 -3
  15. package/dist/{chunk-HQN5AZFY.js.map → chunk-HGKELH6Z.js.map} +1 -1
  16. package/dist/{chunk-QWA4BHC6.js → chunk-ISN726VG.js} +2 -2
  17. package/dist/{chunk-QQ5UGGGE.js → chunk-KZENYIA2.js} +16 -2
  18. package/dist/{chunk-QQ5UGGGE.js.map → chunk-KZENYIA2.js.map} +1 -1
  19. package/dist/{chunk-25YZS7TK.js → chunk-O5GPRG76.js} +4 -4
  20. package/dist/{chunk-UPP6YD2R.js → chunk-PWCBMEV2.js} +4 -4
  21. package/dist/{chunk-X6KAA43Z.js → chunk-Q26UAIXI.js} +2 -2
  22. package/dist/{chunk-U7A22SY4.js → chunk-TIHSXNBC.js} +6 -2
  23. package/dist/{chunk-U7A22SY4.js.map → chunk-TIHSXNBC.js.map} +1 -1
  24. package/dist/{chunk-2U54CHR5.js → chunk-VIUEOHXY.js} +3 -2
  25. package/dist/chunk-VIUEOHXY.js.map +1 -0
  26. package/dist/components/artifacts/artifact-sandbox.d.ts +3 -0
  27. package/dist/components/artifacts/published-html-artifact-frame.d.ts +15 -51
  28. package/dist/components/artifacts/site-view.d.ts +4 -1
  29. package/dist/components/linkified-text.d.ts +18 -0
  30. package/dist/composer-messages.js +2 -2
  31. package/dist/composer.js +4 -4
  32. package/dist/connect.js +2 -2
  33. package/dist/index.js +12 -12
  34. package/dist/interaction.js +2 -2
  35. package/dist/{latest-question-6WZQV23L.js → latest-question-CHE7E753.js} +3 -3
  36. package/dist/lib/format.d.ts +7 -0
  37. package/dist/machines.js +2 -2
  38. package/dist/{platform-activity-row-UQDQDX4C.js → platform-activity-row-5OCKOQWZ.js} +2 -2
  39. package/dist/session-ui.js +10 -10
  40. package/dist/session.js +4 -4
  41. package/dist/sites.js +1 -1
  42. package/dist/timeline/types.d.ts +1 -1
  43. package/dist/timeline-model.js +3 -3
  44. package/package.json +3 -3
  45. package/src/components/artifacts/artifact-sandbox.tsx +4 -0
  46. package/src/components/artifacts/published-html-artifact-frame.tsx +48 -321
  47. package/src/components/artifacts/site-view.tsx +5 -0
  48. package/src/components/human-input-form.tsx +19 -13
  49. package/src/components/linkified-text.tsx +79 -0
  50. package/src/components/message-timeline.tsx +4 -3
  51. package/src/hooks/use-machines.ts +4 -0
  52. package/src/lib/format.ts +25 -1
  53. package/src/timeline/platform-activity-presentation.ts +1 -0
  54. package/src/timeline/projection.ts +18 -1
  55. package/src/timeline/types.ts +1 -0
  56. package/dist/chunk-2U54CHR5.js.map +0 -1
  57. package/dist/chunk-4JUSFDTA.js.map +0 -1
  58. package/dist/chunk-ERLG2X3U.js.map +0 -1
  59. package/dist/chunk-RRYCD3QD.js.map +0 -1
  60. /package/dist/{browser-viewer-RZH5JVFZ.js.map → browser-viewer-M7BIVFTW.js.map} +0 -0
  61. /package/dist/{chat-interactive-block-UCACTZIE.js.map → chat-interactive-block-BO2IWXVK.js.map} +0 -0
  62. /package/dist/{chunk-NIIXN66Z.js.map → chunk-5XEXTWEM.js.map} +0 -0
  63. /package/dist/{chunk-BSIADJ4U.js.map → chunk-6SNDCUPB.js.map} +0 -0
  64. /package/dist/{chunk-6JUSXDF4.js.map → chunk-HBXL6HWG.js.map} +0 -0
  65. /package/dist/{chunk-QWA4BHC6.js.map → chunk-ISN726VG.js.map} +0 -0
  66. /package/dist/{chunk-25YZS7TK.js.map → chunk-O5GPRG76.js.map} +0 -0
  67. /package/dist/{chunk-UPP6YD2R.js.map → chunk-PWCBMEV2.js.map} +0 -0
  68. /package/dist/{chunk-X6KAA43Z.js.map → chunk-Q26UAIXI.js.map} +0 -0
  69. /package/dist/{latest-question-6WZQV23L.js.map → latest-question-CHE7E753.js.map} +0 -0
  70. /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
- OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL,
3
- OPENGENI_SITE_BRIDGE_READY,
4
- OPENGENI_SITE_BRIDGE_RESPONSE,
5
- OPENGENI_SITE_BRIDGE_VERSION,
6
- isOpenGeniSiteBridgeCancelMessage,
7
- isOpenGeniSiteBridgeConnectMessage,
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
- export type PublishedHtmlArtifactToolBridge = {
54
- resolve?: (
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
- html = resolveSiteClientScript(html);
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
- const toolBridgeEnabled = props.toolBridge !== undefined;
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 requests = new SiteBridgeRequestRegistry();
155
- const attachToolPort = (data: unknown, ports: readonly MessagePort[]) => {
156
- const port = openGeniSiteBridgePortFromBootstrap(data, ports);
157
- if (!port) return;
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
- documentLease.load(frameWindow);
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
- documentLease.close();
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
- export class SiteBridgeRequestRegistry {
267
- private readonly controllersByPort = new Map<MessagePort, Map<string, AbortController>>();
268
-
269
- addPort(port: MessagePort): void {
270
- // Several SDK clients may share one document bootstrap. Document teardown,
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
- code: stale
418
- ? nestedCode
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 id={titleId} className="text-og-md font-semibold text-og-fg">
507
- {resolvedTitle}
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">{resolvedDescription}</div>
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
  ))