@opengeni/react 7.6.1 → 7.7.0-canary.37090756480001

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 (142) hide show
  1. package/README.md +19 -2
  2. package/dist/{browser-viewer-5X47I6T2.js → browser-viewer-63TXMINB.js} +6 -5
  3. package/dist/{chunk-UINDK6C3.js → chunk-2QUSQVF2.js} +3 -3
  4. package/dist/{chunk-CHPKSLPN.js → chunk-6ZSWSH4I.js} +3 -1
  5. package/dist/chunk-6ZSWSH4I.js.map +1 -0
  6. package/dist/{chunk-KGENSZKS.js → chunk-AAG4R3QN.js} +4 -3
  7. package/dist/chunk-AAG4R3QN.js.map +1 -0
  8. package/dist/{chunk-Z3DISEQN.js → chunk-CWWKTNKK.js} +35 -18
  9. package/dist/chunk-CWWKTNKK.js.map +1 -0
  10. package/dist/{chunk-5EXGU7UF.js → chunk-DOJWJQ3F.js} +3 -3
  11. package/dist/{chunk-WZ55DVGK.js → chunk-ELCE5SXV.js} +8 -4
  12. package/dist/chunk-ELCE5SXV.js.map +1 -0
  13. package/dist/{chunk-YSMMDOOE.js → chunk-GISEVGS6.js} +10 -2
  14. package/dist/chunk-GISEVGS6.js.map +1 -0
  15. package/dist/{chunk-YRYF2GLG.js → chunk-HNFCAQ5R.js} +39 -31
  16. package/dist/chunk-HNFCAQ5R.js.map +1 -0
  17. package/dist/{chunk-BI57CQUV.js → chunk-I7IWRQLN.js} +2 -2
  18. package/dist/{chunk-XA6R7DJQ.js → chunk-IQUCRAT7.js} +3 -3
  19. package/dist/{chunk-DV2DBNPQ.js → chunk-Q5QBEZIQ.js} +48 -29
  20. package/dist/chunk-Q5QBEZIQ.js.map +1 -0
  21. package/dist/{chunk-FGBCYQOT.js → chunk-RDKBXMWX.js} +120 -74
  22. package/dist/chunk-RDKBXMWX.js.map +1 -0
  23. package/dist/{chunk-45GE35O7.js → chunk-TKB4SB44.js} +10 -2
  24. package/dist/chunk-TKB4SB44.js.map +1 -0
  25. package/dist/{chunk-YCGBREUV.js → chunk-TOAH2BKA.js} +6 -2
  26. package/dist/{chunk-YCGBREUV.js.map → chunk-TOAH2BKA.js.map} +1 -1
  27. package/dist/chunk-UGLEYEMP.js +33 -0
  28. package/dist/chunk-UGLEYEMP.js.map +1 -0
  29. package/dist/{chunk-UJRYEWZI.js → chunk-UTRTG6LH.js} +48 -8
  30. package/dist/chunk-UTRTG6LH.js.map +1 -0
  31. package/dist/{chunk-VGIKFIO2.js → chunk-XOAMRDDK.js} +6 -3
  32. package/dist/chunk-XOAMRDDK.js.map +1 -0
  33. package/dist/{chunk-6NEZPF53.js → chunk-YN7I3STW.js} +153 -32
  34. package/dist/chunk-YN7I3STW.js.map +1 -0
  35. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  36. package/dist/chunk-YQEAA6LT.js.map +1 -0
  37. package/dist/{chunk-LZ3KHA55.js → chunk-ZYALTAEQ.js} +329 -339
  38. package/dist/chunk-ZYALTAEQ.js.map +1 -0
  39. package/dist/client.d.ts +4 -1
  40. package/dist/commands/types.d.ts +2 -0
  41. package/dist/components/approval-surface.d.ts +15 -0
  42. package/dist/composer.js +8 -7
  43. package/dist/{computer-viewer-7GF35SNY.js → computer-viewer-X2MBRZY4.js} +5 -4
  44. package/dist/connect.js +3 -3
  45. package/dist/hooks/use-browser-session.d.ts +4 -0
  46. package/dist/hooks/use-composer.d.ts +2 -0
  47. package/dist/hooks/use-computer-session.d.ts +2 -0
  48. package/dist/hooks/use-file-attachments.d.ts +2 -0
  49. package/dist/hooks/use-session-events.d.ts +1 -1
  50. package/dist/hooks/use-workspace-sessions.d.ts +15 -5
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +28 -21
  53. package/dist/index.js.map +1 -1
  54. package/dist/interaction.d.ts +1 -1
  55. package/dist/interaction.js +6 -5
  56. package/dist/interaction.js.map +1 -1
  57. package/dist/{latest-question-3BJGKQQ6.js → latest-question-CNEZ5KAN.js} +3 -3
  58. package/dist/lib/error-message.d.ts +7 -0
  59. package/dist/lib/format.d.ts +27 -3
  60. package/dist/lib/interaction-errors.d.ts +3 -0
  61. package/dist/machines.js +5 -4
  62. package/dist/{model-policy-picker-menu-PUTA5JOB.js → model-policy-picker-menu-JSA64TNN.js} +4 -4
  63. package/dist/model-policy.js +1 -1
  64. package/dist/provider.d.ts +5 -1
  65. package/dist/{queue-surface-implementation-2CAQAIHM.js → queue-surface-implementation-HENWYFWA.js} +3 -2
  66. package/dist/{queue-surface-implementation-2CAQAIHM.js.map → queue-surface-implementation-HENWYFWA.js.map} +1 -1
  67. package/dist/realtime.js +2 -2
  68. package/dist/{sandbox-files-AEJTWGT4.js → sandbox-files-BBC47SLD.js} +4 -3
  69. package/dist/session-ui.d.ts +1 -1
  70. package/dist/session-ui.js +13 -12
  71. package/dist/session-ui.js.map +1 -1
  72. package/dist/session.js +7 -6
  73. package/dist/usage.js +2 -1
  74. package/dist/usage.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/client.ts +5 -1
  77. package/src/commands/registry.ts +18 -15
  78. package/src/commands/types.ts +2 -0
  79. package/src/components/approval-surface.tsx +166 -42
  80. package/src/components/browser-viewer.tsx +38 -9
  81. package/src/components/composer.tsx +9 -2
  82. package/src/components/computer-viewer.tsx +160 -30
  83. package/src/components/generated-video-player.tsx +4 -2
  84. package/src/components/human-input-form.tsx +18 -5
  85. package/src/components/message-timeline.tsx +120 -168
  86. package/src/components/open-geni-chat.tsx +11 -4
  87. package/src/components/queue-surface-state.tsx +3 -1
  88. package/src/components/session-chrome.tsx +5 -2
  89. package/src/components/session-commands-panel.tsx +3 -1
  90. package/src/components/session-conversation.tsx +4 -2
  91. package/src/components/session-list.tsx +7 -5
  92. package/src/components/session-mcp-capability-card.tsx +11 -1
  93. package/src/hooks/internal.ts +3 -2
  94. package/src/hooks/use-browser-session.ts +115 -79
  95. package/src/hooks/use-composer.ts +70 -31
  96. package/src/hooks/use-computer-session.ts +57 -6
  97. package/src/hooks/use-file-attachments.ts +16 -3
  98. package/src/hooks/use-goal.ts +2 -1
  99. package/src/hooks/use-session-capabilities.ts +3 -2
  100. package/src/hooks/use-session-events.ts +15 -8
  101. package/src/hooks/use-slash-commands.ts +6 -11
  102. package/src/hooks/use-turn-queue.ts +2 -1
  103. package/src/hooks/use-workspace-sessions.ts +40 -7
  104. package/src/index.ts +7 -2
  105. package/src/interaction.ts +1 -0
  106. package/src/lib/error-message.ts +37 -0
  107. package/src/lib/format.ts +73 -9
  108. package/src/lib/interaction-errors.ts +15 -0
  109. package/src/model-policy.ts +5 -0
  110. package/src/provider.tsx +12 -4
  111. package/src/session-ui.ts +1 -1
  112. package/src/timeline/projection.ts +12 -1
  113. package/styles/compiled.css +14 -30
  114. package/dist/chunk-45GE35O7.js.map +0 -1
  115. package/dist/chunk-6NEZPF53.js.map +0 -1
  116. package/dist/chunk-CHPKSLPN.js.map +0 -1
  117. package/dist/chunk-DV2DBNPQ.js.map +0 -1
  118. package/dist/chunk-FGBCYQOT.js.map +0 -1
  119. package/dist/chunk-KGENSZKS.js.map +0 -1
  120. package/dist/chunk-KZ73RL76.js.map +0 -1
  121. package/dist/chunk-LZ3KHA55.js.map +0 -1
  122. package/dist/chunk-UJRYEWZI.js.map +0 -1
  123. package/dist/chunk-VGIKFIO2.js.map +0 -1
  124. package/dist/chunk-WZ55DVGK.js.map +0 -1
  125. package/dist/chunk-YRYF2GLG.js.map +0 -1
  126. package/dist/chunk-YSMMDOOE.js.map +0 -1
  127. package/dist/chunk-Z3DISEQN.js.map +0 -1
  128. package/dist/components/timeline-pill-geometry.d.ts +0 -8
  129. package/dist/components/timeline-pill-placement.d.ts +0 -24
  130. package/dist/timeline-pill-geometry-Z23V243M.js +0 -110
  131. package/dist/timeline-pill-geometry-Z23V243M.js.map +0 -1
  132. package/src/components/timeline-pill-geometry.ts +0 -169
  133. package/src/components/timeline-pill-placement.ts +0 -99
  134. /package/dist/{browser-viewer-5X47I6T2.js.map → browser-viewer-63TXMINB.js.map} +0 -0
  135. /package/dist/{chunk-UINDK6C3.js.map → chunk-2QUSQVF2.js.map} +0 -0
  136. /package/dist/{chunk-5EXGU7UF.js.map → chunk-DOJWJQ3F.js.map} +0 -0
  137. /package/dist/{chunk-BI57CQUV.js.map → chunk-I7IWRQLN.js.map} +0 -0
  138. /package/dist/{chunk-XA6R7DJQ.js.map → chunk-IQUCRAT7.js.map} +0 -0
  139. /package/dist/{computer-viewer-7GF35SNY.js.map → computer-viewer-X2MBRZY4.js.map} +0 -0
  140. /package/dist/{latest-question-3BJGKQQ6.js.map → latest-question-CNEZ5KAN.js.map} +0 -0
  141. /package/dist/{model-policy-picker-menu-PUTA5JOB.js.map → model-policy-picker-menu-JSA64TNN.js.map} +0 -0
  142. /package/dist/{sandbox-files-AEJTWGT4.js.map → sandbox-files-BBC47SLD.js.map} +0 -0
@@ -8,7 +8,10 @@ import type {
8
8
  ComputerTarget,
9
9
  } from "@opengeni/sdk/interaction";
10
10
  import { useCallback, useEffect, useRef, useState } from "react";
11
- import { isNonRetryableInteractionError } from "../lib/interaction-errors";
11
+ import {
12
+ isInteractionControlUnavailable,
13
+ isNonRetryableInteractionError,
14
+ } from "../lib/interaction-errors";
12
15
  import {
13
16
  type EmbeddedComputerInteractionClientOverride,
14
17
  useEmbeddedComputerInteraction,
@@ -29,6 +32,8 @@ export type UseComputerSessionResult = {
29
32
  loading: boolean;
30
33
  mutating: boolean;
31
34
  error: Error | null;
35
+ /** Control service loss, independent of target accessibility inspection and media. */
36
+ controlError: Error | null;
32
37
  refresh: () => Promise<void>;
33
38
  readClipboard: () => Promise<ComputerClipboard>;
34
39
  /** Changes only this viewer's target cursor; it never takes ownership or
@@ -105,6 +110,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
105
110
  requestRef.current.controller?.abort();
106
111
  const controller = new AbortController();
107
112
  requestRef.current = { id, controller };
113
+ let discoveryCompleted = false;
108
114
  try {
109
115
  const [session, targetResponse] = await Promise.all([
110
116
  client.getComputerSession(workspaceId, computerSessionId, {
@@ -114,6 +120,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
114
120
  signal: controller.signal,
115
121
  }),
116
122
  ]);
123
+ discoveryCompleted = true;
117
124
  if (!mountedRef.current || requestRef.current.id !== id) return;
118
125
  const targets = sortComputerTargets(targetResponse.targets);
119
126
  const selected = chooseTarget(targets, selectedTargetIdRef.current, session.platform);
@@ -141,6 +148,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
141
148
  observation: retainedObservation,
142
149
  loading: false,
143
150
  error: null,
151
+ controlError: null,
144
152
  }
145
153
  : current,
146
154
  );
@@ -168,12 +176,19 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
168
176
  );
169
177
  } catch (cause) {
170
178
  if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
179
+ const error = cause instanceof Error ? cause : new Error(String(cause));
171
180
  setState((current) =>
172
181
  current.computerSessionId === computerSessionId
173
182
  ? {
174
183
  ...current,
175
184
  loading: false,
176
- error: cause instanceof Error ? cause : new Error(String(cause)),
185
+ error,
186
+ controlError: isInteractionControlUnavailable(
187
+ error,
188
+ discoveryCompleted ? "observation" : "control",
189
+ )
190
+ ? error
191
+ : current.controlError,
177
192
  }
178
193
  : current,
179
194
  );
@@ -239,11 +254,23 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
239
254
  : current,
240
255
  );
241
256
  try {
242
- return await operation();
257
+ const result = await operation();
258
+ setState((current) =>
259
+ current.computerSessionId === scopeComputerSessionId
260
+ ? { ...current, controlError: null }
261
+ : current,
262
+ );
263
+ return result;
243
264
  } catch (cause) {
244
265
  const error = cause instanceof Error ? cause : new Error(String(cause));
245
266
  setState((current) =>
246
- current.computerSessionId === scopeComputerSessionId ? { ...current, error } : current,
267
+ current.computerSessionId === scopeComputerSessionId
268
+ ? {
269
+ ...current,
270
+ error,
271
+ controlError: isInteractionControlUnavailable(error) ? error : current.controlError,
272
+ }
273
+ : current,
247
274
  );
248
275
  throw error;
249
276
  } finally {
@@ -298,7 +325,14 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
298
325
  const error = cause instanceof Error ? cause : new Error(String(cause));
299
326
  setState((current) =>
300
327
  current.computerSessionId === computerSessionId && current.selectedTargetId === target.id
301
- ? { ...current, loading: false, error }
328
+ ? {
329
+ ...current,
330
+ loading: false,
331
+ error,
332
+ controlError: isInteractionControlUnavailable(error, "observation")
333
+ ? error
334
+ : current.controlError,
335
+ }
302
336
  : current,
303
337
  );
304
338
  throw error;
@@ -405,7 +439,21 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
405
439
 
406
440
  const readClipboard = useCallback(async (): Promise<ComputerClipboard> => {
407
441
  if (!computerSessionId) throw new Error("No desktop is selected.");
408
- return await client.readComputerClipboard(workspaceId, computerSessionId);
442
+ try {
443
+ return await client.readComputerClipboard(workspaceId, computerSessionId);
444
+ } catch (cause) {
445
+ const error = cause instanceof Error ? cause : new Error(String(cause));
446
+ setState((current) =>
447
+ current.computerSessionId === computerSessionId
448
+ ? {
449
+ ...current,
450
+ error,
451
+ controlError: isInteractionControlUnavailable(error) ? error : current.controlError,
452
+ }
453
+ : current,
454
+ );
455
+ throw error;
456
+ }
409
457
  }, [client, computerSessionId, workspaceId]);
410
458
 
411
459
  return {
@@ -417,6 +465,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
417
465
  loading: visible.loading,
418
466
  mutating: visible.mutating,
419
467
  error: visible.error,
468
+ controlError: visible.controlError,
420
469
  refresh,
421
470
  readClipboard,
422
471
  selectTarget,
@@ -434,6 +483,7 @@ type ComputerControlState = {
434
483
  loading: boolean;
435
484
  mutating: boolean;
436
485
  error: Error | null;
486
+ controlError: Error | null;
437
487
  };
438
488
 
439
489
  function emptyState(computerSessionId: string | null, loading: boolean): ComputerControlState {
@@ -446,6 +496,7 @@ function emptyState(computerSessionId: string | null, loading: boolean): Compute
446
496
  loading,
447
497
  mutating: false,
448
498
  error: null,
499
+ controlError: null,
449
500
  };
450
501
  }
451
502
 
@@ -4,6 +4,7 @@ import {
4
4
  useEmbeddedFileAttachments,
5
5
  type EmbeddedFileAttachmentClientOverride,
6
6
  } from "../session-context";
7
+ import { useErrorMessage } from "../lib/error-message";
7
8
 
8
9
  export type UseFileAttachmentsOptions = EmbeddedFileAttachmentClientOverride & {
9
10
  /**
@@ -31,6 +32,8 @@ export type FileAttachment = {
31
32
  /** Stable SDK failure code for UI behavior that must not parse error copy. */
32
33
  errorCode?: "secure_context_required" | undefined;
33
34
  error?: string | undefined;
35
+ /** Original diagnostic failure; presentation copy is kept separately in `error`. */
36
+ errorCause?: unknown;
34
37
  };
35
38
 
36
39
  export type UseFileAttachmentsResult = {
@@ -120,6 +123,7 @@ export function useFileAttachments(
120
123
  options: UseFileAttachmentsOptions = {},
121
124
  ): UseFileAttachmentsResult {
122
125
  const { client, workspaceId } = useEmbeddedFileAttachments(options);
126
+ const formatError = useErrorMessage();
123
127
  const pasteFilter = options.pasteFilter ?? isImage;
124
128
  const [attachments, setAttachments] = useState<FileAttachment[]>([]);
125
129
  const attachmentsRef = useRef(attachments);
@@ -234,6 +238,7 @@ export function useFileAttachments(
234
238
  sizeBytes: asset.sizeBytes,
235
239
  errorCode: undefined,
236
240
  error: undefined,
241
+ errorCause: undefined,
237
242
  }
238
243
  : attachment,
239
244
  ),
@@ -248,14 +253,15 @@ export function useFileAttachments(
248
253
  ...attachment,
249
254
  status: "failed",
250
255
  errorCode: secureContextRequiredErrorCode(error),
251
- error: error instanceof Error ? error.message : String(error),
256
+ error: formatError(error),
257
+ errorCause: error,
252
258
  }
253
259
  : attachment,
254
260
  ),
255
261
  );
256
262
  });
257
263
  },
258
- [client, workspaceId, options.scope],
264
+ [client, workspaceId, options.scope, formatError],
259
265
  );
260
266
 
261
267
  const addFiles = useCallback(
@@ -291,7 +297,13 @@ export function useFileAttachments(
291
297
  setAttachments((current) =>
292
298
  current.map((attachment) =>
293
299
  attachment.id === id
294
- ? { ...attachment, status: "uploading", errorCode: undefined, error: undefined }
300
+ ? {
301
+ ...attachment,
302
+ status: "uploading",
303
+ errorCode: undefined,
304
+ error: undefined,
305
+ errorCause: undefined,
306
+ }
295
307
  : attachment,
296
308
  ),
297
309
  );
@@ -343,6 +355,7 @@ export function useFileAttachments(
343
355
  file,
344
356
  errorCode: undefined,
345
357
  error: undefined,
358
+ errorCause: undefined,
346
359
  }
347
360
  : {
348
361
  id: `restored:${file.id}`,
@@ -1,6 +1,7 @@
1
1
  import { OpenGeniApiError, type SessionEvent, type SessionGoal } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { useEmbeddedGoal, type EmbeddedGoalClientOverride } from "../session-context";
4
+ import { normalizeError } from "../lib/error-message";
4
5
  import {
5
6
  useDebouncedCallback,
6
7
  useMutationRunner,
@@ -117,7 +118,7 @@ export function useGoal(
117
118
  setGoal(null);
118
119
  setError(null);
119
120
  } else {
120
- setError(cause instanceof Error ? cause : new Error(String(cause)));
121
+ setError(normalizeError(cause));
121
122
  }
122
123
  setLoading(false);
123
124
  } finally {
@@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
9
9
  import { useOpenGeni, type ClientOverride } from "../provider";
10
10
  import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
11
11
  import { terminalCanAcquirePty } from "../lib/terminal-capability";
12
+ import { normalizeError } from "../lib/error-message";
12
13
  import { usePageLiveActivity } from "./internal";
13
14
 
14
15
  // "on-demand" is the boxless-benign resting state: the lease is not currently
@@ -289,7 +290,7 @@ export function useSessionCapabilities(
289
290
  return;
290
291
  }
291
292
  setState("error");
292
- setError(cause instanceof Error ? cause : new Error(String(cause)));
293
+ setError(normalizeError(cause));
293
294
  }
294
295
  });
295
296
  }, warmingPollMs);
@@ -469,7 +470,7 @@ export function useSessionCapabilities(
469
470
  return;
470
471
  }
471
472
  setState("error");
472
- setError(cause instanceof Error ? cause : new Error(String(cause)));
473
+ setError(normalizeError(cause));
473
474
  }
474
475
  })();
475
476
 
@@ -10,6 +10,7 @@ import { createOlderHistoryLoadReceipt, type OlderHistoryLoadReceipt } from "../
10
10
  import { buildTimeline, groupTimeline, sessionStatusFromEvents } from "../timeline/projection";
11
11
  import type { TimelineItem } from "../timeline/types";
12
12
  import type { EmbeddedSessionClientLike } from "../client";
13
+ import { normalizeError } from "../lib/error-message";
13
14
  import { usePageLiveActivity } from "./internal";
14
15
  import type { LatestQuestionOptions } from "./latest-question";
15
16
 
@@ -114,8 +115,11 @@ const FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
114
115
  const FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
115
116
  const EMPTY_EVENTS: SessionEvent[] = [];
116
117
  const encoder = new TextEncoder();
117
- export const SESSION_EVENT_BROWSER_MAX_BYTES = 160 * 1024 * 1024;
118
- export const SESSION_EVENT_BROWSER_MAX_COUNT = 200_000;
118
+ // Durable history remains on the server and is available through bounded
119
+ // navigation. Keep the live browser working set small across multiple tabs;
120
+ // serialized payloads also expand into objects and timeline projections.
121
+ export const SESSION_EVENT_BROWSER_MAX_BYTES = 16 * 1024 * 1024;
122
+ export const SESSION_EVENT_BROWSER_MAX_COUNT = 20_000;
119
123
  export const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
120
124
  export const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
121
125
 
@@ -495,7 +499,7 @@ export function useSessionEvents(
495
499
  .then(() => reconcileSession(sessionId))
496
500
  .catch((cause) => {
497
501
  if (isCurrent()) {
498
- setError(cause instanceof Error ? cause : new Error(String(cause)));
502
+ setError(normalizeError(cause));
499
503
  }
500
504
  });
501
505
  },
@@ -536,7 +540,7 @@ export function useSessionEvents(
536
540
  } catch (cause) {
537
541
  if (isCurrent()) {
538
542
  flush();
539
- setError(cause instanceof Error ? cause : new Error(String(cause)));
543
+ setError(normalizeError(cause));
540
544
  setConnectionState("error");
541
545
  // A failed first compact-tail request has no later success path in
542
546
  // this effect instance. End the loading gate so hosts can render the
@@ -878,7 +882,7 @@ export function useSessionEvents(
878
882
  if (!isCurrent()) {
879
883
  return false;
880
884
  }
881
- setNewerError(reason instanceof Error ? reason : new Error(String(reason)));
885
+ setNewerError(normalizeError(reason));
882
886
  // Keep authorization and integrity failures actionable for callers;
883
887
  // timeline-owned invocations attach their own explicit recovery UI.
884
888
  throw reason;
@@ -1078,10 +1082,13 @@ export function useSessionEvents(
1078
1082
  visibleEvents.some((event) => event.sequence === questionEvidence.anchor)
1079
1083
  ? questionEvidence.events
1080
1084
  : EMPTY_EVENTS;
1085
+ const timelineOptions = { partialStart: hasOlder || eventWindow.truncated || after > 0 };
1086
+ if (witness.length === 0) return buildTimeline(visibleEvents, timelineOptions);
1081
1087
  const ids = new Set(visibleEvents.map((event) => event.id));
1082
- return buildTimeline([...visibleEvents, ...witness.filter((event) => !ids.has(event.id))], {
1083
- partialStart: hasOlder || eventWindow.truncated || after > 0,
1084
- });
1088
+ return buildTimeline(
1089
+ [...visibleEvents, ...witness.filter((event) => !ids.has(event.id))],
1090
+ timelineOptions,
1091
+ );
1085
1092
  }, [visibleEvents, hasOlder, eventWindow.truncated, after, questionEvidence, client, streamKey]);
1086
1093
 
1087
1094
  return {
@@ -1,5 +1,6 @@
1
1
  import type { KeyboardEvent } from "react";
2
2
  import { useCallback, useMemo, useRef, useState } from "react";
3
+ import { useErrorMessage } from "../lib/error-message";
3
4
  import {
4
5
  argHint,
5
6
  filterCommands,
@@ -81,6 +82,7 @@ export type UseSlashCommandsResult = {
81
82
  };
82
83
 
83
84
  export function useSlashCommands(options: UseSlashCommandsOptions): UseSlashCommandsResult {
85
+ const formatError = useErrorMessage();
84
86
  const { commands, context, handlers, value, setValue } = options;
85
87
  const [highlight, setHighlight] = useState(0);
86
88
  // Escape closes the palette but keeps the draft. We remember the dismissed
@@ -153,9 +155,9 @@ export function useSlashCommands(options: UseSlashCommandsOptions): UseSlashComm
153
155
  if (!context) {
154
156
  return null;
155
157
  }
156
- return { ...context, ...handlers, confirm: () => handlers.confirm(command) };
158
+ return { ...context, ...handlers, formatError, confirm: () => handlers.confirm(command) };
157
159
  },
158
- [context, handlers],
160
+ [context, handlers, formatError],
159
161
  );
160
162
 
161
163
  const execute = useCallback(
@@ -173,10 +175,10 @@ export function useSlashCommands(options: UseSlashCommandsOptions): UseSlashComm
173
175
  setValue("");
174
176
  }
175
177
  } catch (cause) {
176
- ctx.notice({ tone: "error", message: errorMessage(cause) });
178
+ ctx.notice({ tone: "error", message: formatError(cause) });
177
179
  }
178
180
  },
179
- [buildContext, setValue],
181
+ [buildContext, setValue, formatError],
180
182
  );
181
183
 
182
184
  const autocomplete = useCallback(
@@ -359,10 +361,3 @@ export function useSlashCommands(options: UseSlashCommandsOptions): UseSlashComm
359
361
  autocompleteHighlighted,
360
362
  };
361
363
  }
362
-
363
- function errorMessage(cause: unknown): string {
364
- if (cause instanceof Error) {
365
- return cause.message;
366
- }
367
- return String(cause);
368
- }
@@ -10,6 +10,7 @@ import type {
10
10
  } from "@opengeni/sdk";
11
11
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
12
12
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
13
+ import { normalizeError } from "../lib/error-message";
13
14
  import {
14
15
  useDebouncedCallback,
15
16
  usePageLiveActivity,
@@ -463,7 +464,7 @@ function operationKey(): string {
463
464
  }
464
465
 
465
466
  function asError(cause: unknown): Error {
466
- return cause instanceof Error ? cause : new Error(String(cause));
467
+ return normalizeError(cause);
467
468
  }
468
469
 
469
470
  function isOutcomeUnknownError(cause: unknown): boolean {
@@ -1,15 +1,19 @@
1
- import type { Session } from "@opengeni/sdk";
1
+ import type { Session, SessionListEntry } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useRef } from "react";
3
3
  import { useOpenGeni, type ClientOverride } from "../provider";
4
4
  import { usePolledValue } from "./internal";
5
5
 
6
6
  export type UseWorkspaceSessionsOptions = ClientOverride & {
7
+ /** Compact list records; detail reads remain full sessions. */
8
+ projection?: "summary" | undefined;
7
9
  limit?: number | undefined;
8
10
  parentSessionId?: string | null | undefined;
9
11
  cursor?: string | undefined;
10
12
  search?: string | undefined;
11
13
  /** Return only the complete personal pinned projection. */
12
14
  pinsOnly?: boolean | undefined;
15
+ /** Skip pinned details when the pinned section is loaded separately. */
16
+ includePinned?: boolean | undefined;
13
17
  archivedOnly?: boolean | undefined;
14
18
  sortBy?: "updatedAt" | "createdAt" | "name" | undefined;
15
19
  archiveStatus?: "active" | "archived" | "all" | undefined;
@@ -20,14 +24,14 @@ export type UseWorkspaceSessionsOptions = ClientOverride & {
20
24
  beginRead?: (() => number) | undefined;
21
25
  };
22
26
 
23
- export type UseWorkspaceSessionsResult = {
27
+ export type UseWorkspaceSessionsResult<T extends Session | SessionListEntry = Session> = {
24
28
  /**
25
29
  * All visible rows, with pins first. This preserves the pre-pinning hook
26
30
  * contract for consumers that only read `sessions`.
27
31
  */
28
- sessions: Session[];
32
+ sessions: T[];
29
33
  /** The complete personal pinned section, also present in `sessions`. */
30
- pinned: Session[];
34
+ pinned: T[];
31
35
  /** True when the server omitted older pins from its bounded pinned section. */
32
36
  pinnedTruncated: boolean;
33
37
  nextCursor: string | null;
@@ -41,15 +45,26 @@ export type UseWorkspaceSessionsResult = {
41
45
  };
42
46
 
43
47
  /** List the workspace's sessions — the data behind fleet and manager views. */
48
+ export function useWorkspaceSessions(
49
+ options: UseWorkspaceSessionsOptions & { projection: "summary" },
50
+ ): UseWorkspaceSessionsResult<SessionListEntry>;
51
+ export function useWorkspaceSessions(
52
+ options?: UseWorkspaceSessionsOptions & { projection?: undefined },
53
+ ): UseWorkspaceSessionsResult;
54
+ export function useWorkspaceSessions(
55
+ options: UseWorkspaceSessionsOptions,
56
+ ): UseWorkspaceSessionsResult<Session | SessionListEntry>;
44
57
  export function useWorkspaceSessions(
45
58
  options: UseWorkspaceSessionsOptions = {},
46
- ): UseWorkspaceSessionsResult {
59
+ ): UseWorkspaceSessionsResult<Session | SessionListEntry> {
47
60
  const { client, workspaceId } = useOpenGeni(options);
61
+ const projection = options.projection;
48
62
  const limit = options.limit;
49
63
  const parentSessionId = options.parentSessionId;
50
64
  const cursor = options.cursor;
51
65
  const search = options.search;
52
66
  const pinsOnly = options.pinsOnly;
67
+ const includePinned = options.includePinned;
53
68
  const archivedOnly = options.archivedOnly;
54
69
  const sortBy = options.sortBy;
55
70
  const archiveStatus = options.archiveStatus;
@@ -59,11 +74,13 @@ export function useWorkspaceSessions(
59
74
  const beginRead = options.beginRead;
60
75
  const queryKey = [
61
76
  workspaceId,
77
+ projection ?? "full",
62
78
  limit ?? "",
63
79
  parentSessionId === null ? "null" : (parentSessionId ?? ""),
64
80
  cursor ?? "",
65
81
  search ?? "",
66
82
  pinsOnly ? "1" : "",
83
+ includePinned === false ? "no-pins" : "",
67
84
  archivedOnly ? "archived" : "active",
68
85
  sortBy ?? "",
69
86
  archiveStatus ?? "",
@@ -76,17 +93,31 @@ export function useWorkspaceSessions(
76
93
  const load = useCallback(
77
94
  async (signal?: AbortSignal) => {
78
95
  const readGeneration = beginRead?.() ?? ++nextReadGeneration.current;
79
- const page = await client.listSessionPage(workspaceId, {
96
+ const query = {
80
97
  ...(limit !== undefined ? { limit } : {}),
81
98
  ...(parentSessionId !== undefined ? { parentSessionId } : {}),
82
99
  ...(cursor !== undefined ? { cursor } : {}),
83
100
  ...(search !== undefined ? { search } : {}),
84
101
  ...(pinsOnly ? { pinsOnly: true } : {}),
102
+ ...(includePinned === false ? { includePinned: false } : {}),
85
103
  ...(archivedOnly ? { archivedOnly: true } : {}),
86
104
  ...(sortBy ? { sortBy } : {}),
87
105
  ...(archiveStatus ? { archiveStatus } : {}),
88
106
  signal,
89
- });
107
+ };
108
+ const page =
109
+ projection === "summary"
110
+ ? client.listSessionSummaryPage
111
+ ? await client.listSessionSummaryPage(workspaceId, query)
112
+ : await client.listSessionPage(workspaceId, query).then(async (full) => {
113
+ const { sessionListEntry } = await import("@opengeni/sdk/session-list-entries");
114
+ return {
115
+ ...full,
116
+ pinned: full.pinned.map(sessionListEntry),
117
+ sessions: full.sessions.map(sessionListEntry),
118
+ };
119
+ })
120
+ : await client.listSessionPage(workspaceId, query);
90
121
  return {
91
122
  queryKey,
92
123
  page,
@@ -98,11 +129,13 @@ export function useWorkspaceSessions(
98
129
  beginRead,
99
130
  client,
100
131
  workspaceId,
132
+ projection,
101
133
  limit,
102
134
  parentSessionId,
103
135
  cursor,
104
136
  search,
105
137
  pinsOnly,
138
+ includePinned,
106
139
  archivedOnly,
107
140
  sortBy,
108
141
  archiveStatus,
package/src/index.ts CHANGED
@@ -43,7 +43,7 @@ export type {
43
43
  SessionClientLike,
44
44
  } from "./client";
45
45
  export { OpenGeniProvider } from "./provider";
46
- export type { OpenGeniProviderProps } from "./provider";
46
+ export type { OpenGeniProviderProps, ErrorMessageFormatter } from "./provider";
47
47
  export { useOpenGeni, useOpenGeniClient } from "./session-context";
48
48
  export { useVideoArtifactPlaybackLoader } from "./hooks/use-video-artifact-playback";
49
49
  export type { ClientOverride, OpenGeniContextValue } from "./session-context";
@@ -130,7 +130,12 @@ export type {
130
130
  VoiceRecordingTranscriptionState,
131
131
  VoiceRecordingUploadState,
132
132
  } from "./voice-recording-store";
133
- export { COMPOSER_PAYMENT_REQUIRED_MESSAGE, composerSubmissionErrorMessage } from "./lib/format";
133
+ export {
134
+ COMPOSER_MODEL_UNAVAILABLE_MESSAGE,
135
+ COMPOSER_PAYMENT_REQUIRED_MESSAGE,
136
+ composerSubmissionErrorMessage,
137
+ isModelUnavailableSubmissionError,
138
+ } from "./lib/format";
134
139
  export {
135
140
  INITIAL_TRANSCRIPTION_CONTROL_STATE,
136
141
  appendFinalTranscript,
@@ -58,6 +58,7 @@ export type {
58
58
  } from "./hooks/use-browser-identities";
59
59
  export { useBrowserSession } from "./hooks/use-browser-session";
60
60
  export type {
61
+ BrowserInputFailure,
61
62
  UseBrowserSessionOptions,
62
63
  UseBrowserSessionResult,
63
64
  } from "./hooks/use-browser-session";
@@ -0,0 +1,37 @@
1
+ import { formatErrorMessage } from "@opengeni/sdk";
2
+ import { createContext, useCallback, useContext, useRef } from "react";
3
+
4
+ /** Original diagnostic error and the library's neutral, state-aware display copy. */
5
+ export type ErrorMessageFormatter = (error: unknown, defaultMessage: string) => string | undefined;
6
+
7
+ export const ErrorMessageContext = createContext<ErrorMessageFormatter | undefined>(undefined);
8
+
9
+ const originalCauses = new WeakMap<Error, unknown>();
10
+
11
+ /** Keep existing Error-typed state while retaining non-Error rejections privately. */
12
+ export function normalizeError(cause: unknown): Error {
13
+ if (cause instanceof Error) return cause;
14
+ const error = new Error(String(cause));
15
+ originalCauses.set(error, cause);
16
+ return error;
17
+ }
18
+
19
+ /** Presentation only: callers retain their errors and delivery/retry state. */
20
+ export function useErrorMessage(): (error: unknown, defaultMessage?: string) => string {
21
+ const formatter = useContext(ErrorMessageContext);
22
+ const formatterRef = useRef(formatter);
23
+ formatterRef.current = formatter;
24
+ return useCallback((error, fallback) => {
25
+ const original =
26
+ error instanceof Error && originalCauses.has(error) ? originalCauses.get(error) : error;
27
+ const defaultMessage = fallback ?? formatErrorMessage(original);
28
+ try {
29
+ const message = formatterRef.current?.(original, defaultMessage);
30
+ return typeof message === "string" && message ? message : defaultMessage;
31
+ } catch {
32
+ // Host presentation must never interrupt delivery-state settlement or
33
+ // cause an already-started mutation to be retried.
34
+ return defaultMessage;
35
+ }
36
+ }, []);
37
+ }