@opengeni/react 0.45.0 → 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-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
  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-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
  20. package/dist/chunk-WYA65Y3F.js.map +1 -0
  21. package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
  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 +13 -4
  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 +29 -32
  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 +52 -26
  71. package/src/components/copy-button.tsx +2 -2
  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 +44 -11
  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 +481 -36
  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 +18 -7
  112. package/src/timeline/activity-rail.tsx +15 -6
  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 +8 -8
  119. package/src/timeline/tool-renderers.tsx +166 -22
  120. package/src/timeline/turn-summary.tsx +6 -6
  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-6EDV6YFE.js.map +0 -1
  131. package/dist/chunk-EB67J347.js.map +0 -1
  132. package/dist/chunk-EHSYZ4KP.js.map +0 -1
  133. package/dist/chunk-FD72GJMK.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-L3EWO3UK.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,7 +1,7 @@
1
1
  import {
2
2
  shouldSteerOnKey,
3
3
  shouldSubmitOnKey
4
- } from "./chunk-HFO4ERGQ.js";
4
+ } from "./chunk-FWXGSNXT.js";
5
5
  import {
6
6
  groupPickerRowsByBillingClass
7
7
  } from "./chunk-23EJ676W.js";
@@ -10,12 +10,15 @@ import {
10
10
  TooltipContent,
11
11
  TooltipProvider,
12
12
  TooltipTrigger
13
- } from "./chunk-U3CSCMWA.js";
13
+ } from "./chunk-FK6VG4LL.js";
14
14
  import {
15
15
  composerSubmissionErrorMessage,
16
16
  formatBytes,
17
17
  formatRelativeTime
18
18
  } from "./chunk-LAKLF4PA.js";
19
+ import {
20
+ ComposerResponsiveContext
21
+ } from "./chunk-VZTQ73XT.js";
19
22
  import {
20
23
  cn
21
24
  } from "./chunk-22KP6LR5.js";
@@ -69,6 +72,8 @@ function createVoiceRecordingManifest(input) {
69
72
  uploadState: "pending",
70
73
  transcriptionState: "pending",
71
74
  finalizationState: "pending",
75
+ recoveryMode: "automatic",
76
+ handoffMode: "append",
72
77
  ownerId: input.ownerId ?? null,
73
78
  ownerHeartbeatAt: input.ownerId ? input.createdAt : null,
74
79
  transcriptText: null,
@@ -184,6 +189,7 @@ var IndexedDbVoiceRecordingStore = class {
184
189
  const updated = {
185
190
  ...manifest,
186
191
  captureState: manifest.captureState === "capturing" ? "stopped" : manifest.captureState,
192
+ handoffMode: "explicit",
187
193
  ownerId,
188
194
  ownerHeartbeatAt: claimedAt,
189
195
  updatedAt: claimedAt
@@ -300,7 +306,9 @@ function normalizeManifest(manifest) {
300
306
  ...manifest,
301
307
  ownerId: manifest.ownerId ?? null,
302
308
  ownerHeartbeatAt: manifest.ownerHeartbeatAt ?? null,
303
- transcriptText: manifest.transcriptText ?? null
309
+ transcriptText: manifest.transcriptText ?? null,
310
+ recoveryMode: manifest.recoveryMode ?? "manual",
311
+ handoffMode: manifest.handoffMode ?? "explicit"
304
312
  };
305
313
  }
306
314
  function manifestAvailableToOwner(manifest, ownership) {
@@ -546,7 +554,7 @@ function useTranscription({
546
554
  (diagnostic) => {
547
555
  if (!onDiagnostic) return;
548
556
  try {
549
- onDiagnostic(sanitizeTranscriptionDiagnostic(diagnostic));
557
+ onDiagnostic(normalizeTranscriptionDiagnostic(diagnostic));
550
558
  } catch {
551
559
  }
552
560
  },
@@ -840,18 +848,14 @@ function normalizeTranscriptionErrorCode(code) {
840
848
  return "unknown";
841
849
  }
842
850
  }
843
- function sanitizeTranscriptionDiagnostic(diagnostic) {
851
+ function normalizeTranscriptionDiagnostic(diagnostic) {
844
852
  const source = isRecord(diagnostic) ? diagnostic : {};
845
853
  const operation = source.operation === "start" || source.operation === "session" || source.operation === "cancel" || source.operation === "close" ? source.operation : "session";
846
854
  const rawDetail = typeof source.detail === "string" ? source.detail : "No diagnostic detail.";
847
- const detail = rawDetail.replace(/[\u0000-\u001f\u007f]+/gu, " ").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, "Bearer [REDACTED]").replace(/\bsk-[A-Za-z0-9_-]{4,}\b/gu, "[REDACTED]").replace(
848
- /\b(api[-_ ]?key|authorization|access[-_ ]?token|refresh[-_ ]?token|token|secret)\b\s*[:=]\s*[^\s,;]+/giu,
849
- "$1=[REDACTED]"
850
- ).trim().slice(0, 512);
851
855
  return {
852
856
  operation,
853
857
  code: normalizeTranscriptionErrorCode(source.code),
854
- detail: detail || "No diagnostic detail."
858
+ detail: rawDetail
855
859
  };
856
860
  }
857
861
  function detachSessionCleanup(session, options) {
@@ -929,6 +933,7 @@ function transcriptionPolicyRevision(policy, selection) {
929
933
  }
930
934
 
931
935
  // src/hooks/use-voice-input.ts
936
+ import { OpenGeniApiError } from "@opengeni/sdk";
932
937
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
933
938
 
934
939
  // src/voice-recording-owner.ts
@@ -1113,9 +1118,31 @@ var VOICE_RECORDING_TIMESLICE_MILLISECONDS = 5e3;
1113
1118
  var VOICE_RECORDING_OWNER_HEARTBEAT_MILLISECONDS = 5e3;
1114
1119
  var VOICE_RECORDING_OWNER_STALE_MILLISECONDS = 3e4;
1115
1120
  var VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS = 600;
1121
+ var VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS = 8 * 60 * 60;
1122
+ var VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS = 2e3;
1123
+ var VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS = 3e4;
1124
+ var TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS = 5e3;
1116
1125
  var MIME_PREFERENCES = ["audio/webm;codecs=opus", "audio/mp4", "audio/ogg;codecs=opus"];
1117
1126
  var createDefaultVoiceRecordingId = () => crypto.randomUUID();
1118
1127
  var currentDate = () => /* @__PURE__ */ new Date();
1128
+ function transcriptionRecoveryMutationDelayMilliseconds(retryAfterMilliseconds, attempt, random = Math.random) {
1129
+ const hint = typeof retryAfterMilliseconds === "number" && Number.isInteger(retryAfterMilliseconds) && retryAfterMilliseconds > 0 ? retryAfterMilliseconds : TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS;
1130
+ const boundedHint = Math.max(
1131
+ 500,
1132
+ Math.min(hint, VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS)
1133
+ );
1134
+ const exponent = Math.max(0, Math.min(Math.floor(attempt), 6));
1135
+ const exponential = Math.min(
1136
+ VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS,
1137
+ boundedHint * 2 ** exponent
1138
+ );
1139
+ const sampledJitter = random();
1140
+ const jitterRatio = Number.isFinite(sampledJitter) ? Math.max(0, Math.min(1, sampledJitter)) : 0;
1141
+ return Math.min(
1142
+ VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS,
1143
+ Math.ceil(exponential * (1 + jitterRatio * 0.2))
1144
+ );
1145
+ }
1119
1146
  function useVoiceInput({
1120
1147
  client,
1121
1148
  workspaceId,
@@ -1128,7 +1155,8 @@ function useVoiceInput({
1128
1155
  createRecordingStore,
1129
1156
  createRecordingId = createDefaultVoiceRecordingId,
1130
1157
  createOwnerId,
1131
- now = currentDate
1158
+ now = currentDate,
1159
+ automaticRetryDelayMilliseconds
1132
1160
  }) {
1133
1161
  const [status, setStatus] = useState2("idle");
1134
1162
  const [error, setError] = useState2(null);
@@ -1153,6 +1181,8 @@ function useVoiceInput({
1153
1181
  const recorderRef = useRef2(null);
1154
1182
  const streamRef = useRef2(null);
1155
1183
  const timerRef = useRef2(null);
1184
+ const automaticRetryTimerRef = useRef2(null);
1185
+ const automaticRetryAttemptRef = useRef2(0);
1156
1186
  const ownerHeartbeatTimerRef = useRef2(null);
1157
1187
  const controllerRef = useRef2(null);
1158
1188
  const valueRef = useRef2(value);
@@ -1204,6 +1234,7 @@ function useVoiceInput({
1204
1234
  setRecordingId(null);
1205
1235
  setDurationSeconds(0);
1206
1236
  setLocallySaved(false);
1237
+ automaticRetryAttemptRef.current = 0;
1207
1238
  }, [stopOwnerHeartbeat]);
1208
1239
  const ensureStore = useCallback2(async () => {
1209
1240
  if (storeRef.current) return storeRef.current;
@@ -1318,7 +1349,7 @@ function useVoiceInput({
1318
1349
  rememberManifest(claimed);
1319
1350
  beginOwnerHeartbeat(claimed);
1320
1351
  setStatus(
1321
- claimed.finalizationState === "transcript-ready" && claimed.transcriptText !== null ? "transcript-ready" : "recovered"
1352
+ claimed.finalizationState === "transcript-ready" && claimed.transcriptText !== null ? "transcript-ready" : claimed.recoveryMode === "automatic" ? "retrying" : "recovered"
1322
1353
  );
1323
1354
  setError(null);
1324
1355
  return;
@@ -1337,8 +1368,10 @@ function useVoiceInput({
1337
1368
  const finalizePersistedRecording = useCallback2(
1338
1369
  async (generation) => {
1339
1370
  const manifest = manifestRef.current;
1340
- const maxSizeBytes = capability?.maxSizeBytes;
1341
- if (!manifest || !client || !maxSizeBytes || manifest.workspaceId !== workspaceId) return;
1371
+ const resumable = capability?.resumable && isResumableVoiceInputClient(client) ? capability.resumable : null;
1372
+ const maxSizeBytes = resumable?.maxSizeBytes ?? capability?.maxSizeBytes;
1373
+ if (!manifest || !client || !capability || !maxSizeBytes || manifest.workspaceId !== workspaceId)
1374
+ return;
1342
1375
  const controller = new AbortController();
1343
1376
  controllerRef.current?.abort();
1344
1377
  controllerRef.current = controller;
@@ -1370,7 +1403,12 @@ function useVoiceInput({
1370
1403
  if (chunks.length === 0) {
1371
1404
  const retained = await store.updateManifest(
1372
1405
  transcribing.recordingId,
1373
- { uploadState: "retrying", transcriptionState: "retrying" },
1406
+ {
1407
+ uploadState: "retrying",
1408
+ transcriptionState: "retrying",
1409
+ recoveryMode: "manual",
1410
+ handoffMode: "explicit"
1411
+ },
1374
1412
  readNow().toISOString(),
1375
1413
  ownerIdRef.current ?? void 0
1376
1414
  );
@@ -1378,16 +1416,12 @@ function useVoiceInput({
1378
1416
  await preserveForRetry(retained, "invalid_audio", generation);
1379
1417
  return;
1380
1418
  }
1381
- const audio = new Blob(
1382
- chunks.sort((left, right) => left.chunkNumber - right.chunkNumber).map((chunk) => chunk.audio),
1383
- { type: transcribing.mimeType }
1384
- );
1385
- if (!active()) return;
1386
- if (audio.size > maxSizeBytes) throw { code: "too_large" };
1387
- const response = await client.transcribeAudio(workspaceId, {
1388
- audio,
1389
- mimeType: audio.type,
1390
- durationSeconds: transcribing.totalDurationMilliseconds / 1e3,
1419
+ const response = await transcribePersistedRecording({
1420
+ client,
1421
+ workspaceId,
1422
+ capability,
1423
+ manifest: transcribing,
1424
+ chunks,
1391
1425
  signal: controller.signal
1392
1426
  });
1393
1427
  if (!active()) return;
@@ -1405,6 +1439,13 @@ function useVoiceInput({
1405
1439
  );
1406
1440
  if (!active()) return;
1407
1441
  rememberManifest(ready);
1442
+ automaticRetryAttemptRef.current = 0;
1443
+ if (ready.handoffMode === "explicit") {
1444
+ setStatus("transcript-ready");
1445
+ setError("handoff_uncertain");
1446
+ focusInput();
1447
+ return;
1448
+ }
1408
1449
  const next = appendFinalTranscript(valueRef.current, response.text);
1409
1450
  if (next !== valueRef.current) {
1410
1451
  valueRef.current = next;
@@ -1428,6 +1469,12 @@ function useVoiceInput({
1428
1469
  }
1429
1470
  if (!active()) return;
1430
1471
  rememberManifest(handedOff);
1472
+ if (resumable && isResumableVoiceInputClient(client)) {
1473
+ await client.discardTranscriptionRecording(workspaceId, handedOff.recordingId, {
1474
+ signal: controller.signal
1475
+ }).catch(() => void 0);
1476
+ }
1477
+ if (!active()) return;
1431
1478
  await store.discard(handedOff.recordingId, ownerIdRef.current ?? void 0).catch(() => void 0);
1432
1479
  if (!active()) return;
1433
1480
  clearVisibleRecording();
@@ -1437,22 +1484,28 @@ function useVoiceInput({
1437
1484
  await loadNextRecoverable(generation);
1438
1485
  } catch (reason) {
1439
1486
  if (!active()) return;
1487
+ const code = controller.signal.aborted ? null : errorCode(reason);
1488
+ const retryable = !controller.signal.aborted && isRetryableVoiceInputError(reason);
1440
1489
  const retained = await updateManifestBestEffort(transcribing, {
1441
1490
  uploadState: "retrying",
1442
- transcriptionState: "retrying"
1491
+ transcriptionState: "retrying",
1492
+ recoveryMode: retryable ? "automatic" : "manual",
1493
+ handoffMode: "explicit"
1443
1494
  });
1444
1495
  if (!active()) return;
1445
- await preserveForRetry(
1446
- retained,
1447
- controller.signal.aborted ? null : errorCode(reason),
1448
- generation
1449
- );
1496
+ if (retryable) {
1497
+ rememberManifest(retained);
1498
+ setStatus("retrying");
1499
+ setError(code);
1500
+ } else {
1501
+ await preserveForRetry(retained, code, generation);
1502
+ }
1450
1503
  } finally {
1451
1504
  if (controllerRef.current === controller) controllerRef.current = null;
1452
1505
  }
1453
1506
  },
1454
1507
  [
1455
- capability?.maxSizeBytes,
1508
+ capability,
1456
1509
  clearVisibleRecording,
1457
1510
  client,
1458
1511
  ensureStore,
@@ -1466,6 +1519,44 @@ function useVoiceInput({
1466
1519
  workspaceId
1467
1520
  ]
1468
1521
  );
1522
+ const finalizePersistedRecordingRef = useRef2(finalizePersistedRecording);
1523
+ finalizePersistedRecordingRef.current = finalizePersistedRecording;
1524
+ const clientAvailable = client !== null && client !== void 0;
1525
+ useEffect2(() => {
1526
+ if (status !== "retrying" || !manifestRef.current || manifestRef.current.recoveryMode !== "automatic" || manifestRef.current.workspaceId !== workspaceId || !clientAvailable || disabled || !enabled) {
1527
+ return;
1528
+ }
1529
+ const generation = generationRef.current;
1530
+ let started = false;
1531
+ const run = () => {
1532
+ if (started || generation !== generationRef.current || statusRef.current !== "retrying" || workspaceIdRef.current !== workspaceId) {
1533
+ return;
1534
+ }
1535
+ started = true;
1536
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
1537
+ automaticRetryTimerRef.current = null;
1538
+ automaticRetryAttemptRef.current += 1;
1539
+ void finalizePersistedRecordingRef.current(generation);
1540
+ };
1541
+ const onOnline = () => run();
1542
+ globalThis.addEventListener?.("online", onOnline);
1543
+ if (typeof navigator !== "undefined" && navigator.onLine === false) {
1544
+ return () => globalThis.removeEventListener?.("online", onOnline);
1545
+ }
1546
+ const delay2 = Math.max(
1547
+ 0,
1548
+ automaticRetryDelayMilliseconds ?? transcriptionRecoveryMutationDelayMilliseconds(
1549
+ TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS,
1550
+ automaticRetryAttemptRef.current
1551
+ )
1552
+ );
1553
+ automaticRetryTimerRef.current = setTimeout(run, delay2);
1554
+ return () => {
1555
+ globalThis.removeEventListener?.("online", onOnline);
1556
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
1557
+ automaticRetryTimerRef.current = null;
1558
+ };
1559
+ }, [automaticRetryDelayMilliseconds, clientAvailable, disabled, enabled, status, workspaceId]);
1469
1560
  const stop = useCallback2(() => {
1470
1561
  const recorder = recorderRef.current;
1471
1562
  if (!recorder || recorder.state === "inactive") return;
@@ -1475,7 +1566,7 @@ function useVoiceInput({
1475
1566
  recorder.stop();
1476
1567
  }, []);
1477
1568
  const start = useCallback2(async () => {
1478
- if (disabled || !client || !enabled || !capability?.available || manifestRef.current !== null || status === "requesting-permission" || status === "recording" || status === "saving" || status === "transcribing" || !navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") {
1569
+ if (disabled || !client || !enabled || !capability?.available || manifestRef.current !== null || status === "requesting-permission" || status === "recording" || status === "saving" || status === "transcribing" || status === "retrying" || !navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") {
1479
1570
  return false;
1480
1571
  }
1481
1572
  const prerequisiteGeneration = generationRef.current;
@@ -1568,7 +1659,8 @@ function useVoiceInput({
1568
1659
  });
1569
1660
  if (!startAttemptIsCurrent()) return;
1570
1661
  rememberManifest(result.manifest);
1571
- if (result.manifest.totalBytes > capability.maxSizeBytes) {
1662
+ const maxSizeBytes = capability.resumable && isResumableVoiceInputClient(client) ? capability.resumable.maxSizeBytes : capability.maxSizeBytes;
1663
+ if (result.manifest.totalBytes > maxSizeBytes) {
1572
1664
  attemptCaptureLimitError = "too_large";
1573
1665
  if (attemptOwnsSharedCapture()) captureLimitErrorRef.current = "too_large";
1574
1666
  if (recorder.state !== "inactive") recorder.stop();
@@ -1607,11 +1699,19 @@ function useVoiceInput({
1607
1699
  }
1608
1700
  rememberManifest(stopped);
1609
1701
  if (attemptPersistenceError) {
1610
- await preserveForRetry(stopped, "storage_unavailable", generation);
1702
+ const retained = await updateManifestBestEffort(stopped, {
1703
+ recoveryMode: "manual",
1704
+ handoffMode: "explicit"
1705
+ });
1706
+ await preserveForRetry(retained, "storage_unavailable", generation);
1611
1707
  return;
1612
1708
  }
1613
1709
  if (attemptCaptureLimitError) {
1614
- await preserveForRetry(stopped, attemptCaptureLimitError, generation);
1710
+ const retained = await updateManifestBestEffort(stopped, {
1711
+ recoveryMode: "manual",
1712
+ handoffMode: "explicit"
1713
+ });
1714
+ await preserveForRetry(retained, attemptCaptureLimitError, generation);
1615
1715
  return;
1616
1716
  }
1617
1717
  await finalizePersistedRecording(generation);
@@ -1627,7 +1727,10 @@ function useVoiceInput({
1627
1727
  setStatus("recording");
1628
1728
  timerRef.current = setTimeout(
1629
1729
  stop,
1630
- Math.min(capability.maxDurationSeconds, VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS) * 1e3
1730
+ Math.min(
1731
+ capability.resumable && isResumableVoiceInputClient(client) ? capability.resumable.maxDurationSeconds : capability.maxDurationSeconds,
1732
+ capability.resumable && isResumableVoiceInputClient(client) ? VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS : VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS
1733
+ ) * 1e3
1631
1734
  );
1632
1735
  return true;
1633
1736
  } catch (reason) {
@@ -1693,9 +1796,29 @@ function useVoiceInput({
1693
1796
  }
1694
1797
  const generation = ++generationRef.current;
1695
1798
  controllerRef.current?.abort();
1799
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
1800
+ automaticRetryTimerRef.current = null;
1801
+ automaticRetryAttemptRef.current = 0;
1696
1802
  setError(null);
1697
- void finalizePersistedRecording(generation);
1698
- }, [client, disabled, enabled, finalizePersistedRecording, status, workspaceId]);
1803
+ const manifest = manifestRef.current;
1804
+ void updateManifestBestEffort(manifest, {
1805
+ recoveryMode: "automatic",
1806
+ handoffMode: "append"
1807
+ }).then((retained) => {
1808
+ if (generation !== generationRef.current) return;
1809
+ rememberManifest(retained);
1810
+ void finalizePersistedRecording(generation);
1811
+ });
1812
+ }, [
1813
+ client,
1814
+ disabled,
1815
+ enabled,
1816
+ finalizePersistedRecording,
1817
+ rememberManifest,
1818
+ status,
1819
+ updateManifestBestEffort,
1820
+ workspaceId
1821
+ ]);
1699
1822
  const insertSavedTranscript = useCallback2(async () => {
1700
1823
  const manifest = manifestRef.current;
1701
1824
  if (!manifest || manifest.workspaceId !== workspaceId || manifest.finalizationState !== "transcript-ready" || manifest.transcriptText === null) {
@@ -1728,6 +1851,10 @@ function useVoiceInput({
1728
1851
  return;
1729
1852
  }
1730
1853
  if (generation !== generationRef.current) return;
1854
+ if (capability?.resumable && isResumableVoiceInputClient(client)) {
1855
+ await client.discardTranscriptionRecording(workspaceId, handedOff.recordingId).catch(() => void 0);
1856
+ }
1857
+ if (generation !== generationRef.current) return;
1731
1858
  await store.discard(handedOff.recordingId, ownerIdRef.current ?? void 0).catch(() => void 0);
1732
1859
  if (generation !== generationRef.current) return;
1733
1860
  clearVisibleRecording();
@@ -1737,6 +1864,8 @@ function useVoiceInput({
1737
1864
  await loadNextRecoverable(generation);
1738
1865
  }, [
1739
1866
  clearVisibleRecording,
1867
+ capability?.resumable,
1868
+ client,
1740
1869
  ensureStore,
1741
1870
  focusInput,
1742
1871
  loadNextRecoverable,
@@ -1760,6 +1889,10 @@ function useVoiceInput({
1760
1889
  await captureSettled.catch(() => void 0);
1761
1890
  if (generation !== generationRef.current) return;
1762
1891
  if (manifest) {
1892
+ if (capability?.resumable && isResumableVoiceInputClient(client)) {
1893
+ await client.discardTranscriptionRecording(workspaceId, manifest.recordingId).catch(() => void 0);
1894
+ }
1895
+ if (generation !== generationRef.current) return;
1763
1896
  try {
1764
1897
  await (await ensureStore()).discard(manifest.recordingId, ownerIdRef.current ?? void 0);
1765
1898
  } catch {
@@ -1781,20 +1914,25 @@ function useVoiceInput({
1781
1914
  }, [
1782
1915
  clearCaptureRuntime,
1783
1916
  clearVisibleRecording,
1917
+ capability?.resumable,
1918
+ client,
1784
1919
  ensureStore,
1785
1920
  focusInput,
1786
1921
  loadNextRecoverable,
1787
- rememberManifest
1922
+ rememberManifest,
1923
+ workspaceId
1788
1924
  ]);
1789
1925
  const cancel = useCallback2(() => {
1790
1926
  if (status === "recording" || status === "requesting-permission") {
1791
1927
  void discard();
1792
1928
  return;
1793
1929
  }
1794
- if (status === "saving" || status === "transcribing") {
1930
+ if (status === "saving" || status === "transcribing" || status === "retrying") {
1795
1931
  const generation = ++generationRef.current;
1796
1932
  controllerRef.current?.abort();
1797
1933
  controllerRef.current = null;
1934
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
1935
+ automaticRetryTimerRef.current = null;
1798
1936
  const captureSettled = status === "saving" ? captureSettledRef.current : Promise.resolve();
1799
1937
  void captureSettled.then(async () => {
1800
1938
  const manifest = manifestRef.current;
@@ -1802,7 +1940,9 @@ function useVoiceInput({
1802
1940
  const retained = await updateManifestBestEffort(manifest, {
1803
1941
  captureState: "stopped",
1804
1942
  uploadState: "retrying",
1805
- transcriptionState: "retrying"
1943
+ transcriptionState: "retrying",
1944
+ recoveryMode: "manual",
1945
+ handoffMode: "explicit"
1806
1946
  });
1807
1947
  await preserveForRetry(retained, null, generation);
1808
1948
  });
@@ -1831,7 +1971,12 @@ function useVoiceInput({
1831
1971
  current.recordingId,
1832
1972
  {
1833
1973
  ...current.captureState === "capturing" ? { captureState: "stopped" } : {},
1834
- ...wasProcessing ? { uploadState: "retrying", transcriptionState: "retrying" } : {},
1974
+ ...wasProcessing ? {
1975
+ uploadState: "retrying",
1976
+ transcriptionState: "retrying",
1977
+ recoveryMode: "automatic",
1978
+ handoffMode: "explicit"
1979
+ } : {},
1835
1980
  ownerId: null,
1836
1981
  ownerHeartbeatAt: null
1837
1982
  },
@@ -1855,7 +2000,7 @@ function useVoiceInput({
1855
2000
  ]);
1856
2001
  useEffect2(() => {
1857
2002
  const onKeyDown = (event) => {
1858
- if (event.key === "Escape" && (status === "requesting-permission" || status === "recording" || status === "saving" || status === "transcribing")) {
2003
+ if (event.key === "Escape" && (status === "requesting-permission" || status === "recording" || status === "saving" || status === "transcribing" || status === "retrying")) {
1859
2004
  event.preventDefault();
1860
2005
  cancel();
1861
2006
  }
@@ -1868,6 +2013,8 @@ function useVoiceInput({
1868
2013
  generationRef.current += 1;
1869
2014
  controllerRef.current?.abort();
1870
2015
  controllerRef.current = null;
2016
+ if (automaticRetryTimerRef.current) clearTimeout(automaticRetryTimerRef.current);
2017
+ automaticRetryTimerRef.current = null;
1871
2018
  const manifest = manifestRef.current;
1872
2019
  const ownerReady = ownerIdPromiseRef.current;
1873
2020
  stopOwnerHeartbeat();
@@ -1882,14 +2029,16 @@ function useVoiceInput({
1882
2029
  const ownerId = ownerIdRef.current ?? (ownerReady ? await ownerReady.catch(() => null) : null);
1883
2030
  const store = storeRef.current;
1884
2031
  if (store && manifest && ownerId) {
1885
- const wasProcessing = statusRef.current === "saving" || statusRef.current === "transcribing";
2032
+ const wasProcessing = statusRef.current === "saving" || statusRef.current === "transcribing" || statusRef.current === "retrying";
1886
2033
  await store.updateManifest(
1887
2034
  manifest.recordingId,
1888
2035
  {
1889
2036
  ...manifest.captureState === "capturing" ? { captureState: "stopped" } : {},
1890
2037
  ...wasProcessing ? {
1891
2038
  uploadState: "retrying",
1892
- transcriptionState: "retrying"
2039
+ transcriptionState: "retrying",
2040
+ recoveryMode: "automatic",
2041
+ handoffMode: "explicit"
1893
2042
  } : {},
1894
2043
  ownerId: null,
1895
2044
  ownerHeartbeatAt: null
@@ -1924,6 +2073,187 @@ function useVoiceInput({
1924
2073
  discard
1925
2074
  };
1926
2075
  }
2076
+ async function transcribePersistedRecording(input) {
2077
+ const client = input.client;
2078
+ const resumable = input.capability.resumable;
2079
+ const ordered = [...input.chunks].sort((left, right) => left.chunkNumber - right.chunkNumber);
2080
+ if (!resumable || !isResumableVoiceInputClient(client)) {
2081
+ const audio = new Blob(
2082
+ ordered.map((chunk) => chunk.audio),
2083
+ { type: input.manifest.mimeType }
2084
+ );
2085
+ if (audio.size > input.capability.maxSizeBytes) throw { code: "too_large" };
2086
+ return await client.transcribeAudio(input.workspaceId, {
2087
+ audio,
2088
+ mimeType: audio.type,
2089
+ durationSeconds: input.manifest.totalDurationMilliseconds / 1e3,
2090
+ signal: input.signal
2091
+ });
2092
+ }
2093
+ if (input.manifest.totalBytes > resumable.maxSizeBytes || input.manifest.totalDurationMilliseconds > resumable.maxDurationSeconds * 1e3) {
2094
+ throw { code: "too_large" };
2095
+ }
2096
+ if (ordered.some((chunk, index) => chunk.chunkNumber !== index) || ordered.length !== input.manifest.chunkCount) {
2097
+ throw { code: "invalid_audio" };
2098
+ }
2099
+ const finalizeInput = {
2100
+ chunkCount: input.manifest.chunkCount,
2101
+ totalBytes: input.manifest.totalBytes,
2102
+ totalDurationMilliseconds: input.manifest.totalDurationMilliseconds,
2103
+ signal: input.signal
2104
+ };
2105
+ let remote = await client.createTranscriptionRecording(input.workspaceId, {
2106
+ recordingId: input.manifest.recordingId,
2107
+ mimeType: input.manifest.mimeType,
2108
+ signal: input.signal
2109
+ });
2110
+ for (const chunk of ordered) {
2111
+ if (chunk.chunkNumber < remote.recording.nextChunkNumber) continue;
2112
+ if (chunk.chunkNumber !== remote.recording.nextChunkNumber) {
2113
+ throw { code: "invalid_audio" };
2114
+ }
2115
+ if (chunk.byteLength > resumable.maxChunkSizeBytes) throw { code: "too_large" };
2116
+ const uploaded = await client.uploadTranscriptionRecordingChunk(
2117
+ input.workspaceId,
2118
+ input.manifest.recordingId,
2119
+ chunk.chunkNumber,
2120
+ {
2121
+ audio: chunk.audio,
2122
+ mimeType: input.manifest.mimeType,
2123
+ sha256: chunk.sha256,
2124
+ startMilliseconds: chunk.startMilliseconds,
2125
+ durationMilliseconds: chunk.durationMilliseconds,
2126
+ signal: input.signal
2127
+ }
2128
+ );
2129
+ remote = { recording: uploaded.recording, segments: remote.segments };
2130
+ }
2131
+ if (remote.recording.nextChunkNumber !== ordered.length) {
2132
+ throw { code: "invalid_audio" };
2133
+ }
2134
+ remote = await client.finalizeTranscriptionRecording(
2135
+ input.workspaceId,
2136
+ input.manifest.recordingId,
2137
+ finalizeInput
2138
+ );
2139
+ let recoveryMutationAttempt = 0;
2140
+ let recoveryMutationDueAt = 0;
2141
+ const scheduleRecoveryMutation = (response) => {
2142
+ if (response.recording.state !== "segmenting" && response.recording.state !== "transcribing") {
2143
+ return;
2144
+ }
2145
+ recoveryMutationDueAt = Date.now() + transcriptionRecoveryMutationDelayMilliseconds(
2146
+ response.retryAfterMilliseconds,
2147
+ recoveryMutationAttempt
2148
+ );
2149
+ recoveryMutationAttempt += 1;
2150
+ };
2151
+ scheduleRecoveryMutation(remote);
2152
+ for (; ; ) {
2153
+ if (input.signal.aborted) throw new DOMException("Aborted", "AbortError");
2154
+ switch (remote.recording.state) {
2155
+ case "complete":
2156
+ if (remote.recording.transcriptText === null) throw { code: "invalid_audio" };
2157
+ return {
2158
+ text: remote.recording.transcriptText,
2159
+ languages: remote.recording.languages
2160
+ };
2161
+ case "ready":
2162
+ remote = await client.processNextTranscriptionRecordingSegment(
2163
+ input.workspaceId,
2164
+ input.manifest.recordingId,
2165
+ { signal: input.signal }
2166
+ );
2167
+ scheduleRecoveryMutation(remote);
2168
+ break;
2169
+ case "failed": {
2170
+ if (!remote.recording.retryable) {
2171
+ throw { code: remote.recording.errorCode ?? "unknown", retryable: false };
2172
+ }
2173
+ const retried = remote.recording.segmentCount === 0 ? await client.finalizeTranscriptionRecording(
2174
+ input.workspaceId,
2175
+ input.manifest.recordingId,
2176
+ finalizeInput
2177
+ ) : await client.processNextTranscriptionRecordingSegment(
2178
+ input.workspaceId,
2179
+ input.manifest.recordingId,
2180
+ { signal: input.signal }
2181
+ );
2182
+ if (retried.recording.state === "failed") {
2183
+ throw {
2184
+ code: retried.recording.errorCode ?? "unknown",
2185
+ retryable: retried.recording.retryable
2186
+ };
2187
+ }
2188
+ remote = retried;
2189
+ scheduleRecoveryMutation(remote);
2190
+ break;
2191
+ }
2192
+ case "discarded":
2193
+ throw { code: "invalid_audio" };
2194
+ case "uploading":
2195
+ remote = await client.finalizeTranscriptionRecording(
2196
+ input.workspaceId,
2197
+ input.manifest.recordingId,
2198
+ finalizeInput
2199
+ );
2200
+ scheduleRecoveryMutation(remote);
2201
+ break;
2202
+ case "segmenting":
2203
+ case "transcribing":
2204
+ if (Date.now() < recoveryMutationDueAt) {
2205
+ await abortableDelay(
2206
+ Math.min(
2207
+ recoveryMutationDueAt - Date.now(),
2208
+ VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS
2209
+ ),
2210
+ input.signal
2211
+ );
2212
+ remote = await client.getTranscriptionRecording(
2213
+ input.workspaceId,
2214
+ input.manifest.recordingId,
2215
+ { signal: input.signal }
2216
+ );
2217
+ break;
2218
+ }
2219
+ if (remote.recording.state === "segmenting") {
2220
+ remote = await client.finalizeTranscriptionRecording(
2221
+ input.workspaceId,
2222
+ input.manifest.recordingId,
2223
+ finalizeInput
2224
+ );
2225
+ } else {
2226
+ remote = await client.processNextTranscriptionRecordingSegment(
2227
+ input.workspaceId,
2228
+ input.manifest.recordingId,
2229
+ { signal: input.signal }
2230
+ );
2231
+ }
2232
+ scheduleRecoveryMutation(remote);
2233
+ break;
2234
+ }
2235
+ }
2236
+ }
2237
+ function isResumableVoiceInputClient(client) {
2238
+ return Boolean(
2239
+ client && client.createTranscriptionRecording && client.getTranscriptionRecording && client.uploadTranscriptionRecordingChunk && client.finalizeTranscriptionRecording && client.processNextTranscriptionRecordingSegment && client.discardTranscriptionRecording
2240
+ );
2241
+ }
2242
+ async function abortableDelay(milliseconds, signal) {
2243
+ if (signal.aborted) throw new DOMException("Aborted", "AbortError");
2244
+ await new Promise((resolve, reject) => {
2245
+ const timer = setTimeout(() => {
2246
+ signal.removeEventListener("abort", onAbort);
2247
+ resolve();
2248
+ }, milliseconds);
2249
+ const onAbort = () => {
2250
+ clearTimeout(timer);
2251
+ signal.removeEventListener("abort", onAbort);
2252
+ reject(new DOMException("Aborted", "AbortError"));
2253
+ };
2254
+ signal.addEventListener("abort", onAbort, { once: true });
2255
+ });
2256
+ }
1927
2257
  function chooseMimeType(accepted) {
1928
2258
  const normalized = accepted.map((value) => value.trim().toLowerCase());
1929
2259
  return MIME_PREFERENCES.find((mimeType) => {
@@ -1933,6 +2263,9 @@ function chooseMimeType(accepted) {
1933
2263
  });
1934
2264
  }
1935
2265
  function errorCode(error) {
2266
+ if (error instanceof OpenGeniApiError && typeof error.details?.transcriptionCode === "string") {
2267
+ return error.details.transcriptionCode;
2268
+ }
1936
2269
  if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1937
2270
  return error.code;
1938
2271
  }
@@ -1940,6 +2273,16 @@ function errorCode(error) {
1940
2273
  if (error instanceof DOMException && error.name === "NotSupportedError") return "not_supported";
1941
2274
  return "unknown";
1942
2275
  }
2276
+ function isRetryableVoiceInputError(error) {
2277
+ if (error instanceof OpenGeniApiError) {
2278
+ return error.retryable && error.status !== 409;
2279
+ }
2280
+ if (error instanceof TypeError) return true;
2281
+ if (error && typeof error === "object" && "retryable" in error) {
2282
+ return error.retryable === true;
2283
+ }
2284
+ return false;
2285
+ }
1943
2286
 
1944
2287
  // src/workstream-control-event.ts
1945
2288
  var OPEN_WORKSTREAM_CONTROL_EVENT = "opengeni:open-workstream-control";
@@ -2412,7 +2755,7 @@ function CommandPalette({
2412
2755
  ] }),
2413
2756
  /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [
2414
2757
  command.danger ? /* @__PURE__ */ jsx("span", { className: "rounded-og-xs bg-og-status-failed/15 px-1 text-[10px] uppercase tracking-wide text-og-status-failed", children: dangerLabel }) : null,
2415
- /* @__PURE__ */ jsx("span", { className: "truncate text-[12px] text-og-fg-subtle max-sm:hidden", children: command.description })
2758
+ /* @__PURE__ */ jsx("span", { className: "og-command-description truncate text-[12px] text-og-fg-subtle max-sm:hidden", children: command.description })
2416
2759
  ] })
2417
2760
  ]
2418
2761
  },
@@ -2499,10 +2842,10 @@ function ModelPicker({
2499
2842
  disabled: disabled === true,
2500
2843
  "aria-label": label,
2501
2844
  className: cn(
2502
- "h-8 max-w-[180px] cursor-pointer appearance-none truncate rounded-og-md bg-transparent",
2845
+ "og-model-picker-native h-8 max-w-[180px] cursor-pointer appearance-none truncate rounded-og-md bg-transparent pointer-coarse:h-11",
2503
2846
  "py-0 pl-2 pr-6 text-[13px] text-og-fg-muted",
2504
2847
  "transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
2505
- "focus:outline-none focus-visible:outline-none",
2848
+ "focus:outline-hidden focus-visible:outline-hidden",
2506
2849
  "disabled:cursor-not-allowed disabled:opacity-50"
2507
2850
  ),
2508
2851
  children: useBillingGroups ? billingGroups.map((group) => /* @__PURE__ */ jsx2("optgroup", { label: group.label, children: group.rows.map((row) => /* @__PURE__ */ jsx2("option", { value: row.id, disabled: !row.selectable, children: row.unavailableReason ? `${row.label} (${row.unavailableReason})` : row.label }, row.id)) }, group.billingClass)) : providerGroupList.map((group) => /* @__PURE__ */ jsx2("optgroup", { label: group.label, children: group.models.map((model) => /* @__PURE__ */ jsx2("option", { value: model.id, disabled: model.optionDisabled, children: model.label }, model.id)) }, group.label))
@@ -3003,21 +3346,27 @@ function useChatComposer() {
3003
3346
  function isRecord2(value) {
3004
3347
  return typeof value === "object" && value !== null;
3005
3348
  }
3006
- var Root = forwardRef(function ComposerRoot({ controller, children, className, style, ...props }, forwardedRef) {
3007
- return /* @__PURE__ */ jsx3(ComposerContext.Provider, { value: controller, children: /* @__PURE__ */ jsx3(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs3(
3349
+ var Root = forwardRef(function ComposerRoot({ controller, children, className, style, responsiveBasis = "viewport", ...props }, forwardedRef) {
3350
+ const responsiveRootRef = useRef4(null);
3351
+ const responsiveContext = useMemo4(
3352
+ () => ({ responsiveBasis, rootRef: responsiveRootRef }),
3353
+ [responsiveBasis]
3354
+ );
3355
+ return /* @__PURE__ */ jsx3(ComposerContext.Provider, { value: controller, children: /* @__PURE__ */ jsx3(ComposerResponsiveContext.Provider, { value: responsiveContext, children: /* @__PURE__ */ jsx3(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs3(
3008
3356
  "div",
3009
3357
  {
3010
3358
  ...props,
3011
- ref: mergeRefs(controller.rootRef, forwardedRef),
3359
+ ref: mergeRefs(controller.rootRef, responsiveRootRef, forwardedRef),
3012
3360
  "data-og-composer-id": controller.id,
3013
- className: cn("og-root", className),
3361
+ "data-og-responsive-basis": responsiveBasis,
3362
+ className: cn("og-root og-composer", className),
3014
3363
  style: { paddingBottom: "env(safe-area-inset-bottom)", ...style },
3015
3364
  children: [
3016
3365
  children,
3017
3366
  /* @__PURE__ */ jsx3(ComposerAnnouncements, {})
3018
3367
  ]
3019
3368
  }
3020
- ) }) });
3369
+ ) }) }) });
3021
3370
  });
3022
3371
  var Frame = forwardRef(function ComposerFrame({ className, ...props }, ref) {
3023
3372
  return /* @__PURE__ */ jsx3("div", { ...props, ref, className: cn("relative", className) });
@@ -3163,8 +3512,8 @@ var Input = forwardRef(function ComposerInput({ rows = 1, placeholder, className
3163
3512
  "aria-controls": paletteOpen ? controller.listboxId : void 0,
3164
3513
  "aria-activedescendant": paletteOpen ? `${controller.listboxId}-option-${controller.palette.highlight}` : void 0,
3165
3514
  className: cn(
3166
- "block w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-og-composer md:px-4 md:text-og-composer-wide",
3167
- "text-og-fg placeholder:text-og-fg-subtle focus:outline-none focus-visible:outline-none",
3515
+ "og-composer-input block w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-og-composer md:px-4 md:text-og-composer-wide pointer-coarse:min-h-11",
3516
+ "text-og-fg placeholder:text-og-fg-subtle focus:outline-hidden focus-visible:outline-hidden",
3168
3517
  "disabled:cursor-not-allowed disabled:opacity-60",
3169
3518
  className
3170
3519
  )
@@ -3193,7 +3542,7 @@ var Footer = forwardRef(function ComposerFooter({ className, ...props }, ref) {
3193
3542
  ...props,
3194
3543
  ref,
3195
3544
  className: cn(
3196
- "flex items-end gap-1.5 px-2 pb-2 pt-0.5 sm:px-2.5 sm:pb-2.5",
3545
+ "og-composer-footer flex items-end gap-1.5 px-2 pb-2 pt-0.5 sm:px-2.5 sm:pb-2.5",
3197
3546
  // Mobile: one control row — never wrap into a second toolbar line.
3198
3547
  "max-sm:flex-nowrap max-sm:items-center max-sm:gap-1",
3199
3548
  className
@@ -3209,7 +3558,7 @@ var Controls = forwardRef(
3209
3558
  ...props,
3210
3559
  ref,
3211
3560
  className: cn(
3212
- "flex min-w-0 flex-1 flex-wrap items-center gap-1.5",
3561
+ "og-composer-controls flex min-w-0 flex-1 flex-wrap items-center gap-1.5",
3213
3562
  "max-sm:flex-nowrap max-sm:gap-1",
3214
3563
  className
3215
3564
  )
@@ -3224,7 +3573,10 @@ var Hint = forwardRef(function ComposerHint({ className, children, ...props }, r
3224
3573
  {
3225
3574
  ...props,
3226
3575
  ref,
3227
- className: cn("min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden", className),
3576
+ className: cn(
3577
+ "og-composer-hint min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden",
3578
+ className
3579
+ ),
3228
3580
  children: children ?? controller.messages.keyboardHint
3229
3581
  }
3230
3582
  );
@@ -3236,7 +3588,7 @@ var Actions = forwardRef(function ComposerActions({ className, ...props }, ref)
3236
3588
  ...props,
3237
3589
  ref,
3238
3590
  className: cn(
3239
- "ml-auto flex shrink-0 items-center gap-1.5",
3591
+ "og-composer-actions ml-auto flex shrink-0 items-center gap-1.5",
3240
3592
  "max-sm:flex-nowrap max-sm:gap-1",
3241
3593
  className
3242
3594
  )
@@ -3273,7 +3625,7 @@ var AttachButton = forwardRef(
3273
3625
  className: cn(
3274
3626
  "inline-flex size-8 items-center justify-center rounded-og-md",
3275
3627
  "text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
3276
- "disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-9",
3628
+ "disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
3277
3629
  className
3278
3630
  ),
3279
3631
  children: children ?? /* @__PURE__ */ jsx3(PaperclipIcon, { className: "size-4" })
@@ -3322,7 +3674,7 @@ var PauseButton = forwardRef(
3322
3674
  disabled: busy,
3323
3675
  "aria-label": ariaLabel ?? controller.messages.pauseAriaLabel,
3324
3676
  className: cn(
3325
- "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-9",
3677
+ "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-11",
3326
3678
  "bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
3327
3679
  "hover:border-og-status-waiting/50 hover:text-og-status-waiting",
3328
3680
  "disabled:opacity-50",
@@ -3348,7 +3700,7 @@ var SendButton = forwardRef(
3348
3700
  "data-og-tip": tip,
3349
3701
  "aria-label": ariaLabel ?? (controller.paused ? controller.messages.sendAndResumeAriaLabel : controller.messages.sendMessageAriaLabel),
3350
3702
  className: cn(
3351
- "inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-9",
3703
+ "inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-11",
3352
3704
  "bg-og-accent text-og-accent-fg shadow-og-sm",
3353
3705
  "transition-[background-color,transform,opacity] duration-150 ease-og-spring",
3354
3706
  "hover:bg-og-accent-strong active:scale-95",
@@ -3470,8 +3822,8 @@ function WorkstreamPausedStrip({
3470
3822
  onClick: onResume,
3471
3823
  children: [
3472
3824
  busy ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-3.5 animate-og-spin" }) : /* @__PURE__ */ jsx3(PlayIcon, { className: "size-3.5 fill-current" }),
3473
- /* @__PURE__ */ jsx3("span", { className: "hidden min-[400px]:inline", children: messages.resumeThisWorkstream }),
3474
- /* @__PURE__ */ jsx3("span", { className: "min-[400px]:hidden", children: messages.resumeShort })
3825
+ /* @__PURE__ */ jsx3("span", { className: "og-composer-resume-label-long hidden min-[400px]:inline", children: messages.resumeThisWorkstream }),
3826
+ /* @__PURE__ */ jsx3("span", { className: "og-composer-resume-label-short min-[400px]:hidden", children: messages.resumeShort })
3475
3827
  ]
3476
3828
  }
3477
3829
  ),
@@ -3497,7 +3849,7 @@ function WorkstreamPausedStrip({
3497
3849
  "a",
3498
3850
  {
3499
3851
  href: blockerHref(entry, controlLinks),
3500
- className: "font-medium text-og-fg underline decoration-og-border-strong underline-offset-2 hover:text-og-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-ring/40",
3852
+ className: "font-medium text-og-fg underline decoration-og-border-strong underline-offset-2 hover:text-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-ring/40",
3501
3853
  children: entry.displayName
3502
3854
  }
3503
3855
  ) : /* @__PURE__ */ jsx3("span", { className: "font-medium text-og-fg", children: entry.displayName }),
@@ -3643,7 +3995,7 @@ function AttachmentChips({
3643
3995
  onRemove,
3644
3996
  onRetry
3645
3997
  }) {
3646
- return /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-2 border-b border-og-border px-3 py-2", children: attachments.map((attachment) => {
3998
+ return /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-2 px-3 py-2", children: attachments.map((attachment) => {
3647
3999
  const failed = attachment.status === "failed";
3648
4000
  const statusText = attachment.status === "uploading" ? messages.uploading : failed ? attachment.error || messages.uploadFailed : messages.formatBytes(attachment.sizeBytes);
3649
4001
  return /* @__PURE__ */ jsxs3(
@@ -3759,10 +4111,12 @@ var defaultMessages = {
3759
4111
  stop: "Stop and transcribe",
3760
4112
  cancel: "Cancel recording",
3761
4113
  retry: "Retry voice input",
4114
+ pauseRetrying: "Pause automatic retry",
3762
4115
  requestingPermission: "Requesting microphone\u2026",
3763
4116
  recording: "Recording. Press Escape to cancel.",
3764
4117
  saving: "Saving audio locally\u2026",
3765
4118
  transcribing: "Transcribing\u2026",
4119
+ retrying: "Recording saved. Retrying automatically\u2026",
3766
4120
  recovered: "Recording recovered and saved locally.",
3767
4121
  recoveredTranscript: "Transcript saved locally. Check your draft before inserting.",
3768
4122
  insertRecoveredTranscript: "Insert saved transcript",
@@ -3810,6 +4164,7 @@ function ComposerTranscriptionControl({
3810
4164
  createOwnerId
3811
4165
  });
3812
4166
  const { status } = transcription;
4167
+ const retrying = status === "retrying";
3813
4168
  const cancelTranscription = transcription.cancel;
3814
4169
  useEffect4(() => {
3815
4170
  if (!suppressed) return;
@@ -3818,12 +4173,12 @@ function ComposerTranscriptionControl({
3818
4173
  }
3819
4174
  }, [cancelTranscription, status, suppressed]);
3820
4175
  const active = status === "requesting-permission" || status === "recording" || status === "saving" || status === "transcribing";
3821
- const recoverable = transcription.hasRecoverableRecording && (status === "recovered" || status === "transcript-ready" || status === "error");
4176
+ const recoverable = transcription.hasRecoverableRecording && (retrying || status === "recovered" || status === "transcript-ready" || status === "error");
3822
4177
  const savedTranscript = status === "transcript-ready" && transcription.savedTranscript !== null;
3823
4178
  const unavailableMessage = composer.disabled ? messages.unavailableDisabled : !capability?.available || !workspaceEnabled ? messages.unavailable : !transcription.available ? messages.errorStorageUnavailable : null;
3824
4179
  const idleLabel = unavailableMessage ?? (status === "error" ? messages.retry : messages.start);
3825
4180
  const errorMessage3 = transcription.error ? transcriptionErrorMessage(transcription.error, messages) : null;
3826
- const announcement = status === "requesting-permission" ? messages.requestingPermission : status === "recording" ? messages.recording : status === "saving" ? messages.saving : status === "transcribing" ? messages.transcribing : status === "recovered" ? messages.recovered : status === "transcript-ready" ? messages.recoveredTranscript : status === "error" ? errorMessage3 ?? messages.errorUnknown : unavailableMessage;
4181
+ const announcement = status === "requesting-permission" ? messages.requestingPermission : status === "recording" ? messages.recording : status === "saving" ? messages.saving : status === "transcribing" ? messages.transcribing : retrying ? messages.retrying : status === "recovered" ? messages.recovered : status === "transcript-ready" ? messages.recoveredTranscript : status === "error" ? errorMessage3 ?? messages.errorUnknown : unavailableMessage;
3827
4182
  function start(event) {
3828
4183
  if (unavailableMessage) {
3829
4184
  event.preventDefault();
@@ -3853,21 +4208,27 @@ function ComposerTranscriptionControl({
3853
4208
  "bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11"
3854
4209
  ),
3855
4210
  children: [
3856
- /* @__PURE__ */ jsx4("span", { className: "max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28", children: savedTranscript ? errorMessage3 ?? messages.recoveredTranscript : status === "error" ? errorMessage3 ?? messages.errorRetryable : messages.recovered }),
3857
- /* @__PURE__ */ jsx4(Tip, { tip: savedTranscript ? messages.insertRecoveredTranscript : messages.retry, children: /* @__PURE__ */ jsx4(
3858
- "button",
4211
+ /* @__PURE__ */ jsx4("span", { className: "og-transcription-recovered-label max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28", children: savedTranscript ? errorMessage3 ?? messages.recoveredTranscript : retrying ? messages.retrying : status === "error" ? errorMessage3 ?? messages.errorRetryable : messages.recovered }),
4212
+ /* @__PURE__ */ jsx4(
4213
+ Tip,
3859
4214
  {
3860
- type: "button",
3861
- onClick: () => savedTranscript ? void transcription.insertSavedTranscript() : transcription.retry(),
3862
- "aria-label": savedTranscript ? messages.insertRecoveredTranscript : messages.retry,
3863
- className: cn(
3864
- "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
3865
- "bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
3866
- "hover:bg-og-fg-muted pointer-coarse:size-11"
3867
- ),
3868
- children: savedTranscript ? /* @__PURE__ */ jsx4(ClipboardPasteIcon, { className: "size-3.5" }) : /* @__PURE__ */ jsx4(RefreshCwIcon, { className: "size-3.5" })
4215
+ tip: savedTranscript ? messages.insertRecoveredTranscript : retrying ? messages.pauseRetrying : messages.retry,
4216
+ children: /* @__PURE__ */ jsx4(
4217
+ "button",
4218
+ {
4219
+ type: "button",
4220
+ onClick: () => savedTranscript ? void transcription.insertSavedTranscript() : retrying ? transcription.cancel() : transcription.retry(),
4221
+ "aria-label": savedTranscript ? messages.insertRecoveredTranscript : retrying ? messages.pauseRetrying : messages.retry,
4222
+ className: cn(
4223
+ "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
4224
+ "bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
4225
+ "hover:bg-og-fg-muted pointer-coarse:size-11"
4226
+ ),
4227
+ children: savedTranscript ? /* @__PURE__ */ jsx4(ClipboardPasteIcon, { className: "size-3.5" }) : retrying ? /* @__PURE__ */ jsx4(SquareIcon, { className: "size-3.5" }) : /* @__PURE__ */ jsx4(RefreshCwIcon, { className: "size-3.5" })
4228
+ }
4229
+ )
3869
4230
  }
3870
- ) }),
4231
+ ),
3871
4232
  /* @__PURE__ */ jsx4(Tip, { tip: messages.discardRecovered, children: /* @__PURE__ */ jsx4(
3872
4233
  "button",
3873
4234
  {
@@ -3978,7 +4339,7 @@ function ComposerTranscriptionControl({
3978
4339
  "span",
3979
4340
  {
3980
4341
  "aria-hidden": "true",
3981
- className: "max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24",
4342
+ className: "og-transcription-error-label max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24",
3982
4343
  children: errorMessage3
3983
4344
  }
3984
4345
  ) }) : null,
@@ -4162,4 +4523,4 @@ export {
4162
4523
  Status,
4163
4524
  ComposerTranscriptionControl
4164
4525
  };
4165
- //# sourceMappingURL=chunk-L3EWO3UK.js.map
4526
+ //# sourceMappingURL=chunk-NJ7M2B6Z.js.map