@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
@@ -0,0 +1,382 @@
1
+ import type { UsageAllowancePeriod, WorkspaceUsageResponse } from "@opengeni/sdk/usage-allowances";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { useUsage, type UsageClientLike } from "../hooks/use-usage";
5
+ import { cn } from "../lib/cn";
6
+ import {
7
+ allowanceLabels,
8
+ allowanceResetSentence,
9
+ formatAllowanceInstant,
10
+ type AllowanceLabels,
11
+ } from "../usage/allowance-copy";
12
+ import {
13
+ summarizeUsage,
14
+ usagePercentLabel,
15
+ type UsageReading,
16
+ type UsageSummary,
17
+ } from "../usage/summary";
18
+
19
+ export type UsageMeterLabels = AllowanceLabels & {
20
+ /** Heading for the member's own ceiling. */
21
+ title: string;
22
+ /** Heading when only the shared workspace pool applies. */
23
+ workspaceTitle: string;
24
+ /** "38% left" — `percent` is preformatted ("38", "<1"). */
25
+ left: (percent: string) => string;
26
+ /** "62% used". */
27
+ used: (percent: string) => string;
28
+ limitReached: string;
29
+ /** Status chip beside a hero meter that is close to its limit. */
30
+ nearLimit: string;
31
+ /** "$13.00 left" for a hero meter with amounts. */
32
+ amountLeft: (amount: string) => string;
33
+ /** No ceiling applies to this member. */
34
+ unlimited: string;
35
+ loading: string;
36
+ error: string;
37
+ retry: string;
38
+ /** "$31.00 of $50.00 used" when the host passes `formatAmount`. */
39
+ amounts: (used: string, limit: string) => string;
40
+ /** The shared pool, under a member's own meter: "Workspace: 9% left". */
41
+ workspaceLine: (value: string) => string;
42
+ };
43
+
44
+ export const DEFAULT_USAGE_METER_LABELS: UsageMeterLabels = {
45
+ ...allowanceLabels(),
46
+ title: "Your usage",
47
+ workspaceTitle: "Workspace usage",
48
+ left: (percent) => `${percent}% left`,
49
+ used: (percent) => `${percent}% used`,
50
+ limitReached: "Limit reached",
51
+ nearLimit: "Near limit",
52
+ amountLeft: (amount) => `${amount} left`,
53
+ unlimited: "No usage limit",
54
+ loading: "Checking usage…",
55
+ error: "Couldn't load usage",
56
+ retry: "Try again",
57
+ amounts: (used, limit) => `${used} of ${limit} used`,
58
+ workspaceLine: (value) => `Workspace: ${value}`,
59
+ };
60
+
61
+ export type UsageMeterBaseProps = {
62
+ /** "left" (default) reads like a battery; "used" reads like spend. */
63
+ measure?: "left" | "used" | undefined;
64
+ /**
65
+ * Opt in to amounts. Receives USD micros; return your own unit ("$31.00",
66
+ * "3.1 credits", "1.5× plan"). Without it the meter shows only shares.
67
+ */
68
+ formatAmount?: ((micros: number) => string) | undefined;
69
+ labels?: Partial<UsageMeterLabels> | undefined;
70
+ /**
71
+ * `compact` is one line for menus and headers; `hero` leads a page with the
72
+ * amount (or share) left in large type. An empty `labels.title` hides the
73
+ * heading when the page already names it.
74
+ */
75
+ density?: "default" | "compact" | "hero" | undefined;
76
+ /** Show the shared pool under the member's own meter when it is the tighter one. Default true. */
77
+ showWorkspace?: boolean | undefined;
78
+ className?: string | undefined;
79
+ };
80
+
81
+ export type UsageMeterProps = UsageMeterBaseProps & {
82
+ /**
83
+ * A usage response you already loaded (for example from `useUsage`).
84
+ * Omit it to let the meter read `/usage/me` itself.
85
+ */
86
+ usage?: WorkspaceUsageResponse | null | undefined;
87
+ client?: UsageClientLike | undefined;
88
+ workspaceId?: string | undefined;
89
+ period?: UsageAllowancePeriod | undefined;
90
+ /** Re-read when this changes (see `useUsage`). */
91
+ refreshKey?: unknown;
92
+ };
93
+
94
+ /**
95
+ * The signed-in member's own usage: how much of their limit is left and when
96
+ * it resets. Shares only by default; hosts opt in to amounts.
97
+ */
98
+ export function UsageMeter({
99
+ usage,
100
+ client,
101
+ workspaceId,
102
+ period,
103
+ refreshKey,
104
+ ...view
105
+ }: UsageMeterProps) {
106
+ if (usage !== undefined) {
107
+ return (
108
+ <UsageMeterView
109
+ {...view}
110
+ summary={usage ? summarizeUsage(usage) : null}
111
+ loading={usage === null}
112
+ />
113
+ );
114
+ }
115
+ return (
116
+ <LoadingUsageMeter
117
+ {...view}
118
+ client={client}
119
+ workspaceId={workspaceId}
120
+ period={period}
121
+ refreshKey={refreshKey}
122
+ />
123
+ );
124
+ }
125
+
126
+ function LoadingUsageMeter({
127
+ client,
128
+ workspaceId,
129
+ period,
130
+ refreshKey,
131
+ ...view
132
+ }: UsageMeterBaseProps &
133
+ Pick<UsageMeterProps, "client" | "workspaceId" | "period" | "refreshKey">) {
134
+ const state = useUsage({ client, workspaceId, period, refreshKey });
135
+ return (
136
+ <UsageMeterView
137
+ {...view}
138
+ summary={state.summary}
139
+ loading={state.loading && !state.summary}
140
+ error={state.summary ? null : state.error}
141
+ onRetry={() => void state.refresh()}
142
+ />
143
+ );
144
+ }
145
+
146
+ export type UsageMeterViewProps = UsageMeterBaseProps & {
147
+ summary: UsageSummary | null;
148
+ loading?: boolean | undefined;
149
+ error?: Error | null | undefined;
150
+ onRetry?: (() => void) | undefined;
151
+ };
152
+
153
+ const FILL: Record<UsageReading["status"], string> = {
154
+ ok: "bg-og-accent",
155
+ warning: "bg-og-status-waiting",
156
+ exhausted: "bg-og-danger",
157
+ };
158
+
159
+ function valueText(
160
+ reading: UsageReading,
161
+ measure: "left" | "used",
162
+ labels: UsageMeterLabels,
163
+ ): string {
164
+ if (reading.status === "exhausted") return labels.limitReached;
165
+ return measure === "left"
166
+ ? labels.left(usagePercentLabel(Math.max(0, 1 - reading.fraction)))
167
+ : labels.used(usagePercentLabel(reading.fraction));
168
+ }
169
+
170
+ /** Presentational meter for a summary you computed (`summarizeUsage`). */
171
+ export function UsageMeterView({
172
+ summary,
173
+ loading = false,
174
+ error = null,
175
+ onRetry,
176
+ measure = "left",
177
+ formatAmount,
178
+ labels: overrides,
179
+ density = "default",
180
+ showWorkspace = true,
181
+ className,
182
+ }: UsageMeterViewProps) {
183
+ const labels = { ...DEFAULT_USAGE_METER_LABELS, ...overrides };
184
+ const compact = density === "compact";
185
+ if (loading || error || !summary) {
186
+ return (
187
+ <div
188
+ className={cn("og-root min-w-0 text-og-sm", className)}
189
+ data-og-usage-meter={error ? "error" : "loading"}
190
+ aria-busy={loading || undefined}
191
+ >
192
+ {error ? (
193
+ <p className="flex flex-wrap items-center gap-x-2 text-og-fg-muted" role="alert">
194
+ {labels.error}
195
+ {onRetry ? (
196
+ <button
197
+ type="button"
198
+ onClick={onRetry}
199
+ className="rounded-og-sm text-og-fg underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-og-accent"
200
+ >
201
+ {labels.retry}
202
+ </button>
203
+ ) : null}
204
+ </p>
205
+ ) : (
206
+ <span className="text-og-fg-subtle">{labels.loading}</span>
207
+ )}
208
+ </div>
209
+ );
210
+ }
211
+ const primary = summary.member ?? summary.workspace;
212
+ if (!primary) {
213
+ return (
214
+ <div
215
+ className={cn("og-root min-w-0 text-og-sm text-og-fg-muted", className)}
216
+ data-og-usage-meter="unlimited"
217
+ >
218
+ {compact ? null : <span className="font-medium text-og-fg">{labels.title} · </span>}
219
+ {labels.unlimited}
220
+ </div>
221
+ );
222
+ }
223
+ const title = summary.member ? labels.title : labels.workspaceTitle;
224
+ const value = valueText(primary, measure, labels);
225
+ // The bar always fills with what's used, so "near the limit" reads as a
226
+ // nearly full bar whichever way the words count.
227
+ const fill = Math.min(1, Math.max(0, primary.fraction));
228
+ const reset = allowanceResetSentence(labels, primary.resetsAt);
229
+ const pool =
230
+ showWorkspace &&
231
+ summary.member &&
232
+ summary.workspace &&
233
+ (summary.binding?.scope === "workspace" || summary.workspace.status !== "ok")
234
+ ? summary.workspace
235
+ : null;
236
+ const meterProps = {
237
+ role: "meter" as const,
238
+ "aria-label": title || labels.title,
239
+ "aria-valuemin": 0,
240
+ "aria-valuemax": 100,
241
+ "aria-valuenow": Math.round(fill * 100),
242
+ "aria-valuetext": `${value}. ${reset}${
243
+ pool ? ` ${labels.workspaceLine(valueText(pool, measure, labels))}.` : ""
244
+ }`,
245
+ };
246
+ const bar = (
247
+ <span
248
+ aria-hidden
249
+ className={cn(
250
+ "relative block overflow-hidden rounded-full",
251
+ density === "hero" ? "h-1.5" : "h-1",
252
+ primary.status === "exhausted" ? "bg-og-danger/15" : "bg-og-surface-3",
253
+ compact ? "w-16 shrink-0" : "w-full",
254
+ )}
255
+ >
256
+ <span
257
+ className={cn("absolute inset-y-0 left-0 rounded-full", FILL[primary.status])}
258
+ style={{ width: `${Math.round(fill * 1000) / 10}%` }}
259
+ />
260
+ </span>
261
+ );
262
+ const valueClass = cn(
263
+ "shrink-0 whitespace-nowrap tabular-nums",
264
+ primary.status === "ok" ? "text-og-fg-muted" : "font-medium",
265
+ primary.status === "warning" && "text-og-status-waiting",
266
+ primary.status === "exhausted" && "text-og-danger",
267
+ );
268
+ if (compact) {
269
+ return (
270
+ <span
271
+ {...meterProps}
272
+ data-og-usage-meter={primary.status}
273
+ title={primary.resetsAt ? `${reset} ${formatAllowanceInstant(primary.resetsAt)}` : reset}
274
+ className={cn("og-root inline-flex min-w-0 items-center gap-2 text-og-xs", className)}
275
+ >
276
+ <span className={valueClass}>{value}</span>
277
+ {bar}
278
+ </span>
279
+ );
280
+ }
281
+ const amounts = formatAmount
282
+ ? labels.amounts(formatAmount(primary.used), formatAmount(primary.limit))
283
+ : null;
284
+ if (density === "hero") {
285
+ // Lead with what's left, even at zero; the chip names the state.
286
+ const headline = formatAmount
287
+ ? labels.amountLeft(formatAmount(primary.remaining))
288
+ : labels.left(usagePercentLabel(Math.max(0, 1 - primary.fraction)));
289
+ return (
290
+ <div
291
+ {...meterProps}
292
+ data-og-usage-meter={primary.status}
293
+ className={cn("og-root flex min-w-0 flex-col gap-3", className)}
294
+ >
295
+ {title ? <span className="text-og-sm font-medium text-og-fg-muted">{title}</span> : null}
296
+ <div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
297
+ <span className="text-2xl leading-8 font-semibold tracking-[-0.5px] text-og-fg tabular-nums">
298
+ {headline}
299
+ </span>
300
+ {primary.status !== "ok" ? (
301
+ <span
302
+ className={cn(
303
+ "inline-flex h-[22px] items-center gap-1.5 rounded-full border px-2 text-og-xs font-medium",
304
+ primary.status === "warning"
305
+ ? "border-og-status-waiting/30 text-og-status-waiting"
306
+ : "border-og-danger/30 text-og-danger",
307
+ )}
308
+ >
309
+ <span
310
+ aria-hidden
311
+ className={cn(
312
+ "size-1.5 rounded-full",
313
+ primary.status === "warning" ? "bg-og-status-waiting" : "bg-og-danger",
314
+ )}
315
+ />
316
+ {primary.status === "warning" ? labels.nearLimit : labels.limitReached}
317
+ </span>
318
+ ) : null}
319
+ </div>
320
+ {bar}
321
+ <p className="text-og-xs text-og-fg-muted tabular-nums">
322
+ {[
323
+ amounts ?? labels.used(usagePercentLabel(primary.fraction)),
324
+ amounts ? `${usagePercentLabel(primary.fraction)}%` : null,
325
+ ]
326
+ .filter(Boolean)
327
+ .join(" · ")}{" "}
328
+ ·{" "}
329
+ <span title={primary.resetsAt ? formatAllowanceInstant(primary.resetsAt) : undefined}>
330
+ {reset}
331
+ </span>
332
+ </p>
333
+ {pool ? <PoolLine reading={pool} measure={measure} labels={labels} /> : null}
334
+ </div>
335
+ );
336
+ }
337
+ return (
338
+ <div
339
+ {...meterProps}
340
+ data-og-usage-meter={primary.status}
341
+ className={cn("og-root flex min-w-0 flex-col gap-2 text-og-sm", className)}
342
+ >
343
+ <div className="flex min-w-0 items-baseline justify-between gap-3">
344
+ <span className="min-w-0 truncate font-medium text-og-fg">{title}</span>
345
+ <span className={valueClass}>{value}</span>
346
+ </div>
347
+ {bar}
348
+ <div className="flex min-w-0 flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-og-xs text-og-fg-subtle">
349
+ <span title={primary.resetsAt ? formatAllowanceInstant(primary.resetsAt) : undefined}>
350
+ {reset}
351
+ </span>
352
+ {amounts ? <span className="tabular-nums">{amounts}</span> : null}
353
+ </div>
354
+ {pool ? <PoolLine reading={pool} measure={measure} labels={labels} /> : null}
355
+ </div>
356
+ );
357
+ }
358
+
359
+ function PoolLine({
360
+ reading,
361
+ measure,
362
+ labels,
363
+ }: {
364
+ reading: UsageReading;
365
+ measure: "left" | "used";
366
+ labels: UsageMeterLabels;
367
+ }): ReactNode {
368
+ return (
369
+ <p
370
+ className={cn(
371
+ "text-og-xs",
372
+ reading.status === "exhausted"
373
+ ? "font-medium text-og-danger"
374
+ : reading.status === "warning"
375
+ ? "text-og-status-waiting"
376
+ : "text-og-fg-muted",
377
+ )}
378
+ >
379
+ {labels.workspaceLine(valueText(reading, measure, labels))}
380
+ </p>
381
+ );
382
+ }
@@ -8,7 +8,10 @@ export function conversationTimeline(
8
8
  queue: Pick<UseTurnQueueResult, "queue" | "snapshot" | "acceptedSteers">,
9
9
  composer: Pick<
10
10
  ComposerState,
11
- "optimisticMessages" | "retryOptimisticMessage" | "removeOptimisticMessage"
11
+ | "optimisticMessages"
12
+ | "retryOptimisticMessage"
13
+ | "restoreOptimisticMessage"
14
+ | "removeOptimisticMessage"
12
15
  >,
13
16
  ): TimelineItem[] {
14
17
  const queued = new Set(
@@ -61,7 +64,12 @@ export function conversationTimeline(
61
64
  ...(message.error ? { error: message.error } : {}),
62
65
  ...(message.state === "failed"
63
66
  ? {
64
- onRetry: () => composer.retryOptimisticMessage?.(message.clientEventId),
67
+ ...(message.retryable !== false && composer.retryOptimisticMessage
68
+ ? { onRetry: () => composer.retryOptimisticMessage?.(message.clientEventId) }
69
+ : {}),
70
+ ...(message.retryable === false && composer.restoreOptimisticMessage
71
+ ? { onEdit: () => composer.restoreOptimisticMessage?.(message.clientEventId) }
72
+ : {}),
65
73
  onRemove: () => composer.removeOptimisticMessage?.(message.clientEventId),
66
74
  }
67
75
  : {}),
package/src/desktop.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { DesktopRfbFactory, DesktopRfbLike } from "@opengeni/sdk";
2
+ import { registerDesktopViewer } from "./lib/workbench-peers";
3
+
4
+ export { DesktopViewer } from "./components/desktop-viewer";
5
+ export type { DesktopViewerProps } from "./components/desktop-viewer";
6
+ export { useDesktopStream } from "./hooks/use-desktop-stream";
7
+ export type { UseDesktopStreamOptions, UseDesktopStreamResult } from "./hooks/use-desktop-stream";
8
+ export { registerDesktopViewer } from "./lib/workbench-peers";
9
+
10
+ async function loadDesktop(): Promise<DesktopRfbFactory> {
11
+ const mod = (await import("@novnc/novnc")) as unknown as {
12
+ default: new (
13
+ target: HTMLElement,
14
+ url: string,
15
+ options: Parameters<DesktopRfbFactory>[2],
16
+ ) => DesktopRfbLike;
17
+ };
18
+ return (target, url, options) => new mod.default(target, url, options);
19
+ }
20
+
21
+ /** Opt in once on a VNC route; relay-frame viewers need no noVNC peer. */
22
+ export function enableDesktopViewer(): void {
23
+ registerDesktopViewer(loadDesktop);
24
+ }
package/src/editor.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { registerCodeEditor, type CodeEditorLanguages } from "./lib/workbench-peers";
2
+
3
+ export { CodeEditor, languageForPath } from "./components/code-editor";
4
+ export type { CodeEditorProps } from "./components/code-editor";
5
+ export { registerCodeEditor } from "./lib/workbench-peers";
6
+ export type { CodeEditorLanguage, CodeEditorLanguages } from "./lib/workbench-peers";
7
+
8
+ const loadEditor = async () => ({ module: await import("@uiw/react-codemirror") });
9
+
10
+ /** Only name installed grammar peers in host-supplied language loaders. */
11
+ export function enableCodeEditor(languages?: CodeEditorLanguages): void {
12
+ registerCodeEditor(languages ? async () => ({ ...(await loadEditor()), languages }) : loadEditor);
13
+ }
@@ -72,7 +72,7 @@ export function useWorkspaceModelCatalog(
72
72
  }
73
73
  const [catalog, config] = await Promise.all([
74
74
  client.getWorkspaceModelCatalog(options.workspaceId, { signal }),
75
- client.getClientConfig({ signal }),
75
+ client.getClientConfig({ signal, workspaceId: options.workspaceId }),
76
76
  ]);
77
77
  return {
78
78
  models: catalog.models,
@@ -15,6 +15,7 @@ import {
15
15
  } from "@opengeni/sdk";
16
16
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
17
17
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
18
+ import { composerSubmissionCanRetry, composerSubmissionErrorMessage } from "../lib/format";
18
19
  import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
19
20
 
20
21
  export type ComposerPolicy = {
@@ -97,6 +98,8 @@ export type ComposerOptimisticMessage = {
97
98
  triggerEventId?: string | null | undefined;
98
99
  appliedQueueVersion?: number | null | undefined;
99
100
  error?: string | undefined;
101
+ /** False for a definitive refusal that requires changing payment or allowance settings. */
102
+ retryable?: boolean | undefined;
100
103
  outcomeUnknown?: boolean | undefined;
101
104
  };
102
105
 
@@ -185,7 +188,14 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
185
188
  error:
186
189
  operation.state === "sending"
187
190
  ? "Delivery was interrupted; retry to reconcile this message."
188
- : operation.error,
191
+ : operation.error
192
+ ? composerSubmissionErrorMessage(new Error(operation.error))
193
+ : undefined,
194
+ retryable:
195
+ operation.state === "sending" || operation.outcomeUnknown
196
+ ? true
197
+ : (operation.retryable ??
198
+ (operation.error ? composerSubmissionCanRetry(new Error(operation.error)) : true)),
189
199
  input: operation.input,
190
200
  canRetry: !hasMcpCredentialUpdates,
191
201
  }));
@@ -433,6 +443,8 @@ export type ComposerState = {
433
443
  /** Locally acknowledged ordinary sends awaiting durable timeline reconciliation. */
434
444
  optimisticMessages?: ComposerOptimisticMessage[] | undefined;
435
445
  retryOptimisticMessage?: ((clientEventId: string) => void) | undefined;
446
+ /** Move a definitively refused message into an empty composer without resending it. */
447
+ restoreOptimisticMessage?: ((clientEventId: string) => void) | undefined;
436
448
  removeOptimisticMessage?: ((clientEventId: string) => void) | undefined;
437
449
  /** Supersede current direction with the draft. */
438
450
  steer: (text?: string) => Promise<boolean>;
@@ -1379,7 +1391,8 @@ export function useComposer(
1379
1391
  ? {
1380
1392
  ...candidate,
1381
1393
  state: "failed",
1382
- error: problem.message,
1394
+ error: outcomeUnknown ? problem.message : composerSubmissionErrorMessage(problem),
1395
+ retryable: outcomeUnknown || composerSubmissionCanRetry(problem),
1383
1396
  outcomeUnknown,
1384
1397
  }
1385
1398
  : candidate,
@@ -2138,6 +2151,7 @@ export function useComposer(
2138
2151
  if (operation.outcomeUnknown) {
2139
2152
  return { ...operation, state: "sending", error: undefined };
2140
2153
  }
2154
+ if (operation.retryable === false) return operation;
2141
2155
  const nextClientEventId = generateClientEventId();
2142
2156
  const currentPersonalResourceAttachment = resolveSendExtras(
2143
2157
  sendExtrasRef.current,
@@ -2167,6 +2181,7 @@ export function useComposer(
2167
2181
  occurredAt: new Date().toISOString(),
2168
2182
  state: "sending",
2169
2183
  error: undefined,
2184
+ retryable: undefined,
2170
2185
  outcomeUnknown: false,
2171
2186
  canRetry: true,
2172
2187
  };
@@ -2573,6 +2588,42 @@ export function useComposer(
2573
2588
  );
2574
2589
  }, []);
2575
2590
 
2591
+ const restoreOptimisticMessage = useCallback(
2592
+ (clientEventId: string): void => {
2593
+ if (
2594
+ targetKeyRef.current !== targetKey ||
2595
+ draftLoading ||
2596
+ pendingOperationRef.current ||
2597
+ hasDraftContent()
2598
+ )
2599
+ return;
2600
+ const operation = optimisticSendsRef.current.find(
2601
+ (candidate) => candidate.clientEventId === clientEventId,
2602
+ );
2603
+ if (!operation || operation.state !== "failed" || operation.outcomeUnknown) return;
2604
+ // Restoring is a new draft edit, not a retry. Keep the user's current model
2605
+ // selection and never copy credentials from the old rejected request.
2606
+ localEditRevision.current += 1;
2607
+ valueRef.current = operation.text;
2608
+ restoredResourcesRef.current = [...operation.resources];
2609
+ annotationsRef.current = cloneAnnotations(operation.annotations);
2610
+ setValue(operation.text);
2611
+ setRestoredResources(restoredResourcesRef.current);
2612
+ setAnnotations(annotationsRef.current);
2613
+ setError(null);
2614
+ setOptimisticDraftShadow({
2615
+ text: operation.text,
2616
+ resources: restoredResourcesRef.current,
2617
+ annotations: annotationsRef.current,
2618
+ policy: policyRef.current ?? undefined,
2619
+ });
2620
+ replaceOptimisticSends((current) =>
2621
+ current.filter((candidate) => candidate.clientEventId !== clientEventId),
2622
+ );
2623
+ },
2624
+ [draftLoading, hasDraftContent, replaceOptimisticSends, setOptimisticDraftShadow, targetKey],
2625
+ );
2626
+
2576
2627
  const resolveDraftConflict = useCallback(
2577
2628
  async (choice: "keep_mine" | "use_remote"): Promise<void> => {
2578
2629
  const ownedTargetKey = targetKey;
@@ -2676,6 +2727,10 @@ export function useComposer(
2676
2727
  send,
2677
2728
  optimisticMessages: visibleOptimisticMessages,
2678
2729
  retryOptimisticMessage,
2730
+ restoreOptimisticMessage:
2731
+ !draftLoading && !pendingOperationRef.current && !hasDraftContent()
2732
+ ? restoreOptimisticMessage
2733
+ : undefined,
2679
2734
  removeOptimisticMessage,
2680
2735
  steer,
2681
2736
  steering: visibleSteering,
@@ -6,7 +6,15 @@ import {
6
6
  type DesktopRfbLike,
7
7
  type DesktopStreamCapability,
8
8
  } from "@opengeni/sdk";
9
- import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
9
+ import {
10
+ type RefObject,
11
+ useCallback,
12
+ useEffect,
13
+ useRef,
14
+ useState,
15
+ useSyncExternalStore,
16
+ } from "react";
17
+ import { desktopViewerPeers } from "../lib/workbench-peers";
10
18
  import { type DesktopWebSocketFactory, useRelayFrameStream } from "./use-relay-frame-stream";
11
19
 
12
20
  export type UseDesktopStreamOptions = {
@@ -17,7 +25,7 @@ export type UseDesktopStreamOptions = {
17
25
  /** Read-only by default (v1 ruling H). interactive only when cap.mode allows. */
18
26
  interactive?: boolean | undefined;
19
27
  scaleViewport?: boolean | undefined;
20
- /** Custom RFB factory (tests / a WebRTC swap). Defaults to a lazy @novnc/novnc. */
28
+ /** Custom RFB factory. Otherwise call enableDesktopViewer() on the desktop subpath. */
21
29
  rfbFactory?: DesktopRfbFactory | undefined;
22
30
  /** Additional WebSocket protocols for an authenticated placement proxy. */
23
31
  webSocketProtocols?: string[] | undefined;
@@ -35,29 +43,6 @@ export type UseDesktopStreamResult = {
35
43
  sendKey?: ((keysym: number, code: string, down?: boolean) => boolean) | undefined;
36
44
  };
37
45
 
38
- /** Lazy-load @novnc/novnc's RFB as the default factory. Imported inside the
39
- * connect effect so SSR / non-desktop bundles never pull the DOM-only lib.
40
- * @novnc/novnc ships no types and a single `export default class RFB`. The
41
- * specifier is STATIC (`import("@novnc/novnc")`) so Vite can pre-bundle and
42
- * resolve it — a runtime-string indirection with `@vite-ignore` (the previous
43
- * approach) hands the browser a bare specifier and throws
44
- * "Failed to resolve module specifier '@novnc/novnc'". The dynamic form keeps
45
- * it out of the SSR / non-desktop critical path while staying resolvable. */
46
- async function defaultRfbFactory(): Promise<DesktopRfbFactory> {
47
- const mod = (await import("@novnc/novnc")) as unknown as {
48
- default: new (
49
- t: HTMLElement,
50
- u: string,
51
- o: {
52
- credentials?: { password?: string | undefined } | undefined;
53
- wsProtocols?: string[] | undefined;
54
- },
55
- ) => DesktopRfbLike;
56
- };
57
- const RFB = mod.default;
58
- return (target, url, opts) => new RFB(target, url, opts);
59
- }
60
-
61
46
  /**
62
47
  * Drive the noVNC RFB lifecycle from a `DesktopStreamCapability`, using the
63
48
  * SDK's `desktop.ts` reducer + `desktopSocketUrl`. SSR-safe: the RFB import and
@@ -87,6 +72,12 @@ export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopSt
87
72
  webSocketProtocols,
88
73
  webSocketFactory,
89
74
  } = options;
75
+ const peerRevision = useSyncExternalStore(
76
+ desktopViewerPeers.subscribe,
77
+ desktopViewerPeers.revision,
78
+ desktopViewerPeers.revision,
79
+ );
80
+ const factoryRevision = rfbFactory ? 0 : peerRevision;
90
81
 
91
82
  // The self-hosted PNG-frame path. Always invoked (rules of hooks); dormant
92
83
  // unless `transport === "relay-frames"`, in which case it owns the surface.
@@ -188,7 +179,7 @@ export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopSt
188
179
 
189
180
  void (async () => {
190
181
  try {
191
- const factory = rfbFactoryRef.current ?? (await defaultRfbFactory());
182
+ const factory = rfbFactoryRef.current ?? (await desktopViewerPeers.load());
192
183
  if (disposed) return;
193
184
  const socketUrl = desktopSocketUrl({ url });
194
185
  setBoth(nextDesktopState(stateRef.current, { type: "negotiated" }));
@@ -232,11 +223,11 @@ export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopSt
232
223
  };
233
224
  // ONLY a real transport change reconnects: a fresh url (rotation), a new
234
225
  // credential (RFB password OR bearer subprotocol), the transport flipping,
235
- // or an explicit manual reconnect
226
+ // a newly registered default peer loader, or an explicit manual reconnect
236
227
  // (`nonce`). `interactive`/`scaleViewport`/`mode`/`rfbFactory` are read via
237
228
  // refs and applied live below — they must never re-open the socket.
238
229
  // eslint-disable-next-line react-hooks/exhaustive-deps
239
- }, [url, token, transport, webSocketProtocolsKey, nonce]);
230
+ }, [url, token, transport, webSocketProtocolsKey, nonce, factoryRevision]);
240
231
 
241
232
  // Apply take-control / return-control to the OPEN connection in place. noVNC
242
233
  // honours `viewOnly` live, so flipping it neither blinks the surface nor drops