@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
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  parseMediaGenerationResult,
3
3
  parseToolDisplayMetadata,
4
+ EMPTY_FINAL_REPLY_NOTICE,
5
+ turnCompletedWithEmptyFinalReply,
4
6
  type HumanInputAnswer,
5
7
  type HumanInputQuestion,
6
8
  type HumanInputResponse,
@@ -9,6 +11,10 @@ import {
9
11
  type TimelineAnnotation,
10
12
  } from "@opengeni/sdk";
11
13
  import fleetDecisionItem from "./fleet-decision-projection";
14
+ import {
15
+ allowanceExhaustedMessage,
16
+ parseAllowanceExhaustedRefusal,
17
+ } from "@opengeni/sdk/allowance-refusal";
12
18
  import {
13
19
  CREDIT_EXHAUSTION_MESSAGE,
14
20
  presentFailure,
@@ -145,6 +151,34 @@ export function buildTimeline(
145
151
  options: { partialStart?: boolean } = {},
146
152
  ): TimelineItem[] {
147
153
  const items: TimelineItem[] = [];
154
+ const allowanceNotices = new Set<string>();
155
+ const allowanceNotice = (event: SessionEvent, turnId: string | null) => {
156
+ const refusal = parseAllowanceExhaustedRefusal(event.payload);
157
+ if (!refusal) return null;
158
+ const text = allowanceExhaustedMessage(refusal);
159
+ const key = JSON.stringify([
160
+ turnId,
161
+ event.turnAttemptId,
162
+ startupRecoveryRevisionByTurn.get(turnId ?? "") ?? 0,
163
+ refusal.scope,
164
+ refusal.subjectId,
165
+ refusal.resetsAt,
166
+ ]);
167
+ if (!allowanceNotices.has(key)) {
168
+ allowanceNotices.add(key);
169
+ items.push({
170
+ kind: "notice",
171
+ id: `${event.id}-allowance`,
172
+ tone: "failed",
173
+ text,
174
+ // The canonical sentence replaces upstream prose, so a host renderer
175
+ // reading `message` never shows a wrapper's private wording.
176
+ allowance: { ...refusal, message: text },
177
+ occurredAt: event.occurredAt,
178
+ });
179
+ }
180
+ return text;
181
+ };
148
182
  const prescan = prescanTurnAnchors(events);
149
183
  const ordered = orderTimelineEvents(events, prescan);
150
184
  // A bounded replay can begin halfway through a message (including inside an
@@ -1200,6 +1234,11 @@ export function buildTimeline(
1200
1234
  break;
1201
1235
  }
1202
1236
 
1237
+ case "usage.exhausted": {
1238
+ allowanceNotice(event, turnId);
1239
+ break;
1240
+ }
1241
+
1203
1242
  case "turn.completed": {
1204
1243
  // A standalone manual compaction uses the turn ledger for fencing and
1205
1244
  // recovery, but it is maintenance rather than a conversational turn.
@@ -1218,6 +1257,15 @@ export function buildTimeline(
1218
1257
  // Rendering it as a clean "complete" turn is a lie that leaves the
1219
1258
  // session looking healthy while every future turn silently dies, so it
1220
1259
  // projects exactly like a failed turn plus an explicit notice.
1260
+ const allowance = parseAllowanceExhaustedRefusal(payload);
1261
+ if (allowance) {
1262
+ finalizeOpen(turnId, "complete", event.occurredAt);
1263
+ takeAgentResponse(turnId);
1264
+ takePendingWaitOutcome(turnId);
1265
+ items.push(turnEndItem(event, "failed", ALLOWANCE_TURN_END_TEXT));
1266
+ allowanceNotice(event, turnId);
1267
+ break;
1268
+ }
1221
1269
  if (isCreditExhaustionPayload(payload)) {
1222
1270
  finalizeOpen(turnId, "complete", event.occurredAt);
1223
1271
  takeAgentResponse(turnId);
@@ -1283,6 +1331,16 @@ export function buildTimeline(
1283
1331
  finalizeOpen(turnId, "complete", event.occurredAt);
1284
1332
  const completedTurn = turnEndItem(event, "complete", null);
1285
1333
  items.push(completedTurn);
1334
+ if (turnCompletedWithEmptyFinalReply(payload)) {
1335
+ items.push({
1336
+ kind: "notice",
1337
+ id: `${event.id}-empty-final-reply`,
1338
+ tone: "input",
1339
+ text: EMPTY_FINAL_REPLY_NOTICE,
1340
+ recordedOutcome: true,
1341
+ occurredAt: event.occurredAt,
1342
+ });
1343
+ }
1286
1344
  const hasCompletedFinalResponse =
1287
1345
  latestAgentResponse?.completed === true &&
1288
1346
  latestAgentResponse.item.phase !== "commentary" &&
@@ -1317,16 +1375,21 @@ export function buildTimeline(
1317
1375
  // Credit death can hide behind fields `failureMessage` doesn't read
1318
1376
  // (detail/segmentLimit), so classify the whole payload before falling
1319
1377
  // back to the generic error/message extraction.
1320
- const failureText = isCreditExhaustionPayload(payload)
1321
- ? CREDIT_EXHAUSTION_MESSAGE
1322
- : failureMessage(payload);
1378
+ const allowance = parseAllowanceExhaustedRefusal(payload);
1379
+ const failureText = allowance
1380
+ ? ALLOWANCE_TURN_END_TEXT
1381
+ : isCreditExhaustionPayload(payload)
1382
+ ? CREDIT_EXHAUSTION_MESSAGE
1383
+ : failureMessage(payload);
1323
1384
  // The TURN failed — the in-flight items did not. Chip doctrine: red is
1324
1385
  // spent once, on the turn-level outcome. Items caught mid-flight read
1325
1386
  // as calm "interrupted" (same as turn.cancelled); an item that itself
1326
1387
  // failed keeps its own failed status from its output event.
1327
1388
  finalizeOpen(turnId, "cancelled", event.occurredAt);
1328
1389
  items.push(turnEndItem(event, "failed", failureText));
1329
- if (!hadActivity) {
1390
+ if (allowance) {
1391
+ allowanceNotice(event, turnId);
1392
+ } else if (!hadActivity) {
1330
1393
  items.push({
1331
1394
  kind: "notice",
1332
1395
  id: event.id,
@@ -1688,8 +1751,16 @@ export function stripOpaqueCitationTokens(text: string): string {
1688
1751
  return text.replace(/\s*cite(?:[^]+)+/gu, "");
1689
1752
  }
1690
1753
 
1754
+ /**
1755
+ * The turn summary's short outcome for a usage ceiling. The adjacent notice row
1756
+ * carries who can raise it and when it resets (and is host-customizable), so
1757
+ * the summary line stays a neutral fact instead of repeating remedy prose.
1758
+ */
1759
+ const ALLOWANCE_TURN_END_TEXT = "Usage limit reached";
1760
+
1691
1761
  /** The turn-end payload shape, as `isCreditExhaustion` wants it. */
1692
1762
  function isCreditExhaustionPayload(payload: Record<string, unknown>): boolean {
1763
+ if (payload.code === "allowance_exhausted") return false;
1693
1764
  return isCreditExhaustion({
1694
1765
  error: typeof payload.error === "string" ? payload.error : null,
1695
1766
  detail: typeof payload.detail === "string" ? payload.detail : null,
@@ -2348,6 +2419,9 @@ export function isTurnExecutionEvidence(type: string): boolean {
2348
2419
  isAgentActivityEvent(type) ||
2349
2420
  type === "turn.completed" ||
2350
2421
  type === "turn.failed" ||
2422
+ // An admission refusal can settle a queued turn before it ever starts:
2423
+ // its prompt still belongs above the "usage limit reached" row.
2424
+ type === "usage.exhausted" ||
2351
2425
  type === "turn.superseded" ||
2352
2426
  type === "turn.recovery.requested" ||
2353
2427
  type === "turn.capacity_waiting" ||
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AllowanceExhaustedRefusal,
2
3
  HumanInputQuestion,
3
4
  HumanInputResponse,
4
5
  MediaGenerationResult,
@@ -56,6 +57,7 @@ export type UserMessageItem = {
56
57
  state: "sending" | "queued" | "failed";
57
58
  error?: string | undefined;
58
59
  onRetry?: (() => void) | undefined;
60
+ onEdit?: (() => void) | undefined;
59
61
  onRemove?: (() => void) | undefined;
60
62
  }
61
63
  | undefined;
@@ -391,6 +393,12 @@ export type NoticeItem = {
391
393
  /** Optional evidence kept inspectable without overwhelming the main rail. */
392
394
  details?: { label: string; value: unknown };
393
395
  action?: { label: string; url: string };
396
+ /**
397
+ * A usage ceiling refused further work. `text` keeps the canonical sentence
398
+ * for plain-text consumers; `MessageTimeline` renders this structured row
399
+ * (customizable with `renderAllowanceExhausted` / `allowanceExhaustedLabels`).
400
+ */
401
+ allowance?: AllowanceExhaustedRefusal;
394
402
  occurredAt: string;
395
403
  };
396
404
 
@@ -0,0 +1,64 @@
1
+ // Shared, replaceable words for usage allowances. The timeline refusal row
2
+ // (session graph) and the opt-in `@opengeni/react/usage` surfaces both read
3
+ // these, so a host that renames "workspace" or "admin" does it once.
4
+ //
5
+ // Product rule: never show raw credit amounts here. Hosts that want money or
6
+ // plan multiples opt in with `formatAmount` on the meter components.
7
+
8
+ /** Which ceiling ran out: the shared workspace pool, or one member's share of it. */
9
+ export type AllowanceScope = "workspace" | "member";
10
+
11
+ export type AllowanceLabels = {
12
+ /** "Usage limit reached" for a member ceiling. */
13
+ memberLimitReachedTitle: string;
14
+ /** "Workspace usage limit reached" for the shared pool. */
15
+ workspaceLimitReachedTitle: string;
16
+ /** Who can fix a member ceiling. */
17
+ memberRemedy: string;
18
+ /** Who can fix the workspace pool. */
19
+ workspaceRemedy: string;
20
+ /** "Resets Nov 1". `date` is already formatted by `formatDate`. */
21
+ resets: (date: string) => string;
22
+ /** Shown when a ceiling has no automatic reset. */
23
+ noReset: string;
24
+ /** Formats a reset instant for `resets`. Defaults to a short local date. */
25
+ formatDate: (iso: string) => string;
26
+ };
27
+
28
+ export function formatAllowanceDate(iso: string, now: Date = new Date()): string {
29
+ const date = new Date(iso);
30
+ if (Number.isNaN(date.getTime())) return iso;
31
+ return new Intl.DateTimeFormat(undefined, {
32
+ month: "short",
33
+ day: "numeric",
34
+ ...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
35
+ }).format(date);
36
+ }
37
+
38
+ /** Full local date and time, for a title/tooltip next to the short date. */
39
+ export function formatAllowanceInstant(iso: string): string {
40
+ const date = new Date(iso);
41
+ if (Number.isNaN(date.getTime())) return iso;
42
+ return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(
43
+ date,
44
+ );
45
+ }
46
+
47
+ export const DEFAULT_ALLOWANCE_LABELS: AllowanceLabels = {
48
+ memberLimitReachedTitle: "Usage limit reached",
49
+ workspaceLimitReachedTitle: "Workspace usage limit reached",
50
+ memberRemedy: "A workspace admin can raise this limit.",
51
+ workspaceRemedy: "An organization admin can raise the workspace budget.",
52
+ resets: (date) => `Resets ${date}.`,
53
+ noReset: "It doesn't reset automatically.",
54
+ formatDate: (iso) => formatAllowanceDate(iso),
55
+ };
56
+
57
+ export function allowanceLabels(overrides?: Partial<AllowanceLabels>): AllowanceLabels {
58
+ return overrides ? { ...DEFAULT_ALLOWANCE_LABELS, ...overrides } : DEFAULT_ALLOWANCE_LABELS;
59
+ }
60
+
61
+ /** "Resets Nov 1." or the no-reset sentence. */
62
+ export function allowanceResetSentence(labels: AllowanceLabels, resetsAt: string | null): string {
63
+ return resetsAt === null ? labels.noReset : labels.resets(labels.formatDate(resetsAt));
64
+ }
@@ -0,0 +1,93 @@
1
+ import type {
2
+ MemberAllowanceUsage,
3
+ UsageAllowanceStatus,
4
+ WorkspaceAllowanceUsage,
5
+ WorkspaceUsageResponse,
6
+ } from "@opengeni/sdk/usage-allowances";
7
+
8
+ import type { AllowanceScope } from "./allowance-copy";
9
+
10
+ /** One ceiling as a person reads it: how much of it is used, and when it resets. */
11
+ export type UsageReading = {
12
+ scope: AllowanceScope;
13
+ /** Used share of the ceiling. Can exceed 1 after post-call overshoot. */
14
+ fraction: number;
15
+ status: UsageAllowanceStatus;
16
+ resetsAt: string | null;
17
+ /** USD micros. Only shown when a host opts in with `formatAmount`. */
18
+ used: number;
19
+ limit: number;
20
+ remaining: number;
21
+ };
22
+
23
+ /**
24
+ * What a member can still do: `unlimited` when no ceiling applies, otherwise
25
+ * the state of whichever ceiling stops them first (`binding`).
26
+ */
27
+ export type UsageSummary = {
28
+ state: "unlimited" | "ok" | "warning" | "exhausted";
29
+ /** The ceiling a person would ask to raise; null when unlimited. */
30
+ binding: UsageReading | null;
31
+ /** Their own ceiling, when one applies. */
32
+ member: UsageReading | null;
33
+ /** The shared workspace pool, when one is set. */
34
+ workspace: UsageReading | null;
35
+ };
36
+
37
+ function reading(
38
+ scope: AllowanceScope,
39
+ usage: Pick<
40
+ WorkspaceAllowanceUsage | MemberAllowanceUsage,
41
+ "limit" | "used" | "remaining" | "fraction" | "status" | "resetsAt"
42
+ >,
43
+ ): UsageReading | null {
44
+ if (usage.limit === null) return null;
45
+ return {
46
+ scope,
47
+ fraction: usage.fraction ?? (usage.limit === 0 ? 1 : usage.used / usage.limit),
48
+ status: usage.status,
49
+ resetsAt: usage.resetsAt,
50
+ used: usage.used,
51
+ limit: usage.limit,
52
+ remaining: Math.max(0, usage.remaining ?? usage.limit - usage.used),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Fold a usage response (normally `/usage/me`) into what to tell one member.
58
+ * The server's `status` decides warning/exhausted (it applies the configured
59
+ * thresholds); when both ceilings apply, the one with less room left binds.
60
+ */
61
+ export function summarizeUsage(
62
+ usage: WorkspaceUsageResponse,
63
+ subjectId?: string | undefined,
64
+ ): UsageSummary {
65
+ const row =
66
+ subjectId === undefined
67
+ ? usage.members[0]
68
+ : usage.members.find((member) => member.subjectId === subjectId);
69
+ const member = row ? reading("member", row) : null;
70
+ const workspace = reading("workspace", usage.workspace);
71
+ const readings = [member, workspace].filter((value): value is UsageReading => value !== null);
72
+ if (readings.length === 0) return { state: "unlimited", binding: null, member, workspace };
73
+ // Raising a member ceiling cannot help while the shared pool is spent, so an
74
+ // exhausted workspace binds first.
75
+ const exhausted =
76
+ (workspace?.status === "exhausted" ? workspace : null) ??
77
+ (member?.status === "exhausted" ? member : null);
78
+ if (exhausted) return { state: "exhausted", binding: exhausted, member, workspace };
79
+ const warnings = readings.filter((value) => value.status === "warning");
80
+ const candidates = warnings.length > 0 ? warnings : readings;
81
+ const binding = candidates.reduce((tightest, value) =>
82
+ value.remaining < tightest.remaining ? value : tightest,
83
+ );
84
+ return { state: warnings.length > 0 ? "warning" : "ok", binding, member, workspace };
85
+ }
86
+
87
+ /** A whole-number percentage for display: "<1" for a sliver, never "0" for some use. */
88
+ export function usagePercentLabel(fraction: number): string {
89
+ if (!Number.isFinite(fraction) || fraction <= 0) return "0";
90
+ if (fraction < 0.01) return "<1";
91
+ if (fraction < 1 && Math.round(fraction * 100) === 100) return "99";
92
+ return String(Math.round(fraction * 100));
93
+ }
package/src/usage.ts ADDED
@@ -0,0 +1,50 @@
1
+ // @opengeni/react/usage — usage allowances for people, kept out of the package
2
+ // root and the session entry so hosts that don't meter usage never load it.
3
+ //
4
+ // Member-facing: `useUsage` + `<UsageMeter>` read the signed-in member's own
5
+ // `/usage/me` (the session proxy serves exactly that route) and
6
+ // `<UsageLimitNotice>` is the calm near/at-limit line for a composer header.
7
+ // Admin-facing: `<UsageMemberList>` shows every member against their limit and
8
+ // edits it with a share-of-budget slider; your backend saves the rule.
9
+ //
10
+ // Shares and percentages only by default. Pass `formatAmount` / `amounts` to
11
+ // show money, credits, or plan multiples in your own unit.
12
+ export { useUsage } from "./hooks/use-usage";
13
+ export type { UsageClientLike, UseUsageOptions, UseUsageResult } from "./hooks/use-usage";
14
+ export { summarizeUsage, usagePercentLabel } from "./usage/summary";
15
+ export type { UsageReading, UsageSummary } from "./usage/summary";
16
+ export {
17
+ DEFAULT_ALLOWANCE_LABELS,
18
+ allowanceLabels,
19
+ allowanceResetSentence,
20
+ formatAllowanceDate,
21
+ } from "./usage/allowance-copy";
22
+ export type { AllowanceLabels, AllowanceScope } from "./usage/allowance-copy";
23
+ export { DEFAULT_USAGE_METER_LABELS, UsageMeter, UsageMeterView } from "./components/usage-meter";
24
+ export type {
25
+ UsageMeterBaseProps,
26
+ UsageMeterLabels,
27
+ UsageMeterProps,
28
+ UsageMeterViewProps,
29
+ } from "./components/usage-meter";
30
+ export {
31
+ DEFAULT_USAGE_LIMIT_NOTICE_LABELS,
32
+ UsageLimitNotice,
33
+ UsageLimitNoticeView,
34
+ } from "./components/usage-limit-notice";
35
+ export type {
36
+ UsageLimitNoticeLabels,
37
+ UsageLimitNoticeProps,
38
+ } from "./components/usage-limit-notice";
39
+ export {
40
+ DEFAULT_USAGE_MEMBER_LIST_LABELS,
41
+ UsageMemberList,
42
+ describeMemberLimit,
43
+ } from "./components/usage-member-list";
44
+ export type {
45
+ UsageAmountFormat,
46
+ UsageMemberIdentity,
47
+ UsageMemberListLabels,
48
+ UsageMemberListProps,
49
+ } from "./components/usage-member-list";
50
+ export type { RenderAllowanceExhausted } from "./timeline/allowance-exhausted-row";