@opengeni/react 7.4.0 → 7.5.0-canary.36927100590001

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
@@ -153,3 +153,27 @@ export function sessionLinkResolver(input: {
153
153
  return null;
154
154
  };
155
155
  }
156
+
157
+ /** An editable artifact or Site to open in a host-mounted viewer. */
158
+ export type OpenGeniViewerTarget = Readonly<{
159
+ kind: "editable-artifact" | "site";
160
+ artifactId: string;
161
+ /** The link text the agent wrote, until the viewer loads the real title. */
162
+ title?: string | undefined;
163
+ }>;
164
+
165
+ /**
166
+ * "Open in host viewer": resolve agent links to editable artifacts and Sites
167
+ * in this workspace to `open(target)`, for example to mount
168
+ * `SessionArtifactViewer` beside the conversation. Other targets fall through.
169
+ */
170
+ export function viewerLinkResolver(input: {
171
+ workspaceId: string;
172
+ open: (target: OpenGeniViewerTarget) => void;
173
+ }): OpenGeniLinkResolver {
174
+ return (target) => {
175
+ if (target.kind !== "editable-artifact" && target.kind !== "site") return null;
176
+ if (target.workspaceId !== input.workspaceId) return null;
177
+ return { open: () => input.open({ kind: target.kind, artifactId: target.artifactId }) };
178
+ };
179
+ }
@@ -1,5 +1,12 @@
1
1
  import type { TerminalCapability } from "@opengeni/sdk";
2
- import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import {
3
+ type ReactNode,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ } from "react";
3
10
  import { cn } from "../lib/cn";
4
11
  import { type TerminalStreamStatus, useTerminalStream } from "../hooks/use-terminal-stream";
5
12
  import type { UseSandboxTerminalResult } from "../hooks/use-sandbox-terminal";
@@ -7,6 +14,7 @@ import { resolveTerminalFont, xtermThemeFromTokens } from "../lib/xterm-theme";
7
14
  import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
8
15
  import { terminalCanAcquirePty } from "../lib/terminal-capability";
9
16
  import { attachRenderer, type RendererLoaders, type RendererTier } from "../lib/xterm-renderer";
17
+ import { sandboxTerminalPeers } from "../lib/workbench-peers";
10
18
 
11
19
  /**
12
20
  * A COMPLETE xterm `ITheme` (subset by intent, but every field xterm colors a
@@ -114,6 +122,19 @@ type XtermLike = {
114
122
  };
115
123
  };
116
124
  type FitAddonLike = { fit: () => void };
125
+ type TerminalPeerBundle = {
126
+ Terminal: new (options: Record<string, unknown>) => XtermLike;
127
+ FitAddon: new () => FitAddonLike;
128
+ WebLinksAddon: new (handler: (event: MouseEvent, uri: string) => void) => unknown;
129
+ loadWebgl?:
130
+ | (() => Promise<{
131
+ WebglAddon: new () => {
132
+ dispose: () => void;
133
+ onContextLoss?: (callback: () => void) => void;
134
+ };
135
+ }>)
136
+ | undefined;
137
+ };
117
138
 
118
139
  export type TerminalSurfaceState =
119
140
  | "loading"
@@ -237,6 +258,12 @@ export function SandboxTerminal({
237
258
  const wroteFirehoseRef = useRef(false);
238
259
  const bootActiveRef = useRef(false);
239
260
  const [ready, setReady] = useState(false);
261
+ const [loadError, setLoadError] = useState<Error | null>(null);
262
+ const peerRevision = useSyncExternalStore(
263
+ sandboxTerminalPeers.subscribe,
264
+ sandboxTerminalPeers.revision,
265
+ sandboxTerminalPeers.revision,
266
+ );
240
267
  const [inputReady, setInputReady] = useState(false);
241
268
  const [activated, setActivated] = useState(false);
242
269
 
@@ -353,12 +380,10 @@ export function SandboxTerminal({
353
380
 
354
381
  ensureXtermBaseCss();
355
382
 
383
+ setLoadError(null);
356
384
  void (async () => {
357
- const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
358
- import("@xterm/xterm"),
359
- import("@xterm/addon-fit"),
360
- import("@xterm/addon-web-links"),
361
- ]);
385
+ const { Terminal, FitAddon, WebLinksAddon, loadWebgl } =
386
+ (await sandboxTerminalPeers.load()) as TerminalPeerBundle;
362
387
  if (disposed) return;
363
388
  await waitForSize(el);
364
389
  if (disposed) return;
@@ -397,13 +422,17 @@ export function SandboxTerminal({
397
422
  const failSet = forcedRendererFailures();
398
423
  const loaders: RendererLoaders = {
399
424
  webgl: async (onLoss) => {
425
+ if (disposed) throw new Error("Terminal was disposed");
400
426
  if (failSet.has("webgl")) throw new Error("forced webgl failure");
401
- const { WebglAddon } = await import("@xterm/addon-webgl");
427
+ if (!loadWebgl) throw new Error("WebGL terminal renderer is not enabled");
428
+ const { WebglAddon } = await loadWebgl();
429
+ if (disposed) throw new Error("Terminal was disposed");
402
430
  const addon = new WebglAddon() as unknown as {
403
431
  dispose: () => void;
404
432
  onContextLoss?: (cb: () => void) => void;
405
433
  };
406
434
  addon.onContextLoss?.(() => {
435
+ if (disposed) return;
407
436
  try {
408
437
  addon.dispose();
409
438
  } catch {
@@ -416,6 +445,7 @@ export function SandboxTerminal({
416
445
  },
417
446
  };
418
447
  await attachRenderer("webgl", loaders, (tier) => {
448
+ if (disposed) return;
419
449
  rendererRef.current = tier;
420
450
  el.setAttribute("data-og-term-renderer", tier);
421
451
  });
@@ -448,7 +478,9 @@ export function SandboxTerminal({
448
478
  }
449
479
  setReady(true);
450
480
  });
451
- })();
481
+ })().catch((cause: unknown) => {
482
+ if (!disposed) setLoadError(cause instanceof Error ? cause : new Error(String(cause)));
483
+ });
452
484
 
453
485
  return () => {
454
486
  disposed = true;
@@ -461,9 +493,9 @@ export function SandboxTerminal({
461
493
  bootActiveRef.current = false;
462
494
  setReady(false);
463
495
  };
464
- // Mount ONCE — see the effects below for the runtime option syncs.
496
+ // Mount once per peer registration; runtime options sync without a remount.
465
497
  // eslint-disable-next-line react-hooks/exhaustive-deps
466
- }, []);
498
+ }, [peerRevision]);
467
499
 
468
500
  // Sync stdin/cursor at runtime (NOT a remount) when interactivity flips, so the
469
501
  // projection→PTY handoff keeps the single Terminal instance + its scrollback.
@@ -621,7 +653,12 @@ export function SandboxTerminal({
621
653
  onPointerDownCapture={handleActivate}
622
654
  onFocusCapture={handleActivate}
623
655
  >
624
- {!ready && (placeholder ?? <TerminalPlaceholder />)}
656
+ {!ready &&
657
+ (loadError ? (
658
+ <p role="alert">{loadError.message}</p>
659
+ ) : (
660
+ (placeholder ?? <TerminalPlaceholder />)
661
+ ))}
625
662
  {/* Kept `visibility:hidden` until the first successful fit so the first
626
663
  VISIBLE frame is already correctly sized (no 80×24 flash — E4). */}
627
664
  <div
@@ -826,6 +826,7 @@ export function SessionChrome({
826
826
  onRefresh={queue.refresh}
827
827
  onClearMutationError={queue.clearMutationError}
828
828
  onRetryOptimistic={composer?.retryOptimisticMessage}
829
+ onRestoreOptimistic={composer?.restoreOptimisticMessage}
829
830
  onRemoveOptimistic={composer?.removeOptimisticMessage}
830
831
  readOnly={!canMutateQueue}
831
832
  mutationFor={queue.mutationFor}
@@ -1333,6 +1334,7 @@ function QueuePanel({
1333
1334
  onRefresh,
1334
1335
  onClearMutationError,
1335
1336
  onRetryOptimistic,
1337
+ onRestoreOptimistic,
1336
1338
  onRemoveOptimistic,
1337
1339
  readOnly,
1338
1340
  mutationFor,
@@ -1351,6 +1353,7 @@ function QueuePanel({
1351
1353
  onRefresh: () => Promise<void>;
1352
1354
  onClearMutationError: () => void;
1353
1355
  onRetryOptimistic?: ((clientEventId: string) => void) | undefined;
1356
+ onRestoreOptimistic?: ((clientEventId: string) => void) | undefined;
1354
1357
  onRemoveOptimistic?: ((clientEventId: string) => void) | undefined;
1355
1358
  readOnly: boolean;
1356
1359
  mutationFor: UseTurnQueueResult["mutationFor"];
@@ -1536,17 +1539,24 @@ function QueuePanel({
1536
1539
  <span className="shrink-0 font-og-mono text-[10px] leading-4 text-og-fg-subtle">
1537
1540
  {turns.length + index + 1}
1538
1541
  </span>
1539
- <CompactQueueItemContent text={message.text} annotations={message.annotations} />
1542
+ <div className="min-w-0 flex-1">
1543
+ <CompactQueueItemContent text={message.text} annotations={message.annotations} />
1544
+ {message.state === "failed" && message.error ? (
1545
+ <p className="break-words text-og-xs">{message.error}</p>
1546
+ ) : null}
1547
+ </div>
1540
1548
  <span className="sr-only">
1541
1549
  {message.state === "failed"
1542
- ? "Not confirmed"
1550
+ ? message.outcomeUnknown
1551
+ ? "Not confirmed"
1552
+ : "Message not sent"
1543
1553
  : message.state === "sending"
1544
1554
  ? "Placing in queue"
1545
1555
  : "Queued"}
1546
1556
  </span>
1547
1557
  {message.state === "failed" ? (
1548
1558
  <div className="flex shrink-0 items-center gap-1 text-[10px]">
1549
- {onRetryOptimistic ? (
1559
+ {message.retryable !== false && onRetryOptimistic ? (
1550
1560
  <button
1551
1561
  type="button"
1552
1562
  className="rounded-og-sm px-1.5 py-1 font-medium hover:bg-og-surface-2 pointer-coarse:min-h-11 pointer-coarse:min-w-11"
@@ -1555,6 +1565,15 @@ function QueuePanel({
1555
1565
  Retry
1556
1566
  </button>
1557
1567
  ) : null}
1568
+ {message.retryable === false && onRestoreOptimistic ? (
1569
+ <button
1570
+ type="button"
1571
+ className="rounded-og-sm px-1.5 py-1 font-medium hover:bg-og-surface-2 pointer-coarse:min-h-11 pointer-coarse:min-w-11"
1572
+ onClick={() => onRestoreOptimistic(message.clientEventId)}
1573
+ >
1574
+ Edit message
1575
+ </button>
1576
+ ) : null}
1558
1577
  {onRemoveOptimistic ? (
1559
1578
  <button
1560
1579
  type="button"
@@ -1,5 +1,15 @@
1
- import type { SendMessageInput } from "@opengeni/sdk";
2
- import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
1
+ import { SESSION_SCOPE_HEADER, type SendMessageInput } from "@opengeni/sdk";
2
+ import {
3
+ lazy,
4
+ Suspense,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type CSSProperties,
11
+ } from "react";
12
+ import type { SiteSnapshotClient } from "./artifacts/chat-interactive-block";
3
13
  import { useOpenGeni, type ClientOverride } from "../session-context";
4
14
  import { useWorkspaceModelCatalog } from "../hooks/use-available-models";
5
15
  import { ModelPolicyPicker } from "./model-policy-picker";
@@ -20,7 +30,9 @@ import {
20
30
  chainLinkResolvers,
21
31
  sessionLinkResolver,
22
32
  useOpenGeniLinkResolver,
33
+ viewerLinkResolver,
23
34
  type OpenGeniLinkResolver,
35
+ type OpenGeniViewerTarget,
24
36
  } from "./open-geni-links";
25
37
  import type { UserMessageDisclosureLabels } from "./user-message-body";
26
38
  import { conversationTimeline } from "../conversation-timeline";
@@ -37,8 +49,27 @@ export type SessionConversationProps = ClientOverride & {
37
49
  * editable artifacts and Sites stay unavailable until the host resolves them.
38
50
  */
39
51
  resolveLink?: OpenGeniLinkResolver | undefined;
52
+ /**
53
+ * Open agent links to editable artifacts and Sites in a host viewer, for
54
+ * example `SessionArtifactViewer` mounted beside the conversation. Asked
55
+ * after `resolveLink`.
56
+ */
57
+ onOpenArtifact?: ((target: OpenGeniViewerTarget) => void) | undefined;
58
+ /**
59
+ * Inline previews for assistant `opengeni-site` / `opengeni-html` fences.
60
+ * Defaults to the OpenGeni preview (Site reads need the proxy's
61
+ * `artifacts` option); `false` shows the fence as code.
62
+ */
63
+ renderInteractiveBlock?: MessageTimelineProps["renderInteractiveBlock"] | false;
40
64
  /** Product-specific tool-call renderers; defaults to the built-in registry. */
41
65
  toolRegistry?: MessageTimelineProps["toolRegistry"];
66
+ /**
67
+ * Replace the "usage limit reached" row for an `allowance_exhausted`
68
+ * refusal, for example to link your own plan or admin page.
69
+ */
70
+ renderAllowanceExhausted?: MessageTimelineProps["renderAllowanceExhausted"];
71
+ /** Replace the words of the default "usage limit reached" row. */
72
+ allowanceExhaustedLabels?: MessageTimelineProps["allowanceExhaustedLabels"];
42
73
  /**
43
74
  * File attachments in the composer. Defaults to true; the attach control
44
75
  * appears only when the deployment's client config enables file uploads.
@@ -72,7 +103,11 @@ function Conversation({
72
103
  sessionId,
73
104
  renderMessageText,
74
105
  resolveLink,
106
+ onOpenArtifact,
107
+ renderInteractiveBlock,
75
108
  toolRegistry,
109
+ renderAllowanceExhausted,
110
+ allowanceExhaustedLabels,
76
111
  attachments: attachmentsRequested = true,
77
112
  modelPicker,
78
113
  userMessageDisclosureLabels,
@@ -85,8 +120,6 @@ function Conversation({
85
120
  }: SessionConversationProps) {
86
121
  const scope = { client, workspaceId };
87
122
  const context = useOpenGeni(scope);
88
- const scopeRef = useRef(context);
89
- scopeRef.current = context;
90
123
  const config = useClientConfigFlags(context.client);
91
124
  const showModelPicker = modelPicker ?? config.modelSelection;
92
125
  const catalog = useWorkspaceModelCatalog({
@@ -98,10 +131,6 @@ function Conversation({
98
131
  const options = { ...scope, events: feed.events };
99
132
  const detail = useSession(sessionId, options);
100
133
  const queue = useTurnQueue(sessionId, options);
101
- const [queueFocusTarget, setQueueFocusTarget] = useState<{
102
- turnId: string;
103
- requestId: number;
104
- }>();
105
134
  const human = useHumanInputRequests(sessionId, options);
106
135
  const control = useSessionControl(sessionId, scope);
107
136
  const approvals = useMemo(() => projectPendingApprovals(feed.events), [feed.events]);
@@ -109,6 +138,7 @@ function Conversation({
109
138
  const uploadsEnabled = attachmentsRequested && config.uploads;
110
139
  const status = feed.sessionStatus ?? detail.session?.status;
111
140
  const terminal = status === "cancelled";
141
+ const importedArchive = detail.session?.importedArchive?.readOnly === true;
112
142
  const releaseSentFiles = (input: SendMessageInput) =>
113
143
  files.removeReadyFiles(
114
144
  (input.resources ?? []).flatMap((resource) =>
@@ -129,6 +159,11 @@ function Conversation({
129
159
  : {}),
130
160
  });
131
161
  const region = useRef<HTMLDivElement>(null);
162
+ const defaultInteractiveBlock = useDefaultInteractiveBlock(
163
+ context.client,
164
+ context.workspaceId,
165
+ sessionId,
166
+ );
132
167
  const inheritedLinks = useOpenGeniLinkResolver();
133
168
  const defaultLinks = useMemo(
134
169
  () =>
@@ -140,9 +175,22 @@ function Conversation({
140
175
  }),
141
176
  [context.client, context.workspaceId, sessionId, config.sandboxFiles],
142
177
  );
178
+ const onOpenArtifactRef = useRef(onOpenArtifact);
179
+ onOpenArtifactRef.current = onOpenArtifact;
180
+ const opensArtifacts = onOpenArtifact !== undefined;
181
+ const viewerLinks = useMemo(
182
+ () =>
183
+ opensArtifacts
184
+ ? viewerLinkResolver({
185
+ workspaceId: context.workspaceId,
186
+ open: (target) => onOpenArtifactRef.current?.(target),
187
+ })
188
+ : null,
189
+ [context.workspaceId, opensArtifacts],
190
+ );
143
191
  const links = useMemo(
144
- () => chainLinkResolvers(resolveLink, inheritedLinks, defaultLinks) ?? undefined,
145
- [resolveLink, inheritedLinks, defaultLinks],
192
+ () => chainLinkResolvers(resolveLink, viewerLinks, inheritedLinks, defaultLinks) ?? undefined,
193
+ [resolveLink, viewerLinks, inheritedLinks, defaultLinks],
146
194
  );
147
195
  const error = detail.error ?? feed.error ?? human.error;
148
196
  return (
@@ -159,7 +207,14 @@ function Conversation({
159
207
  <MessageTimeline
160
208
  renderMessageText={renderMessageText}
161
209
  resolveLink={links}
210
+ renderInteractiveBlock={
211
+ renderInteractiveBlock === false
212
+ ? undefined
213
+ : (renderInteractiveBlock ?? defaultInteractiveBlock)
214
+ }
162
215
  userMessageDisclosureLabels={userMessageDisclosureLabels}
216
+ renderAllowanceExhausted={renderAllowanceExhausted}
217
+ allowanceExhaustedLabels={allowanceExhaustedLabels}
163
218
  className="min-h-0 flex-1"
164
219
  {...(toolRegistry ? { toolRegistry } : {})}
165
220
  events={feed.events}
@@ -177,104 +232,142 @@ function Conversation({
177
232
  }}
178
233
  loadingOldest={feed.loadingOldest}
179
234
  onJumpToLatest={feed.jumpToLatest}
180
- onJumpToLatestQuestion={() =>
181
- feed.jumpToLatestQuestion({
182
- onQueuedQuestion: async (turn, navigation) => {
183
- await queue.refresh();
184
- if (
185
- !navigation.isCurrent() ||
186
- scopeRef.current.client !== context.client ||
187
- scopeRef.current.workspaceId !== context.workspaceId
188
- )
189
- return;
190
- setQueueFocusTarget((previous) => ({
191
- turnId: turn.id,
192
- requestId: (previous?.requestId ?? 0) + 1,
193
- }));
194
- },
195
- })
196
- }
197
- onAnnotate={composer.addAnnotation}
235
+ onAnnotate={importedArchive ? undefined : composer.addAnnotation}
198
236
  />
199
- <div className="min-h-0 max-h-[40%] shrink-0 overflow-y-auto" data-og-conversation-inputs="">
200
- {approvals.length > 0 && !terminal ? (
201
- <ApprovalSurface
202
- className="mx-auto max-w-3xl"
203
- approvals={approvals}
204
- onApprove={async (approval) => {
205
- await control.approve(approval.id);
206
- }}
207
- onReject={async (approval) => {
208
- await control.reject(approval.id);
209
- }}
210
- responding={control.responding}
211
- error={control.error}
212
- />
213
- ) : null}
214
- <HumanInputSurface
215
- loadSkillReview={loadSkillReview}
216
- requests={human.requests}
217
- onSubmit={async (id, response) => {
218
- await human.respond(id, response);
219
- }}
220
- respondingRequestId={human.respondingRequestId}
221
- error={human.mutationError?.message}
222
- autoFocus={false}
223
- />
224
- {terminal ? (
225
- <SessionChrome
226
- queue={queue}
227
- queueFocusTarget={queueFocusTarget}
228
- sessionStatus={status}
229
- readOnly
230
- />
231
- ) : (
232
- <SessionChrome
233
- queue={queue}
234
- queueFocusTarget={queueFocusTarget}
235
- composer={composer}
236
- sessionStatus={status}
237
- onComposerFocus={() =>
238
- region.current?.querySelector<HTMLTextAreaElement>("textarea")?.focus()
239
- }
240
- />
241
- )}
242
- </div>
243
- <div className="shrink-0" data-og-conversation-composer="">
244
- <ChatComposer
245
- {...composerProps}
246
- composer={composer}
247
- attachments={uploadsEnabled ? files : undefined}
248
- disabled={terminal || composerProps?.disabled}
249
- controlsStart={
250
- composerProps?.controlsStart ??
251
- (showModelPicker && composer.policy && (
252
- <ModelPolicyPicker
253
- rows={catalog.rows}
254
- model={composer.policy.model}
255
- effort={composer.policy.reasoningEffort}
256
- latencyMode={composer.policy.latencyMode}
257
- loading={catalog.loading}
258
- error={catalog.error?.message}
259
- disabled={terminal}
260
- sessionKey={sessionId}
261
- onModelChange={(model) => composer.setModel?.(model)}
262
- onEffortChange={(effort) => composer.setReasoningEffort?.(effort)}
263
- onLatencyModeChange={(mode) => composer.setLatencyMode?.(mode)}
237
+ {importedArchive ? (
238
+ <p className="shrink-0 px-4 py-2 text-center text-sm text-og-muted" role="status">
239
+ Archived conversation · Read only
240
+ </p>
241
+ ) : (
242
+ <>
243
+ <div
244
+ className="min-h-0 max-h-[40%] shrink-0 overflow-y-auto"
245
+ data-og-conversation-inputs=""
246
+ >
247
+ {approvals.length > 0 && !terminal ? (
248
+ <ApprovalSurface
249
+ className="mx-auto max-w-3xl"
250
+ approvals={approvals}
251
+ onApprove={async (approval) => {
252
+ await control.approve(approval.id);
253
+ }}
254
+ onReject={async (approval) => {
255
+ await control.reject(approval.id);
256
+ }}
257
+ responding={control.responding}
258
+ error={control.error}
259
+ />
260
+ ) : null}
261
+ <HumanInputSurface
262
+ loadSkillReview={loadSkillReview}
263
+ requests={human.requests}
264
+ onSubmit={async (id, response) => {
265
+ await human.respond(id, response);
266
+ }}
267
+ respondingRequestId={human.respondingRequestId}
268
+ error={human.mutationError?.message}
269
+ autoFocus={false}
270
+ />
271
+ {terminal ? (
272
+ <SessionChrome queue={queue} sessionStatus={status} readOnly />
273
+ ) : (
274
+ <SessionChrome
275
+ queue={queue}
276
+ composer={composer}
277
+ sessionStatus={status}
278
+ onComposerFocus={() =>
279
+ region.current?.querySelector<HTMLTextAreaElement>("textarea")?.focus()
280
+ }
264
281
  />
265
- ))
266
- }
267
- responsiveBasis={composerProps?.responsiveBasis ?? "container"}
268
- effectiveControl={
269
- composer.effectiveControl ?? queue.effectiveControl ?? detail.session?.effectiveControl
270
- }
271
- queuedAheadCount={queue.queue.length}
272
- />
273
- </div>
282
+ )}
283
+ </div>
284
+ <div className="shrink-0" data-og-conversation-composer="">
285
+ <ChatComposer
286
+ {...composerProps}
287
+ composer={composer}
288
+ attachments={uploadsEnabled ? files : undefined}
289
+ disabled={terminal || composerProps?.disabled}
290
+ controlsStart={
291
+ composerProps?.controlsStart ??
292
+ (showModelPicker && composer.policy && (
293
+ <ModelPolicyPicker
294
+ rows={catalog.rows}
295
+ model={composer.policy.model}
296
+ effort={composer.policy.reasoningEffort}
297
+ latencyMode={composer.policy.latencyMode}
298
+ loading={catalog.loading}
299
+ error={catalog.error?.message}
300
+ disabled={terminal}
301
+ sessionKey={sessionId}
302
+ onModelChange={(model) => composer.setModel?.(model)}
303
+ onEffortChange={(effort) => composer.setReasoningEffort?.(effort)}
304
+ onLatencyModeChange={(mode) => composer.setLatencyMode?.(mode)}
305
+ />
306
+ ))
307
+ }
308
+ responsiveBasis={composerProps?.responsiveBasis ?? "container"}
309
+ effectiveControl={
310
+ composer.effectiveControl ??
311
+ queue.effectiveControl ??
312
+ detail.session?.effectiveControl
313
+ }
314
+ queuedAheadCount={queue.queue.length}
315
+ />
316
+ </div>
317
+ </>
318
+ )}
274
319
  </div>
275
320
  );
276
321
  }
277
322
 
323
+ const LazyChatInteractiveBlock = lazy(() =>
324
+ import("./artifacts/chat-interactive-block").then((module) => ({
325
+ default: module.ChatInteractiveBlock,
326
+ })),
327
+ );
328
+
329
+ type ScopableClient = SiteSnapshotClient & {
330
+ withHeaders?: (headers: Readonly<Record<string, string>>) => SiteSnapshotClient;
331
+ };
332
+
333
+ /** Inline Site/HTML preview reading through this conversation's session scope. */
334
+ function useDefaultInteractiveBlock(
335
+ client: unknown,
336
+ workspaceId: string,
337
+ sessionId: string,
338
+ ): NonNullable<MessageTimelineProps["renderInteractiveBlock"]> {
339
+ // Scope lazily: only a rendered preview reads, and some clients (a Site's
340
+ // own client) cannot add headers.
341
+ const scoped = useMemo((): SiteSnapshotClient => {
342
+ let resolved: SiteSnapshotClient | null = null;
343
+ const get = () => {
344
+ const candidate = client as ScopableClient;
345
+ resolved ??=
346
+ typeof candidate.withHeaders === "function"
347
+ ? candidate.withHeaders({ [SESSION_SCOPE_HEADER]: sessionId })
348
+ : candidate;
349
+ return resolved;
350
+ };
351
+ return {
352
+ getWorkspaceArtifact: (...args) => get().getWorkspaceArtifact(...args),
353
+ getWorkspaceArtifactHtml: (...args) => get().getWorkspaceArtifactHtml(...args),
354
+ getWorkspaceArtifactContent: (...args) => {
355
+ const target = get();
356
+ if (!target.getWorkspaceArtifactContent) throw new Error("Site version unavailable");
357
+ return target.getWorkspaceArtifactContent(...args);
358
+ },
359
+ };
360
+ }, [client, sessionId]);
361
+ return useCallback(
362
+ (block) => (
363
+ <Suspense fallback={<span role="status">Loading preview…</span>}>
364
+ <LazyChatInteractiveBlock workspaceId={workspaceId} client={scoped} {...block} />
365
+ </Suspense>
366
+ ),
367
+ [scoped, workspaceId],
368
+ );
369
+ }
370
+
278
371
  /** Deployment/proxy flags from the client config: uploads, and whether model choice is open. */
279
372
  function useClientConfigFlags(client: {
280
373
  getClientConfig: () => Promise<{
@@ -163,6 +163,7 @@ export function TimelineAnnotationSelection({
163
163
  }) {
164
164
  const [candidate, setCandidate] = useState<SelectionCandidate | null>(null);
165
165
  const buttonRef = useRef<HTMLButtonElement | null>(null);
166
+ const initialSelectionObservedRef = useRef(false);
166
167
  const pointerOriginRef = useRef<"unset" | "inside" | "outside">("unset");
167
168
  const portalStyle = usePortalTokenStyle(rootRef.current);
168
169
 
@@ -175,6 +176,12 @@ export function TimelineAnnotationSelection({
175
176
  const update = (keyboard: boolean) => {
176
177
  setCandidate(buildCandidate(root, sources, keyboard));
177
178
  };
179
+ // A user may finish selecting text before this lazy component arrives.
180
+ // Reconcile once; later prop changes must not revive dismissed selections.
181
+ if (!initialSelectionObservedRef.current) {
182
+ initialSelectionObservedRef.current = true;
183
+ update(false);
184
+ }
178
185
  const onRootPointerUp = (event: PointerEvent) => {
179
186
  if (event.target instanceof Node && buttonRef.current?.contains(event.target)) return;
180
187
  // Tests may fire only pointerup on the source. A real click that started