@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
@@ -0,0 +1,100 @@
1
+ import type { OpenGeniClient } from "@opengeni/sdk";
2
+ import {
3
+ getMyUsage,
4
+ type UsageAllowancePeriod,
5
+ type WorkspaceUsageResponse,
6
+ } from "@opengeni/sdk/usage-allowances";
7
+ import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
8
+
9
+ import { OpenGeniContext } from "../session-context";
10
+ import { summarizeUsage, type UsageSummary } from "../usage/summary";
11
+ import { usePolledValue } from "./internal";
12
+
13
+ /**
14
+ * Anything that can read the signed-in member's own usage: the root SDK client
15
+ * pointed at your session proxy (`getMyUsage`), or any client exposing
16
+ * `requestJson` (for example the narrower browser client).
17
+ */
18
+ export type UsageClientLike =
19
+ | Pick<OpenGeniClient, "getMyUsage">
20
+ | Pick<OpenGeniClient, "requestJson">;
21
+
22
+ export type UseUsageOptions = {
23
+ /** Defaults to the `<OpenGeniProvider>` client. */
24
+ client?: UsageClientLike | undefined;
25
+ /** Defaults to the `<OpenGeniProvider>` workspace. */
26
+ workspaceId?: string | undefined;
27
+ /** `"current"` (default) or a `YYYY-MM` allowance period. */
28
+ period?: UsageAllowancePeriod | undefined;
29
+ /** Re-read on an interval. Off by default; usage changes only when work settles. */
30
+ pollIntervalMs?: number | undefined;
31
+ enabled?: boolean | undefined;
32
+ /**
33
+ * Re-read whenever this value changes, keeping the current reading while it
34
+ * loads. Pass something that changes when work settles (a turn count, the
35
+ * session status) so the meter follows real usage without polling.
36
+ */
37
+ refreshKey?: unknown;
38
+ };
39
+
40
+ export type UseUsageResult = {
41
+ /** The raw `/usage/me` response: the workspace pool and your own row. */
42
+ usage: WorkspaceUsageResponse | null;
43
+ /** What to tell this member; null until loaded. */
44
+ summary: UsageSummary | null;
45
+ loading: boolean;
46
+ error: Error | null;
47
+ refresh: () => Promise<void>;
48
+ };
49
+
50
+ function readOwnUsage(
51
+ client: UsageClientLike,
52
+ workspaceId: string,
53
+ period: UsageAllowancePeriod | undefined,
54
+ signal: AbortSignal | undefined,
55
+ ): Promise<WorkspaceUsageResponse> {
56
+ const query = period === undefined ? {} : { period };
57
+ const options = signal ? { signal } : {};
58
+ if ("getMyUsage" in client && typeof client.getMyUsage === "function") {
59
+ return client.getMyUsage(workspaceId, query, options);
60
+ }
61
+ if ("requestJson" in client && typeof client.requestJson === "function") {
62
+ return getMyUsage(client, workspaceId, query, options);
63
+ }
64
+ throw new Error("@opengeni/react: the usage client needs getMyUsage or requestJson.");
65
+ }
66
+
67
+ /**
68
+ * The signed-in member's own usage in one workspace, read through `/usage/me`
69
+ * (the session proxy serves exactly this route). Never reads another member.
70
+ */
71
+ export function useUsage(options: UseUsageOptions = {}): UseUsageResult {
72
+ const context = useContext(OpenGeniContext);
73
+ const client = (options.client ?? context?.client) as UsageClientLike | undefined;
74
+ const workspaceId = options.workspaceId ?? context?.workspaceId;
75
+ if (!client || !workspaceId) {
76
+ throw new Error(
77
+ "@opengeni/react: useUsage needs a client and workspace. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId }.",
78
+ );
79
+ }
80
+ const period = options.period;
81
+ const load = useCallback(
82
+ (signal?: AbortSignal) => readOwnUsage(client, workspaceId, period, signal),
83
+ [client, workspaceId, period],
84
+ );
85
+ const state = usePolledValue(load, {
86
+ pollIntervalMs: options.pollIntervalMs,
87
+ enabled: options.enabled,
88
+ });
89
+ const refreshKey = options.refreshKey;
90
+ const seenKey = useRef(refreshKey);
91
+ const refresh = state.refresh;
92
+ useEffect(() => {
93
+ if (Object.is(seenKey.current, refreshKey)) return;
94
+ seenKey.current = refreshKey;
95
+ void refresh();
96
+ }, [refreshKey, refresh]);
97
+ const usage = state.data;
98
+ const summary = useMemo(() => (usage ? summarizeUsage(usage) : null), [usage]);
99
+ return { usage, summary, loading: state.loading, error: state.error, refresh };
100
+ }
package/src/index.ts CHANGED
@@ -536,6 +536,8 @@ export { MessageTimeline, TimelineRow } from "./components/message-timeline";
536
536
  export { GeneratedVideoPlayer } from "./components/generated-video-player";
537
537
  export type { GeneratedVideoPlayerProps } from "./components/generated-video-player";
538
538
  export type { MessageTimelineProps } from "./components/message-timeline";
539
+ export type { RenderAllowanceExhausted } from "./timeline/allowance-exhausted-row";
540
+ export type { AllowanceLabels, AllowanceScope } from "./usage/allowance-copy";
539
541
  export type { TimelineSearchTarget } from "./components/timeline-search";
540
542
  export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
541
543
  export type {
@@ -557,13 +559,20 @@ export {
557
559
  chainLinkResolvers,
558
560
  sessionLinkResolver,
559
561
  useOpenGeniLinkResolver,
562
+ viewerLinkResolver,
560
563
  } from "./components/open-geni-links";
561
564
  export type {
562
565
  OpenGeniLinkResolution,
563
566
  OpenGeniLinkResolver,
564
567
  OpenGeniLinkTarget,
568
+ OpenGeniViewerTarget,
565
569
  } from "./components/open-geni-links";
566
570
  export { parseOpenGeniLink } from "@opengeni/sdk";
571
+ export {
572
+ ArtifactLabelsProvider,
573
+ DEFAULT_ARTIFACT_LABELS,
574
+ type ArtifactLabels,
575
+ } from "./components/artifacts/artifact-chrome";
567
576
  export { CopyButton, CopyHoverFrame } from "./components/copy-button";
568
577
  export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
569
578
  export { SessionStatus, StatusDot, SESSION_STATUS_META } from "./components/session-status";
@@ -588,6 +597,19 @@ export { PierreDiff } from "./components/pierre-diff";
588
597
  // Opt in to the optional `@pierre/diffs` peer with `enablePierreDiffs()` from
589
598
  // `@opengeni/react/diffs`; the root entry never names the peer.
590
599
  export { registerPierreDiffs, type PierreDiffsLoader } from "./lib/pierre-diffs-loader";
600
+ // Bundler-visible optional workbench imports live only on opt-in subpaths.
601
+ export {
602
+ registerSandboxTerminal,
603
+ registerCodeEditor,
604
+ registerDesktopViewer,
605
+ } from "./lib/workbench-peers";
606
+ export type {
607
+ WorkbenchPeerLoader,
608
+ SandboxTerminalPeers,
609
+ CodeEditorPeers,
610
+ CodeEditorLanguage,
611
+ CodeEditorLanguages,
612
+ } from "./lib/workbench-peers";
591
613
  export type { PierreDiffProps } from "./components/pierre-diff";
592
614
  export { PierreFile } from "./components/pierre-file";
593
615
  export type { PierreFileProps } from "./components/pierre-file";
package/src/lib/format.ts CHANGED
@@ -111,20 +111,46 @@ export function tryParseJson(text: string): unknown {
111
111
  export const CREDIT_EXHAUSTION_MESSAGE =
112
112
  "Out of Opengeni credits — this workspace's balance is empty. Add credits to continue; the conversation is preserved.";
113
113
 
114
+ /** A usage ceiling refused the send; the default allowance wording, plus what is safe. */
115
+ export const COMPOSER_MEMBER_ALLOWANCE_MESSAGE =
116
+ "Usage limit reached. A workspace admin can raise this limit. Your draft is preserved.";
117
+ export const COMPOSER_WORKSPACE_ALLOWANCE_MESSAGE =
118
+ "Workspace usage limit reached. An organization admin can raise the workspace budget. Your draft is preserved.";
119
+
114
120
  /**
115
121
  * Actionable composer copy for an edge rejection before a turn is accepted.
116
122
  * Unlike an in-flight credit exhaustion, this path has not consumed the
117
123
  * actor-private draft or any finalized attachment.
118
124
  */
119
125
  export const COMPOSER_PAYMENT_REQUIRED_MESSAGE =
120
- "This turn requires Opengeni managed credits, but the account balance is empty. Add credits or choose a connected Codex subscription model, then retry. Your draft and attachments are preserved.";
126
+ "Your organization doesn't have enough OpenGeni credits to send this message. Add credits or choose a model with another payment source. Your message and attachments are saved.";
121
127
 
122
128
  export function composerSubmissionErrorMessage(error: Error): string {
123
- return error instanceof OpenGeniApiError &&
124
- error.status === 402 &&
125
- error.code === "payment_required"
126
- ? COMPOSER_PAYMENT_REQUIRED_MESSAGE
127
- : error.message;
129
+ if (error instanceof OpenGeniApiError && error.code === "allowance_exhausted") {
130
+ // OpenGeniAllowanceExhaustedError carries the scope; read it structurally
131
+ // so this startup-path helper adds no SDK or wording imports.
132
+ return (error as { scope?: unknown }).scope === "workspace"
133
+ ? COMPOSER_WORKSPACE_ALLOWANCE_MESSAGE
134
+ : COMPOSER_MEMBER_ALLOWANCE_MESSAGE;
135
+ }
136
+ return isComposerCreditRefusal(error) ? COMPOSER_PAYMENT_REQUIRED_MESSAGE : error.message;
137
+ }
138
+
139
+ /** These definitive refusals need a payment or allowance change, not an unchanged retry. */
140
+ export function composerSubmissionCanRetry(error: Error): boolean {
141
+ return !(
142
+ isComposerCreditRefusal(error) ||
143
+ (error instanceof OpenGeniApiError && error.code === "allowance_exhausted")
144
+ );
145
+ }
146
+
147
+ function isComposerCreditRefusal(error: Error): boolean {
148
+ return (
149
+ (error instanceof OpenGeniApiError &&
150
+ error.status === 402 &&
151
+ (error.code === "payment_required" || error.code === "insufficient_credits")) ||
152
+ isCreditExhaustion(error.message)
153
+ );
128
154
  }
129
155
 
130
156
  /**
@@ -0,0 +1,62 @@
1
+ import type { DesktopRfbFactory } from "@opengeni/sdk";
2
+
3
+ /** Host-owned imports keep optional peers outside the root's bundler graph. */
4
+ export type WorkbenchPeerLoader<T = unknown> = () => Promise<T>;
5
+ export type CodeEditorLanguage = "javascript" | "json" | "python" | "markdown" | "css" | "html";
6
+ export type CodeEditorLanguages = Partial<Record<CodeEditorLanguage, WorkbenchPeerLoader>>;
7
+ export type CodeEditorPeers = { module: unknown; languages?: CodeEditorLanguages | undefined };
8
+ export type SandboxTerminalPeers = {
9
+ Terminal: unknown;
10
+ FitAddon: unknown;
11
+ WebLinksAddon: unknown;
12
+ loadWebgl?: WorkbenchPeerLoader | undefined;
13
+ };
14
+
15
+ function optionalPeer<T>(setup: string) {
16
+ let loader: WorkbenchPeerLoader<T> | null = null;
17
+ let loaded: Promise<T> | null = null;
18
+ let version = 0;
19
+ const listeners = new Set<() => void>();
20
+ return {
21
+ register(next: WorkbenchPeerLoader<T> | null): void {
22
+ if (next === loader) return;
23
+ loader = next;
24
+ loaded = null;
25
+ version += 1;
26
+ for (const listener of listeners) listener();
27
+ },
28
+ revision: () => version,
29
+ subscribe(listener: () => void): () => void {
30
+ listeners.add(listener);
31
+ return () => listeners.delete(listener);
32
+ },
33
+ load(): Promise<T> {
34
+ if (!loader) return Promise.reject(new Error(`Call ${setup} to enable this surface`));
35
+ if (!loaded) {
36
+ // A synchronous host-loader failure is also a rejected, retryable load.
37
+ const pending = Promise.resolve()
38
+ .then(loader)
39
+ .catch((error: unknown) => {
40
+ if (loaded === pending) loaded = null;
41
+ throw error;
42
+ });
43
+ loaded = pending;
44
+ }
45
+ return loaded;
46
+ },
47
+ };
48
+ }
49
+
50
+ export const sandboxTerminalPeers = optionalPeer<SandboxTerminalPeers>(
51
+ "enableSandboxTerminal() from @opengeni/react/terminal",
52
+ );
53
+ export const codeEditorPeers = optionalPeer<CodeEditorPeers>(
54
+ "enableCodeEditor() from @opengeni/react/editor",
55
+ );
56
+ export const desktopViewerPeers = optionalPeer<DesktopRfbFactory>(
57
+ "enableDesktopViewer() from @opengeni/react/desktop",
58
+ );
59
+
60
+ export const registerSandboxTerminal = sandboxTerminalPeers.register;
61
+ export const registerCodeEditor = codeEditorPeers.register;
62
+ export const registerDesktopViewer = desktopViewerPeers.register;
package/src/session-ui.ts CHANGED
@@ -13,18 +13,27 @@ export type { ApprovalSurfaceProps, ApprovalSurfaceMessages } from "./components
13
13
  export type { HumanInputSurfaceProps } from "./components/human-input-surface";
14
14
  export { MessageTimeline, TimelineRow } from "./components/message-timeline";
15
15
  export type { MessageTimelineProps } from "./components/message-timeline";
16
+ export type { RenderAllowanceExhausted } from "./timeline/allowance-exhausted-row";
17
+ export type { AllowanceLabels, AllowanceScope } from "./usage/allowance-copy";
16
18
  export {
17
19
  OpenGeniLinkProvider,
18
20
  chainLinkResolvers,
19
21
  sessionLinkResolver,
20
22
  useOpenGeniLinkResolver,
23
+ viewerLinkResolver,
21
24
  } from "./components/open-geni-links";
22
25
  export type {
23
26
  OpenGeniLinkResolution,
24
27
  OpenGeniLinkResolver,
25
28
  OpenGeniLinkTarget,
29
+ OpenGeniViewerTarget,
26
30
  } from "./components/open-geni-links";
27
31
  export { parseOpenGeniLink } from "@opengeni/sdk";
32
+ export {
33
+ ArtifactLabelsProvider,
34
+ DEFAULT_ARTIFACT_LABELS,
35
+ type ArtifactLabels,
36
+ } from "./components/artifacts/artifact-chrome";
28
37
  export type { TimelineSearchTarget } from "./components/timeline-search";
29
38
  export { createOlderHistoryLoadReceipt } from "./older-history";
30
39
  export type { OlderHistoryLoader, OlderHistoryLoadReceipt } from "./older-history";
@@ -43,6 +52,19 @@ export type {
43
52
  TurnSummaryOptions,
44
53
  } from "./timeline/turn-summary";
45
54
  export { QueueSurface } from "./components/queue-surface";
55
+ // The provider too, so a host needs only this entry (the root also exports the
56
+ // workbench, whose editors, terminal and desktop are optional peers).
57
+ export { OpenGeniProvider } from "./provider";
58
+ export type { OpenGeniProviderProps } from "./provider";
59
+ // The tool-renderer registry, so custom tool renderers need no root import.
60
+ export { createDefaultToolRegistry, createToolRegistry, defaultToolRegistry } from "./timeline";
61
+ export type {
62
+ CreateToolRegistryOptions,
63
+ ToolRegistry,
64
+ ToolRegistryEntry,
65
+ ToolRenderer,
66
+ ToolRendererProps,
67
+ } from "./timeline";
46
68
  export { SessionConversation } from "./components/session-conversation";
47
69
  export { SessionList } from "./components/session-list";
48
70
  export type { SessionListLabels, SessionListProps } from "./components/session-list";
@@ -0,0 +1,26 @@
1
+ import {
2
+ registerSandboxTerminal,
3
+ type SandboxTerminalPeers,
4
+ type WorkbenchPeerLoader,
5
+ } from "./lib/workbench-peers";
6
+
7
+ export { SandboxTerminal } from "./components/sandbox-terminal";
8
+ export type { SandboxTerminalProps, XtermTheme } from "./components/sandbox-terminal";
9
+ export { registerSandboxTerminal } from "./lib/workbench-peers";
10
+
11
+ async function loadTerminal(): Promise<SandboxTerminalPeers> {
12
+ const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
13
+ import("@xterm/xterm"),
14
+ import("@xterm/addon-fit"),
15
+ import("@xterm/addon-web-links"),
16
+ ]);
17
+ return { Terminal, FitAddon, WebLinksAddon };
18
+ }
19
+
20
+ /** Opt in once on a terminal route; WebGL remains a separate optional import. */
21
+ export function enableSandboxTerminal(options?: { webgl?: WorkbenchPeerLoader | undefined }): void {
22
+ const loadWebgl = options?.webgl;
23
+ registerSandboxTerminal(
24
+ loadWebgl ? async () => ({ ...(await loadTerminal()), loadWebgl }) : loadTerminal,
25
+ );
26
+ }
@@ -105,7 +105,8 @@ export function ActivityRail({
105
105
  const providerResponded = phases.some(
106
106
  (item) => item.phase === "provider_first_byte" && item.status === "complete",
107
107
  );
108
- const preparing =
108
+ const responsePending = phases.some((item) => item.phase === "provider_first_byte");
109
+ const loading =
109
110
  !hasWork &&
110
111
  !interrupted &&
111
112
  (startupActive ?? (!providerResponded && phases.some((item) => item.status === "running")));
@@ -164,7 +165,7 @@ export function ActivityRail({
164
165
  )}
165
166
  >
166
167
  <AnimatePresence initial={false}>
167
- {preparing && !debug ? (
168
+ {loading && !debug ? (
168
169
  <motion.div
169
170
  key="startup"
170
171
  initial={{ opacity: 0 }}
@@ -182,13 +183,14 @@ export function ActivityRail({
182
183
  >
183
184
  <GenieLoading
184
185
  startedAt={startedAt}
186
+ phase={responsePending ? "waiting" : "preparing"}
185
187
  detailsOpen={detailsOpen}
186
188
  onShowDetails={() => setDetailsOpen((open) => !open)}
187
189
  />
188
190
  </motion.div>
189
191
  ) : null}
190
192
  </AnimatePresence>
191
- {detailsOpen && !debug && !preparing ? (
193
+ {detailsOpen && !debug && !loading ? (
192
194
  <button
193
195
  type="button"
194
196
  className="og-genie-details self-start"
@@ -0,0 +1,86 @@
1
+ import type { AllowanceExhaustedRefusal } from "@opengeni/sdk";
2
+ import { GaugeIcon } from "lucide-react";
3
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
4
+
5
+ import { cn } from "../lib/cn";
6
+ import {
7
+ allowanceLabels,
8
+ allowanceResetSentence,
9
+ formatAllowanceInstant,
10
+ type AllowanceLabels,
11
+ } from "../usage/allowance-copy";
12
+ import { useEntranceAnimation } from "./entrance";
13
+
14
+ /**
15
+ * Replace the built-in "usage limit reached" row. Return `undefined` to keep
16
+ * the default for this refusal (for example, only customize member limits).
17
+ */
18
+ export type RenderAllowanceExhausted = (
19
+ refusal: AllowanceExhaustedRefusal,
20
+ context: { labels: AllowanceLabels; defaultRow: ReactNode },
21
+ ) => ReactNode | undefined;
22
+
23
+ type AllowancePresentation = {
24
+ render: RenderAllowanceExhausted | undefined;
25
+ labels: AllowanceLabels;
26
+ };
27
+
28
+ const AllowancePresentationContext = createContext<AllowancePresentation>({
29
+ render: undefined,
30
+ labels: allowanceLabels(),
31
+ });
32
+
33
+ export function AllowancePresentationProvider({
34
+ render,
35
+ labels,
36
+ children,
37
+ }: {
38
+ render: RenderAllowanceExhausted | undefined;
39
+ labels: Partial<AllowanceLabels> | undefined;
40
+ children: ReactNode;
41
+ }) {
42
+ const value = useMemo(() => ({ render, labels: allowanceLabels(labels) }), [render, labels]);
43
+ return (
44
+ <AllowancePresentationContext.Provider value={value}>
45
+ {children}
46
+ </AllowancePresentationContext.Provider>
47
+ );
48
+ }
49
+
50
+ /**
51
+ * A usage ceiling stopped further work. Calm by design: nothing failed, the
52
+ * conversation is kept, and the row says who can raise the limit and when it
53
+ * resets. Never the server's prose or a member identifier.
54
+ */
55
+ export function AllowanceExhaustedRow({ refusal }: { refusal: AllowanceExhaustedRefusal }) {
56
+ const { render, labels } = useContext(AllowancePresentationContext);
57
+ const enter = useEntranceAnimation();
58
+ const workspace = refusal.scope === "workspace";
59
+ const defaultRow = (
60
+ <div
61
+ className={cn(
62
+ enter && "animate-og-enter",
63
+ "flex items-start gap-2.5 rounded-og-md border border-og-status-waiting/35 bg-og-status-waiting/8 px-3.5 py-2.5 text-og-menu text-og-fg",
64
+ )}
65
+ role="status"
66
+ data-og-allowance-exhausted={refusal.scope}
67
+ >
68
+ <GaugeIcon aria-hidden className="mt-0.5 size-4 shrink-0 text-og-status-waiting" />
69
+ <div className="min-w-0 flex-1">
70
+ <p className="font-medium">
71
+ {workspace ? labels.workspaceLimitReachedTitle : labels.memberLimitReachedTitle}
72
+ </p>
73
+ <p className="mt-0.5 text-og-fg-muted">
74
+ {workspace ? labels.workspaceRemedy : labels.memberRemedy}{" "}
75
+ <span
76
+ title={refusal.resetsAt ? formatAllowanceInstant(refusal.resetsAt) : undefined}
77
+ className="whitespace-nowrap"
78
+ >
79
+ {allowanceResetSentence(labels, refusal.resetsAt)}
80
+ </span>
81
+ </p>
82
+ </div>
83
+ </div>
84
+ );
85
+ return <>{render?.(refusal, { labels, defaultRow }) ?? defaultRow}</>;
86
+ }
@@ -4,6 +4,7 @@ import { createContext, useContext, useEffect, useState, type ReactNode } from "
4
4
 
5
5
  export type GenieLoadingRenderProps = {
6
6
  startedAt: string;
7
+ phase: "preparing" | "waiting";
7
8
  detailsOpen: boolean;
8
9
  onShowDetails: () => void;
9
10
  };
@@ -55,19 +56,23 @@ const PHRASES = [
55
56
  "Putting a wish into motion…",
56
57
  "A little hocus. A little pocus…",
57
58
  ];
59
+ const WAITING_PHRASES = ["Waiting for a response…"];
58
60
 
59
61
  /** Decorative copy never substitutes for a failure or claims measurable progress. */
60
62
  export function GenieLoading({
61
63
  startedAt,
64
+ phase = "preparing",
62
65
  onShowDetails,
63
66
  detailsOpen = false,
64
67
  }: {
65
68
  startedAt: string;
69
+ phase?: GenieLoadingRenderProps["phase"];
66
70
  onShowDetails: () => void;
67
71
  detailsOpen?: boolean;
68
72
  }) {
69
73
  const options = useContext(GenieLoadingOptionsContext);
70
- const phrases = options?.phrases?.length ? options.phrases : PHRASES;
74
+ const waiting = phase === "waiting";
75
+ const phrases = options?.phrases?.length ? options.phrases : waiting ? WAITING_PHRASES : PHRASES;
71
76
  const theme = useThemeType(undefined);
72
77
  const [phrase, setPhrase] = useState(0);
73
78
  const [showDetails, setShowDetails] = useState(false);
@@ -82,7 +87,7 @@ export function GenieLoading({
82
87
  const timer = window.setInterval(update, 5_000);
83
88
  return () => window.clearInterval(timer);
84
89
  }, [startedAt, phrases]);
85
- if (options?.render) return options.render({ startedAt, detailsOpen, onShowDetails });
90
+ if (options?.render) return options.render({ startedAt, phase, detailsOpen, onShowDetails });
86
91
  return (
87
92
  <div className="og-genie-loading">
88
93
  <div
@@ -99,12 +104,17 @@ export function GenieLoading({
99
104
  <div className="og-genie-copy">
100
105
  <span className="sr-only" role="status">
101
106
  {slow
102
- ? (options?.messages?.slowStatus ?? "Preparing your task. Taking longer than usual.")
103
- : (options?.messages?.status ?? "Preparing your task.")}
107
+ ? (options?.messages?.slowStatus ??
108
+ (waiting
109
+ ? "Waiting for a response. Taking longer than usual."
110
+ : "Preparing your task. Taking longer than usual."))
111
+ : (options?.messages?.status ??
112
+ (waiting ? "Waiting for a response." : "Preparing your task."))}
104
113
  </span>
105
114
  <span key={slow ? "slow" : phrase} className="og-genie-phrase" aria-hidden="true">
106
115
  {slow
107
- ? (options?.messages?.slowText ?? "A little longer than usual…")
116
+ ? (options?.messages?.slowText ??
117
+ (waiting ? "Still waiting for a response…" : "A little longer than usual…"))
108
118
  : phrases[phrase % phrases.length]}
109
119
  </span>
110
120
  {showDetails || detailsOpen ? (
@@ -116,7 +126,7 @@ export function GenieLoading({
116
126
  >
117
127
  {detailsOpen
118
128
  ? (options?.messages?.hideDetails ?? "Hide details")
119
- : (options?.messages?.showDetails ?? "Behind the magic")}
129
+ : (options?.messages?.showDetails ?? (waiting ? "Show details" : "Behind the magic"))}
120
130
  <span aria-hidden="true"> ↗</span>
121
131
  </button>
122
132
  ) : null}