@opengeni/react 0.46.1 → 0.48.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 (141) hide show
  1. package/README.md +68 -12
  2. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  3. package/dist/chunk-FK6VG4LL.js.map +1 -0
  4. package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
  5. package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
  6. package/dist/chunk-I4MPFDRP.js.map +1 -0
  7. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  8. package/dist/chunk-KZ73RL76.js.map +1 -0
  9. package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
  10. package/dist/chunk-LHLYWG5R.js.map +1 -0
  11. package/dist/{chunk-3PB7MIT6.js → chunk-NJ7M2B6Z.js} +207 -67
  12. package/dist/chunk-NJ7M2B6Z.js.map +1 -0
  13. package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
  14. package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
  15. package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
  16. package/dist/chunk-OL6QVXGF.js.map +1 -0
  17. package/dist/chunk-VZTQ73XT.js +86 -0
  18. package/dist/chunk-VZTQ73XT.js.map +1 -0
  19. package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
  20. package/dist/chunk-WYA65Y3F.js.map +1 -0
  21. package/dist/{chunk-LGVMUIRV.js → chunk-XCNTXWBB.js} +264 -115
  22. package/dist/chunk-XCNTXWBB.js.map +1 -0
  23. package/dist/client.d.ts +1 -1
  24. package/dist/components/chat-composer.d.ts +7 -2
  25. package/dist/components/composer-transcription-control.d.ts +2 -0
  26. package/dist/components/composer.d.ts +14 -0
  27. package/dist/components/machines/machine-detail.d.ts +3 -1
  28. package/dist/components/machines-dashboard.d.ts +3 -3
  29. package/dist/components/message-timeline.d.ts +12 -2
  30. package/dist/components/sandbox-files.d.ts +2 -2
  31. package/dist/components/sandbox-terminal.d.ts +29 -0
  32. package/dist/components/sandbox-workspace.d.ts +5 -1
  33. package/dist/components/workspace-dock.d.ts +3 -1
  34. package/dist/composer.d.ts +2 -2
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/internal.d.ts +6 -0
  37. package/dist/hooks/use-codex-accounts.d.ts +1 -1
  38. package/dist/hooks/use-environments.d.ts +10 -4
  39. package/dist/hooks/use-machines.d.ts +7 -0
  40. package/dist/hooks/use-sandbox-files.d.ts +19 -0
  41. package/dist/hooks/use-sandbox-git.d.ts +7 -0
  42. package/dist/hooks/use-transcription.d.ts +2 -2
  43. package/dist/hooks/use-voice-input.d.ts +5 -3
  44. package/dist/index.d.ts +6 -5
  45. package/dist/index.js +904 -538
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/composer-responsive-context.d.ts +9 -0
  48. package/dist/lib/use-portal-token-style.d.ts +14 -3
  49. package/dist/machines.js +2 -2
  50. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
  51. package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
  52. package/dist/realtime/realtime-control.d.ts +9 -0
  53. package/dist/realtime.js +27 -30
  54. package/dist/realtime.js.map +1 -1
  55. package/dist/session-ui.js +5 -5
  56. package/dist/session.js +4 -4
  57. package/dist/timeline/activity-rail.d.ts +3 -2
  58. package/dist/timeline/index.d.ts +2 -2
  59. package/dist/timeline/parsers.d.ts +3 -3
  60. package/dist/timeline/registry.d.ts +3 -0
  61. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  62. package/dist/timeline/types.d.ts +4 -0
  63. package/dist/voice-recording-store.d.ts +8 -2
  64. package/package.json +20 -4
  65. package/src/client.ts +3 -0
  66. package/src/components/chat-composer.tsx +12 -2
  67. package/src/components/code-editor.tsx +1 -1
  68. package/src/components/command-palette.tsx +1 -1
  69. package/src/components/composer-transcription-control.tsx +41 -16
  70. package/src/components/composer.tsx +49 -23
  71. package/src/components/copy-button.tsx +1 -1
  72. package/src/components/desktop-viewer.tsx +2 -2
  73. package/src/components/file-browser.tsx +39 -17
  74. package/src/components/human-input-form.tsx +2 -2
  75. package/src/components/machine-card.tsx +1 -1
  76. package/src/components/machines/machine-detail.tsx +39 -15
  77. package/src/components/machines-dashboard.tsx +7 -6
  78. package/src/components/message-timeline.tsx +39 -6
  79. package/src/components/model-picker.tsx +2 -2
  80. package/src/components/model-policy-picker.tsx +16 -15
  81. package/src/components/queue-surface-implementation.tsx +20 -16
  82. package/src/components/queue-surface-state.tsx +2 -2
  83. package/src/components/sandbox-files.tsx +140 -60
  84. package/src/components/sandbox-terminal.tsx +24 -5
  85. package/src/components/sandbox-workspace.tsx +180 -142
  86. package/src/components/session-chrome.tsx +14 -14
  87. package/src/components/tooltip.tsx +13 -16
  88. package/src/components/user-message-body.tsx +1 -1
  89. package/src/components/workbench-changes.tsx +4 -4
  90. package/src/components/workspace-dock.tsx +13 -5
  91. package/src/composer.ts +3 -0
  92. package/src/hooks/internal.ts +132 -38
  93. package/src/hooks/use-codex-accounts.ts +4 -4
  94. package/src/hooks/use-environments.ts +24 -5
  95. package/src/hooks/use-file-attachments.ts +70 -32
  96. package/src/hooks/use-goal.ts +17 -5
  97. package/src/hooks/use-machines.ts +47 -1
  98. package/src/hooks/use-sandbox-files.ts +310 -59
  99. package/src/hooks/use-sandbox-git.ts +148 -41
  100. package/src/hooks/use-session-capabilities.ts +35 -24
  101. package/src/hooks/use-session-events.ts +18 -4
  102. package/src/hooks/use-transcription.ts +4 -14
  103. package/src/hooks/use-turn-queue.ts +17 -5
  104. package/src/hooks/use-voice-input.ts +163 -19
  105. package/src/hooks/use-windowed-sections.ts +9 -7
  106. package/src/index.ts +11 -6
  107. package/src/lib/composer-responsive-context.ts +15 -0
  108. package/src/lib/use-portal-token-style.ts +60 -15
  109. package/src/provider.tsx +52 -7
  110. package/src/realtime/dropdown-menu.tsx +15 -19
  111. package/src/realtime/realtime-control.tsx +16 -5
  112. package/src/timeline/activity-rail.tsx +14 -5
  113. package/src/timeline/index.ts +1 -1
  114. package/src/timeline/parsers.ts +33 -20
  115. package/src/timeline/projection.ts +23 -0
  116. package/src/timeline/registry.ts +7 -0
  117. package/src/timeline/screenshot-lightbox.tsx +15 -4
  118. package/src/timeline/shared.tsx +5 -5
  119. package/src/timeline/tool-renderers.tsx +166 -22
  120. package/src/timeline/turn-summary.tsx +2 -2
  121. package/src/timeline/types.ts +4 -0
  122. package/src/voice-recording-store.ts +15 -0
  123. package/styles/compiled.css +5065 -0
  124. package/styles/compiled.d.ts +2 -0
  125. package/styles/effective-tokens.css +27 -0
  126. package/styles/index.css +3 -0
  127. package/styles/responsive.css +196 -0
  128. package/styles/responsive.d.ts +2 -0
  129. package/styles/tokens.css +50 -17
  130. package/dist/chunk-3PB7MIT6.js.map +0 -1
  131. package/dist/chunk-6EDV6YFE.js.map +0 -1
  132. package/dist/chunk-EB67J347.js.map +0 -1
  133. package/dist/chunk-FGZUCXZF.js.map +0 -1
  134. package/dist/chunk-JB5725BI.js +0 -48
  135. package/dist/chunk-JB5725BI.js.map +0 -1
  136. package/dist/chunk-LGVMUIRV.js.map +0 -1
  137. package/dist/chunk-LWR4MXSS.js.map +0 -1
  138. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  139. package/dist/chunk-U3CSCMWA.js.map +0 -1
  140. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
  141. /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ClientVoiceInputConfig, OpenGeniClient } from "@opengeni/sdk";
1
+ import { OpenGeniApiError, type ClientVoiceInputConfig, type OpenGeniClient } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import {
4
4
  IndexedDbVoiceRecordingStore,
@@ -21,6 +21,7 @@ export type VoiceInputStatus =
21
21
  | "recording"
22
22
  | "saving"
23
23
  | "transcribing"
24
+ | "retrying"
24
25
  | "recovered"
25
26
  | "transcript-ready"
26
27
  | "error";
@@ -39,6 +40,8 @@ export type UseVoiceInputOptions = {
39
40
  createRecordingId?: (() => string) | undefined;
40
41
  createOwnerId?: (() => string) | undefined;
41
42
  now?: (() => Date) | undefined;
43
+ /** Test/embed seam. Production uses bounded exponential recovery backoff. */
44
+ automaticRetryDelayMilliseconds?: number | undefined;
42
45
  };
43
46
 
44
47
  type ResumableVoiceInputClient = Pick<
@@ -86,7 +89,6 @@ export const VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS = 8 * 60 * 60
86
89
  export const VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS = 2_000;
87
90
  export const VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS = 30_000;
88
91
  const TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS = 5_000;
89
-
90
92
  const MIME_PREFERENCES = ["audio/webm;codecs=opus", "audio/mp4", "audio/ogg;codecs=opus"];
91
93
  const createDefaultVoiceRecordingId = () => crypto.randomUUID();
92
94
  const currentDate = () => new Date();
@@ -132,6 +134,7 @@ export function useVoiceInput({
132
134
  createRecordingId = createDefaultVoiceRecordingId,
133
135
  createOwnerId,
134
136
  now = currentDate,
137
+ automaticRetryDelayMilliseconds,
135
138
  }: UseVoiceInputOptions): UseVoiceInputResult {
136
139
  const [status, setStatus] = useState<VoiceInputStatus>("idle");
137
140
  const [error, setError] = useState<string | null>(null);
@@ -156,6 +159,8 @@ export function useVoiceInput({
156
159
  const recorderRef = useRef<MediaRecorder | null>(null);
157
160
  const streamRef = useRef<MediaStream | null>(null);
158
161
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
162
+ const automaticRetryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
163
+ const automaticRetryAttemptRef = useRef(0);
159
164
  const ownerHeartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
160
165
  const controllerRef = useRef<AbortController | null>(null);
161
166
  const valueRef = useRef(value);
@@ -211,6 +216,7 @@ export function useVoiceInput({
211
216
  setRecordingId(null);
212
217
  setDurationSeconds(0);
213
218
  setLocallySaved(false);
219
+ automaticRetryAttemptRef.current = 0;
214
220
  }, [stopOwnerHeartbeat]);
215
221
 
216
222
  const ensureStore = useCallback(async (): Promise<VoiceRecordingStore> => {
@@ -348,7 +354,9 @@ export function useVoiceInput({
348
354
  setStatus(
349
355
  claimed.finalizationState === "transcript-ready" && claimed.transcriptText !== null
350
356
  ? "transcript-ready"
351
- : "recovered",
357
+ : claimed.recoveryMode === "automatic"
358
+ ? "retrying"
359
+ : "recovered",
352
360
  );
353
361
  setError(null);
354
362
  return;
@@ -415,7 +423,12 @@ export function useVoiceInput({
415
423
  if (chunks.length === 0) {
416
424
  const retained = await store.updateManifest(
417
425
  transcribing.recordingId,
418
- { uploadState: "retrying", transcriptionState: "retrying" },
426
+ {
427
+ uploadState: "retrying",
428
+ transcriptionState: "retrying",
429
+ recoveryMode: "manual",
430
+ handoffMode: "explicit",
431
+ },
419
432
  readNow().toISOString(),
420
433
  ownerIdRef.current ?? undefined,
421
434
  );
@@ -447,6 +460,14 @@ export function useVoiceInput({
447
460
  if (!active()) return;
448
461
  rememberManifest(ready);
449
462
 
463
+ automaticRetryAttemptRef.current = 0;
464
+ if (ready.handoffMode === "explicit") {
465
+ setStatus("transcript-ready");
466
+ setError("handoff_uncertain");
467
+ focusInput();
468
+ return;
469
+ }
470
+
450
471
  const next = appendFinalTranscript(valueRef.current, response.text);
451
472
  if (next !== valueRef.current) {
452
473
  valueRef.current = next;
@@ -490,16 +511,22 @@ export function useVoiceInput({
490
511
  await loadNextRecoverable(generation);
491
512
  } catch (reason) {
492
513
  if (!active()) return;
514
+ const code = controller.signal.aborted ? null : errorCode(reason);
515
+ const retryable = !controller.signal.aborted && isRetryableVoiceInputError(reason);
493
516
  const retained = await updateManifestBestEffort(transcribing, {
494
517
  uploadState: "retrying",
495
518
  transcriptionState: "retrying",
519
+ recoveryMode: retryable ? "automatic" : "manual",
520
+ handoffMode: "explicit",
496
521
  });
497
522
  if (!active()) return;
498
- await preserveForRetry(
499
- retained,
500
- controller.signal.aborted ? null : errorCode(reason),
501
- generation,
502
- );
523
+ if (retryable) {
524
+ rememberManifest(retained);
525
+ setStatus("retrying");
526
+ setError(code);
527
+ } else {
528
+ await preserveForRetry(retained, code, generation);
529
+ }
503
530
  } finally {
504
531
  if (controllerRef.current === controller) controllerRef.current = null;
505
532
  }
@@ -519,6 +546,61 @@ export function useVoiceInput({
519
546
  workspaceId,
520
547
  ],
521
548
  );
549
+ // Live session renders must not postpone a durable retry deadline. Execute the
550
+ // latest finalizer without making its callback identity part of the timer key.
551
+ const finalizePersistedRecordingRef = useRef(finalizePersistedRecording);
552
+ finalizePersistedRecordingRef.current = finalizePersistedRecording;
553
+ const clientAvailable = client !== null && client !== undefined;
554
+
555
+ useEffect(() => {
556
+ if (
557
+ status !== "retrying" ||
558
+ !manifestRef.current ||
559
+ manifestRef.current.recoveryMode !== "automatic" ||
560
+ manifestRef.current.workspaceId !== workspaceId ||
561
+ !clientAvailable ||
562
+ disabled ||
563
+ !enabled
564
+ ) {
565
+ return;
566
+ }
567
+ const generation = generationRef.current;
568
+ let started = false;
569
+ const run = () => {
570
+ if (
571
+ started ||
572
+ generation !== generationRef.current ||
573
+ statusRef.current !== "retrying" ||
574
+ workspaceIdRef.current !== workspaceId
575
+ ) {
576
+ return;
577
+ }
578
+ started = true;
579
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
580
+ automaticRetryTimerRef.current = null;
581
+ automaticRetryAttemptRef.current += 1;
582
+ void finalizePersistedRecordingRef.current(generation);
583
+ };
584
+ const onOnline = () => run();
585
+ globalThis.addEventListener?.("online", onOnline);
586
+ if (typeof navigator !== "undefined" && navigator.onLine === false) {
587
+ return () => globalThis.removeEventListener?.("online", onOnline);
588
+ }
589
+ const delay = Math.max(
590
+ 0,
591
+ automaticRetryDelayMilliseconds ??
592
+ transcriptionRecoveryMutationDelayMilliseconds(
593
+ TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS,
594
+ automaticRetryAttemptRef.current,
595
+ ),
596
+ );
597
+ automaticRetryTimerRef.current = setTimeout(run, delay);
598
+ return () => {
599
+ globalThis.removeEventListener?.("online", onOnline);
600
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
601
+ automaticRetryTimerRef.current = null;
602
+ };
603
+ }, [automaticRetryDelayMilliseconds, clientAvailable, disabled, enabled, status, workspaceId]);
522
604
 
523
605
  const stop = useCallback(() => {
524
606
  const recorder = recorderRef.current;
@@ -540,6 +622,7 @@ export function useVoiceInput({
540
622
  status === "recording" ||
541
623
  status === "saving" ||
542
624
  status === "transcribing" ||
625
+ status === "retrying" ||
543
626
  !navigator.mediaDevices?.getUserMedia ||
544
627
  typeof MediaRecorder === "undefined"
545
628
  ) {
@@ -716,11 +799,19 @@ export function useVoiceInput({
716
799
  }
717
800
  rememberManifest(stopped);
718
801
  if (attemptPersistenceError) {
719
- await preserveForRetry(stopped, "storage_unavailable", generation);
802
+ const retained = await updateManifestBestEffort(stopped, {
803
+ recoveryMode: "manual",
804
+ handoffMode: "explicit",
805
+ });
806
+ await preserveForRetry(retained, "storage_unavailable", generation);
720
807
  return;
721
808
  }
722
809
  if (attemptCaptureLimitError) {
723
- await preserveForRetry(stopped, attemptCaptureLimitError, generation);
810
+ const retained = await updateManifestBestEffort(stopped, {
811
+ recoveryMode: "manual",
812
+ handoffMode: "explicit",
813
+ });
814
+ await preserveForRetry(retained, attemptCaptureLimitError, generation);
724
815
  return;
725
816
  }
726
817
  await finalizePersistedRecording(generation);
@@ -826,9 +917,29 @@ export function useVoiceInput({
826
917
  }
827
918
  const generation = ++generationRef.current;
828
919
  controllerRef.current?.abort();
920
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
921
+ automaticRetryTimerRef.current = null;
922
+ automaticRetryAttemptRef.current = 0;
829
923
  setError(null);
830
- void finalizePersistedRecording(generation);
831
- }, [client, disabled, enabled, finalizePersistedRecording, status, workspaceId]);
924
+ const manifest = manifestRef.current;
925
+ void updateManifestBestEffort(manifest, {
926
+ recoveryMode: "automatic",
927
+ handoffMode: "append",
928
+ }).then((retained) => {
929
+ if (generation !== generationRef.current) return;
930
+ rememberManifest(retained);
931
+ void finalizePersistedRecording(generation);
932
+ });
933
+ }, [
934
+ client,
935
+ disabled,
936
+ enabled,
937
+ finalizePersistedRecording,
938
+ rememberManifest,
939
+ status,
940
+ updateManifestBestEffort,
941
+ workspaceId,
942
+ ]);
832
943
 
833
944
  const insertSavedTranscript = useCallback(async (): Promise<void> => {
834
945
  const manifest = manifestRef.current;
@@ -951,10 +1062,12 @@ export function useVoiceInput({
951
1062
  void discard();
952
1063
  return;
953
1064
  }
954
- if (status === "saving" || status === "transcribing") {
1065
+ if (status === "saving" || status === "transcribing" || status === "retrying") {
955
1066
  const generation = ++generationRef.current;
956
1067
  controllerRef.current?.abort();
957
1068
  controllerRef.current = null;
1069
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
1070
+ automaticRetryTimerRef.current = null;
958
1071
  const captureSettled = status === "saving" ? captureSettledRef.current : Promise.resolve();
959
1072
  void captureSettled.then(async () => {
960
1073
  const manifest = manifestRef.current;
@@ -963,6 +1076,8 @@ export function useVoiceInput({
963
1076
  captureState: "stopped",
964
1077
  uploadState: "retrying",
965
1078
  transcriptionState: "retrying",
1079
+ recoveryMode: "manual",
1080
+ handoffMode: "explicit",
966
1081
  });
967
1082
  await preserveForRetry(retained, null, generation);
968
1083
  });
@@ -996,7 +1111,12 @@ export function useVoiceInput({
996
1111
  {
997
1112
  ...(current.captureState === "capturing" ? { captureState: "stopped" as const } : {}),
998
1113
  ...(wasProcessing
999
- ? { uploadState: "retrying" as const, transcriptionState: "retrying" as const }
1114
+ ? {
1115
+ uploadState: "retrying" as const,
1116
+ transcriptionState: "retrying" as const,
1117
+ recoveryMode: "automatic" as const,
1118
+ handoffMode: "explicit" as const,
1119
+ }
1000
1120
  : {}),
1001
1121
  ownerId: null,
1002
1122
  ownerHeartbeatAt: null,
@@ -1028,7 +1148,8 @@ export function useVoiceInput({
1028
1148
  (status === "requesting-permission" ||
1029
1149
  status === "recording" ||
1030
1150
  status === "saving" ||
1031
- status === "transcribing")
1151
+ status === "transcribing" ||
1152
+ status === "retrying")
1032
1153
  ) {
1033
1154
  event.preventDefault();
1034
1155
  cancel();
@@ -1043,6 +1164,8 @@ export function useVoiceInput({
1043
1164
  generationRef.current += 1;
1044
1165
  controllerRef.current?.abort();
1045
1166
  controllerRef.current = null;
1167
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
1168
+ automaticRetryTimerRef.current = null;
1046
1169
  const manifest = manifestRef.current;
1047
1170
  const ownerReady = ownerIdPromiseRef.current;
1048
1171
  stopOwnerHeartbeat();
@@ -1061,7 +1184,9 @@ export function useVoiceInput({
1061
1184
  const store = storeRef.current;
1062
1185
  if (store && manifest && ownerId) {
1063
1186
  const wasProcessing =
1064
- statusRef.current === "saving" || statusRef.current === "transcribing";
1187
+ statusRef.current === "saving" ||
1188
+ statusRef.current === "transcribing" ||
1189
+ statusRef.current === "retrying";
1065
1190
  await store
1066
1191
  .updateManifest(
1067
1192
  manifest.recordingId,
@@ -1073,6 +1198,8 @@ export function useVoiceInput({
1073
1198
  ? {
1074
1199
  uploadState: "retrying" as const,
1075
1200
  transcriptionState: "retrying" as const,
1201
+ recoveryMode: "automatic" as const,
1202
+ handoffMode: "explicit" as const,
1076
1203
  }
1077
1204
  : {}),
1078
1205
  ownerId: null,
@@ -1232,7 +1359,7 @@ async function transcribePersistedRecording(input: {
1232
1359
  break;
1233
1360
  case "failed": {
1234
1361
  if (!remote.recording.retryable) {
1235
- throw { code: remote.recording.errorCode ?? "unknown" };
1362
+ throw { code: remote.recording.errorCode ?? "unknown", retryable: false };
1236
1363
  }
1237
1364
  const retried =
1238
1365
  remote.recording.segmentCount === 0
@@ -1247,7 +1374,10 @@ async function transcribePersistedRecording(input: {
1247
1374
  { signal: input.signal },
1248
1375
  );
1249
1376
  if (retried.recording.state === "failed") {
1250
- throw { code: retried.recording.errorCode ?? "unknown" };
1377
+ throw {
1378
+ code: retried.recording.errorCode ?? "unknown",
1379
+ retryable: retried.recording.retryable,
1380
+ };
1251
1381
  }
1252
1382
  remote = retried;
1253
1383
  scheduleRecoveryMutation(remote);
@@ -1350,6 +1480,9 @@ function chooseMimeType(accepted: string[]): string | undefined {
1350
1480
  }
1351
1481
 
1352
1482
  function errorCode(error: unknown): string {
1483
+ if (error instanceof OpenGeniApiError && typeof error.details?.transcriptionCode === "string") {
1484
+ return error.details.transcriptionCode;
1485
+ }
1353
1486
  if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1354
1487
  return error.code;
1355
1488
  }
@@ -1357,3 +1490,14 @@ function errorCode(error: unknown): string {
1357
1490
  if (error instanceof DOMException && error.name === "NotSupportedError") return "not_supported";
1358
1491
  return "unknown";
1359
1492
  }
1493
+
1494
+ function isRetryableVoiceInputError(error: unknown): boolean {
1495
+ if (error instanceof OpenGeniApiError) {
1496
+ return error.retryable && error.status !== 409;
1497
+ }
1498
+ if (error instanceof TypeError) return true;
1499
+ if (error && typeof error === "object" && "retryable" in error) {
1500
+ return error.retryable === true;
1501
+ }
1502
+ return false;
1503
+ }
@@ -108,16 +108,18 @@ export function useWindowedSections(opts: {
108
108
  Array.from({ length: count }, (_, i) => estimateHeight(i)),
109
109
  );
110
110
 
111
- // Re-seed heights when the section count changes (a new diff arrives), keeping
112
- // any already-measured heights for surviving indices.
111
+ // Re-seed when the estimate function changes as well as when the count changes.
112
+ // A replacement diff can contain the same number of files but radically
113
+ // different section sizes; retaining measurements by index would then place
114
+ // every unvisited section at an offset belonging to the previous diff.
113
115
  useEffect(() => {
114
116
  setHeights((prev) => {
115
- if (prev.length === count) return prev;
116
- return Array.from({ length: count }, (_, i) => prev[i] ?? estimateHeight(i));
117
+ const next = Array.from({ length: count }, (_, i) => estimateHeight(i));
118
+ return prev.length === next.length && prev.every((height, index) => height === next[index])
119
+ ? prev
120
+ : next;
117
121
  });
118
- // estimateHeight is expected stable; count is the real trigger.
119
- // eslint-disable-next-line react-hooks/exhaustive-deps
120
- }, [count]);
122
+ }, [count, estimateHeight]);
121
123
 
122
124
  const heightsRef = useRef(heights);
123
125
  heightsRef.current = heights;
package/src/index.ts CHANGED
@@ -3,9 +3,10 @@
3
3
 
4
4
  // @opengeni/react — hooks + styled components on @opengeni/sdk.
5
5
  //
6
- // Import the styles once in your Tailwind entry CSS:
7
- // @import "@opengeni/react/styles.css";
8
- // @source "../node_modules/@opengeni/react/src";
6
+ // Import the ready-to-use styles once:
7
+ // import "@opengeni/react/compiled.css";
8
+ // Tailwind v4 hosts may instead use the additive styles.css + @source bridge;
9
+ // container-responsive composers additionally import responsive.css.
9
10
 
10
11
  export type {
11
12
  EmbeddedFileAttachmentClientLike,
@@ -22,6 +23,7 @@ export { OpenGeniProvider } from "./provider";
22
23
  export type { OpenGeniProviderProps } from "./provider";
23
24
  export { useOpenGeni, useOpenGeniClient } from "./session-context";
24
25
  export type { ClientOverride, OpenGeniContextValue } from "./session-context";
26
+ export { usePageLiveActivity } from "./hooks/internal";
25
27
 
26
28
  // Hooks
27
29
  export { useSession, isTitleEvent } from "./hooks/use-session";
@@ -79,7 +81,9 @@ export type {
79
81
  VoiceRecordingChunk,
80
82
  VoiceRecordingChunkUploadState,
81
83
  VoiceRecordingFinalizationState,
84
+ VoiceRecordingHandoffMode,
82
85
  VoiceRecordingManifest,
86
+ VoiceRecordingRecoveryMode,
83
87
  VoiceRecordingStore,
84
88
  VoiceRecordingTranscriptionState,
85
89
  VoiceRecordingUploadState,
@@ -229,12 +233,14 @@ export type {
229
233
  CapturedFileUnavailableReason,
230
234
  FileTreeNode,
231
235
  FileTreeStatus,
236
+ SandboxFilesGitSummary,
232
237
  SandboxWriteFileOptions,
233
238
  UseSandboxFilesOptions,
234
239
  UseSandboxFilesResult,
235
240
  } from "./hooks/use-sandbox-files";
236
241
  export { useSandboxGit } from "./hooks/use-sandbox-git";
237
242
  export type {
243
+ SandboxGitComparison,
238
244
  SandboxGitFileDiff,
239
245
  UseSandboxGitOptions,
240
246
  UseSandboxGitResult,
@@ -364,7 +370,7 @@ export type {
364
370
  TurnSummaryProps,
365
371
  } from "./timeline";
366
372
 
367
- // Pure provider-shape parsers (exec banner, V4A diff, secret redaction, …)
373
+ // Pure provider-shape parsers (exec banner, V4A diff, tool arguments, …)
368
374
  export {
369
375
  applyPatchOps,
370
376
  applyPatchOpsFromToolItem,
@@ -376,7 +382,6 @@ export {
376
382
  parseExecBannerSessionId,
377
383
  parseFreeformApplyPatch,
378
384
  parseToolArgs,
379
- redactSecrets,
380
385
  sandboxCommandExitCode,
381
386
  stripExecBanner,
382
387
  tailPeek,
@@ -423,7 +428,7 @@ export type {
423
428
  ComposerTranscriptionMessages,
424
429
  } from "./components/composer-transcription-control";
425
430
  export { defaultChatComposerMessages } from "./components/composer";
426
- export type { ChatComposerMessages } from "./components/composer";
431
+ export type { ChatComposerMessages, ResponsiveBasis } from "./components/composer";
427
432
  export { ModelPicker } from "./components/model-picker";
428
433
  export type { ModelPickerProps } from "./components/model-picker";
429
434
  export {
@@ -0,0 +1,15 @@
1
+ import { createContext, useContext, type RefObject } from "react";
2
+
3
+ /** The measurement surface used by responsive composer chrome. */
4
+ export type ResponsiveBasis = "viewport" | "container";
5
+
6
+ export type ComposerResponsiveContextValue = {
7
+ responsiveBasis: ResponsiveBasis;
8
+ rootRef: RefObject<HTMLDivElement | null>;
9
+ };
10
+
11
+ export const ComposerResponsiveContext = createContext<ComposerResponsiveContextValue | null>(null);
12
+
13
+ export function useComposerResponsiveContext(): ComposerResponsiveContextValue | null {
14
+ return useContext(ComposerResponsiveContext);
15
+ }
@@ -1,6 +1,37 @@
1
- import { type CSSProperties, type RefObject, useEffect, useState } from "react";
1
+ import {
2
+ type CSSProperties,
3
+ type RefCallback,
4
+ type RefObject,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { useComposerResponsiveContext } from "./composer-responsive-context";
2
11
 
3
- export type PortalTokenStyle = CSSProperties & Record<`--${string}`, string>;
12
+ export type PortalTokenStyle = Pick<CSSProperties, "colorScheme"> &
13
+ Partial<Record<`--${string}`, string>>;
14
+
15
+ export const PORTAL_SOURCE_INLINE_SIZE = "--og-portal-source-inline-size";
16
+
17
+ /**
18
+ * A callback ref that publishes the currently mounted source element through
19
+ * React state. Unlike a bare mutable ref, late mounts and replacements cause
20
+ * consumers to render and restart their observers without polling.
21
+ */
22
+ export function usePortalTokenSource<T extends HTMLElement>(): {
23
+ source: T | null;
24
+ currentRef: RefObject<T | null>;
25
+ ref: RefCallback<T>;
26
+ } {
27
+ const currentRef = useRef<T | null>(null);
28
+ const [source, setSource] = useState<T | null>(null);
29
+ const ref = useCallback<RefCallback<T>>((node) => {
30
+ currentRef.current = node;
31
+ setSource((current) => (current === node ? current : node));
32
+ }, []);
33
+ return { source, currentRef, ref };
34
+ }
4
35
 
5
36
  /**
6
37
  * Copy the effective SDK tokens across a portal boundary. A locally themed or
@@ -8,11 +39,13 @@ export type PortalTokenStyle = CSSProperties & Record<`--${string}`, string>;
8
39
  * `<body>`. Ancestor theme/class/style mutations and OS color-scheme changes
9
40
  * keep the copied values live.
10
41
  */
11
- export function usePortalTokenStyle(sourceRef: RefObject<HTMLElement | null>): PortalTokenStyle {
42
+ export function usePortalTokenStyle(source: HTMLElement | null): PortalTokenStyle {
43
+ const responsiveContext = useComposerResponsiveContext();
12
44
  const [style, setStyle] = useState<PortalTokenStyle>({});
13
45
  useEffect(() => {
14
- const source = sourceRef.current;
15
- if (!source || typeof MutationObserver === "undefined") return;
46
+ if (!source || typeof window === "undefined") return;
47
+ const layoutSource =
48
+ responsiveContext?.responsiveBasis === "container" ? responsiveContext.rootRef.current : null;
16
49
  let signature = "";
17
50
  const sync = () => {
18
51
  const computed = getComputedStyle(source);
@@ -22,6 +55,12 @@ export function usePortalTokenStyle(sourceRef: RefObject<HTMLElement | null>): P
22
55
  if (!property.startsWith("--og-")) continue;
23
56
  next[property as `--${string}`] = computed.getPropertyValue(property);
24
57
  }
58
+ if (layoutSource) {
59
+ const inlineSize = layoutSource.clientWidth || layoutSource.getBoundingClientRect().width;
60
+ if (inlineSize > 0) {
61
+ next[PORTAL_SOURCE_INLINE_SIZE] = `${Math.round(inlineSize * 1000) / 1000}px`;
62
+ }
63
+ }
25
64
  const nextSignature = JSON.stringify(next);
26
65
  if (nextSignature !== signature) {
27
66
  signature = nextSignature;
@@ -29,23 +68,29 @@ export function usePortalTokenStyle(sourceRef: RefObject<HTMLElement | null>): P
29
68
  }
30
69
  };
31
70
  const observers: MutationObserver[] = [];
32
- let ancestor: HTMLElement | null = source;
33
- while (ancestor) {
34
- const observer = new MutationObserver(sync);
35
- observer.observe(ancestor, {
36
- attributes: true,
37
- attributeFilter: ["class", "data-og-density", "data-og-theme", "style"],
38
- });
39
- observers.push(observer);
40
- ancestor = ancestor.parentElement;
71
+ if (typeof MutationObserver !== "undefined") {
72
+ let ancestor: HTMLElement | null = source;
73
+ while (ancestor) {
74
+ const observer = new MutationObserver(sync);
75
+ observer.observe(ancestor, {
76
+ attributes: true,
77
+ attributeFilter: ["class", "data-og-density", "data-og-theme", "style"],
78
+ });
79
+ observers.push(observer);
80
+ ancestor = ancestor.parentElement;
81
+ }
41
82
  }
83
+ const resizeObserver =
84
+ layoutSource && typeof ResizeObserver !== "undefined" ? new ResizeObserver(sync) : null;
85
+ if (layoutSource) resizeObserver?.observe(layoutSource);
42
86
  const colorScheme = window.matchMedia?.("(prefers-color-scheme: dark)");
43
87
  colorScheme?.addEventListener?.("change", sync);
44
88
  sync();
45
89
  return () => {
46
90
  for (const observer of observers) observer.disconnect();
91
+ resizeObserver?.disconnect();
47
92
  colorScheme?.removeEventListener?.("change", sync);
48
93
  };
49
- }, [sourceRef]);
94
+ }, [responsiveContext?.responsiveBasis, responsiveContext?.rootRef, source]);
50
95
  return style;
51
96
  }