@opengeni/react 0.52.1 → 1.0.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 (132) hide show
  1. package/README.md +51 -1
  2. package/dist/artifacts.d.ts +1 -1
  3. package/dist/artifacts.js +43 -14
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
  6. package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
  7. package/dist/chunk-6KLX2RH5.js.map +1 -0
  8. package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
  9. package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
  10. package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
  11. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  12. package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
  13. package/dist/chunk-HA46CYQK.js.map +1 -0
  14. package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
  15. package/dist/chunk-LLOLUHVG.js +5937 -0
  16. package/dist/chunk-LLOLUHVG.js.map +1 -0
  17. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  18. package/dist/chunk-MKPIAGVL.js.map +1 -0
  19. package/dist/chunk-R3AZOAB5.js +933 -0
  20. package/dist/chunk-R3AZOAB5.js.map +1 -0
  21. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  22. package/dist/chunk-XBZWOM2K.js.map +1 -0
  23. package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
  24. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  25. package/dist/client.d.ts +6 -4
  26. package/dist/components/artifacts/index.d.ts +1 -0
  27. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  28. package/dist/components/browser-input.d.ts +7 -0
  29. package/dist/components/browser-viewer.d.ts +5 -4
  30. package/dist/components/computer-viewer.d.ts +2 -1
  31. package/dist/components/desktop-viewer.d.ts +26 -2
  32. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  33. package/dist/components/sandbox-terminal.d.ts +4 -1
  34. package/dist/components/sandbox-workspace.d.ts +3 -0
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/use-auth-runs.d.ts +32 -0
  37. package/dist/hooks/use-browser-downloads.d.ts +25 -0
  38. package/dist/hooks/use-browser-identities.d.ts +5 -1
  39. package/dist/hooks/use-browser-session.d.ts +2 -1
  40. package/dist/hooks/use-browser-sessions.d.ts +1 -1
  41. package/dist/hooks/use-channels.d.ts +26 -0
  42. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  43. package/dist/hooks/use-computer-session.d.ts +2 -1
  44. package/dist/hooks/use-computer-sessions.d.ts +1 -1
  45. package/dist/hooks/use-desktop-stream.d.ts +4 -0
  46. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  47. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  48. package/dist/hooks/use-network-routes.d.ts +25 -0
  49. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  50. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  51. package/dist/hooks/use-terminal-stream.d.ts +3 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +1220 -1545
  54. package/dist/index.js.map +1 -1
  55. package/dist/interaction.d.ts +13 -3
  56. package/dist/interaction.js +520 -6
  57. package/dist/interaction.js.map +1 -1
  58. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  59. package/dist/lib/host-platform.d.ts +5 -0
  60. package/dist/lib/relay-wire.d.ts +20 -1
  61. package/dist/machines.js +3 -3
  62. package/dist/model-policy.d.ts +1 -1
  63. package/dist/model-policy.js +1 -1
  64. package/dist/provider.d.ts +3 -2
  65. package/dist/realtime/realtime-control.d.ts +6 -2
  66. package/dist/realtime.js +138 -24
  67. package/dist/realtime.js.map +1 -1
  68. package/dist/session-context.d.ts +10 -2
  69. package/dist/session-ui.js +3 -3
  70. package/dist/session.js +11 -11
  71. package/dist/timeline/types.d.ts +5 -0
  72. package/package.json +2 -2
  73. package/src/artifacts.ts +3 -0
  74. package/src/client.ts +73 -3
  75. package/src/components/artifacts/index.ts +5 -0
  76. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  77. package/src/components/browser-input.ts +65 -0
  78. package/src/components/browser-viewer.tsx +1287 -177
  79. package/src/components/computer-viewer.tsx +611 -114
  80. package/src/components/desktop-viewer.tsx +114 -1
  81. package/src/components/interaction-intervention-banner.tsx +99 -0
  82. package/src/components/machines/machine-detail.tsx +1 -1
  83. package/src/components/model-policy-picker.tsx +15 -0
  84. package/src/components/sandbox-terminal.tsx +24 -7
  85. package/src/components/sandbox-workspace.tsx +42 -4
  86. package/src/components/workspace-dock.tsx +59 -5
  87. package/src/hooks/use-attached-browsers.ts +13 -11
  88. package/src/hooks/use-auth-runs.ts +278 -0
  89. package/src/hooks/use-browser-downloads.ts +218 -0
  90. package/src/hooks/use-browser-frame-stream.ts +168 -59
  91. package/src/hooks/use-browser-identities.ts +47 -6
  92. package/src/hooks/use-browser-session.ts +94 -27
  93. package/src/hooks/use-browser-sessions.ts +19 -14
  94. package/src/hooks/use-channels.ts +99 -0
  95. package/src/hooks/use-computer-frame-stream.ts +184 -61
  96. package/src/hooks/use-computer-session.ts +22 -2
  97. package/src/hooks/use-computer-sessions.ts +19 -13
  98. package/src/hooks/use-desktop-stream.ts +39 -7
  99. package/src/hooks/use-interaction-interventions.ts +242 -0
  100. package/src/hooks/use-interaction-invalidation.ts +66 -0
  101. package/src/hooks/use-machine-chip.ts +13 -3
  102. package/src/hooks/use-network-routes.ts +214 -0
  103. package/src/hooks/use-site-auth-connections.ts +226 -0
  104. package/src/hooks/use-supergrok-accounts.ts +81 -0
  105. package/src/hooks/use-terminal-stream.ts +288 -55
  106. package/src/index.ts +8 -0
  107. package/src/interaction.ts +30 -0
  108. package/src/lib/host-platform.ts +10 -0
  109. package/src/lib/relay-wire.ts +43 -4
  110. package/src/lib/terminal-capability.ts +1 -1
  111. package/src/model-policy.ts +19 -4
  112. package/src/provider.tsx +133 -1
  113. package/src/realtime/realtime-control.tsx +181 -26
  114. package/src/session-context.ts +66 -1
  115. package/src/timeline/activity-rail.tsx +6 -0
  116. package/src/timeline/projection.ts +58 -0
  117. package/src/timeline/types.ts +2 -0
  118. package/styles/compiled.css +64 -6
  119. package/dist/chunk-23EJ676W.js.map +0 -1
  120. package/dist/chunk-6BBWWOOU.js +0 -82
  121. package/dist/chunk-6BBWWOOU.js.map +0 -1
  122. package/dist/chunk-7I2KB3OK.js +0 -3895
  123. package/dist/chunk-7I2KB3OK.js.map +0 -1
  124. package/dist/chunk-HDBG7XGO.js.map +0 -1
  125. package/dist/chunk-JAGDUCZQ.js.map +0 -1
  126. package/dist/chunk-JR3QFY6B.js.map +0 -1
  127. package/dist/chunk-WYA65Y3F.js.map +0 -1
  128. package/dist/chunk-X4S56Y2H.js.map +0 -1
  129. package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
  130. /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
  131. /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
  132. /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
@@ -1,6 +1,10 @@
1
1
  import type { ClientModel, ReasoningEffort, WorkspaceModelCatalogModel } from "@opengeni/sdk";
2
2
 
3
- export type PickerBillingClass = "opengeni_credits" | "codex_subscription" | "byok";
3
+ export type PickerBillingClass =
4
+ | "opengeni_credits"
5
+ | "codex_subscription"
6
+ | "supergrok_subscription"
7
+ | "byok";
4
8
 
5
9
  export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
6
10
  id: string;
@@ -21,12 +25,14 @@ export type LatencyModeId = "standard" | "priority" | "fast";
21
25
  const BILLING_CLASS_ORDER: PickerBillingClass[] = [
22
26
  "opengeni_credits",
23
27
  "codex_subscription",
28
+ "supergrok_subscription",
24
29
  "byok",
25
30
  ];
26
31
 
27
32
  const BILLING_CLASS_LABELS: Record<PickerBillingClass, string> = {
28
33
  opengeni_credits: "OpenGeni",
29
34
  codex_subscription: "Codex",
35
+ supergrok_subscription: "SuperGrok",
30
36
  byok: "Your Gateway",
31
37
  };
32
38
 
@@ -41,6 +47,9 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
41
47
  };
42
48
 
43
49
  export function billingClassForModel(model: ClientModel): PickerBillingClass {
50
+ if (model.source === "supergrok") {
51
+ return "supergrok_subscription";
52
+ }
44
53
  if (model.source === "codex") {
45
54
  return "codex_subscription";
46
55
  }
@@ -48,7 +57,9 @@ export function billingClassForModel(model: ClientModel): PickerBillingClass {
48
57
  return "byok";
49
58
  }
50
59
  if (model.credentialSource?.kind === "connected_subscription") {
51
- return "codex_subscription";
60
+ return model.credentialSource.provider === "xai"
61
+ ? "supergrok_subscription"
62
+ : "codex_subscription";
52
63
  }
53
64
  if (model.credentialSource?.kind === "workspace_connection") {
54
65
  return "byok";
@@ -135,7 +146,9 @@ export function payerSummaryForModel(model: ClientModel): string {
135
146
  return "OpenGeni credits · automatic managed route";
136
147
  }
137
148
  if (billing.upstreamPayer === "connected_subscription") {
138
- return "Codex subscription · external billing";
149
+ return model.source === "supergrok"
150
+ ? "SuperGrok subscription · external billing"
151
+ : "Codex subscription · external billing";
139
152
  }
140
153
  if (billing.upstreamPayer === "workspace") {
141
154
  return "Billed to your AI Gateway";
@@ -149,7 +162,9 @@ export function advancedSourceSummary(model: ClientModel): string | null {
149
162
  return null;
150
163
  }
151
164
  if (source.kind === "connected_subscription") {
152
- return "Connected Codex subscription";
165
+ return source.provider === "xai"
166
+ ? "Connected SuperGrok subscription"
167
+ : "Connected Codex subscription";
153
168
  }
154
169
  if (source.kind === "workspace_connection") {
155
170
  return "Workspace AI Gateway";
package/src/provider.tsx CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  OPENGENI_API_CONTRACT_REVISION,
4
4
  type StreamConnectionState,
5
5
  type WorkspaceControlEvent,
6
+ type WorkspaceInteractionRevisionEvent,
6
7
  } from "@opengeni/sdk";
7
8
  import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
8
9
  import type { SessionClientLike } from "./client";
@@ -17,6 +18,7 @@ export type OpenGeniProviderProps = {
17
18
  client: SessionClientLike;
18
19
  workspaceId: string;
19
20
  onWorkspaceControlEvent?: ((event: WorkspaceControlEvent) => void) | undefined;
21
+ onWorkspaceInteractionEvent?: ((event: WorkspaceInteractionRevisionEvent) => void) | undefined;
20
22
  children?: ReactNode;
21
23
  };
22
24
 
@@ -28,6 +30,7 @@ export function OpenGeniProvider({
28
30
  client,
29
31
  workspaceId,
30
32
  onWorkspaceControlEvent,
33
+ onWorkspaceInteractionEvent,
31
34
  children,
32
35
  }: OpenGeniProviderProps) {
33
36
  const [workspaceControlEvent, setWorkspaceControlEvent] = useState<WorkspaceControlEvent | null>(
@@ -36,15 +39,25 @@ export function OpenGeniProvider({
36
39
  const [workspaceControlConnectionState, setWorkspaceControlConnectionState] = useState<
37
40
  StreamConnectionState | "idle" | "error"
38
41
  >("idle");
42
+ const [workspaceInteractionEvent, setWorkspaceInteractionEvent] =
43
+ useState<WorkspaceInteractionRevisionEvent | null>(null);
44
+ const [workspaceInteractionConnectionState, setWorkspaceInteractionConnectionState] = useState<
45
+ StreamConnectionState | "idle" | "error"
46
+ >("idle");
39
47
  const [contractMismatch, setContractMismatch] = useState<OpenGeniApiContractMismatchError | null>(
40
48
  null,
41
49
  );
42
50
  const callbackRef = useRef(onWorkspaceControlEvent);
51
+ const interactionCallbackRef = useRef(onWorkspaceInteractionEvent);
43
52
  const reconcilersRef = useRef(new Map<string, Map<string, () => Promise<void>>>());
44
53
  const reconcileInFlightRef = useRef(new Map<string, Promise<void>>());
45
54
  const workspaceControlSequencesRef = useRef(new Map<string, number>());
55
+ const workspaceInteractionSequencesRef = useRef(new Map<string, number>());
46
56
  const pageLive = usePageLiveActivity();
57
+ const supportsWorkspaceLiveStream =
58
+ "streamWorkspaceLiveEvents" in client && typeof client.streamWorkspaceLiveEvents === "function";
47
59
  callbackRef.current = onWorkspaceControlEvent;
60
+ interactionCallbackRef.current = onWorkspaceInteractionEvent;
48
61
 
49
62
  const verifyApiContract = useCallback(async (): Promise<void> => {
50
63
  try {
@@ -105,6 +118,78 @@ export function OpenGeniProvider({
105
118
  );
106
119
 
107
120
  useEffect(() => {
121
+ if (!supportsWorkspaceLiveStream) return;
122
+ const openLiveStream = client.streamWorkspaceLiveEvents!;
123
+ setWorkspaceControlEvent(null);
124
+ setWorkspaceInteractionEvent(null);
125
+ if (!pageLive) {
126
+ setWorkspaceControlConnectionState("idle");
127
+ setWorkspaceInteractionConnectionState("idle");
128
+ return;
129
+ }
130
+ const controller = new AbortController();
131
+ const setConnectionState = (state: StreamConnectionState) => {
132
+ setWorkspaceControlConnectionState(state);
133
+ setWorkspaceInteractionConnectionState(state);
134
+ };
135
+ setConnectionState("connecting");
136
+ void (async () => {
137
+ try {
138
+ await verifyApiContract();
139
+ const workspace = await client.getWorkspace(workspaceId);
140
+ const controlAfter = Math.max(
141
+ workspaceControlSequencesRef.current.get(workspaceId) ?? 0,
142
+ workspace.inferenceControl.revision,
143
+ );
144
+ const interactionAfter = workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0;
145
+ workspaceControlSequencesRef.current.set(workspaceId, controlAfter);
146
+ const stream = openLiveStream.call(client, workspaceId, {
147
+ controlAfter,
148
+ interactionAfter,
149
+ signal: controller.signal,
150
+ onStateChange: setConnectionState,
151
+ });
152
+ for await (const event of stream) {
153
+ if (controller.signal.aborted) return;
154
+ if (event.type === "workspace.control.changed") {
155
+ workspaceControlSequencesRef.current.set(
156
+ workspaceId,
157
+ Math.max(workspaceControlSequencesRef.current.get(workspaceId) ?? 0, event.sequence),
158
+ );
159
+ setWorkspaceControlEvent((current) =>
160
+ !current || event.sequence > current.sequence ? event : current,
161
+ );
162
+ callbackRef.current?.(event);
163
+ } else {
164
+ workspaceInteractionSequencesRef.current.set(
165
+ workspaceId,
166
+ Math.max(
167
+ workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0,
168
+ event.sequence,
169
+ ),
170
+ );
171
+ setWorkspaceInteractionEvent((current) =>
172
+ !current || event.sequence > current.sequence ? event : current,
173
+ );
174
+ interactionCallbackRef.current?.(event);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ if (error instanceof OpenGeniApiContractMismatchError) {
179
+ setContractMismatch(error);
180
+ reloadForContractMismatchOnce(error);
181
+ }
182
+ if (!controller.signal.aborted) {
183
+ setWorkspaceControlConnectionState("error");
184
+ setWorkspaceInteractionConnectionState("error");
185
+ }
186
+ }
187
+ })();
188
+ return () => controller.abort();
189
+ }, [client, pageLive, supportsWorkspaceLiveStream, verifyApiContract, workspaceId]);
190
+
191
+ useEffect(() => {
192
+ if (supportsWorkspaceLiveStream) return;
108
193
  if (!pageLive) {
109
194
  setWorkspaceControlConnectionState("idle");
110
195
  return;
@@ -145,7 +230,50 @@ export function OpenGeniProvider({
145
230
  }
146
231
  })();
147
232
  return () => controller.abort();
148
- }, [client, pageLive, verifyApiContract, workspaceId]);
233
+ }, [client, pageLive, supportsWorkspaceLiveStream, verifyApiContract, workspaceId]);
234
+
235
+ useEffect(() => {
236
+ if (supportsWorkspaceLiveStream) return;
237
+ setWorkspaceInteractionEvent(null);
238
+ if (!pageLive) {
239
+ setWorkspaceInteractionConnectionState("idle");
240
+ return;
241
+ }
242
+ const controller = new AbortController();
243
+ setWorkspaceInteractionConnectionState("connecting");
244
+ void (async () => {
245
+ try {
246
+ await verifyApiContract();
247
+ const resumeAfter = workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0;
248
+ const stream = client.streamWorkspaceInteractionRevisions(workspaceId, {
249
+ after: resumeAfter,
250
+ signal: controller.signal,
251
+ onStateChange: setWorkspaceInteractionConnectionState,
252
+ });
253
+ for await (const event of stream) {
254
+ if (controller.signal.aborted) return;
255
+ workspaceInteractionSequencesRef.current.set(
256
+ workspaceId,
257
+ Math.max(
258
+ workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0,
259
+ event.sequence,
260
+ ),
261
+ );
262
+ setWorkspaceInteractionEvent((current) =>
263
+ !current || event.sequence > current.sequence ? event : current,
264
+ );
265
+ interactionCallbackRef.current?.(event);
266
+ }
267
+ } catch (error) {
268
+ if (error instanceof OpenGeniApiContractMismatchError) {
269
+ setContractMismatch(error);
270
+ reloadForContractMismatchOnce(error);
271
+ }
272
+ if (!controller.signal.aborted) setWorkspaceInteractionConnectionState("error");
273
+ }
274
+ })();
275
+ return () => controller.abort();
276
+ }, [client, pageLive, supportsWorkspaceLiveStream, verifyApiContract, workspaceId]);
149
277
 
150
278
  const value = useMemo(
151
279
  () => ({
@@ -153,6 +281,8 @@ export function OpenGeniProvider({
153
281
  workspaceId,
154
282
  workspaceControlEvent,
155
283
  workspaceControlConnectionState,
284
+ workspaceInteractionEvent,
285
+ workspaceInteractionConnectionState,
156
286
  registerSessionReconciler,
157
287
  reconcileSession,
158
288
  }),
@@ -161,6 +291,8 @@ export function OpenGeniProvider({
161
291
  workspaceId,
162
292
  workspaceControlEvent,
163
293
  workspaceControlConnectionState,
294
+ workspaceInteractionEvent,
295
+ workspaceInteractionConnectionState,
164
296
  registerSessionReconciler,
165
297
  reconcileSession,
166
298
  ],
@@ -57,7 +57,7 @@ import {
57
57
  export type RealtimeModelOption = {
58
58
  id: SessionRealtimeModel;
59
59
  label: string;
60
- provider: "OpenGeni" | "Connected Codex" | "Your Gateway";
60
+ provider: "OpenGeni" | "Connected Codex" | "Connected SuperGrok" | "Your Gateway";
61
61
  description: string;
62
62
  available: boolean;
63
63
  unavailableReason: string | null;
@@ -74,7 +74,12 @@ const CODEX_LIVE_MODEL: RealtimeModelOption = {
74
74
  recommended: false,
75
75
  };
76
76
 
77
- const REALTIME_MODEL_PROVIDERS = ["OpenGeni", "Connected Codex", "Your Gateway"] as const;
77
+ const REALTIME_MODEL_PROVIDERS = [
78
+ "OpenGeni",
79
+ "Connected Codex",
80
+ "Connected SuperGrok",
81
+ "Your Gateway",
82
+ ] as const;
78
83
  type RealtimeModelProvider = (typeof REALTIME_MODEL_PROVIDERS)[number];
79
84
 
80
85
  const REALTIME_PROVIDER_META: Record<
@@ -86,12 +91,115 @@ const REALTIME_PROVIDER_META: Record<
86
91
  billingClass: "codex_subscription",
87
92
  hint: "ChatGPT / Codex plan",
88
93
  },
94
+ "Connected SuperGrok": {
95
+ billingClass: "supergrok_subscription",
96
+ hint: "SuperGrok / xAI plan",
97
+ },
89
98
  "Your Gateway": { billingClass: "byok", hint: "Billed to your AI Gateway" },
90
99
  };
91
100
  const REALTIME_MODEL_STORAGE_PREFIX = "opengeni:realtime-model";
101
+ const REALTIME_MODEL_CATALOG_CACHE_TTL_MS = 60_000;
102
+ const REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES = 64;
92
103
 
93
104
  type RealtimeControllerClient = EmbeddedRealtimeSessionClientLike & SessionRealtimeClientLike;
94
105
 
106
+ type RealtimeModelCatalogCacheEntry =
107
+ | { state: "loading"; promise: Promise<RealtimeModelOption[]> }
108
+ | { state: "ready"; models: RealtimeModelOption[]; expiresAt: number };
109
+
110
+ // Scope advisory availability to the exact client object and workspace. The
111
+ // server still authorizes every realtime operation; changing principals should
112
+ // replace the client just as it does for the rest of the SDK state.
113
+ const realtimeModelCatalogCache = new WeakMap<
114
+ RealtimeControllerClient,
115
+ Map<string, RealtimeModelCatalogCacheEntry>
116
+ >();
117
+
118
+ function realtimeModelFallback(codexConnected: boolean): RealtimeModelOption[] {
119
+ return [
120
+ {
121
+ ...CODEX_LIVE_MODEL,
122
+ available: codexConnected,
123
+ unavailableReason: codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason,
124
+ },
125
+ ];
126
+ }
127
+
128
+ function realtimeModelCatalogCacheFor(
129
+ client: RealtimeControllerClient,
130
+ ): Map<string, RealtimeModelCatalogCacheEntry> {
131
+ const existing = realtimeModelCatalogCache.get(client);
132
+ if (existing) return existing;
133
+ const created = new Map<string, RealtimeModelCatalogCacheEntry>();
134
+ realtimeModelCatalogCache.set(client, created);
135
+ return created;
136
+ }
137
+
138
+ function readCachedRealtimeModelCatalog(
139
+ client: RealtimeControllerClient,
140
+ workspaceId: string,
141
+ now = Date.now(),
142
+ ): RealtimeModelOption[] | null {
143
+ const cache = realtimeModelCatalogCache.get(client);
144
+ const entry = cache?.get(workspaceId);
145
+ if (!entry || entry.state === "loading") return null;
146
+ if (entry.expiresAt > now) return entry.models;
147
+ cache?.delete(workspaceId);
148
+ return null;
149
+ }
150
+
151
+ function pruneRealtimeModelCatalogCache(
152
+ cache: Map<string, RealtimeModelCatalogCacheEntry>,
153
+ now: number,
154
+ ): void {
155
+ for (const [workspaceId, entry] of cache) {
156
+ if (entry.state === "ready" && entry.expiresAt <= now) cache.delete(workspaceId);
157
+ }
158
+ while (cache.size >= REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES) {
159
+ const oldestWorkspaceId = cache.keys().next().value;
160
+ if (oldestWorkspaceId === undefined) return;
161
+ cache.delete(oldestWorkspaceId);
162
+ }
163
+ }
164
+
165
+ function loadRealtimeModelCatalog(
166
+ client: RealtimeControllerClient,
167
+ workspaceId: string,
168
+ ): Promise<RealtimeModelOption[]> | null {
169
+ const load = client.getWorkspaceRealtimeModelCatalog;
170
+ if (!load) return null;
171
+ const now = Date.now();
172
+ const cache = realtimeModelCatalogCacheFor(client);
173
+ const entry = cache.get(workspaceId);
174
+ if (entry?.state === "loading") return entry.promise;
175
+ if (entry?.state === "ready" && entry.expiresAt > now) return Promise.resolve(entry.models);
176
+ if (entry) cache.delete(workspaceId);
177
+ pruneRealtimeModelCatalogCache(cache, now);
178
+
179
+ const promise = load
180
+ .call(client, workspaceId)
181
+ .then((response) => response.models.map(toRealtimeModelOption))
182
+ .then((models) => {
183
+ const current = cache.get(workspaceId);
184
+ if (current?.state === "loading" && current.promise === promise) {
185
+ cache.delete(workspaceId);
186
+ cache.set(workspaceId, {
187
+ state: "ready",
188
+ models,
189
+ expiresAt: Date.now() + REALTIME_MODEL_CATALOG_CACHE_TTL_MS,
190
+ });
191
+ }
192
+ return models;
193
+ })
194
+ .catch((error: unknown) => {
195
+ const current = cache.get(workspaceId);
196
+ if (current?.state === "loading" && current.promise === promise) cache.delete(workspaceId);
197
+ throw error;
198
+ });
199
+ cache.set(workspaceId, { state: "loading", promise });
200
+ return promise;
201
+ }
202
+
95
203
  export type SessionRealtimeControllerFactory = (
96
204
  options: CreateSessionRealtimeControllerOptions,
97
205
  ) => SessionRealtimeController;
@@ -129,6 +237,8 @@ export function useSessionRealtime(options: {
129
237
  model?: SessionRealtimeModel | undefined;
130
238
  modelAvailable?: boolean | undefined;
131
239
  modelUnavailableReason?: string | null | undefined;
240
+ /** Model-visible application context captured with each durable realtime message. */
241
+ getModelContext?: (() => string | undefined) | undefined;
132
242
  /** Deterministic browser-test/demo seam. Production hosts should use the SDK default. */
133
243
  controllerFactory?: SessionRealtimeControllerFactory | undefined;
134
244
  }) {
@@ -140,8 +250,14 @@ export function useSessionRealtime(options: {
140
250
  const audioRef = useRef<HTMLAudioElement>(null);
141
251
  const controllerRef = useRef<SessionRealtimeController | null>(null);
142
252
  const controllerModelRef = useRef<SessionRealtimeModel | null>(null);
253
+ const modelContextProviderRef = useRef(options.getModelContext);
254
+ modelContextProviderRef.current = options.getModelContext;
143
255
  const [snapshot, setSnapshot] = useState<SessionRealtimeControllerSnapshot>(() => ({
144
- status: hasStoredSessionRealtimeOwnerProof({ workspaceId, sessionId: options.sessionId, model })
256
+ status: hasStoredSessionRealtimeOwnerProof({
257
+ workspaceId,
258
+ sessionId: options.sessionId,
259
+ model,
260
+ })
145
261
  ? "recovering"
146
262
  : "idle",
147
263
  realtimeId: null,
@@ -180,6 +296,7 @@ export function useSessionRealtime(options: {
180
296
  sessionId: options.sessionId,
181
297
  ...(audioRef.current ? { remoteAudio: audioRef.current } : {}),
182
298
  model,
299
+ getModelContext: () => modelContextProviderRef.current?.(),
183
300
  });
184
301
  controllerRef.current = controller;
185
302
  controllerModelRef.current = model;
@@ -293,6 +410,8 @@ export function SessionRealtimeControl(props: {
293
410
  events: SessionEvent[];
294
411
  eventsReady: boolean;
295
412
  codexConnected: boolean;
413
+ /** Model-visible application context captured with each durable realtime message. */
414
+ getModelContext?: (() => string | undefined) | undefined;
296
415
  realtimeAutostartModel?: SessionRealtimeModel | undefined;
297
416
  onRealtimeAutostartConsumed?: (() => void) | undefined;
298
417
  /** Host hides dictate (and can choreograph layout) while realtime voice is live. */
@@ -398,31 +517,52 @@ export function useRealtimeModelSelection(options: {
398
517
  workspaceId: options.workspaceId,
399
518
  });
400
519
  const activeModel = options.activeModel;
401
- const [catalog, setCatalog] = useState<RealtimeModelOption[]>(() => [
402
- {
403
- ...CODEX_LIVE_MODEL,
404
- available: options.codexConnected,
405
- unavailableReason: options.codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason,
406
- },
407
- ]);
408
- const [selectedModelId, setSelectedModelId] = useState<SessionRealtimeModel>(
409
- () => readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id,
520
+ const initialCachedCatalog = readCachedRealtimeModelCatalog(client, workspaceId);
521
+ const [catalog, setCatalog] = useState<RealtimeModelOption[]>(
522
+ () => initialCachedCatalog ?? realtimeModelFallback(options.codexConnected),
410
523
  );
524
+ const catalogScopeRef = useRef({
525
+ client,
526
+ workspaceId,
527
+ source: initialCachedCatalog ? ("cache" as const) : ("fallback" as const),
528
+ });
529
+ const [selectedModelId, setSelectedModelId] = useState<SessionRealtimeModel>(() => {
530
+ const preferred = readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id;
531
+ if (!initialCachedCatalog) return preferred;
532
+ if (initialCachedCatalog.some((model) => model.id === preferred && model.available)) {
533
+ return preferred;
534
+ }
535
+ return initialCachedCatalog.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
536
+ });
411
537
 
412
538
  useEffect(() => {
413
539
  let disposed = false;
414
- const load = client.getWorkspaceRealtimeModelCatalog;
415
- if (!load) return;
416
- void load
417
- .call(client, workspaceId)
418
- .then((response) => {
540
+ const applyCatalog = (models: RealtimeModelOption[]) => {
541
+ catalogScopeRef.current = { client, workspaceId, source: "cache" };
542
+ setCatalog(models);
543
+ setSelectedModelId((current) => {
544
+ if (models.some((model) => model.id === current && model.available)) return current;
545
+ return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
546
+ });
547
+ };
548
+ const cached = readCachedRealtimeModelCatalog(client, workspaceId);
549
+ if (cached) {
550
+ const scope = catalogScopeRef.current;
551
+ if (
552
+ scope.client !== client ||
553
+ scope.workspaceId !== workspaceId ||
554
+ scope.source !== "cache"
555
+ ) {
556
+ applyCatalog(cached);
557
+ }
558
+ return;
559
+ }
560
+ const loading = loadRealtimeModelCatalog(client, workspaceId);
561
+ if (!loading) return;
562
+ void loading
563
+ .then((models) => {
419
564
  if (disposed) return;
420
- const models = response.models.map(toRealtimeModelOption);
421
- setCatalog(models);
422
- setSelectedModelId((current) => {
423
- if (models.some((model) => model.id === current && model.available)) return current;
424
- return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
425
- });
565
+ applyCatalog(models);
426
566
  })
427
567
  .catch(() => undefined);
428
568
  return () => {
@@ -684,7 +824,13 @@ export function RealtimeVoiceControl(props: {
684
824
  opacity: 1,
685
825
  transition: reduceMotion
686
826
  ? { duration: 0 }
687
- : { delay: 0.32, type: "spring", stiffness: 320, damping: 30, mass: 0.8 },
827
+ : {
828
+ delay: 0.32,
829
+ type: "spring",
830
+ stiffness: 320,
831
+ damping: 30,
832
+ mass: 0.8,
833
+ },
688
834
  }}
689
835
  {...(reduceMotion
690
836
  ? {}
@@ -1160,7 +1306,11 @@ function statusContent(
1160
1306
  };
1161
1307
  }
1162
1308
  if (!canStart && admissionBlocker) {
1163
- return { phase: "unavailable", label: "Voice unavailable", detail: admissionBlocker };
1309
+ return {
1310
+ phase: "unavailable",
1311
+ label: "Voice unavailable",
1312
+ detail: admissionBlocker,
1313
+ };
1164
1314
  }
1165
1315
  return {
1166
1316
  phase: "idle",
@@ -1268,7 +1418,11 @@ function RealtimeStatusDot(props: { phase: RealtimeVisualPhase; reduceMotion: bo
1268
1418
  <motion.span
1269
1419
  className="absolute inset-0 rounded-full bg-og-status-running"
1270
1420
  animate={{ opacity: [0.45, 0], scale: [1, 2.2] }}
1271
- transition={{ duration: 1.4, ease: "easeOut", repeat: Number.POSITIVE_INFINITY }}
1421
+ transition={{
1422
+ duration: 1.4,
1423
+ ease: "easeOut",
1424
+ repeat: Number.POSITIVE_INFINITY,
1425
+ }}
1272
1426
  />
1273
1427
  ) : null}
1274
1428
  <span
@@ -1343,6 +1497,7 @@ function isRealtimeModel(value: string | null): value is SessionRealtimeModel {
1343
1497
  value === "opengeni-gateway/openai/gpt-realtime-2.1" ||
1344
1498
  value === "opengeni-gateway/openai/gpt-realtime-mini" ||
1345
1499
  value === "opengeni-gateway/xai/grok-voice-think-fast-2.0" ||
1500
+ value === "supergrok/grok-voice-think-fast-2.0" ||
1346
1501
  value === "workspace-gateway/openai/gpt-realtime-2.1" ||
1347
1502
  value === "workspace-gateway/openai/gpt-realtime-mini" ||
1348
1503
  value === "workspace-gateway/xai/grok-voice-think-fast-2.0"