@opengeni/react 7.6.1 → 7.7.0-canary.37111190284001

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 (145) hide show
  1. package/README.md +47 -2
  2. package/dist/{browser-viewer-5X47I6T2.js → browser-viewer-63TXMINB.js} +6 -5
  3. package/dist/{chunk-5EXGU7UF.js → chunk-55DDDVKU.js} +3 -3
  4. package/dist/{chunk-UINDK6C3.js → chunk-7CYYXWHU.js} +5 -8
  5. package/dist/chunk-7CYYXWHU.js.map +1 -0
  6. package/dist/{chunk-LZ3KHA55.js → chunk-A2L5N3EI.js} +332 -339
  7. package/dist/chunk-A2L5N3EI.js.map +1 -0
  8. package/dist/{chunk-KGENSZKS.js → chunk-AAG4R3QN.js} +4 -3
  9. package/dist/chunk-AAG4R3QN.js.map +1 -0
  10. package/dist/{chunk-Z3DISEQN.js → chunk-CWWKTNKK.js} +35 -18
  11. package/dist/chunk-CWWKTNKK.js.map +1 -0
  12. package/dist/{chunk-WZ55DVGK.js → chunk-ELCE5SXV.js} +8 -4
  13. package/dist/chunk-ELCE5SXV.js.map +1 -0
  14. package/dist/{chunk-YSMMDOOE.js → chunk-GISEVGS6.js} +10 -2
  15. package/dist/chunk-GISEVGS6.js.map +1 -0
  16. package/dist/{chunk-BI57CQUV.js → chunk-I7IWRQLN.js} +2 -2
  17. package/dist/{chunk-XA6R7DJQ.js → chunk-IQUCRAT7.js} +3 -3
  18. package/dist/{chunk-CHPKSLPN.js → chunk-NBZ64YY7.js} +4 -2
  19. package/dist/chunk-NBZ64YY7.js.map +1 -0
  20. package/dist/{chunk-DV2DBNPQ.js → chunk-Q5QBEZIQ.js} +48 -29
  21. package/dist/chunk-Q5QBEZIQ.js.map +1 -0
  22. package/dist/{chunk-FGBCYQOT.js → chunk-RDKBXMWX.js} +120 -74
  23. package/dist/chunk-RDKBXMWX.js.map +1 -0
  24. package/dist/{chunk-45GE35O7.js → chunk-TKB4SB44.js} +10 -2
  25. package/dist/chunk-TKB4SB44.js.map +1 -0
  26. package/dist/{chunk-YCGBREUV.js → chunk-TOAH2BKA.js} +6 -2
  27. package/dist/{chunk-YCGBREUV.js.map → chunk-TOAH2BKA.js.map} +1 -1
  28. package/dist/chunk-UGLEYEMP.js +33 -0
  29. package/dist/chunk-UGLEYEMP.js.map +1 -0
  30. package/dist/{chunk-UJRYEWZI.js → chunk-UTRTG6LH.js} +48 -8
  31. package/dist/chunk-UTRTG6LH.js.map +1 -0
  32. package/dist/{chunk-YRYF2GLG.js → chunk-WYVLAAJA.js} +39 -31
  33. package/dist/chunk-WYVLAAJA.js.map +1 -0
  34. package/dist/{chunk-VGIKFIO2.js → chunk-XOAMRDDK.js} +6 -3
  35. package/dist/chunk-XOAMRDDK.js.map +1 -0
  36. package/dist/{chunk-6NEZPF53.js → chunk-YN7I3STW.js} +153 -32
  37. package/dist/chunk-YN7I3STW.js.map +1 -0
  38. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  39. package/dist/chunk-YQEAA6LT.js.map +1 -0
  40. package/dist/client.d.ts +4 -1
  41. package/dist/commands/types.d.ts +2 -0
  42. package/dist/components/approval-surface.d.ts +15 -0
  43. package/dist/components/session-conversation.d.ts +3 -0
  44. package/dist/composer.js +8 -7
  45. package/dist/{computer-viewer-7GF35SNY.js → computer-viewer-X2MBRZY4.js} +5 -4
  46. package/dist/connect.js +3 -3
  47. package/dist/hooks/use-browser-session.d.ts +4 -0
  48. package/dist/hooks/use-composer.d.ts +2 -0
  49. package/dist/hooks/use-computer-session.d.ts +2 -0
  50. package/dist/hooks/use-file-attachments.d.ts +2 -0
  51. package/dist/hooks/use-session-events.d.ts +1 -1
  52. package/dist/hooks/use-workspace-sessions.d.ts +15 -5
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +28 -21
  55. package/dist/index.js.map +1 -1
  56. package/dist/interaction.d.ts +1 -1
  57. package/dist/interaction.js +6 -5
  58. package/dist/interaction.js.map +1 -1
  59. package/dist/{latest-question-3BJGKQQ6.js → latest-question-CNEZ5KAN.js} +3 -3
  60. package/dist/lib/error-message.d.ts +7 -0
  61. package/dist/lib/format.d.ts +27 -3
  62. package/dist/lib/interaction-errors.d.ts +3 -0
  63. package/dist/machines.js +5 -4
  64. package/dist/{model-policy-picker-menu-PUTA5JOB.js → model-policy-picker-menu-GN2LTB7Z.js} +4 -4
  65. package/dist/model-policy.js +1 -1
  66. package/dist/provider.d.ts +5 -1
  67. package/dist/{queue-surface-implementation-2CAQAIHM.js → queue-surface-implementation-HENWYFWA.js} +3 -2
  68. package/dist/{queue-surface-implementation-2CAQAIHM.js.map → queue-surface-implementation-HENWYFWA.js.map} +1 -1
  69. package/dist/realtime.js +2 -2
  70. package/dist/{sandbox-files-AEJTWGT4.js → sandbox-files-BBC47SLD.js} +4 -3
  71. package/dist/session-ui.d.ts +1 -1
  72. package/dist/session-ui.js +13 -12
  73. package/dist/session-ui.js.map +1 -1
  74. package/dist/session.js +7 -6
  75. package/dist/usage.js +2 -1
  76. package/dist/usage.js.map +1 -1
  77. package/package.json +3 -3
  78. package/src/client.ts +5 -1
  79. package/src/commands/registry.ts +18 -15
  80. package/src/commands/types.ts +2 -0
  81. package/src/components/approval-surface.tsx +166 -42
  82. package/src/components/browser-viewer.tsx +38 -9
  83. package/src/components/composer.tsx +9 -2
  84. package/src/components/computer-viewer.tsx +160 -30
  85. package/src/components/generated-video-player.tsx +4 -2
  86. package/src/components/human-input-form.tsx +18 -5
  87. package/src/components/message-timeline.tsx +120 -168
  88. package/src/components/model-policy-picker.tsx +2 -15
  89. package/src/components/open-geni-chat.tsx +11 -4
  90. package/src/components/queue-surface-state.tsx +3 -1
  91. package/src/components/session-chrome.tsx +5 -2
  92. package/src/components/session-commands-panel.tsx +3 -1
  93. package/src/components/session-conversation.tsx +10 -3
  94. package/src/components/session-list.tsx +7 -5
  95. package/src/components/session-mcp-capability-card.tsx +11 -1
  96. package/src/hooks/internal.ts +3 -2
  97. package/src/hooks/use-browser-session.ts +115 -79
  98. package/src/hooks/use-composer.ts +70 -31
  99. package/src/hooks/use-computer-session.ts +57 -6
  100. package/src/hooks/use-file-attachments.ts +16 -3
  101. package/src/hooks/use-goal.ts +2 -1
  102. package/src/hooks/use-session-capabilities.ts +3 -2
  103. package/src/hooks/use-session-events.ts +15 -8
  104. package/src/hooks/use-slash-commands.ts +6 -11
  105. package/src/hooks/use-turn-queue.ts +2 -1
  106. package/src/hooks/use-workspace-sessions.ts +40 -7
  107. package/src/index.ts +7 -2
  108. package/src/interaction.ts +1 -0
  109. package/src/lib/error-message.ts +37 -0
  110. package/src/lib/format.ts +73 -9
  111. package/src/lib/interaction-errors.ts +15 -0
  112. package/src/model-policy.ts +6 -1
  113. package/src/provider.tsx +12 -4
  114. package/src/session-ui.ts +1 -1
  115. package/src/timeline/projection.ts +12 -1
  116. package/styles/compiled.css +14 -30
  117. package/dist/chunk-45GE35O7.js.map +0 -1
  118. package/dist/chunk-6NEZPF53.js.map +0 -1
  119. package/dist/chunk-CHPKSLPN.js.map +0 -1
  120. package/dist/chunk-DV2DBNPQ.js.map +0 -1
  121. package/dist/chunk-FGBCYQOT.js.map +0 -1
  122. package/dist/chunk-KGENSZKS.js.map +0 -1
  123. package/dist/chunk-KZ73RL76.js.map +0 -1
  124. package/dist/chunk-LZ3KHA55.js.map +0 -1
  125. package/dist/chunk-UINDK6C3.js.map +0 -1
  126. package/dist/chunk-UJRYEWZI.js.map +0 -1
  127. package/dist/chunk-VGIKFIO2.js.map +0 -1
  128. package/dist/chunk-WZ55DVGK.js.map +0 -1
  129. package/dist/chunk-YRYF2GLG.js.map +0 -1
  130. package/dist/chunk-YSMMDOOE.js.map +0 -1
  131. package/dist/chunk-Z3DISEQN.js.map +0 -1
  132. package/dist/components/timeline-pill-geometry.d.ts +0 -8
  133. package/dist/components/timeline-pill-placement.d.ts +0 -24
  134. package/dist/timeline-pill-geometry-Z23V243M.js +0 -110
  135. package/dist/timeline-pill-geometry-Z23V243M.js.map +0 -1
  136. package/src/components/timeline-pill-geometry.ts +0 -169
  137. package/src/components/timeline-pill-placement.ts +0 -99
  138. /package/dist/{browser-viewer-5X47I6T2.js.map → browser-viewer-63TXMINB.js.map} +0 -0
  139. /package/dist/{chunk-5EXGU7UF.js.map → chunk-55DDDVKU.js.map} +0 -0
  140. /package/dist/{chunk-BI57CQUV.js.map → chunk-I7IWRQLN.js.map} +0 -0
  141. /package/dist/{chunk-XA6R7DJQ.js.map → chunk-IQUCRAT7.js.map} +0 -0
  142. /package/dist/{computer-viewer-7GF35SNY.js.map → computer-viewer-X2MBRZY4.js.map} +0 -0
  143. /package/dist/{latest-question-3BJGKQQ6.js.map → latest-question-CNEZ5KAN.js.map} +0 -0
  144. /package/dist/{model-policy-picker-menu-PUTA5JOB.js.map → model-policy-picker-menu-GN2LTB7Z.js.map} +0 -0
  145. /package/dist/{sandbox-files-AEJTWGT4.js.map → sandbox-files-BBC47SLD.js.map} +0 -0
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState, type FormEvent } from "react";
4
4
  import { useWorkspaceSessions } from "../hooks/use-workspace-sessions";
5
5
  import { cn } from "../lib/cn";
6
6
  import { formatRelativeTime } from "../lib/format";
7
+ import { useErrorMessage } from "../lib/error-message";
7
8
  import { useOpenGeni, type ClientOverride } from "../session-context";
8
9
 
9
10
  export type SessionListLabels = {
@@ -90,7 +91,8 @@ export function SessionList({
90
91
  }, [refresh, refreshKey]);
91
92
  const [editing, setEditing] = useState<{ id: string; title: string } | null>(null);
92
93
  const [busy, setBusy] = useState<string | null>(null);
93
- const [error, setError] = useState<string | null>(null);
94
+ const [error, setError] = useState<{ cause: unknown } | null>(null);
95
+ const formatError = useErrorMessage();
94
96
  const archiveClient = context.client as unknown as ArchiveClient;
95
97
  const canArchive = archive && typeof archiveClient.updateSessionArchive === "function";
96
98
 
@@ -101,7 +103,7 @@ export function SessionList({
101
103
  await action();
102
104
  await list.refresh();
103
105
  } catch (cause) {
104
- setError(cause instanceof Error ? cause.message : String(cause));
106
+ setError({ cause });
105
107
  } finally {
106
108
  setBusy(null);
107
109
  }
@@ -148,16 +150,16 @@ export function SessionList({
148
150
  </button>
149
151
  ) : null}
150
152
  </div>
151
- {error ? (
153
+ {error || list.error ? (
152
154
  <p role="alert" className="px-2 text-og-xs text-og-status-failed">
153
- {error}
155
+ {formatError(error ? error.cause : list.error)}
154
156
  </p>
155
157
  ) : null}
156
158
  <ul className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-1 pb-2">
157
159
  {list.loading && sessions.length === 0 ? (
158
160
  <li className="px-2 py-2 text-og-xs text-og-fg-subtle">{labels.loading}</li>
159
161
  ) : null}
160
- {!list.loading && sessions.length === 0 ? (
162
+ {!list.loading && !list.error && sessions.length === 0 ? (
161
163
  <li className="px-2 py-2 text-og-xs text-og-fg-subtle">{labels.empty}</li>
162
164
  ) : null}
163
165
  {sessions.map((session) => {
@@ -127,7 +127,17 @@ function ScopedCard({
127
127
  ]);
128
128
  if (!invocation || invocation.signal.aborted || lifetime.current !== invocation || !active())
129
129
  return null;
130
- const resolved = catalog.items.find((entry) => entry.id === capabilityId);
130
+ let resolved = catalog.items.find((entry) => entry.id === capabilityId);
131
+ // Custom catalog entries may not declare authentication. Discover it from
132
+ // the live catalog's endpoint, never from recommendation/host copy.
133
+ if (resolved?.kind === "mcp" && !resolved.enabled && !resolved.authKind) {
134
+ const endpoint = resolved.mcpUrl ?? resolved.endpointUrl;
135
+ if (endpoint) {
136
+ const inspected = await client.inspectMcpAuthentication(workspaceId, endpoint);
137
+ if (invocation.signal.aborted || lifetime.current !== invocation || !active()) return null;
138
+ if (inspected.kind === "oauth2") resolved = { ...resolved, authKind: "oauth2" };
139
+ }
140
+ }
131
141
  if (!resolved || resolved.kind !== "mcp" || resolved.authKind !== "oauth2")
132
142
  throw new Error(
133
143
  "This recommendation is not an available OAuth MCP integration. Review the current connection catalog.",
@@ -1,6 +1,7 @@
1
1
  import type { SessionEvent } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import type { EmbeddedSessionEventClientLike } from "../client";
4
+ import { normalizeError } from "../lib/error-message";
4
5
 
5
6
  const HIDDEN_STREAM_GRACE_MS = 2_000;
6
7
 
@@ -116,7 +117,7 @@ export function usePolledValue<T>(
116
117
  activeLoadRef.current === load &&
117
118
  !requestAbort.signal.aborted
118
119
  ) {
119
- setError(cause instanceof Error ? cause : new Error(String(cause)));
120
+ setError(normalizeError(cause));
120
121
  setLoading(false);
121
122
  }
122
123
  } finally {
@@ -258,7 +259,7 @@ export function useMutationRunner(identity: unknown = undefined): MutationState
258
259
  generation.current === ownedGeneration &&
259
260
  Object.is(identityRef.current, ownedIdentity)
260
261
  ) {
261
- setMutationError(cause instanceof Error ? cause : new Error(String(cause)));
262
+ setMutationError(normalizeError(cause));
262
263
  }
263
264
  return null;
264
265
  } finally {
@@ -30,6 +30,10 @@ export type BrowserFrameInputFence = Pick<
30
30
  | "frameId"
31
31
  >;
32
32
 
33
+ export type BrowserInputFailure = Pick<BrowserActionReceipt, "operationId" | "error"> & {
34
+ state: Exclude<BrowserActionReceipt["state"], "completed">;
35
+ };
36
+
33
37
  export type UseBrowserSessionOptions = EmbeddedBrowserInteractionClientOverride & {
34
38
  browserSessionId: string | null;
35
39
  enabled?: boolean | undefined;
@@ -47,6 +51,7 @@ export type UseBrowserSessionResult = {
47
51
  loading: boolean;
48
52
  mutating: boolean;
49
53
  error: Error | null;
54
+ inputFailure: BrowserInputFailure | null;
50
55
  refresh: () => Promise<void>;
51
56
  observeForInput: () => Promise<BrowserObservation>;
52
57
  actFromObservation: (
@@ -96,10 +101,13 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
96
101
  loading: boolean;
97
102
  mutating: boolean;
98
103
  error: Error | null;
104
+ inputFailure: BrowserInputFailure | null;
99
105
  }>(() => emptyState(browserSessionId, enabled));
100
106
  const visible =
101
107
  state.browserSessionId === browserSessionId ? state : emptyState(browserSessionId, enabled);
102
108
  const refreshBlocked = isNonRetryableInteractionError(visible.error);
109
+ const inputFailureRef = useRef(visible.inputFailure);
110
+ inputFailureRef.current = visible.inputFailure;
103
111
  const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
104
112
  selectedTargetIdRef.current = visible.selectedTargetId;
105
113
  const selectedTargetRef = useRef<BrowserTarget | null>(
@@ -139,88 +147,104 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
139
147
  requestRef.current = { id, controller: null };
140
148
  }, []);
141
149
 
142
- const refresh = useCallback(async (): Promise<void> => {
143
- if (!enabled || !browserSessionId) return;
144
- const id = requestRef.current.id + 1;
145
- requestRef.current.controller?.abort();
146
- const controller = new AbortController();
147
- requestRef.current = { id, controller };
148
- try {
149
- const [session, targetResponse] = await Promise.all([
150
- client.getBrowserSession(workspaceId, browserSessionId, {
151
- signal: controller.signal,
152
- }),
153
- client.listBrowserTargets(workspaceId, browserSessionId, {
154
- signal: controller.signal,
155
- }),
156
- ]);
157
- if (!mountedRef.current || requestRef.current.id !== id) return;
158
- const selected = chooseTarget(targetResponse.targets, selectedTargetIdRef.current);
159
- selectedTargetIdRef.current = selected?.id ?? null;
160
- const previousObservation =
161
- observationRef.current.browserSessionId === browserSessionId
162
- ? observationRef.current.observation
163
- : null;
164
- const retainedObservation =
165
- selected && previousObservation && sameObservationTarget(previousObservation, selected)
166
- ? previousObservation
167
- : null;
168
- observationRef.current = { browserSessionId, observation: retainedObservation };
169
- setState((current) =>
170
- current.browserSessionId === browserSessionId
171
- ? {
172
- ...current,
173
- session,
174
- targets: targetResponse.targets,
175
- selectedTargetId: selected?.id ?? null,
176
- observation: retainedObservation,
177
- loading: false,
178
- error: null,
179
- }
180
- : current,
181
- );
182
- if (!selected || !semanticObservationEnabledRef.current) return;
150
+ const refreshInventory = useCallback(
151
+ async (checkInput = false) => {
152
+ if (!enabled || !browserSessionId) return;
153
+ const checkedInputFailure = inputFailureRef.current;
154
+ const id = requestRef.current.id + 1;
155
+ requestRef.current.controller?.abort();
156
+ const controller = new AbortController();
157
+ requestRef.current = { id, controller };
183
158
  try {
184
- const observation = await client.observeBrowserTarget(
185
- workspaceId,
186
- browserSessionId,
187
- selected.id,
188
- { signal: controller.signal },
189
- );
159
+ const [session, targetResponse] = await Promise.all([
160
+ client.getBrowserSession(workspaceId, browserSessionId, {
161
+ signal: controller.signal,
162
+ }),
163
+ client.listBrowserTargets(workspaceId, browserSessionId, {
164
+ signal: controller.signal,
165
+ }),
166
+ ]);
190
167
  if (!mountedRef.current || requestRef.current.id !== id) return;
191
- observationRef.current = { browserSessionId, observation };
192
- lastObservationAtRef.current = Date.now();
168
+ const selected = chooseTarget(targetResponse.targets, selectedTargetIdRef.current);
169
+ selectedTargetIdRef.current = selected?.id ?? null;
170
+ const previousObservation =
171
+ observationRef.current.browserSessionId === browserSessionId
172
+ ? observationRef.current.observation
173
+ : null;
174
+ const retainedObservation =
175
+ selected && previousObservation && sameObservationTarget(previousObservation, selected)
176
+ ? previousObservation
177
+ : null;
178
+ observationRef.current = { browserSessionId, observation: retainedObservation };
193
179
  setState((current) =>
194
- current.browserSessionId === browserSessionId &&
195
- current.selectedTargetId === observation.target.id
196
- ? { ...current, observation }
180
+ current.browserSessionId === browserSessionId
181
+ ? {
182
+ ...current,
183
+ session,
184
+ targets: targetResponse.targets,
185
+ selectedTargetId: selected?.id ?? null,
186
+ observation: retainedObservation,
187
+ loading: false,
188
+ error: null,
189
+ // Media and background polling cannot confirm an input outcome.
190
+ // Only an explicit fresh check clears the notice it began with.
191
+ inputFailure:
192
+ checkInput &&
193
+ session.id === browserSessionId &&
194
+ targetResponse.browserSessionId === browserSessionId &&
195
+ current.inputFailure === checkedInputFailure
196
+ ? null
197
+ : current.inputFailure,
198
+ }
197
199
  : current,
198
200
  );
201
+ if (!selected || !semanticObservationEnabledRef.current) return;
202
+ try {
203
+ const observation = await client.observeBrowserTarget(
204
+ workspaceId,
205
+ browserSessionId,
206
+ selected.id,
207
+ { signal: controller.signal },
208
+ );
209
+ if (!mountedRef.current || requestRef.current.id !== id) return;
210
+ observationRef.current = { browserSessionId, observation };
211
+ lastObservationAtRef.current = Date.now();
212
+ setState((current) =>
213
+ current.browserSessionId === browserSessionId &&
214
+ current.selectedTargetId === observation.target.id
215
+ ? { ...current, observation }
216
+ : current,
217
+ );
218
+ } catch (cause) {
219
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
220
+ return;
221
+ // Target inventory and the media plane remain authoritative for the
222
+ // human-facing browser. A failed semantic snapshot must not blank or
223
+ // disable an otherwise live visual browser.
224
+ void cause;
225
+ }
199
226
  } catch (cause) {
200
227
  if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
201
228
  return;
202
- // Target inventory and the media plane remain authoritative for the
203
- // human-facing browser. A failed semantic snapshot must not blank or
204
- // disable an otherwise live visual browser.
205
- void cause;
206
- }
207
- } catch (cause) {
208
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
209
- setState((current) =>
210
- current.browserSessionId === browserSessionId
211
- ? {
212
- ...current,
213
- loading: false,
214
- error: cause instanceof Error ? cause : new Error(String(cause)),
215
- }
216
- : current,
217
- );
218
- } finally {
219
- if (requestRef.current.id === id) {
220
- requestRef.current = { id, controller: null };
229
+ setState((current) =>
230
+ current.browserSessionId === browserSessionId
231
+ ? {
232
+ ...current,
233
+ loading: false,
234
+ error: cause instanceof Error ? cause : new Error(String(cause)),
235
+ }
236
+ : current,
237
+ );
238
+ } finally {
239
+ if (requestRef.current.id === id) {
240
+ requestRef.current = { id, controller: null };
241
+ }
221
242
  }
222
- }
223
- }, [browserSessionId, client, enabled, workspaceId]);
243
+ },
244
+ [browserSessionId, client, enabled, workspaceId],
245
+ );
246
+
247
+ const refresh = useCallback(async () => await refreshInventory(true), [refreshInventory]);
224
248
 
225
249
  const refreshSemantic = useCallback(async (): Promise<void> => {
226
250
  if (!enabled || !browserSessionId) return;
@@ -265,12 +289,12 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
265
289
  ? { ...current, loading: true }
266
290
  : emptyState(browserSessionId, true),
267
291
  );
268
- void refresh();
292
+ void refreshInventory();
269
293
  return () => {
270
294
  mountedRef.current = false;
271
295
  invalidateRefresh();
272
296
  };
273
- }, [browserSessionId, enabled, invalidateRefresh, refresh]);
297
+ }, [browserSessionId, enabled, invalidateRefresh, refreshInventory]);
274
298
 
275
299
  useEffect(() => {
276
300
  const previous = previousSemanticObservationRef.current;
@@ -306,17 +330,17 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
306
330
  useEffect(() => {
307
331
  if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
308
332
  const timer = setInterval(() => {
309
- if (!requestRef.current.controller) void refresh();
333
+ if (!requestRef.current.controller) void refreshInventory();
310
334
  }, pollIntervalMs);
311
335
  return () => clearInterval(timer);
312
- }, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
336
+ }, [enabled, pageLive, pollIntervalMs, refreshInventory, refreshBlocked, visible.mutating]);
313
337
 
314
338
  useEffect(() => {
315
339
  if (!enabled || !browserSessionId || !pageLive || refreshBlocked) return;
316
340
  let disposed = false;
317
341
  const heartbeat = () => {
318
342
  void client.heartbeatBrowserSession(workspaceId, browserSessionId).catch(() => {
319
- if (!disposed) void refresh();
343
+ if (!disposed) void refreshInventory();
320
344
  });
321
345
  };
322
346
  const timer = setInterval(heartbeat, 30_000);
@@ -324,7 +348,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
324
348
  disposed = true;
325
349
  clearInterval(timer);
326
350
  };
327
- }, [browserSessionId, client, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
351
+ }, [browserSessionId, client, enabled, pageLive, refreshInventory, refreshBlocked, workspaceId]);
328
352
 
329
353
  const runMutation = useCallback(
330
354
  async <T>(scopeBrowserSessionId: string, operation: () => Promise<T>): Promise<T> => {
@@ -532,6 +556,16 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
532
556
  observationMode,
533
557
  action,
534
558
  });
559
+ if (receipt.state !== "completed") {
560
+ const inputFailure: BrowserInputFailure = {
561
+ operationId: receipt.operationId,
562
+ state: receipt.state,
563
+ error: receipt.error,
564
+ };
565
+ setState((current) =>
566
+ current.browserSessionId === browserSessionId ? { ...current, inputFailure } : current,
567
+ );
568
+ }
535
569
  if (receipt.observation) {
536
570
  observationRef.current = {
537
571
  browserSessionId,
@@ -643,6 +677,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
643
677
  loading: visible.loading,
644
678
  mutating: visible.mutating,
645
679
  error: visible.error,
680
+ inputFailure: visible.inputFailure,
646
681
  refresh,
647
682
  observeForInput,
648
683
  actFromObservation,
@@ -666,6 +701,7 @@ function emptyState(browserSessionId: string | null, loading: boolean) {
666
701
  loading,
667
702
  mutating: false,
668
703
  error: null as Error | null,
704
+ inputFailure: null as BrowserInputFailure | null,
669
705
  };
670
706
  }
671
707
 
@@ -5,7 +5,7 @@ import {
5
5
  type EffectiveControlResumeOption,
6
6
  type EffectiveSessionControl,
7
7
  type LatencyMode,
8
- type OpenGeniApiError,
8
+ OpenGeniApiError,
9
9
  type ReasoningEffort,
10
10
  type ResourceRef,
11
11
  type SaveComposerDraftRequest,
@@ -15,7 +15,15 @@ import {
15
15
  } from "@opengeni/sdk";
16
16
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
17
17
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
18
- import { composerSubmissionCanRetry, composerSubmissionErrorMessage } from "../lib/format";
18
+ import {
19
+ COMPOSER_MODEL_UNAVAILABLE_MESSAGE,
20
+ ComposerReconciliationRequiredError,
21
+ ComposerStateError,
22
+ ComposerWorkspaceControlUnavailableError,
23
+ composerSubmissionCanRetry,
24
+ composerSubmissionErrorMessage,
25
+ } from "../lib/format";
26
+ import { normalizeError, useErrorMessage } from "../lib/error-message";
19
27
  import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
20
28
 
21
29
  export type ComposerPolicy = {
@@ -98,6 +106,8 @@ export type ComposerOptimisticMessage = {
98
106
  triggerEventId?: string | null | undefined;
99
107
  appliedQueueVersion?: number | null | undefined;
100
108
  error?: string | undefined;
109
+ /** Bounded support reference retained independently of display copy across remounts. */
110
+ correlationId?: string | undefined;
101
111
  /** False for a definitive refusal that requires changing payment or allowance settings. */
102
112
  retryable?: boolean | undefined;
103
113
  outcomeUnknown?: boolean | undefined;
@@ -185,12 +195,19 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
185
195
  operation.state === "sending" || operation.state === "queued"
186
196
  ? true
187
197
  : operation.outcomeUnknown,
188
- error:
189
- operation.state === "sending"
190
- ? "Delivery was interrupted; retry to reconcile this message."
191
- : operation.error
192
- ? composerSubmissionErrorMessage(new Error(operation.error))
193
- : undefined,
198
+ correlationId: restoredCorrelationId(operation.correlationId),
199
+ error: restoredErrorReference(
200
+ operation.correlationId,
201
+ hasMcpCredentialUpdates && (operation.state === "sending" || operation.outcomeUnknown)
202
+ ? composerSubmissionErrorMessage(new ComposerReconciliationRequiredError())
203
+ : operation.state === "sending"
204
+ ? "Delivery was interrupted; retry to reconcile this message."
205
+ : operation.outcomeUnknown
206
+ ? "The request could not be confirmed. Check its status before retrying."
207
+ : operation.error
208
+ ? composerSubmissionErrorMessage(new Error(operation.error))
209
+ : undefined,
210
+ ),
194
211
  retryable:
195
212
  operation.state === "sending" || operation.outcomeUnknown
196
213
  ? true
@@ -201,6 +218,20 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
201
218
  }));
202
219
  }
203
220
 
221
+ function restoredCorrelationId(value: unknown): string | undefined {
222
+ return typeof value === "string" && /^[\w.:-]{1,128}$/u.test(value) ? value : undefined;
223
+ }
224
+
225
+ function restoredErrorReference(
226
+ reference: unknown,
227
+ message: string | undefined,
228
+ ): string | undefined {
229
+ const correlationId = restoredCorrelationId(reference);
230
+ return message && correlationId && message !== COMPOSER_MODEL_UNAVAILABLE_MESSAGE
231
+ ? `${message} Reference: ${correlationId}.`
232
+ : message;
233
+ }
234
+
204
235
  function rememberOptimisticSendOperations(
205
236
  key: string | null,
206
237
  operations: OptimisticSendOperation[],
@@ -651,6 +682,7 @@ export function useComposer(
651
682
  options: UseComposerOptions = {},
652
683
  ): ComposerControllerState {
653
684
  const { client, workspaceId, registerSessionReconciler } = useEmbeddedSession(options);
685
+ const formatError = useErrorMessage();
654
686
  const durableDrafts = options.draftPersistence !== "disabled";
655
687
  if (!durableDrafts && !options.initialPolicy) {
656
688
  throw new Error("useComposer requires initialPolicy when draft persistence is disabled");
@@ -1071,7 +1103,9 @@ export function useComposer(
1071
1103
  const problem = asError(cause);
1072
1104
  const timedOut = problem.name === "TimeoutError";
1073
1105
  setDraftReadError(
1074
- timedOut ? new Error("Draft sync timed out. Retrying…", { cause }) : problem,
1106
+ timedOut
1107
+ ? new ComposerStateError("Draft sync timed out. Retrying…", { cause })
1108
+ : problem,
1075
1109
  );
1076
1110
  if (
1077
1111
  timedOut ||
@@ -1378,22 +1412,34 @@ export function useComposer(
1378
1412
  optimisticProcessorBusyRef.current = true;
1379
1413
  void (async () => {
1380
1414
  let mutationFailureObserved = false;
1381
- const markFailed = (problem: Error, outcomeUnknown: boolean): void => {
1415
+ const markFailed = (cause: unknown, outcomeUnknown: boolean): void => {
1382
1416
  if (
1383
1417
  targetKeyRef.current !== ownedTargetKey ||
1384
1418
  targetGeneration.current !== ownedGeneration
1385
1419
  ) {
1386
1420
  return;
1387
1421
  }
1422
+ const problem = asError(cause);
1388
1423
  replaceOptimisticSends((current) =>
1389
1424
  current.map((candidate) =>
1390
1425
  candidate.clientEventId === operation.clientEventId
1391
1426
  ? {
1392
1427
  ...candidate,
1393
1428
  state: "failed",
1394
- error: outcomeUnknown ? problem.message : composerSubmissionErrorMessage(problem),
1429
+ error: formatError(
1430
+ cause,
1431
+ outcomeUnknown &&
1432
+ !isOutcomeUnknownError(problem) &&
1433
+ !(problem instanceof ComposerReconciliationRequiredError)
1434
+ ? `The request could not be confirmed. Check its status before retrying.${problem instanceof OpenGeniApiError && problem.correlationId ? ` Reference: ${problem.correlationId}.` : ""}`
1435
+ : composerSubmissionErrorMessage(problem),
1436
+ ),
1395
1437
  retryable: outcomeUnknown || composerSubmissionCanRetry(problem),
1396
1438
  outcomeUnknown,
1439
+ correlationId:
1440
+ problem instanceof OpenGeniApiError
1441
+ ? problem.correlationId
1442
+ : candidate.correlationId,
1397
1443
  }
1398
1444
  : candidate,
1399
1445
  ),
@@ -1420,7 +1466,7 @@ export function useComposer(
1420
1466
  // This read cannot settle the prior mutation in either direction.
1421
1467
  // Preserve its exact key and uncertain status until reconciliation
1422
1468
  // succeeds or that same mutation is actually replayed.
1423
- markFailed(asError(cause), true);
1469
+ markFailed(cause, true);
1424
1470
  return;
1425
1471
  }
1426
1472
  const reconciled = reconcileOptimisticSendFromEvents(operation, events);
@@ -1444,15 +1490,13 @@ export function useComposer(
1444
1490
  return;
1445
1491
  }
1446
1492
  if (!operation.canRetry) {
1447
- throw new Error(
1448
- "Opengeni cannot safely retry this uncertain request after remount; reconcile the session before sending again.",
1449
- );
1493
+ throw new ComposerReconciliationRequiredError();
1450
1494
  }
1451
1495
  }
1452
1496
  let expectedDraftRevision: number | undefined;
1453
1497
  if (durableDrafts && operation.draftPayload) {
1454
1498
  if (!(await persistPayload(operation.draftPayload))) {
1455
- throw new Error("The message draft could not be saved before delivery.");
1499
+ throw new ComposerStateError("The message draft could not be saved before delivery.");
1456
1500
  }
1457
1501
  expectedDraftRevision = draftRef.current?.revision;
1458
1502
  }
@@ -1470,7 +1514,7 @@ export function useComposer(
1470
1514
  let acceptedEvent: SessionEvent | null = null;
1471
1515
  if (durableDrafts) {
1472
1516
  if (!operation.draftPayload || expectedDraftRevision === undefined) {
1473
- throw new Error("The durable composer draft is not ready for delivery.");
1517
+ throw new ComposerStateError("The durable composer draft is not ready for delivery.");
1474
1518
  }
1475
1519
  const result = await client
1476
1520
  .submitComposerDraft(workspaceId, sessionId, {
@@ -1568,7 +1612,7 @@ export function useComposer(
1568
1612
  operation.outcomeUnknown && !mutationFailureObserved
1569
1613
  ? true
1570
1614
  : isOutcomeUnknownError(cause);
1571
- markFailed(problem, outcomeUnknown);
1615
+ markFailed(cause, outcomeUnknown);
1572
1616
  } finally {
1573
1617
  optimisticProcessorBusyRef.current = false;
1574
1618
  if (targetKeyRef.current === ownedTargetKey) {
@@ -1580,6 +1624,7 @@ export function useComposer(
1580
1624
  adoptDraftBase,
1581
1625
  client,
1582
1626
  durableDrafts,
1627
+ formatError,
1583
1628
  markOptimisticAccepted,
1584
1629
  loadDraft,
1585
1630
  onSent,
@@ -1748,7 +1793,7 @@ export function useComposer(
1748
1793
  input.reasoningEffort === undefined ||
1749
1794
  input.latencyMode === undefined
1750
1795
  ) {
1751
- throw new Error("The durable composer draft is not ready for delivery.");
1796
+ throw new ComposerStateError("The durable composer draft is not ready for delivery.");
1752
1797
  }
1753
1798
  const result = await client.submitComposerDraft(workspaceId, sessionId, {
1754
1799
  text: input.text,
@@ -1853,11 +1898,7 @@ export function useComposer(
1853
1898
  return true;
1854
1899
  }
1855
1900
  if (!pending.canRetry) {
1856
- setError(
1857
- new Error(
1858
- "Opengeni cannot safely retry this uncertain request after remount; reconcile the session before sending again.",
1859
- ),
1860
- );
1901
+ setError(new ComposerReconciliationRequiredError());
1861
1902
  return false;
1862
1903
  }
1863
1904
  try {
@@ -2252,7 +2293,7 @@ export function useComposer(
2252
2293
  targetKeyRef.current === ownedTargetKey &&
2253
2294
  targetGeneration.current === ownedGeneration
2254
2295
  ) {
2255
- setError(cause instanceof Error ? cause : new Error(String(cause)));
2296
+ setError(normalizeError(cause));
2256
2297
  }
2257
2298
  } finally {
2258
2299
  if (
@@ -2296,7 +2337,7 @@ export function useComposer(
2296
2337
  targetKeyRef.current === ownedTargetKey &&
2297
2338
  targetGeneration.current === ownedGeneration
2298
2339
  ) {
2299
- setError(cause instanceof Error ? cause : new Error(String(cause)));
2340
+ setError(normalizeError(cause));
2300
2341
  }
2301
2342
  } finally {
2302
2343
  if (
@@ -2323,9 +2364,7 @@ export function useComposer(
2323
2364
  (blocker) => blocker.kind === "workspace",
2324
2365
  );
2325
2366
  if (!client.setWorkspaceInferenceState) {
2326
- throw new Error(
2327
- "@opengeni/react: workspace-scoped resume requires setWorkspaceInferenceState.",
2328
- );
2367
+ throw new ComposerWorkspaceControlUnavailableError();
2329
2368
  }
2330
2369
  const clientEventId = generateClientEventId();
2331
2370
  await replayOutcomeUnknown(() =>
@@ -2455,7 +2494,7 @@ export function useComposer(
2455
2494
  const addAnnotation = useCallback(
2456
2495
  (annotation: DraftTimelineAnnotation) => {
2457
2496
  if (annotationsRef.current.length >= 12) {
2458
- setError(new Error("A message can include at most 12 timeline annotations."));
2497
+ setError(new ComposerStateError("A message can include at most 12 timeline annotations."));
2459
2498
  return;
2460
2499
  }
2461
2500
  const next = [...annotationsRef.current, annotation];
@@ -2852,7 +2891,7 @@ async function replayOutcomeUnknown<T>(command: () => Promise<T>): Promise<T> {
2852
2891
  }
2853
2892
 
2854
2893
  function asError(cause: unknown): Error {
2855
- return cause instanceof Error ? cause : new Error(String(cause));
2894
+ return normalizeError(cause);
2856
2895
  }
2857
2896
 
2858
2897
  function promptRoutingFromAcceptedEvent(