@opengeni/react 0.36.1 → 0.40.0

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 (85) hide show
  1. package/README.md +92 -2
  2. package/dist/{chunk-J2RVJ6JX.js → chunk-4IJCL7YO.js} +13 -2
  3. package/dist/chunk-4IJCL7YO.js.map +1 -0
  4. package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
  5. package/dist/chunk-EB67J347.js.map +1 -0
  6. package/dist/chunk-EKZH6IDG.js +141 -0
  7. package/dist/chunk-EKZH6IDG.js.map +1 -0
  8. package/dist/{chunk-OKKMZDQF.js → chunk-JALF5FI3.js} +9 -7
  9. package/dist/{chunk-OKKMZDQF.js.map → chunk-JALF5FI3.js.map} +1 -1
  10. package/dist/{chunk-Z5FV355Z.js → chunk-KR2SK5GJ.js} +1382 -724
  11. package/dist/chunk-KR2SK5GJ.js.map +1 -0
  12. package/dist/{chunk-N4XEBE23.js → chunk-OMCFRWHL.js} +22 -13
  13. package/dist/{chunk-N4XEBE23.js.map → chunk-OMCFRWHL.js.map} +1 -1
  14. package/dist/{chunk-FT2TXNGZ.js → chunk-Q2NCKWTK.js} +183 -18
  15. package/dist/chunk-Q2NCKWTK.js.map +1 -0
  16. package/dist/{chunk-22RM4GMO.js → chunk-SJKT4TKW.js} +26 -2
  17. package/dist/chunk-SJKT4TKW.js.map +1 -0
  18. package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
  19. package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
  20. package/dist/chunk-WZT5G5OR.js +433 -0
  21. package/dist/chunk-WZT5G5OR.js.map +1 -0
  22. package/dist/client.d.ts +2 -0
  23. package/dist/components/chat-composer.d.ts +7 -1
  24. package/dist/components/human-input-form.d.ts +16 -6
  25. package/dist/components/human-input-surface.d.ts +19 -0
  26. package/dist/components/machine-input-display.d.ts +12 -0
  27. package/dist/components/message-timeline.d.ts +6 -1
  28. package/dist/components/model-policy-picker.d.ts +78 -0
  29. package/dist/components/session-chrome.d.ts +2 -1
  30. package/dist/components/tip-follow.d.ts +30 -0
  31. package/dist/composer.d.ts +2 -0
  32. package/dist/composer.js +22 -5
  33. package/dist/hooks/use-composer.d.ts +10 -1
  34. package/dist/index.d.ts +7 -1
  35. package/dist/index.js +2261 -2230
  36. package/dist/index.js.map +1 -1
  37. package/dist/machines.js +4 -3
  38. package/dist/model-policy.d.ts +12 -4
  39. package/dist/model-policy.js +5 -1
  40. package/dist/realtime/dropdown-menu.d.ts +16 -0
  41. package/dist/realtime/realtime-control.d.ts +126 -0
  42. package/dist/realtime.d.ts +11 -0
  43. package/dist/realtime.js +1023 -0
  44. package/dist/realtime.js.map +1 -0
  45. package/dist/session-context.d.ts +7 -1
  46. package/dist/session-ui.d.ts +2 -0
  47. package/dist/session-ui.js +6 -4
  48. package/dist/session.js +5 -4
  49. package/dist/timeline/compute-label.d.ts +2 -0
  50. package/dist/timeline/index.d.ts +1 -0
  51. package/dist/timeline/types.d.ts +5 -0
  52. package/package.json +7 -2
  53. package/src/client.ts +13 -0
  54. package/src/components/chat-composer.tsx +19 -5
  55. package/src/components/composer-transcription-control.tsx +2 -2
  56. package/src/components/composer.tsx +14 -5
  57. package/src/components/human-input-form.tsx +506 -186
  58. package/src/components/human-input-surface.tsx +80 -0
  59. package/src/components/machine-input-display.ts +83 -0
  60. package/src/components/message-timeline.tsx +348 -308
  61. package/src/components/model-policy-picker.tsx +587 -0
  62. package/src/components/sandbox-files.tsx +1 -1
  63. package/src/components/session-chrome.tsx +141 -15
  64. package/src/components/tip-follow.ts +127 -6
  65. package/src/composer.ts +13 -0
  66. package/src/hooks/use-composer.ts +229 -29
  67. package/src/index.ts +19 -0
  68. package/src/model-policy.ts +51 -7
  69. package/src/realtime/dropdown-menu.tsx +123 -0
  70. package/src/realtime/realtime-control.tsx +1178 -0
  71. package/src/realtime.ts +27 -0
  72. package/src/session-context.ts +16 -0
  73. package/src/session-ui.ts +2 -0
  74. package/src/timeline/compute-label.tsx +18 -0
  75. package/src/timeline/index.ts +3 -0
  76. package/src/timeline/projection.ts +21 -1
  77. package/src/timeline/tool-renderers.tsx +232 -43
  78. package/src/timeline/types.ts +5 -0
  79. package/styles/index.css +61 -0
  80. package/styles/tokens.css +2 -0
  81. package/dist/chunk-22RM4GMO.js.map +0 -1
  82. package/dist/chunk-7CJOAW3V.js.map +0 -1
  83. package/dist/chunk-FT2TXNGZ.js.map +0 -1
  84. package/dist/chunk-J2RVJ6JX.js.map +0 -1
  85. package/dist/chunk-Z5FV355Z.js.map +0 -1
@@ -0,0 +1,1023 @@
1
+ import {
2
+ BillingClassMark,
3
+ PickerAnimatedPage,
4
+ PickerBackHeader,
5
+ PickerNavRow
6
+ } from "./chunk-WZT5G5OR.js";
7
+ import {
8
+ useEmbeddedRealtimeSession
9
+ } from "./chunk-EKZH6IDG.js";
10
+ import "./chunk-SJKT4TKW.js";
11
+ import {
12
+ cn
13
+ } from "./chunk-TK7G6XLT.js";
14
+
15
+ // src/realtime/realtime-control.tsx
16
+ import {
17
+ hasStoredSessionRealtimeOwnerProof,
18
+ projectSessionRealtimeLifecycle
19
+ } from "@opengeni/sdk/realtime";
20
+ import {
21
+ AudioLinesIcon,
22
+ CheckIcon,
23
+ ChevronDownIcon,
24
+ CircleAlertIcon,
25
+ LoaderCircleIcon,
26
+ MicIcon,
27
+ MicOffIcon,
28
+ RotateCcwIcon,
29
+ SquareIcon,
30
+ Volume2Icon,
31
+ VolumeXIcon
32
+ } from "lucide-react";
33
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
34
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
35
+
36
+ // src/realtime/dropdown-menu.tsx
37
+ import { ChevronRightIcon } from "lucide-react";
38
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
39
+ import { jsx, jsxs } from "react/jsx-runtime";
40
+ function DropdownMenu(props) {
41
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
42
+ }
43
+ function DropdownMenuTrigger(props) {
44
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
45
+ }
46
+ function DropdownMenuContent({
47
+ className,
48
+ sideOffset = 4,
49
+ ...props
50
+ }) {
51
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
52
+ DropdownMenuPrimitive.Content,
53
+ {
54
+ "data-slot": "dropdown-menu-content",
55
+ sideOffset,
56
+ className: cn(
57
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
58
+ className
59
+ ),
60
+ ...props
61
+ }
62
+ ) });
63
+ }
64
+ function DropdownMenuItem({
65
+ className,
66
+ inset,
67
+ variant = "default",
68
+ ...props
69
+ }) {
70
+ return /* @__PURE__ */ jsx(
71
+ DropdownMenuPrimitive.Item,
72
+ {
73
+ "data-slot": "dropdown-menu-item",
74
+ "data-inset": inset,
75
+ "data-variant": variant,
76
+ className: cn(
77
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
78
+ className
79
+ ),
80
+ ...props
81
+ }
82
+ );
83
+ }
84
+ function DropdownMenuSeparator({
85
+ className,
86
+ ...props
87
+ }) {
88
+ return /* @__PURE__ */ jsx(
89
+ DropdownMenuPrimitive.Separator,
90
+ {
91
+ "data-slot": "dropdown-menu-separator",
92
+ className: cn("-mx-1 my-1 h-px bg-border", className),
93
+ ...props
94
+ }
95
+ );
96
+ }
97
+ function DropdownMenuSub(props) {
98
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Sub, { "data-slot": "dropdown-menu-sub", ...props });
99
+ }
100
+ function DropdownMenuSubTrigger({
101
+ className,
102
+ inset,
103
+ children,
104
+ ...props
105
+ }) {
106
+ return /* @__PURE__ */ jsxs(
107
+ DropdownMenuPrimitive.SubTrigger,
108
+ {
109
+ "data-slot": "dropdown-menu-sub-trigger",
110
+ "data-inset": inset,
111
+ className: cn(
112
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
113
+ className
114
+ ),
115
+ ...props,
116
+ children: [
117
+ children,
118
+ /* @__PURE__ */ jsx(ChevronRightIcon, { className: "ml-auto size-4" })
119
+ ]
120
+ }
121
+ );
122
+ }
123
+ function DropdownMenuSubContent({
124
+ className,
125
+ ...props
126
+ }) {
127
+ return /* @__PURE__ */ jsx(
128
+ DropdownMenuPrimitive.SubContent,
129
+ {
130
+ "data-slot": "dropdown-menu-sub-content",
131
+ className: cn(
132
+ "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
133
+ className
134
+ ),
135
+ ...props
136
+ }
137
+ );
138
+ }
139
+
140
+ // src/realtime/realtime-control.tsx
141
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
142
+ var CODEX_LIVE_MODEL = {
143
+ id: "gpt-live-1-boulder-alpha",
144
+ label: "Codex Live",
145
+ provider: "Connected Codex",
146
+ description: "Deep session integration",
147
+ available: false,
148
+ unavailableReason: "Connect Codex to use this voice model",
149
+ recommended: false
150
+ };
151
+ var REALTIME_MODEL_PROVIDERS = ["OpenGeni", "Connected Codex", "Your Gateway"];
152
+ var REALTIME_PROVIDER_META = {
153
+ OpenGeni: { billingClass: "opengeni_credits", hint: "Will use credits" },
154
+ "Connected Codex": {
155
+ billingClass: "codex_subscription",
156
+ hint: "ChatGPT / Codex plan"
157
+ },
158
+ "Your Gateway": { billingClass: "byok", hint: "Billed to your AI Gateway" }
159
+ };
160
+ var REALTIME_MODEL_STORAGE_PREFIX = "opengeni:realtime-model";
161
+ function codexRealtimeAdmissionAllowed(input) {
162
+ return codexRealtimeAdmissionBlocker(input) === null;
163
+ }
164
+ function codexRealtimeAdmissionBlocker(input) {
165
+ if (input.sessionStatus === "cancelled") return "This session was cancelled.";
166
+ if (input.controlState !== "active") return "Resume this session before starting voice.";
167
+ if (input.settlement !== null) return "Wait for the current session transition to finish.";
168
+ if (!input.codexConnected) return "Connect Codex to use this voice model.";
169
+ if (input.lifecycleActive) return "Voice is already active for this session.";
170
+ return null;
171
+ }
172
+ function useSessionRealtime(options) {
173
+ const { client, workspaceId } = useEmbeddedRealtimeSession({
174
+ client: options.client,
175
+ workspaceId: options.workspaceId
176
+ });
177
+ const model = options.model ?? CODEX_LIVE_MODEL.id;
178
+ const audioRef = useRef(null);
179
+ const controllerRef = useRef(null);
180
+ const controllerModelRef = useRef(null);
181
+ const [snapshot, setSnapshot] = useState(() => ({
182
+ status: hasStoredSessionRealtimeOwnerProof({ workspaceId, sessionId: options.sessionId, model }) ? "recovering" : "idle",
183
+ realtimeId: null,
184
+ mode: null,
185
+ bridge: null,
186
+ microphone: "inactive",
187
+ inputMuted: false,
188
+ audibleOutput: "inactive",
189
+ outputMuted: false,
190
+ connectionGeneration: 0,
191
+ reconnectAttempt: 0,
192
+ diagnostic: null,
193
+ error: null
194
+ }));
195
+ const lifecycle = useMemo(
196
+ () => projectSessionRealtimeLifecycle(options.events),
197
+ [options.events]
198
+ );
199
+ const lifecycleRef = useRef(lifecycle);
200
+ const eventsReadyRef = useRef(options.eventsReady);
201
+ lifecycleRef.current = lifecycle;
202
+ eventsReadyRef.current = options.eventsReady;
203
+ const lifecycleActive = lifecycle?.state === "active";
204
+ useEffect(() => {
205
+ let disposed = false;
206
+ let controller = null;
207
+ let unsubscribe = null;
208
+ void import("@opengeni/sdk/realtime").then(({ createSessionRealtimeController }) => {
209
+ if (disposed) return;
210
+ const controllerFactory = options.controllerFactory ?? createSessionRealtimeController;
211
+ controller = controllerFactory({
212
+ client,
213
+ workspaceId,
214
+ sessionId: options.sessionId,
215
+ ...audioRef.current ? { remoteAudio: audioRef.current } : {},
216
+ model
217
+ });
218
+ controllerRef.current = controller;
219
+ controllerModelRef.current = model;
220
+ unsubscribe = controller.subscribe(setSnapshot);
221
+ if (eventsReadyRef.current) {
222
+ void controller.observeLifecycle(lifecycleRef.current).catch(() => void 0);
223
+ }
224
+ }).catch((error) => {
225
+ if (disposed) return;
226
+ setSnapshot({
227
+ status: "error",
228
+ realtimeId: null,
229
+ mode: null,
230
+ bridge: null,
231
+ microphone: "inactive",
232
+ inputMuted: false,
233
+ audibleOutput: "inactive",
234
+ outputMuted: false,
235
+ connectionGeneration: 0,
236
+ reconnectAttempt: 0,
237
+ diagnostic: {
238
+ kind: "negotiation_failure",
239
+ message: error instanceof Error ? error.message : "Codex realtime controller failed to load",
240
+ recoverable: false,
241
+ connectionGeneration: 0,
242
+ attempt: 0
243
+ },
244
+ error: error instanceof Error ? error.message : "Codex realtime controller failed to load"
245
+ });
246
+ });
247
+ return () => {
248
+ disposed = true;
249
+ unsubscribe?.();
250
+ controller?.close();
251
+ if (controllerRef.current === controller) {
252
+ controllerRef.current = null;
253
+ controllerModelRef.current = null;
254
+ }
255
+ };
256
+ }, [client, model, options.controllerFactory, options.sessionId, workspaceId]);
257
+ useEffect(() => {
258
+ if (!options.eventsReady) return;
259
+ void controllerRef.current?.observeLifecycle(lifecycle).catch(() => void 0);
260
+ }, [lifecycle, options.eventsReady]);
261
+ const start = useCallback(async () => {
262
+ const controller = controllerRef.current;
263
+ if (!controller || controllerModelRef.current !== model) {
264
+ throw new Error("The selected voice model is still preparing");
265
+ }
266
+ await controller.start();
267
+ }, [model]);
268
+ const stop = useCallback(async () => {
269
+ await controllerRef.current?.stop();
270
+ }, []);
271
+ const retry = useCallback(async () => {
272
+ await controllerRef.current?.retry();
273
+ }, []);
274
+ const retryAudibleOutput = useCallback(async () => {
275
+ await controllerRef.current?.retryAudibleOutput();
276
+ }, []);
277
+ const setInputMuted = useCallback((muted) => {
278
+ controllerRef.current?.setInputMuted(muted);
279
+ }, []);
280
+ const setOutputMuted = useCallback((muted) => {
281
+ controllerRef.current?.setOutputMuted(muted);
282
+ }, []);
283
+ const admissionInput = {
284
+ sessionStatus: options.sessionStatus,
285
+ controlState: options.effectiveControl.state,
286
+ settlement: options.effectiveControl.settlement,
287
+ codexConnected: options.modelAvailable ?? options.codexConnected,
288
+ lifecycleActive
289
+ };
290
+ const admissionBlocker = options.modelAvailable === false ? options.modelUnavailableReason ?? "This voice model is unavailable." : codexRealtimeAdmissionBlocker(admissionInput);
291
+ const canStart = controllerRef.current !== null && controllerModelRef.current === model && ["idle", "error"].includes(snapshot.status) && admissionBlocker === null;
292
+ return {
293
+ snapshot,
294
+ lifecycleActive,
295
+ canStart,
296
+ admissionBlocker,
297
+ codexConnected: options.codexConnected,
298
+ audioRef,
299
+ start,
300
+ stop,
301
+ retry,
302
+ retryAudibleOutput,
303
+ setInputMuted,
304
+ setOutputMuted
305
+ };
306
+ }
307
+ function SessionRealtimeControl(props) {
308
+ const lifecycle = useMemo(() => projectSessionRealtimeLifecycle(props.events), [props.events]);
309
+ const selection = useRealtimeModelSelection({
310
+ client: props.client,
311
+ workspaceId: props.workspaceId,
312
+ codexConnected: props.codexConnected,
313
+ activeModel: lifecycle?.state === "active" ? lifecycle.model : null
314
+ });
315
+ const selectedModel = selection.selectedModel;
316
+ const realtime = useSessionRealtime({
317
+ ...props,
318
+ model: selectedModel.id,
319
+ modelAvailable: selectedModel.available,
320
+ modelUnavailableReason: selectedModel.unavailableReason
321
+ });
322
+ const autostartModelRef = useRef(props.realtimeAutostartModel ?? null);
323
+ const autostartStartedRef = useRef(false);
324
+ const { models, selectedModel: selectedRealtimeModel, selectModel } = selection;
325
+ const { canStart, start } = realtime;
326
+ const onRealtimeAutostartConsumed = props.onRealtimeAutostartConsumed;
327
+ useEffect(() => {
328
+ const pending = autostartModelRef.current;
329
+ if (!pending || autostartStartedRef.current || lifecycle?.state === "active") return;
330
+ const available = models.some((model) => model.id === pending && model.available);
331
+ if (!available) return;
332
+ if (selectedRealtimeModel.id !== pending) {
333
+ selectModel(pending);
334
+ return;
335
+ }
336
+ if (!canStart) return;
337
+ autostartStartedRef.current = true;
338
+ void start().then(() => {
339
+ autostartModelRef.current = null;
340
+ onRealtimeAutostartConsumed?.();
341
+ }).catch(() => {
342
+ autostartStartedRef.current = false;
343
+ });
344
+ }, [
345
+ canStart,
346
+ lifecycle?.state,
347
+ models,
348
+ props.sessionId,
349
+ props.workspaceId,
350
+ onRealtimeAutostartConsumed,
351
+ selectModel,
352
+ selectedRealtimeModel.id,
353
+ start
354
+ ]);
355
+ return /* @__PURE__ */ jsx2(
356
+ RealtimeVoiceControl,
357
+ {
358
+ snapshot: realtime.snapshot,
359
+ canStart: realtime.canStart,
360
+ admissionBlocker: realtime.admissionBlocker,
361
+ modelAvailable: selectedModel.available,
362
+ menuSide: "top",
363
+ audioRef: realtime.audioRef,
364
+ selectedModel,
365
+ models: selection.models,
366
+ onSelectModel: realtime.lifecycleActive ? void 0 : selection.selectModel,
367
+ onStart: realtime.start,
368
+ onStop: realtime.stop,
369
+ onRetry: realtime.retry,
370
+ onRetryAudibleOutput: realtime.retryAudibleOutput,
371
+ onSetInputMuted: realtime.setInputMuted,
372
+ onSetOutputMuted: realtime.setOutputMuted
373
+ }
374
+ );
375
+ }
376
+ function useRealtimeModelSelection(options) {
377
+ const { client, workspaceId } = useEmbeddedRealtimeSession({
378
+ client: options.client,
379
+ workspaceId: options.workspaceId
380
+ });
381
+ const activeModel = options.activeModel;
382
+ const [catalog, setCatalog] = useState(() => [
383
+ {
384
+ ...CODEX_LIVE_MODEL,
385
+ available: options.codexConnected,
386
+ unavailableReason: options.codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason
387
+ }
388
+ ]);
389
+ const [selectedModelId, setSelectedModelId] = useState(
390
+ () => readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id
391
+ );
392
+ useEffect(() => {
393
+ let disposed = false;
394
+ const load = client.getWorkspaceRealtimeModelCatalog;
395
+ if (!load) return;
396
+ void load.call(client, workspaceId).then((response) => {
397
+ if (disposed) return;
398
+ const models = response.models.map(toRealtimeModelOption);
399
+ setCatalog(models);
400
+ setSelectedModelId((current) => {
401
+ if (models.some((model) => model.id === current && model.available)) return current;
402
+ return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
403
+ });
404
+ }).catch(() => void 0);
405
+ return () => {
406
+ disposed = true;
407
+ };
408
+ }, [client, workspaceId]);
409
+ useEffect(() => {
410
+ if (activeModel) setSelectedModelId(activeModel);
411
+ }, [activeModel]);
412
+ const selectedModel = catalog.find((model) => model.id === selectedModelId) ?? {
413
+ ...CODEX_LIVE_MODEL,
414
+ available: options.codexConnected,
415
+ unavailableReason: options.codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason
416
+ };
417
+ const selectModel = useCallback(
418
+ (value) => {
419
+ const model = catalog.find((candidate) => candidate.id === value);
420
+ if (!model || !model.available || activeModel) return;
421
+ setSelectedModelId(model.id);
422
+ writeRealtimeModelPreference(workspaceId, model.id);
423
+ },
424
+ [activeModel, catalog, workspaceId]
425
+ );
426
+ return { models: catalog, selectedModel, selectModel };
427
+ }
428
+ var IDLE_REALTIME_SNAPSHOT = {
429
+ status: "idle",
430
+ realtimeId: null,
431
+ mode: null,
432
+ bridge: null,
433
+ microphone: "inactive",
434
+ inputMuted: false,
435
+ audibleOutput: "inactive",
436
+ outputMuted: false,
437
+ connectionGeneration: 0,
438
+ reconnectAttempt: 0,
439
+ diagnostic: null,
440
+ error: null
441
+ };
442
+ function NewSessionRealtimeControl(props) {
443
+ const selection = useRealtimeModelSelection({
444
+ client: props.client,
445
+ workspaceId: props.workspaceId,
446
+ codexConnected: props.codexConnected
447
+ });
448
+ const audioRef = useRef(null);
449
+ const [starting, setStarting] = useState(false);
450
+ const [error, setError] = useState(null);
451
+ const snapshot = useMemo(
452
+ () => starting ? { ...IDLE_REALTIME_SNAPSHOT, status: "starting" } : error ? { ...IDLE_REALTIME_SNAPSHOT, status: "error", error } : IDLE_REALTIME_SNAPSHOT,
453
+ [error, starting]
454
+ );
455
+ const canStart = selection.selectedModel.available && props.disabled !== true && !starting;
456
+ const onStart = props.onStart;
457
+ const selectedModelId = selection.selectedModel.id;
458
+ const start = useCallback(async () => {
459
+ if (!canStart) return;
460
+ setStarting(true);
461
+ setError(null);
462
+ try {
463
+ const started = await onStart(selectedModelId);
464
+ if (!started) setError("The voice session could not be created.");
465
+ } catch (cause) {
466
+ setError(cause instanceof Error ? cause.message : String(cause));
467
+ } finally {
468
+ setStarting(false);
469
+ }
470
+ }, [canStart, onStart, selectedModelId]);
471
+ return /* @__PURE__ */ jsx2(
472
+ RealtimeVoiceControl,
473
+ {
474
+ snapshot,
475
+ canStart,
476
+ admissionBlocker: selection.selectedModel.unavailableReason ?? props.disabledReason ?? null,
477
+ modelAvailable: selection.selectedModel.available,
478
+ selectionDisabled: starting,
479
+ menuSide: "bottom",
480
+ showDiagnostics: false,
481
+ audioRef,
482
+ selectedModel: selection.selectedModel,
483
+ models: selection.models,
484
+ onSelectModel: selection.selectModel,
485
+ onStart: start,
486
+ onStop: async () => void 0,
487
+ onRetry: start,
488
+ onRetryAudibleOutput: async () => void 0,
489
+ onSetInputMuted: () => void 0,
490
+ onSetOutputMuted: () => void 0
491
+ }
492
+ );
493
+ }
494
+ function RealtimeVoiceControl(props) {
495
+ const reduceMotion = useReducedMotion();
496
+ const selectedModel = props.selectedModel ?? {
497
+ ...CODEX_LIVE_MODEL,
498
+ available: props.modelAvailable,
499
+ unavailableReason: props.modelAvailable ? null : CODEX_LIVE_MODEL.unavailableReason
500
+ };
501
+ const models = props.models ?? [selectedModel];
502
+ const status = statusContent(
503
+ props.snapshot,
504
+ props.modelAvailable,
505
+ props.canStart,
506
+ props.admissionBlocker ?? null,
507
+ selectedModel.label
508
+ );
509
+ const modeOwned = props.snapshot.mode?.state === "active";
510
+ const retryConnection = modeOwned && props.snapshot.status !== "stopping" && props.snapshot.status !== "active" && props.snapshot.diagnostic?.recoverable === true;
511
+ const audioBlocked = props.snapshot.audibleOutput === "blocked" && !props.snapshot.outputMuted;
512
+ const mainDisabled = props.snapshot.status === "stopping" || props.snapshot.status === "lost_owner" || !modeOwned && !props.canStart || props.snapshot.status === "starting" && !modeOwned;
513
+ const mainLabel = audioBlocked ? "Resume voice audio" : retryConnection ? "Retry voice connection" : modeOwned ? "End voice conversation" : `Start voice with ${selectedModel.label}`;
514
+ const runMainAction = audioBlocked ? props.onRetryAudibleOutput : retryConnection ? props.onRetry : modeOwned ? props.onStop : props.onStart;
515
+ const diagnosticsVisible = props.showDiagnostics ?? import.meta.env.DEV;
516
+ const [pickerOpen, setPickerOpen] = useState(false);
517
+ const [pickerProvider, setPickerProvider] = useState(
518
+ selectedModel.provider
519
+ );
520
+ const [pickerDirection, setPickerDirection] = useState(1);
521
+ useEffect(() => {
522
+ if (!pickerOpen) setPickerProvider(selectedModel.provider);
523
+ }, [pickerOpen, selectedModel.provider]);
524
+ return /* @__PURE__ */ jsxs2(
525
+ "div",
526
+ {
527
+ "aria-label": "Realtime voice",
528
+ "data-picker-side": props.menuSide ?? "top",
529
+ className: "inline-flex shrink-0 items-center",
530
+ children: [
531
+ /* @__PURE__ */ jsx2("audio", { ref: props.audioRef, autoPlay: true, className: "hidden", "aria-hidden": "true" }),
532
+ /* @__PURE__ */ jsx2(RealtimeComposerGlow, { phase: status.phase, reduceMotion: reduceMotion === true }),
533
+ /* @__PURE__ */ jsx2(AnimatePresence, { initial: false, children: modeOwned ? /* @__PURE__ */ jsxs2(
534
+ motion.div,
535
+ {
536
+ "data-testid": "realtime-mute-controls",
537
+ initial: reduceMotion ? false : { opacity: 0, width: 0, marginRight: 0 },
538
+ animate: { opacity: 1, width: "auto", marginRight: 4 },
539
+ ...reduceMotion ? {} : { exit: { opacity: 0, width: 0, marginRight: 0 } },
540
+ transition: { duration: reduceMotion ? 0 : 0.18, ease: [0.2, 0.8, 0.2, 1] },
541
+ className: "inline-flex shrink-0 items-center gap-0.5 overflow-hidden",
542
+ children: [
543
+ /* @__PURE__ */ jsx2(
544
+ RealtimeMuteButton,
545
+ {
546
+ muted: props.snapshot.inputMuted,
547
+ kind: "microphone",
548
+ reduceMotion: reduceMotion === true,
549
+ onToggle: () => props.onSetInputMuted(!props.snapshot.inputMuted)
550
+ }
551
+ ),
552
+ /* @__PURE__ */ jsx2(
553
+ RealtimeMuteButton,
554
+ {
555
+ muted: props.snapshot.outputMuted,
556
+ kind: "output",
557
+ reduceMotion: reduceMotion === true,
558
+ onToggle: () => props.onSetOutputMuted(!props.snapshot.outputMuted)
559
+ }
560
+ )
561
+ ]
562
+ },
563
+ "realtime-mute-controls"
564
+ ) : null }),
565
+ /* @__PURE__ */ jsx2(
566
+ motion.button,
567
+ {
568
+ type: "button",
569
+ "data-testid": "realtime-primary-action",
570
+ "data-phase": status.phase,
571
+ "aria-label": mainLabel,
572
+ "aria-pressed": modeOwned,
573
+ title: mainLabel,
574
+ disabled: mainDisabled,
575
+ ...reduceMotion ? {} : { whileTap: { scale: 0.92 } },
576
+ transition: { type: "spring", stiffness: 520, damping: 30 },
577
+ onClick: () => void runMainAction().catch(() => void 0),
578
+ className: cn(
579
+ "relative inline-flex size-8 items-center justify-center rounded-l-og-md border outline-none",
580
+ "transition-[background-color,border-color,color,box-shadow] duration-200 ease-og-out",
581
+ "focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
582
+ "disabled:cursor-not-allowed disabled:opacity-45 pointer-coarse:size-11",
583
+ voiceButtonTone(status.phase)
584
+ ),
585
+ children: /* @__PURE__ */ jsx2(
586
+ RealtimeActionGlyph,
587
+ {
588
+ phase: status.phase,
589
+ audioBlocked,
590
+ reduceMotion: reduceMotion === true
591
+ }
592
+ )
593
+ }
594
+ ),
595
+ /* @__PURE__ */ jsxs2(DropdownMenu, { open: pickerOpen, onOpenChange: setPickerOpen, children: [
596
+ /* @__PURE__ */ jsx2(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(
597
+ "button",
598
+ {
599
+ type: "button",
600
+ "aria-label": "Choose voice model and options",
601
+ title: `Voice model: ${selectedModel.label}`,
602
+ disabled: props.selectionDisabled,
603
+ className: cn(
604
+ "inline-flex h-8 w-6 items-center justify-center rounded-r-og-md border border-l-0 outline-none",
605
+ "transition-[background-color,border-color,color] duration-200 ease-og-out",
606
+ "focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
607
+ "pointer-coarse:h-11 pointer-coarse:w-7",
608
+ voiceButtonTone(status.phase)
609
+ ),
610
+ children: /* @__PURE__ */ jsx2(ChevronDownIcon, { className: "size-3", "aria-hidden": true })
611
+ }
612
+ ) }),
613
+ /* @__PURE__ */ jsxs2(
614
+ DropdownMenuContent,
615
+ {
616
+ side: props.menuSide ?? "top",
617
+ align: "end",
618
+ sideOffset: 8,
619
+ collisionPadding: 12,
620
+ className: "flex w-72 max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] flex-col overflow-hidden rounded-xl border-border bg-surface p-1.5 shadow-xl",
621
+ onCloseAutoFocus: (event) => event.preventDefault(),
622
+ children: [
623
+ /* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children: /* @__PURE__ */ jsx2(
624
+ RealtimeModelPickerMenu,
625
+ {
626
+ models,
627
+ selectedModel,
628
+ provider: pickerProvider,
629
+ direction: pickerDirection,
630
+ disabled: props.selectionDisabled === true || props.snapshot.mode?.state === "active",
631
+ onProviderChange: (provider, direction) => {
632
+ setPickerDirection(direction);
633
+ setPickerProvider(provider);
634
+ },
635
+ onSelect: (modelId) => {
636
+ props.onSelectModel?.(modelId);
637
+ setPickerOpen(false);
638
+ }
639
+ }
640
+ ) }),
641
+ status.phase !== "idle" ? /* @__PURE__ */ jsxs2(Fragment, { children: [
642
+ /* @__PURE__ */ jsx2(DropdownMenuSeparator, { className: "mx-1 bg-og-border" }),
643
+ /* @__PURE__ */ jsxs2("div", { className: "px-2.5 py-2", role: "status", "aria-live": "polite", children: [
644
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-og-sm font-medium text-og-fg", children: [
645
+ /* @__PURE__ */ jsx2(RealtimeStatusDot, { phase: status.phase, reduceMotion: reduceMotion === true }),
646
+ status.label
647
+ ] }),
648
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-og-xs leading-5 text-og-fg-subtle", children: status.detail })
649
+ ] })
650
+ ] }) : null,
651
+ audioBlocked ? /* @__PURE__ */ jsxs2(
652
+ DropdownMenuItem,
653
+ {
654
+ className: "rounded-og-md",
655
+ onSelect: () => void props.onRetryAudibleOutput().catch(() => void 0),
656
+ children: [
657
+ /* @__PURE__ */ jsx2(Volume2Icon, {}),
658
+ "Resume audio"
659
+ ]
660
+ }
661
+ ) : null,
662
+ retryConnection ? /* @__PURE__ */ jsxs2(
663
+ DropdownMenuItem,
664
+ {
665
+ className: "rounded-og-md",
666
+ onSelect: () => void props.onRetry().catch(() => void 0),
667
+ children: [
668
+ /* @__PURE__ */ jsx2(RotateCcwIcon, {}),
669
+ "Retry connection"
670
+ ]
671
+ }
672
+ ) : null,
673
+ modeOwned && props.snapshot.status !== "lost_owner" ? /* @__PURE__ */ jsxs2(
674
+ DropdownMenuItem,
675
+ {
676
+ variant: "destructive",
677
+ className: "rounded-og-md",
678
+ disabled: props.snapshot.status === "stopping",
679
+ onSelect: () => void props.onStop().catch(() => void 0),
680
+ children: [
681
+ /* @__PURE__ */ jsx2(SquareIcon, {}),
682
+ "End voice conversation"
683
+ ]
684
+ }
685
+ ) : null,
686
+ diagnosticsVisible ? /* @__PURE__ */ jsx2(RealtimeDiagnosticsMenu, { snapshot: props.snapshot }) : null
687
+ ]
688
+ }
689
+ )
690
+ ] }),
691
+ /* @__PURE__ */ jsxs2("span", { className: "sr-only", role: "status", "aria-live": "polite", children: [
692
+ status.label,
693
+ ". ",
694
+ status.detail
695
+ ] }),
696
+ props.snapshot.error ? /* @__PURE__ */ jsx2("span", { className: "sr-only", role: "alert", children: props.snapshot.error }) : null
697
+ ]
698
+ }
699
+ );
700
+ }
701
+ function RealtimeMuteButton(props) {
702
+ const microphone = props.kind === "microphone";
703
+ const label = microphone ? props.muted ? "Unmute microphone" : "Mute microphone" : props.muted ? "Unmute voice audio" : "Mute voice audio";
704
+ const Icon = microphone ? props.muted ? MicOffIcon : MicIcon : props.muted ? VolumeXIcon : Volume2Icon;
705
+ return /* @__PURE__ */ jsx2(
706
+ motion.button,
707
+ {
708
+ type: "button",
709
+ "aria-label": label,
710
+ "aria-pressed": props.muted,
711
+ title: label,
712
+ ...props.reduceMotion ? {} : { whileTap: { scale: 0.92 } },
713
+ onClick: props.onToggle,
714
+ className: cn(
715
+ "inline-flex size-8 shrink-0 items-center justify-center rounded-og-md border outline-none",
716
+ "transition-[background-color,border-color,color] duration-150 ease-og-out",
717
+ "focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:size-11",
718
+ props.muted ? "border-og-accent/35 bg-og-accent-soft text-og-accent" : "border-og-border bg-og-surface-2 text-og-fg-muted hover:border-og-accent/35 hover:text-og-fg"
719
+ ),
720
+ children: /* @__PURE__ */ jsx2(Icon, { className: "size-3.5", "aria-hidden": true })
721
+ }
722
+ );
723
+ }
724
+ function RealtimeModelPickerMenu(props) {
725
+ const providers = REALTIME_MODEL_PROVIDERS.filter(
726
+ (provider) => props.models.some((model) => model.provider === provider)
727
+ );
728
+ const pageKey = props.provider ? `voice-models:${props.provider}` : "voice-providers";
729
+ const body = props.provider ? /* @__PURE__ */ jsxs2("div", { "data-testid": "realtime-model-picker-models", children: [
730
+ /* @__PURE__ */ jsx2(
731
+ PickerBackHeader,
732
+ {
733
+ label: props.provider,
734
+ icon: /* @__PURE__ */ jsx2(
735
+ BillingClassMark,
736
+ {
737
+ billingClass: REALTIME_PROVIDER_META[props.provider].billingClass,
738
+ "aria-label": ""
739
+ }
740
+ ),
741
+ onBack: () => props.onProviderChange(null, -1)
742
+ }
743
+ ),
744
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-col gap-0.5", children: props.models.filter((model) => model.provider === props.provider).map((model) => {
745
+ const selected = model.id === props.selectedModel.id;
746
+ return /* @__PURE__ */ jsx2(
747
+ PickerNavRow,
748
+ {
749
+ label: model.label,
750
+ hint: model.unavailableReason ?? (model.recommended ? `Recommended \xB7 ${model.description}` : model.description),
751
+ disabled: props.disabled || !model.available,
752
+ title: model.unavailableReason ?? model.description,
753
+ active: selected,
754
+ showChevron: false,
755
+ trailing: selected ? /* @__PURE__ */ jsx2(CheckIcon, { className: "size-3.5", "aria-hidden": true }) : void 0,
756
+ testId: `realtime-model-choice-${model.id}`,
757
+ onClick: () => props.onSelect(model.id)
758
+ },
759
+ model.id
760
+ );
761
+ }) })
762
+ ] }) : /* @__PURE__ */ jsx2("div", { className: "flex flex-col gap-0.5", "data-testid": "realtime-model-picker-providers", children: providers.map((provider) => {
763
+ const meta = REALTIME_PROVIDER_META[provider];
764
+ return /* @__PURE__ */ jsx2(
765
+ PickerNavRow,
766
+ {
767
+ label: provider,
768
+ hint: meta.hint,
769
+ icon: /* @__PURE__ */ jsx2(BillingClassMark, { billingClass: meta.billingClass, "aria-label": "" }),
770
+ active: props.selectedModel.provider === provider,
771
+ testId: `realtime-model-provider-${meta.billingClass}`,
772
+ onClick: () => props.onProviderChange(provider, 1)
773
+ },
774
+ provider
775
+ );
776
+ }) });
777
+ return /* @__PURE__ */ jsx2("div", { className: "relative overflow-hidden", "data-testid": "realtime-model-picker-menu", children: /* @__PURE__ */ jsx2(PickerAnimatedPage, { pageKey, direction: props.direction, children: body }) });
778
+ }
779
+ function RealtimeDiagnosticsMenu({ snapshot }) {
780
+ const bridge = snapshot.bridge;
781
+ const rows = [
782
+ ["controller", snapshot.status],
783
+ ["microphone", snapshot.microphone],
784
+ ["microphone muted", String(snapshot.inputMuted)],
785
+ ["audio", snapshot.audibleOutput],
786
+ ["audio muted", String(snapshot.outputMuted)],
787
+ ["generation", String(snapshot.connectionGeneration)],
788
+ ["reconnect attempt", String(snapshot.reconnectAttempt)],
789
+ ["mode", snapshot.mode?.state ?? "none"],
790
+ ["mode version", snapshot.mode ? String(snapshot.mode.version) : "\u2014"],
791
+ ["connection epoch", snapshot.mode ? String(snapshot.mode.connectionEpoch) : "\u2014"],
792
+ ["provider started", bridge ? String(bridge.providerStarted) : "false"],
793
+ ["provider speaking", bridge ? String(bridge.speaking) : "false"],
794
+ ["delegation", bridge?.activeDelegationId ? "active" : "none"],
795
+ ["pending durable events", bridge ? String(bridge.pendingInbound) : "0"],
796
+ ["pending bytes", bridge ? String(bridge.pendingInboundBytes) : "0"],
797
+ ["client ack", bridge?.clientAckThroughSequence?.toString() ?? "\u2014"],
798
+ ["ignored provider events", bridge ? String(bridge.ignoredEventCount) : "0"],
799
+ ["last ignored event", bridge?.lastIgnoredEventType ?? "none"],
800
+ ["bridge error", bridge?.lastError ?? "none"],
801
+ ["diagnostic", snapshot.diagnostic?.kind ?? "none"]
802
+ ];
803
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
804
+ /* @__PURE__ */ jsx2(DropdownMenuSeparator, { className: "mx-1 bg-og-border" }),
805
+ /* @__PURE__ */ jsxs2(DropdownMenuSub, { children: [
806
+ /* @__PURE__ */ jsx2(DropdownMenuSubTrigger, { className: "rounded-og-md text-og-fg-muted", children: "Realtime diagnostics" }),
807
+ /* @__PURE__ */ jsx2(DropdownMenuSubContent, { className: "max-h-80 w-72 overflow-y-auto rounded-og-lg border-og-border bg-og-surface-1 p-1.5 text-og-fg shadow-og-lg", children: rows.map(([label, value]) => /* @__PURE__ */ jsxs2(
808
+ DropdownMenuItem,
809
+ {
810
+ disabled: true,
811
+ className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-3 rounded-og-md font-mono text-[11px] opacity-100",
812
+ children: [
813
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-og-fg-subtle", children: label }),
814
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-right text-og-fg", children: value })
815
+ ]
816
+ },
817
+ label
818
+ )) })
819
+ ] })
820
+ ] });
821
+ }
822
+ function statusContent(snapshot, modelAvailable, canStart, admissionBlocker, modelLabel) {
823
+ if (snapshot.audibleOutput === "blocked" && !snapshot.outputMuted) {
824
+ return {
825
+ phase: "blocked",
826
+ label: "Audio output blocked",
827
+ detail: "Resume audio to hear the existing voice connection."
828
+ };
829
+ }
830
+ switch (snapshot.status) {
831
+ case "starting":
832
+ return {
833
+ phase: "connecting",
834
+ label: "Connecting\u2026",
835
+ detail: "Preparing your microphone and live voice."
836
+ };
837
+ case "active":
838
+ return snapshot.bridge?.speaking ? {
839
+ phase: "speaking",
840
+ label: "Speaking",
841
+ detail: "Interrupt naturally whenever you want."
842
+ } : {
843
+ phase: "listening",
844
+ label: "Listening",
845
+ detail: "Voice and typed messages share this session."
846
+ };
847
+ case "stopping":
848
+ return {
849
+ phase: "stopping",
850
+ label: "Ending voice\u2026",
851
+ detail: "Saving the remaining conversation context."
852
+ };
853
+ case "recovering":
854
+ return {
855
+ phase: "reconnecting",
856
+ label: "Reconnecting\u2026",
857
+ detail: snapshot.error ?? "Keeping this voice conversation connected."
858
+ };
859
+ case "lost_owner":
860
+ return {
861
+ phase: "elsewhere",
862
+ label: "Voice active elsewhere",
863
+ detail: "Return to the browser that started it, or wait for that connection to expire."
864
+ };
865
+ case "error":
866
+ return {
867
+ phase: "error",
868
+ label: "Voice unavailable",
869
+ detail: snapshot.error ?? "The connection could not be started."
870
+ };
871
+ case "idle":
872
+ if (!modelAvailable) {
873
+ return {
874
+ phase: "unavailable",
875
+ label: "Voice model unavailable",
876
+ detail: admissionBlocker ?? "Choose another voice model."
877
+ };
878
+ }
879
+ if (!canStart && admissionBlocker) {
880
+ return { phase: "unavailable", label: "Voice unavailable", detail: admissionBlocker };
881
+ }
882
+ return {
883
+ phase: "idle",
884
+ label: "Start voice",
885
+ detail: `Talk live with ${modelLabel}.`
886
+ };
887
+ }
888
+ }
889
+ function RealtimeComposerGlow(props) {
890
+ const visible = [
891
+ "connecting",
892
+ "listening",
893
+ "speaking",
894
+ "reconnecting",
895
+ "blocked",
896
+ "error",
897
+ "stopping"
898
+ ].includes(props.phase);
899
+ return /* @__PURE__ */ jsx2(AnimatePresence, { initial: false, children: visible ? /* @__PURE__ */ jsx2(
900
+ motion.span,
901
+ {
902
+ "aria-hidden": true,
903
+ initial: { opacity: 0 },
904
+ animate: props.reduceMotion ? { opacity: 0.48 } : props.phase === "speaking" ? { opacity: [0.42, 0.82, 0.52] } : props.phase === "listening" ? { opacity: [0.34, 0.62, 0.42] } : { opacity: [0.28, 0.58, 0.28] },
905
+ exit: { opacity: 0 },
906
+ transition: props.reduceMotion ? { duration: 0.12 } : {
907
+ duration: props.phase === "speaking" ? 1.15 : 2.1,
908
+ ease: "easeInOut",
909
+ repeat: Number.POSITIVE_INFINITY
910
+ },
911
+ className: cn(
912
+ "pointer-events-none absolute -inset-px z-[1] rounded-og-lg ring-1 ring-inset shadow-og-glow",
913
+ props.phase === "speaking" ? "ring-og-status-running/50" : props.phase === "blocked" || props.phase === "error" ? "ring-og-status-waiting/45" : "ring-og-accent/45"
914
+ )
915
+ },
916
+ props.phase
917
+ ) : null });
918
+ }
919
+ function RealtimeActionGlyph(props) {
920
+ if (["connecting", "reconnecting", "stopping"].includes(props.phase)) {
921
+ return /* @__PURE__ */ jsx2(LoaderCircleIcon, { className: "size-3.5 animate-og-spin", "aria-hidden": true });
922
+ }
923
+ if (props.phase === "blocked") {
924
+ return props.audioBlocked ? /* @__PURE__ */ jsx2(Volume2Icon, { className: "size-3.5", "aria-hidden": true }) : /* @__PURE__ */ jsx2(CircleAlertIcon, { className: "size-3.5", "aria-hidden": true });
925
+ }
926
+ if (props.phase === "error" || props.phase === "elsewhere" || props.phase === "unavailable") {
927
+ return /* @__PURE__ */ jsx2(CircleAlertIcon, { className: "size-3.5", "aria-hidden": true });
928
+ }
929
+ if (props.phase === "listening" || props.phase === "speaking") {
930
+ return /* @__PURE__ */ jsxs2("span", { className: "relative inline-flex size-4 items-center justify-center", children: [
931
+ !props.reduceMotion ? /* @__PURE__ */ jsx2(
932
+ motion.span,
933
+ {
934
+ "aria-hidden": true,
935
+ className: "absolute inset-0 rounded-full border border-current",
936
+ animate: { opacity: [0.65, 0], scale: [0.65, 1.45] },
937
+ transition: {
938
+ duration: props.phase === "speaking" ? 0.9 : 1.5,
939
+ ease: "easeOut",
940
+ repeat: Number.POSITIVE_INFINITY
941
+ }
942
+ }
943
+ ) : null,
944
+ /* @__PURE__ */ jsx2(SquareIcon, { className: "relative size-2.5 fill-current", "aria-hidden": true })
945
+ ] });
946
+ }
947
+ return /* @__PURE__ */ jsx2(AudioLinesIcon, { className: "size-4", "aria-hidden": true });
948
+ }
949
+ function RealtimeStatusDot(props) {
950
+ const live = props.phase === "listening" || props.phase === "speaking";
951
+ return /* @__PURE__ */ jsxs2("span", { className: "relative inline-flex size-2 shrink-0", "aria-hidden": true, children: [
952
+ live && !props.reduceMotion ? /* @__PURE__ */ jsx2(
953
+ motion.span,
954
+ {
955
+ className: "absolute inset-0 rounded-full bg-og-status-running",
956
+ animate: { opacity: [0.45, 0], scale: [1, 2.2] },
957
+ transition: { duration: 1.4, ease: "easeOut", repeat: Number.POSITIVE_INFINITY }
958
+ }
959
+ ) : null,
960
+ /* @__PURE__ */ jsx2(
961
+ "span",
962
+ {
963
+ className: cn(
964
+ "relative size-2 rounded-full",
965
+ live ? "bg-og-status-running" : props.phase === "blocked" || props.phase === "error" ? "bg-og-status-waiting" : props.phase === "elsewhere" || props.phase === "unavailable" ? "bg-og-fg-subtle" : "bg-og-accent"
966
+ )
967
+ }
968
+ )
969
+ ] });
970
+ }
971
+ function voiceButtonTone(phase) {
972
+ if (phase === "listening" || phase === "speaking") {
973
+ return "border-og-accent/45 bg-og-accent text-og-accent-fg shadow-og-sm hover:bg-og-accent-strong";
974
+ }
975
+ if (phase === "blocked" || phase === "error") {
976
+ return "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting hover:bg-og-status-waiting/15";
977
+ }
978
+ if (phase === "connecting" || phase === "reconnecting" || phase === "stopping") {
979
+ return "border-og-accent/35 bg-og-accent-soft text-og-accent";
980
+ }
981
+ return "border-og-border bg-og-surface-2 text-og-fg-muted hover:border-og-accent/35 hover:bg-og-accent-soft hover:text-og-accent";
982
+ }
983
+ function toRealtimeModelOption(model) {
984
+ return { ...model };
985
+ }
986
+ function readRealtimeModelPreference(workspaceId) {
987
+ if (typeof localStorage === "undefined") return null;
988
+ try {
989
+ const value = localStorage.getItem(`${REALTIME_MODEL_STORAGE_PREFIX}:${workspaceId}`);
990
+ return isRealtimeModel(value) ? value : null;
991
+ } catch {
992
+ return null;
993
+ }
994
+ }
995
+ function writeRealtimeModelPreference(workspaceId, model) {
996
+ if (typeof localStorage === "undefined") return;
997
+ try {
998
+ localStorage.setItem(`${REALTIME_MODEL_STORAGE_PREFIX}:${workspaceId}`, model);
999
+ } catch {
1000
+ }
1001
+ }
1002
+ function isRealtimeModel(value) {
1003
+ return value === "gpt-live-1-boulder-alpha" || value === "opengeni-gateway/openai/gpt-realtime-2.1" || value === "opengeni-gateway/openai/gpt-realtime-mini" || value === "opengeni-gateway/xai/grok-voice-think-fast-2.0" || value === "workspace-gateway/openai/gpt-realtime-2.1" || value === "workspace-gateway/openai/gpt-realtime-mini" || value === "workspace-gateway/xai/grok-voice-think-fast-2.0";
1004
+ }
1005
+ var useSessionCodexRealtime = useSessionRealtime;
1006
+ var useWorkspaceRealtimeModelSelection = useRealtimeModelSelection;
1007
+ var SessionCodexRealtimeControl = SessionRealtimeControl;
1008
+ var CodexRealtimeControl = RealtimeVoiceControl;
1009
+ export {
1010
+ CodexRealtimeControl,
1011
+ NewSessionRealtimeControl,
1012
+ RealtimeModelPickerMenu,
1013
+ RealtimeVoiceControl,
1014
+ SessionCodexRealtimeControl,
1015
+ SessionRealtimeControl,
1016
+ codexRealtimeAdmissionAllowed,
1017
+ codexRealtimeAdmissionBlocker,
1018
+ useRealtimeModelSelection,
1019
+ useSessionCodexRealtime,
1020
+ useSessionRealtime,
1021
+ useWorkspaceRealtimeModelSelection
1022
+ };
1023
+ //# sourceMappingURL=realtime.js.map