@opengeni/react 3.1.0 → 3.3.2-canary.1

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 (73) hide show
  1. package/dist/accounts.js +87 -11
  2. package/dist/accounts.js.map +1 -1
  3. package/dist/{browser-viewer-AEFHUYZK.js → browser-viewer-MKKFRRRV.js} +3 -3
  4. package/dist/{chunk-YFKJ7VZH.js → chunk-7EAFGICR.js} +45 -11
  5. package/dist/chunk-7EAFGICR.js.map +1 -0
  6. package/dist/{chunk-W6NFJXSA.js → chunk-7KBJLTKS.js} +52 -12
  7. package/dist/chunk-7KBJLTKS.js.map +1 -0
  8. package/dist/{chunk-YJFGAFBD.js → chunk-E5KISKFG.js} +241 -97
  9. package/dist/chunk-E5KISKFG.js.map +1 -0
  10. package/dist/{chunk-MZ2G2XBA.js → chunk-IOLVN74D.js} +31 -4
  11. package/dist/chunk-IOLVN74D.js.map +1 -0
  12. package/dist/{chunk-D3UIAJKY.js → chunk-JAQ5K7W6.js} +3 -2
  13. package/dist/{chunk-D3UIAJKY.js.map → chunk-JAQ5K7W6.js.map} +1 -1
  14. package/dist/{chunk-LAKLF4PA.js → chunk-VTOREEXF.js} +8 -7
  15. package/dist/chunk-VTOREEXF.js.map +1 -0
  16. package/dist/{chunk-X4A66EWZ.js → chunk-X5S426G2.js} +38 -44
  17. package/dist/chunk-X5S426G2.js.map +1 -0
  18. package/dist/{chunk-HQVCKCLG.js → chunk-YG4QEQF5.js} +354 -255
  19. package/dist/chunk-YG4QEQF5.js.map +1 -0
  20. package/dist/{chunk-XXWH2JLC.js → chunk-YVCHAQZY.js} +2 -2
  21. package/dist/{chunk-RGALCTWO.js → chunk-ZKNTK7IA.js} +13 -9
  22. package/dist/{chunk-RGALCTWO.js.map → chunk-ZKNTK7IA.js.map} +1 -1
  23. package/dist/{chunk-OAM4WAAM.js → chunk-ZPBTI3TE.js} +2 -2
  24. package/dist/components/fleet-tile.d.ts +2 -2
  25. package/dist/components/model-policy-picker.d.ts +9 -0
  26. package/dist/components/tip-follow.d.ts +16 -0
  27. package/dist/composer.js +6 -6
  28. package/dist/hooks/use-file-attachments.d.ts +2 -0
  29. package/dist/hooks/use-session-events.d.ts +3 -3
  30. package/dist/index.js +77 -69
  31. package/dist/index.js.map +1 -1
  32. package/dist/interaction.js +2 -2
  33. package/dist/machines.js +2 -2
  34. package/dist/model-policy.d.ts +2 -1
  35. package/dist/model-policy.js +1 -1
  36. package/dist/realtime.js +47 -8
  37. package/dist/realtime.js.map +1 -1
  38. package/dist/session-ui.js +4 -4
  39. package/dist/session.js +4 -4
  40. package/dist/timeline/entrance.d.ts +13 -1
  41. package/package.json +3 -2
  42. package/src/accounts.tsx +116 -14
  43. package/src/components/composer.tsx +15 -5
  44. package/src/components/fleet-tile.tsx +4 -15
  45. package/src/components/message-timeline.tsx +237 -101
  46. package/src/components/model-policy-picker.tsx +59 -13
  47. package/src/components/tip-follow.ts +52 -0
  48. package/src/components/tooltip.tsx +38 -5
  49. package/src/hooks/use-available-models.ts +20 -14
  50. package/src/hooks/use-composer.ts +49 -3
  51. package/src/hooks/use-file-attachments.ts +28 -2
  52. package/src/hooks/use-goal.ts +31 -7
  53. package/src/hooks/use-last-started-turn-policy.ts +11 -5
  54. package/src/hooks/use-session-events.ts +186 -61
  55. package/src/hooks/use-session-lineage.ts +20 -16
  56. package/src/hooks/use-session.ts +23 -19
  57. package/src/hooks/use-workspace-sessions.ts +31 -27
  58. package/src/lib/format.ts +8 -6
  59. package/src/model-policy.ts +45 -54
  60. package/src/realtime/realtime-control.tsx +55 -6
  61. package/src/timeline/entrance.tsx +28 -3
  62. package/src/timeline/projection.ts +4 -0
  63. package/styles/compiled.css +5 -0
  64. package/dist/chunk-HQVCKCLG.js.map +0 -1
  65. package/dist/chunk-LAKLF4PA.js.map +0 -1
  66. package/dist/chunk-MZ2G2XBA.js.map +0 -1
  67. package/dist/chunk-W6NFJXSA.js.map +0 -1
  68. package/dist/chunk-X4A66EWZ.js.map +0 -1
  69. package/dist/chunk-YFKJ7VZH.js.map +0 -1
  70. package/dist/chunk-YJFGAFBD.js.map +0 -1
  71. /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-MKKFRRRV.js.map} +0 -0
  72. /package/dist/{chunk-XXWH2JLC.js.map → chunk-YVCHAQZY.js.map} +0 -0
  73. /package/dist/{chunk-OAM4WAAM.js.map → chunk-ZPBTI3TE.js.map} +0 -0
@@ -1,11 +1,11 @@
1
+ import {
2
+ compareModelPickerOrder,
3
+ modelPickerBillingClassFor,
4
+ type ModelPickerBillingClass,
5
+ } from "@opengeni/sdk/model-picker-order";
1
6
  import type { ClientModel, ReasoningEffort, WorkspaceModelCatalogModel } from "@opengeni/sdk";
2
7
 
3
- export type PickerBillingClass =
4
- | "opengeni_credits"
5
- | "external"
6
- | "codex_subscription"
7
- | "supergrok_subscription"
8
- | "byok";
8
+ export type PickerBillingClass = ModelPickerBillingClass;
9
9
 
10
10
  export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
11
11
  id: string;
@@ -23,20 +23,12 @@ export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogM
23
23
 
24
24
  export type LatencyModeId = "standard" | "priority" | "fast";
25
25
 
26
- const BILLING_CLASS_ORDER: PickerBillingClass[] = [
27
- "opengeni_credits",
28
- "external",
29
- "codex_subscription",
30
- "supergrok_subscription",
31
- "byok",
32
- ];
33
-
34
26
  const BILLING_CLASS_LABELS: Record<PickerBillingClass, string> = {
35
27
  opengeni_credits: "OpenGeni",
36
28
  external: "External",
37
29
  codex_subscription: "Codex",
38
30
  supergrok_subscription: "SuperGrok",
39
- byok: "Your Gateway",
31
+ byok: "Workspace providers",
40
32
  };
41
33
 
42
34
  const AVAILABILITY_REASON_LABELS: Record<string, string> = {
@@ -50,33 +42,10 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
50
42
  };
51
43
 
52
44
  export function billingClassForModel(model: ClientModel): PickerBillingClass {
53
- if (model.billing?.metering === "external" && model.billing.upstreamPayer === "deployment") {
54
- return "external";
55
- }
56
- if (model.source === "supergrok") {
57
- return "supergrok_subscription";
58
- }
59
- if (model.source === "codex") {
60
- return "codex_subscription";
61
- }
62
- if (model.source === "workspace_gateway") {
63
- return "byok";
64
- }
65
- if (model.credentialSource?.kind === "connected_subscription") {
66
- return model.credentialSource.provider === "xai"
67
- ? "supergrok_subscription"
68
- : "codex_subscription";
69
- }
70
- if (model.credentialSource?.kind === "workspace_connection") {
45
+ if (model.provider === "workspace-gateway" || model.provider === "workspace-openrouter") {
71
46
  return "byok";
72
47
  }
73
- if (model.billing?.upstreamPayer === "connected_subscription") {
74
- return "codex_subscription";
75
- }
76
- if (model.billing?.upstreamPayer === "workspace") {
77
- return "byok";
78
- }
79
- return "opengeni_credits";
48
+ return modelPickerBillingClassFor(model);
80
49
  }
81
50
 
82
51
  export function billingClassLabel(billingClass: PickerBillingClass): string {
@@ -144,6 +113,23 @@ export function labelReasoningEffort(effort: ReasoningEffort): string {
144
113
  }
145
114
 
146
115
  export function payerSummaryForModel(model: ClientModel): string {
116
+ if (model.cost === "free") {
117
+ return "Free in this deployment";
118
+ }
119
+ if (model.cost === "credits") {
120
+ return "OpenGeni credits";
121
+ }
122
+ if (model.cost === "subscription") {
123
+ return model.source === "supergrok"
124
+ ? "SuperGrok subscription · external billing"
125
+ : "Codex subscription · external billing";
126
+ }
127
+ if (model.cost === "workspace") {
128
+ return workspaceProviderPayerSummary(model);
129
+ }
130
+
131
+ // Older client-config payloads do not carry `cost`; preserve their existing
132
+ // settlement-derived label until every deployment has rolled forward.
147
133
  const billing = model.billing;
148
134
  if (!billing) {
149
135
  return "Route unknown";
@@ -157,7 +143,7 @@ export function payerSummaryForModel(model: ClientModel): string {
157
143
  : "Codex subscription · external billing";
158
144
  }
159
145
  if (billing.upstreamPayer === "workspace") {
160
- return "Billed to your AI Gateway";
146
+ return workspaceProviderPayerSummary(model);
161
147
  }
162
148
  return "External provider · no OpenGeni credits";
163
149
  }
@@ -175,7 +161,13 @@ export function advancedSourceSummary(model: ClientModel): string | null {
175
161
  : "Connected Codex subscription";
176
162
  }
177
163
  if (source.kind === "workspace_connection") {
178
- return "Workspace AI Gateway";
164
+ if (model.provider === "workspace-openrouter") {
165
+ return "Workspace OpenRouter connection";
166
+ }
167
+ if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
168
+ return "Workspace Vercel AI Gateway";
169
+ }
170
+ return "Workspace provider connection";
179
171
  }
180
172
  if (source.kind === "deployment") {
181
173
  return source.mechanism === "azure_ad_bearer"
@@ -185,6 +177,16 @@ export function advancedSourceSummary(model: ClientModel): string | null {
185
177
  return null;
186
178
  }
187
179
 
180
+ function workspaceProviderPayerSummary(model: ClientModel): string {
181
+ if (model.provider === "workspace-openrouter") {
182
+ return "Billed to the workspace OpenRouter account";
183
+ }
184
+ if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
185
+ return "Billed to the workspace Vercel account";
186
+ }
187
+ return "Billed to the workspace provider account";
188
+ }
189
+
188
190
  export function projectPickerRows(models: WorkspaceModelCatalogModel[]): PickerModelRow[] {
189
191
  return models
190
192
  .filter((catalog) => {
@@ -232,18 +234,7 @@ export function projectClientModelRows(models: ClientModel[]): PickerModelRow<Cl
232
234
  export function sortPickerRows<TCatalog extends ClientModel>(
233
235
  rows: PickerModelRow<TCatalog>[],
234
236
  ): PickerModelRow<TCatalog>[] {
235
- return [...rows].sort((left, right) => {
236
- const classDelta =
237
- BILLING_CLASS_ORDER.indexOf(left.billingClass) -
238
- BILLING_CLASS_ORDER.indexOf(right.billingClass);
239
- if (classDelta !== 0) {
240
- return classDelta;
241
- }
242
- if (left.selectable !== right.selectable) {
243
- return left.selectable ? -1 : 1;
244
- }
245
- return left.label.localeCompare(right.label);
246
- });
237
+ return [...rows].sort(compareModelPickerOrder);
247
238
  }
248
239
 
249
240
  export function findPickerRow<TCatalog extends ClientModel>(
@@ -105,7 +105,12 @@ const REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES = 64;
105
105
  type RealtimeControllerClient = EmbeddedRealtimeSessionClientLike & SessionRealtimeClientLike;
106
106
 
107
107
  type RealtimeModelCatalogCacheEntry =
108
- | { state: "loading"; promise: Promise<RealtimeModelOption[]> }
108
+ | {
109
+ state: "loading";
110
+ promise: Promise<RealtimeModelOption[]>;
111
+ controller: AbortController;
112
+ consumers: number;
113
+ }
109
114
  | { state: "ready"; models: RealtimeModelOption[]; expiresAt: number };
110
115
 
111
116
  // Scope advisory availability to the exact client object and workspace. The
@@ -166,19 +171,26 @@ function pruneRealtimeModelCatalogCache(
166
171
  function loadRealtimeModelCatalog(
167
172
  client: RealtimeControllerClient,
168
173
  workspaceId: string,
174
+ signal?: AbortSignal,
169
175
  ): Promise<RealtimeModelOption[]> | null {
170
176
  const load = client.getWorkspaceRealtimeModelCatalog;
171
177
  if (!load) return null;
178
+ if (signal?.aborted) {
179
+ return Promise.reject(signal.reason ?? new DOMException("Request aborted", "AbortError"));
180
+ }
172
181
  const now = Date.now();
173
182
  const cache = realtimeModelCatalogCacheFor(client);
174
183
  const entry = cache.get(workspaceId);
175
- if (entry?.state === "loading") return entry.promise;
184
+ if (entry?.state === "loading" && !entry.controller.signal.aborted) {
185
+ return consumeRealtimeModelCatalog(entry, signal);
186
+ }
176
187
  if (entry?.state === "ready" && entry.expiresAt > now) return Promise.resolve(entry.models);
177
188
  if (entry) cache.delete(workspaceId);
178
189
  pruneRealtimeModelCatalogCache(cache, now);
179
190
 
191
+ const controller = new AbortController();
180
192
  const promise = load
181
- .call(client, workspaceId)
193
+ .call(client, workspaceId, { signal: controller.signal })
182
194
  .then((response) => response.models.map(toRealtimeModelOption))
183
195
  .then((models) => {
184
196
  const current = cache.get(workspaceId);
@@ -197,8 +209,43 @@ function loadRealtimeModelCatalog(
197
209
  if (current?.state === "loading" && current.promise === promise) cache.delete(workspaceId);
198
210
  throw error;
199
211
  });
200
- cache.set(workspaceId, { state: "loading", promise });
201
- return promise;
212
+ const loading: RealtimeModelCatalogCacheEntry & { state: "loading" } = {
213
+ state: "loading",
214
+ promise,
215
+ controller,
216
+ consumers: 0,
217
+ };
218
+ cache.set(workspaceId, loading);
219
+ return consumeRealtimeModelCatalog(loading, signal);
220
+ }
221
+
222
+ function consumeRealtimeModelCatalog(
223
+ entry: RealtimeModelCatalogCacheEntry & { state: "loading" },
224
+ signal?: AbortSignal,
225
+ ): Promise<RealtimeModelOption[]> {
226
+ entry.consumers += 1;
227
+ return new Promise((resolve, reject) => {
228
+ let released = false;
229
+ const release = (abandoned: boolean) => {
230
+ if (released) return;
231
+ released = true;
232
+ entry.consumers = Math.max(0, entry.consumers - 1);
233
+ if (abandoned && entry.consumers === 0) {
234
+ entry.controller.abort(
235
+ signal?.reason ?? new DOMException("Request abandoned", "AbortError"),
236
+ );
237
+ }
238
+ };
239
+ const onAbort = () => {
240
+ release(true);
241
+ reject(signal?.reason ?? new DOMException("Request aborted", "AbortError"));
242
+ };
243
+ signal?.addEventListener("abort", onAbort, { once: true });
244
+ void entry.promise.then(resolve, reject).finally(() => {
245
+ signal?.removeEventListener("abort", onAbort);
246
+ release(false);
247
+ });
248
+ });
202
249
  }
203
250
 
204
251
  export type SessionRealtimeControllerFactory = (
@@ -550,6 +597,7 @@ export function useRealtimeModelSelection(options: {
550
597
 
551
598
  useEffect(() => {
552
599
  let disposed = false;
600
+ const requestAbort = new AbortController();
553
601
  const applyCatalog = (models: RealtimeModelOption[]) => {
554
602
  catalogScopeRef.current = { client, workspaceId, source: "cache" };
555
603
  setCatalog(models);
@@ -570,7 +618,7 @@ export function useRealtimeModelSelection(options: {
570
618
  }
571
619
  return;
572
620
  }
573
- const loading = loadRealtimeModelCatalog(client, workspaceId);
621
+ const loading = loadRealtimeModelCatalog(client, workspaceId, requestAbort.signal);
574
622
  if (!loading) return;
575
623
  void loading
576
624
  .then((models) => {
@@ -580,6 +628,7 @@ export function useRealtimeModelSelection(options: {
580
628
  .catch(() => undefined);
581
629
  return () => {
582
630
  disposed = true;
631
+ requestAbort.abort();
583
632
  };
584
633
  }, [client, workspaceId]);
585
634
 
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useRef } from "react";
1
+ import { createContext, useContext, useRef, type ReactNode } from "react";
2
2
 
3
3
  /* ----------------------------------------------------------------------------
4
4
  Entrance animation gating
@@ -15,8 +15,33 @@ import { createContext, useContext, useRef } from "react";
15
15
  -------------------------------------------------------------------------- */
16
16
 
17
17
  const EntranceAnimationContext = createContext(true);
18
+ const EntranceAnimationLiveContext = createContext(true);
18
19
 
19
- export const EntranceAnimationProvider = EntranceAnimationContext.Provider;
20
+ /**
21
+ * Freeze the mount-time gate for this subtree while publishing a separate live
22
+ * gate to activity rails that need later appends to animate after a bulk paint.
23
+ * MessageTimeline mounts one provider per durable group, so existing groups do
24
+ * not receive a context invalidation when a prepend toggles bulk mode and new
25
+ * groups still capture the value from the commit that created them.
26
+ */
27
+ export function EntranceAnimationProvider({
28
+ value,
29
+ liveValue = value,
30
+ children,
31
+ }: {
32
+ value: boolean;
33
+ liveValue?: boolean | undefined;
34
+ children: ReactNode;
35
+ }) {
36
+ const mountedValue = useRef(value).current;
37
+ return (
38
+ <EntranceAnimationContext.Provider value={mountedValue}>
39
+ <EntranceAnimationLiveContext.Provider value={liveValue}>
40
+ {children}
41
+ </EntranceAnimationLiveContext.Provider>
42
+ </EntranceAnimationContext.Provider>
43
+ );
44
+ }
20
45
 
21
46
  /**
22
47
  * Live entrance gate from the nearest provider. Prefer
@@ -25,7 +50,7 @@ export const EntranceAnimationProvider = EntranceAnimationContext.Provider;
25
50
  * later appends after a bulk window clears.
26
51
  */
27
52
  export function useEntranceAnimationLive(): boolean {
28
- return useContext(EntranceAnimationContext);
53
+ return useContext(EntranceAnimationLiveContext);
29
54
  }
30
55
 
31
56
  /**
@@ -2091,6 +2091,10 @@ function findOpenCall(
2091
2091
  if (byId) {
2092
2092
  return byId as ToolCallItem | WorkerItem;
2093
2093
  }
2094
+ // An explicit provider correlation id is authoritative. Falling back to
2095
+ // an unrelated running call corrupts its status/output when compact pages
2096
+ // or interleaved providers deliver an unmatched historical result.
2097
+ return undefined;
2094
2098
  }
2095
2099
  return reversed.find(
2096
2100
  (item): item is ToolCallItem | WorkerItem => isCall(item) && item.status === "running",
@@ -4495,6 +4495,11 @@
4495
4495
  max-width: 12rem;
4496
4496
  }
4497
4497
  }
4498
+ :where(.og-root).sm\:max-w-\[32rem\],:where(.og-root) .sm\:max-w-\[32rem\] {
4499
+ @media (width >= 40rem) {
4500
+ max-width: 32rem;
4501
+ }
4502
+ }
4498
4503
  :where(.og-root).sm\:max-w-\[82\%\],:where(.og-root) .sm\:max-w-\[82\%\] {
4499
4504
  @media (width >= 40rem) {
4500
4505
  max-width: 82%;