@opengeni/react 0.44.5 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +3 -0
  2. package/dist/{chunk-L3EWO3UK.js → chunk-3PB7MIT6.js} +243 -22
  3. package/dist/chunk-3PB7MIT6.js.map +1 -0
  4. package/dist/{chunk-EHSYZ4KP.js → chunk-FGZUCXZF.js} +2 -2
  5. package/dist/{chunk-EHSYZ4KP.js.map → chunk-FGZUCXZF.js.map} +1 -1
  6. package/dist/{chunk-LYRJUU5V.js → chunk-LGVMUIRV.js} +543 -200
  7. package/dist/chunk-LGVMUIRV.js.map +1 -0
  8. package/dist/components/user-message-body.d.ts +36 -0
  9. package/dist/composer.js +2 -2
  10. package/dist/hooks/use-voice-input.d.ts +8 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +8 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/realtime.js +4 -4
  15. package/dist/realtime.js.map +1 -1
  16. package/dist/session-ui.d.ts +2 -0
  17. package/dist/session-ui.js +7 -3
  18. package/package.json +2 -2
  19. package/src/components/composer.tsx +4 -4
  20. package/src/components/copy-button.tsx +1 -1
  21. package/src/components/markdown.tsx +1 -1
  22. package/src/components/message-timeline.tsx +134 -25
  23. package/src/components/model-picker.tsx +1 -1
  24. package/src/components/model-policy-picker.tsx +1 -1
  25. package/src/components/user-message-body.tsx +341 -0
  26. package/src/hooks/use-codex-accounts.ts +1 -1
  27. package/src/hooks/use-voice-input.ts +320 -19
  28. package/src/index.ts +2 -0
  29. package/src/realtime/realtime-control.tsx +3 -3
  30. package/src/session-ui.ts +2 -0
  31. package/src/timeline/activity-rail.tsx +1 -1
  32. package/src/timeline/shared.tsx +3 -3
  33. package/src/timeline/turn-summary.tsx +4 -4
  34. package/dist/chunk-L3EWO3UK.js.map +0 -1
  35. package/dist/chunk-LYRJUU5V.js.map +0 -1
@@ -5,6 +5,7 @@ import {
5
5
  VoiceRecordingOwnedError,
6
6
  VoiceRecordingStorageUnavailableError,
7
7
  createVoiceRecordingManifest,
8
+ type VoiceRecordingChunk,
8
9
  type VoiceRecordingManifest,
9
10
  type VoiceRecordingStore,
10
11
  } from "../voice-recording-store";
@@ -25,7 +26,7 @@ export type VoiceInputStatus =
25
26
  | "error";
26
27
 
27
28
  export type UseVoiceInputOptions = {
28
- client: Pick<OpenGeniClient, "transcribeAudio"> | null;
29
+ client: VoiceInputClient | null;
29
30
  workspaceId: string;
30
31
  capability: ClientVoiceInputConfig | null;
31
32
  enabled: boolean;
@@ -40,6 +41,19 @@ export type UseVoiceInputOptions = {
40
41
  now?: (() => Date) | undefined;
41
42
  };
42
43
 
44
+ type ResumableVoiceInputClient = Pick<
45
+ OpenGeniClient,
46
+ | "createTranscriptionRecording"
47
+ | "getTranscriptionRecording"
48
+ | "uploadTranscriptionRecordingChunk"
49
+ | "finalizeTranscriptionRecording"
50
+ | "processNextTranscriptionRecordingSegment"
51
+ | "discardTranscriptionRecording"
52
+ >;
53
+
54
+ type VoiceInputClient = Pick<OpenGeniClient, "transcribeAudio"> &
55
+ Partial<ResumableVoiceInputClient>;
56
+
43
57
  export type UseVoiceInputResult = {
44
58
  status: VoiceInputStatus;
45
59
  error: string | null;
@@ -68,11 +82,43 @@ export const VOICE_RECORDING_TIMESLICE_MILLISECONDS = 5_000;
68
82
  export const VOICE_RECORDING_OWNER_HEARTBEAT_MILLISECONDS = 5_000;
69
83
  export const VOICE_RECORDING_OWNER_STALE_MILLISECONDS = 30_000;
70
84
  export const VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS = 600;
85
+ export const VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS = 8 * 60 * 60;
86
+ export const VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS = 2_000;
87
+ export const VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS = 30_000;
88
+ const TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS = 5_000;
71
89
 
72
90
  const MIME_PREFERENCES = ["audio/webm;codecs=opus", "audio/mp4", "audio/ogg;codecs=opus"];
73
91
  const createDefaultVoiceRecordingId = () => crypto.randomUUID();
74
92
  const currentDate = () => new Date();
75
93
 
94
+ export function transcriptionRecoveryMutationDelayMilliseconds(
95
+ retryAfterMilliseconds: number | undefined,
96
+ attempt: number,
97
+ random: () => number = Math.random,
98
+ ): number {
99
+ const hint =
100
+ typeof retryAfterMilliseconds === "number" &&
101
+ Number.isInteger(retryAfterMilliseconds) &&
102
+ retryAfterMilliseconds > 0
103
+ ? retryAfterMilliseconds
104
+ : TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS;
105
+ const boundedHint = Math.max(
106
+ 500,
107
+ Math.min(hint, VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS),
108
+ );
109
+ const exponent = Math.max(0, Math.min(Math.floor(attempt), 6));
110
+ const exponential = Math.min(
111
+ VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS,
112
+ boundedHint * 2 ** exponent,
113
+ );
114
+ const sampledJitter = random();
115
+ const jitterRatio = Number.isFinite(sampledJitter) ? Math.max(0, Math.min(1, sampledJitter)) : 0;
116
+ return Math.min(
117
+ VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS,
118
+ Math.ceil(exponential * (1 + jitterRatio * 0.2)),
119
+ );
120
+ }
121
+
76
122
  export function useVoiceInput({
77
123
  client,
78
124
  workspaceId,
@@ -322,8 +368,17 @@ export function useVoiceInput({
322
368
  const finalizePersistedRecording = useCallback(
323
369
  async (generation: number): Promise<void> => {
324
370
  const manifest = manifestRef.current;
325
- const maxSizeBytes = capability?.maxSizeBytes;
326
- if (!manifest || !client || !maxSizeBytes || manifest.workspaceId !== workspaceId) return;
371
+ const resumable =
372
+ capability?.resumable && isResumableVoiceInputClient(client) ? capability.resumable : null;
373
+ const maxSizeBytes = resumable?.maxSizeBytes ?? capability?.maxSizeBytes;
374
+ if (
375
+ !manifest ||
376
+ !client ||
377
+ !capability ||
378
+ !maxSizeBytes ||
379
+ manifest.workspaceId !== workspaceId
380
+ )
381
+ return;
327
382
  const controller = new AbortController();
328
383
  controllerRef.current?.abort();
329
384
  controllerRef.current = controller;
@@ -368,18 +423,12 @@ export function useVoiceInput({
368
423
  await preserveForRetry(retained, "invalid_audio", generation);
369
424
  return;
370
425
  }
371
- const audio = new Blob(
372
- chunks
373
- .sort((left, right) => left.chunkNumber - right.chunkNumber)
374
- .map((chunk) => chunk.audio),
375
- { type: transcribing.mimeType },
376
- );
377
- if (!active()) return;
378
- if (audio.size > maxSizeBytes) throw { code: "too_large" };
379
- const response = await client.transcribeAudio(workspaceId, {
380
- audio,
381
- mimeType: audio.type,
382
- durationSeconds: transcribing.totalDurationMilliseconds / 1_000,
426
+ const response = await transcribePersistedRecording({
427
+ client,
428
+ workspaceId,
429
+ capability,
430
+ manifest: transcribing,
431
+ chunks,
383
432
  signal: controller.signal,
384
433
  });
385
434
  if (!active()) return;
@@ -422,6 +471,14 @@ export function useVoiceInput({
422
471
  }
423
472
  if (!active()) return;
424
473
  rememberManifest(handedOff);
474
+ if (resumable && isResumableVoiceInputClient(client)) {
475
+ await client
476
+ .discardTranscriptionRecording(workspaceId, handedOff.recordingId, {
477
+ signal: controller.signal,
478
+ })
479
+ .catch(() => undefined);
480
+ }
481
+ if (!active()) return;
425
482
  await store
426
483
  .discard(handedOff.recordingId, ownerIdRef.current ?? undefined)
427
484
  .catch(() => undefined);
@@ -448,7 +505,7 @@ export function useVoiceInput({
448
505
  }
449
506
  },
450
507
  [
451
- capability?.maxSizeBytes,
508
+ capability,
452
509
  clearVisibleRecording,
453
510
  client,
454
511
  ensureStore,
@@ -602,7 +659,11 @@ export function useVoiceInput({
602
659
  });
603
660
  if (!startAttemptIsCurrent()) return;
604
661
  rememberManifest(result.manifest);
605
- if (result.manifest.totalBytes > capability.maxSizeBytes) {
662
+ const maxSizeBytes =
663
+ capability.resumable && isResumableVoiceInputClient(client)
664
+ ? capability.resumable.maxSizeBytes
665
+ : capability.maxSizeBytes;
666
+ if (result.manifest.totalBytes > maxSizeBytes) {
606
667
  attemptCaptureLimitError = "too_large";
607
668
  if (attemptOwnsSharedCapture()) captureLimitErrorRef.current = "too_large";
608
669
  if (recorder.state !== "inactive") recorder.stop();
@@ -675,8 +736,14 @@ export function useVoiceInput({
675
736
  setStatus("recording");
676
737
  timerRef.current = setTimeout(
677
738
  stop,
678
- Math.min(capability.maxDurationSeconds, VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS) *
679
- 1_000,
739
+ Math.min(
740
+ capability.resumable && isResumableVoiceInputClient(client)
741
+ ? capability.resumable.maxDurationSeconds
742
+ : capability.maxDurationSeconds,
743
+ capability.resumable && isResumableVoiceInputClient(client)
744
+ ? VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS
745
+ : VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS,
746
+ ) * 1_000,
680
747
  );
681
748
  return true;
682
749
  } catch (reason) {
@@ -800,6 +867,12 @@ export function useVoiceInput({
800
867
  return;
801
868
  }
802
869
  if (generation !== generationRef.current) return;
870
+ if (capability?.resumable && isResumableVoiceInputClient(client)) {
871
+ await client
872
+ .discardTranscriptionRecording(workspaceId, handedOff.recordingId)
873
+ .catch(() => undefined);
874
+ }
875
+ if (generation !== generationRef.current) return;
803
876
  await store
804
877
  .discard(handedOff.recordingId, ownerIdRef.current ?? undefined)
805
878
  .catch(() => undefined);
@@ -811,6 +884,8 @@ export function useVoiceInput({
811
884
  await loadNextRecoverable(generation);
812
885
  }, [
813
886
  clearVisibleRecording,
887
+ capability?.resumable,
888
+ client,
814
889
  ensureStore,
815
890
  focusInput,
816
891
  loadNextRecoverable,
@@ -835,6 +910,12 @@ export function useVoiceInput({
835
910
  await captureSettled.catch(() => undefined);
836
911
  if (generation !== generationRef.current) return;
837
912
  if (manifest) {
913
+ if (capability?.resumable && isResumableVoiceInputClient(client)) {
914
+ await client
915
+ .discardTranscriptionRecording(workspaceId, manifest.recordingId)
916
+ .catch(() => undefined);
917
+ }
918
+ if (generation !== generationRef.current) return;
838
919
  try {
839
920
  await (await ensureStore()).discard(manifest.recordingId, ownerIdRef.current ?? undefined);
840
921
  } catch {
@@ -856,10 +937,13 @@ export function useVoiceInput({
856
937
  }, [
857
938
  clearCaptureRuntime,
858
939
  clearVisibleRecording,
940
+ capability?.resumable,
941
+ client,
859
942
  ensureStore,
860
943
  focusInput,
861
944
  loadNextRecoverable,
862
945
  rememberManifest,
946
+ workspaceId,
863
947
  ]);
864
948
 
865
949
  const cancel = useCallback(() => {
@@ -1032,6 +1116,223 @@ export function useVoiceInput({
1032
1116
  };
1033
1117
  }
1034
1118
 
1119
+ async function transcribePersistedRecording(input: {
1120
+ client: VoiceInputClient;
1121
+ workspaceId: string;
1122
+ capability: ClientVoiceInputConfig;
1123
+ manifest: VoiceRecordingManifest;
1124
+ chunks: VoiceRecordingChunk[];
1125
+ signal: AbortSignal;
1126
+ }): Promise<{ text: string; languages: string[] }> {
1127
+ const client = input.client;
1128
+ const resumable = input.capability.resumable;
1129
+ const ordered = [...input.chunks].sort((left, right) => left.chunkNumber - right.chunkNumber);
1130
+ if (!resumable || !isResumableVoiceInputClient(client)) {
1131
+ const audio = new Blob(
1132
+ ordered.map((chunk) => chunk.audio),
1133
+ { type: input.manifest.mimeType },
1134
+ );
1135
+ if (audio.size > input.capability.maxSizeBytes) throw { code: "too_large" };
1136
+ return await client.transcribeAudio(input.workspaceId, {
1137
+ audio,
1138
+ mimeType: audio.type,
1139
+ durationSeconds: input.manifest.totalDurationMilliseconds / 1_000,
1140
+ signal: input.signal,
1141
+ });
1142
+ }
1143
+
1144
+ if (
1145
+ input.manifest.totalBytes > resumable.maxSizeBytes ||
1146
+ input.manifest.totalDurationMilliseconds > resumable.maxDurationSeconds * 1_000
1147
+ ) {
1148
+ throw { code: "too_large" };
1149
+ }
1150
+ if (
1151
+ ordered.some((chunk, index) => chunk.chunkNumber !== index) ||
1152
+ ordered.length !== input.manifest.chunkCount
1153
+ ) {
1154
+ throw { code: "invalid_audio" };
1155
+ }
1156
+
1157
+ const finalizeInput = {
1158
+ chunkCount: input.manifest.chunkCount,
1159
+ totalBytes: input.manifest.totalBytes,
1160
+ totalDurationMilliseconds: input.manifest.totalDurationMilliseconds,
1161
+ signal: input.signal,
1162
+ };
1163
+
1164
+ let remote = await client.createTranscriptionRecording(input.workspaceId, {
1165
+ recordingId: input.manifest.recordingId,
1166
+ mimeType: input.manifest.mimeType,
1167
+ signal: input.signal,
1168
+ });
1169
+ for (const chunk of ordered) {
1170
+ if (chunk.chunkNumber < remote.recording.nextChunkNumber) continue;
1171
+ if (chunk.chunkNumber !== remote.recording.nextChunkNumber) {
1172
+ throw { code: "invalid_audio" };
1173
+ }
1174
+ if (chunk.byteLength > resumable.maxChunkSizeBytes) throw { code: "too_large" };
1175
+ const uploaded = await client.uploadTranscriptionRecordingChunk(
1176
+ input.workspaceId,
1177
+ input.manifest.recordingId,
1178
+ chunk.chunkNumber,
1179
+ {
1180
+ audio: chunk.audio,
1181
+ mimeType: input.manifest.mimeType,
1182
+ sha256: chunk.sha256,
1183
+ startMilliseconds: chunk.startMilliseconds,
1184
+ durationMilliseconds: chunk.durationMilliseconds,
1185
+ signal: input.signal,
1186
+ },
1187
+ );
1188
+ remote = { recording: uploaded.recording, segments: remote.segments };
1189
+ }
1190
+ if (remote.recording.nextChunkNumber !== ordered.length) {
1191
+ throw { code: "invalid_audio" };
1192
+ }
1193
+
1194
+ remote = await client.finalizeTranscriptionRecording(
1195
+ input.workspaceId,
1196
+ input.manifest.recordingId,
1197
+ finalizeInput,
1198
+ );
1199
+
1200
+ let recoveryMutationAttempt = 0;
1201
+ let recoveryMutationDueAt = 0;
1202
+ const scheduleRecoveryMutation = (response: typeof remote): void => {
1203
+ if (response.recording.state !== "segmenting" && response.recording.state !== "transcribing") {
1204
+ return;
1205
+ }
1206
+ recoveryMutationDueAt =
1207
+ Date.now() +
1208
+ transcriptionRecoveryMutationDelayMilliseconds(
1209
+ response.retryAfterMilliseconds,
1210
+ recoveryMutationAttempt,
1211
+ );
1212
+ recoveryMutationAttempt += 1;
1213
+ };
1214
+ scheduleRecoveryMutation(remote);
1215
+
1216
+ for (;;) {
1217
+ if (input.signal.aborted) throw new DOMException("Aborted", "AbortError");
1218
+ switch (remote.recording.state) {
1219
+ case "complete":
1220
+ if (remote.recording.transcriptText === null) throw { code: "invalid_audio" };
1221
+ return {
1222
+ text: remote.recording.transcriptText,
1223
+ languages: remote.recording.languages,
1224
+ };
1225
+ case "ready":
1226
+ remote = await client.processNextTranscriptionRecordingSegment(
1227
+ input.workspaceId,
1228
+ input.manifest.recordingId,
1229
+ { signal: input.signal },
1230
+ );
1231
+ scheduleRecoveryMutation(remote);
1232
+ break;
1233
+ case "failed": {
1234
+ if (!remote.recording.retryable) {
1235
+ throw { code: remote.recording.errorCode ?? "unknown" };
1236
+ }
1237
+ const retried =
1238
+ remote.recording.segmentCount === 0
1239
+ ? await client.finalizeTranscriptionRecording(
1240
+ input.workspaceId,
1241
+ input.manifest.recordingId,
1242
+ finalizeInput,
1243
+ )
1244
+ : await client.processNextTranscriptionRecordingSegment(
1245
+ input.workspaceId,
1246
+ input.manifest.recordingId,
1247
+ { signal: input.signal },
1248
+ );
1249
+ if (retried.recording.state === "failed") {
1250
+ throw { code: retried.recording.errorCode ?? "unknown" };
1251
+ }
1252
+ remote = retried;
1253
+ scheduleRecoveryMutation(remote);
1254
+ break;
1255
+ }
1256
+ case "discarded":
1257
+ throw { code: "invalid_audio" };
1258
+ case "uploading":
1259
+ remote = await client.finalizeTranscriptionRecording(
1260
+ input.workspaceId,
1261
+ input.manifest.recordingId,
1262
+ finalizeInput,
1263
+ );
1264
+ scheduleRecoveryMutation(remote);
1265
+ break;
1266
+ case "segmenting":
1267
+ case "transcribing":
1268
+ if (Date.now() < recoveryMutationDueAt) {
1269
+ await abortableDelay(
1270
+ Math.min(
1271
+ recoveryMutationDueAt - Date.now(),
1272
+ VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS,
1273
+ ),
1274
+ input.signal,
1275
+ );
1276
+ remote = await client.getTranscriptionRecording(
1277
+ input.workspaceId,
1278
+ input.manifest.recordingId,
1279
+ { signal: input.signal },
1280
+ );
1281
+ break;
1282
+ }
1283
+ if (remote.recording.state === "segmenting") {
1284
+ // Re-enter the server-authoritative assembly claim only when its
1285
+ // durable recovery schedule is due.
1286
+ remote = await client.finalizeTranscriptionRecording(
1287
+ input.workspaceId,
1288
+ input.manifest.recordingId,
1289
+ finalizeInput,
1290
+ );
1291
+ } else {
1292
+ // Re-enter the server-authoritative segment claim only when its
1293
+ // durable recovery schedule is due.
1294
+ remote = await client.processNextTranscriptionRecordingSegment(
1295
+ input.workspaceId,
1296
+ input.manifest.recordingId,
1297
+ { signal: input.signal },
1298
+ );
1299
+ }
1300
+ scheduleRecoveryMutation(remote);
1301
+ break;
1302
+ }
1303
+ }
1304
+ }
1305
+
1306
+ function isResumableVoiceInputClient(
1307
+ client: VoiceInputClient | null,
1308
+ ): client is VoiceInputClient & ResumableVoiceInputClient {
1309
+ return Boolean(
1310
+ client &&
1311
+ client.createTranscriptionRecording &&
1312
+ client.getTranscriptionRecording &&
1313
+ client.uploadTranscriptionRecordingChunk &&
1314
+ client.finalizeTranscriptionRecording &&
1315
+ client.processNextTranscriptionRecordingSegment &&
1316
+ client.discardTranscriptionRecording,
1317
+ );
1318
+ }
1319
+
1320
+ async function abortableDelay(milliseconds: number, signal: AbortSignal): Promise<void> {
1321
+ if (signal.aborted) throw new DOMException("Aborted", "AbortError");
1322
+ await new Promise<void>((resolve, reject) => {
1323
+ const timer = setTimeout(() => {
1324
+ signal.removeEventListener("abort", onAbort);
1325
+ resolve();
1326
+ }, milliseconds);
1327
+ const onAbort = () => {
1328
+ clearTimeout(timer);
1329
+ signal.removeEventListener("abort", onAbort);
1330
+ reject(new DOMException("Aborted", "AbortError"));
1331
+ };
1332
+ signal.addEventListener("abort", onAbort, { once: true });
1333
+ });
1334
+ }
1335
+
1035
1336
  function chooseMimeType(accepted: string[]): string | undefined {
1036
1337
  const normalized = accepted.map((value) => value.trim().toLowerCase());
1037
1338
  return MIME_PREFERENCES.find((mimeType) => {
package/src/index.ts CHANGED
@@ -460,6 +460,8 @@ export {
460
460
  export type { LatencyModeId, PickerBillingClass, PickerModelRow } from "./model-policy";
461
461
  export { MessageTimeline, TimelineRow } from "./components/message-timeline";
462
462
  export type { MessageTimelineProps } from "./components/message-timeline";
463
+ export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
464
+ export type { UserMessageBodyProps } from "./components/user-message-body";
463
465
  export { Markdown } from "./components/markdown";
464
466
  export { CopyButton, CopyHoverFrame } from "./components/copy-button";
465
467
  export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
@@ -746,11 +746,11 @@ export function RealtimeVoiceControl(props: {
746
746
  title={`Voice model: ${selectedModel.label}`}
747
747
  disabled={props.selectionDisabled}
748
748
  className={cn(
749
- // ≥24px wide so WCAG 2.2 target-size passes when the chevron is shown.
750
- "inline-flex h-8 w-6 items-center justify-center rounded-r-og-md outline-none",
749
+ // Keep the split trigger compact on desktop while preserving a
750
+ // full 44px mobile target independent of pointer-media support.
751
+ "inline-flex h-11 w-11 items-center justify-center rounded-r-og-md outline-none sm:h-8 sm:w-6",
751
752
  "transition-[background-color,border-color,color] duration-200 ease-og-out",
752
753
  "focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
753
- "pointer-coarse:h-11 pointer-coarse:w-7",
754
754
  desktopOnlyModelMenu && "hidden sm:inline-flex",
755
755
  voiceChevronTone(status.phase),
756
756
  )}
package/src/session-ui.ts CHANGED
@@ -11,6 +11,8 @@ export { HumanInputSurface } from "./components/human-input-surface";
11
11
  export type { HumanInputSurfaceProps } from "./components/human-input-surface";
12
12
  export { MessageTimeline, TimelineRow } from "./components/message-timeline";
13
13
  export type { MessageTimelineProps } from "./components/message-timeline";
14
+ export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
15
+ export type { UserMessageBodyProps } from "./components/user-message-body";
14
16
  export { BUILT_IN_TURN_SUMMARY_FACET_IDS } from "./timeline/turn-summary";
15
17
  export type {
16
18
  BuiltInTurnSummaryFacetId,
@@ -419,7 +419,7 @@ function WorkerRow({
419
419
  className={cn(
420
420
  "group/worker -mx-1.5 flex w-full items-start gap-2 rounded-og-sm px-1.5 py-1.5 text-left",
421
421
  "outline-none transition-colors duration-150 hover:bg-og-surface-1 focus-visible:ring-2 focus-visible:ring-og-accent",
422
- "pointer-coarse:py-2.5",
422
+ "pointer-coarse:min-h-11 pointer-coarse:py-2.5",
423
423
  )}
424
424
  >
425
425
  {inner}
@@ -154,9 +154,9 @@ export function ActivityDisclosure({
154
154
  const rowClass = cn(
155
155
  "group/disclosure flex w-full min-w-0 items-center gap-2 rounded-og-sm px-1.5 py-1.5 text-left text-og-base",
156
156
  "text-og-fg-muted transition-colors duration-150",
157
- // A tool row is a touch target on coarse pointers: grow its padding so the
158
- // hit area clears the 40px minimum without loosening the dense desktop rail.
159
- "pointer-coarse:py-2.5",
157
+ // A tool row is a touch target on coarse pointers: grow its hit area to the
158
+ // 44px mobile minimum without loosening the dense desktop rail.
159
+ "pointer-coarse:min-h-11 pointer-coarse:py-2.5",
160
160
  );
161
161
  // The chevron rotates to point down when open; it tracks `data-state` on this
162
162
  // same row (the Trigger), so the affordance never freezes.
@@ -416,8 +416,8 @@ export function TurnSummary({
416
416
  // as a turn landmark above any nested cluster rows it groups.
417
417
  "group flex w-full items-center rounded-og-sm text-left transition-colors",
418
418
  // A folded turn is a touch target on coarse pointers: grow the row so it
419
- // clears the 40px minimum without disturbing the calm desktop rhythm.
420
- "pointer-coarse:py-2.5",
419
+ // clears the 44px minimum without disturbing the calm desktop rhythm.
420
+ "pointer-coarse:min-h-11 pointer-coarse:py-2.5",
421
421
  bare
422
422
  ? "gap-2 px-1.5 py-1.5 text-og-sm text-og-fg-muted"
423
423
  : "-mx-2 gap-2.5 px-2 py-1.5 text-og-base text-og-fg-muted",
@@ -646,8 +646,8 @@ const BUILT_IN_TURN_SUMMARY_FACETS: readonly TurnSummaryFacet[] = Object.freeze(
646
646
  contextCompactionCount === 1 ? "compacted" : `${contextCompactionCount} compacts`,
647
647
  ariaLabel:
648
648
  contextCompactionCount === 1
649
- ? "Conversation memory compacted"
650
- : `${contextCompactionCount} conversation memory compactions`,
649
+ ? "Conversation history compacted"
650
+ : `${contextCompactionCount} conversation history compactions`,
651
651
  }
652
652
  : null,
653
653
  },