@opengeni/react 7.4.0 → 7.5.0-canary.36919050519001

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 (210) hide show
  1. package/README.md +124 -27
  2. package/dist/artifacts-document.js +3 -2
  3. package/dist/artifacts-presentation.js +3 -2
  4. package/dist/artifacts-spreadsheet.js +2 -2
  5. package/dist/artifacts.d.ts +1 -0
  6. package/dist/artifacts.js +516 -1413
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/{browser-viewer-2E6R2Y4W.js → browser-viewer-Z7S7QEJL.js} +6 -5
  9. package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
  10. package/dist/{chunk-Y3QTZN2F.js → chunk-2VFNCX4E.js} +6 -6
  11. package/dist/{chunk-UDD6DLLT.js → chunk-3IJJEYW4.js} +15 -3
  12. package/dist/chunk-3IJJEYW4.js.map +1 -0
  13. package/dist/chunk-4RVYOY4O.js +379 -0
  14. package/dist/chunk-4RVYOY4O.js.map +1 -0
  15. package/dist/chunk-5CVXZA3Y.js +292 -0
  16. package/dist/chunk-5CVXZA3Y.js.map +1 -0
  17. package/dist/{chunk-5YK2ZDIG.js → chunk-5J4FOW6Q.js} +2 -10
  18. package/dist/chunk-5J4FOW6Q.js.map +1 -0
  19. package/dist/{chunk-KVERXCFY.js → chunk-5KYZIQFY.js} +40 -3
  20. package/dist/chunk-5KYZIQFY.js.map +1 -0
  21. package/dist/{chunk-KNDIXZN6.js → chunk-7UAMYXJY.js} +25 -18
  22. package/dist/{chunk-KNDIXZN6.js.map → chunk-7UAMYXJY.js.map} +1 -1
  23. package/dist/{chunk-SEEVCT36.js → chunk-A4UJB2MM.js} +2 -2
  24. package/dist/{chunk-X4HIN6TV.js → chunk-AFLPWUQ7.js} +71 -162
  25. package/dist/chunk-AFLPWUQ7.js.map +1 -0
  26. package/dist/chunk-B3V6TG77.js +41 -0
  27. package/dist/chunk-B3V6TG77.js.map +1 -0
  28. package/dist/{chunk-XEJEOAOV.js → chunk-B7WMS3CS.js} +51 -40
  29. package/dist/chunk-B7WMS3CS.js.map +1 -0
  30. package/dist/{chunk-S5JQ4R22.js → chunk-BDKR6BAR.js} +54 -24
  31. package/dist/chunk-BDKR6BAR.js.map +1 -0
  32. package/dist/{chunk-4MDLEHXL.js → chunk-DYWBUX5Q.js} +24 -12
  33. package/dist/chunk-DYWBUX5Q.js.map +1 -0
  34. package/dist/{chunk-YZ3YYMOM.js → chunk-E3G7SAYZ.js} +252 -624
  35. package/dist/chunk-E3G7SAYZ.js.map +1 -0
  36. package/dist/chunk-JT4JW7BC.js +54 -0
  37. package/dist/chunk-JT4JW7BC.js.map +1 -0
  38. package/dist/{chunk-5AZSK6MV.js → chunk-LWNLETQT.js} +5 -5
  39. package/dist/{chunk-USXVQACU.js → chunk-M7LBYP63.js} +5 -4
  40. package/dist/{chunk-USXVQACU.js.map → chunk-M7LBYP63.js.map} +1 -1
  41. package/dist/{chunk-L4OSXQ6H.js → chunk-NBOHNRLJ.js} +1289 -1060
  42. package/dist/chunk-NBOHNRLJ.js.map +1 -0
  43. package/dist/chunk-RHREZ7BO.js +11 -0
  44. package/dist/chunk-RHREZ7BO.js.map +1 -0
  45. package/dist/chunk-SCOTPE2I.js +589 -0
  46. package/dist/chunk-SCOTPE2I.js.map +1 -0
  47. package/dist/chunk-WEHMPOXK.js +39 -0
  48. package/dist/chunk-WEHMPOXK.js.map +1 -0
  49. package/dist/chunk-WRHIZYZF.js +988 -0
  50. package/dist/chunk-WRHIZYZF.js.map +1 -0
  51. package/dist/{chunk-3E2AITTZ.js → chunk-WUISZ3WU.js} +32 -28
  52. package/dist/{chunk-3E2AITTZ.js.map → chunk-WUISZ3WU.js.map} +1 -1
  53. package/dist/chunk-XCHWDJ22.js +107 -0
  54. package/dist/chunk-XCHWDJ22.js.map +1 -0
  55. package/dist/{chunk-DUEBTL3N.js → chunk-Y2PRMYLC.js} +4 -3
  56. package/dist/{chunk-DUEBTL3N.js.map → chunk-Y2PRMYLC.js.map} +1 -1
  57. package/dist/chunk-YBXQDQFX.js +1772 -0
  58. package/dist/chunk-YBXQDQFX.js.map +1 -0
  59. package/dist/chunk-YV4L3ILF.js +9 -0
  60. package/dist/chunk-YV4L3ILF.js.map +1 -0
  61. package/dist/{chunk-V2ABIVYJ.js → chunk-ZXSEDZP7.js} +70 -6
  62. package/dist/chunk-ZXSEDZP7.js.map +1 -0
  63. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  64. package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
  65. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  66. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  67. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  68. package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
  69. package/dist/components/artifacts/index.d.ts +7 -0
  70. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  71. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  72. package/dist/components/artifacts/site-view.d.ts +23 -0
  73. package/dist/components/browser-select-control.d.ts +3 -1
  74. package/dist/components/code-editor.d.ts +1 -4
  75. package/dist/components/desktop-viewer.d.ts +1 -1
  76. package/dist/components/message-timeline.d.ts +20 -3
  77. package/dist/components/open-geni-links.d.ts +16 -0
  78. package/dist/components/session-conversation.d.ts +20 -1
  79. package/dist/components/timeline-pill-geometry.d.ts +8 -0
  80. package/dist/components/timeline-pill-placement.d.ts +24 -0
  81. package/dist/components/usage-limit-notice.d.ts +49 -0
  82. package/dist/components/usage-member-list.d.ts +91 -0
  83. package/dist/components/usage-meter.d.ts +73 -0
  84. package/dist/composer.js +5 -5
  85. package/dist/{computer-viewer-WMJE6OA3.js → computer-viewer-KJWLH3GI.js} +7 -5
  86. package/dist/connect.js +7 -6
  87. package/dist/connect.js.map +1 -1
  88. package/dist/conversation-timeline.d.ts +1 -1
  89. package/dist/desktop.d.ts +7 -0
  90. package/dist/desktop.js +26 -0
  91. package/dist/desktop.js.map +1 -0
  92. package/dist/editor.d.ts +7 -0
  93. package/dist/editor.js +21 -0
  94. package/dist/editor.js.map +1 -0
  95. package/dist/hooks/latest-question.d.ts +1 -1
  96. package/dist/hooks/use-composer.d.ts +4 -0
  97. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  98. package/dist/hooks/use-usage.d.ts +40 -0
  99. package/dist/index.d.ts +7 -2
  100. package/dist/index.js +460 -1386
  101. package/dist/index.js.map +1 -1
  102. package/dist/interaction.js +8 -6
  103. package/dist/interaction.js.map +1 -1
  104. package/dist/{latest-question-QB4EUCW5.js → latest-question-IS445HOW.js} +3 -3
  105. package/dist/lib/format.d.ts +6 -1
  106. package/dist/lib/workbench-peers.d.ts +36 -0
  107. package/dist/machines.js +2 -2
  108. package/dist/{markdown-table-layout-PFUEZ2U4.js → markdown-table-layout-QXSLK4BY.js} +37 -5
  109. package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
  110. package/dist/{queue-surface-implementation-IM2HD3SK.js → queue-surface-implementation-2CAQAIHM.js} +2 -2
  111. package/dist/realtime.js +3 -3
  112. package/dist/{sandbox-files-PXPIZAFG.js → sandbox-files-AEJTWGT4.js} +8 -5
  113. package/dist/sandbox-files-AEJTWGT4.js.map +1 -0
  114. package/dist/session-ui.d.ts +9 -2
  115. package/dist/session-ui.js +39 -20
  116. package/dist/session-ui.js.map +1 -1
  117. package/dist/session.js +4 -4
  118. package/dist/sites.js +4 -289
  119. package/dist/sites.js.map +1 -1
  120. package/dist/terminal.d.ts +8 -0
  121. package/dist/terminal.js +31 -0
  122. package/dist/terminal.js.map +1 -0
  123. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  124. package/dist/timeline/genie-loading.d.ts +3 -1
  125. package/dist/timeline/types.d.ts +8 -1
  126. package/dist/{timeline-annotation-selection-ODLVF4OI.js → timeline-annotation-selection-DMLMM2TX.js} +6 -1
  127. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
  128. package/dist/timeline-pill-geometry-Z23V243M.js +110 -0
  129. package/dist/timeline-pill-geometry-Z23V243M.js.map +1 -0
  130. package/dist/usage/allowance-copy.d.ts +25 -0
  131. package/dist/usage/summary.d.ts +35 -0
  132. package/dist/usage.d.ts +13 -0
  133. package/dist/usage.js +1073 -0
  134. package/dist/usage.js.map +1 -0
  135. package/package.json +19 -3
  136. package/src/artifacts.ts +37 -0
  137. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  138. package/src/components/artifacts/artifact-sandbox.tsx +185 -0
  139. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  140. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  141. package/src/components/artifacts/editable-artifact-ui.tsx +3 -1
  142. package/src/components/artifacts/editable-artifact-view.tsx +187 -0
  143. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  144. package/src/components/artifacts/editable-document.tsx +34 -36
  145. package/src/components/artifacts/editable-presentation.tsx +21 -16
  146. package/src/components/artifacts/index.ts +41 -0
  147. package/src/components/artifacts/latest-task-runner.ts +49 -0
  148. package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
  149. package/src/components/artifacts/site-view.tsx +116 -0
  150. package/src/components/browser-select-control.tsx +4 -1
  151. package/src/components/browser-viewer.tsx +81 -22
  152. package/src/components/code-editor.tsx +13 -16
  153. package/src/components/computer-viewer.tsx +51 -23
  154. package/src/components/desktop-viewer.tsx +1 -1
  155. package/src/components/markdown-table-layout.ts +49 -5
  156. package/src/components/markdown.tsx +5 -2
  157. package/src/components/message-timeline.tsx +557 -516
  158. package/src/components/open-geni-links.tsx +24 -0
  159. package/src/components/sandbox-terminal.tsx +48 -11
  160. package/src/components/session-chrome.tsx +22 -3
  161. package/src/components/session-conversation.tsx +195 -102
  162. package/src/components/timeline-annotation-selection.tsx +7 -0
  163. package/src/components/timeline-pill-geometry.ts +169 -0
  164. package/src/components/timeline-pill-placement.ts +99 -0
  165. package/src/components/usage-limit-notice.tsx +179 -0
  166. package/src/components/usage-member-list.tsx +792 -0
  167. package/src/components/usage-meter.tsx +382 -0
  168. package/src/conversation-timeline.ts +10 -2
  169. package/src/desktop.ts +24 -0
  170. package/src/editor.ts +13 -0
  171. package/src/hooks/use-available-models.ts +1 -1
  172. package/src/hooks/use-composer.ts +57 -2
  173. package/src/hooks/use-desktop-stream.ts +19 -28
  174. package/src/hooks/use-usage.ts +100 -0
  175. package/src/index.ts +22 -0
  176. package/src/lib/format.ts +32 -6
  177. package/src/lib/workbench-peers.ts +62 -0
  178. package/src/session-ui.ts +22 -0
  179. package/src/terminal.ts +26 -0
  180. package/src/timeline/activity-rail.tsx +5 -3
  181. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  182. package/src/timeline/genie-loading.tsx +16 -6
  183. package/src/timeline/projection.ts +78 -4
  184. package/src/timeline/types.ts +8 -0
  185. package/src/usage/allowance-copy.ts +64 -0
  186. package/src/usage/summary.ts +93 -0
  187. package/src/usage.ts +50 -0
  188. package/styles/compiled.css +325 -0
  189. package/dist/chunk-4MDLEHXL.js.map +0 -1
  190. package/dist/chunk-5YK2ZDIG.js.map +0 -1
  191. package/dist/chunk-KVERXCFY.js.map +0 -1
  192. package/dist/chunk-L4OSXQ6H.js.map +0 -1
  193. package/dist/chunk-S5JQ4R22.js.map +0 -1
  194. package/dist/chunk-UDD6DLLT.js.map +0 -1
  195. package/dist/chunk-UGMKPUEM.js +0 -299
  196. package/dist/chunk-UGMKPUEM.js.map +0 -1
  197. package/dist/chunk-V2ABIVYJ.js.map +0 -1
  198. package/dist/chunk-X4HIN6TV.js.map +0 -1
  199. package/dist/chunk-XEJEOAOV.js.map +0 -1
  200. package/dist/chunk-YZ3YYMOM.js.map +0 -1
  201. package/dist/markdown-table-layout-PFUEZ2U4.js.map +0 -1
  202. package/dist/timeline-annotation-selection-ODLVF4OI.js.map +0 -1
  203. /package/dist/{browser-viewer-2E6R2Y4W.js.map → browser-viewer-Z7S7QEJL.js.map} +0 -0
  204. /package/dist/{computer-viewer-WMJE6OA3.js.map → chat-interactive-block-UGBTNB3L.js.map} +0 -0
  205. /package/dist/{chunk-Y3QTZN2F.js.map → chunk-2VFNCX4E.js.map} +0 -0
  206. /package/dist/{chunk-SEEVCT36.js.map → chunk-A4UJB2MM.js.map} +0 -0
  207. /package/dist/{chunk-5AZSK6MV.js.map → chunk-LWNLETQT.js.map} +0 -0
  208. /package/dist/{sandbox-files-PXPIZAFG.js.map → computer-viewer-KJWLH3GI.js.map} +0 -0
  209. /package/dist/{latest-question-QB4EUCW5.js.map → latest-question-IS445HOW.js.map} +0 -0
  210. /package/dist/{queue-surface-implementation-IM2HD3SK.js.map → queue-surface-implementation-2CAQAIHM.js.map} +0 -0
@@ -33,6 +33,7 @@ import {
33
33
  type PresentationSlideProjection,
34
34
  } from "./presentation-editor";
35
35
  import { ArtifactSurface } from "./artifact-surface";
36
+ import { useLatestTaskRunner } from "./latest-task-runner";
36
37
  import {
37
38
  asEditableArtifactError,
38
39
  editableArtifactAccessRevoked,
@@ -105,31 +106,35 @@ export function EditablePresentationArtifactSurface({
105
106
  error: null,
106
107
  });
107
108
  const loadGeneration = useRef(0);
109
+ const scheduleLoad = useLatestTaskRunner(session);
108
110
  const sceneNodes = useRef(new Map<string, PresentationArtifactEditorSceneNode>());
109
111
 
110
112
  useEffect(() => {
111
113
  const generation = ++loadGeneration.current;
112
114
  let cancelled = false;
113
115
  setState((current) => ({ ...current, loading: true, error: null }));
114
- void composePresentationEditorProjection(session).then(
115
- (composed) => {
116
- if (cancelled || generation !== loadGeneration.current) return;
117
- sceneNodes.current = new Map(composed.sceneNodes);
118
- setState({ composed, loading: false, error: null });
119
- },
120
- (cause) => {
121
- if (cancelled || generation !== loadGeneration.current) return;
122
- setState({
123
- composed: null,
124
- loading: false,
125
- error: asEditableArtifactError(cause, "Could not open this presentation"),
126
- });
127
- },
128
- );
116
+ scheduleLoad(async () => {
117
+ if (cancelled || generation !== loadGeneration.current) return;
118
+ await composePresentationEditorProjection(session).then(
119
+ (composed) => {
120
+ if (cancelled || generation !== loadGeneration.current) return;
121
+ sceneNodes.current = new Map(composed.sceneNodes);
122
+ setState({ composed, loading: false, error: null });
123
+ },
124
+ (cause) => {
125
+ if (cancelled || generation !== loadGeneration.current) return;
126
+ setState({
127
+ composed: null,
128
+ loading: false,
129
+ error: asEditableArtifactError(cause, "Could not open this presentation"),
130
+ });
131
+ },
132
+ );
133
+ });
129
134
  return () => {
130
135
  cancelled = true;
131
136
  };
132
- }, [invalidator, retryEpoch, session]);
137
+ }, [invalidator, retryEpoch, scheduleLoad, session]);
133
138
 
134
139
  const refresh = useCallback(() => setRetryEpoch((value) => value + 1), []);
135
140
  const writable = !readOnly && view.writable;
@@ -1,3 +1,44 @@
1
+ export {
2
+ ArtifactBadge,
3
+ ArtifactLabelsProvider,
4
+ DEFAULT_ARTIFACT_LABELS,
5
+ useArtifactLabels,
6
+ type ArtifactLabels,
7
+ ArtifactButton,
8
+ ArtifactLoading,
9
+ ArtifactProblem,
10
+ ArtifactSelect,
11
+ ArtifactViewerHeader,
12
+ artifactKindIcon,
13
+ artifactKindSubtitle,
14
+ artifactLoadErrorMessage,
15
+ artifactLoadErrorView,
16
+ type ArtifactKind,
17
+ type ArtifactLoadErrorKind,
18
+ type ArtifactLoadErrorView,
19
+ } from "./artifact-chrome";
20
+ export { ArtifactSandbox, type ArtifactSandboxProps } from "./artifact-sandbox";
21
+ export { DeferredChatMedia } from "./deferred-chat-media";
22
+ export {
23
+ ChatInteractiveBlock,
24
+ type ChatInteractiveBlockProps,
25
+ type SiteSnapshotClient,
26
+ type SiteToolBridgeFactory,
27
+ type SiteToolScope,
28
+ } from "./chat-interactive-block";
29
+ export { SiteView, type SiteViewProps } from "./site-view";
30
+ export {
31
+ EditableArtifactView,
32
+ type EditableArtifactRuntimes,
33
+ type EditableArtifactViewProps,
34
+ type OpenedEditableArtifact,
35
+ } from "./editable-artifact-view";
36
+ export {
37
+ SessionArtifactViewer,
38
+ type SessionArtifactTarget,
39
+ type SessionArtifactViewerLabels,
40
+ type SessionArtifactViewerProps,
41
+ } from "./session-artifact-viewer";
1
42
  export {
2
43
  ArtifactSurface,
3
44
  type ArtifactModality,
@@ -0,0 +1,49 @@
1
+ import { useRef } from "react";
2
+
3
+ /**
4
+ * Runs one task at a time; a task scheduled while another runs replaces any
5
+ * task still waiting, so only the newest request starts next. Editors use it
6
+ * for projection loads: replaying a long history emits one revision per
7
+ * committed transaction, and composing a projection for each of them at once
8
+ * would overrun the artifact Worker's bounded request queue.
9
+ */
10
+ export function createLatestTaskRunner(): (task: () => Promise<void>) => void {
11
+ let running = false;
12
+ let waiting: (() => Promise<void>) | null = null;
13
+ const drain = async () => {
14
+ running = true;
15
+ try {
16
+ while (waiting) {
17
+ const task = waiting;
18
+ waiting = null;
19
+ try {
20
+ await task();
21
+ } catch {
22
+ // Tasks settle their own errors; one failure must not stall the next.
23
+ }
24
+ }
25
+ } finally {
26
+ running = false;
27
+ }
28
+ };
29
+ return (task) => {
30
+ waiting = task;
31
+ if (!running) void drain();
32
+ };
33
+ }
34
+
35
+ /**
36
+ * One runner per artifact session. A composition still pending on a replaced
37
+ * session (for example one whose Worker stopped answering) must never hold up
38
+ * the next session's first load, so the runner is scoped to the session object
39
+ * rather than to the component.
40
+ */
41
+ export function useLatestTaskRunner(session: object): (task: () => Promise<void>) => void {
42
+ const runner = useRef<{ session: object; schedule: (task: () => Promise<void>) => void } | null>(
43
+ null,
44
+ );
45
+ if (runner.current?.session !== session) {
46
+ runner.current = { session, schedule: createLatestTaskRunner() };
47
+ }
48
+ return runner.current.schedule;
49
+ }
@@ -0,0 +1,376 @@
1
+ import { SESSION_SCOPE_HEADER, type ClientConfig, type OpenGeniClient } from "@opengeni/sdk";
2
+ import { createEditableArtifactReplicaId } from "@opengeni/sdk/editable-artifacts";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ import { cn } from "../../lib/cn";
6
+ import { useOpenGeni, type ClientOverride } from "../../session-context";
7
+ import {
8
+ ArtifactLabelsProvider,
9
+ ArtifactLoading,
10
+ ArtifactProblem,
11
+ ArtifactViewerHeader,
12
+ artifactLoadErrorView,
13
+ useArtifactLabels,
14
+ type ArtifactKind,
15
+ type ArtifactLabels,
16
+ } from "./artifact-chrome";
17
+ import { EditableArtifactView, type EditableArtifactRuntimes } from "./editable-artifact-view";
18
+ import type { SiteToolBridgeFactory } from "./chat-interactive-block";
19
+ import { SiteView } from "./site-view";
20
+
21
+ /** An artifact opened from a session: the targets agent links and Site previews name. */
22
+ export type SessionArtifactTarget = Readonly<
23
+ | { kind: "editable-artifact"; artifactId: string; title?: string | undefined }
24
+ | { kind: "site"; artifactId: string; title?: string | undefined }
25
+ >;
26
+
27
+ /** Every string the viewer shows, including its Site frame and states. */
28
+ export type SessionArtifactViewerLabels = Partial<ArtifactLabels>;
29
+
30
+ export type SessionArtifactViewerProps = ClientOverride &
31
+ Readonly<{
32
+ /** The conversation the artifact belongs to; the proxy only serves its artifacts. */
33
+ sessionId: string;
34
+ target: SessionArtifactTarget;
35
+ onClose: () => void;
36
+ /** Show a Back control, for example to return to the conversation on phones. */
37
+ onBack?: (() => void) | undefined;
38
+ /** Browser kernels and Worker for documents, spreadsheets, and presentations. */
39
+ editableRuntimes?: EditableArtifactRuntimes | undefined;
40
+ theme?: "light" | "dark" | undefined;
41
+ /** Host-owned Site API access; omit it and Sites render without workspace tools. */
42
+ siteToolBridge?: SiteToolBridgeFactory | undefined;
43
+ labels?: SessionArtifactViewerLabels | undefined;
44
+ className?: string | undefined;
45
+ }>;
46
+
47
+ type ViewerClient = Pick<
48
+ OpenGeniClient,
49
+ | "withHeaders"
50
+ | "apiUrl"
51
+ | "fetchApi"
52
+ | "getClientConfig"
53
+ | "getEditableArtifact"
54
+ | "getWorkspaceArtifact"
55
+ | "getWorkspaceArtifactHtml"
56
+ >;
57
+
58
+ const viewerClientKeys = new WeakMap<ViewerClient, string>();
59
+
60
+ function viewerClientKey(client: ViewerClient): string {
61
+ let key = viewerClientKeys.get(client);
62
+ if (!key) {
63
+ key = crypto.randomUUID();
64
+ viewerClientKeys.set(client, key);
65
+ }
66
+ return key;
67
+ }
68
+
69
+ /**
70
+ * A host-mountable viewer for one editable artifact or Site from a session,
71
+ * with its own header (title, Back, Close). Mount it in any sized container
72
+ * (a side panel, a sheet); it fills it. Data flows through the provider's
73
+ * client and `createSessionProxyHandler({ artifacts: true })`, scoped to
74
+ * `sessionId`.
75
+ */
76
+ export function SessionArtifactViewer({ labels, ...props }: SessionArtifactViewerProps) {
77
+ return (
78
+ <ArtifactLabelsProvider labels={labels}>
79
+ <Viewer {...props} />
80
+ </ArtifactLabelsProvider>
81
+ );
82
+ }
83
+
84
+ function Viewer({
85
+ client: clientOverride,
86
+ workspaceId: workspaceOverride,
87
+ sessionId,
88
+ target,
89
+ onClose,
90
+ onBack,
91
+ editableRuntimes,
92
+ theme,
93
+ siteToolBridge,
94
+ className,
95
+ }: Omit<SessionArtifactViewerProps, "labels">) {
96
+ const labels = useArtifactLabels();
97
+ const context = useOpenGeni({ client: clientOverride, workspaceId: workspaceOverride });
98
+ const base = context.client as unknown as ViewerClient;
99
+ const workspaceId = context.workspaceId;
100
+ const client = useMemo(
101
+ () => base.withHeaders({ [SESSION_SCOPE_HEADER]: sessionId }),
102
+ [base, sessionId],
103
+ );
104
+ const [loaded, setLoaded] = useState<{
105
+ key: string;
106
+ client: ViewerClient;
107
+ workspaceId: string;
108
+ title: string;
109
+ kind: ArtifactKind | null;
110
+ } | null>(null);
111
+ const targetKey = `${target.kind}:${target.artifactId}`;
112
+ const current =
113
+ loaded?.key === targetKey && loaded.client === client && loaded.workspaceId === workspaceId
114
+ ? loaded
115
+ : null;
116
+ const kind: ArtifactKind | null = target.kind === "site" ? "site" : (current?.kind ?? null);
117
+ const [unavailable, setUnavailable] = useState<{
118
+ key: string;
119
+ client: ViewerClient;
120
+ workspaceId: string;
121
+ } | null>(null);
122
+ const markUnavailable = useCallback(
123
+ () => setUnavailable({ key: targetKey, client, workspaceId }),
124
+ [client, targetKey, workspaceId],
125
+ );
126
+ const title =
127
+ current?.title ??
128
+ target.title ??
129
+ (unavailable?.key === targetKey &&
130
+ unavailable.client === client &&
131
+ unavailable.workspaceId === workspaceId
132
+ ? labels.artifact
133
+ : labels.opening);
134
+ return (
135
+ <section
136
+ aria-label={title}
137
+ data-og-artifact-viewer={target.kind}
138
+ className={cn("og-root flex h-full min-h-0 w-full min-w-0 flex-col bg-bg text-fg", className)}
139
+ >
140
+ <ArtifactViewerHeader
141
+ kind={kind}
142
+ title={title}
143
+ onBack={onBack}
144
+ backLabel={labels.back}
145
+ onClose={onClose}
146
+ closeLabel={labels.close}
147
+ />
148
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
149
+ {target.kind === "site" ? (
150
+ <SiteView
151
+ key={targetKey}
152
+ client={client}
153
+ workspaceId={workspaceId}
154
+ siteId={target.artifactId}
155
+ theme={theme}
156
+ toolBridge={siteToolBridge}
157
+ showTitle={false}
158
+ onTitle={(next) =>
159
+ setLoaded({ key: targetKey, client, workspaceId, title: next, kind: "site" })
160
+ }
161
+ />
162
+ ) : (
163
+ <EditableBody
164
+ key={targetKey}
165
+ client={client}
166
+ workspaceId={workspaceId}
167
+ sessionId={sessionId}
168
+ artifactId={target.artifactId}
169
+ runtimes={editableRuntimes}
170
+ onOpened={(next, modality) =>
171
+ setLoaded({ key: targetKey, client, workspaceId, title: next, kind: modality })
172
+ }
173
+ onUnavailable={markUnavailable}
174
+ />
175
+ )}
176
+ </div>
177
+ </section>
178
+ );
179
+ }
180
+
181
+ function EditableBody({
182
+ client,
183
+ workspaceId,
184
+ sessionId,
185
+ artifactId,
186
+ runtimes,
187
+ onOpened,
188
+ onUnavailable,
189
+ }: {
190
+ client: ViewerClient;
191
+ workspaceId: string;
192
+ sessionId: string;
193
+ artifactId: string;
194
+ runtimes: EditableArtifactRuntimes | undefined;
195
+ onOpened: (title: string, modality: ArtifactKind) => void;
196
+ onUnavailable: () => void;
197
+ }) {
198
+ const labels = useArtifactLabels();
199
+ const clientKey = viewerClientKey(client);
200
+ const [retryEpoch, setRetryEpoch] = useState(0);
201
+ const [configState, setConfigState] = useState<{
202
+ client: ViewerClient;
203
+ workspaceId: string;
204
+ sessionId: string;
205
+ result:
206
+ | { kind: "ready"; config: NonNullable<ClientConfig["artifacts"]> }
207
+ | { kind: "unavailable" }
208
+ | { kind: "error"; error: unknown };
209
+ } | null>(null);
210
+ const current =
211
+ configState?.client === client &&
212
+ configState.workspaceId === workspaceId &&
213
+ configState.sessionId === sessionId
214
+ ? configState.result
215
+ : null;
216
+ const config = current?.kind === "ready" ? current.config : null;
217
+ const scopeRef = useRef({ client, workspaceId, sessionId });
218
+ scopeRef.current = { client, workspaceId, sessionId };
219
+ useEffect(() => {
220
+ let live = true;
221
+ setConfigState(null);
222
+ client.getClientConfig().then(
223
+ (value) =>
224
+ live &&
225
+ setConfigState({
226
+ client,
227
+ workspaceId,
228
+ sessionId,
229
+ result: value.artifacts
230
+ ? { kind: "ready", config: value.artifacts }
231
+ : { kind: "unavailable" },
232
+ }),
233
+ (error: unknown) =>
234
+ live &&
235
+ setConfigState({ client, workspaceId, sessionId, result: { kind: "error", error } }),
236
+ );
237
+ return () => {
238
+ live = false;
239
+ };
240
+ }, [client, retryEpoch, sessionId, workspaceId]);
241
+ const baseUrl = useMemo(
242
+ () => new URL(client.apiUrl("/"), globalThis.location?.href ?? "http://localhost/"),
243
+ [client],
244
+ );
245
+ const transport = useMemo(() => {
246
+ if (!config) return undefined;
247
+ const socket = new URL(config.editableLiveUrl);
248
+ let scope = new AbortController();
249
+ const checkCurrent = (signal: AbortSignal) => {
250
+ signal.throwIfAborted();
251
+ if (
252
+ scopeRef.current.client !== client ||
253
+ scopeRef.current.workspaceId !== workspaceId ||
254
+ scopeRef.current.sessionId !== sessionId
255
+ ) {
256
+ throw new DOMException("Artifact viewer scope changed", "AbortError");
257
+ }
258
+ };
259
+ return {
260
+ activate: () => {
261
+ if (scope.signal.aborted) scope = new AbortController();
262
+ },
263
+ abort: () => scope.abort(),
264
+ fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
265
+ const signal = AbortSignal.any([scope.signal, ...(init?.signal ? [init.signal] : [])]);
266
+ checkCurrent(signal);
267
+ const url = new URL(input instanceof Request ? input.url : input, baseUrl);
268
+ if (
269
+ init?.method === "POST" &&
270
+ url.pathname ===
271
+ new URL(
272
+ `v1/workspaces/${encodeURIComponent(workspaceId)}/editable-artifacts/${encodeURIComponent(artifactId)}/live-ticket`,
273
+ baseUrl,
274
+ ).pathname
275
+ ) {
276
+ // A reconnect must revalidate the current host principal before
277
+ // minting a ticket for the editor's retained cache partition.
278
+ const next = (await client.getClientConfig({ signal })).artifacts;
279
+ checkCurrent(signal);
280
+ if (!next || artifactCapabilityKey(next) !== artifactCapabilityKey(config)) {
281
+ setConfigState({
282
+ client,
283
+ workspaceId,
284
+ sessionId,
285
+ result: next ? { kind: "ready", config: next } : { kind: "unavailable" },
286
+ });
287
+ scope.abort();
288
+ signal.throwIfAborted();
289
+ }
290
+ const { replicaId } = (await new Request(input, init).json()) as {
291
+ replicaId?: unknown;
292
+ };
293
+ if (typeof replicaId !== "string") throw new TypeError("Artifact replica is required");
294
+ await client.getEditableArtifact(workspaceId, artifactId, { replicaId, signal });
295
+ checkCurrent(signal);
296
+ }
297
+ return client.fetchApi(url, { ...init, signal });
298
+ }) as typeof fetch,
299
+ webSocketUrl: socket,
300
+ ...(isLoopback(baseUrl) && isLoopback(socket)
301
+ ? { allowInsecureDevelopmentTransport: true }
302
+ : {}),
303
+ };
304
+ }, [artifactId, baseUrl, client, config, sessionId, workspaceId]);
305
+ useEffect(() => {
306
+ transport?.activate();
307
+ return () => transport?.abort();
308
+ }, [transport]);
309
+ const blocked = current !== null && (current.kind !== "ready" || !runtimes);
310
+ useEffect(() => {
311
+ if (blocked) onUnavailable();
312
+ }, [blocked, onUnavailable]);
313
+ if (!current) return <ArtifactLoading label={labels.opening} />;
314
+ if (current.kind === "error") {
315
+ return (
316
+ <ArtifactProblem
317
+ view={artifactLoadErrorView(current.error, "editable", labels)}
318
+ onRetry={() => setRetryEpoch((value) => value + 1)}
319
+ />
320
+ );
321
+ }
322
+ if (!config || !transport) {
323
+ return <ArtifactProblem view={{ ...labels.viewingDisabled, retryable: false }} />;
324
+ }
325
+ if (!runtimes) {
326
+ return <ArtifactProblem view={{ ...labels.editorsMissing, retryable: false }} />;
327
+ }
328
+ return (
329
+ <EditableArtifactView
330
+ baseUrl={baseUrl}
331
+ workspaceId={workspaceId}
332
+ artifactId={artifactId}
333
+ authorityKey={JSON.stringify([clientKey, sessionId, artifactCapabilityKey(config)])}
334
+ runtimes={{
335
+ ...runtimes,
336
+ ...(isLoopback(baseUrl) ? { allowInsecureDevelopmentAssets: true } : {}),
337
+ }}
338
+ transport={transport}
339
+ showHeader={false}
340
+ open={async (signal) => {
341
+ const replicaId = createEditableArtifactReplicaId();
342
+ const artifact = await client.getEditableArtifact(workspaceId, artifactId, {
343
+ replicaId,
344
+ signal,
345
+ });
346
+ signal.throwIfAborted();
347
+ onOpened(artifact.title, artifact.modality);
348
+ return {
349
+ artifact,
350
+ replicaId,
351
+ authority: {
352
+ deploymentOrigin: baseUrl.origin,
353
+ workspaceId,
354
+ ...config.cachePartition,
355
+ },
356
+ };
357
+ }}
358
+ />
359
+ );
360
+ }
361
+
362
+ function artifactCapabilityKey(config: NonNullable<ClientConfig["artifacts"]>): string {
363
+ return JSON.stringify([
364
+ config.editableLiveUrl,
365
+ config.cachePartition.accountId,
366
+ config.cachePartition.principalId,
367
+ config.cachePartition.authorizationEpoch,
368
+ ]);
369
+ }
370
+
371
+ function isLoopback(url: URL): boolean {
372
+ return (
373
+ (url.protocol === "http:" || url.protocol === "ws:") &&
374
+ (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]")
375
+ );
376
+ }
@@ -0,0 +1,116 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import { loadSiteSnapshot } from "../../sites-ui";
4
+ import {
5
+ ArtifactLoading,
6
+ ArtifactProblem,
7
+ artifactLoadErrorView,
8
+ useArtifactLabels,
9
+ } from "./artifact-chrome";
10
+ import { ArtifactSandbox } from "./artifact-sandbox";
11
+ import type { SiteSnapshotClient, SiteToolBridgeFactory } from "./chat-interactive-block";
12
+
13
+ type Snapshot = Awaited<ReturnType<typeof loadSiteSnapshot>>;
14
+
15
+ export type SiteViewProps = {
16
+ client: SiteSnapshotClient;
17
+ workspaceId: string;
18
+ siteId: string;
19
+ /** Host-owned Site API access. Omit it and the Site renders without workspace tools. */
20
+ toolBridge?: SiteToolBridgeFactory | undefined;
21
+ theme?: "light" | "dark" | undefined;
22
+ /** Shown instead of the archived notice; the console links to its full page. */
23
+ archivedMessage?: string | undefined;
24
+ /** Show the Site title in the frame bar; hide it under a host header that already does. */
25
+ showTitle?: boolean | undefined;
26
+ /** Loading copy; defaults to "Loading Site…". */
27
+ loadingLabel?: string | undefined;
28
+ /** Called with the loaded title, for a host header. */
29
+ onTitle?: ((title: string) => void) | undefined;
30
+ className?: string | undefined;
31
+ };
32
+
33
+ /**
34
+ * A published Site filling its container: the console's session dock and an
35
+ * embedding host's artifact viewer render the same view.
36
+ */
37
+ export function SiteView({
38
+ client,
39
+ workspaceId,
40
+ siteId,
41
+ toolBridge,
42
+ theme,
43
+ archivedMessage,
44
+ showTitle = true,
45
+ loadingLabel,
46
+ onTitle,
47
+ className,
48
+ }: SiteViewProps) {
49
+ const labels = useArtifactLabels();
50
+ const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
51
+ const [error, setError] = useState<unknown>(null);
52
+ const readAbort = useRef<AbortController | null>(null);
53
+ const onTitleRef = useRef(onTitle);
54
+ onTitleRef.current = onTitle;
55
+ const load = useCallback(async () => {
56
+ readAbort.current?.abort();
57
+ const abort = new AbortController();
58
+ readAbort.current = abort;
59
+ try {
60
+ setError(null);
61
+ const next = await loadSiteSnapshot(client, workspaceId, siteId, {
62
+ signal: abort.signal,
63
+ includeArchivedContent: true,
64
+ });
65
+ if (abort.signal.aborted) return;
66
+ setSnapshot(next);
67
+ onTitleRef.current?.(next.detail.artifact.title);
68
+ } catch (nextError) {
69
+ if (abort.signal.aborted) return;
70
+ setSnapshot(null);
71
+ setError(nextError);
72
+ }
73
+ }, [client, siteId, workspaceId]);
74
+ useEffect(() => {
75
+ setSnapshot(null);
76
+ void load();
77
+ return () => readAbort.current?.abort();
78
+ }, [load]);
79
+ const content = snapshot?.content;
80
+ const bridge = useMemo(
81
+ () =>
82
+ content
83
+ ? toolBridge?.({
84
+ artifactId: siteId,
85
+ siteVersionId: content.versionId,
86
+ requestedTools: content.requestedTools,
87
+ })
88
+ : undefined,
89
+ [content, siteId, toolBridge],
90
+ );
91
+ if (error)
92
+ return (
93
+ <ArtifactProblem
94
+ view={artifactLoadErrorView(error, "site", labels)}
95
+ onRetry={() => void load()}
96
+ />
97
+ );
98
+ if (!snapshot || !content) return <ArtifactLoading label={loadingLabel ?? labels.loadingSite} />;
99
+ if (snapshot.detail.artifact.status === "archived")
100
+ return (
101
+ <div className="p-4 text-sm text-fg-muted">{archivedMessage ?? labels.siteArchived}</div>
102
+ );
103
+ return (
104
+ <ArtifactSandbox
105
+ html={content.html}
106
+ title={snapshot.detail.artifact.title}
107
+ showTitle={showTitle}
108
+ versionLabel={`v${snapshot.detail.artifact.currentVersion?.revision}`}
109
+ toolBridge={bridge}
110
+ connectedToolCount={content.requestedTools.length}
111
+ theme={theme}
112
+ fill
113
+ className={className ?? "h-full rounded-none border-0"}
114
+ />
115
+ );
116
+ }
@@ -56,6 +56,8 @@ function focusedSelect(
56
56
 
57
57
  /** Human fallback for native popup windows missing from Chromium page frames. */
58
58
  export function BrowserSelectControl(props: {
59
+ /** Older controllers need an explicit read to discover native popup options. */
60
+ manualFallback?: boolean;
59
61
  activation?: {
60
62
  observation: BrowserObservation | null;
61
63
  anchor: { x: number; y: number } | null;
@@ -186,6 +188,7 @@ export function BrowserSelectControl(props: {
186
188
  if (current === request.current) setBusy(false);
187
189
  }
188
190
  };
191
+ if (props.manualFallback === false && !open) return null;
189
192
  return (
190
193
  <div
191
194
  ref={popupRef}
@@ -199,7 +202,7 @@ export function BrowserSelectControl(props: {
199
202
  : { bottom: 12, left: 12 }
200
203
  }
201
204
  >
202
- {!anchor || !open || error ? (
205
+ {(props.manualFallback !== false && (!anchor || !open)) || error ? (
203
206
  <button
204
207
  type="button"
205
208
  className="rounded-og-sm border border-og-border bg-og-bg px-2 py-1 text-og-fg"